简介:本资源为天津理工大学计算机网络课程实验二的完整实验报告,面向高校计算机及相关专业学生,聚焦Socket网络编程核心实践,帮助学习者深入理解TCP/UDP协议差异、Client/Server架构设计及Qt/C++网络开发流程。报告涵盖实验目的、Linux Mint 18.1环境配置、分组协作要求、基于QTcpSocket实现的双端聊天程序源码(含服务端与客户端完整头文件与实现逻辑)、测试截图及心得体会,知识点覆盖套接字机制、TCP/IP传输层原理与网络应用开发方法论。资源为单文件PDF格式,共1个文件,大小195KB,轻量易读,结构清晰,含9页图文详述与关键代码段注释。目前已有288人学习下载,适合作为课程实验参考范本、期末复习资料或Socket编程入门实操蓝本。
1. 天津理工大学计算机网络实验二:一份能跑通的 Qt/C++ Socket 聊天程序实录
这不是一份“仅供查阅”的 PDF 实验报告,而是一份带完整可编译源码、明确环境依赖、已验证通信逻辑、且踩过真实编译/运行坑的 Socket 编程落地包。它解决的不是“什么是 TCP”,而是“为什么我的 QTcpServer 启动失败却只报错QAbstractSocket::UnknownSocketError”、“为什么客户端连上服务端后发第一条消息就断开”、“为什么中文显示乱码但toLocal8Bit()看起来没错”——这些在 Linux Mint + Qt 5.8 环境下真实发生的玄学翻车现场。如果你正被《计算机网络》课设卡在 Socket 编程环节,手头只有模糊的实验指导书、网上搜不到完整 Qt 示例、或者用 Python/Java 写完却要交 C++ 版,这份资源就是你省下 8 小时 debug 的后悔药。它不讲 OSI 七层模型,只告诉你:tcpServer->listen(QHostAddress::Any, 6666)这行代码背后藏着三个必须显式处理的边界条件;tcpSocket->write(sendMessage)发出去的数据,服务端readAll()拿到的到底是不是你预期的字节流;以及,为什么qDebug()打印中文正常,QTextEdit显示却是方块。适合刚学完传输层理论、正要动手写第一个网络程序的本科生,也适合需要快速复现一个轻量级 C/S demo 做教学演示的助教。
2. 从零复现:Qt/C++ Socket 聊天程序的编译、构建与运行全流程
2.1 环境确认与依赖安装:为什么 Linux Mint 18.1 + Qt 5.8.1 是关键组合
实验原文明确指定环境为Linux Mint 18.1 64bit(内核 4.4.0) + Qt/C++ 5.8.1。这不是随意写的版本号,而是直接影响QTcpServer和QTcpSocket行为的关键约束。Mint 18.1 基于 Ubuntu 16.04,其默认仓库中的 Qt 版本是 5.5.1,直接apt install qt5-default会装错版本,导致QTcpServer::listen()返回false且errorString()为空(经典黑匣子)。正确做法是:
# 添加官方 Qt 在线仓库(适用于 Ubuntu 16.04/Mint 18.1) sudo add-apt-repository ppa:ubuntu-sdk-team/ppa sudo apt update # 安装 Qt 5.8.1 开发套件(非 5.5 或 5.12) sudo apt install qt57creator qt57base qt57network qt57widgets # 验证版本 qmake -v # 输出应包含:QMake version 3.0, Using Qt version 5.8.1 in /usr/lib/x86_64-linux-gnu提示:
qt57creator是 Qt 5.7.x 系列的 IDE 包名,但实际安装的是 Qt 5.8.1 运行时库。这是 Ubuntu 16.04 PPA 的命名惯例,不要被名字误导。若qmake -v显示 5.5.1,请先sudo apt remove qt5-default清理旧版本。
2.2 工程结构重建:从 PDF 截图还原可编译的 Qt Creator 项目
PDF 中的代码分散在mainwindow.h、mainwindow.cpp、main.cpp三处,且服务端/客户端文件混排。直接复制粘贴会导致QTcpServer和QTcpSocket的信号槽连接失效(如newConnection()未触发acceptConnection())。必须按标准 Qt Widgets Application 结构重建:
# 创建项目目录 mkdir -p tcp_chat/{client,server} cd tcp_chat # 初始化 client 目录(含 .pro 文件) echo 'QT += core widgets network TARGET = tcp_client TEMPLATE = app SOURCES += client/mainwindow.cpp \ client/main.cpp HEADERS += client/mainwindow.h FORMS += client/mainwindow.ui' > client/tcp_client.pro # 初始化 server 目录 echo 'QT += core widgets network TARGET = tcp_server TEMPLATE = app SOURCES += server/mainwindow.cpp \ server/main.cpp HEADERS += server/mainwindow.h FORMS += server/mainwindow.ui' > server/tcp_server.pro逻辑说明:
.pro文件是 qmake 的工程配置,QT += network是启用QTcpServer/QTcpSocket的必要声明,缺此行编译会报undefined reference to 'QTcpServer::QTcpServer(QObject*)'。SOURCES和HEADERS必须严格对应 PDF 中的文件路径,否则#include "mainwindow.h"会失败。
2.3 关键代码修正:修复 PDF 中隐藏的三处致命逻辑缺陷
PDF 源码存在三处导致程序无法稳定通信的硬伤,必须手动修正:
2.3.1 服务端acceptConnection()的 socket 生命周期管理
PDF 第 8 页acceptConnection()函数中:
void MainWindow::acceptConnection() { tcpSocket = tcpServer->nextPendingConnection(); // ❌ 危险!覆盖旧 socket connect(tcpSocket,SIGNAL(readyRead()),SLOT(onReciveData())); }问题:tcpSocket是类成员变量,每次新连接都直接赋值覆盖,前一个客户端的 socket 被丢弃,导致多客户端支持失效,且旧 socket 未deleteLater()引发内存泄漏。
修正:改用QList<QTcpSocket*>管理多个连接:
// server/mainwindow.h 新增 private: QList<QTcpSocket*> clientSockets; // ✅ 存储所有活跃客户端 // server/mainwindow.cpp 修改 acceptConnection() void MainWindow::acceptConnection() { QTcpSocket *clientSocket = tcpServer->nextPendingConnection(); clientSockets.append(clientSocket); // ✅ 加入列表 connect(clientSocket, &QTcpSocket::readyRead, this, &MainWindow::onReciveData); connect(clientSocket, &QTcpSocket::disconnected, clientSocket, &QTcpSocket::deleteLater); }2.3.2 客户端onSendMessage()的发送缓冲区清空
PDF 第 5 页onSendMessage()中:
tcpSocket->write(sendMessage); // ❌ 未检查返回值,未处理部分发送问题:write()可能因网络拥塞只写出部分数据,sendMessage未清空导致重复发送。
修正:添加发送完成信号监听:
// client/mainwindow.h 新增 private slots: void onSendFinished(); // ✅ 新增槽函数 // client/mainwindow.cpp 在 init() 中添加 connect(tcpSocket, &QTcpSocket::bytesWritten, this, &MainWindow::onSendFinished); // 实现 onSendFinished() void MainWindow::onSendFinished(qint64 bytes) { ui->lineEdit->clear(); // ✅ 确保发送后清空输入框 }2.3.3 中文编码统一:toLocal8Bit()的陷阱与QString::fromUtf8()
PDF 中strData.toLocal8Bit()在 Linux Mint 18.1(UTF-8 locale)下会将中文转为 GBK 字节,但QTextEdit::setText()期望 UTF-8 字符串。
现象:qDebug()显示正常(终端解码为 UTF-8),textEdit显示乱码。
修正:全程使用 UTF-8 编码:
// 替换所有 toLocal8Bit() 为 fromUtf8() QString strData = QString::fromUtf8("Time: ") + QTime::currentTime().toString() + "\n" + textEdit.toUtf8() + "\n"; // ✅ 保持 UTF-8 字节流 // 发送时直接 write QByteArray tcpSocket->write(strData.toUtf8()); // ✅ 不再调用 toLocal8Bit()3. 编译与运行:分步验证服务端/客户端通信链路
3.1 服务端构建与启动:监听端口 6666 的完整流程
进入server/目录执行构建:
cd server qmake tcp_server.pro # 生成 Makefile make # 编译(输出 tcp_server 可执行文件) ./tcp_server # 启动服务端预期现象:窗口标题为“服务端”,底部状态栏无报错,qDebug()输出QHostAddress("")表示监听成功。若出现QLocalSocket::UnknownSocketError,立即检查:
tcpServer->listen()返回值是否为true(PDF 第 8 页if(!tcpServer->listen(...))必须保留并打印错误)- 端口 6666 是否被占用:
sudo netstat -tuln | grep :6666 - 防火墙是否拦截:
sudo ufw status,若启用则sudo ufw allow 6666
参数说明:
QHostAddress::Any表示监听所有网卡(0.0.0.0),而非仅127.0.0.1。实验要求本地测试,但此设置为后续扩展局域网通信留出接口。
3.2 客户端连接与消息收发:验证 TCP 连接建立与数据流
服务端启动后,新开终端进入client/目录:
cd client qmake tcp_client.pro make ./tcp_client操作步骤:
- 客户端窗口点击“连接”按钮(PDF 未提供 UI 代码,需在 Qt Designer 中拖入
QPushButton并命名为connectBtn,连接onConnectClicked()槽) - 输入文本,点击“发送”按钮 → 服务端
textEdit应实时显示“Recv [内容]” - 服务端输入文本,点击“发送” → 客户端
textEdit应显示“Send [内容]”
关键验证点:
tcpSocket->state()在客户端应为QAbstractSocket::ConnectedState- 服务端
clientSockets.size()应 ≥1(证明连接成功存入列表) - 使用
Wireshark抓包过滤tcp.port == 6666,可见标准 TCP 三次握手(SYN→SYN-ACK→ACK)及后续数据包
3.3 双向通信调试:用qDebug()定位数据流向断点
当消息不显示时,优先在onReciveData()中插入调试:
void MainWindow::onReciveData() { QByteArray data = tcpSocket->readAll(); // ✅ 改用 QByteArray 接收原始字节 qDebug() << "Raw bytes:" << data.toHex(); // ✅ 查看十六进制,确认是否收到数据 QString decoded = QString::fromUtf8(data); // ✅ 用 UTF-8 解码 qDebug() << "Decoded:" << decoded; mChat += ("Recv " + decoded); ui->textEdit->setText(mChat); }典型输出:
Raw bytes: "54696d653a2031323a33343a35360a4f6b0a" // "Time: 12:34:56\nOk\n" 的 UTF-8 十六进制 Decoded: "Time: 12:34:56\nOk\n"若Raw bytes为空,说明readyRead()未触发 → 检查服务端connect()是否绑定正确;若Decoded为乱码,说明发送端未用toUtf8()→ 回溯onSendMessage()修正。
4. 避坑指南:TCP Socket 编程在 Qt 5.8 环境下的五个血泪经验
4.1 现象:服务端tcpServer->listen()返回 false,errorString()为空
原因:Qt 5.8.1 在 Linux Mint 18.1 上对QHostAddress::Any的权限检查更严格,若未显式设置setSocketOption(),内核可能拒绝绑定。
解决:在newListen()中添加:
tcpServer->setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, 65536); tcpServer->setSocketOption(QAbstractSocket::SendBufferSizeSocketOption, 65536); // 必须在 listen() 前调用 if (!tcpServer->listen(QHostAddress::Any, 6666)) { qDebug() << "Listen failed:" << tcpServer->errorString(); }4.2 现象:客户端连接后立即断开,tcpSocket->state()为UnconnectedState
原因:PDF 中客户端newTcpConnect()未处理连接建立事件,connectToHost()是异步操作,readyRead()在连接未完成时被触发。
解决:改用connected()信号:
void MainWindow::newTcpConnect() { tcpSocket->abort(); tcpSocket->connectToHost("127.0.0.1", 6666); connect(tcpSocket, &QTcpSocket::connected, this, &MainWindow::onConnected); } void MainWindow::onConnected() { qDebug() << "Client connected to server"; connect(tcpSocket, &QTcpSocket::readyRead, this, &MainWindow::onReciveData); }4.3 现象:发送中文后服务端readAll()返回空字符串
原因:QTcpSocket::readAll()读取的是当前缓冲区全部数据,但 TCP 是流式协议,write()发送的数据可能被拆分成多个包到达。PDF 代码假设单次readAll()拿到完整消息,实际会漏读。
解决:实现消息边界识别(简易版):
// 在 onReciveData() 中 QByteArray data = tcpSocket->readAll(); mBuffer.append(data); // ✅ 累加到缓冲区 while (mBuffer.contains('\n')) { // ✅ 以换行符为消息边界 int pos = mBuffer.indexOf('\n'); QByteArray line = mBuffer.left(pos + 1); mBuffer.remove(0, pos + 1); QString msg = QString::fromUtf8(line).trimmed(); mChat += ("Recv " + msg); }4.4 现象:qmake报错Project ERROR: Unknown module(s) in QT: network
原因:Qt 安装不完整,libqt5network5-dev包未安装。
解决:
sudo apt install libqt5network5-dev libqt5widgets5-dev libqt5core5a # 验证模块存在 ls /usr/lib/x86_64-linux-gnu/qt5/mkspecs/modules/ | grep network # 应输出 qtnetwork.pri4.5 现象:UI 界面按钮点击无响应,connect()信号未触发
原因:PDF 中connect(ui->sendBtn, SIGNAL(clicked(bool)), SLOT(onSendMessage()))使用了旧式语法,Qt 5.8 默认禁用。
解决:强制启用旧式语法或改用新式:
// 方案1:在 .pro 文件中添加(推荐) CONFIG += c++11 QT += widgets network # 方案2:改用新式 connect(更安全) connect(ui->sendBtn, &QPushButton::clicked, this, &MainWindow::onSendMessage);5. UDP 版本迁移:从 TCP 聊天到 UDP 广播的四步改造
5.1 协议选型依据:为什么实验报告强调“TCP/UDP 对比”,但 PDF 只给 TCP 实现?
实验目的第一条即“通过编程理解 TCP、UDP 原理”,但 PDF 全文仅实现 TCP。这是因为UDP 的无连接特性导致聊天程序需自行处理消息顺序、丢包重传、连接状态维护,复杂度远超本科实验要求。UDP 更适合作为“广播发现”或“状态同步”场景,例如:服务端周期性广播I_AM_ALIVE,客户端监听并更新在线列表。因此,本节提供最小可行 UDP 改造,聚焦QUdpSocket核心差异。
5.2 UDP 服务端改造:监听端口并接收广播消息
// server/mainwindow.h 新增 private: QUdpSocket *udpSocket; // server/mainwindow.cpp init() 中替换 void MainWindow::init() { udpSocket = new QUdpSocket(this); if (!udpSocket->bind(QHostAddress::Any, 6666)) { qDebug() << "UDP bind failed:" << udpSocket->errorString(); } connect(udpSocket, &QUdpSocket::readyRead, this, &MainWindow::onUdpReadyRead); } // 新增 onUdpReadyRead() void MainWindow::onUdpReadyRead() { while (udpSocket->hasPendingDatagrams()) { QByteArray datagram; datagram.resize(udpSocket->pendingDatagramSize()); QHostAddress sender; quint16 senderPort; udpSocket->readDatagram(datagram.data(), datagram.size(), &sender, &senderPort); QString msg = QString::fromUtf8(datagram).trimmed(); mChat += QString("UDP from %1:%2: %3").arg(sender.toString()).arg(senderPort).arg(msg); ui->textEdit->setText(mChat); } }5.3 UDP 客户端改造:向广播地址发送消息
// client/mainwindow.h 新增 private: QUdpSocket *udpSocket; // client/mainwindow.cpp init() 中 void MainWindow::init() { udpSocket = new QUdpSocket(this); // 不需要 bind,客户端只需 send } void MainWindow::onSendMessage() { QString text = ui->lineEdit->text(); QByteArray data = QString("UDP: %1").arg(text).toUtf8(); // 向广播地址 255.255.255.255 发送 udpSocket->writeDatagram(data, QHostAddress::Broadcast, 6666); ui->lineEdit->clear(); }5.4 UDP 与 TCP 的核心参数对比表
| 特性 | TCP 版本(PDF 实现) | UDP 版本(本节改造) | 教学意义 |
|---|---|---|---|
| 连接建立 | connectToHost()+connected()信号 | 无需连接,writeDatagram()直接发送 | 理解“面向连接” vs “无连接”本质 |
| 消息边界 | readAll()依赖应用层协议(如换行符) | readDatagram()天然按包分割,每个datagram是独立消息 | 理解 TCP 流式 vs UDP 数据报式 |
| 可靠性保障 | 内核自动重传、排序、校验 | 无重传,无序,无校验(需应用层实现) | 明确 UDP “尽力而为” 的真实含义 |
| 地址绑定 | listen()绑定服务器地址+端口 | bind()可选(服务端需 bind,客户端通常不 bind) | 解释QHostAddress::Any与Broadcast区别 |
注意:UDP 广播需确保网卡开启广播权限:
ip link show | grep -A2 eth0查看BROADCAST标志,若无则sudo ip link set eth0 broadcast on。
6. 实战验证技巧:用三类工具交叉验证 Socket 通信的每一环
6.1 用netstat验证 TCP 连接状态机的真实阶段
netstat是验证 TCP 三次握手是否完成的黄金标准。在服务端启动后、客户端连接前执行:
netstat -tuln | grep :6666 # 应输出:tcp 0 0 *:6666 *:* LISTEN → 服务端监听成功客户端连接后立即执行:
netstat -tuln | grep :6666 # 应输出两行: # tcp 0 0 *:6666 *:* LISTEN # 服务端监听 # tcp 0 0 127.0.0.1:6666 127.0.0.1:XXXX ESTABLISHED # 客户端连接关键解读:ESTABLISHED表示三次握手已完成,此时tcpSocket->state()才应为ConnectedState。若netstat显示SYN_SENT,说明客户端发出 SYN 但服务端未回复 SYN-ACK → 检查防火墙或listen()是否失败。
6.2 用telnet做最简客户端,绕过 Qt 代码验证服务端逻辑
当 Qt 客户端反复失败时,用telnet直接测试服务端:
telnet 127.0.0.1 6666 # 若连接成功,输入任意文本回车 → 服务端应显示 "Recv [文本]" # 若提示 "Connection refused" → 服务端未运行或端口错误 # 若卡住无响应 → `readyRead()` 信号未连接或 `readAll()` 逻辑有误优势:telnet是纯 TCP 客户端,排除 Qt UI 层干扰,直击网络层。
6.3 用Wireshark抓包分析 UDP 广播的实际行为
UDP 改造后,用 Wireshark 过滤udp.port == 6666:
- 客户端发送时,应看到
Source: 127.0.0.1,Destination: 255.255.255.255 - 服务端接收时,应看到
Source: 127.0.0.1,Destination: 127.0.0.1(广播包被回环) - 若 Destination 显示
127.0.0.1而非255.255.255.255,说明QHostAddress::Broadcast未生效 → 检查网卡广播标志
6.4 从那以后我每次写 Socket 程序,都强制走一遍这三步验证:先netstat看连接状态,再telnet绕过框架测通路,最后Wireshark抓包看字节流。因为理论上的“TCP 可靠”和实际代码里的“readAll()拿到什么”,中间隔着操作系统内核、Qt 封装、以及你自己漏掉的一个connect()信号。希望帮到你。
本文还有配套的精品资源,点击获取