读《网络是怎样连接的》读到 1.4.1 这一节时我有种“前面的线终于收拢住了”的感觉前面几节讲了 HTTP 请求消息长什么样、DNS 怎么把域名解析成 IP但网页真正出现在屏幕上还得靠操作系统内部的“数据收发操作整体流程”把它跑通。这一节是整个数据收发框架的总纲Socket、协议栈、TCP 分段、ACK 确认、IP 分包、网卡驱动的分工都在这里第一次被完整串起来。如果你此前只知道“TCP 三次握手”“四次挥手”这类零散名词但对它们到底发生在哪个环节、和 HTTP 消息是什么关系、为什么握手要三次而不是两次一知半解那这一节非常值得精读。今天这篇我就把 1.4.1 概览部分的核心拆开结合我实际抓包、排查连接故障的经验把数据从浏览器到网卡这条路上的每一站讲清楚。1. 从用户输入网址到网卡发包整体流程的四个阶段这一节叫“概览”是因为它第一次把前面几节的成果组装了起来浏览器生成了 HTTP 请求消息DNS 解析出了目标服务器的 IP 地址但从这里开始数据收发操作才真正登上舞台。对整个系统来说一次数据收发可以拆成四个阶段查 IP、建连接、搬数据、断连接。这个过程很多人默认它“本来就该发生”但真正值得注意的恰恰是这四个阶段不是连续的数据流而是几次“状态切换”。我当初粗读时以为发送 HTTP 消息就是一整块数据直接从浏览器流到网线后来才意识到数据是先被拆碎、加上各种头部、再一段一段通过协议栈搬运的。整个过程里每一次搬运都发生在不同的“层”每一层都只关心自己该管的那部分。这四个阶段分别是第一阶段DNS 解析。浏览器调用操作系统的解析器向 DNS 服务器查询域名对应的 IP 地址拿到 IP 后才能进入下一步。实际上这一阶段在 1.4.1 之前已经完成但它是整个流程的起点。第二阶段创建套接字并建立连接。浏览器通过 Socket 库发起创建套接字的调用随后协议栈里的 TCP 模块与服务器进行三次握手建立一个逻辑连接。第三阶段收发数据。HTTP 请求消息被交给 TCP 模块TCP 把消息拆成若干块加上 TCP 头部变成“段”再交给 IP 模块IP 加上 IP 头部和 MAC 头部最终由网卡把数字信号发送出去。收到响应时反向执行一遍TCP 再负责把零散的块重新组装成完整消息交给浏览器。第四阶段断开连接并删除套接字。数据收发完成后TCP 进行四次挥手断开连接套接字进入一段等待时间后被删除操作系统回收相关资源。如果把这四个阶段画成一张表你会看得更清楚阶段发生位置主要参与者一句话概括DNS 解析应用层到 DNS 服务器浏览器、解析器把域名翻译成 IP创建套接字与建立连接协议栈内部Socket 库、TCP 模块双方协商并记录连接状态收发数据应用层到网卡TCP/IP/网卡驱动数据拆块、封装、发送、组装断开连接协议栈内部TCP 模块确认收发结束回收资源提示读懂这个整体框架后再往细节钻效率会高很多。很多人学网络最先背的是“三次握手、四次挥手”却不清楚它们分别处于哪个阶段结果遇到网络问题时脑子里没有一条完整的“路径图”排查起来到处乱撞。2. 连接的本质是一场“参数协商”三次握手到底在谈什么很多人把“建立连接”想象成“像打电话一样拨通对方”这个类比其实有误导性。网络里的连接并不是一条真实存在的线路TCP 的“连接”本质上是通信双方各自在内存里维护一组状态信息并共同约定一套初始参数。三次握手做的事情就是把这些参数互相告诉对方让对方确认“我知道你要跟我通信了我也准备好跟你通信了”。2.1 握手过程中交换的四个关键要素以浏览器作为客户端访问某台服务器为例三次握手的过程是这样的客户端发一个 TCP 头部设置 SYN1 的包其中包含初始序列号假设为 x表示“我想建立连接我的数据将从序列号 x 开始编号”。服务器收到后如果同意建立连接就回复一个 SYN1、ACK1 的包序列号为 y确认号为 x1表示“我收到了你的同步请求我的数据从 y 开始编号我期待你下一个包的序列号是 x1”。客户端收到后再回一个 ACK1 的包确认号为 y1表示“我收到了你的同步和确认我期待你下一个包的序列号是 y1”。这里每一次交互都不只是“打招呼”而是在协商四个关键参数初始序列号、确认号、窗口大小、可选能力比如是否支持 SACK、MSS 是多少。握手完成后双方各自记录对方的 IP、端口、初始序列号、窗口大小一条“逻辑连接”才算建立。我学习时最大的一个收获是连接状态不是网络上的某个设备帮忙记着的而是两端各自记着的。如果中间某个路由器断电连接本身并不会“断”只是两端某次发送数据后迟迟等不到 ACK才会在超时后判断连接失效。这个认知对排查“连接为什么突然断了”非常关键。2.2 为什么必须是三次两次和四次为什么不行这是精读时绕不开的问题。简单说三次握手的核心目的是防止失效的连接请求突然到达服务器导致服务器白白建立一条空连接。假设只有两次握手客户端发了 SYN但因为网络拥塞迟迟没到客户端超时重发了一个 SYN服务器收到后回 ACK连接建立数据传输完毕并关闭。这时第一个失效的 SYN 又姗姗来迟服务器收到后以为客户端想再建一条新连接于是回复 ACK 并进入等待状态。这条连接存不存在呢客户端根本不知道服务器却要一直维护它资源就被浪费了。三次握手解决这个问题的方式是客户端收到服务器的 SYNACK 后只有它再回一个 ACK服务器才正式进入连接建立状态。如果服务器等不到这个 ACK就会放弃这条未完成的连接。也就是说第三次握手本质上是在“确认对方确实愿意连接”。至于为什么不是四次原理也很直白三次已经能让双方都确认“我能发、你能收你能发、我能收”再增加一次往返只会增加建立连接的延迟没有任何信息增益。现实中关键的“对称确认”只需要三次所以 TCP 就把流程定在这儿了。实操提示用抓包工具观察握手时你会看到客户端发 SYN 后状态变成 SYN_SENT服务器收到 SYN 后状态变成 SYN_RCVD。如果你的服务器上出现大量卡在 SYN_RCVD 的连接极可能是有人只发 SYN 不完成第三次握手——这就是经典的“半连接攻击”场景相关防护和后续排查思路都可以从这个状态机出发去理解。3. 收发阶段的工作台分段、ACK 与窗口三条主线连接建立之后数据收发这件“看起来很简单”的事实际上包含了三条并行推进的主线发送方如何把大块数据变成网络包、接收方如何确认收到、双方如何协调收发速度。1.4.1 概览这一节把这三条线都点了名但没有展开。这里我结合实践把每条线的关键逻辑讲透。3.1 分段为什么网络包不能“想多大就多大”HTTP 请求消息可能只有几百字节也可能大到几 MB。但无论多大它都不能作为一个整体直接从网卡发出去因为网络链路对单个包的大小有限制。以太网常见的 MTU最大传输单元是 1500 字节左右超过这个尺寸的包会被丢弃或分片。TCP 模块在发送前会先看 MTU 是多少算出一个数据段最多能装多少字节——这个值叫 MSS最大段大小通常是 1500 减去 IP 头 20 字节再减去 TCP 头 20 字节即 1460 字节。所以HTTP 消息被 TCP 模块切割成多个“段”每个段都加上 TCP 头部后再交给 IP 模块。IP 模块为每个段加上 IP 头和 MAC 头形成完整的网络包由网卡逐个发送出去。接收端的 TCP 模块则根据 TCP 头部里的序列号把收到的零散数据块按顺序拼回原来的消息。我曾见过不熟悉网络细节的开发者在应用层直接发送超大的 UDP 包指望协议栈帮忙切片。结果包到了某些交换机或路由器上因为超过 MTU 又被切成碎片接收端一旦丢了一个分片整个数据报只能抛弃应用层看到的现象就是“偶尔收不到偶尔卡住”。理解分段机制后这类问题就不难定位了要么把应用层消息控制在合理大小要么接受 UDP 本身的不可靠。3.2 ACK让发送方“确定对方收到了”送达确认是可靠传输的基石。接收方每收到一个段TCP 模块都要返回一个确认包这个包就是 ACK。它的核心字段是“确认号”含义是“你下一个段应该从哪个序列号开始发”。发送方只有在收到 ACK 后才确认“这一段数据对方确实收到了”然后才从缓冲区里丢掉这段数据。但实际运行中有几个细节很多人容易忽略ACK 可以累积。接收方不一定要每收到一个段就立刻回一个 ACK它可以攒几个段后一次性确认比如收到序列号 1、2、3 的三个段直接回一个“确认号 4”的 ACK表示前三个都收了。这大大减少了反向确认包的数量。ACK 可能会延迟发送。接收端的 TCP 通常会等待一小段时间常见实现是 40 毫秒左右或等待有数据要发时“捎带”确认目的是把多个确认合并提高链路利用率。超时重传的计时是动态的。发送方发完一个段就启动超时计时器如果在规定时间内没收到 ACK就重传。这个“规定时间”不是写死的而是根据历史往返时间来估测的网络越稳定估测越准网络抖动厉害时超时时间也随之变大。还有一个经常引发困惑的点是“快速重传”。如果某个段丢了接收方会不断回复“我还在等那个序列号”的 ACK发送方连续收到三个重复 ACK 后就可以断定该段丢失直接重传而不用傻等超时计时器到期。这是 TCP 提高重传效率的重要手段。实践体会有一次我在本地联调一个上传服务发现大文件上传偶尔会卡住十几秒再继续。抓包后看到的现象正是“超时重传”网络环境本身就是同一个局域网按说不该有丢包。最后排查发现是服务端处理速度跟不上接收缓冲区溢出导致部分段被内核丢弃。消息瞬间理解了一个原理——ACK 不只是网络通不通的体现更是接收方处理能力的一种反馈。3.3 窗口吞吐量的杠杆早期的 TCP 是“发一个等一个确认”的停等协议一对一确认固然可靠但每发一个包都要等一个往返时间吞吐量会低得可怜。现代 TCP 引入了一个核心概念窗口Window。窗口的本质是“允许在没有收到确认的情况下最多还能发送多少字节”。这个数字不是发送方自己拍脑袋定的而是接收方根据自己的缓冲区大小通过每个 ACK 包里的窗口字段告诉发送方“我还有多少空间你可以继续发。”比如接收方缓冲区能装 64KB 数据它就在 ACK 里写“窗口 65535”发送方在没有收到新 ACK 前累计发送的数据不能超过这个窗口大小。这意味着理论上网络里可以同时“在途”很多数据包不用来回等待吞吐量就上去了。更聪明的是接收方的窗口不是固定不变的如果应用层读取数据慢了缓冲区快满了接收方就会在 ACK 里把窗口值调小发送方看到后就会降低发送速度。这就是所谓的“流量控制”相当于一个动态调节的水龙头接收方说“水流开大点”或“开小点”发送方听指挥。我个人的理解方式是确认号负责说“收到了哪些”窗口负责说“还能收多少”。一个管正确性一个管速度两者连在一起才构成 TCP 收发阶段完整的信任与限速机制。4. 断开不是终点四次挥手与套接字的最终归宿数据收发完成后客户端或服务端就可以发起断开连接。很多人知道“四次挥手”但不知道它和套接字的生命周期有什么关系。这一节里最容易忽略的恰恰是“断开之后套接字不是立刻消失TIME_WAIT 状态里还有重要逻辑”。4.1 四次挥手的完整过程以客户端主动断开为例完整过程是客户端发送 FIN1 的包表示“我的数据发完了我想断开”。服务端收到后回一个 ACK表示“我收到了你的断开请求但我这边可能还有数据没发完”。等服务端把剩余数据都发完它也发送一个 FIN1 的包表示“我的数据也发完了可以断了”。客户端收到后回一个 ACK。到这一步双方才真正完成断开。注意第一步和第三步之间可能有时间差因为服务端可能还有数据要发。这也是为什么断开需要“四次”而连接只需要“三次”——连接是从两端同时确认“准备好”而断开是两条方向各自独立完成的不能合并。收到最终的 ACK 后主动断开的一方不会立刻把套接字删除而是进入 TIME_WAIT 状态等待一段时间后再真正删除。这个等待时间一般是 2MSLMSL 是报文在网络里最长生存时间常见实现约为 2 到 4 分钟。4.2 TIME_WAIT 的作用与常见误区为什么主动断开的一方要等这么久有三个层面的原因。第一确保最后一个 ACK 对方收到了。如果客户端回复的最终 ACK 在网络中丢失服务端会以为自己的 FIN 没被收到从而超时重发 FIN。如果客户端立刻删除套接字直接关闭重发的 FIN 就没人理会了服务端会陷入“无法确认断开”的状态。TIME_WAIT 等待期间客户端还能响应服务端重发的 FIN。第二让可能还在网络中游荡的旧数据包自然消亡。假设连接关闭后某个旧包因为网络延迟还在链路上游荡如果此时立刻用同样的 IP 和端口新建一个连接这个旧包就可能被新连接收到造成数据污染。等待 2MSL足以让旧包在网络里彻底消失。第三保证连接的“身份”不会错乱。TCP 连接用四元组源 IP、源端口、目标 IP、目标端口来区分如果端口和 IP 很快被复用新连接和旧连接就可能混淆。TIME_WAIT 是 TCP 自身做的“防串线”机制。实际运维中常见的现象是高并发的短连接服务上服务端或客户端积累大量 TIME_WAIT 状态的连接导致端口被占满、新连接无法建立。很多文章一上来就建议“把 TIME_WAIT 时间减小”但我的经验是这需要结合业务场景仔细权衡。如果是客户端主动断开且每个连接都要占用一个临时端口那么高并发短连接场景下临时端口耗尽才是真正的问题这时更合理的思路是复用连接keep-alive而不是粗暴缩短 TIME_WAIT。注意TIME_WAIT 状态只存在于主动断开连接的一端。如果你用 netstat 看到大量处于 TIME_WAIT 的连接先确认是谁主动断开的再决定优化方向不要一刀切。5. 用抓包把这些过程“看见”一份最小实操指南原理读得再多不如亲眼看一次包在网络里跑来跑去。1.4.1 这一节是极适合“边读边抓包”的位置——前面的流程刚建立完整框架后面的细节才开始展开。这里分享一套我自己用过的“最小抓包方案”大概十几分钟就能把三次握手、数据分段、四次挥手全部看明白。5.1 如何用抓包验证三次握手与四次挥手最简单的办法是抓本机访问任意 HTTP 网站的包。我用的是命令行工具在终端里执行sudo tcpdump -i any -n tcp port 80 -w http_capture.pcap另开一个终端访问一个普通网站或者用命令行请求工具发一个请求等请求结束后停止抓包然后用图形化抓包工具打开生成的 pcap 文件按时间排序你基本会看到这样的现象最前面三条记录是三次握手第一条 SYN第二条 SYNACK第三条 ACK。中间的大批记录是数据收发可能是几个“长包”携带数据的包和若干“短包”纯 ACK。最后四条记录是四次挥手FIN、ACK、FIN、ACK。我第一次完整看到这个过程时最震撼的是HTTP 请求的消息体用肉眼根本看不出是“一段完整的东西”它在网络上是以很多个带着序列号的独立包形式存在的。只有抓包视角能让你真正理解“TCP 是流不是消息”这句话。5.2 从这套流程反推日常排查思路理解了整体流程之后再遇到网络问题排查思路会清晰很多。我把常用的“症状到阶段”对应关系整理成了表格方便参考现象可能所在的阶段优先查看内容请求发出后长时间无响应浏览器一直转圈三次握手阶段是否卡在 SYN_SENT、SYN_RCVD是否存在防火墙丢包页面能打开但图片或部分接口偶尔失败数据收发阶段是否存在大量 TCP 重传、丢包、窗口为 0请求结束但端口被占用新连接建不起来断开阶段TIME_WAIT 数量、临时端口范围是否耗尽服务器负载很低但连接建立慢握手阶段半连接队列、全连接队列是否溢出这套方法看起来是“用工具看现象”但底层依赖的其实是这一节建立的整体流程印象先判断问题发生在哪个阶段再深入对应的协议细节而不是一开始就扎进 TCP 报文的每个字段里。另外补充两个我自己在精读后立刻实践过的小技巧用netstat -anp或对应系统等价命令查看各种状态的连接数量。如果看到大量 SYN_SENT说明你在主动连接对方但对方没响应可能是对方端口未监听或防火墙拦截如果看到大量 ESTABLISHED说明连接建立正常问题更可能在应用层。用命令行请求工具加上--trace-ascii之类的参数观察请求各个阶段的时间分布。你会发现“DNS 解析”“TCP 连接”“TLS 握手”“等待响应”是分开计时的哪个阶段耗时长一目了然。这和抓包看到的握手过程完全可以对应起来。6. 精读这一节后我对“网络分层”这件事有了新的体感说了这么多原理和实操最后聊点我个人的体会。精读 1.4.1 之前“TCP/IP 分层”对我而言只是一个知识框架知道有应用层、传输层、网络层、链路层每一层有各自的协议但真要说“发一条 HTTP 请求到底经过了什么”我是讲不出一个完整故事的。读完这一节之后最大的变化是脑子里有了那条清晰的路径浏览器调用 Socket 创建套接字TCP 在内存里记录连接状态并完成三次握手HTTP 消息被切成段并编号IP 层负责路由和分组网卡把数字信号送上线缆对端收到后再反向组装。这条路径看似简单但它解释了大量日常开发中遇到的现象为什么局域网里也会断连、为什么高并发短连接会端口耗尽、为什么抓包能看到重传、为什么服务端要调大队列长度。以前这些知识点是零散的现在它们挂在了同一个时间线上。如果你也正在读这本书我的建议是不要急着往后翻先在 1.4.1 这节停一停亲手抓一次包把四次阶段在抓包结果里对号入座一遍。这张全局图一旦建立起来后续 IP 层、以太网、路由器、服务器那些章节都只是往这张图里填充细节而已。等把整条链路都走通后再回头看你会发现自己已经能像一个真正的网络工程师那样顺着线索而不是凭感觉排查问题了。