ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TCP与Socket编程实战:三次握手、粘包分帧与C语言示例

TCP与Socket编程实战:三次握手、粘包分帧与C语言示例 如果你准备进入网络编程第一个绕不过去的概念组合一定是 TCP 和 Socket。我在网上搜过、也踩过坑更见过不少新人的同一种翻车模式三次握手背得滚瓜烂熟真到写代码时 bind 和 listen 的顺序搞反recv 返回 0 不知道意味着什么客户端连上了服务端却收不到数据重启服务端又报 Address already in use。这些问题的根源往往不是代码写错而是对 TCP 协议行为和 Socket 编程模型之间的对应关系没建立起来。这篇文章会从 TCP 协议核心开始讲但不会停留在概念——重点放在协议机制如何在 Socket 编程中体现比如为什么需要三次握手、为什么会有粘包、为什么服务端重启会报端口占用然后给出一份可以直接跑的 C 语言 TCP echo 服务端/客户端代码再配一个 Python 写的调试脚本和一套排查网络问题的工具箱。不管你是刚入门的学生、写课程设计的开发者还是被线上 tcp 连接问题困扰的运维同学这篇文章应该都能帮上忙。1. 为什么网络编程总是绕不过 TCP协议的价值与学习路线1.1 从一次真实的新手翻车说起我第一次写 TCP server 时本地客户端连得好好的换到另一台电脑上就连不上客户端一直报 Connection refused。折腾了很久才发现服务端 bind 的是 127.0.0.1 这个回环地址。回环地址只在本机内部有效局域网内其他机器当然无法连接。换成 INADDR_ANY绑定本机所有网卡后问题立刻消失。这个经历让我意识到tcp 连接看似简单其实每一步都有明确的协议语义。bind 绑的是本地地址listen 打开的是接受连接的能力accept 取出的是已经完成握手的连接。如果你不理解这些系统调用背后的状态变化遇到报错就只能靠猜。而靠猜就是网络编程新手最常犯的错误。TCP 之所以避不开是因为它撑起了互联网应用的大半边天HTTP、FTP、SSH、MQTT、MySQL、Modbus TCP底层几乎全是 TCP。你现在刷网页、传文件、连数据库每一次可靠传输背后都有 TCP 在干活。所以无论是纯理论面试还是实际开发排障你迟早都要回来补这一课。1.2 TCP 在五层模型中的定位传输可靠性的接力棒网上经常看到tcp/ip模型各层功能详解tcp五层架构示意图这类搜索词说明大家都想先把模型搞清楚。简化版的五层模型大概是这样应用层负责业务数据格式传输层负责端到端传输网络层负责路由转发链路层和物理层负责实际介质传输。IP 层本身是不可靠的它只负责尽力把数据报送到目的地中途丢包、乱序、重复都不管。TCP 就是在 IP 之上加了一套可靠机制让应用层拿到的是一个有序、不丢、不重的字节流。打个比方IP 是普通平信寄出去就不管了TCP 是挂号信每封信都有编号收件人要确认签收丢了可以补寄。这里的编号就是 TCP 头里的序号sequence number和确认号acknowledgment number。UDP 则更像不加服务的平信只管发不保证到。所以 TCP 适合需要可靠传输的场景UDP 适合允许丢包、追求低延迟的场景。整理一张对照表会更直观维度TCPUDP连接方式面向连接先握手后通信无连接直接发数据报可靠性确认、重传、按序到达尽力而为可能丢包乱序传输单位字节流无边界数据报保留消息边界头部开销20~60 字节8 字节典型应用HTTP、SSH、MySQL、Modbus TCPDNS、音视频通话、游戏位置同步1.3 给入门者的三条学习路线按我个人的经验学习顺序比学习时长重要。建议走这条路线第一先看协议再看代码。TCP 的状态机、握手、挥手不弄懂后面遇到 TIME_WAIT、半关闭、半开连接会一直懵。第二用最小代码跑通一次回环通信。语言用 C 或 Python 都行重点不是堆功能而是观察收发过程、close 之后的现象、端口状态的变化。第三一定要学会抓包。装好 Wireshark 或者 tcpdump亲手看一次三次握手的 SYN、ACK 标志很多线上问题瞬间就有了解释。不要一上来就钻进大型框架比如 Netty、Asio、libuv。框架当然好但它们封装了太多细节先用裸 TCP 写一遍你才能建立对各种报错和性能问题的直觉。2. 三次握手与四次挥手你不只是在面试题里用得上它们2.1 三次握手为什么必须正好三次三次握手的流程大家都见过客户端发 SYN服务端回 SYNACK客户端再回 ACK。换成报文语义就是客户端初始序号为 x发 SYN 说我要开始连接了。服务端初始序号为 y回 SYNACK同时确认收到客户端的 x告诉客户端我也准备好了。客户端再回一个 ACK确认收到服务端的 y连接建立。为什么不能只握两次核心原因之一是防止已经失效的连接请求忽然到达服务端造成资源浪费。如果只有两次握手服务端收到一个很久以前的旧 SYN 就会分配连接资源并进入 ESTABLISHED但客户端此时可能并不想建立连接也不会继续通信服务端就白白挂着一个空连接。三次握手让服务端只有在收到客户端针对本次握手的 ACK 之后才确认对端确实活着而且认可这次连接。更严谨地说握手的过程同时完成了双方初始序号的同步后续数据才能按序处理。2.2 socket 状态变化如何用命令行观察明白了状态变化你就能读懂 netstat 和 ss 的输出。我常用ss -tnp查看本机 TCP 状态和对应进程一下就能看出哪些连接处于 TIME_WAIT、哪些卡在 CLOSE_WAIT。阶段客户端状态服务端状态发起连接SYN_SENTLISTEN服务端收到 SYN 并回 SYNACK-SYN_RCVD客户端最终回 ACKESTABLISHEDESTABLISHED主动关闭后FIN_WAIT_1 - FIN_WAIT_2CLOSE_WAIT最后阶段TIME_WAITLAST_ACK - CLOSED实操中我总结过一个小规律如果线上出现大量 CLOSE_WAIT多半是服务端代码收到对方关闭后没有正确 close()fd 泄漏了如果出现大量 TIME_WAIT则说明客户端在频繁创建短连接不是严重故障但可以通过长连接或连接复用来缓解。TIME_WAIT 不是洪水猛兽真正要避免的是由它导致的端口暂时不可用。2.3 四次挥手、半关闭与 TIME_WAIT 对重启的真实影响TCP 是全双工协议数据双向独立传输所以关闭也必须每个方向单独进行。主动关闭方发 FIN表示我不再发数据了对方回 ACK 确认等对方把自己的数据发完也发一个 FIN主动关闭方再回 ACK。这就是为什么要四次挥手。这里有一个实战中极其常见的坑主动关闭的一方会进入 TIME_WAIT且维持约 2MSLLinux 默认约 60 秒。如果你在开发环境 CtrlC 杀掉服务端立刻重启很可能报Address already in use因为上一个连接的 TIME_WAIT 还没结束端口还不能复用。解决办法是在 bind 之前设置 SO_REUSEADDRint on 1; setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, on, sizeof(on));这个选项允许新 socket 绑定到处于 TIME_WAIT 的本地地址是开发环境最常用的处理方式。但要提醒一句不要为了压测随意去调内核里的 tcp_tw_reuse、tcp_tw_recycle 这类参数它们在不同内核版本里行为差异很大某些已经废弃乱调容易引入更隐蔽的问题。更稳妥的做法是让连接尽量长命减少频繁断开重建。2.4 半开连接与心跳tcp 断连不是立刻知道的拔掉网线、手机断 WiFi、设备断电TCP 层不会立刻感知。你调用 send 时数据可能先进内核缓冲区看起来发送成功要过很久才可能在 send/recv 时报 ETIMEDOUT 或 Connection reset。TCP 自带 Keepalive 默认也不可靠Linux 下默认两小时才探测一次。所以业务层的长连接几乎都要自己做心跳客户端定时发一个 ping服务端回一个 pong连续几次收不到就主动断开重连。服务端也要给 recv 设置读超时及时清理死连接。很多 lwip tcp 断连、esp32 收发异常、Socket read timed out 的问题排查到最后都会落到没有心跳机制上。3. 字节流模型与粘包TCP 给你的是水管不是豆腐块3.1 先建立流的直觉TCP 是一个字节流协议它不保留应用层的消息边界。当你调用 send() 发送 1000 字节接收方 recv() 可能一次收到 300 字节、500 字节也可能下一次 recv 同时拿到你前一次和后一次发送的内容。因为内核里只有发送缓冲区和接收缓冲区TCP 只管按字节流动不知道你业务层的一条消息从哪开始、到哪结束。我见过不少新手第一次遇到粘包时非常困惑明明服务端先 recv 了一部分结果客户端下一次发送的数据被拼在了上一次的尾巴上。这其实准确讲不叫粘包而是业务层没有做消息分帧。所谓粘包和半包本质都是一次 recv 不代表一条消息导致的认知错位。3.2 网上奇数字节后面补随机数的真相有个很热的搜索词是为什么 socket 接收到奇数字节后面会补一个随机数。我猜提问者在抓包时发现收到的数据长度不是自己发的长度末尾多了一些看起来随机的字节。真相通常来自以下几种情况第一种没有正确处理半包把上一段数据的残留当成了本条数据的尾巴。第二种抓包时把 TCP 头部或者其他字段也算进去了看起来多出来的东西其实是协议本身的数据。第三种应用层协议带长度头、填充字段、CRC 或者分块对齐规则凑齐长度是协议自己的设计和 TCP 无关。第四种发送端连续调用了多次 send接收端一次 recv 把多段内容合并了。TCP 不会为了奇偶对齐在数据末尾补随机字节。如果你看到这种数据应该去应用层协议格式里找原因而不是怀疑 TCP 动了手脚。3.3 三种分帧方案与选型要在字节流里切出消息必须在应用层定好分帧规则。主流方案有三种方案原理优点缺点适用场景分隔符消息以 \r\n 或 \n 结尾可读性好调试直观正文不能包含分隔符需要逐字节扫描文本协议、HTTP 行、IRC固定长度每条消息固定 N 字节实现最简单浪费带宽变长消息难处理字段长度固定的机器间协议长度前缀先写 4 字节长度再跟正文通用、解析高效要处理网络字节序与长度上限绝大多数二进制协议我的建议是通用业务协议优先考虑长度前缀做成长度头 正文的格式后续扩展版本号、消息类型字段就变成了典型的 TLV 结构。HTTP 其实也是混合分帧的典型请求行和头部用 CRLF 分隔body 用 Content-Length 或 chunked 编码标识长度。3.4 落地recv_exact 才是关键函数既然一次 recv 不保证读满就需要一个循环 recv 直到读够指定字节数的函数。这个函数我在几乎所有 TCP 项目里都会保留一份C 语言版本大概长这样static ssize_t recv_exact(int fd, void *buf, size_t len, int flags) { char *p (char *)buf; size_t got 0; while (got len) { ssize_t n recv(fd, p got, len - got, flags); if (n 0) { return (ssize_t)got; // 对端关闭 } if (n 0) { if (errno EINTR) { continue; } return -1; } got (size_t)n; } return (ssize_t)got; }用法很简单先 recv_exact 读 4 字节长度用 ntohl 把网络字节序转成主机字节序再 recv_exact 读正文。发送方对应要用 htonl 写入长度。长度前缀这个思路一旦焊进脑子里几乎所有 TCP 协议解析都会顺畅很多。4. C 语言写第一个 TCP 服务端从 socket 到 accept 的全过程4.1 为什么建议第一个版本写 C如果只为了跑通 demoPython 十几行就能完成。但我还是建议第一个 TCP 版本用 C 写一遍因为 socket/bind/listen/accept/connect 这套系统调用就是 TCP 协议的官方实现接口C 让你直面 fd、缓冲区、errno对每一步在做什么有最直接的感知。学会了这套 API再看 Python、Java、Go、Rust 的 socket 封装基本都是同构迁移。环境上只要有一个 Linux 或 WSL 环境配上 gcc 就够了。Windows 原生的 Winsock 需要先 WSAStartupAPI 稍有区别本文默认 Linux 环境。4.2 服务端代码与关键系统调用解释这是一个最简的 echo 服务端客户端发什么它就回什么直到客户端关闭连接。#include stdio.h #include string.h #include stdlib.h #include unistd.h #include errno.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define PORT 8899 #define BUFSIZE 4096 int main(void) { int lfd, cfd; struct sockaddr_in addr; socklen_t addrlen sizeof(addr); char buf[BUFSIZE]; int on 1; lfd socket(AF_INET, SOCK_STREAM, 0); if (lfd 0) { perror(socket); exit(1); } setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, on, sizeof(on)); memset(addr, 0, sizeof(addr)); addr.sin_family AF_INET; addr.sin_port htons(PORT); addr.sin_addr.s_addr htonl(INADDR_ANY); if (bind(lfd, (struct sockaddr *)addr, sizeof(addr)) 0) { perror(bind); exit(1); } if (listen(lfd, 16) 0) { perror(listen); exit(1); } printf(listening on 0.0.0.0:%d\n, PORT); while (1) { cfd accept(lfd, (struct sockaddr *)addr, addrlen); if (cfd 0) { perror(accept); continue; } printf(client connected from %s:%d\n, inet_ntoa(addr.sin_addr), ntohs(addr.sin_port)); ssize_t n; while ((n recv(cfd, buf, sizeof(buf), 0)) 0) { send(cfd, buf, (size_t)n, 0); } printf(client closed\n); close(cfd); } close(lfd); return 0; }逐个看关键调用socket(AF_INET, SOCK_STREAM, 0) 创建 IPv4 字节流套接字内核根据 AF_INET 和 SOCK_STREAM 自动选择 TCP 协议。bind 把套接字绑定到本地地址和端口监听套接字必须在 listen 之前完成绑定。listen 把套接字变成被动监听状态内核开始接受外部的连接请求。accept 从已完成握手的队列里取出一个连接并返回一个新的 fd。注意监听 fd 和连接 fd 是两个东西后续收发数据都走连接 fd。recv/send 默认是阻塞的。recv 返回 0 表示对端正常关闭send 在阻塞模式下也不能绝对保证一次发完严谨的代码应该循环发送。上面代码为了演示做了简化生产环境必须加上错误处理和循环发送。4.3 客户端代码客户端比服务端简单很多不需要 bind 和 listenconnect 时内核会自动分配一个临时端口#include stdio.h #include string.h #include stdlib.h #include unistd.h #include sys/socket.h #include netinet/in.h #include arpa/inet.h #define SERVER_IP 127.0.0.1 #define SERVER_PORT 8899 int main(void) { int fd; struct sockaddr_in saddr; char buf[4096]; fd socket(AF_INET, SOCK_STREAM, 0); if (fd 0) { perror(socket); exit(1); } memset(saddr, 0, sizeof(saddr)); saddr.sin_family AF_INET; saddr.sin_port htons(SERVER_PORT); inet_pton(AF_INET, SERVER_IP, saddr.sin_addr); if (connect(fd, (struct sockaddr *)saddr, sizeof(saddr)) 0) { perror(connect); close(fd); exit(1); } printf(connected\n); const char *msg hello tcp; send(fd, msg, strlen(msg), 0); ssize_t n recv(fd, buf, sizeof(buf), 0); if (n 0) { printf(echo: %.*s\n, (int)n, buf); } close(fd); return 0; }注意 inet_pton 把点分十进制的 IP 字符串转换成二进制地址转换失败会返回 0 或 -1生产代码里要检查返回值。connect 返回后客户端实际上已经完成了三次握手进入 ESTABLISHED 状态。4.4 编译运行与第一个坑编译时建议加上警告选项gcc -Wall -O2 server.c -o server gcc -Wall -O2 client.c -o client先启动 server再开一个终端执行 client正常情况下应该看到服务端打印客户端地址客户端打印 echo 回来的内容。这个环节最容易踩的第一个坑就是 EADDRINUSE。服务端 CtrlC 之后立刻重启报Address already in use原因在前面说过主动关闭的一方这里是服务端进入 TIME_WAIT端口暂时无法复用。通过 SO_REUSEADDR 可以绕过。第二个坑来自单线程 accept 模型。当前客户端连接后服务端进入 recv 循环如果这个客户端一直不发送数据服务端就阻塞在 recv 上第二个客户端即使 connect 成功也得不到响应。这不是 bug而是模型限制。后续第 7 节会专门讲并发模型。第三个坑是防火墙。跨机器连接时Windows 防火墙或者云服务器安全组很可能拦截端口建议先用nc 127.0.0.1 8899本机测通再换局域网 IP最后再检查防火墙规则。4.5 从这段代码学到的三个习惯第一每个系统调用都要检查返回值。第二消息要分帧不要默认一次 recv 就是一条完整消息半包处理必须按第 3 节的方式做。第三如果还要等待对方发送剩余数据再关闭不要直接 close应该先 shutdown(SHUT_WR) 表示我不再发数据了但我还能收完成半关闭后再 close。这个习惯在实现自定义协议时很常见。5. 实战中的排查工具箱调试助手、抓包与常见报错对照表5.1 最顺手的三个工具排查 TCP 问题我推荐一套组合拳命令行用 nc快速验证端口通不通Windows 下用图形化 TCP 调试助手方便发送字符串和十六进制报文要定位协议层问题必须上 tcpdump 或 Wireshark。最常用的命令nc -v 127.0.0.1 8899 sudo tcpdump -i lo port 8899 -nn -XWireshark 的过滤器很简单tcp.port 8899找到连接后右键 Follow TCP Stream 就能看到这台机器上完整的应用数据流。学会用 Wireshark 以后你会发现自己看很多网络问题都像开了透视一样清楚因为连接的建立、断开、重传、RST 全部可见。5.2 常见报错与可能原因对照整理一张表覆盖高频报错现象常见原因先查什么Connection refused服务端没监听或者端口不对ss -lntp看端口和进程Connection reset by peer对端已关闭连接但还在收发数据抓包看最后的 RST 包recv 返回 0对端正常关闭连接不算错误但要 close 并清理 fdSocket read timed outrecv 设置了超时对端没按期发数据检查心跳和应用协议节奏Address already in use端口被占用或处于 TIME_WAITss -lntp、SO_REUSEADDRBroken pipe向已关闭的 socket 写数据触发 SIGPIPE处理 SIGPIPE 或先检测连接状态顺便提一个容易混淆的热词MySQL 报cant connect to local mysql server through socket /tmp/mysql.sock这里的 socket 是 UNIX domain socket属于同一主机的进程间通信方式地址族是 AF_UNIX和网络上的 TCP socket 不是一回事。它通常表示 mysqld 没启动或者 socket 文件路径不对。5.3 一个完整的排查案例一次真实排错中客户端每隔几秒发一条心跳但服务端总在连接建立后不久报 Connection reset。我在两端同时抓包用 Wireshark 打开发现服务端在收到客户端第一条消息后回了 ACK然后客户端紧接着发下一条服务端直接回了 RST。顺着应用日志一查真正原因很诡异服务端把心跳数据当成业务消息去解析解析失败后立刻 close socket没有读完接收缓冲区TCP 只能回 RST 终止连接。这个案例说明一个规律90% 的 reset 和超时问题报错发生在 TCP 层根因却在应用层。如果你只是在两端看报错永远看不到真相抓包之后RST 的位置和时间点会直接告诉你到底是哪一方、在收到什么数据后放弃了这个连接。5.4 Nagle 算法与 TCP_NODELAY为什么小包会延迟你可能遇到过这种情况客户端 send 一个很小的数据包服务端却要等几十毫秒才收到。原因通常是 Nagle 算法和延迟 ACK 的叠加。Nagle 会把多个小包合并成大包再发送减少网络里的小包数量接收方收到数据后又延迟 ACK希望等一个有数据的响应一起回。两者合在一起小交互延迟可能达到 40ms 左右。交互式应用可以将 TCP_NODELAY 打开关闭 Nagleint flag 1; setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, flag, sizeof(flag));但不要以为开了这个就能让数据即时到达。它只负责把单个小包立即发出去实际的到达时间还要受网络延迟、带宽、丢包重传影响。批量上传场景下开着 Nagle 反而有利于吞吐。6. Python 快速原型用 30 行代码写一个压测与调试脚本6.1 为什么我总喜欢配一个 Python 调试客户端写 C 服务端最大的问题是每改一点都要重新编译。所以我通常会在旁边配一个 Python 调试客户端用它来模拟多个并发连接、构造粘包场景、测量响应时间。Python 的 socket API 是 C 的镜像学起来不冲突而且可以直接跑在 Windows 和 Linux 上。6.2 完整脚本分帧发送、循环接收、并发模拟下面这个脚本配合长度前缀协议服务端比较顺手如果你的服务端是第 4 节那种普通 echo只需把 send_frame 替换成sock.sendall(payload)就好。import socket import struct from concurrent.futures import ThreadPoolExecutor HOST, PORT 127.0.0.1, 8899 def recv_exact(sock, n): data b while len(data) n: chunk sock.recv(n - len(data)) if not chunk: raise ConnectionError(peer closed) data chunk return data def send_frame(sock, payload): sock.sendall(struct.pack(I, len(payload)) payload) def one_client(idx): with socket.create_connection((HOST, PORT), timeout3) as s: payload bmsg-%02d-%s % (idx, bx * 100) send_frame(s, payload) length struct.unpack(I, recv_exact(s, 4))[0] body recv_exact(s, length) print(idx, len(body), body[:16]) with ThreadPoolExecutor(max_workers8) as pool: for i in range(20): pool.submit(one_client, i)这个脚本同时演示了几个关键点socket.create_connection 负责连接并设置超时sendall 保证一次发完recv_exact 解决了半包问题ThreadPoolExecutor 模拟并发。服务端如果用 Python 写也可以用同款逻辑做分帧回显代码很短适合对照学习。6.3 Python socket 的三个高频坑第一个坑是 send 和 sendall。send 可能不会把整个缓冲区的数据发完数据量大了容易丢尾巴sendall 会内部循环直到全部发出。所以 Python 里能用 sendall 就别用 send。第二个坑是超时后的 socket 状态。一旦 socket 因为 recv 超时抛出异常这个 socket 就不能继续 recv/send 了要继续通信只能重新连接。第三个坑是非阻塞模式。setblocking(False) 之后 recv 会抛 BlockingIOError必须配合 select/poll 使用入门阶段先老老实实用阻塞模式。7. 从 demo 到生产并发模型、select/epoll 与扩展方向7.1 单线程阻塞模型为什么不行第 4 节的 echo 服务端有一个明显问题一次只能服务一个客户端。第一个客户端连接后服务端阻塞在 recv 上第二个客户端就算 TCP 连接建立成功服务端也腾不出手去 accept。对于学习协议来说这不是问题但做任何真实服务都必须解决同时等待多个 socket这件事。7.2 多线程、select/poll、epoll 怎么选解决并发常见方案有三种模型实现方式优点缺点适用规模多线程/多进程每个连接一个线程编程直观、业务隔离好线程开销大共享状态复杂几十到几百连接select/poll单线程遍历 fd 集合跨平台、实现简单fd 数量有上限线性扫描效率低几百连接epoll内核事件驱动只处理活跃 fd高并发、伸缩性好Linux 专属编程模型复杂万级以上连接选型逻辑很简单连接少就多线程连接多就用 epoll。Windows 上的 IOCP、macOS 的 kqueue 是另一个体系。跨平台库如 libuv、Asio、Netty本质都是对多路复用机制的封装。你看 Asio 的 async_read、strand 和回调时会觉得难是因为它在不同的操作系统上要统一这些底层的差异先理解了 epoll 的思路再回去看封装库会容易很多。7.3 嵌入式与 IoT 场景的 TCP 注意事项热词里有一堆 lwIP、RT-Thread、ESP32-S3、Modbus TCP 相关的问题我也简单提几句。lwIP 是嵌入式场景最常见的 TCP/IP 协议栈API 尽量兼容 BSD socket但内核配置里通常要把内存池、接收窗口、MSS 设好。ESP32 上先确认 WiFi 已连接再测 TCP 连接如果连不上优先看 RSSI、MTU、MSS 以及服务端是否真的监听在对应端口。RT-Thread 配合 lwIP 时常见问题集中在 socket fd 的释放和线程上下文。断线后如果应用层没有及时 closefd 会泄漏直到 pool 耗尽。Modbus TCP 则是一个很好的应用层例子它在 TCP 之上加了 7 字节报文头包含事务标识、协议标识、后续长度、单元标识本质还是长度前缀 业务数据的分帧结构默认端口 502。7.4 继续往哪里学如果你想深入下去有三件事我觉得性价比很高。第一啃一遍 RFC 793 和《TCP/IP 详解 卷1》很多面试题的细节都来自那里。第二自己做一个带分帧、心跳、select/epoll 并发的 echo server比看十篇教程都管用。第三把 echo server 改成最简单的 HTTP 服务器解析 GET 请求返回 HTML 页面。这个练习能让你彻底分清 HTTP 和 TCP 的区别HTTP 是应用层协议TCP 是传输层协议HTTP/1.1 的 Content-Length 本质上就是分帧头Connection: keep-alive 就是长连接的复用策略。最后说一个我自己保持到现在的习惯每写一个 TCP 服务我都会在旁边跑一个持续抓包的命令然后故意制造断线、重启、粘包场景再回放抓包看状态变化。这个习惯帮我排掉了大量看起来像玄学的问题。TCP 协议核心和 Socket 编程说白了就两个词分帧与状态。把消息边界定清楚把连接状态的回收时机搞清楚大部分网络问题都不会再困扰你。
返回列表