简介:一套计算机网络实验用的Socket编程代码包,围绕TCP/UDP协议实现一对多聊天与多人聊天室场景,适合正在学习网络编程、需要完成课程实验或想动手验证传输层原理的学生与开发者。压缩包总共14个文件,核心源码包括C语言编写的server1.c、client1.c、server2.c、client2.c等功能文件,以及Python实现的server3.py、client3.py,同时附有编译生成的可执行程序(server1、server2、client1、client2等),方便直接运行或结合源码调试。包体仅34KB,按task1、task2、task3三个模块划分,分别演示TCP一对一通信中的连接建立与数据处理、TCP多客户端并发响应、以及UDP广播式多人聊天室;实验里还包含服务端收到客户端消息后逆序回传、UDP将消息转发给全部在线用户等典型操作。已有1204人学习下载。通过这份代码,能直观对比TCP与UDP在连接维护、可靠性、延迟上的差异,理解多线程/循环处理多客户端请求的思路,并学会编写包含异常捕获的健壮网络程序,为后续网络开发打下扎实基础。
1. 实验三不是玩具:Socket 编程决定聊天室能不能扛住人
很多计算机网络实验卡在“实验三 socket 编程”这一步,不是因为 API 难背,而是因为第一次真正面对“一对多”这件事。单机上的聊天 Demo 谁都能跑,但要让多个客户端同时连上、消息还能转发给所有人,背后是 socket 生命周期管理、并发模型取舍、TCP/UDP 协议差异三个坎。这个实验的典型交付物是一个 .rar 压缩包,里面通常是一对 TCP 和 UDP 的聊天室代码加实验报告,验收时老师会拿两三个终端连你的服务端,看能不能互发消息、有人退出后其他人是否还正常。
本文就用这个经典的“多人聊天室”场景,把服务端骨架、客户端循环、协议差异、排错手段一次讲透,直接对着敲就能跑通。适合正在赶计算机网络实验的学生,也适合刚接触 socket 编程、想把 select 和阻塞 I/O 理清楚的从业者。先给结论:这个实验的分数差距基本不在“能不能通”,而在“断线怎么处理、UDP 丢包怎么解释、并发模型为什么这么选”,这三件事恰恰是后面所有网络编程的地基。
2. 服务端骨架:socket、bind、listen、select 这套固定动作怎么搭出多人聊天室
2.1 先分清阻塞与非阻塞:一对多聊天室的第一个岔路口
最简单的服务端写法是“一次只服务一个客户端”——accept 一个连接,处理完消息,再 accept 下一个。这在真正的多人聊天室里是没法用的,因为 accept 本身会阻塞,只要有一个客户端连上不发消息,其他客户端全被卡在门外。常见做法是改成多线程,用 accept 循环配合 pthread_create 给每个客户端开一个线程;另一种做法是用 select 做 I/O 多路复用,把关心读事件的文件描述符全部丢进一个集合,交给内核去等,内核说“有数据到了”你再挨个处理。
选哪种,要看实验报告的侧重点。多线程模型直观,但线程同步和“客户端退出后怎么回收线程”反而比 select 更容易写成翻车现场。select 虽然老,但胜在单线程、无锁、可解释性强,代码量也短,最适合课程验收。我一般会建议学生用 select 实现 TCP 版本,因为这样你只需要维护一个 client 数组,逻辑上就是“把所有客户端放进同一个观察名单”,跟实验标题里“一对多聊天”的语义完全对应。
select 的核心是 FD_SET、FD_ISSET、FD_ZERO 这三个宏加一个 select 调用,配合两个集合:一个是监听 socket 的读集合,一个是所有已连接客户端 socket 的读集合。每次循环重新构造集合,然后交给 select 等待,返回后逐个 FD_ISSET 检查到底谁有数据。
2.2 TCP 服务端完整代码:select 单线程转发实现
下面这段代码是 TCP 多人聊天室服务端的完整核心,编译环境是 Linux + gcc,保存成server_tcp.c直接编译就能用:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <sys/select.h> #define PORT 8888 #define MAX_CLIENTS 100 #define BUFFER_SIZE 1024 int main() { int server_fd, client_fds[MAX_CLIENTS]; fd_set read_fds; struct sockaddr_in server_addr, client_addr; socklen_t addr_len = sizeof(client_addr); char buffer[BUFFER_SIZE]; // 1. 创建 TCP socket:AF_INET=IPv4, SOCK_STREAM=面向连接 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd < 0) { perror("socket"); exit(1); } // 2. 设置端口复用,避免 TIME_WAIT 状态下重启失败 int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); // 3. bind 绑定地址和端口 if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); exit(1); } // 4. listen 开始监听,backlog=10 表示等待队列长度 if (listen(server_fd, 10) < 0) { perror("listen"); exit(1); } printf("Server listening on port %d...\n", PORT); // 初始化客户端 fd 数组,-1 表示空位 for (int i = 0; i < MAX_CLIENTS; i++) { client_fds[i] = -1; } while (1) { FD_ZERO(&read_fds); FD_SET(server_fd, &read_fds); int max_fd = server_fd; // 把所有已连接客户端也加入读集合 for (int i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] != -1) { FD_SET(client_fds[i], &read_fds); if (client_fds[i] > max_fd) { max_fd = client_fds[i]; } } } // 5. select 阻塞等待,直到有 fd 可读 int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL); if (activity < 0) { perror("select"); continue; } // 6. 有新连接进来 if (FD_ISSET(server_fd, &read_fds)) { int new_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len); if (new_fd < 0) { perror("accept"); continue; } printf("New client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); int placed = 0; for (int i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] == -1) { client_fds[i] = new_fd; placed = 1; break; } } if (!placed) { printf("Client limit reached, closing new connection\n"); close(new_fd); } } // 7. 遍历所有客户端,处理收到的消息并转发 for (int i = 0; i < MAX_CLIENTS; i++) { if (client_fds[i] != -1 && FD_ISSET(client_fds[i], &read_fds)) { memset(buffer, 0, BUFFER_SIZE); int bytes = recv(client_fds[i], buffer, BUFFER_SIZE - 1, 0); if (bytes <= 0) { // 客户端断开或出错,关掉并清空槽位 printf("Client fd %d disconnected\n", client_fds[i]); close(client_fds[i]); client_fds[i] = -1; } else { printf("Received: %s", buffer); // 转发给除发送者外的所有客户端 for (int j = 0; j < MAX_CLIENTS; j++) { if (client_fds[j] != -1 && j != i) { send(client_fds[j], buffer, bytes, 0); } } } } } } close(server_fd); return 0; }逻辑说明:主循环每次重新构建 fd_set,把监听 socket 和所有在线客户端 socket 都加进去,然后 select 会阻塞等待。一旦有事件发生,先判断是不是新连接,是就 accept 并存入数组;不是就从数组里逐个找谁有数据可读,读出来之后给除发送者外所有人转发。客户端断开时 recv 返回 0,这时候把 fd 关掉、槽位置 -1,这是最容易漏的一步,漏了就会出现“假在线”僵尸连接。
参数说明:MAX_CLIENTS 控制最大在线数,数组存 fd 的简单方案适合课程实验;select的第一个参数要传“最大 fd 值 + 1”,不是传某个固定值,很多人在这里丢分。recv的缓冲大小用 BUFFER_SIZE-1,留一个字节给\0防止字符串越界。send的返回值没检查是故意的——课程实验里简单处理可以,但生产环境必须处理 send 被信号打断或缓冲区满的情况。
2.3 backlog 和端口复用:这两个参数直接影响验收表现
listen的第二个参数 backlog 是“已完成三次握手但还没被 accept 的连接队列长度”,调试时会遇到的现象是:客户端瞬间连太多,后面的连接直接报 Connection refused,不是因为你的程序挂了,而是内核里的队列满了。课程实验设 10 够用,但如果现场有十个以上客户端同时启动,适当调到 32 或 64 更稳妥。
setsockopt的 SO_REUSEADDR 是血泪经验换来的。没有这行,服务端 Ctrl+C 退出后立刻重启会报 “Address already in use”,因为上一次的连接还处于 TIME_WAIT 状态,要等两分钟左右才能完全释放。加上这行后,端口可以立即复用,验收时不断改代码重启服务端就不用等。
这两个参数在实验报告里都值得写一段,因为它们是“让程序在真实场景里不翻车”的关键,也是面试里经常追问的点。
3. TCP 与 UDP 的分工:一对多聊天里谁负责可靠,谁负责广播
3.1 UDP 版聊天室的根本差异:没有连接,只有报文
TCP 版是“先 connect 再聊天”,服务端用 accept 接受连接、用 recv/send 收发数据;UDP 版完全不同——服务端只需要 socket + bind,不需要 listen、accept、connect 这三件套,因为 UDP 根本没有连接概念。服务端用recvfrom收数据,同时拿到发送方的地址和端口,再用sendto按地址把消息发回去或广播给所有人。
这个差异直接决定了“一对多聊天”在 UDP 下的实现方式:TCP 靠客户端 fd 数组维护在线列表,UDP 靠一个struct sockaddr_in数组记录每个客户端的地址和端口。每个客户端发来消息,你就把消息连同发送者地址一起,逐个sendto给其他已登记的客户端。UDP 没有 recv 返回 0 表示断开的说法,所以“谁下线了”只能靠心跳超时来判断,这也是 UDP 聊天室比 TCP 难维护的地方。
下面是 UDP 服务端核心片段,只贴与 TCP 不同的部分:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #define PORT 9999 #define MAX_CLIENTS 100 #define BUFFER_SIZE 1024 struct client_info { struct sockaddr_in addr; int active; }; int main() { int sock_fd; struct sockaddr_in server_addr; struct client_info clients[MAX_CLIENTS]; char buffer[BUFFER_SIZE]; struct sockaddr_in sender_addr; socklen_t sender_len = sizeof(sender_addr); // UDP socket:SOCK_DGRAM 表示数据报 sock_fd = socket(AF_INET, SOCK_DGRAM, 0); if (sock_fd < 0) { perror("socket"); exit(1); } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(PORT); if (bind(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("bind"); exit(1); } for (int i = 0; i < MAX_CLIENTS; i++) { clients[i].active = 0; } while (1) { memset(buffer, 0, BUFFER_SIZE); int bytes = recvfrom(sock_fd, buffer, BUFFER_SIZE - 1, 0, (struct sockaddr*)&sender_addr, &sender_len); if (bytes <= 0) { perror("recvfrom"); continue; } buffer[bytes] = '\0'; // 识别发送者是否已登记,未登记则加入在线列表 int sender_id = -1; for (int i = 0; i < MAX_CLIENTS; i++) { if (clients[i].active && clients[i].addr.sin_port == sender_addr.sin_port && clients[i].addr.sin_addr.s_addr == sender_addr.sin_addr.s_addr) { sender_id = i; break; } } if (sender_id == -1) { for (int i = 0; i < MAX_CLIENTS; i++) { if (!clients[i].active) { clients[i].addr = sender_addr; clients[i].active = 1; sender_id = i; break; } } } // 转发给除发送者外的所有已登记客户端 for (int i = 0; i < MAX_CLIENTS; i++) { if (clients[i].active && i != sender_id) { sendto(sock_fd, buffer, bytes, 0, (struct sockaddr*)&clients[i].addr, sizeof(clients[i].addr)); } } printf("Forwarded %d bytes from %s:%d\n", bytes, inet_ntoa(sender_addr.sin_addr), ntohs(sender_addr.sin_port)); } close(sock_fd); return 0; }逻辑说明:UDP 服务端本质是“收到一条数据报,识别来源地址,然后向所有已登记的地址复制转发”。这里用sin_port和sin_addr.s_addr联合作为客户端标识,因为 UDP 没有 fd 可用。sender_len每次调用前要重新赋值,因为 recvfrom 会把它改写为实际地址长度,不重置会出现地址解析错乱。
3.2 UDP 和 TCP 协议的区别在这个实验里的具体体现
很多学生把这个实验的两个版本写成各写各的,报告里却说不清“为什么要做两个版本”。其实这个实验的核心命题就是对比:TCP 是可靠传输,保证消息顺序和完整性,但代价是三次握手、确认重传、流量控制,收到“你好”和“大家好”两条消息时,先发的先到;UDP 是尽力而为,直接一个数据报扔到网络上,不保证到达、不保证顺序,但发送开销极小、延迟可控。
放在聊天室场景里,TCP 适合需要逐字确认的消息,比如文本内容、文件传输;UDP 适合实时性要求高但能容忍偶发丢失的数据,比如语音通话、位置上报。实验里如果要求你实现 UDP 版,一般还会加一条要求“消息丢失时聊天室仍然可用”——也就是说,你要在报告里解释清楚 UD P 版可能出现的丢包现象,并给出应对手段:客户端显示“消息已发送”但对方没收到,这是 UDP 的正常行为,不是 bug。
粘包和半包是 TCP 版特有的坑。TCP 是字节流,recv 返回的字节数不一定是对方 send 的字节数,可能一次收到半条消息,也可能一次收到两条拼一起。课程验收时消息短,问题不明显;数据一多就露馅。简单做法是给每条消息加固定长度的头部,比如前 4 字节表示消息长度,再跟消息体;复杂做法是加分隔符。实验报告里写“我遇到了粘包问题,用长度头解决”是实打实的加分项。
3.3 实验报告怎么对比这两个版本
写报告时建议用一张参数对比表,把两个版本的关键结论列清楚:可靠性与丢包、连接建立与断开的状态管理、并发模型、消息边界处理方式、适用场景。表格放在报告的“实验结果分析”一节,比堆代码截图更有说服力。
另外,UDP 版实验的另一个观察点是“对端主动退出后,服务端无法知道”。TCP 版里服务端能检测到断开并清理槽位,UDP 版里只能等心跳超时才能清理。这也是 UDP 聊天室在线列表“越用越满”的原因,实验报告需要写明这个限制以及对应的解决思路。
4. 客户端实现:从 connect 到收发循环,以及一对多场景的体验细节
4.1 TCP 客户端的最小实现:connect、send、recv 三件事
客户端的写法比服务端简单,但依然有两个细节决定验收体验:一是输入消息时不能阻塞在 recv 上,否则你既要打字又要收消息会卡死;二是退出时要正确关闭 socket,不能直接 Ctrl+C 留下一堆半开连接。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <pthread.h> #define SERVER_IP "127.0.0.1" #define SERVER_PORT 8888 #define BUFFER_SIZE 1024 // 接收线程:阻塞在 recv 上,收到消息就打印 void* receive_thread(void* arg) { int sock_fd = *(int*)arg; char buffer[BUFFER_SIZE]; while (1) { memset(buffer, 0, BUFFER_SIZE); int bytes = recv(sock_fd, buffer, BUFFER_SIZE - 1, 0); if (bytes <= 0) { printf("Disconnected from server.\n"); break; } buffer[bytes] = '\0'; printf("%s", buffer); fflush(stdout); } pthread_exit(NULL); } int main() { int sock_fd; struct sockaddr_in server_addr; char buffer[BUFFER_SIZE]; sock_fd = socket(AF_INET, SOCK_STREAM, 0); if (sock_fd < 0) { perror("socket"); exit(1); } memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); server_addr.sin_port = htons(SERVER_PORT); // connect 建立连接,失败时打印错误并退出 if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("connect"); exit(1); } printf("Connected to server. Type your messages:\n"); // 单独开线程处理 recv,主线程负责读 stdin pthread_t tid; pthread_create(&tid, NULL, receive_thread, &sock_fd); while (1) { fgets(buffer, BUFFER_SIZE, stdin); send(sock_fd, buffer, strlen(buffer), 0); } close(sock_fd); return 0; }逻辑说明:客户端最重要的结构是“一个线程收 + 主循环发”。不这么做的话,你在终端里输入半条消息,服务端广播过来的消息会打印到输入行中间,把界面搅成乱码。这里把收到的消息原样打印,消息格式可以自己定义,比如[用户名]: 内容,由客户端在send前拼接。
参数说明:fgets会把换行符一起读进 buffer,所以打印时消息末尾自带换行。send的返回值没有检查,如果连接断开后继续输入会触发 SIGPIPE 导致进程直接退出,实际项目里要么忽略 SIGPIPE,要么检查 send 返回值。
4.2 用户名的处理:实验报告里可写可不写的加分项
“一对多聊天”如果只显示消息内容,不显示是谁发的,验收时老师会建议你改进。常见做法是客户端启动时让用户输入昵称,之后每次发送时在消息前面拼一个固定格式的头部。服务端不需要解析昵称,直接把整条消息原样转发即可。这样最简单,而且足够满足实验要求。
更讲究一点的做法是客户端只在首次连接时发送昵称,服务端登记并维护昵称与 fd 的映射,转发时自动加前缀。但这样做服务端要维护多一个数组,再加上 BUFFER_SIZE 的边界问题,复杂度是几何级上升。课程实验建议用“整条消息带昵称”的简单方案,把精力留给核心的并发和协议问题。
UDP 客户端需要注意的地方是:UDP 不需要 connect 也能 sendto,但实验里建议先 connect 一下,这样后续可以像 TCP 一样直接调用 send 和 recv。这在 UDP socket 里是合法且常见的用法,connect 只是帮内核记住默认对端地址,并没有真的建立连接,代码会简洁很多。
5. 避坑手册:Socket 聊天室最常见的 5 个翻车现场
5.1 端口占用,服务端重启总是报 Address already in use
现象:服务端第一次运行正常,Ctrl+C 后再次启动,bind 报错,程序退出。
原因:主动关闭的一端进入 TIME_WAIT 状态,默认要等 2 个最大分段生存期(MSL),这段时间内端口被内核占用。
解决:在 bind 之前加setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))。这是最稳妥的解法。如果加了还是报错,检查是不是有两个服务端进程同时在跑,netstat -tlnp | grep 8888查一下就知道。
5.2 客户端一多,服务端直接不响应或卡死
现象:三四个客户端连上后收发正常,第八个连上来后整个服务端像死了一样,所有客户端都收不到消息。
原因:客户端 fd 数组满了,新连接被 close,但这不是主要原因。更常见的坑是select的max_fd没更新,新加入的 fd 值大于当前 max_fd,select 不去监听它,所有后续消息全部丢失。用 FD_SETSIZE 默认值只能管到 1024,超过边界还会内存越界。
解决:每次向 read_fds 加 fd 时都重新计算 max_fd,核心代码在上文有,遍历数组取最大值即可。同时检查自己的 MAX_CLIENTS 是否超过 FD_SETSIZE,超过的部分 select 监听不了,要改用 epoll 或降低数组上限。
5.3 recv 返回 0 和返回 -1 混为一谈,断开连接处理错乱
现象:客户端正常关闭后,服务端日志没打印断开信息;或者客户端收到乱码。
原因:recv 返回 0 表示对端优雅关闭,返回 -1 表示出错。新手常只判断bytes == -1,把 0 当成正常数据去 send,结果把一个空消息广播给所有人。
解决:统一用bytes <= 0作为关闭条件,关闭 fd 并清槽位。也可以单独打印返回 -1 时的 errno,如果 errno 是 ECONNRESET,说明对端强制关闭,这在 UDP 场景里常见于 ICMP 端口不可达。
5.4 UDP 版“在线列表越用越大”,旧地址占满数组
现象:多个客户端轮流退出再重新登录后,新客户端无法加入聊天室,或者消息发给了离线的人。
原因:UDP 服务端没有断开通知,而代码里又没有心跳超时机制,客户端退出后他的地址仍然留在 clients 数组里,active永远为 1。
解决:给struct client_info加一个last_active时间戳,每次收到该客户端的消息就更新,后台定期扫描清理超过 60 秒没消息的客户端。60 秒只是一个示意值,实际要根据实验要求的“离线检测延迟”来调。这是 UDP 聊天室里最值得写进报告的一笔,因为大多数同学的 UDP 版都有这个隐患。
5.5 客户端输入中文消息乱码或者半截消息
现象:输入中文后,对方收到的文本出现半个字符。
原因:TCP 是按字节流传输,如果按固定 BUFFER_SIZE 切分,可能把一个 UTF-8 中文字符(3 字节)切成了两半;最后一个字节到达后,客户端按\0截断,中文就乱了。
解决:两个办法。一是客户端输入后不截断,整段 fgets 后一次性 send,服务端设一个较小的 BUFFER_SIZE 防止跨字符切割;二是服务端做消息边界处理,用长度头或者循环读满一个完整消息再转发。课程验收一般不会刻意制造粘包,但报告能写“用长度头解决消息边界”是很大的加分项。更进一步地说,正确的做法是定义一个消息结构:前 4 字节存消息长度,后面存消息体,服务端读满头部再读体。
6. 验证与进阶:从“能跑”到“敢说做完了”
6.1 本地验收的最小测试方案
先用自己写的最小步骤确认功能闭环:开三个终端,终端 A 启动服务端,终端 B、C、D 启动客户端;在 B 输入消息,观察 C 和 D 是否同时收到、B 自己是否收到(是否回显依服务端实现而定)。这个方案能覆盖“服务端监听”“连接建立”“消息转发”三个基本环节。
接着测异常流程:在 C 里按 Ctrl+C 强制退出,看服务端是否打印断开日志、B 和 D 是否还能正常互发消息。这一步最容易暴露“客户端 fd 槽位没清理”的漏判。再把 D 也退掉,看服务端是否正常回到等待状态,重新拉起一个 E 连上去,确认新连接可以被 accept。
最后测协议边界:B 连续粘贴几行长文本一次性发送,看 C 端是否被拆成多条消息;B 发一段中文,看 C 端是否有乱码。这些都能复现出上面的讨论。
6.2 进阶改进按优先级排:心跳、断线重连、epoll
如果实验要求是“多人聊天室”,完成前面五章内容就已经达标。时间预算充足的话,按这个优先级做改进:第一梯队是显式心跳协议,客户端每 30 秒发一个PING,服务端回PONG,超时 90 秒未收到 PING 就踢掉——这能把 UDP 在线列表的“假在线”问题彻底解决;第二梯队是服务端把 send 的返回值都检查一遍,对信号中断(errno == EINTR)做重发;第三梯队是客户端支持/quit命令优雅退出,而不是直接 Ctrl+C。
EPoll 不是必须的,但如果你想在报告里写“本实现基于 select,能支撑 XX 并发,可扩展为 epoll 模型”,至少要知道 select 和 epoll 的核心差异:select 每次要把 fd 集合从用户态拷到内核态,返回后还要线性遍历所有 fd 才知道谁就绪;epoll 用红黑树维护监听集合,就绪的描述符通过链表直接返回,复杂度是 O(1) 级。
6.3 用系统调用跟踪工具验证自己对 socket 生命周期的理解
Linux 下可以顺手用strace观察系统调用序列。启动服务端后,在另一个终端执行strace -p <服务端 PID> -e trace=accept,recvfrom,sendto,close -o trace.log,然后让一个客户端连上发消息,打开 trace.log 看系统调用的先后顺序。如果 accept、recvfrom、sendto 的 fd 和端口与你预期一致,说明你对 socket 生命周期、以及 TCP 和 UDP 在系统调用层面的差异是真正理解了,而不只是跑了别人的代码。
这是我当年做实验时养成的习惯,现在回头看,strace帮我把“教科书上的三次握手”变成了一次看得见、摸得着的过程。每次学生拿着“代码能跑但说不出为什么”的代码找我验收,我都建议他们做一次这个观察。希望这套步骤能替你把实验三从头到尾验扎实。
本文还有配套的精品资源,点击获取