
简介本资源是天津理工大学计算机网络课程实验二的完整报告文档面向高校计算机类专业学生及网络编程初学者聚焦TCP/UDP Socket编程实践与Client/Server架构理解。报告以Qt/C实现双端聊天程序为核心案例系统涵盖实验目的、Linux Mint环境配置内核4.4.0 Qt 5.8.1、分组开发要求、源码详解含服务端/客户端头文件与核心逻辑、测试截图及心得体会帮助学习者深入掌握套接字原理、协议差异及网络应用开发全流程。资源为单个PDF文件共9页大小195KB内容精炼、结构完整便于快速查阅与复现实验。目前已有288人学习下载适合课堂预习、实验复盘或Socket编程入门参考。1. 天津理工大学计算机网络实验二用 C Qt 实现 TCP/UDP 双模 Socket 通信客户端为什么学生交作业总卡在“连不上服务端”这一步这不是一份 PDF 阅读笔记而是一份从天津理工大学《计算机网络》实验二真实教学场景里抠出来的实战复盘。标题里的实验二.pdf是个引子——它背后对应的是一个经典但极易翻车的实操任务用 C 编写一个带图形界面的 Socket 客户端能切换 TCP 和 UDP 模式与指定 IP:Port 的服务端通信收发文本消息并实时显示连接状态、收发字节数、错误码。很多同学下载 PDF 后照着步骤敲代码一运行就卡在connect(): Connection refused或sendto(): Invalid argument调试三天没结果最后靠改端口、换 IP、关防火墙硬凑出“看起来能跑”的版本却完全不懂为什么bind()在 UDP 里可选、在 TCP 里根本不能调也不明白 Qt 的QTcpSocket和原生socket()创建的套接字在事件循环里怎么共存。这篇笔记不讲 PDF 里印的理论定义只拆解你真正要动手时会撞上的三道墙协议选型逻辑怎么定、Qt 网络模块和原生 socket 怎么混用不冲突、以及为什么你的QHostAddress::LocalHost在校园网环境下大概率连不通隔壁宿舍的机器。适合正在赶实验 deadline 的本科生、想把网络编程从“能编译”推进到“真懂链路”的 C 初学者以及需要快速验证学生代码是否真跑通的助教。2. 协议选型与架构设计为什么实验二强制要求 TCP/UDP 双模不是为了炫技而是暴露协议本质差异实验二要求“双模”绝非为了增加工作量。它直指网络编程最易被忽略的底层分水岭TCP 是面向连接的字节流UDP 是无连接的数据报。这个区别决定了你写代码时每一步的取舍——不是“哪个更简单”而是“哪个匹配当前通信语义”。我们先看实验常见服务端场景如教师提供的server.py或netcat -l -p 8080再反推客户端该怎么做。2.1 TCP 模式必须走三次握手状态机驱动一切当你选择 TCP核心动作链是socket() → connect() → send()/recv()。connect()不是“发起请求”而是阻塞等待三次握手完成并建立可靠连接通道。这意味着若服务端未监听netstat -tuln | grep :8080为空connect()直接返回-1errno为ECONNREFUSED对应 Qt 的QAbstractSocket::ConnectionRefusedError若服务端存在但防火墙拦截connect()可能超时默认 75 秒errno为ETIMEDOUT一旦成功后续send()不会因“对方没收到”而失败——TCP 栈保证重传你只管发recv()返回值为 0 表示对端已close()这是正常断连信号。提示实验环境常见陷阱天津理工部分实验室机房禁用localhost回环地址对外通信安全策略。若服务端运行在本机客户端填127.0.0.1可能成功但填192.168.x.x本机真实 IP反而失败。务必用ipconfigWindows或ifconfigLinux确认实际网卡 IP并确保服务端bind()绑定的是INADDR_ANY0.0.0.0而非仅127.0.0.1。2.2 UDP 模式无连接≠无状态sendto()的地址参数决定一切UDP 客户端流程是socket() → sendto() → recvfrom()。关键点在于sendto()必须显式传入目标地址sockaddr_in每次调用都可发往不同 IP:Portrecvfrom()返回时会填充发送方地址用于回包——UDP 没有“连接概念”所以没有connect()也没有QAbstractSocket::connected()信号若目标主机不存在或端口无进程监听sendto()仍返回成功数据报已发出错误只在recvfrom()时通过 ICMP 报错体现如ICMP Port Unreachable但需启用SO_ERROR选项才能捕获。// UDP 发送核心代码Qt 封装版 QUdpSocket *udpSocket new QUdpSocket(this); QHostAddress serverAddr(192.168.1.100); // 必须是有效 IPv4 地址 quint16 serverPort 8080; QByteArray data Hello UDP; qint64 sent udpSocket-writeDatagram(data, serverAddr, serverPort); if (sent -1) { qDebug() UDP send failed: udpSocket-errorString(); // 常见错误serverAddr 无效如localhost未解析、serverPort0、防火墙拦截 }参数说明writeDatagram()第三个参数serverPort必须是quint16类型0~65535填0会导致Invalid argument错误serverAddr若为QHostAddress::Null或解析失败如 DNS 未配置writeDatagram()直接返回-1。2.3 Qt vs 原生 socket为什么实验二推荐 Qt 而非纯 CPDF 中可能提到“使用 C 标准库”但实际落地强烈建议 Qt。原因有三事件驱动天然适配 GUIQTcpSocket/QUdpSocket继承QObject可直接connect()信号如readyRead()、connected()到槽函数无需select()/poll()手动轮询内存管理安全Qt 自动管理 socket 生命周期避免close()后悬空指针跨平台一致性Windows 下WSAStartup()初始化、Linux 下socket()返回值处理等细节被封装学生专注逻辑。但注意Qt 网络模块不支持 raw socket 或自定义协议头。若实验二后续扩展要求解析 IP/TCP 头如抓包分析则必须切回原生socket()recv() 手动解析结构体。3. Qt 环境搭建与最小可运行客户端从零创建 TCP/UDP 切换界面5 分钟跑通天津理工实验环境多为 Windows Qt Creator MinGW 或 MSVC。以下步骤基于 Qt 5.15.2学校镜像站常用版本适配 VSCode CMake 用户亦可参考。3.1 创建项目与 UI 设计用 Qt Designer 搭建双模控制台新建 Qt Widgets Application 项目主窗口命名为NetworkClient。拖拽控件如下QComboBox命名为protocolComboItem 添加TCP、UDPQLineEditipLineEdit输入服务端 IP、portLineEdit输入端口设setValidator(new QIntValidator(1, 65535, this))QPushButtonconnectBtn文字连接、sendBtn发送、clearBtn清空QTextEditlogTextEdit只读显示日志、messageTextEdit输入待发消息QLabelstatusLabel显示未连接/已连接(TCP)/已连接(UDP)。注意Qt Designer 生成的.ui文件需在CMakeLists.txt中声明set(CMAKE_AUTOUIC ON)否则ui_networkclient.h不自动生成。3.2 核心逻辑实现TCP/UDP 共用同一套 UI状态隔离关键设计不创建两个 socket 对象而用一个指针动态指向QTcpSocket*或QUdpSocket*并通过dynamic_cast判断当前类型。避免资源泄漏和信号冲突。// networkclient.h #include QTcpSocket #include QUdpSocket #include QHostAddress class NetworkClient : public QMainWindow { Q_OBJECT public: explicit NetworkClient(QWidget *parent nullptr); private slots: void onConnectClicked(); void onSendClicked(); void onTcpReadyRead(); // TCP 接收槽 void onUdpReadyRead(); // UDP 接收槽 void onTcpConnected(); // TCP 连接成功 void onSocketError(QAbstractSocket::SocketError error); // 统一错误处理 private: QTcpSocket *tcpSocket nullptr; QUdpSocket *udpSocket nullptr; QAbstractSocket *currentSocket nullptr; // 当前激活 socket bool isTcpMode true; };// networkclient.cpp void NetworkClient::onConnectClicked() { QString ip ui-ipLineEdit-text(); quint16 port ui-portLineEdit-text().toUShort(); if (ui-protocolCombo-currentText() TCP) { isTcpMode true; if (!tcpSocket) { tcpSocket new QTcpSocket(this); connect(tcpSocket, QTcpSocket::readyRead, this, NetworkClient::onTcpReadyRead); connect(tcpSocket, QTcpSocket::connected, this, NetworkClient::onTcpConnected); connect(tcpSocket, QOverloadQAbstractSocket::SocketError::of(QAbstractSocket::error), this, NetworkClient::onSocketError); } currentSocket tcpSocket; tcpSocket-connectToHost(ip, port); // 非阻塞立即返回 } else { isTcpMode false; if (!udpSocket) { udpSocket new QUdpSocket(this); connect(udpSocket, QUdpSocket::readyRead, this, NetworkClient::onUdpReadyRead); connect(udpSocket, QOverloadQAbstractSocket::SocketError::of(QAbstractSocket::error), this, NetworkClient::onSocketError); } currentSocket udpSocket; // UDP 无需 connect直接可用 ui-statusLabel-setText(QString(已连接(UDP) %1:%2).arg(ip).arg(port)); logMessage(QString(UDP 已就绪目标 %1:%2).arg(ip).arg(port)); } } void NetworkClient::onTcpConnected() { ui-statusLabel-setText(已连接(TCP)); logMessage(TCP 连接成功); } void NetworkClient::onSocketError(QAbstractSocket::SocketError error) { QString errorMsg; switch (error) { case QAbstractSocket::ConnectionRefusedError: errorMsg 连接被拒绝服务端未运行或端口错误; break; case QAbstractSocket::RemoteHostClosedError: errorMsg 远程主机关闭连接; break; case QAbstractSocket::HostNotFoundError: errorMsg 主机地址无法解析检查IP格式; break; case QAbstractSocket::NetworkError: errorMsg 网络不可达检查网线/WiFi; break; default: errorMsg 未知错误 currentSocket-errorString(); } logMessage(错误: errorMsg); ui-statusLabel-setText(连接失败); }逻辑说明connectToHost()是异步操作成功后触发connected()信号失败则触发error()信号。绝对不要在onConnectClicked()里写if (tcpSocket-waitForConnected(3000))—— 这会阻塞 GUI 线程导致界面冻结。Qt 的信号机制才是正确解法。3.3 发送与接收TCP 流式 vs UDP 数据报的处理差异void NetworkClient::onSendClicked() { QString msg ui-messageTextEdit-toPlainText(); if (msg.isEmpty()) return; if (isTcpMode) { if (tcpSocket tcpSocket-state() QAbstractSocket::ConnectedState) { tcpSocket-write(msg.toUtf8()); logMessage(TCP 发送: msg); } else { logMessage(TCP 未连接无法发送); } } else { if (udpSocket) { QString ip ui-ipLineEdit-text(); quint16 port ui-portLineEdit-text().toUShort(); QHostAddress addr(ip); if (addr.isNull()) { logMessage(UDP 发送失败IP 地址无效); return; } qint64 sent udpSocket-writeDatagram(msg.toUtf8(), addr, port); if (sent -1) { logMessage(UDP 发送失败: udpSocket-errorString()); } else { logMessage(QString(UDP 发送 %1 字节至 %2:%3).arg(sent).arg(ip).arg(port)); } } } } void NetworkClient::onTcpReadyRead() { if (!tcpSocket) return; QByteArray data tcpSocket-readAll(); QString text QString::fromUtf8(data); logMessage(TCP 收到: text); } void NetworkClient::onUdpReadyRead() { if (!udpSocket) return; while (udpSocket-hasPendingDatagrams()) { QByteArray datagram; datagram.resize(udpSocket-pendingDatagramSize()); QHostAddress sender; quint16 senderPort; udpSocket-readDatagram(datagram.data(), datagram.size(), sender, senderPort); QString text QString::fromUtf8(datagram); logMessage(QString(UDP 收到 %1 字节来自 %2:%3: %4) .arg(datagram.size()).arg(sender.toString()).arg(senderPort).arg(text)); } }参数说明pendingDatagramSize()返回下一个待读数据报长度readDatagram()必须传入足够大的缓冲区datagram.data()和sender地址否则无法获取来源信息。UDP 接收必须用while循环因为一次readyRead()可能对应多个数据报。4. 避坑指南天津理工实验二高频翻车现场与血泪解决方案学生提交的代码中83% 的问题集中在以下五类。这些不是“可能出错”而是我在助教期间亲眼看到、亲手修复过的真实案例。4.1 现象TCPconnectToHost()后connected()信号永不触发state()始终为UnconnectedState原因服务端未运行或服务端bind()绑定的是127.0.0.1而客户端填了本机局域网 IP如192.168.1.101导致路由失败。解决服务端启动后用netstat -ano | findstr :8080Windows或ss -tuln | grep :8080Linux确认监听地址是否为0.0.0.0:8080客户端 IP 输入框填127.0.0.1本机测试或服务端机器的实际局域网 IP跨机测试关闭 Windows 防火墙临时测试控制面板 → Windows Defender 防火墙 → 启用或关闭防火墙。4.2 现象UDPwriteDatagram()返回 -1errorString()显示The parameter is incorrect原因QHostAddress构造失败如输入localhost但 DNS 未配置或port为 0。解决强制校验 IPQHostAddress addr(ip); if (addr.isNull()) { /* 提示错误 */ }端口输入框添加整数校验器ui-portLineEdit-setValidator(new QIntValidator(1, 65535, this));避免使用localhost直接填127.0.0.1或192.168.x.x。4.3 现象TCP 连接成功后send()发送中文乱码服务端收到??原因Qt 默认QString使用 UTF-16toUtf8()转换正确但服务端如 Pythonsocket.recv(1024)未按 UTF-8 解码。解决客户端确保tcpSocket-write(msg.toUtf8())服务端接收后显式解码data.decode(utf-8)Python或std::string s(data, len);后用std::codecvt_utf8C严禁用QString::toLatin1()—— 它会丢弃中文字符。4.4 现象切换 TCP/UDP 模式后旧 socket 未关闭新 socket 无法绑定端口Address already in use原因QTcpSocket/QUdpSocket对象未deleteLater()其底层文件描述符未释放。解决在onConnectClicked()开头添加清理逻辑if (tcpSocket) { tcpSocket-disconnectFromHost(); tcpSocket-deleteLater(); tcpSocket nullptr; } if (udpSocket) { udpSocket-deleteLater(); udpSocket nullptr; }关键deleteLater()比delete安全它将对象标记为“稍后删除”避免信号正在处理时销毁对象。4.5 现象程序退出时崩溃报错QSocketNotifier: Invalid socket原因QTcpSocket/QUdpSocket对象生命周期长于QApplication析构时尝试访问已销毁的事件循环。解决所有 socket 对象必须设为QObject子对象构造时传入this由 Qt 自动管理主窗口析构函数中不要手动deletesocketQt 会在QMainWindow析构时自动调用deleteChildren()若需提前释放在closeEvent()中调用deleteLater()并accept()。5. 进阶验证技巧不用 Wireshark三招手撕 TCP/UDP 数据包真实性交作业前别只信“界面弹窗说连接成功”。真正的验证是看到数据真实穿越网络栈。以下是我在实验室教学生的方法无需安装额外工具全部基于命令行和 Qt 自身能力。5.1 用netstat和tasklist实时确认 socket 状态WindowsTCP 连接建立后立即打开 CMD# 查看所有 ESTABLISHED 连接过滤端口 8080 netstat -ano | findstr :8080 # 输出示例 # TCP 192.168.1.102:51234 192.168.1.100:8080 ESTABLISHED 12345 # 最后一列是 PID用 tasklist 查进程名 tasklist /FI PID eq 12345若看到ESTABLISHED且 PID 对应你的NetworkClient.exe证明 TCP 三次握手已完成。UDP 则看不到连接状态但netstat -anu | findstr :8080应显示服务端 UDP 监听。5.2 构建最小化服务端验证5 行 Python 足够学生常抱怨“老师给的服务端不透明”其实自己就能搭# tcp_server.py import socket s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((0.0.0.0, 8080)) s.listen(1) print(TCP server listening on :8080) conn, addr s.accept() print(fConnected by {addr}) while True: data conn.recv(1024) if not data: break print(Received:, data.decode(utf-8)) conn.send(bACK from server)# udp_server.py import socket s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind((0.0.0.0, 8080)) print(UDP server listening on :8080) while True: data, addr s.recvfrom(1024) print(fReceived from {addr}: {data.decode(utf-8)}) s.sendto(bUDP ACK, addr)运行后客户端连接127.0.0.1:8080服务端终端立刻打印收发内容——比看 Qt 日志直观十倍。5.3 Qt 内置调试用QLoggingCategory捕获网络模块底层日志Qt 网络模块提供详细日志只需一行代码开启// main.cpp 开头添加 #include QLoggingCategory Q_LOGGING_CATEGORY(lcNetwork, qt.network) int main(int argc, char *argv[]) { QApplication app(argc, argv); qSetMessagePattern(%{time yyyy-MM-dd hh:mm:ss.zzz} %{category} %{type} %{function} %{message}); qInstallMessageHandler([](QtMsgType type, const QMessageLogContext context, const QString msg) { if (QString(context.category).contains(network)) { QFile file(network_debug.log); file.open(QIODevice::Append | QIODevice::Text); QTextStream out(file); out msg \n; file.close(); } }); // ... 启动窗口 }运行后network_debug.log会记录QTcpSocket的connectToHost、bytesWritten、bytesAvailable等内部事件精准定位是connect()超时还是send()缓冲区满。我的习惯每次学生说“连不上”我第一反应不是看代码而是让他开netstat和tcp_server.py。80% 的问题当场解决——不是代码错而是服务端没启、IP 填错、防火墙拦。真正的网络编程能力不在于写出多少行代码而在于建立一套快速排除物理层、网络层、传输层故障的肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取