资讯详情

资讯详情

基于UDP协议与Winsock的C语言命令行聊天程序实现解析

最近整理旧硬盘时翻出一个早年写的课程设计题目正好是“用UDP协议在dos命令行里模拟一个聊天程序”。看着满屏的printf和recvfrom当年在宿舍蹲到半夜调通第一个UDP包时那种兴奋感又回来了。这个项目在网上被问了很多次也是很多高校网络编程课的老面孔但真正能一口气跑通的人并不多。今天把整个设计思路、完整代码、编译步骤还有我踩过的坑一次性聊透想交作业的、想搞懂UDP实际用法的、或者纯粹想写个命令行小玩具的都能在这篇里找到能直接抄的答案。先说清楚一点这里的“dos命令行”在实际操作中绝大多数是指 Windows 的命令提示符窗口cmd或 Windows Terminal原因是纯 DOS 环境下根本没有现代网络协议栈想跑 socket 还得挂 packet driver折腾成本极高不划算。所以我们聊的是“长得像 dos、黑底白字的命令行环境”本质上是一个 Windows 控制台程序。我会用 C 语言加 Winsock 来实现编译出来就是一个.exe双击打开、传参运行两个黑色窗口就能互相发消息复古感拉满。1. 项目概述与核心思路拆解1.1 这个项目到底在做什么需求一句话在命令行界面里用 UDP 协议实现两个或多个终端之间的文字聊天。打开两个 cmd 窗口分别运行程序一个绑定端口监听接收消息另一个指定对方的 IP 和端口发送消息双方就能像 QQ 一样收发文字只不过界面不是图形对话框而是黑底白字的滚动文本。这里有个容易混淆的点。标题里的“dos”指的是 Disk Operating System 这个老操作系统环境但现在大家听到“dos”第一反应往往是“拒绝服务攻击”。为了不产生歧义我在正文里统一用“命令行窗口”来指代运行环境。安全上也要多说一句UDP 协议本身是网络通信的基础工具这篇文章只讲聊天程序的实现不涉及任何攻击性内容信号不好时该丢的包照样丢我们老老实实用它该用的地方。为什么专门用 UDP 而不是 TCP这是很多人问的第一个问题。核心原因是 UDP 无连接的特性特别适合演示“最小可用聊天系统”所需要的几个要素创建 socket、绑定端口、发送数据报、接收数据报。少了三次握手、状态维护、拥塞控制这些概念代码量大幅减少初学者能更快看到运行结果建立起对网络编程的整体直觉。下面用一个表格快速对比 TCP 和 UDP 在聊天场景下的差异方便理解选型理由对比维度TCPUDP连接状态需三次握手建立连接无连接直接发包可靠性可靠丢包重传、有序到达不可靠可能丢包、乱序传输效率有握手和确认开销相对慢无确认机制开销小、延迟低编程复杂度需处理 accept、read、write 大量状态只需 sendto、recvfrom适合场景文件传输、网页、需要完整性的数据音视频流、实时游戏、局域网工具聊天程序用 TCP 其实更能保证消息不丢但作为教学演示和轻量工具UDP 的“短平快”优势非常明显而且这恰好能带出网络编程里一个重要的思想不靠谱的协议也能做出靠谱的应用只要你在应用层做好补偿比如消息编号、发送确认、超时重发这些都是后话后面我会在扩展部分展开。1.2 技术选型为什么用 C 语言加 Winsock实现一个命令行聊天程序可选的语言和方案很多Python 的 socket 模块几行就能跑Java 的 DatagramSocket 也很方便C# 的 UdpClient 封装得更是舒服。但既然标题点明了“dos 命令行”C 语言加 Winsock 才是最贴近原始气质的组合。原因有三。第一Winsock 是 Windows 系统最底层的网络接口直接操作 socket 句柄、sockaddr_in 结构体、sendto/recvfrom 函数能清楚看到一条 UDP 报文从应用层到内核协议栈再到网卡驱动要经过哪些环节这种“透明感”是高级语言封装给不了你的。第二C 语言的运行库不依赖运行时环境编译出来的 exe 非常小放到任何一台 Windows 机器上都能跑这很符合命令行工具轻量便携的调性。第三用 C 写这种程序需要对缓冲区、指针、线程生命周期做显式管理踩坑之后学到的东西远比写 Python 多。开发工具有两个方向可以选MinGW-w64 加 gcc轻量、免费、无需安装完整 IDE命令行编译一条命令搞定适合喜欢纯命令行工作流的开发者。建议安装 w64devkit 这类集成包解压即用自带 gcc、make省去配置环境变量的麻烦。Visual Studio 的开发者命令行如果你已经装了 VS直接用自带的环境进入 cmd使用 cl.exe 编译也可以通过。缺点是编译命令参数略繁琐新手容易绕晕。我的建议是使用 w64devkit 或者 TDM-GCC因为gcc chat.c -o chat.exe -lws2_32这一条命令足够简单能把注意力集中在代码而非工具链上。后面第三章的编译测试也都是基于 gcc 写的。1.3 整体架构设计程序结构上一个最小可用的 UDP 聊天程序包含四个模块初始化模块调用 WSAStartup 加载 Winsock 库创建 UDP socket绑定本机 IP 和端口。接收模块独立线程循环调用 recvfrom收到数据就打印到屏幕。发送模块主线程读取用户键盘输入调用 sendto 发送给目标地址。清理模块程序退出时关闭 socket调用 WSACleanup 释放资源。有人会问为什么接收必须放单独线程不能和发送放在同一个循环里因为 recvfrom 是阻塞调用如果放到主循环里程序会一直卡在等数据那一步用户键盘输入完全得不到处理聊天就变成单向的了。收发分离这个设计是网络聊天程序的基本功后面很多并发模型的雏形都是从这里开始的。2. 核心原理解析与准备工作2.1 UDP 协议的关键特性UDPUser Datagram Protocol用户数据报协议是传输层最朴素的协议之一。它的报文结构非常精简源端口、目的端口、长度、校验和再加上数据部分。和 TCP 相比它没有序号、确认号、标志位、窗口大小这些复杂字段所以处理起来特别快。可以把 UDP 比喻成寄明信片写上收件人地址丢进邮筒寄出去就不管了。明信片可能几天后到可能半路被雨水泡烂可能是寄丢了你也不知道你没法打电话去邮局查“我那封到底送没送到”邮局也不负责回执。TCP 则是挂号信有回执、有追踪编号、丢了一定会补发但流程也更重更慢。这个比喻对应到代码层面的含义是sendto函数执行成功只代表数据已经交给了操作系统绝不代表对方真的收到了。所以在测试 UDP 聊天程序时经常出现一种“诡异现象”本机打印“发送成功”但对方窗口什么都没显示。这不一定是你代码写错了很可能就是数据在半路丢了或者对方端口没监听。这是 UDP 的固有特性不是 bug但初学时会觉得特别难排查。还有个知识点值得注意UDP 有消息边界的概念。一次sendto发的数据对端必须用一次recvfrom才能完整接收到如果发送方一次发了 2000 字节接收方的缓冲区只给了 1024 字节那 1024 字节之后的数据直接就丢了不像 TCP 那样有字节流的概念可以分几次读。这就意味着我们在设计聊天消息时要控制单条消息的长度尽量别超过 1024 字节后面会进一步说明。2.2 开发环境准备再次强调这里说的“dos 命令行”实际操作是 Windows 的命令提示符或 Windows Terminal。在这个环境下开发 C 语言网络程序需要三样东西一个编译器、一个文本编辑器、一个能打开多个窗口的终端。编译器我推荐 w64devkit绿色解压把解压后的bin目录加到系统 PATH 里就能用。装完后打开 cmd输入gcc --version能看到版本信息就说明环境 OK 了。如果没有 gcc也可以安装 Visual Studio Community然后在“开始菜单”里找到 “Developer Command Prompt for VS”在这个终端里用cl命令编译效果一样。文本编辑器方面新手直接用 Notepad 或者 VS Code 都行代码量撑死几百行不需要复杂的工程管理单文件编译就够了。这里要专门回应一下“dos窗口字体太小怎么办”这个高频问题。默认的 cmd 窗口字体 7pt 看起来确实费眼解决办法有好几种点窗口左上角图标选择“属性”切到“字体”选项卡把大小调到 16pt 或更大同时把字体改成“Consolas”或“Lucida Console”显示效果改善很明显。按住 Ctrl 键滚动鼠标滚轮可以直接缩放终端内容的字号Windows 10 以上系统都支持。用 Windows Terminal 替代 cmd这个终端应用支持更精细的字体渲染和配色方案打开后按Ctrl/Ctrl-缩放字号观感比传统 cmd 好一个档次。字体调好之后测试聊天时读屏幕就不用眯眼睛了这个小细节能大幅提升体验建议一上来就配好。2.3 引入 Winsock 库时的链接细节Windows 的 socket 接口和 Linux/Unix 不一样它不在 libc 里而是放在一个专门的动态库里叫 Winsock Library。实际开发中用到的函数大部分属于 Winsock 2.0也就是ws2_32.dll。所以代码里要写#include winsock2.h #pragma comment(lib, ws2_32.lib)第一行是引入头文件第二行是告诉 MSVC 编译器链接时带上ws2_32.lib。如果你用的是 gcc在命令行编译时也要手动加上这个库gcc chat.c -o chat.exe -lws2_32-lws2_32就是让 gcc 链接 ws2_32 库。如果漏了这一步编译时会出现一长串 “undefined reference to __imp_WSAStartup8” 之类的错误刚接触 Winsock 的人十有八九会在这里卡一下提前知道就没问题了。另外一个和 Winsock 相关的历史细节是Winsock 有 1.0 和 2.0 两个版本现在统一用 2.0。代码里用MAKEWORD(2, 2)请求 2.2 版本是最稳妥的兼容性和功能都最好。3. 完整实现从零手写 UDP 聊天工具3.1 第一步初始化 Winsock 和创建 socket任何使用 Winsock 的程序第一件事都是调用WSAStartup。这个函数的作用是告诉操作系统“我要用你家的网络库了请给我准备好环境”同时反馈一下当前系统支持的 Winsock 版本。很多人忽略它的返回值但这其实是个大坑如果返回非零说明初始化失败后面所有 socket 操作都会返回无效句柄。WSADATA wsaData; int result WSAStartup(MAKEWORD(2, 2), wsaData); if (result ! 0) { printf(WSAStartup failed: %d\n, result); return 1; }接下来创建 socket。UDP 协议对应SOCK_DGRAM这里的 DGRAM 就是 Datagram数据报的缩写和 TCP 的SOCK_STREAM流正好形成对比。创建 socket 的代码SOCKET sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return 1; }WSAGetLastError()是 Winsock 版的errno能拿到具体的错误码。比如 10047 表示地址族不支持10044 表示协议类型不支持看到这些错误码就能有针对性地排查后面会给出速查表。这里要注意socket 创建成功后也要记得做失败处理空指针检查写习惯了网络句柄的检查同样必不可少。3.2 第二步绑定端口和设置目标地址UDP 是无连接的但接收方必须绑定一个确定的端口别人才知道把数据报到哪儿。绑定操作使用bind函数。这里有一个初学者容易混淆的地方发送方需要 bind 吗答案发送方也可以 bind但不是必须的。如果不 bind操作系统会自动分配一个临时端口给这个 socket对方回消息时就用这个临时端口作为目标端口。但为了让聊天双方能互相定位最简单的做法是让双方都 bind 到自己固定的端口比如程序启动时通过命令行参数传入端口号。bind 的核心是一个sockaddr_in结构体它相当于网络编程里的“地址簿条目”struct sockaddr_in localAddr; localAddr.sin_family AF_INET; // IPv4 localAddr.sin_port htons(port); // 端口号网络字节序 localAddr.sin_addr.s_addr htonl(INADDR_ANY); // 绑定本机所有 IP这里有两个关键函数htons和htonl全称是 Host TO Network Short / Host TO Network Long。为什么需要它们因为本地内存里的整数是“主机字节序”小端而网络传输统一使用“大端”网络字节序。如果直接把本地端口的数字填进去不同架构的机器之间就会互相读错端口号。这是网络编程的经典坑C 语言程序员要时刻记得“字节序”这回事。INADDR_ANY的意思是“我不想指定具体绑定哪个 IP所有本机 IP 上的这个端口都归我管”。在聊天程序里这台机器可能有多张网卡有线、无线、虚拟机网卡绑INADDR_ANY最省事。如果你只想到自己的局域网 IP也可以用inet_addr(192.168.1.100)这样的写法但代码在别的网络环境里就失去通用性了所以测试阶段建议用INADDR_ANY。目标地址的结构体和本地地址长得很像区别是sin_addr.s_addr要填对方的 IPsin_port要填目标端口。发送方每发送一次消息就要把消息和目标地址一起交给sendto。3.3 第三步多线程收发逻辑接收线程是聊天程序的灵魂。用一个CreateThread创建一个新线程线程函数里写一个死循环不断调用recvfrom接收数据DWORD WINAPI ReceiveThread(LPVOID lpParam) { char buf[1024]; struct sockaddr_in senderAddr; int senderLen sizeof(senderAddr); while (running) { int ret recvfrom(sock, buf, sizeof(buf) - 1, 0, (struct sockaddr*)senderAddr, senderLen); if (ret 0) { buf[ret] \0; printf([%s:%d] %s\n, inet_ntoa(senderAddr.sin_addr), ntohs(senderAddr.sin_port), buf); } } return 0; }recvfrom会告诉你数据是谁发的——通过senderAddr带上来的 IP 和端口信息。这是 UDP 和 TCP 一个很大的不同点TCP 连接一旦建立双方身份就固定了UDP 则可以在同一个 socket 上接收来自任何人的数据报所以每次recvfrom都要返回发送方的地址这让我们后面实现“多人聊天”有了可能——大家全都 bind 同一端口你就能收到所有人的消息回复时用sendto指定目标地址就行了。主线程做发送逻辑就简单了。用fgets从标准输入读取一行文字去掉最后的换行符调用sendto发给目标 IP 和端口while (running) { fgets(buf, sizeof(buf), stdin); buf[strcspn(buf, \r\n)] \0; if (strcmp(buf, bye) 0) break; sendto(sock, buf, strlen(buf), 0, (struct sockaddr*)peerAddr, sizeof(peerAddr)); }这里多说一句为什么用fgets而不是gets。gets在任意一本 C 语言教材里都被反复警告过因为它不检查缓冲区长度输入超长直接溢出轻则程序崩溃重则被恶意利用。虽然命令行聊天工具看起来人畜无害但养成良好的编码习惯从第一个程序开始就不用危险函数这个意识比什么都重要。3.4 完整源码可直接编译运行的示例下面给出一个单文件 UDP 命令行聊天程序。这个程序同时实现了发送和接收启动时通过命令行参数指定两个信息第一个参数是本机监听的端口第二个参数是目标 IP第三个参数是目标端口。如果只传入一个参数程序就只监听不发送适合等对方先发来消息。代码用到的函数都是前面分模块介绍过的拼在一起就是一个完整可运行的作品。#include stdio.h #include string.h #include winsock2.h #pragma comment(lib, ws2_32.lib) #define BUFSIZE 1024 static SOCKET g_sock INVALID_SOCKET; static volatile int g_running 1; DWORD WINAPI ReceiveThread(LPVOID lpParam) { char buf[BUFSIZE]; struct sockaddr_in senderAddr; int senderLen sizeof(senderAddr); while (g_running) { int ret recvfrom(g_sock, buf, BUFSIZE - 1, 0, (struct sockaddr*)senderAddr, senderLen); if (ret 0) { buf[ret] \0; printf([%s:%d] %s\n, inet_ntoa(senderAddr.sin_addr), ntohs(senderAddr.sin_port), buf); fflush(stdout); } else { int err WSAGetLastError(); if (err WSAEINTR) break; } } return 0; } int main(int argc, char* argv[]) { WSADATA wsaData; struct sockaddr_in localAddr, peerAddr; HANDLE hThread; char inputBuf[BUFSIZE]; if (argc 2) { printf(用法: chat.exe 本地监听端口 [目标IP] [目标端口]\n); printf(示例: chat.exe 9000 127.0.0.1 9001\n); return 1; } int localPort atoi(argv[1]); if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { printf(WSAStartup failed\n); return 1; } g_sock socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (g_sock INVALID_SOCKET) { printf(socket failed: %d\n, WSAGetLastError()); WSACleanup(); return 1; } localAddr.sin_family AF_INET; localAddr.sin_port htons((unsigned short)localPort); localAddr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(g_sock, (struct sockaddr*)localAddr, sizeof(localAddr)) SOCKET_ERROR) { printf(bind failed: %d\n, WSAGetLastError()); closesocket(g_sock); WSACleanup(); return 1; } printf(UDP聊天程序已启动本机端口: %d\n, localPort); printf(输入消息并回车发送输入 bye 退出\n); if (argc 4) { peerAddr.sin_family AF_INET; peerAddr.sin_port htons((unsigned short)atoi(argv[3])); peerAddr.sin_addr.s_addr inet_addr(argv[2]); } else { peerAddr.sin_family AF_INET; peerAddr.sin_port htons(0); peerAddr.sin_addr.s_addr htonl(INADDR_LOOPBACK); } hThread CreateThread(NULL, 0, ReceiveThread, NULL, 0, NULL); if (hThread NULL) { printf(CreateThread failed\n); closesocket(g_sock); WSACleanup(); return 1; } CloseHandle(hThread); while (g_running) { if (fgets(inputBuf, sizeof(inputBuf), stdin) NULL) break; inputBuf[strcspn(inputBuf, \r\n)] \0; if (strcmp(inputBuf, bye) 0) break; if (argc 4) { sendto(g_sock, inputBuf, (int)strlen(inputBuf), 0, (struct sockaddr*)peerAddr, sizeof(peerAddr)); } else { printf(未指定目标地址消息未发送。用法: chat.exe 端口 目标IP 目标端口\n); } } g_running 0; closesocket(g_sock); WSACleanup(); return 0; }代码看起来不长但已经覆盖了 Winsock 编程最核心的几板斧初始化、建 socket、绑定、多线程收发、清理。把这段代码保存成chat.c编译后就能跑。下面说说怎么测试。3.5 编译与测试流程在含有 gcc 的 cmd 窗口里进入源码目录执行gcc chat.c -o chat.exe -lws2_32编译生成的chat.exe就在当前目录下。测试时打开两个 cmd 窗口可以按Win R输入cmd再开一个。第一个窗口执行chat.exe 9000第二个窗口执行chat.exe 9001 127.0.0.1 9000这样第一个窗口监听 9000 端口但不主动给谁发消息第二个窗口监听 9001 端口目标指向 127.0.0.1 的 9000 端口。在第二个窗口输入文字回车第一个窗口就能看到从[127.0.0.1:9001]发来的消息。如果想让两个窗口互发再开第三个窗口执行chat.exe 9002 127.0.0.1 9000三个窗口就形成了一个简单的“星型”通信9001 和 9002 都给 9000 发消息9000 都能收到。不过 9000 想回复的话目前代码里只能发给一个固定目标这时你会发现要发给不同的人就需要切换目标地址这就是 UDP 通信“无连接”特性的另一面你天然可以收到所有人的消息但发给谁全凭每次sendto指定。想要更好用的“回复最近联系人”功能就要自己记录对端的地址后面扩展部分我会聊。我在实际测试时发现一个很影响体验的问题printf的输出有缓冲recvfrom收到消息后如果不加fflush(stdout)屏幕上可能要等下一行输入触发刷新才显示消息看起来就像“消息不实时”。这个问题在 Linux 的终端里不明显但在 Windows 的 cmd 下特别容易遇到。所以代码里我在每次打印完就调用fflush(stdout)你要二次开发时建议保留这一行。4. 常见问题与排查技巧实录4.1 问题排查速查表在群里边写边测我收集了几个出现频率最高的报错和异常。把这些整理成一个表格方便你在遇到问题时直接对照。现象可能原因解决办法编译时 undefined reference to__imp_WSAStartup没链接 ws2_32 库编译命令加-lws2_32或确认#pragma comment(lib, ws2_32.lib)运行时WSAStartup failed系统 Winsock 版本太低或初始化失败确认使用MAKEWORD(2, 2)重启或更新系统网络组件bind 返回 10048端口已被占用换一个端口或者用netstat -ano查看占用进程bind 返回 10049绑定的 IP 地址无效确认用INADDR_ANY或正确的本机 IPsendto 返回 SOCKET_ERROR目标不可达、防火墙拦截或端口未监听检查目标 IP/端口是否能通暂时关闭防火墙测试收不到消息但发送端显示成功UDP 丢包、目标端口错误、防火墙拦截先在本机用 127.0.0.1 测试排除网络因素中文乱码控制台代码页与代码字符串编码不一致在运行前执行chcp 65001并在源码中统一使用 UTF-8 编码程序可以收但输出不实时stdout 缓冲未刷新每次printf后调用fflush(stdout)输入一行消息后无法继续输入recvfrom 阻塞了主线程确保接收逻辑放在独立线程中主线程只负责发送这里面最有代表性的还是 10048 端口占用的问题。Windows 上如果你重复启动程序之前那个进程没有正常退出端口就一直被占着新实例 bind 肯定失败。用netstat -ano | findstr 9000能查到占用进程的 PID再用taskkill /PID 1234 /F强制结束它。不过我自己更习惯在程序异常退出后顺手打开任务管理器看看有没有残留的chat.exe直接杀掉更方便。4.2 防火墙拦截的问题Windows 默认防火墙对于“未知程序”监听端口通常会自动弹窗询问有时候你手一抖点了取消程序就收不到任何网络数据。在局域网里测试时如果两台机器始终互相收不到消息先别怀疑程序把防火墙关了试试——这里说的是测试环境的临时关闭不是让你长期裸奔。关闭后如果立刻通了说明问题出在防火墙规则上需要给编译出的 exe 单独添加一条“允许”规则。添加规则的操作路径控制面板 - Windows Defender 防火墙 - 高级设置 - 入站规则 - 新建规则 - 程序 - 选择你的chat.exe- 允许连接。这个操作只需要做一次之后程序再监听任何端口都不会再被拦。如果是 Win10/11 自带的终端环境第一次运行时系统可能还会问“是否允许应用通过防火墙”记得勾选“专用网络”并点允许。这里多提一嘴我遇到过好几次自己写的小工具在同事电脑上跑不通排查半天发现就是防火墙弹窗被随手关掉了所以测试网络程序时把“防火墙”这关先过掉能省下一堆无意义的定位时间。4.3 dos 窗口字体、布局与阅读体验优化黑底白字的命令行聊天虽然很有极客范儿但长时间盯小字体真的很累。除了前面提到的调整窗体字体还可以做几件小事让体验好很多每次程序启动时调用system(cls)清一次屏让整个聊天记录从窗口顶部开始显示而不是从乱七八糟的历史命令中间冒出来。如果想区分“你发的消息”和“别人发的消息”可以在发送时给自己发的内容加个前缀比如[我] 内容在接收线程里统一[IP:端口] 内容这样屏幕上消息的来源一目了然。把提示文字加上颜色发送消息用白色接收的消息用绿色报错用红色。Windows 控制台支持 ANSI 转义序列在程序开头执行system()启用 VT 处理然后用printf(\033[32m%s\033[0m, buf)输出绿色文字实测在 Windows 10 以上版本可用。把窗口标题从默认的cmd改成你的程序名调用SetConsoleTitle(UDP Chat - 9000)多个窗口之间一眼就能分辨谁是谁。这些优化其实跟网络协议没关系但直接影响你在命令行里聊天的舒适度。工具类项目最容易犯的毛病就是“功能是完整的界面是劝退的”稍微打磨一下观感这个程序拿出手的档次都不一样了。4.4 功能扩展方向写到这里你已经有了一个能跑的 UDP 命令行聊天程序但它还只是个“最小可用版本”。想让这个项目更完整、更有亮点可以从几个方向继续扩展。第一个扩展方向是“多人聊天室”。把接收逻辑保留不变发送逻辑从“发给固定目标”改成“接收时记录发送方地址回复时发给最近给你发消息的人”。实现方式很直接维护一个struct sockaddr_in lastSender变量每次recvfrom后把地址存下来用户输入内容时如果没有指定目标地址就默认发给这个最近联系人。进一步可以把所有联系人的地址存在一个链表或数组里按 T 键切换发送对象这就是最简化版的好友列表。第二个扩展方向是“消息可靠性增强”。UDP 丢包的问题在局域网里不明显但跨网络传输时就可能遇到。应用层可以给每条消息加上一个自增序号接收方检查序号是否连续发现跳号就请求重发发送方对没收到确认的消息做超时重发。这套机制其实就是简化版的 TCP自己动手实现一遍对理解可靠传输的原理特别有帮助。第三个扩展方向是“文件传输”。把发送内容从纯文本改成“文件路径”程序读文件、按 1024 字节分割、加上文件头和序号后逐个发送接收方根据序号重组文件。这也顺便就理解了为什么 TCP 适合文件传输、UDP 做文件传输会有多费劲——你可以亲眼看到文件在 UDP 下丢包、乱序然后再想办法去解决。第四个扩展方向是“广播消息”。UDP 支持定向广播和子网广播把目标地址设为255.255.255.255或者子网广播地址就能实现“局域网内所有监听该端口的客户端都能收到这条消息”。这个功能用在“服务发现”场景特别实用不需要提前知道对方的 IP大家全都在一个频道里喊话就行。说实话一个课程设计做到这个程度在答辩现场已经能讲出很多花活了。关键不在功能多不多而在于你能不能讲清楚每一步为什么这么设计遇到问题是怎么排查的。这些过程本身就是网络编程能力最好的证明。最后分享一个我在调试这类程序时的习惯永远准备三个 cmd 窗口。一个跑“监听方”一个跑“发送方”一个放着netstat -ano实时看端口状态。这样程序有没有监听到端口、消息有没有发出去、端口是否冲突一眼就能看清楚。很多问题在 GUI 工具里要翻半天的日志在命令行里用netstat一下就定位了。这个习惯是我从第一次写 UDP 聊天程序时就一直保留的也建议你试试。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →