简介:一份用于计算机网络实验的套接字编程代码包,围绕 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.c | TCP 一对一 | 基础回显,服务端收数据处理后返回 |
| client1_bind / server1_bind | TCP 一对一 | 单独验证 bind 步骤的最小程序 |
| server1_1.3bind.c | TCP 一对一 | 服务端 bind 的 1.3 版改进 |
| client2.c / server2.c | TCP 一对多 | fork 多进程处理多个客户端 |
| server3.py / client3.py | UDP 多人聊天室 | 广播式聊天,无连接 |
我的习惯是先把client1.c和server1.c同时编译跑通一次,再去看 bind 变体。这一步能让你把所有报错都集中在网络概念上,而不是文件版本错乱上。
2.2 TCP 服务端的基础五步:socket、bind、listen、accept
TCP 服务端的代码骨架就是五件事:创建套接字、绑定地址、监听、接受连接、收发数据。下面这段是 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是实验中最关键的后悔药,没有它,服务端 Ctrl+C 结束后立刻重启,几乎必然报 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(f"UDP 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 会得到一长串b'hello'形式的字节字面量,客户端互相收发时类型错乱直接抛异常。
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(target=receive_loop, args=(sock,), daemon=True).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 实验翻车都翻在这里。
另一个注意点是接收线程设了daemon=True,这样主线程输入 exit 退出后,整个进程可以即时结束,不会被阻塞在 recvfrom 的线程里。很多人的 UDP 聊天室按 Ctrl+C 退不干净,就是没把线程设为守护线程。
3.4 广播语义与消息格式边界
实验 ppt 里常说的「向所有客户端广播」,在 UDP 代码里的准确含义是「服务端逐个向已知地址发一份拷贝」,这不是真正意义上的 IP 广播。如果你直接用255.255.255.255做广播地址,局域网里所有主机的所有进程都会收到,这种无差别发送在实验项目里一般不会用,因为它突破了进程边界,容易干扰其他主机的网络栈。服务端转发式广播更可控:客户端之间不是直连的,所有流量经过服务端集中转发,这也方便老师抓包检查。
消息长度上,UDP 单个报文建议控制在 1024 字节以内。实验里 recvfrom 缓冲设 1024,如果发一个 2KB 的消息,内核会截断,导致接收端拿到残缺数据且无法感知。真正要发大内容,就得自己拆包并给每个分片编号,这又是应用层的活了。
4. socket 实验常见问题排查:五个真能让人卡半天的坑
4.1 现象:bind 报 Address already in use
第一次运行服务端正常,Ctrl+C 结束进程,紧接着第二次运行就失败,报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 或 firewalld,Windows 下是入站规则。
排查顺序:先在服务端执行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 缓冲区加大后丢包率降低。这种改参数观察现象的方式,比重新读一遍代码管用得多。希望这三个验证步骤和改动练习能帮你在网络编程这条路上少走几步弯路。
本文还有配套的精品资源,点击获取