ARTICLE DETAIL

资讯详情

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

一次HTTP请求的TCP/IP之旅:从DNS解析到Wireshark排查

一次HTTP请求的TCP/IP之旅:从DNS解析到Wireshark排查 调试线上接口的时候我经常遇到一种情况应用日志显示请求已经发出服务端日志显示响应已经返回但客户端就是报超时。这时候再不情愿也得打开 Wireshark 从 TCP/IP 底层一帧一帧地看。说白了HTTP 只是应用层的一张“快递单”真正负责把数据从一台机器搬到另一台机器的是底下默默工作的 TCP/IP 协议栈。这篇文章想完整梳理一遍当一个 HTTP 请求从浏览器或 curl 发出、直到收到响应、连接关闭它在 TCP/IP 各层到底经历了什么。如果你经常跟接口对接、网络故障排查打交道或者正在啃计算机网络知识这份梳理能帮你把脑子里零散的概念串成一条线。1. 一条 curl 命令背后的接力赛先把整条链路装进脑子里1.1 我不是在讲教科书是在讲一次真实寄件过程假设你在终端里敲了这么一条命令curl -v http://example.com/api/users表面上这只是“发了一个 HTTP 请求”但背后实际参与干活的不止 HTTP 一个协议。整个过程更像寄快递应用层你在快递单上填写收件人信息和包裹内容这是 HTTP 干的活传输层快递公司给你的包裹装箱、贴运单编号、登记收发件人这是 TCP 干的活网络层快递分拣中心根据收件地址规划路线决定包裹下一站发往哪个城市这是 IP 干的活链路层快递员骑着车在小区里送件根据门牌号找到具体一栋楼这是以太网和 ARP 干的活。所以很多人一开始学网络觉得头大是因为把“分层”理解成了“串行执行”。其实数据从上层到下层是不断“套娃”加头部的过程每一层只关心自己负责的那一段信息。1.2 整条流水线上的 7 个关键动作用一张文本流程图可以看得很清楚你在终端输入 URL ↓ 1. DNS 解析把域名翻译成 IP应用层 ↓ 2. TCP 三次握手建立可靠连接传输层 ↓ 3. 构造 HTTP 请求报文交给 TCP 分段应用层 → 传输层 ↓ 4. IP 封装加上源/目的 IP查路由表选下一跳网络层 ↓ 5. ARP 获取下一跳 MAC封装以太网帧链路层 ↓ 6. 数据从网卡发出经过交换机、路由器到达服务器 ↓ 7. 服务器逆过程拆包、处理响应再原路返回记住一个核心法则数据从上往下走是每层加一个头从下往上走是每层摘一个头。后面的每一章都是在给这张图补充细节。2. 应用层第一步DNS 翻译与 HTTP 请求的诞生2.1 浏览器敲回车后的第一个请求其实不是 HTTP很多人以为输入 URL 回车后第一个发出去的数据包是 HTTP GET其实不是。HTTP 需要一个 IP 地址才能建立连接而 example.com 只是一个人类友好的名字所以第一个请求通常是 DNS 查询。DNS 是一个典型的应用层协议默认跑在 UDP 53 端口上。一次完整解析过程可以简单理解为浏览器先查自己的缓存没有就查操作系统 hosts 文件和系统 DNS 缓存都没有就向本地配置的 DNS 服务器比如 223.5.5.5发查询请求本地 DNS 服务器递归替你去问根域名服务器、顶级域服务器、权威域名服务器最终拿到 example.com 对应的 A 记录结果会按 TTL存活时间缓存下来下次再访问就不用重新走完整流程。你在自己机器上可以这样验证dig noall answer example.comexample.com. 3600 IN A 93.184.216.34这里 3600 就是 TTL单位是秒。想强制刷新 DNS 缓存可以执行sudo dscacheutil -flushcachemacOS或ipconfig /flushdnsWindows。2.2 HTTP 请求报文是怎么被组装出来的拿到 IP 之后浏览器或 curl 开始构造 HTTP 请求。用curl -v可以看到最原始的报文头curl -v http://example.com/ GET / HTTP/1.1 Host: example.com User-Agent: curl/8.0.1 Accept: */* 第一行是请求行由方法、路径、协议版本组成后面是若干请求头再往后必须有一个空行如果有请求体比如 POST 的 JSON空行之后才是 body。有个细节容易被忽略HTTP/1.1 协议里Host头是必选的。因为一台服务器上可能用同一个 IP 跑多个域名服务器需要靠 Host 字段判断你要访问哪个虚拟站点。这也是为什么有时候你用 IP 直连访问不到正确站点而用域名访问就正常。2.3 应用层“看似完工”真正发送却要交给内核协议栈很多人以为应用层“写了一个 HTTP 报文”就等于发送了其实不是。应用只是调用 socket 的write()或send()把数据交给操作系统内核后续的切片、重传、路由、封装全部由内核协议栈接管。这里需要理解一个关键点socket 是应用层与传输层之间的“门”。你往门里丢数据门后面的 TCP 就开始干活了。所以排查 HTTP 问题时如果确认应用层代码没问题下一步就要把目光移到 TCP 层去。这也是为什么 Wireshark 抓包永远比只看应用日志更接近真相。3. 传输层重头戏三次握手、数据分段与窗口控制3.1 三次握手为什么必须是三次TCP 是面向连接的可靠传输协议连接建立靠的是著名的三次握手客户端 → 服务端SYN, seqx 服务端 → 客户端SYNACK, seqy, ackx1 客户端 → 服务端ACK, seqx1, acky1为什么不能只握手两次最简单的解释是两次握手无法防止“失效的历史连接请求”突然到达服务端。假设客户端第一次发的 SYN 在网络里堵了很久客户端已经超时重发了新的 SYN结果旧 SYN 又先到了服务端。如果只靠两次握手服务端会误以为这是新连接立刻分配资源并返回 SYNACK但客户端根本不会理它——这就白白浪费了服务端的资源还可能造成连接混乱。只有第三次握手才能让服务端确认“客户端确实收到了我的 SYNACK并且愿意继续通信”。换句话说三次握手让双方都确认了彼此的收发能力都正常全双工通道才算真正建立。3.2 数据被切成段MSS、序号与确认HTTP 报文生成之后TCP 要做的第一件事是分段。以太网标准 MTU最大传输单元是 1500 字节去掉 IP 头 20 字节和 TCP 头 20 字节每个 TCP 段最多能装 1460 字节数据这个值就是 MSS。假设你的 HTTP 请求体有 5000 字节TCP 就会把它切成 4 段1460 1460 1460 620。每一段都会带上一个序号接收方每收到一段就回一个确认号。例如段1: seq1000, 数据 1460 字节 段2: seq2460, 数据 1460 字节 段3: seq3920, 数据 1460 字节 段4: seq5380, 数据 620 字节如果中间某个段丢了接收方不会确认它发送方等不到 ACK 就会超时重传。这个机制保证了 HTTP 数据在不可靠的物理网络上照样能完整到达对端。你在抓包时看到大量TCP Retransmission提示就是这一段在反复“返工”对应的用户感受往往是接口延迟或超时。3.3 滑动窗口与拥塞控制真正决定请求快慢的隐藏机制很多人觉得 TCP 只要分段、确认、重传就够了但实际影响性能的还有两个窗口接收窗口和拥塞窗口。接收窗口rwnd接收方在 TCP 头里通告自己还能收多少字节防止发送方太快把它打爆拥塞窗口cwnd发送方根据网络拥塞程度动态调整的发送上限初始一般很小然后按“慢启动”指数增长。实际发送方一次能发出去的数据等于min(rwnd, cwnd)。慢启动对 HTTP 的影响非常直观如果你的 API 只返回几 KB 数据通常一个 RTT往返时间内就能发完请求快不快主要看网络延迟但如果是下载几十 MB 的文件cwnd 需要好几个 RTT 才能涨到足够大传输速度才会慢慢提上来。还有一个容易踩坑的组合Nagle 算法和延迟 ACK。Nagle 会把小包攒在一起发送延迟 ACK 会把确认包延迟 40ms两者一叠加某些小请求就会平白无故多出 40ms 延迟。对于延迟敏感的服务通常要在 socket 上设置TCP_NODELAY关闭 Nagle这也是很多高性能服务端框架默认开启tcp_nodelay的原因。4. 网络层IP 包如何跨越路由器找到目标主机4.1 IP 头里的关键字段TCP 分段之后每一段都要交给 IP 层封装成数据报。IPv4 头里几个字段值得记住字段长度作用版本4 bitIPv4 或 IPv6TTL8 bit每经过一台路由器减 1防止数据包死循环协议号8 bit6 表示 TCP17 表示 UDP源 IP32 bit发起方地址目的 IP32 bit接收方地址标识、片偏移32 bit用于 IP 分片与重组其中协议号这个字段经常被忽略但抓包时很重要你看到 IP 层协议号是 6才确定上层是 TCP看到 17才确定是 UDP。DNS 使用 UDPHTTP 使用 TCP正是靠这个字段区分。TTL 也是一个有意思的字段。每经过一台路由器它就会减 1减到 0 时数据包会被丢弃同时路由器会回一个 ICMP 报文。Linux 默认初始 TTL 通常是 64Windows 是 128。你可以用ping看到响应里的 TTL 值这能帮你粗略判断对端系统类型。4.2 路由表与“下一跳”机制IP 层不负责把数据包一口气送到目标它只负责找“下一个路口”。每台主机和路由器上都有一张路由表数据包到达一个节点后节点查路由表、选择匹配度最高的下一跳、转发出去。用ip routeLinux或route -nWindows/macOS 通用可以看到自己的路由表default via 192.168.1.1 dev eth0 192.168.1.0/24 dev eth0 scope link第一条是默认路由第二条是本地子网路由。当你想访问 93.184.216.34 时系统会先判断它跟本机是否在同一子网如果在同一子网直接 ARP 找对方如果不在就把包发给默认网关 192.168.1.1由网关继续转发。这里有个很关键的理解整个过程中源 IP 和目的 IP 是端到端不变的但每一跳的源 MAC 地址和目的 MAC 地址都会变化。也就是说IP 地址是“终极门牌号”MAC 地址只是“一段路程里的临时标签”。4.3 MTU 与分片为什么大包总是莫名其妙地挂以太网链路 MTU 默认是 1500。如果 IP 数据报超过这个值路由器可能把包分片但分片之后要由接收方重组这个过程既消耗性能又容易丢包。更麻烦的是很多应用会设置 DFDont Fragment位禁止分片一旦路径上存在 MTU 不一致数据包就会被直接丢弃。排查 MTU 问题有个经典方法——强制用指定大小去 pingping -M do -s 1472 目标IP1472 是 1500 减去 28 字节IP 头 ICMP 头。如果这个命令不通而小包正常基本就可以判断路径上的 MTU 有问题。我排查过好几次“小请求正常、大请求超时”的故障最后都落在 MTU 不一致上。5. 链路层ARP、以太网帧与最后一公里5.1 以太网帧的结构IP 数据报封装成以太网帧后才真正能在物理链路上传输。一个经典以太网帧长这样目的MAC(6字节) 源MAC(6字节) 类型(2字节) 数据(46~1500字节) FCS(4字节)类型字段如果是0x0800代表上层是 IPv4如果是0x0806代表上层是 ARP。帧尾的 FCS 用于校验帧在传输过程中有没有损坏网卡发现 FCS 错误会直接丢帧。为什么帧数据部分最少要 46 字节因为早期以太网设计时为了保证冲突检测机制正常工作规定整个帧最短 64 字节。如果数据不够链路层会填充 padding。这个细节在抓包时也能看到不要以为是多余的数据。5.2 ARP怎么用 IP 找到 MAC前面说了数据包要通过交换机、路由器转发但真正在局域网内部传输时设备之间是用 MAC 地址通信的。那么问题来了我们知道目标 IP 是 192.168.1.1但它的 MAC 地址是什么答案是 ARP。主机在发送数据前会广播一个 ARP 请求who has 192.168.1.1? tell 192.168.1.100整个局域网内的设备都能收到这个广播只有 IP 是 192.168.1.1 的设备会回复单播报文192.168.1.1 is at 00:11:22:33:44:55之后发起方会把这条映射关系缓存在 ARP 表里。在 Linux 上可以用arp -a查看缓存用ip neigh刷新、删除指定条目。抓包时看到大量重复 ARP 请求通常说明目标设备的 ARP 缓存被清空或者网络里存在异常。5.3 交换机的转发逻辑帧从你的网卡发出去后第一站是交换机。交换机不像集线器那样无脑广播它会维护一张 MAC 地址表记录“哪个 MAC 在哪条端口下”。第一次收到未知 MAC 的帧时交换机会向所有端口泛洪等目标设备回复后再学到它的位置后续就只往对应端口转发。当数据跨网段时帧到达路由器后路由器会解开帧头、查看目的 IP、查路由表重新封装一个目的 MAC 为下一跳设备的新帧再发出去。这个过程反复多次直到数据到达目标主机所在的局域网。6. 服务器端的逆流水线从网卡中断到响应出门6.1 内核收包路径数据帧到达服务器网卡后流程几乎是“逆流水线”网卡通过 DMA 把数据写入内存中的环形缓冲区网卡触发硬件中断内核进入中断处理程序内核用 NAPI 机制轮询收包把数据封装成sk_buff套接字缓冲区IP 层校验、查路由确认是发给本机的TCP 层根据四元组源 IP、源端口、目的 IP、目的端口找到对应的 socket数据进入 socket 的接收队列应用进程被唤醒read()读取数据。这里有个常见的误解你写的服务端代码accept()一个连接并不意味着这个连接的所有数据都已经到内存了。accept()只是从内核的全连接队列里取一个已建立的连接真正的数据收发全在内核里异步进行。这也是为什么高并发服务往往需要配合多线程、事件循环来读数据——内核不等人read()不快接收队列就会堆积。6.2 应用解析请求与构造响应服务器进程从 socket 读到 HTTP 请求后接着做应用层的解析路由匹配、参数解析、业务逻辑处理最后构造 HTTP 响应。响应报文结构和请求类似状态行版本、状态码、原因短语、响应头、空行、响应体。几个常见状态码对应的网络层表现也值得记住状态码含义网络层表现200OKTCP 流程完整应用正常返回301/302重定向TCP 流程完整浏览器再发第二次请求403ForbiddenTCP 流程完整服务端拒绝404Not FoundTCP 流程完整资源不存在500Internal Server ErrorTCP 流程完整服务端业务异常502Bad Gateway网关层连接上游失败常伴随 RST504Gateway Timeout客户端等了太久常伴随对端无响应重点是除了 502/504 可能涉及连接建立失败或等待超时大多数 4xx/5xx 状态码在 TCP 和 IP 层看起来是完全正常的。抓包时如果只看到完整的三次握手、请求、响应、四次挥手那么问题就纯粹在应用层。6.3 为什么大响应会突然变慢窗口收缩与零拷贝服务端返回响应时同样要走 TCP 分段、IP 封装、链路层发送。这里最常见的一个性能陷阱是应用一次性write()了一大块数据但接收方窗口很小发送方只能按窗口大小分批发速度自然上不去。抓包时你会看到响应被切成很多段且每批之间都隔着很长的确认等待。这种“发一点、等确认、再发一点”的节奏就是窗口受限的典型特征。解决办法要么是让客户端尽快读走数据、避免接收窗口变小要么在服务端用零拷贝技术比如sendfile把文件数据直接从磁盘映射到网卡缓冲区减少内核态与用户态之间的拷贝次数。7. 收尾阶段四次挥手、TIME_WAIT 与连接复用7.1 挥手为什么需要四次HTTP 响应发完之后TCP 连接进入关闭阶段。正常关闭需要四次挥手主动方 → 被动方FIN, seqm 被动方 → 主动方ACK, ackm1 被动方 → 主动方FIN, seqn 主动方 → 被动方ACK, ackn1因为 TCP 连接是全双工的两个方向的数据通道是独立的。第一对 FIN/ACK 表示“我不再发数据了”但接收通道还开着第二对 FIN/ACK 才表示“我也不再发数据了”。所以关闭一个连接必须两个方向各关一次自然需要四次。主动关闭方在发出最后一个 ACK 后会进入 TIME_WAIT 状态等待 2 个 MSL报文最大存活时间目的有两个一是确保最后一个 ACK 如果丢了可以重发二是让旧连接的数据包在网络里彻底消失避免污染新连接。7.2 TIME_WAIT 过多与连接复用的矛盾很多后端服务一遇到高并发短请求ss -tan里会看到大量 TIME_WAIT 状态。这通常是健康现象TIME_WAIT 是主动关闭方在正常等待但如果量太大导致端口耗尽就要考虑连接复用了。HTTP/1.1 默认支持持久连接响应头里带Connection: keep-alive时客户端可以在同一个 TCP 连接上连续发多个 HTTP 请求从根本上减少创建连接的次数。HTTP/2 更进一步用多路复用让多个请求在一个连接上并发交错传输彻底避免浏览器对同一域名最多只能建 6 个 TCP 连接的瓶颈。开发者在优化 API 性能时优先考虑复用连接通常比增大服务端并发数更有效。毕竟一次 TCP 握手至少需要一个 RTT对跨地域访问来说这个成本远高于应用层本身的处理耗时。7.3 常见连接级报错排查实际生产环境里应用层经常报这么几个错每个都对应不同的 TCP 行为Connection refused目标端口没有进程监听内核直接回 RSTConnection reset by peer对端收到你不该发的数据或者对端异常关闭直接回 RSTOperation timed outSYN 重传多次没人理通常是防火墙丢包或对端宕机Broken pipe往一个已关闭的连接上写数据内核直接返回错误。遇到这类问题tcpdump比应用日志直观得多tcpdump -i eth0 host 目标IP and port 80 -w http.pcap抓到包之后重点看有没有 RST、有没有 Retransmission、SYN 有没有回包基本就能定位是哪一层出的问题。8. Wireshark 实测给整次请求拍一张 X 光片8.1 最常用的过滤与追踪流用 Wireshark 分析 HTTP 请求别一上来就乱抓。先明确过滤条件然后抓一小段就行场景过滤器只看 HTTP 报文http只看某个 IP 的通话ip.addr 目标IP只看某个端口的流量tcp.port 80按编号跟踪某条完整连接tcp.stream eq 0其中tcp.stream eq 0是我最常用的。它会展示某一条 TCP 连接从建立到关闭的全部报文不会混入其他流量。右键任意一个包选择 “Follow → TCP Stream”Wireshark 还会直接把请求和响应拼成一份完整的文本方便你确认 HTTP 报文本身是否正常。8.2 用时间戳判断“慢”到底慢在哪很多 HTTP 性能问题不是“能不能通”而是“为什么慢”。这时候抓包的意义就体现出来了。你可以用curl先做一个粗测curl -w DNS:%{time_namelookup} TCP:%{time_connect} TTFB:%{time_starttransfer} 总耗时:%{time_total} http://example.com/然后打开 Wireshark去看几个关键时间点客户端发 SYN 到收到 SYNACK 的时间差表示 TCP 握手 RTTSYNACK 到最后一个 ACK 的时间差一般为 0说明客户端响应迅速发送 HTTP 请求到收到响应首字节的时间差就是 TTFB响应首字节到响应结束的时间差取决于服务端发送速度和网络带宽。我习惯把四个时间点列出来再结合抓包里的 Expert Info比如有没有 Dup ACK、Out-of-Order、Retransmission基本能定位到问题出在“网络上”“服务端处理上”还是“应用代码上”。8.3 一次真实排查案例偶发 200ms 延迟是怎么找出来的之前遇到一个内部 API 偶发变慢客户端和服务端日志看都是正常的也没有报错但采样统计显示 p99 从 50ms 涨到了 200ms。抓包之后发现每次慢请求都伴随一次TCP Dup ACK和TCP Fast Retransmission而且发生在响应传输到中间某个片段时。进一步跟踪发现响应体里有 1460 字节的分段被丢弃了一次TCP 层快速重传RTT 因此多了一轮。为什么会丢链路层的某台设备上存在 MTU 不一致大段数据接近上限时容易出问题。后来把路径上的 MTU 做了统一p99 很快就恢复到了原来的水平。这种问题只看应用层永远看不到因为应用日志里 TCP 重传根本不报错。这也是我为什么强调HTTP 只是表象TCP/IP 才是底牌。排查经验丰富的工程师抓包上手速度往往比看半天日志快得多。最后再分享一个小习惯我会在每台开发机上把tcpdump和 Wireshark 都配好平时接口联调直接抓包看到 SYN/SYN-ACK/ACK 三次数齐了、HTTP 报文完整、挥手正常才敢真的说“这个接口通了”。网络分层带来的最大价值就是让你永远知道问题到底出在哪一层。
返回列表