简介:本资源是一份面向计算机专业本科生的TCP/IP网络编程课程设计实践材料,聚焦基于TCP协议的C语言客户/服务器通信系统开发,解决网络应用层功能实现与I/O模型实践问题。文档为单个Word文件(.doc),大小1.54MB,完整覆盖总体设计、协议选型依据、通信流程建模、数据包格式定义(含注册/登录/私聊等指令编码规则)、客户端与服务器端功能模块说明、程序清单节选及运行截图,结构清晰、理论与代码结合紧密。已有972人学习下载,适合网络编程初学者系统掌握事件对象I/O管理机制、有连接服务主体架构设计、以及注册登录、在线人数统计、单聊私聊等典型即时通信功能的C语言落地实现。
1. 这不是“写个socket就完事”的作业:一个能跑通、能调试、能验证三次握手、还能在Windows和Linux下复现的TCP通信C语言实战方案
你手上的这份《TCPIP大作业基于TCP的网络通信编程C语言版.doc》,大概率是高校计算机网络课程的期末大作业——它不考你背OSI七层模型,而是逼你亲手用socket()、bind()、listen()、accept()、connect()把TCP连接从零搭起来。但现实很骨感:很多同学交了代码,gcc client.c -o client && ./client一跑就卡在Connection refused;netstat -an | grep :8080发现端口根本没监听;用Wireshark抓包,连SYN包都看不到;更别说在Windows上用telnet 127.0.0.1 8080测试通不通,或者跨主机时被防火墙、IP配置、字节序搞到怀疑人生。这不是C语言基础差,而是缺一套可验证、可打断点、可抓包、可跨平台对照的最小可行路径。本文不讲抽象协议栈,只聚焦“怎么让两个进程真正在TCP上说上话”:从struct sockaddr_in字段填什么、htons()到底转谁、listen()的backlog设5还是128、为什么send()返回值必须检查、以及tcpdump里看到[SYN]后没回[SYN, ACK]时该查哪三层——全部用真实命令、真实错误输出、真实抓包截图逻辑(文字还原)带你过一遍。适合正在赶deadline的本科生、想补网络底层的嵌入式初学者,以及需要给新人讲清“TCP不是自动连上的”那一课的带教工程师。
2. 从零构建可验证的TCP通信对:服务端监听与客户端连接的完整闭环
要让“基于TCP的网络通信”不沦为伪命题,必须建立一个可观测、可中断、可复现的端到端链路。这意味着不能只写send()/recv()就交差,而要确保:服务端真正在指定IP+端口上listen()成功;客户端能发出SYN并收到SYN-ACK;三次握手完成后数据能双向收发;任意一端异常退出,另一端能感知断连。下面以最简但最硬核的方式实现——不依赖任何框架,纯POSIX socket API,同时兼容Windows(Winsock2)和Linux(glibc)。
2.1 服务端:绑定、监听、接受连接的三步铁律
服务端核心在于让内核为你分配一个“门牌号”(IP+端口),并准备好接待访客。关键不是listen(),而是bind()前的地址初始化和accept()后的错误处理。
// server.c —— 兼容Windows/Linux的最小服务端 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> // Linux: close(), read(), write() #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #ifdef _WIN32 #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") #endif int main(int argc, char *argv[]) { int server_fd, client_fd; struct sockaddr_in server_addr, client_addr; socklen_t client_len = sizeof(client_addr); char buffer[1024]; // 【Windows必需】初始化Winsock(Linux忽略) #ifdef _WIN32 WSADATA wsa; if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) { fprintf(stderr, "WSAStartup failed: %d\n", WSAGetLastError()); return 1; } #endif // 1. 创建socket:IPv4 + TCP + 默认协议 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { perror("socket creation failed"); goto cleanup; } // 2. 初始化服务器地址结构:INADDR_ANY表示监听所有本地IP memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; // IPv4 server_addr.sin_port = htons(8080); // 端口8080 → 网络字节序 server_addr.sin_addr.s_addr = INADDR_ANY; // 监听本机所有IPv4地址(0.0.0.0) // 3. 绑定:将socket与地址绑定 if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("bind failed"); goto cleanup; } // 4. 开始监听:backlog=5表示等待队列最多5个未accept的连接 if (listen(server_fd, 5) == -1) { perror("listen failed"); goto cleanup; } printf("Server listening on 0.0.0.0:8080...\n"); // 5. 阻塞等待客户端连接 client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd == -1) { perror("accept failed"); goto cleanup; } // 打印连接客户端信息(关键调试点!) char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); printf("Client connected from %s:%d\n", client_ip, ntohs(client_addr.sin_port)); // 6. 收发数据(简化为一次echo) ssize_t n = recv(client_fd, buffer, sizeof(buffer)-1, 0); if (n > 0) { buffer[n] = '\0'; printf("Received: %s", buffer); send(client_fd, "ACK: ", 5, 0); send(client_fd, buffer, n, 0); } cleanup: #ifdef _WIN32 if (server_fd != -1) closesocket(server_fd); if (client_fd != -1) closesocket(client_fd); WSACleanup(); #else if (server_fd != -1) close(server_fd); if (client_fd != -1) close(client_fd); #endif return 0; }逻辑说明与参数深挖:
AF_INETvsAF_UNSPEC:作业明确要求TCP/IP,必须用AF_INET(IPv4)。AF_UNSPEC会尝试IPv4/IPv6双栈,增加不可控变量。htons(8080):这是血泪经验。端口号是16位整数,网络传输要求大端序(Big-Endian),x86/x64主机是小端序,必须用htons()(host to network short)转换。填8080不转?服务端监听的将是端口33152(8080的十六进制0x1F90按小端存为0x901F→十进制36895),客户端永远连不上。INADDR_ANY:填127.0.0.1只能被本机访问;填具体IP(如192.168.1.100)则仅限该网卡。作业要求“网络通信”,必须用INADDR_ANY才能被局域网其他机器访问。backlog=5:不是最大并发连接数,而是已完成三次握手但尚未被accept()取走的连接队列长度。设太小(如1)会导致高并发时客户端connect()直接失败(ECONNREFUSED);设太大(如1024)在低内存设备上浪费资源。教学场景5足够,生产环境根据/proc/sys/net/core/somaxconn调整。inet_ntop():比过时的inet_ntoa()安全,支持IPv4/IPv6,且不使用静态缓冲区,避免多线程冲突。
2.2 客户端:主动连接、发送、接收的原子操作
客户端代码必须体现TCP的“面向连接”本质——connect()是阻塞的,它内部完成了完整的三次握手。如果connect()返回成功,说明SYN→SYN-ACK→ACK已全部完成。
// client.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> #ifdef _WIN32 #include <winsock2.h> #pragma comment(lib, "ws2_32.lib") #endif int main(int argc, char *argv[]) { int sock; struct sockaddr_in server_addr; char buffer[1024]; if (argc != 3) { fprintf(stderr, "Usage: %s <server_ip> <port>\n", argv[0]); return 1; } #ifdef _WIN32 WSADATA wsa; if (WSAStartup(MAKEWORD(2,2), &wsa) != 0) { fprintf(stderr, "WSAStartup failed: %d\n", WSAGetLastError()); return 1; } #endif // 1. 创建socket sock = socket(AF_INET, SOCK_STREAM, 0); if (sock == -1) { perror("socket creation failed"); goto cleanup; } // 2. 初始化服务器地址 memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(atoi(argv[2])); // 将字符串端口转整数再转网络序 if (inet_pton(AF_INET, argv[1], &server_addr.sin_addr) <= 0) { fprintf(stderr, "Invalid address/Address not supported\n"); goto cleanup; } // 3. 发起连接:这里发生三次握手! if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1) { perror("connect failed"); // 关键!这里失败=三次握手失败 goto cleanup; } printf("Connected to %s:%s\n", argv[1], argv[2]); // 4. 发送数据 const char *msg = "Hello from TCP client!\n"; if (send(sock, msg, strlen(msg), 0) == -1) { perror("send failed"); goto cleanup; } // 5. 接收响应 ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0); if (n > 0) { buffer[n] = '\0'; printf("Server reply: %s", buffer); } cleanup: #ifdef _WIN32 if (sock != -1) closesocket(sock); WSACleanup(); #else if (sock != -1) close(sock); #endif return 0; }关键动作解析:
inet_pton():替代危险的inet_addr()(已废弃)。它将点分十进制IP(如"192.168.1.100")安全地转为二进制网络地址。inet_addr()遇到非法输入(如"123.456.789.0")会静默返回-1,极易埋雷。connect()的返回值:这是验证TCP是否真正工作的黄金指标。如果返回-1且errno为ECONNREFUSED,说明服务端没在监听或端口错误;如果是ETIMEDOUT,可能是防火墙拦截或服务端IP不可达;EHOSTUNREACH则是路由问题。作业中必须打印这个错误,而不是忽略。send()/recv()的返回值检查:send()可能只发出部分数据(如缓冲区满),返回值是实际发送字节数,必须循环发送直到发完;recv()返回0表示对端关闭连接(FIN),返回-1表示错误。作业里常犯的错是假设send()一定发全、recv()一定收全。
2.3 编译与运行:Windows与Linux双平台实操命令
Linux下编译运行:
# 编译服务端和客户端 gcc -o server server.c gcc -o client client.c # 终端1:启动服务端(监听所有IP的8080端口) ./server # 终端2:用客户端连接本机 ./client 127.0.0.1 8080 # 终端3:用另一台Linux机器连接(需在同一局域网) ./client 192.168.1.100 8080 # 192.168.1.100是服务端机器的局域网IPWindows下编译运行(需MinGW或Visual Studio):
:: 使用MinGW编译(推荐,与Linux语法一致) gcc -o server.exe server.c gcc -o client.exe client.c :: 启动服务端 server.exe :: 用cmd连接本机 client.exe 127.0.0.1 8080 :: 用PowerShell检查端口是否监听(验证bind是否成功) netstat -ano | findstr :8080 :: 输出应包含:TCP 0.0.0.0:8080 0.0.0.0:0 LISTENING <PID>为什么强调双平台?
因为作业常被学生在Windows写完,到实验室Linux服务器上一跑就崩——根源往往是#ifdef _WIN32没加、closesocket()没调、或WSAStartup()遗漏。真正的工程能力,是代码在不同环境都能给出一致的行为和清晰的错误提示,而不是靠“我本地能跑”。
3. 用Wireshark和netstat亲手验证三次握手:让黑匣子变成透明管道
写完代码只是第一步。TCP的魔力在于它的可靠性,而可靠性来自精妙的状态机和报文交互。如果跳过抓包验证,你永远不知道connect()背后发生了什么,也无从解释为什么有时连得上有时连不上。这一章教你用免费工具,把TCP从“玄学”变成“可视化学实验”。
3.1 用Wireshark捕获并标记三次握手全过程
Wireshark是网络协议分析的瑞士军刀。我们不追求复杂过滤,只聚焦最核心的三个包:SYN、SYN-ACK、ACK。
操作步骤:
- 启动Wireshark,选择正确的网卡(通常是
Ethernet或Wi-Fi,不是Loopback,除非你只测127.0.0.1) - 在过滤栏输入:
tcp.port == 8080(只看8080端口的TCP流) - 启动服务端:
./server - 在另一终端执行:
./client 127.0.0.1 8080 - Wireshark立即捕获到3个包(可能还有后续的PSH/ACK数据包)
三次握手报文特征(务必对照你的抓包截图):
| 序号 | 方向 | 标志位 | Seq | Ack | 说明 |
|---|---|---|---|---|---|
| 1 | Client → Server | [SYN] | Seq=0 | Ack=0 | 客户端发起连接,随机初始序列号(ISN) |
| 2 | Server → Client | [SYN, ACK] | Seq=0 | Ack=1 | 服务端确认,并发自己的ISN(Ack=Client_ISN+1) |
| 3 | Client → Server | [ACK] | Seq=1 | Ack=1 | 客户端确认服务端ISN(Ack=Server_ISN+1),连接建立 |
提示:Wireshark默认显示相对序列号(Relative Seq/Ack)。右键任一TCP包 → “Protocol Preferences” → 取消勾选“Relative sequence numbers” → 重启Wireshark,即可看到绝对序列号,更符合RFC描述。
为什么这一步不可跳过?
- 如果只看到第1个
[SYN],没看到第2个[SYN, ACK]:说明服务端没监听(bind()/listen()失败)、端口被防火墙拦截、或IP填错(如服务端绑192.168.1.100,客户端连127.0.0.1)。 - 如果看到
[SYN]和[SYN, ACK],但没看到第3个[ACK]:极罕见,通常是客户端网卡驱动或中间设备故障。 - 如果三个包都有,但
recv()收不到数据:问题出在应用层(如send()没调、缓冲区溢出、recv()超时设置不当),而非TCP层。
3.2 用netstat实时观测套接字状态变迁
netstat是系统级的连接状态快照,比Wireshark更轻量,适合快速诊断。
Linux常用命令:
# 查看所有监听中的TCP端口(找你的8080) netstat -tlnp | grep :8080 # 输出示例:tcp6 0 0 :::8080 :::* LISTEN 12345/./server # 查看当前所有TCP连接(含ESTABLISHED) netstat -tn | grep :8080 # 输出示例:tcp 0 0 127.0.0.1:45678 127.0.0.1:8080 ESTABLISHED # 查看连接详细状态(包括Recv-Q/Send-Q) ss -tni | grep :8080 # ss是netstat的现代替代品,更精准Windows对应命令:
:: 查看监听端口 netstat -ano | findstr :8080 :: 查看所有TCP连接及PID netstat -ano :: 根据PID找进程名(如PID=1234) tasklist | findstr 1234状态码解读(TCP状态机核心):
LISTEN:服务端调用listen()后,等待连接。这是bind()成功的铁证。SYN_SENT:客户端调用connect()后,发出SYN但未收到SYN-ACK。此时若长时间停留,说明网络不通或服务端宕机。ESTABLISHED:三次握手完成,连接可用。send()/recv()操作在此状态下进行。TIME_WAIT:主动关闭方(通常是客户端)在发送FIN后进入此状态,持续2MSL(通常60秒),防止旧报文干扰新连接。作业中不必深究,但看到它说明连接已正常关闭。
注意:
netstat显示tcp6不代表IPv6独占。Linux内核默认启用IPv6双栈,:::8080表示监听IPv6的::(等价于IPv4的0.0.0.0)。只要INADDR_ANY正确,IPv4客户端仍可连接。
3.3 telnet:最古老却最有效的连通性验证术
别小看telnet。它不传业务数据,只做一件事:触发TCP三次握手并报告结果。它是排除“是代码问题还是网络问题”的第一道关卡。
验证流程:
# 场景1:服务端已启动,验证本机连通性 telnet 127.0.0.1 8080 # 成功:显示"Connected to 127.0.0.1",光标闪烁(等待输入) # 失败:显示"Unable to connect to remote host: Connection refused" # 场景2:服务端未启动,验证错误提示 # 先kill掉server进程,再执行: telnet 127.0.0.1 8080 # 输出:"Trying 127.0.0.1... telnet: Unable to connect to remote host: Connection refused" # 场景3:跨主机验证(Client A: 192.168.1.101, Server B: 192.168.1.100) # 在Client A上执行: telnet 192.168.1.100 8080 # 若失败,先ping:ping 192.168.1.100 → 通则查防火墙,不通则查物理连接为什么telnet比自己写的client更可靠?
因为它绕过了你的C代码逻辑(如send()内容、recv()缓冲区大小),纯粹测试TCP层是否可达。如果telnet能连上,而你的client连不上,问题100%在client代码里(如IP字符串解析错误、端口没转网络序);如果telnet也连不上,问题在服务端或网络层。
4. 常见问题排查:那些让你熬夜到凌晨三点的TCP血泪坑
写TCP代码最痛苦的不是语法,而是错误不报、现象诡异、原因难查。以下是我在带教20+届学生、review过300+份作业后,总结出的最高频、最隐蔽、最容易集体翻车的5个坑。每一条都附真实错误现象、根因分析和可立即执行的解决方案。
4.1 现象:connect()返回Connection refused,但netstat显示服务端在LISTEN
现象还原:
$ ./server & [1] 12345 Server listening on 0.0.0.0:8080... $ netstat -tlnp | grep :8080 tcp6 0 0 :::8080 :::* LISTEN 12345/./server $ ./client 127.0.0.1 8080 connect failed: Connection refused原因:
服务端bind()时填了INADDR_LOOPBACK(即127.0.0.1)而非INADDR_ANY。netstat显示tcp6 :::8080是因为IPv6双栈,但127.0.0.1是IPv4地址,而::1才是IPv6的localhost。当客户端用IPv4地址127.0.0.1连接时,内核找不到匹配的IPv4监听套接字。
解决:
修改服务端bind()部分:
// ❌ 错误:只监听回环地址 server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // ✅ 正确:监听所有本地IPv4地址 server_addr.sin_addr.s_addr = INADDR_ANY;4.2 现象:客户端connect()卡住几秒后才返回Timeout,Wireshark只看到一个[SYN]
现象还原:
$ ./client 192.168.1.200 8080 connect failed: Connection timed out # 卡约20秒Wireshark只捕获到Client发给192.168.1.200:8080的一个[SYN],无任何回复。
原因:
目标IP192.168.1.200根本不存在于当前局域网,或该机器已关机。路由器收到SYN后无法ARP解析MAC地址,直接丢弃,也不发ICMP不可达。客户端connect()超时(Linux默认约20秒)后返回ETIMEDOUT。
解决:
- 先
ping 192.168.1.200确认IP可达; - 若
ping不通,检查目标机器是否开机、网线是否插好、IP是否配错; - 若
ping通但telnet不通,检查目标机器防火墙:# Ubuntu/Debian sudo ufw status verbose # 查看ufw是否阻止8080 sudo ufw allow 8080 # 临时放行
4.3 现象:服务端accept()后recv()一直阻塞,客户端send()后程序卡死
现象还原:
服务端打印Client connected from 127.0.0.1:54321,然后停住;客户端send()后也停住,无任何输出。
原因:recv()默认是阻塞模式,且未设置超时。当客户端send()后未close(),服务端recv()会一直等更多数据,直到对方关闭连接(发送FIN)或超时。但作业代码中客户端发完就退出,close()由操作系统在进程退出时自动调用,时机不可控。
解决:
在服务端recv()前设置超时,或改用非阻塞模式。教学场景推荐简单方案:
// 在accept()后,recv()前添加: struct timeval timeout = {5, 0}; // 5秒超时 setsockopt(client_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); // recv()后检查返回值 ssize_t n = recv(client_fd, buffer, sizeof(buffer)-1, 0); if (n == 0) { printf("Client closed connection\n"); } else if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { printf("recv timeout\n"); } else { perror("recv error"); } } else { buffer[n] = '\0'; printf("Received: %s", buffer); }4.4 现象:Windows客户端能连通,Linux客户端connect()失败,报No route to host
现象还原:
Windows上client.exe 192.168.1.100 8080成功;
Linux上./client 192.168.1.100 8080失败,strace显示connect()返回-113 (No route to host)。
原因:
Linux客户端所在机器与服务端不在同一子网,且没有配置默认网关。例如:
- 服务端IP:
192.168.1.100/24 - Linux客户端IP:
10.0.0.5/24
此时Linux内核路由表中无到达192.168.1.0/24的路由,直接返回EHOSTUNREACH。
解决:
ip route show查看路由表;- 确保两台机器在同一网段(如都设为
192.168.1.x/24); - 或手动添加路由(临时):
sudo ip route add 192.168.1.0/24 via 10.0.0.1 # 假设10.0.0.1是网关
4.5 现象:服务端send()返回值小于数据长度,客户端recv()只收到部分数据
现象还原:
客户端发送"Hello World!"(12字节),服务端recv()只返回8,buffer中只有"Hello Wo"。
原因:
TCP是字节流协议,不是消息协议。send()将数据推入内核发送缓冲区,返回值是实际拷贝进缓冲区的字节数,可能因缓冲区满而少于请求长度。recv()同理,返回的是本次从接收缓冲区读出的字节数,与send()的调用次数无关。
解决:
必须循环调用send()/recv()直到发完/收完。封装为可靠函数:
// 可靠发送函数 ssize_t send_all(int fd, const void *buf, size_t len) { size_t sent = 0; ssize_t n; while (sent < len) { n = send(fd, (const char*)buf + sent, len - sent, 0); if (n == -1) { if (errno == EINTR) continue; // 被信号中断,重试 return -1; } sent += n; } return (ssize_t)sent; } // 使用 send_all(client_fd, msg, strlen(msg));5. 进阶技巧:用tcpdump在无GUI环境抓包、自定义协议头、以及一个让老师眼前一亮的验证彩蛋
当作业提交截止前2小时,你突然发现老师要求“提供三次握手抓包截图”,而实验室电脑没装Wireshark,只有命令行——别慌。tcpdump就是你的后悔药。再进一步,如果想让通信不只是“Hello World”,而是传递结构化数据(如学号、姓名、成绩),你需要理解如何安全地序列化C结构体。最后,分享一个我带过的最惊艳的作业技巧:用nc(netcat)作为第三方验证器,彻底堵死“我的代码有问题还是你的有问题”的扯皮。
5.1 无Wireshark?用tcpdump在Linux服务器上抓包并导出
tcpdump是Wireshark的命令行兄弟,功能一样强大,且能直接保存为.pcap文件供后续用Wireshark分析。
基础命令:
# 抓取所有经过eth0网卡、端口8080的TCP包,保存到文件 sudo tcpdump -i eth0 -w server_8080.pcap port 8080 # 只抓三次握手(SYN/SYN-ACK/ACK),实时打印(-n禁用DNS解析,-q精简输出) sudo tcpdump -i eth0 -n -q 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn' port 8080 # 抓包后用Wireshark打开(需先scp到本地) scp user@server:/path/to/server_8080.pcap .关键过滤表达式解析:
port 8080:只抓8080端口的包(比tcp.port == 8080更高效)tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn:提取TCP标志位,与SYN-ACK做位运算,等于SYN即为SYN包(三次握手第一步)-w file.pcap:后台静默抓包,不打印到屏幕,适合长时间监控
提示:
tcpdump需要root权限(sudo),因为要访问原始网络接口。普通用户可提前用sudo setcap cap_net_raw+ep /usr/sbin/tcpdump授予权限,避免每次输密码。
5.2 从字符串到结构体:安全传递二进制数据的C语言实践
作业常要求“发送学生信息”,如果还用send()发字符串,就太浅了。真正的网络编程,要处理结构体序列化。
问题:
struct student { int id; // 4字节 char name[20]; // 20字节 float score; // 4字节 };直接send(sockfd, &stu, sizeof(stu), 0)有两大风险:
- 字节序不一致:x86是小端,网络是大端,
id=12345在网络上传输会变成0x3039(小端)→0x3930(大端); - 内存对齐填充:编译器可能在
name[20]后插入4字节填充使score地址对齐,sizeof(stu)可能是32而非28,导致接收端解析错乱。
解决方案:手动序列化(教学首选)
// 发送端 void serialize_student(const struct student *s, uint8_t *buf) { // 手动转网络序 *(uint32_t*)buf = htonl(s->id); // id → 大端 memcpy(buf + 4, s->name, 20); // name原样复制(ASCII) *(float*)(buf + 24) = htonf(s->score); // float需特殊处理(见下) } // 接收端 void deserialize_student(const uint8_t *buf, struct student *s) { s->id = ntohl(*(uint32_t*)buf); memcpy(s->name, buf + 4, 20); s->score = ntohf(*(float*)(buf + 24)); } // float网络序转换(简单起见,用union,生产环境用memcpy避免strict aliasing) float htonf(float f) { union { float f; uint32_t i; } u; u.f = f; u.i = htonl(u.i); return u.f; }为什么不用#pragma pack(1)?
虽然#pragma pack(1)能消除填充,但它让结构体失去CPU对齐优化,且在不同平台(ARM/x86)行为不一致。手动序列化控制力更强,也教会你数据在网络上的真实形态。
5.3 彩蛋技巧:用netcat(nc)作为“第三者”验证,让答辩零争议
最怕答辩时老师说:“你这client能连,但我不信你的server逻辑对”。这时,掏出nc——一个预装在几乎所有Linux/macOS的瑞士军刀:
# 启动你的server ./server # 不用你的client,用nc连接并发送 echo "Test from nc" | nc 127.0.0.1 8080 # 观察server终端输出:它会打印"Received: Test from nc" # 再用nc监听端口,让你的client连nc(反向验证) nc -lvp 8080 # -l监听 -v详细 -p端口 ./client <p> <a href="https://download.csdn.net/download/feijiaogu7393/19293024" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>