ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于WinSock的TCP/UDP聊天程序实现详解:从Socket到MFC界面优化

基于WinSock的TCP/UDP聊天程序实现详解:从Socket到MFC界面优化 简介东南大学计算机网络第四次实验报告围绕基于TCP/IP与UDP/IP的通信应用程序设计展开适合自动化、计算机、信息通信等专业学生参考。报告内容覆盖实验目的与要求、实验原理、方案与步骤、设备配置、实验记录、总结及部分代码详细展示了WinSock环境下客户机/服务器工作流程包括创建套接字、绑定本地地址、监听端口、建立连接及收发消息等关键环节。附录的代码片段可辅助理解网络编程实现。资源为单个PDF文档压缩包大小20KB下载后即可查看。目前已有88人学习浏览。报告中还记录了界面设计、功能改进以及自定义命令等细节通过TCP与UDP两个实验的对比能帮助读者理解面向连接与无连接通信的差异掌握Socket程序设计与调试方法。这份报告结构完整、步骤清晰既适合课程实验参考也适合入门网络编程的读者对照练习。1. 计算机网络实验报告里的干货TCP与UDP聊天程序能复现到什么程度这份东南大学计算机网络第四次实验报告内容是信息通信网络概论课程里的「计算机网络通信应用程序设计」落点非常明确用 WinSock 接口在 MFC 框架下写两个聊天程序一个走 TCP一个走 UDP。别被「实验报告」四个字劝退它其实是一份可以直接照着复现的工程笔记——服务器怎么 listen、客户端怎么 connect、消息怎么带主机名和时间戳、怎么用 ShellExecute 打开资源管理器、怎么把聊天记录导出成 txt全部有代码片段和界面截图。适合三类人正在做同主题课程设计的学生、想快速捡起 WinSock 编程的从业者、以及需要一份「TCP/UDP 对比 聊天程序扩展功能」参考实现的工程师。报告里的坑也不少比如 UDP 服务器不填客户端 IP 就发不了消息、清空列表后收发计数要重置这些都是实际调试时最容易卡住的地方。2. TCP 聊天程序WinSock 的五个调用与三次握手外的细节2.1 服务器端socket→bind→listen→accept 的四步定式TCP 服务器端的流程在报告里写得很清楚创建套接字、绑定本地地址和端口、进入监听模式、等待客户请求、用返回的新套接字通信。写成 WinSock 调用就是下面这套固定动作// 服务器端创建、绑定、监听、接受连接 SOCKET srvSock socket(AF_INET, SOCK_STREAM, 0); // 1. 创建流式套接字 SOCKADDR_IN srvAddr; srvAddr.sin_family AF_INET; // 地址族IPv4 srvAddr.sin_port htons(8888); // 端口8888注意字节序 srvAddr.sin_addr.s_addr htonl(INADDR_ANY); // 绑定本机所有网卡地址 bind(srvSock, (SOCKADDR*)srvAddr, sizeof(srvAddr)); // 2. 绑定本地地址与端口 listen(srvSock, 5); // 3. 进入监听backlog5 SOCKADDR_IN cliAddr; int len sizeof(cliAddr); SOCKET cliSock accept(srvSock, (SOCKADDR*)cliAddr, len); // 4. 接受连接返回通信套接字这里有个关键点accept返回的cliSock才是真正用来收发数据的套接字srvSock继续留在监听状态等待下一个客户连接。实验报告里特别强调「用返回的套接字和客户端进行通信」很多初学者第一次写时容易拿监听套接字直接send/recv结果要么报错要么数据发不出去。htons和htonl是主机字节序转网络字节序端口 8888 和地址INADDR_ANY是常见设置INADDR_ANY表示绑定本机所有 IP调试时不用关心服务器有几张网卡。listen的第二个参数 5 是等待连接队列的最大长度单机调试这个值够用。2.2 客户端 connect 与收发消息格式里藏着双方约定客户端比服务器端简单创建套接字后直接connect到服务器的 IP 和端口然后就可以收发数据// 客户端创建套接字并连接服务器 SOCKET cliSock socket(AF_INET, SOCK_STREAM, 0); SOCKADDR_IN srvAddr; srvAddr.sin_family AF_INET; srvAddr.sin_port htons(8888); // 必须与服务器端口一致 srvAddr.sin_addr.s_addr inet_addr(127.0.0.1); // 服务器 IP本机调试用回环地址 int ret connect(cliSock, (SOCKADDR*)srvAddr, sizeof(srvAddr)); if (ret SOCKET_ERROR) { // 这里要处理连接失败检查服务器是否已点击“侦听” }收发数据用send和recv但报告里藏着一个容易被忽略的设计双方约定了消息格式。服务器向客户机发送Welcome my friend!客户机向服务器发送I am Paul这不仅是打招呼更是在验证双方是否建立了双向字节流。TCP 是面向连接的可靠传输数据按序到达所以recv收到的消息顺序和send发送的顺序一致——这一点在聊天场景里很重要如果改成 UDP消息顺序就无法保证了。2.3 给消息加上主机名与时间戳gethostname 与 localtime报告里的第一个改进点是获取发送方主机名和发送时间代码片段挺有代表性// 获取本机主机名并格式化当前时间 char hostname[64] {0}; gethostname(hostname, sizeof(hostname)); // 获取本机主机名 time_t t1 time(NULL); // 获取当前系统时间戳 struct tm* t2 localtime(t1); // 转换为本地时间结构 CString mTime; mTime.Format(%d/%d/%d %d:%d:%d, t2-tm_year 1900, t2-tm_mon 1, t2-tm_mday, t2-tm_hour, t2-tm_min, t2-tm_sec); // 格式年/月/日 时/分/秒这段代码的价值在于它演示了「在接收消息列表里显示发送方信息」的实现路径gethostname拿到的是本机主机名在客户机与服务器分别调用时显示的就是各自的主机名实验里用同一台电脑调试所以两边都是20WQ。localtime返回的是静态结构体指针不需要手动释放但要注意tm_year是从 1900 年开始计数必须加 1900tm_mon从 0 开始必须加 1。报告中把时间戳放在消息列表首部、主机名放在信息前面这是聊天工具里最常见的展示逻辑。3. UDP 版通信无连接编程的流程与双向通信的坑3.1 UDP 服务器与客户端bind recvfrom/sendto 就够了UDP 部分的实验原理比 TCP 短很多服务器创建套接字并绑定到本地地址和端口然后等待接收数据客户端创建套接字后直接向服务器发送数据。代码结构也简洁// UDP 服务器创建套接字、绑定端口、接收数据 SOCKET udpSrv socket(AF_INET, SOCK_DGRAM, 0); // 创建数据报套接字 SOCKADDR_IN srvAddr; srvAddr.sin_family AF_INET; srvAddr.sin_port htons(9999); // 服务器端口 srvAddr.sin_addr.s_addr htonl(INADDR_ANY); // 绑定本机所有地址 bind(udpSrv, (SOCKADDR*)srvAddr, sizeof(srvAddr)); // 绑定端口 char buf[1024] {0}; SOCKADDR_IN cliAddr; int len sizeof(cliAddr); recvfrom(udpSrv, buf, sizeof(buf), 0, (SOCKADDR*)cliAddr, len); // 接收数据并获取发送方地址SOCK_DGRAM对应 UDPSOCK_STREAM对应 TCP这是两者在 WinSock 层面最直观的区别。recvfrom的最后一个参数cliAddr会返回发送方的地址信息——实验中服务器没有填写客户机 IP 也能收到消息并且能获得发送方 IP靠的就是这个参数。这个设计有个实际价值服务器拿到对端地址后才能向对方回发数据这也是 UDP 双向通信的基础。3.2 双向聊天的一个常见做法双方都绑定端口实验记录里有一个很值得注意的现象服务器没填客户机 IP 时能收到消息但不能给客户机回消息只有填了客户机 IP 后才能把消息发给客户机。这是因为 UDP 是无连接的sendto必须指定完整的目标地址。服务器要主动发消息给客户机就得知道客户机的 IP 和端口。常见做法是双方固定端口通信时互相告知地址或者像报告中这样在界面上手动填写对端 IP// UDP 客户端向服务器发送数据 SOCKADDR_IN srvAddr; srvAddr.sin_family AF_INET; srvAddr.sin_port htons(9999); // 目标端口需填写服务器端口 srvAddr.sin_addr.s_addr inet_addr(127.0.0.1); // 目标 IP需填写服务器 IP sendto(cliSock, msg, strlen(msg), 0, (SOCKADDR*)srvAddr, sizeof(srvAddr));这里的坑在于如果服务器只bind了端口但没填客户端地址它调用sendto时就不知道把数据发到哪里。报告里的解决方式是「只有填写客户机后才能发送消息给客户机」这在实现上就是给服务器界面加一个「客户机 IP」输入框发送时从输入框取值填入srvAddr.sin_addr.s_addr。我做这类实验的习惯是先让客户端发一条空消息或握手包服务器从recvfrom返回的地址结构里自动记录客户机地址比手动填 IP 更稳也免去换 IP 后忘记更新的麻烦。3.3 消息导出与自定义指令fopen/fwrite 与 ShellExecuteUDP 部分的另一个改进是导出消息记录代码思路是先把列表内容写进字符串再用文件读写函数落盘// 导出消息记录到 txt 文件 FILE* fp fopen(chat_log.txt, a); // 追加模式打开文件 if (fp ! NULL) { fwrite(content, strlen(content), 1, fp); // 将消息字符串写入文件 fclose(fp); // 关闭文件 }a是追加模式文件不存在会自动创建内容写在末尾适合聊天记录这种持续增长的场景。这个功能比 TCP 部分的扩展更有工程价值聊天记录要持久化不能程序一关数据就没了。UDP 版还定义了更多字符指令/e打开指定网址、/t打开指定图片、/r打开资源管理器并定位到C:/windows/media这些操作都通过ShellExecute完成后面会专门讲。4. 把聊天程序做得像 QQ扩展指令、字符画与控件美化4.1 字符画与表情在对话框里画牛和电话报告里最有意思的部分是字符指令系统。输入/n在聊天对话框画一头牛输入/p画一个电话输入/x画一只小象。实现思路不复杂本质是把多行字符串逐行输出到对话框// 自定义字符画输入 /x 时绘制小象 CString TP_xin_str[] { __ __ , / \\ / \\ , /| (\\ |( , ^ \\ /___\\ /\\ | , |__| |__|- }; int TP_xin_int 5; // 字符画总行数 for (int i 0; i TP_xin_int; i) { // 将第 i 行字符画追加到聊天显示列表 }这段代码的关键在于字符画是一行一行拼出来的每行字符串的宽度必须一致否则画出来的图形是歪的。报告中牛和电话的字符画看起来正常是因为每行末尾都做了空格补齐。字符画在 MFC 里通常往CListBox或CEdit控件里逐行追加不要塞进同一个CString再一次性显示否则换行符会被吃掉。表情符号的实现更简单输入/s弹出难过 ( ╥ ﹏╥ )输入/a弹出生气 ( ▼ 皿▼ #)本质是switch分支后调用AfxMessageBox适合做演示但缺少实际聊天工具的「插入到消息输入框」逻辑这一点在做课程设计答辩时可以主动提到算是一个「我知道还能更好」的加分点。4.2 打开外部程序ShellExecute 的四种用法报告里用ShellExecute实现了打开 mp3、网址、图片、资源管理器等操作这个函数是 Windows 下最常用的「打开外部资源」入口四种典型用法值得记下来// ShellExecute 的四种场景 // 1. 打开网址 ShellExecute(NULL, open, , NULL, NULL, SW_SHOWNORMAL); // 2. 打开指定文件图片 ShellExecute(NULL, open, F:/picture.jpg, NULL, NULL, SW_SHOWNORMAL); // 3. 打开资源管理器并定位到指定目录 ShellExecute(NULL, explore, C:/windows/media, NULL, NULL, SW_SHOWNORMAL); // 4. 播放音频文件 ShellExecute(NULL, NULL, 老人与海.mp3, , NULL, SW_SHOWMAXIMIZED);第二个参数是操作类型open表示用默认关联程序打开explore表示在资源管理器中打开目录第三个参数是要操作的对象网址传完整 URL文件传绝对路径倒数第三个参数是工作目录没有特殊需求传NULL。报告里/r指令对应explore这里有一个细节explore操作在部分 Windows 版本上对不存在的目录会静默失败界面无反应排查时先确认路径存在而不是怀疑代码写错。4.3 控件透明与背景图OnCtlColor 的细节界面美化部分报告用了两个手段OnPaint()里按图片 ID 绘制背景bmp以及WM_CTLCOLOR消息自动生成OnCtlColor()来设置控件背景透明和字体颜色。UDP 部分贴了关键代码// 设置控件背景透明与静态文本颜色 HBRUSH CUDPprojectDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) { if (pWnd-GetDlgCtrlID() IDC_EDIT_OUTMSG) { // 输入框背景 HBRUSH brush CreateSolidBrush(RGB(255, 255, 255)); // 白色背景 return (HBRUSH)brush; } else if (nCtlColor CTLCOLOR_STATIC) { // 静态文本控件 pDC-SetBkMode(TRANSPARENT); // 背景透明 return (HBRUSH)::GetStockObject(NULL_BRUSH); // 透明画刷 } return CDialog::OnCtlColor(pDC, pWnd, nCtlColor); }报告里提到「尝试直接在 OnCtlColor 中进行设置没有达到预期效果」这个我在做 MFC 界面时也遇到过。原因是OnCtlColor对不同类型的控件分支处理逻辑不同IDC_EDIT_OUTMSG走输入框分支CTLCOLOR_STATIC走静态文本分支两个分支不能混用。还有一个常见的坑CreateSolidBrush创建的画刷需要手动DeleteObject释放否则每次界面刷新都会泄漏一个 GDI 对象实验时长看不出问题程序开一晚上就会变卡。报告中把背景图放在OnPaint里按图片 ID 切换注意先画背景再绘制控件否则背景会盖住消息列表。5. 避坑网络实验里最常见的六个翻车现场5.1 「明明在同一台电脑却连不上」的本地地址陷阱现象服务器已点击「侦听」客户端输入127.0.0.1或本机 IP 后点「连接」程序卡住或提示连接失败。原因最常见的是端口不一致——服务器bind的是 8888客户端connect写成了 9999其次是服务器listen后没有调用accept连接请求堆积在等待队列里客户端connect会一直阻塞。解决先检查两端端口是否完全一致再确认服务器在accept处设置了断点或有界面提示。我在单机调试时习惯服务器和客户端各开一个实例分别设断点看accept是否返回。5.2 「发送和接收计数对不上」的初始化问题现象初始化后还没发消息发送数和接收数就已经是 1清空列表后计数重置为 0但再发消息时计数从 1 开始还是对不上。原因报告里写了「初始化时发送数和接收数均为 1」这是因为初始化时程序自动发送了欢迎消息接收列表里也有一条欢迎消息所以计数不是从 0 起。解决计数逻辑要区分「用户主动发送」和「系统自动消息」我一般用一个IsUserMessage标志位只有用户点击发送按钮或收到对端消息时才递增计数。这个细节在验收答辩时经常被老师追问「为什么初始是 1」。5.3 端口被占用bind 失败后没有任何提示现象上次程序没关闭干净再次运行点击「侦听」或「开始聊天」界面无反应但收发消息全部失败。原因WinSock 里bind返回SOCKET_ERROR但代码没有检查返回值错误被吞掉了。解决每次bind、listen、accept之后都要判断返回值失败时调用WSAGetLastError()拿错误码——WSAEADDRINUSE就是端口被占用换个端口或关掉旧进程即可。还有一个小技巧在socket之后调用setsockopt设置SO_REUSEADDR可以避免TIME_WAIT状态的端口残留问题这在实验里最实用。5.4 字符画错乱中文编码与换行的组合坑现象输入/n后画出来的牛歪歪扭扭字符对不齐或者中文消息和字符画混在一行。原因实验环境是中文 WindowsMFC 默认使用 GBK 编码CString里的中文字符占两个字节字符画里的空格和符号占一个字节显示时行宽不一致就错位了。解决字符画全部用 ASCII 字符不要混中文每行末尾用空格补齐到相同宽度。报告中的字符画是纯符号显示正常但如果你要自己加新图案比如/z画小猪一定用等宽字体并把输出控件的字体设置为Courier New或Fixedsys。5.5 UDP 收不到消息先怀疑 sendto 的目标地址现象客户端sendto后服务器recvfrom一直阻塞收不到任何数据。原因UDP 是面向无连接的sendto就算地址写错了比如端口差了 1或者 IP 写成了别的机器也返回成功数据包被系统静默丢弃。解决先看sendto的返回值——它只表示数据已交给系统发送队列不代表对端已收到。排查时用 Wireshark 抓包最直观没有抓包工具就先确认端口、IP 是否匹配再确认服务器确实执行了bind且绑定的端口与客户端sendto的目标端口一致。我个人遇到的 UDP 翻车九成是端口号写错或忘写htons。5.6 防火墙弹窗与实验机房的策略限制现象程序在本机能跑通换到实验机房两台电脑之间通信失败或者程序第一次运行时 Windows 防火墙弹窗拦截。原因防火墙默认拦截未信任程序的入站连接实验机房的统一安全策略也可能限制端口范围。解决开发阶段在防火墙设置里允许程序通过专用网络如果机房禁止自定义规则把通信端口改成策略允许范围内的端口常见的是 8000-9000 或 50000 以上。报告里实验用的是本机调试没遇到这个问题但换真实网络环境时这是必踩的坑。6. 实验验收与进阶从「能跑」到「能讲清楚」6.1 一份可以照着自检的 TCP/UDP 对比清单做这种实验老师验收时问得最多的不是「能不能聊天」而是「TCP 和 UDP 到底差在哪」。报告最后有三道思考题答案已经在报告里写得很完整我把它们在验收前整理成一张自检表对着表过一遍比临时背概念靠谱得多。维度TCPUDP连接状态面向连接需 connect 建立虚拟连接无连接bind 端口后直接收发可靠性可靠不丢数据数据按序到达不可靠可能丢包不保证顺序开销握手过程消耗资源开销大无握手开销小速度快适用场景大数据量交换、要求正确传输实时传输如 IP 电话、视频直播报告里给了一个很典型的应用判断控制焊接机器人必须用 TCP因为控制数据必须正确传输IP 电话用 UDP因为实时性优先少量丢包可以接受。这个结论不止是考试答案它背后是「可靠性换实时性」的取舍思路。面向连接的 TCP 是有连接的所以握手过程会消耗资源可靠但相对慢UDP 只要绑定端口就能发送速度快但没有保障。这个对比在多路复用、拥塞控制等进阶知识点上也成立。6.2 如果要做改进三个值得动手的方向报告里的聊天程序已经覆盖了基础通信、扩展指令、界面美化但离「像 QQ」还有距离。我做这类实验时一般会建议三条改进路线第一把收发消息改成多线程当前版本的recvfrom和recv会阻塞界面线程消息一多界面就卡死——用CreateThread开一个接收线程收到消息后用PostMessage通知界面更新这是真实聊天工具的标准做法。第二增加文件传输功能TCP 版可以用send循环发送文件块配合开始标志和结束标志接收方写文件UDP 版可以加序号和确认机制这也是理解「TCP 可靠传输为什么复杂」的最佳实践。第三把指令系统改成可配置的协议表比如用std::map把/e、/t、/r映射到操作函数指针这样加新指令不用改switch这也正好是报告里说的「设计简单的应用层协议」。这份报告我拆完最大的感受是它的价值不在代码有多高级而在于把「用网络编程做一个能演示的应用」的完整链路讲清楚了——从 socket 创建、绑定端口、收发消息到加主机名时间戳、加指令系统、美化界面每一步都有对应的代码和踩坑记录。特别是 UDP 部分「服务器没填写客户机 IP 就发不了消息」这个细节不做一遍实验根本意识不到。从那以后我每次做网络实验都强制走一遍这个流程先画连接时序图标清楚哪端 bind、哪端 connect、哪端先发数据再动手写代码写完后对照验收清单逐项自测。这套习惯帮我避免了很多「代码写完了但不知道在干什么」的尴尬时刻。希望这份实验报告的拆解对你有用照着复现一遍TCP 和 UDP 的差别会比背十遍概念都深刻。本文还有配套的精品资源点击获取
返回列表