首页 / 资讯中心 / 文章详情

一条HTTP请求的旅程:从封装到解封装的底层原理与抓包实战

一条HTTP请求的旅程:从封装到解封装的底层原理与抓包实战 ★ FEATURED ARTICLE
开写之前先说个感受很多后端、客户端开发常年工作在应用层天天调接口、发请求、接回调可真要问他“一条HTTP请求从你按下回车到对面服务器返回数据这一路上数据到底经历了哪些处理”能一口气讲清楚的人真不多。也不是大家不努力而是网络通信数据处理过程被操作系统、协议栈、中间件层层封装得很好开发者在上面写代码根本感知不到这些细节。但一旦线上出现超时、丢包、连接被重置、TCP重传率飙升不懂底层这套处理逻辑的人排查起来基本就靠瞎猜。这篇我用一条真实的数据从发送端到接收端的旅程作为主线把封装、寻址、转发、校验、解封装这些核心环节拆开揉碎尽量用大白话加实操记录的方式讲清楚。适合刚接触网络编程的初学者也适合后端开发、客户端开发、运维和测试同学用来补齐底层视野。1. 数据处理链路全景从应用到网线的完整路径先记住一个结论网络通信数据处理过程本质上是“分包—封装—传输—解包—重组”的循环。发送方把用户数据从高层往低层一层层包上信封接收方再逆着方向一层层拆开信封直到还原成原始应用数据。每层只关心自己负责的那点事不越权、不越级这就是整套体系能稳定运行六十多年的根本原因。1.1 为什么网络协议必须要分层你想想寄快递的生活场景。你写一封信把信纸装进信封写上收件人地址和邮编交给快递公司。快递公司不会看你信里写了什么只负责把你的信封从A城市运到B城市。到了B城市分拣中心根据地址把信送到收件人邮箱收件人拆开信封读信。在这个过程中“信的内容”“收件地址”“运输路线”是三个完全独立的问题每一层都可以各自优化而互不影响。网络通信就是照抄了这个分治思想。国际标准化组织定义的OSI七层模型也好实际工业界用的TCP/IP四层模型也好核心思路都是把复杂的通信问题拆成几个小问题每层解决特定的职责应用层解决“数据是什么”传输层解决“数据给谁”网络层解决“数据走哪条路”数据链路层解决“数据如何交给相邻设备”物理层解决“数据用什么信号表示”。每层只对相邻层提供清晰接口本层协议有更新不需要推翻其他层。这套分层设计带来的直接好处有三个。第一可替换性强某一层的实现从有线换成无线、从铜缆换成光纤其他层完全无感第二便于分工做传输层协议的人不需要关心应用层是跑网页还是跑视频第三有利于排障数据在哪一层出的问题就可以把排查范围精准锁在哪一层不至于乱成一锅粥。1.2 一条HTTP请求走过哪些环节我用一个最常见的场景把整条链路串起来你在浏览器地址栏输入某个网址并敲下回车一直到页面内容出现在屏幕上。这期间数据处理过程大致经历九个关键环节。第一个环节浏览器把URL、请求头、请求体组装成标准的HTTP报文。报文是纯文本内容对TCP/IP协议栈来说只是个无意义的字节流。第二个环节HTTP报文被交给传输层。以HTTPS为例此处会先经过TLS握手协商出对称加密密钥然后加密后的数据再交给TCP协议。TCP层干的第一件事是给数据分段因为下面网络的承载能力有限不可能把一大整块数据直接丢出去所以要按合理大小切块每一块叫一个TCP段。同时TCP要为每个段添加端口号、序号、确认号等头部信息。第三个环节每个TCP段被交给网络层也就是IP协议。IP层负责在头部填上源IP和目标IP地址形成IP数据报然后查询路由表决定下一跳该往哪儿送。第四个环节IP数据报被交给数据链路层这里要加MAC头、帧尾封装成以太网帧通过物理网卡把数据变成电信号或光信号发送到网线上。第五个环节数据在沿途经过多个路由器每个路由器都只做一件事拆到IP层看一眼目标地址决定转发给下一个设备然后再重新封装成帧发出去。第六个环节数据到达目标服务器的网卡服务器从物理层开始逆向上推。网卡先把电信号还原成比特流数据链路层校验帧的完整性去掉MAC头部交给IP层IP层去掉IP头交给TCP层TCP层根据端口号找到对应的进程把字节流按序号重新拼装完整。第七个环节解密后的HTTP报文被交给Web服务器程序。第八个环节服务器处理请求并生成HTTP响应然后走一遍完全相同的封装流程返回给浏览器。第九个环节浏览器收到响应解析HTML、CSS、JavaScript渲染出页面。你会发现发送和接收的处理过程完全对称只是方向相反。理解了这个对称性后面看任何抓包数据都会觉得顺理成章。2. 核心细节解析报文封装与逐层头部信息这层窗户纸捅破之后还要往深处再走一层。真正让数据能正确送达的是一整套精细的头部字段设计。很多人对网络通信的理解停留在“有IP地址和端口号就能通”但实际每条报文里的关键信息远不止这两个。2.1 传输层核心概念端口、序列号与确认机制传输层负责把数据可靠地交付给目标进程。这里的关键是端口号。端口号本质上是进程在主机内部的编号HTTP默认80、HTTPS默认443这些是共识端口好比总机号码真正的数据交互用的是动态协商的临时端口就好比分机号码。一个TCP连接用四元组唯一标识源IP、源端口、目标IP、目标端口。两个不同的TCP连接不可能同时拥有相同的四元组。比端口号更值得深入理解的是TCP的序列号和确认机制。TCP是流式协议应用层不管发多少字节TCP都会给每个字节编号。首字节序号通常不是从0开始的而是双方在握手时各自随机生成一个初始序列号ISN。发送方每发一个数据段头部里带着本段第一个字节的序号接收方收到后确认确认号表示“你下一个该发序号为多少的字节给我”。这套机制看起来简单但它是可靠传输的基石。发送方发了一份数据如果在超时时间内没有收到确认就认为丢了重传一次。接收方收到重复数据靠序号识别并丢弃重复内容数据乱序到达也靠序号重新排序。所谓“可靠传输”并不是保证数据一定不丢而是保证丢了能被发现、能重传、最终接收方能恢复出正确的顺序。生活化类比就是寄快递时你填了一张回执单对方收到后在回执单写下“第几箱收到了”你没收到回执就知道要补寄。2.2 网络层与链路层核心概念IP地址、路由与MAC地址IP层的工作重点是“把数据送到目标主机所处的网络”。IP地址是逻辑地址好比你的家庭住址它是分层规划的、可以变化的MAC地址是物理地址好比你的身份证号理论上全球唯一、烧录在网卡里。IP层只关心目标IP是多少然后查路由表决定“下一步交给谁”至于这个“谁”用什么物理手段接包是链路层的事。这里有个新手容易混淆的点IP数据报在每一跳传输时MAC地址都在变化但IP地址始终保持不变。源MAC写当前设备的网卡地址目标MAC写下一跳设备的网卡地址。数据每经过一个路由器路由器的链路层就把上一跳的MAC头剥掉重新封装一个新的MAC头给下一跳。这就像你从北京开车到广州导航目的地始终是广州但你在河北境内时GPS只告诉你“下一个路口右转”这个“下一个路口”就是MAC地址每一次转向都会刷新。还有个非常关键但经常被忽略的字段是TTL。IPv4中用TTL字段限制数据在网络中的生存时间每经过一个路由器就把TTL减1减到0时路由器直接丢弃并返回一个ICMP超时报文给发送方。设计这个字段的目的是防止数据包因为路由环路在网络里无限兜圈子。常见操作系统的默认TTL不尽相同Linux默认64Windows默认128很多排障场景里看丢包和超时第一反应就是检查TTL。链路层的MTU值同样影响深远。以太网标准MTU是1500字节意味着IP数据报超过1500字节就必须分片或者由传输层避免产生超大报文。IPv4允许在网络层分片但分片会带来性能损耗和重组丢包风险所以现代网络实践倾向于使用路径MTU发现机制提前找到端到端不触发分片的报文大小上限。TCP的MSS就是根据MTU减去IP头、TCP头算出来的“单段数据最大字节数”通常为1460字节。理解了这两参数的关系后面看抓包里面分段长度为什么集中在一千四百多字节就有数了。2.3 五元组与连接四元组的实际意义把上面这些信息整合成一个看问题工具就非常有效五元组。五元组由源IP、源端口、目标IP、目标端口、协议号组成。这是网络通信中最常用的识别维度基本所有防火墙规则、负载均衡会话保持、NAT会话表、连接跟踪模块都靠五元组来决定流量怎么处理。协议号用来区分上层协议TCP是6UDP是17ICMP是1。所以排查数据不通时先确定五元组的每一部分是否可到达是最高效的路径。比如客户端访问服务器不通先ping测试网络层通不通通的话再测目标端口是否开放端口不通就用telnet或nc测试应用层的报错再去抓包看传输层的响应。这种逐层排除法的本质就是按协议分层的思路去定位问题比拿一堆工具乱试靠谱得多。3. 实操过程用抓包观察TCP三次握手与数据传输理论讲得再多不如亲手抓一次包印象深。我在学习网络通信时走过不少弯路最早是干看书看不进去后来发现只要在实验室里把一条TCP连接从建立到关闭的全过程抓下来对照着看一遍很多概念瞬间就通透了。3.1 搭建可控环境的准备工作要抓包先得有可控的实验环境。我建议你准备一台普通电脑、一台路由器或一台能开启热点的手机会面然后用Wireshark抓本地回环接口或无线网卡的包。有一个比Wireshark更适合命令行的选择是tshark它是Wireshark的命令行版本抓完直接输出文本数据结构对于本文这种需要展示字段的场景反而更清晰。我常用三条命令。第一条捕获所有流量实时打印关键字段tshark -i eth0 -T fields \ -e frame.number -e ip.src -e ip.dst \ -e tcp.srcport -e tcp.dstport -e tcp.flags第二条约在抓包期间只盯某个TCP连接tcpdump -i eth0 -nn tcp port 80 -w /tmp/http.pcap第三条查看抓包文件的概要信息tshark -r /tmp/http.pcap -Y tcp -T fields \ -e frame.time_relative -e tcp.seq -e tcp.ack \ -e tcp.len -e tcp.flags.str抓包工具本身不产生流量所以你要在抓包开始后再触发一次请求比如用curl随便请求一个http站点。直接用curl的好处是干净可控没有浏览器各种扩展和预连接的干扰。命令参考curl -v http://example-site.com/3.2 三次握手抓包数据解读我在环境里实际执行了一次访问抓到整个交互过程的简写输出如下这是典型的HTTP请求往返过程。第一次握手客户端发送SYN报文序列号是一个随机生成的初始值假设为1000标志位SYN置1不携带数据。第二次握手服务器回应SYNACK报文确认号是1001意思是“我收到了你的初始序号1000下一步请从1001开始发”同时服务器也告诉客户端自己的初始序号假设为5000。第三次握手客户端发送ACK报文确认号是5001意思是“我收到了你的初始序号5000”这个报文可以同时携带应用层数据。逻辑无比清爽。握手的核心目的有两个一是双向沟通初始序列号让双方后续数据编序号有了共识二是验证双方的收发路径都是通的。因为是双向通信所以要完成三次而非两次第二次握手的SYN和ACK合并到一个报文里算是一种常见的优化手段。实际抓包时你还会发现第三个ACK报文后面往往紧跟着客户端的HTTP请求数据段这叫TCP快速打开的特性优化目标是减少一个RTT的往返延迟。很多新版本内核默认开启这种优化所以你在抓包软件里看到的实际序号并不是刚好从0开始而是加了随机偏移。3.3 数据传输阶段的序列号与确认号演进连接建立后数据传输的交互模式变得更加机械但精确。客户端申请发送HTTP请求内容长度假如是150字节就会形成一个TCP段头部SEQ是1001数据长度150随即发出。服务器收到后会回复一个ACK报文ACK值变为1151表示“请从1151开始发下一个段”。服务器随后单独发送HTTP响应假设响应体2000字节它会把响应分成合适的TCP段比如前两个段是1460字节和540字节避免超过MSS。每个段都带着自己的序号和长度客户端每收到一个段就回复对应的ACK。抓包时最容易感动人的一幕是服务器连续发出两个段后客户端并不一定每段都回一个ACK而是可以把两个段的确认合并在一个ACK里这就引出了延迟确认机制。延迟确认的意图是减少网络上的小报文数量因为一小段数据就要回一个单独的空ACK包在大量小文件的场景下简直是带宽浪费和CPU损耗。后来TCP又增加了Nagle算法做相反方向的优化把发送方的小数据块攒起来批量发送。这两个机制叠加能让TCP连接上的报文数大幅下降代价是增加了一点延迟。还有一个实操中经常遇到的字段是窗口大小。接收方在每个ACK或报文中都会带上自己的接收窗口值表示自己当前还能接收多少字节。发送方必须确保在途未确认数据量不超过对方的接收窗口。这个值会动态变化应用程序读取数据越快窗口越大读取慢窗口就会被压得很小。真正能跑满带宽的高性能服务器都会想方设法保证接收窗口足够大必要时开启TCP窗口缩放选项让窗口上限从默认的64KB提升到GB级别。3.4 数据传输中容易踩的坑粘包与半包在应用层写网络程序时牢牢记住一件事TCP是字节流协议它不保留消息边界。你调一次send写入500字节对端recv时可能一次性读出500字节可能先读出200再读出300也可能再次调用还能把后面紧跟着的另一块数据一起读出来。这正是“粘包”和“半包”问题出现的原因。具体来说粘包是多次send的数据被接收方一次读出好像粘在了一起半包是一次send的数据被拆成多次读出接收方读到的不是完整的一条消息。由于这两个现象在TCP下都是完全正常的应用层必须自己定义消息边界。通用做法有四种按长度前缀4字节大端整数头消息体、按固定分隔符行尾用换行符、按固定长度报文以及带类型字段的自描述格式。HTTP协议就同时用了固定分隔符和长度信息请求头以空行结束正文长度用Content-Length指定。理解了这一点以后再写自定义TCP协议就不会犯“接收方循环recv一次处理一条”的低级错误。务必先收够头部长度再按头部里声明的正文长度读完整条消息再进入下一条数据处理循环。4. 常见问题与排查技巧实录把链路和封装细节都理解到位以后再遇到网络问题就不再是玄学。这一章我把我从业以来真正踩过、并且反复被别人问到的几个典型问题整理出来每条都附上排查思路和解决手段。4.1 TCP连接无法建立的三种典型表现遇到“客户端连不上服务器”的第一反应不要瞎猜先去抓包。我归纳过三种高频原因每一种的抓包表现完全不同。第一种客户端发出SYN后没有任何响应抓包只看到一个SYN。这种情况大概率是防火墙把SYN包静默丢弃了目标端口根本没有进程在监听时服务器通常会回一个RST包而不是毫无反应。所以看到SYN石沉大海优先查防火墙规则、安全组配置。第二种客户端发SYN后收到RST。端口没监听、服务进程没启动、或者对方主动拒绝连接这是最常见的两个方向。如果目标机上确实没有程序监听那个端口TCP协议栈会礼貌地回一个RST包告诉你“别等了”。第三种客户端SYN发了多遍中间隔了指数退避的时间间隔仍然没收到响应。这就需要考虑物理链路问题、IP配置错误、或者中间设备丢包。我建议用分段定位法先在客户端上ping网关再ping目标IP接着用nc测端口最后抓包看SYN真正走到了哪一跳。配合traceroute看每一跳的路径基本十五分钟就能定位。4.2 大量TIME_WAIT连接堆积的处理经验许多压力测试场景结束后你会在服务器上看到大量TIME_WAIT状态的连接。TIME_WAIT是主动关闭连接的一方在发出最后一个ACK后进入的状态保持时长是2MSL通常是60秒左右。它的作用是确保最后一个ACK能被对端收到以及让网络中旧的报文自然消亡不干扰后续的新连接。但如果量太大本地端口被占满服务就会出现“端口不够用”的问题。我在一次压测中就遇到服务器TIME_WAIT多达几万个的情况。处理方向有两个。一是应用层优化让长连接复用二是内核参数调优适当减小TIME_WAIT的超时时间或者开启连接复用选项。但这类参数不能无脑改必须在业务允许的情况下再调整。千万不要为了立竿见影就直接把回收时间设为0否则可能引发报文错乱带来更隐蔽的故障。另一个很多初学不知的小细节是主动关闭连接的一方才有TIME_WAIT。如果你看到服务器上大量TIME_WAIT通常说明服务端在主动断开连接这时候与其纠结内核参数不如先查一下应用代码里为什么会频繁close连接。4.3 TCP重传与网络延迟的关系判断线上服务偶发超时排查时最常碰见的就是TCP重传。抓包里面看到大量重传的报文这是网络抖动或接收端处理不过来的信号但来源要分清楚。第一种发送方超时没收到ACK而重传。这往往是网络的往返时间变长或者中间丢包严重。用ping和MTR观察各跳的丢包率重点看最后一跳到达目标服务器前的网络状况。如果只有最后一跳有延迟和丢包八成是目标服务器负载过高、软中断处理不过来、接收队列满了导致内核把包丢弃。第二种快速重传。接收方收到次序不对的数据段会立刻发送三个重复的ACK给发送方发送方收到后不等超时直接重传。这种情况说明网络乱序现象比较严重多半是链路负载不均衡或者中间设备的缓存策略导致部分数据被临时延迟。排查重传问题时还要顺手看一眼接收窗口和MSS。接收窗口持续为0说明接收方处理速度跟不上MSS两端不一致会导致路径中较大的报文被中间设备分片增加丢失概率。TCP协议栈里面每个字段都不是白设计的出问题的时候它们都是突破口。4.4 HTTPS请求慢浏览器一直转圈怎么定位这个场景我最近半年已经帮不同的人排查过三次了。现象是网页打开特别慢但点击刷新后有时又突然变快看似毫无规律。抓包之后发现慢在TLS握手阶段握手需要多个RTT进行密钥协商和证书传输如果客户端和服务器之间的链路往返延迟本来就偏高TLS握手带来的额外时延会被放大。排查思路分两步走。第一步看服务器数量如果服务器在多个地域负载均衡客户端每次连接可能被调度到不同距离的节点上某些节点链路本身质量差就会导致握手缓慢。第二步用openssl s_client观察握手耗时openssl s_client -connect target-site.com:443 -servername target-site.com /dev/null 2/dev/null单独测几次如果每次耗时波动非常大大概率是调度或网络路径不稳定。如果每次都稳定慢建议优先检查证书链是否包含中间证书有时不完整会导致需要额外下载证书再校验再检查TLS会话复用是否开启开启后可以在短时间内跳过完整的握手流程每次请求至少省下两个RTT体验改善非常明显。4.5 一张速查表把网络排查的常见手段串起来手边备一张表往往是在生产环境快速恢复的最有效武器。我把排障中最常用的观察对象、命令和对应问题整理成表格方便照着做。观察对象常用命令揭示的问题连通性ping网络层通不通、丢包率、延迟抖动路由路径traceroute / MTR哪一跳延迟高、哪一跳丢包端口开放telnet / nc -vz目标端口是否监听连接状态netstat -anpLISTEN、ESTABLISHED、TIME_WAIT等状态分布重传情况tcpdump / tshark 过滤tcp.analysis.retransmission重传率、快速重传频率收发缓冲ss -m / topt发送和接收队列是否积压请求耗时curl -w各项时间连接建立、TLS握手、首字节各阶段耗时CPU软中断top中 %si / mpstat网卡处理是否成为瓶颈再次强调一个核心方法无论你用哪张表都要先明确自己当前要查的目标是哪一层。链路层问题不会因为应用层重试而消失网络层通也不代表端口肯定通端口通了也不代表应用层逻辑没问题。一层层缩圈永远比一个命令一把梭快得多。5. 我的实际操作体会这几年看下来网络通信数据处理过程最反直觉的地方在于它明明极精密却在绝大多数时间里运转得像空气一样透明。框架封住了细节操作系统隐藏了细节中间设备又吞掉了更多细节结果就是平时无事发生一旦出事整个链路的知识缺口全部暴露出来。我自己补这块短板最有效的方法就是在测试环境反复抓包看真实报文。不要只在理论上看状态图而是真正发起一次七层转发请求把每个包的Seq、Ack、Flags、Window一字一字地对着看。一开始速度很慢但大概是坚持抓了二十多次不同场景的包之后那些记忆零散的协议字段就自然串成了一张有结构的图。此后排障时我心里就始终有一条从物理层到应用层的竖线从下往上逐层排除而不是东一下西一下试运气。最后说个小技巧平时没事干的时候可以开着抓包工具放在后台正常访问几个常用站点然后回放一天的抓包记录试着从报文中推测当时的应用行为。比如看到一个TCP段长度稳定在一千四百多字节你就知道它做过了路径MTU协商看到客户端发出四个相同的ACK紧接着服务端重传你就知道当时发生过乱序。这些观察多了以后你会比大多数开发更懂“数据到底是怎么走的”这件事。网络不是慢是细节太多看多了自然就不慌了。
阅读完成 · 觉得有帮助?
咨询建站