
简介一份用于计算机网络实验的套接字编程代码包围绕 TCP 与 UDP 两种传输层协议实现一对多聊天及多人聊天室功能适合高校学生配合课程实验理解网络编程原理。压缩包共 14 个文件包含 6 个 C 语言源文件、2 个 Python 脚本以及编译生成的服务器、客户端可执行文件整体仅 34KB轻量易部署。代码按任务划分成三个模块任务一演示面向连接的通信流程包括监听、连接建立与数据回传任务二通过多连接处理实现一对多并发聊天任务三基于无连接协议完成广播式多人聊天室并在关键环节加入异常捕捉增强程序健壮性。已有 1204 人学习下载对于希望掌握套接字编程基础、对比两类协议差异或参考多人聊天室实现的读者这份源码提供了可直接运行的示例与清晰的排错思路。1. 从代码包到多人聊天室这份 socket 实验资源到底能帮你解决什么如果你是正在赶计算机网络实验、需要在一周内把 TCP/UDP 从教材概念变成能跑通的代码这份「实验三 socket 编程代码」的资源可以说是直接照着抄的作业。task 1 用 C 语言实现 TCP 一对一通信task 2 在服务端引入多进程处理一对多连接task 3 用 Python 写 UDP 多人聊天室三个任务正好覆盖了 socket 编程最核心的三个场景可靠连接、并发处理和广播通信。整个实验的关键不是看懂某一个函数而是理解服务器端从 bind、listen 到 accept 的状态机以及 TCP 与 UDP 在代码写法上的本质差异。跑通这套代码你对网络编程的认知会比只看教材深刻得多。2. TCP 实验代码拆解从 bind 到 accept把一对一同构到一对多2.1 代码包里的文件关系一张表先说清拿到压缩包先别急着编译先把文件名对上号。task 1 里的client1.c和server1.c是一对一的 TCP 回显程序server1_1.3bind.c是服务端带 bind 显式绑定过程的变体client1_bind和server1_bind是早期验证 bind 步骤用的独立小版本适合单独编译看每一步返回值。task 2 里client2.c和server2.c则是一对多版本的客户端与服务端。task 3 全部换成 Python 文件server3.py是 UDP 聊天室服务端client3.py是聊天室客户端。文件版本作用client1.c / server1.cTCP 一对一基础回显服务端收数据处理后返回client1_bind / server1_bindTCP 一对一单独验证 bind 步骤的最小程序server1_1.3bind.cTCP 一对一服务端 bind 的 1.3 版改进client2.c / server2.cTCP 一对多fork 多进程处理多个客户端server3.py / client3.pyUDP 多人聊天室广播式聊天无连接我的习惯是先把client1.c和server1.c同时编译跑通一次再去看 bind 变体。这一步能让你把所有报错都集中在网络概念上而不是文件版本错乱上。2.2 TCP 服务端的基础五步socket、bind、listen、acceptTCP 服务端的代码骨架就是五件事创建套接字、绑定地址、监听、接受连接、收发数据。下面这段是 task 1 服务端的核心结构实验代码基本就是这个套路。#include stdio.h #include stdlib.h #include string.h #include unistd.h #include arpa/inet.h #define PORT 8888 #define BACKLOG 5 int main() { int server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd 0) { perror(socket); return 1; } struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_addr.s_addr INADDR_ANY; // 监听所有网卡地址 addr.sin_port htons(PORT); // 关键一行解决 bind 时 Address already in use int reuse 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, reuse, sizeof(reuse)); if (bind(server_fd, (struct sockaddr*)addr, sizeof(addr)) 0) { perror(bind); return 1; } if (listen(server_fd, BACKLOG) 0) { perror(listen); return 1; } printf(server listening on %d\n, PORT); struct sockaddr_in client_addr; socklen_t len sizeof(client_addr); int client_fd accept(server_fd, (struct sockaddr*)client_addr, len); if (client_fd 0) { perror(accept); return 1; } // 真正收发数据 char buf[1024]; while (1) { int n read(client_fd, buf, sizeof(buf) - 1); if (n 0) break; buf[n] \0; printf(client: %s\n, buf); // 逆序回传task1 的实验要求 for (int i 0; i n / 2; i) { char t buf[i]; buf[i] buf[n - 1 - i]; buf[n - 1 - i] t; } write(client_fd, buf, n); } close(client_fd); close(server_fd); return 0; }代码逻辑不复杂但三个细节决定了你能否跑通第一INADDR_ANY表示监听本机所有 IP而不是只监听 127.0.0.1这样同一台机器上的多客户端以及局域网内的其他机器都能连接第二htons(PORT)把端口从主机字节序转成网络字节序漏掉它端口就会对不上第三SO_REUSEADDR是实验中最关键的后悔药没有它服务端 CtrlC 结束后立刻重启几乎必然报 Address already in use。实验中要求逆序回传是 task 1 的教学重点服务端收hello回传olleh验证数据完整走了一圈。read返回 0 代表客户端关闭返回 -1 代表异常这两者都直接 break 退出循环。初学者最容易在这里糊弄过去——只处理正常路径不处理断开。实验可以跑但面试官一问连接断开你怎么感知就露馅了。2.3 一对多实现fork 多进程与循环 accept 的配合单次 accept 只能处理一个客户端第二次连进来的连接会一直排在队列里没人管。task 2 的做法是经典的 fork父进程负责 accept每来一个连接就 fork 一个子进程去处理父进程自己回到 accept 继续迎接下一个客户端。代码骨架如下。while (1) { int client_fd accept(server_fd, (struct sockaddr*)client_addr, len); if (client_fd 0) { perror(accept); continue; } pid_t pid fork(); if (pid 0) { // 子进程只处理当前这一个连接 close(server_fd); // 子进程不需要监听套接字 handle_client(client_fd); close(client_fd); exit(0); } else if (pid 0) { close(client_fd); // 父进程不处理业务关掉连接描述符 } }这里的两个close是必须理解的细节。fork 之后父子进程共享全部文件描述符如果不主动关闭不需要的那一端连接描述符和监听描述符的引用计数就永远是 2导致资源泄漏甚至无法正常断开连接。子进程里close(server_fd)、父进程里close(client_fd)这一对操作是并发版本的灵魂。实验代码里没有做僵尸进程回收但这不怪它——教学实验一般不要求你处理 SIGCHLD。真到工程里signal(SIGCHLD, SIG_IGN)或waitpid是必须的不然跑半天背后全是僵尸进程。如果 task 2 你发现客户端连上去收发一次后服务器响应变慢最可能的原因就是这个。另一种常见实现是多线程每个连接 new 一个 pthread但 C 语言的线程同步和共享状态处理起来比 fork 更麻烦实验课用 fork 是为了让你绕过锁的复杂度专心感受并发模型。2.4 关于 listen 的 backlog 与并发上限实验代码里BACKLOG通常设成 5 或 10这是内核允许排队的连接数量。超过这个数字的 connect 请求会直接失败客户端报 Connection refused。很多初学者的困惑就在这里我已经 fork 了为什么第 11 个客户端连不进来答案是 backlog 限制了排队长度而不是连接总数。半连接队列加满后新连接在内核层面就被丢掉了。真实服务端不会只靠 backlog 撑并发而是用事件驱动模型这是后话。但实验阶段把 backlog 调大到 32 再配合 fork课上演示二十个客户端同时连绰绰有余。3. UDP 多人聊天室无连接协议在 Python 里的落地方式3.1 TCP 与 UDP 的差异直接表现为代码形态差异TCP 和 UDP 的区别不只是教材里的那张对比表代码写法上完全是两套逻辑。TCP 要先 connect 建立连接然后基于连接读写UDP 压根没有连接这回事每个报文都是独立的你只需要告诉内核目标地址直接 sendto。服务端也不用 listen 和 accept因为根本没有「连接」需要接受。task 3 的聊天室能做成广播正是利用了 UDP 无连接的特性服务端收回报文再把它群发给所有已知的客户端地址。UDP 不保证不丢包、不保证有序实验里你可能会看到消息偶尔乱序、甚至丢一条。这不是代码 bug是协议本性。实验能接受的边界是「大部分消息能到」如果你需要确定性就得在应用层做序号和重传那相当于在 UDP 之上重新发明一个 TCP作业里不需要。3.2 UDP 服务端一个死循环加一个地址集合task 3 服务端的核心逻辑非常短维护一个客户端地址集合收到数据就广播给集合里的所有人。下面是典型的实验版实现。import socket SERVER_HOST 0.0.0.0 SERVER_PORT 9999 server socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server.bind((SERVER_HOST, SERVER_PORT)) clients set() # 保存每个客户端的 (ip, port) 地址元组 print(fUDP chat server listening on {SERVER_PORT}) while True: try: data, addr server.recvfrom(1024) # 收一次消息addr 是发送方地址 clients.add(addr) # 新地址自动被记录 message data.decode(utf-8) print(f[{addr[0]}:{addr[1]}] {message}) # 广播给其他所有客户端 for client in clients: if client ! addr: server.sendto(data, client) except KeyboardInterrupt: break server.close()代码只有两个关键点。第一个是recvfrom的返回值data是字节串addr是发送方的 IP 和端口元组你必须把 addr 存下来才能回发。第二个是clients集合加一个新客户端的方式——第一次收到它的消息就自动加入没有任何握手流程。这是 UDP 服务端与 TCP 服务端最大的思维差异TCP 里建立连接是一个显式动作UDP 里「知道你是谁」就藏在收包的过程里。data.decode(utf-8)是 Python 3 的硬性要求socket 收发的是 bytes 不是 str。实验里最常见的炸点就是这里忘记 decode 直接 print 会得到一长串bhello形式的字节字面量客户端互相收发时类型错乱直接抛异常。3.3 UDP 客户端发送和接收必须拆成两个独立通道客户端的难点在于如果只有一个 while 循环反复 recvfrom用户压根没法一边输入一边收消息。常见做法是把收消息放进后台线程主线程只负责读键盘输入并 sendto。下面是 task 3 客户端的基本结构。import socket import threading SERVER_ADDR (127.0.0.1, 9999) def receive_loop(sock): while True: try: data, _ sock.recvfrom(1024) print(data.decode(utf-8)) except OSError: break sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind((0.0.0.0, 0)) # 本地随机端口让服务端能回包 threading.Thread(targetreceive_loop, args(sock,), daemonTrue).start() while True: msg input() if msg exit: break sock.sendto(msg.encode(utf-8), SERVER_ADDR) sock.close()这里务必注意那行sock.bind((0.0.0.0, 0))。UDP 客户端如果不 bind 固定端口系统会给它分配一个临时端口这个临时端口就是你注册到服务端的身份凭证。忽略 bind 直接 sendto你发的第一包消息会被服务端记下 addr服务端能收到你的消息但你收不到任何回包——因为本地 socket 没有绑定端口时系统无法把收到的数据路由到你的 socket 上。这属于血泪坑大部分 UDP 实验翻车都翻在这里。另一个注意点是接收线程设了daemonTrue这样主线程输入 exit 退出后整个进程可以即时结束不会被阻塞在 recvfrom 的线程里。很多人的 UDP 聊天室按 CtrlC 退不干净就是没把线程设为守护线程。3.4 广播语义与消息格式边界实验 ppt 里常说的「向所有客户端广播」在 UDP 代码里的准确含义是「服务端逐个向已知地址发一份拷贝」这不是真正意义上的 IP 广播。如果你直接用255.255.255.255做广播地址局域网里所有主机的所有进程都会收到这种无差别发送在实验项目里一般不会用因为它突破了进程边界容易干扰其他主机的网络栈。服务端转发式广播更可控客户端之间不是直连的所有流量经过服务端集中转发这也方便老师抓包检查。消息长度上UDP 单个报文建议控制在 1024 字节以内。实验里 recvfrom 缓冲设 1024如果发一个 2KB 的消息内核会截断导致接收端拿到残缺数据且无法感知。真正要发大内容就得自己拆包并给每个分片编号这又是应用层的活了。4. socket 实验常见问题排查五个真能让人卡半天的坑4.1 现象bind 报 Address already in use第一次运行服务端正常CtrlC 结束进程紧接着第二次运行就失败报bind: Address already in use。原因TCP 连接断开后服务端进入 TIME_WAIT 状态端口在短时间内不会被释放。TIME_WAIT 的持续时间通常为 2MSL也就是大约一到两分钟这是操作系统为了确保旧的延迟报文不会污染新连接而设计的。实验里频繁启停服务端必然撞上这个窗口。解决代码里在socket()之后、bind()之前加上setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, reuse, sizeof(reuse))。这一行能允许内核把 TIME_WAIT 状态的端口重新分配给你。我见过不少人翻车就翻在这一步——代码没问题就是没加这一行重启即原地死亡。4.2 现象TCP 服务器只能处理一个客户端第二个客户端连上后毫无反应客户端 connect 返回成功但数据发出去石沉大海服务端也打印不出任何消息。原因服务端在一个 client_fd 上做死循环 read处理完第一个连接就阻塞在读循环里永远不会执行第二次 accept。第二个客户端的连接虽然在内核里排队成功但用户空间没有任何代码去接受它。解决把 accept 放进外层 while 循环每次接受新连接后立刻 fork 子进程去处理。代码结构参照 2.3 节核心是「父进程不碰业务子进程不碰监听」。调试时可以用netstat -anp | grep 8888确认第二个连接是否处于 ESTABLISHED 状态——一旦确认连接在内核层面已经建立问题就锁定在你的用户态代码没有 accept而不是网络配置问题。4.3 现象UDP 聊天室消息偶尔丢失或乱序看起来像灵异事件同一个局域网里A 发言 B 偶尔听不到或者消息到达顺序和发送顺序不一致。原因UDP 协议本身不保证可靠和有序。丢包可能发生在交换机队列溢出也可能发生在接收端 socket 缓冲区满。乱序则是 IP 网络路由多路径的常规行为UDP 报文走了不同链路。解决不要试图在实验层面修复它而是要确认这是协议行为而非代码缺陷。验证方法是让一个客户端连续发送 1000 条带序号的短消息服务端统计实际收到的数量和顺序。如果收到的数量接近 1000 且乱序率低于 0.5%代码就没问题。工程方案是应用层加序号与重传机制但实验报告里你只需要如实记录这一现象并解释其原理这反而是加分项。4.4 现象客户端在另一台机器上连不上服务端但本机测试正常本机用 127.0.0.1 连接一切正常换到局域网另一台电脑就连不上。现象通常是 connect 超时或直接拒绝。原因原因有两个挨个查。第一个是服务端 bind 到了 127.0.0.1只监听回环地址外部请求根本进不来第二个是系统防火墙拦截了对应端口Linux 下可能是 iptables 或 firewalldWindows 下是入站规则。排查顺序先在服务端执行ss -tlnp | grep 8888确认监听地址是 0.0.0.0 而不是 127.0.0.1再临时关闭防火墙测一遍确认通后再把端口加白名单。别一上来就改代码先分清是哪一层的拦截。4.5 现象Python 聊天室收发中文消息乱码或直接崩溃客户端输入中文服务端打印出乱码甚至直接抛UnicodeEncodeError。原因Python 3 的 str 需要显式编码为 bytes编码方式不一致一个 UTF-8 一个 GBK就会乱码。C 语言实验不会遇到这个问题因为 char 数组存的本来就是原始字节但 Python 的抽象层级更高编码问题避不开。解决统一在 sendto 前用utf-8编码在 recvfrom 后解码。注意代码里不要混用encode()和decode()的默认参数——它会用系统默认编码Linux 是 UTF-8 没问题Windows 下默认可能是 GBK跨机器跑就必炸。显式写上utf-8是唯一保险的做法。5. 复现验收与三个进阶改动跑通只是第一步拿到代码包按下面的顺序复现一遍每个任务都确认关键里程碑才算真的拿到了这份资源的价值。第一步是 TCP 一对一终端 1 编译并启动server1.c终端 2 编译并启动client1.c在客户端输入hello等待服务端返回olleh。这一步验证的是 socket 五步流程和读写闭环。第二步是 TCP 一对多同时启动三个客户端分别连server2每个客户端发不同内容确认服务端全部都能回复且互不干扰。第三步是 UDP 聊天室一个终端跑server3.py另外开两个终端跑client3.py任意一端发消息另外一端能收到。验收之后给你三个依次递进的改动方向。第一个是给 TCP 服务端增加最大连接数限制在 fork 之前用getpid()记录已创建的子进程数超过阈值直接返回错误包。第二个是把 UDP 客户端接收线程改为可退出模式定义一个全局running标志主线程退出前置为 False接收线程用select.select配合超时循环轮询让程序干净退出。第三个是给 UDP 聊天室增加在线人数通知服务端维护clients集合每次有新人加入时遍历集合发送一条系统消息这个改动难度不大但能让你彻底掌握地址集合管理的边界情况。做完这三个改动这份实验代码就被拆解消化成你自己的东西了。我自己的习惯是每次跑通新代码都复制一个干净备份版本然后随机改一个参数观察行为变化。你可能会发现把 backlog 从 5 改成 128 后客户端排队明显变快也可能会发现 UDP 缓冲区加大后丢包率降低。这种改参数观察现象的方式比重新读一遍代码管用得多。希望这三个验证步骤和改动练习能帮你在网络编程这条路上少走几步弯路。本文还有配套的精品资源点击获取