
简介面向计算机网络实验的Socket编程完整代码包围绕TCP与UDP两种传输层协议覆盖一对多聊天与多人聊天室场景帮助学习进程间通信、并发服务端及异常处理。压缩包共14个文件以6个C源文件为主另含2个Python脚本及编译好的server1、client1、client2等可执行文件整体仅34KB目录按task1、task2、task3划分便于对照学习已有1204人下载学习。代码中TCP部分实现了一对一与一对多通信UDP部分模拟了广播式聊天室并涵盖多连接管理、异常捕获等关键处理。资源还提供了带bind的服务器/客户端变体用于理解地址绑定对通信流程的影响。通过运行和改写这些代码可深入理解Socket API调用流程、TCP的可靠传输机制与UDP的低延迟特性掌握网络编程中常见的并发模型与排错思路适合正在完成计算机网络实验或希望巩固Socket编程基础的学习者。1. 拿到“实验三socket编程代码.rar”之后先别急着解压看到“实验三socket编程代码.rar”这个压缩包名字大多数人的第一反应是找个现成代码改个变量名就交差。这个思路应付普通实验也许能过关但这个实验我不建议这么干因为标题里那串关键词——socket编程、tcp/udp、一对多聊天、多人聊天室——正好对应计算机网络课程里最常考、也最容易在面试中挂人的三块内容运输层协议选型、socket调用流程、服务端如何管理大量并发连接。这个实验要做的其实很明确写一个基于socket的多人聊天室服务端同时服务多个客户端客户端A发消息服务端收到后转发给其他所有人。只要你能把“多个人同时在线聊天”这一个场景在自己机器上跑通再顺手解释清楚一个关键函数调用这份实验报告的质量就比网上随便下载的rar高出一截。适合做这件事的人不只是正在赶实验报告的在校生。期末复习计算机网络、准备考研复试、或者想补一下网络编程基础的同学都可以把聊天室当成一个最小但完整的练习场。这个实验真正的价值不在代码本身而在你能把“为什么这么写”讲明白。2. TCP还是UDP聊天室主干的两种设计路径2.1 TCP为什么是多人聊天室的默认主干计算机网络八股里的标准答案先解决选型问题。聊天室的数据是文本消息要求不丢、不乱序、不重复TCP天然满足这些要求。计算机网络八股里关于tcp和udp的区别那一页背得再熟落到实验里也要能说出来TCP提供面向连接的可靠字节流服务发送方知道消息被确认接收方按序重组UDP提供无连接的数据报服务只保证尽力而为不保证到达顺序。具体到聊天室场景差别是这样的你发一句“大家好”如果走UDP这句消息可能因为网络拥塞被丢弃对方什么都没看到如果走TCP内核协议栈会负责重传、排序直到对端应用层读到完整数据。文本聊天对可靠性的要求远高于对实时性的要求所以默认选TCP这不是偏好是需求决定的。把选型理由写进实验报告时可以给这样一张表维度TCPUDP连接管理三次握手建立连接四次挥手释放无连接可靠性可靠交付丢包重传尽力而为可能丢失数据边界字节流无消息边界保留报文边界实时性略高延迟低延迟适用场景文本聊天、文件传输音视频、广播、状态上报2.2 UDP能不能做聊天室bind、recvfrom与广播地址UDP也不是完全不能做实验。有一种常见做法是做一个局域网内无中心节点的广播聊天室所有客户端绑定同一个端口发送时把消息发到广播地址255.255.255.255其他所有监听该端口的进程都能收到。这在宿舍局域网里跑起来效果很直观适合用来演示UDP的一对多特性。最小UDP广播发送代码是这样的// 创建UDP socket int fd socket(AF_INET, SOCK_DGRAM, 0); // 允许发送广播报文默认情况下SO_BROADCAST是关闭的 int opt 1; setsockopt(fd, SOL_SOCKET, SO_BROADCAST, opt, sizeof(opt)); struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(8888); addr.sin_addr.s_addr htonl(INADDR_BROADCAST); // 255.255.255.255 // 直接把消息扔到广播地址局域网内所有人都能收到 sendto(fd, hello, 5, 0, (struct sockaddr*)addr, sizeof(addr));收到消息的一端用recvfrom接收同时拿到发送方的IP和端口。这种方式不需要中心服务器每个节点天然一对多。但代价也很明显没有服务器就不知道谁在线消息丢了也没有重传机制广播报文不会被路由器转发跨网段就失效。所以它只能作为局域网演示方案真要做作业里的“多人聊天室”还是TCP加中心服务器更靠谱。3. 用select实现服务端一对多从阻塞到可用的关键代码3.1 单线程阻塞模型为何撑不住第二个客户端很多学生第一次写聊天室服务端写出来是这样的先socket、bind、listen然后一个accept接一个客户端接着在循环里recv等待数据。这样做的直接后果是只服务第一个客户端第二个客户端连进来之后一直在accept队列里排队服务端永远没机会处理它。要给这个模型“开窍”常见做法是引入多线程accept到一个新连接就pthread_create一个线程去处理。线程方案可行但之后你很快会碰到新问题多个线程要同时往其他客户端发消息那块公共的客户端列表谁来加锁一个客户端退出对应的线程怎么清理这些坑在学生实验里几乎每个都会踩一遍。所以这个实验我更建议用select。select做的事很简单把一批文件描述符交给内核内核阻塞在那里等其中任意一个可读、可写或出错时返回然后你用FD_ISSET逐个检查是哪个fd有事件。它让单线程也能管理几十个连接代码量小答辩时还容易讲清楚。3.2 select最小核心代码fd_set必须在循环里重建下面这段是聊天室服务端的主体循环按常见做法缩略了错误处理关键路径都在#include sys/socket.h #include sys/select.h #include stdio.h #include stdlib.h #include unistd.h #include string.h #define MAX_CLIENT 64 int main() { int listen_fd socket(AF_INET, SOCK_STREAM, 0); int opt 1; // 允许重启后立即复用端口避免TIME_WAIT阻塞bind setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr {0}; addr.sin_family AF_INET; addr.sin_port htons(8888); addr.sin_addr.s_addr htonl(INADDR_ANY); bind(listen_fd, (struct sockaddr*)addr, sizeof(addr)); listen(listen_fd, 32); int clients[MAX_CLIENT]; for (int i 0; i MAX_CLIENT; i) clients[i] -1; fd_set readfds; while (1) { // 每次循环都要重新组装fd_set因为select会修改传入的集合 FD_ZERO(readfds); FD_SET(listen_fd, readfds); int maxfd listen_fd; for (int i 0; i MAX_CLIENT; i) { if (clients[i] 0) { FD_SET(clients[i], readfds); if (clients[i] maxfd) maxfd clients[i]; } } // 第一个参数必须传maxfd1不是maxfd int ret select(maxfd 1, readfds, NULL, NULL, NULL); if (ret 0) { perror(select); break; } // 监听socket可读说明有新连接进来 if (FD_ISSET(listen_fd, readfds)) { int fd accept(listen_fd, NULL, NULL); for (int i 0; i MAX_CLIENT; i) { if (clients[i] -1) { clients[i] fd; break; } } } // 逐个检查已有客户端哪个可读就收数据 for (int i 0; i MAX_CLIENT; i) { if (clients[i] 0 FD_ISSET(clients[i], readfds)) { char buf[1024]; int n recv(clients[i], buf, sizeof(buf) - 1, 0); if (n 0) { // 对方关闭或出错释放这个槽位 close(clients[i]); clients[i] -1; } else { buf[n] \0; // 这里做协议解析再广播给其他客户端 } } } } return 0; }这段代码有三个关键点。第一fd_set每次循环都要重新初始化并重新加入所有fd因为select返回时会把它修改成“只有就绪fd”的集合不复用旧集合就会导致后续轮询漏掉客户端。第二select第一个参数是maxfd加1不是maxfd这个加1是内核遍历fd表的上界写错会表现出“连接正常但事件永远等不到”的灵异现象。第三recv返回0表示对端正常关闭返回-1要检查errnoEINTR说明被信号打断重试即可ECONNRESET说明对端直接发了RST。Windows环境下写法稍有不同需要先WSAStartup头文件换成winsock2.hclose换成closesocket链接ws2_32库。select的语义一样传maxfd1在Windows下也能正常工作。4. 聊天室协议与缓冲区设计定义消息格式才能不翻车4.1 协议先于代码LOGIN、CHAT、LOGOUT三种消息服务端光有select还不够它接收到的是一堆字节得知道谁上线了、说的哪句话、问谁离开。所以动手写转发逻辑之前先定义一个最小文本协议。常见做法是竖线分隔第一段是消息类型后面是参数客户端 - 服务端: LOGIN|nickname CHAT|text content LOGOUT|nickname 服务端 - 客户端: SYSTEM|nickname 进入聊天室 CHAT|nickname|text content SYSTEM|nickname 离开聊天室为什么用竖线不用空格因为聊天内容里大概率有空格用空格分隔第一次解析就会翻车。竖线在正常输入里很少出现用来当分隔符足够安全。服务端在收到LOGIN时把“客户端fd→昵称”的映射记在数组里之后收到CHAT消息广播前把昵称拼上去其他客户端就知道这句话是谁说的。对应解析代码可以这样写// 假设buf里是一条完整消息以\0结尾 if (strncmp(buf, LOGIN|, 6) 0) { // 把昵称存到clients_nick[i]里i是当前客户端槽位下标 strncpy(clients_nick[i], buf 6, MAX_NICK - 1); // 广播一条SYSTEM消息给所有其他人 } if (strncmp(buf, CHAT|, 5) 0) { char packet[MAX_BUF]; snprintf(packet, sizeof(packet), CHAT|%s|%s, clients_nick[i], buf 5); // 遍历其他客户端逐个send(packet) }这个协议的好处是后续扩展特别方便。想加私聊加一种PRIV|目标昵称|内容就行想加表情包在CHAT消息里加一个类型字段。答辩时老师问“你这个聊天室还能做什么”你就能指着协议说再加一个消息类型就可以支持私聊。4.2 recv返回值藏着断线信号半包与粘包的判断标准TCP是字节流协议没有消息边界。你send两次“hello”对端recv一次可能同时读走两句话这叫粘包你send一句很长的消息对端recv一次可能只读到一半这叫半包。本地回环测试的时候通常感觉不到因为消息短、网络快但换到真实网络或者消息一长问题就集中爆发。应对方案在实验级别不复杂每条消息末尾加一个换行符\n服务端维护一个逐连接的接收缓存recv到数据先追加到缓存尾部然后用strstr或手动遍历按\n切割切出来的每一条完整消息再交给协议解析。这样半包会等到下一个\n补齐粘包会被切分成多条一次性处理干净。还有一个和缓冲区边界强相关的坑recv的返回值有三种语义。大于0是实际收到的字节数等于0表示对端正常关闭应该清理这个客户端槽位并广播离开消息小于0是出错大多数情况直接关闭连接。很多学生只在n0时处理数据忘记处理n0的情况结果客户端已经关掉聊天窗口服务端还保留着这个fd下次select一直认为它可读读到0又没清理最后整个循环卡住。UDP没有这个烦恼每个sendto对应一个数据报recvfrom原样收到完整报文自带消息边界。这也是为什么很多音视频实时传输选UDP的原因之一不需要在应用层处理粘包。5. 实验三避坑实录五个常见崩法和排查顺序网络编程实验的报错多少带点玄学色彩同一个代码在不同的机器上表现完全不一样。这里把我自己跑实验时踩过的坑按现象、原因、解决整理一遍每条都是血泪经验。5.1 bind提示Address already in useTIME_WAIT的坑现象服务端第一次运行正常CtrlC停掉之后马上重新启动bind直接报Address already in use程序退出。过两分钟再启动又好了。原因主动关闭连接的一端会进入TIME_WAIT状态持续大约2分钟。服务端CtrlC退出时所有已建立的连接都由服务端主动关闭所以服务端进入TIME_WAIT端口被占用bind无法复用。解决bind前先设置SO_REUSEADDRint opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));注意SO_REUSEADDR的作用是允许新连接的监听socket绑定到处于TIME_WAIT状态的端口它不能同时让两个正在监听的socket绑定同一个端口。5.2 中文消息乱码与回车换行丢失现象本机测试一切正常把服务端放到虚拟机里跑客户端在Windows上发中文另一端收到的是乱码。原因Windows控制台默认GBK编码Linux默认UTF-8编码两边字节序列不一致另外Windows下发送消息按回车会同时带\r\nLinux只认\n解析出的内容尾部会残留\r符号。解决统一使用UTF-8编码收发Windows下在代码开头调用setlocale(LC_ALL, )或者用Visual Studio的项目属性把字符集改成使用UTF-8接收端拿到数据后把末尾的\r和\n剥掉再解析。5.3 客户端一多服务端开始漏消息现象两个客户端聊天正常第三个一加入某人发的消息其他人时有时无地收不到。原因最常见的是fd_set使用错误。select会修改传入的fd_set只保留就绪的fd如果每次循环没有重新FD_ZERO和FD_SET而是在旧集合上继续FD_ISSET那后面轮询到的就永远是最初那一批fd新加入的客户端被漏掉。解决把组装fd_set的代码放在循环顶部每次select前重新执行一遍。这也是第3章代码里强调过的那句话出现漏消息时第一个查这里。5.4 客户端强退服务端直接退出终端显示Broken pipe现象用CtrlC强行关掉一个客户端窗口服务端进程没过几秒也跟着退出终端最后一行写着Broken pipe。原因对端关闭连接后服务端再向这个fd调用send内核会发送SIGPIPE信号该信号的默认动作是终止进程。也就是说你不是被逻辑干掉的是被信号干掉的。解决在服务端启动时屏蔽SIGPIPEsignal(SIGPIPE, SIG_IGN);同时send返回-1且errno为EPIPE时说明对端已经不在此时要主动关闭这个fd并清理槽位。只屏蔽信号不检查返回值还是会在后续逻辑里用到坏fd。5.5 在另一台机器上永远连不上监听地址与防火墙现象同一台机器上本机开服务端开客户端一切正常换到另一台电脑就卡在connect超时。原因先确认服务端绑定的IP是不是INADDR_ANY即0.0.0.0。如果绑定的是127.0.0.1那这个socket只能被本机访问外部机器根本连不上。再确认防火墙或路由器的入站规则是否放行了对应端口最后用ping确认两台设备链路通。排查顺序固定为先本机测、再IP通不通、再端口放行。很多人一上来查代码最后发现是防火墙设置把聊天室静默挡掉了。6. 验收前做一次半小时压测把日志和人数做成答辩加分项6.1 半小时验收清单先确认三种边界行为实验做完不要急着写报告先按下面这张表过一遍验收项操作方法通过标准多人同时在线本机开4个模拟客户端所有人都能收到任何一人的发言断线清理强制关闭一个客户端其他客户端收到离开系统消息服务端不崩重启复用CtrlC退出服务端后立刻重启bind不报Address already in use模拟客户端最简单的方式是用netcatLinux下一行命令nc 127.0.0.1 8888开四个终端就是四个客户端输入文字回车发送。Windows没有nc就写个十行的Python脚本用socket.create_connection连上之后循环读标准输入发送效果一样。6.2 能拉开差距的两个小功能报告答辩时真正能拉开差距的不是聊天功能本身而是两个小细节。第一是显示当前在线人数和昵称列表服务端在每次LOGIN和LOGOUT事件后都广播一条系统消息这个逻辑只加十几行代码但能让老师一眼看出你真的用select管理了所有连接。第二是消息带时间戳格式类似“20:31:05”实现方式就是发送前用time函数取一次本地时间拼进消息前缀。这两个功能都不影响主流程却在答辩时帮你多提供了一堆可讲的细节。验证代码时还有个小习惯供你参考打印每次send和recv的返回值哪怕只打印一个数字。网络程序最常见的翻车点不在逻辑而在你假定对方收到了而实际上对方早就关了。调试socket的这些年我一直保留这个习惯哪怕代码看起来再简单也会把返回值打出来因为TCP是一个号称可靠、但在应用层需要你认真对待每次调用的协议。希望这些思路和数据能帮到你也祝你这份实验做得比那个rar里的模板更值得写进简历。本文还有配套的精品资源点击获取