简介:这是一份面向计算机网络实验的Socket编程代码包,聚焦TCP/UDP协议,覆盖一对一、一对多及多人聊天室三种典型通信场景,适合正在学习网络编程或需要完成课程设计的学生参考,也可作为计算机网络课程实验报告的辅助材料。压缩包共14个文件,以6个C语言源码和2个Python脚本为主体,另有编译生成的服务端与客户端可执行文件,目录结构清晰,整体仅34KB,轻巧易用;已有1204人浏览学习。代码演示了TCP面向连接通信中服务器监听、客户端连接请求处理、数据逆序回传等关键流程,也通过UDP广播机制实现无需持久连接的多人聊天室,并加入异常捕获处理以优雅应对网络中断,同时体现了多客户端并发处理思路。通过学习这些代码,可深入理解Socket API在多用户交互场景下的实际运用,为后续开发更复杂的网络应用打下坚实基础。
1. 实验三 Socket 编程代码拆解:TCP 一对一/一对多与 UDP 广播聊天室的完整实现
计算机网络实验里,Socket 编程这道坎几乎人人都踩过。我拆过不少学生提交的实验三源码包,最常见的情况是:报告写得头头是道,代码一跑就崩——不是 bind 失败就是 recv 阻塞,要么 UDP 聊天室里消息发出去收不到。这次拿到的资源包包含 C 和 Python 两套实现,C 版覆盖 TCP 一对其逆序回显和一对多并发处理,Python 版实现 UDP 广播聊天室,代码量不大但覆盖了 socket 编程最核心的几个知识点:socket 创建、bind 绑定、listen 监听、accept 接受连接、recv/send 数据收发、select 或多线程处理并发、UDP 广播收发。实际场景里无论是应付课程设计、准备网络编程面试,还是想搞懂 TCP 和 UDP 在代码层面的真正差异,这套代码都值得逐个文件过一遍。下面按我的拆解习惯,从 C 源码开始,再进入 Python UDP 实现,最后讲清楚那些不跑一遍根本发现不了的坑。
2. 先把资源包的文件结构和代码跑起来:从 TCP 一对一逆序回显开始
2.1 实验包文件清单与编译环境准备
拿到压缩包解开后,会看到三个 task 目录,对应实验的三个阶段。task 1 里是 client1.c、server1.c 以及带 bind 后缀的变体,是 TCP 一对一通信的 C 代码;task 2 里是 client2.c、server2.c,对应 TCP 一对多并发处理;task 3 里是 server3.py、client3.py,用 Python 实现了 UDP 广播多人聊天室。
我一般先看 C 代码的编译条件。这份代码没有用任何第三方库,纯 POSIX socket API,编译命令就是最常见的 gcc 一行:
gcc server1.c -o server1 -Wall gcc client1.c -o client1 -Wall这里说明一下,-Wall 打开编译警告,对于学习 socket 编程足够用了。代码里用到的头文件是 sys/socket.h、netinet/in.h、arpa/inet.h、unistd.h,全部是 Linux 系统自带的。如果你拿到的是带 bind 后缀的版本,比如 server1_1.3bind.c,那是同一个程序的迭代版本,改的是 bind 地址和端口处理部分,核心 socket 流程不变。
编译环境推荐 Ubuntu 或 CentOS 这类 Linux 发行版,Windows 上用 WSL 也可以,但要注意 Windows 原生 socket 编程是 Winsock API,需要 WSAStartup 初始化,和这份代码差异很大,不建议硬搬。
2.2 TCP 一对一流程:socket、bind、listen、accept、recv、send
task 1 的 server1.c 是整个实验的地基。它的逻辑非常标准:创建套接字、绑定端口、进入监听状态、接受一个客户端连接、接收数据做逆序处理再回传。我在代码里标注几个关键行:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main() { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); char buffer[1024]; // 创建 TCP 套接字,AF_INET 表示 IPv4,SOCK_STREAM 表示面向流 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket"); return 1; } // 绑定端口 8888,INADDR_ANY 表示监听所有网卡 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8888); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); return 1; } // 进入监听状态,backlog 设为 5 if (listen(server_fd, 5) < 0) { perror("listen"); return 1; } // 接受单个客户端连接 client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("accept"); return 1; } // 接收客户端消息并逆序回传 while (1) { int n = recv(client_fd, buffer, sizeof(buffer) - 1, 0); if (n <= 0) break; buffer[n] = '\0'; printf("收到: %s\n", buffer); // 字符串逆序 int len = strlen(buffer); for (int i = 0; i < len / 2; i++) { char tmp = buffer[i]; buffer[i] = buffer[len - 1 - i]; buffer[len - 1 - i] = tmp; } send(client_fd, buffer, len, 0); } close(client_fd); close(server_fd); return 0; }几个参数值得展开说。sin_port 用 htons 把主机字节序转成网络字节序,端口 8888 是实验代码里选的一个高位端口,避开 1024 以下的特权端口,这样普通用户就能 bind。htonl(INADDR_ANY) 让服务器监听所有网卡地址,如果只想监听某个具体 IP,比如本机 192.168.1.10,就把 htonl(INADDR_ANY) 换成 inet_addr("192.168.1.10")。backlog 参数是连接等待队列长度,5 在实验场景够用,生产环境一般会调到 128 左右。
recv 的第四个参数 flags 传 0,表示阻塞方式读取。buffer 大小 1024 字节,网络编程里缓冲区尺寸是个权衡点:太小会频繁触发系统调用,太大浪费内存。这个实验场景 1024 完全足够。
2.3 client.c 的对应实现与启动顺序
client1.c 的逻辑更简单,创建 socket 后直接 connect 到服务器地址,然后循环发消息收消息:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> int main() { int sock_fd; struct sockaddr_in server_addr; char buffer[1024]; sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("socket"); return 1; } // 连接 127.0.0.1:8888,即本机服务器 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(8888); server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); if (connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) { perror("connect"); return 1; } while (1) { printf("请输入消息: "); fgets(buffer, sizeof(buffer), stdin); buffer[strcspn(buffer, "\n")] = '\0'; send(sock_fd, buffer, strlen(buffer), 0); int n = recv(sock_fd, buffer, sizeof(buffer) - 1, 0); if (n <= 0) break; buffer[n] = '\0'; printf("服务器回显: %s\n", buffer); } close(sock_fd); return 0; }connect 是在 TCP 三次握手完成后才返回的,所以只要 connect 成功,连接就是可用的。inet_addr("127.0.0.1") 把点分十进制字符串转成网络字节序的 32 位整数,这是 C socket 编程里最常用的地址转换函数。
启动顺序必须是先起服务器再起客户端,因为客户端 connect 时如果服务器还没有 listen,会直接返回 ECONNREFUSED——连接被拒绝。这个顺序搞反了是新手最常见的第一坑。
2.4 一对一变一对多:task 2 代码里的并发思路
task 2 的 server2.c 是实验的重点——一对多聊天服务。这里有两个实现路线:一个是循环 accept + 每次 accept 后 fork 子进程去处理客户端,另一个是单线程里用 select 或 poll 多路复用。实验代码里用的是循环加子进程方式,代码结构简单,但能看出设计思路:
while (1) { client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len); if (client_fd < 0) { perror("accept"); continue; } // 子进程处理客户端通信,父进程继续 accept 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); // 父进程不需要连接套接字 } }这里必须理解 fork 之后父子进程各自拥有的文件描述符是什么。fork 会把调用进程的整个地址空间复制一份,包括打开的文件描述符表。所以子进程里要 close(server_fd) 关闭自己那份监听套接字,父进程要 close(client_fd) 关闭自己那份连接套接字。各关各的,真正的资源释放靠内核的引用计数机制——只有当所有进程都把某个 fd 关闭,内核才会真正释放这个 socket。这个点如果搞混了,会看到大量 TIME_WAIT 连接堆积。
3. TCP 一对多并发处理:fork 进程模型与端口占用排查
3.1 为什么一对多必须做并发
task 1 到 task 2 的本质差别是:task 1 的 accept 在循环外面,程序 accept 一次就进入 while 循环处理这一个客户端的数据,处理完再 accept 下一个。这意味着如果第一个客户端连上来之后不发数据,服务器就永远阻塞在 recv 上,其他客户端连不进来。这就是 TCP 一对多要引入并发的原因。
fork 进程模型是最直接的做法。每个客户端一个子进程,父进程继续 accept 新的连接。这样做的好处是代码逻辑清晰,每个子进程处理一个客户端,不需要考虑多个客户端共享状态的同步问题。代价是进程开销大,客户端多了之后性能扛不住。对于计算机网络实验来说,这个取舍完全够用,你要真正搞懂的是 fork 之后描述符的归属问题。
选 fork 而不是 pthread 还有一个原因:历史上很多 Linux 教材的网络编程章节用 fork 做例子,实验代码自然照搬。pthread 多线程模型需要处理共享内存的互斥,实验报告里涉及并发和同步两个考点,fork 只需考并发。但从代码可维护性看,pthread 更轻量,fork 进程间数据隔离更彻底,各有利弊。
3.2 多客户端连接时的 fd 生命周期
我用一个实际场景说明这个模型怎么工作。假设三个客户端 A、B、C 先后连接服务器:
- 服务器启动,socket 得到 fd 3,bind 后 listen
- 客户端 A connect,服务器 accept 返回 fd 4,然后 fork
- 子进程 1 持有 fd 4,父进程 close(fd 4) 后继续 accept
- 客户端 B connect,父进程 accept 返回 fd 5,fork 出子进程 2,父进程 close(fd 5)
- 客户端 C 同理,得到子进程 3 处理
三个子进程各自用自己的 fd 4/5/6 与客户端通信,互不干扰。父进程手里只有监听 fd 3。这里有个关键点:每个子进程虽然只处理一个客户端,但 fork 复制出来的描述符表里最初包含所有 fd,所以必须在子进程里显式 close 掉不用的。我在上面代码里已经标出来了。
假设代码里漏了 close(server_fd),子进程不退出,父进程也 close 不掉监听套接字——事实上如果父进程 close 了自己的那份,但子进程还持有一份副本,监听 socket 不会真正关闭,端口会一直保持占用状态。这就是很多人实验做完了,服务器进程退出了,再启动新服务器却报 Address already in use 的原因。真正释放端口需要等所有子进程退出,或者显式设置 SO_REUSEADDR。
3.3 实验里的循环接收陷阱
很多提交的代码里,server2.c 是通过循环接收来处理多个客户端的,类似这样:
while (1) { int n = recv(client_fd, buffer, sizeof(buffer) - 1, 0); ... }这个写法本身没问题,前提是一次 accept 之后只管一个客户端。但如果想用单循环遍历多个客户端,没有 select 或者非阻塞 socket 根本做不到。原因很简单:recv 是阻塞调用,如果客户端 A 不发数据,recv(fd_A) 就一直不返回,后面的客户端 B、C 即使有数据到达也没机会被 recv 到。这就是为什么实验指导书里会要求用 fork 或者 select。从验收角度,只跑到一对一就交差的学生不少,但想要高分,一对多的并发处理是必须展示的。
3.4 TCP 调试必备工具与端口状态观察
跑 TCP 实验时,我建议你每一步都通过 netstat 观察系统状态:
netstat -tlnp | grep 8888输出会显示监听套接字的状态是 LISTEN。客户端 connect 成功后,服务端多出一个 ESTABLISHED 连接。如果客户端主动断开但代码里没处理,会看到 CLOSE_WAIT;如果服务器关闭但客户端没退出,客户端那边会看到 FIN_WAIT_2。这些都是排查故障的线索。
对于 fork 多进程模型,用 ps 看进程树更直观:
ps -ef | grep server2正常情况会看到一个父进程加多个子进程,每个子进程对应一个客户端。如果只看到父进程,说明代码可能没有 fork,或者 fork 失败。配合 strace 可以看到系统调用层面的阻塞位置:
strace -p <pid> -e trace=accept,recv,send在排查「为什么客户端连不上」或者「为什么数据没发出去」时,这是最快的定位手段。
4. UDP 多人聊天室:Python 实现与广播机制分析
4.1 TCP 与 UDP 的核心差异在代码上的体现
task 3 换到了 Python,实现 UDP 多人聊天室。UDP 最核心的特性是无连接,所以 Python 代码里不会出现 listen 和 accept,也没有 connect——对于 UDP,sendto 直接指定目标地址就行。另一个关键差异是 recvfrom 返回两个值:数据和发送方地址。这个地址就是回信时 sendto 的第二参数。
对比 TCP 的 recv/send,你会发现 UDP 的 socket API 设计更简单。这是因为 UDP 的语义就是「发一个数据报出去,能不能到、顺序对不对、有没有重复,都不保证」。实验说明里提到 UDP 不可靠、无连接、低延迟,全部体现在 API 层面。可靠性的缺失意味着如果聊天室里有人消息丢了,UDP 层不会重传,这要在应用层自己处理。
4.2 UDP 聊天室代码逐段拆解
server3.py 的核心逻辑非常短,通常长这样:
import socket # AF_INET 指定 IPv4,SOCK_DGRAM 指定 UDP server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind(('0.0.0.0', 8888)) clients = set() # 保存所有客户端地址 print("UDP 聊天室启动,监听 8888 端口") while True: try: # recvfrom 返回数据和来源地址 data, addr = server_socket.recvfrom(1024) message = data.decode('utf-8') print(f"{addr} 说: {message}") if addr not in clients: clients.add(addr) # 新客户端上线 # 广播给所有客户端 for client in clients: if client != addr: server_socket.sendto(data, client) except KeyboardInterrupt: print("服务器关闭") break server_socket.close()这段代码有几个关键点。setsockopt(SO_REUSEADDR) 允许端口重用,否则服务器上次运行时留下的 TIME_WAIT 状态会导致 bind 失败。clients 集合保存着所有上线客户端的地址,每收到一条消息就遍历所有人广播——这就是 UDP 聊天室能「一对多」的机制。注意广播时排除发送者本人,否则会把自己发的消息原样回传。
client3.py 的逻辑也一样清晰:
import socket import threading client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定固定端口,方便服务器识别不同用户 client_socket.bind(('0.0.0.0', 8889)) server_addr = ('127.0.0.1', 8888) def receive_messages(): while True: try: data, addr = client_socket.recvfrom(1024) print(data.decode('utf-8')) except: break # 启动接收线程,否则 recvfrom 会阻塞主线程 threading.Thread(target=receive_messages, daemon=True).start() while True: message = input() client_socket.sendto(message.encode('utf-8'), server_addr)客户端绑定 8889 端口,这样服务器通过 recvfrom 拿到的 addr 就是 (IP, 8889),能在识别不同客户端时区分来源。接收消息单独开一个线程,因为 recvfrom 是阻塞的,如果放在主线程,用户输入消息时无法同时接收别人的消息。
4.3 UDP 广播与端口绑定解读
这段代码里最容易被误解的是「广播」这个词。严格说,这里实现的是「向所有已上线客户端循环发送消息」,不是网络层的广播地址(255.255.255.255)。网络层广播需要 setsockopt(SO_BROADCAST) 并且发送到 255.255.255.255,而实验代码用的是应用层遍历。
两者的区别很重要:网络层广播是发到局域网里所有主机的某个端口,不管那个主机有没有聊天室客户端在跑;实验代码的应用层广播只发给已经用相同客户端程序上线的人,数据不会打扰局域网其他设备。做实验报告时建议明确写清楚这一点,是自己实现的逻辑广播而非套接字选项广播。
聊天室场景对消息丢失不敏感,因为每条消息都是独立广播,发出去就完事。考虑到 UDP 本身的不可靠性,如果哪条消息丢了,聊天室不会像 TCP 那样自动重传。实验代码里没有做可靠性补偿,这没问题,实验目的本来就是展示 UDP 的语义而不是实现可靠 UDP。真要可靠聊天室,就得自己加序号和 ACK,复杂度立刻上去。
4.4 多客户端模拟方法
跑 UDP 聊天室,最好的方式是开多个终端窗口,每个窗口跑一个 client3.py。注意每个客户端必须用不同的端口,否则无法区分。如果只能在一台机器上测试,多开终端即可,不需要虚拟机。如果想在同一台机器模拟局域网多设备,用 127.0.0.1 就够了,因为 socket 不关心地址是回环还是真实网卡 IP,只要端口不同即可。
实操时我习惯开四个终端:一个跑 server3.py,三个跑 client3.py,每个客户端绑定不同端口。改端口的方式是在代码里改 bind 那一行的 8889 为 8890、8891 等。客户端收到的广播消息会显示「来自 (127.0.0.1, 8889)」,就能直观看到哪个客户端在发言。
5. 避坑手册:bind 失败、recv 阻塞、TIME_WAIT 与 UDP 收不到消息的排查
5.1 bind 报 Address already in use
现象:第二次运行服务器时报 bind: Address already in use,或者服务器自己挂了再重启也报错。
原因:上一次运行的服务器进程没有完全退出,或者连接关闭后端口仍处于 TIME_WAIT 状态。TIME_WAIT 是 TCP 主动关闭方必须停留的状态,持续约 2MSL(约 1 到 4 分钟)。实验反复改代码重编译重跑,频繁触发这个状态很正常。另外一个可能原因是前一次程序用 Ctrl+Z 挂起而不是正常退出,进程还在后台占着端口。
解决:先看进程是否真正退干净。ps -ef | grep server2如果还有残留,kill 掉。然后代码里加上 SO_REUSEADDR:
int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));注意 SO_REUSEADDR 不是「允许两个进程同时 bind 同一个端口」,而是「允许端口在 TIME_WAIT 状态下被重新绑定」。它对这些残留问题基本都能解决,但两个进程同时 bind 同一个端口依然不行——那不是 SO_REUSEADDR 的职责范围。
5.2 客户端 connect 被拒绝 ECONNREFUSED
现象:客户端先跑,connect 返回 Connection refused。
原因:服务器没有启动、服务器绑定端口和客户端连接端口不一致、或者服务器处于非 LISTEN 状态。
解决:顺序问题最好解决——先启动服务器确认进入 accept 循环,再启动客户端。端口一致性用 netstat -tlnp 查看服务器监听的端口号,确认和代码里 htons 的目标端口一致。我在多台机器上联调时经常发现端口不一致是因为代码里写死了端口常量,但两台机器的代码版本不同,一个改过端口另一个没改。所以联调之前先统一代码版本。
5.3 recv 返回 0 导致程序卡死
现象:客户端断开了连接,服务器还在循环 recv,没有任何反应或者出现奇怪行为。
原因:recv 有三种返回值需要区分:大于 0 是收到数据;等于 0 是对方正常关闭——对端发送了 FIN 并完成了四次挥手;小于 0 是出错。很多同学只处理小于 0 的情况,忽略了等于 0。
解决:对返回 0 的情况必须 break 并关闭套接字。代码里加上:
if (n == 0) { printf("客户端断开连接\n"); break; }如果不处理,服务器会一直卡在 recv 上。对于非阻塞 socket 还要额外处理 EAGAIN,但实验代码是阻塞模型,不需要处理。
5.4 客户端能连上但服务器收不到数据
现象:客户端 send 正常返回值,但服务器端没有打印任何消息。
原因:一种情况是客户端和服务器不在同一台机器,客户端连了错误的 IP。代码里默认 inet_addr("127.0.0.1") 只在本机有效,跨机器必须改成服务器的实际 IP。另一种情况是数据还在缓冲区里,send 只是把数据复制到内核缓冲区,真正的发送由内核完成——如果服务器进程没有调用 recv,数据就一直在内核队列里堆积。
解决:先用 ping 确认两台机器网络通。再看客户端 connect 的 IP 是否是正确的服务器地址。最后确认服务器在对应网卡监听——绑定 0.0.0.0 或 INADDR_ANY 时监听所有网卡,绑定具体 IP 时只监听那个网卡。我排查这类问题时会用 tcpdump 抓包:
tcpdump -i any port 8888抓包能立刻看出 SYN、ACK、数据包的流向,比干看代码快得多。
5.5 UDP 聊天室的新用户加入前丢了前面所有消息
现象:新客户端加入聊天室后,只能看到加入之后其他人发的消息,之前的聊天记录完全看不到。
原因:这不是 Bug,而是 UDP 无状态特性的直接后果。服务器端保存的 clients 集合只记录当前在线客户端,新用户加入时服务器不会把历史消息补发给新人。TCP 聊天室里可能有历史记录同步的逻辑,但 UDP 代码默认不保存历史。
解决:如果实验要求展示「所有消息对所有客户端可见」,可以在服务器端维护一个消息列表,新客户端加入时把历史消息逐个发给它。这个逻辑很好加,在 clients.add(addr) 后面遍历历史消息列表发送即可。代码改动量很小,但能作为实验报告里的「功能扩展」亮点。
5.6 端口 8888 在 Windows 上被系统进程占用
现象:Windows 机器上跑服务器 bind 8888 失败,netstat 显示端口被某个 PID 占着,但那个进程看起来不像是自己的程序。
原因:Windows 系统服务可能占用某些端口,尤其是一些云服务控制台或者 Hyper-V 保留端口范围。之前遇到过 Hyper-V 动态端口范围包含 8888,导致用户态程序无法绑定。
解决:换一个端口,比如 6666 或 9999。代码里把 server_addr.sin_port 和客户端 connect 的端口同步改掉。如果必须用 8888,可以用 PowerShell 查看保留端口范围:
netsh int ipv4 show excludedportrange protocol=tcp然后在保留范围之外选端口。实验代码对端口没有硬性要求,只要客户服务器统一即可。
5.7 fork 后僵尸进程问题
现象:跑了一段时间 task 2 之后,ps -ef里看到很多<defunct>僵尸进程。
原因:子进程退出时,父进程没有调用 wait/waitpid 回收子进程状态。子进程先变成僵尸态,等待父进程收取退出状态。实验里客户端反复断开重连,子进程退出频繁,僵尸进程就堆积起来。
解决:在父进程循环里增加信号回调,或者用 waitpid(pid, NULL, WNOHANG) 非阻塞回收:
signal(SIGCHLD, SIG_IGN);或者:
while (waitpid(-1, NULL, WNOHANG) > 0);signal(SIGCHLD, SIG_IGN) 是操作系统层面直接丢弃子进程退出状态,最简单。waitpid 循环可以避免 SIG_IGN 导致某些版本下的问题,但需要小心放在哪里调用。这个坑在实验验收时经常被问到,代码里没有处理的话建议补上。
6. 从实验代码到独立实现:用 gobuster 思路验证多路复用与性能边界
实验代码提供的 C 和 Python 实现各有侧重点,但它的边界也很明确:C 版本用的是阻塞式 socket 和 fork 并发模型,Python 版本用的是阻塞式 recvfrom 加纯遍历广播。这些模型在实验环境没问题,但并发量上到几百个客户端时就要换思路了。
我自己验证这份代码价值的方法是:先把 TCP 一对一改成带日志的时间戳版,然后压测。压测工具用 Python 写个简单脚本最方便,不用装额外依赖。下面是我常用的一个压力测试思路:
import socket import time import threading def tcp_test(server_ip, server_port, message_count): # 每轮测试创建一个新连接发送多条消息 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.settimeout(5) client.connect((server_ip, server_port)) total_time = 0 for i in range(message_count): payload = f"msg_{i}".encode() start = time.time() client.send(payload) resp = client.recv(1024) total_time += time.time() - start client.close() return total_time # 起 100 个线程并发连接,观察服务器的 accept 和 fork 表现 threads = [] results = [] for i in range(100): t = threading.Thread(target=lambda i=i: results.append(tcp_test("127.0.0.1", 8888, 10))) t.start() threads.append(t) for t in threads: t.join() print(f"平均每轮耗时: {sum(results)/len(results):.3f}s")压测时一边跑一边用 netstat 观察连接数,用vmstat 1观察进程上下文切换。如果是 fork 模型,每来一个客户端就 fork 一个进程,100 个客户端就是 100 个进程,系统上下文切换开销会非常明显。换成 select 或 epoll 多路复用后,同样的并发量单进程就能扛住。
我自己折腾这份实验代码后的套路是这样的:先跑通 TCP 一对一,确认收发方向没问题;再跑通一对多 fork 模型,观察每个客户端独立处理;然后跑 UDP 聊天室,验证广播机制和端口隔离;最后把 UDP 聊天室里新增客户端收不到历史消息的短板补上。每次动手之前先看一遍目标协议的核心特性——TCP 可靠有序、UDP 不可靠无序——再回到代码里验证是不是这样实现的。从那以后我每次做网络实验都强制先跑一遍最小闭环,再去叠加并发和可靠性要求,基本没翻过车。希望帮到你。
本文还有配套的精品资源,点击获取