1. 从一个抓包现场说起:为什么报文格式值得死磕
刚入行那会儿,我在一个后台服务项目里排查一个“偶发丢消息”的问题。日志里一切正常,应用层显示发送成功,但对面就是收不到。折腾了大半天,最后用抓包工具一看,问题出在一个很基础的地方:发送端把UDP报文发出去时,目的端口写错了,报文被对端内核直接丢弃,连个回执都没有。那一刻我才真正意识到,报文格式不是教科书上背一背就完事的考点,而是排查问题时唯一能“看到真相”的东西。
TCP、IP、UDP这些协议,名字天天挂在嘴边,但真让你把它们的报文结构一个字段一个字段画出来、说清楚每个字段在什么场景下会变、变了之后会引发什么现象,很多人是含糊的。这篇内容就是想把这件事讲透。它适合三类人:一是正在学网络基础、被各种“首部长度”“校验和”绕晕的初学者;二是做后端、运维、测试,需要靠抓包定位问题的从业者;三是准备面试、想把协议细节答扎实的人。我会从整体设计思路讲起,再逐个拆解报文结构,配上实操抓包和常见问题排查,尽量做到你看完就能自己动手验证。
核心关键词先摆出来:TCP报文格式、IP报文格式、UDP报文格式、首部字段、抓包分析、校验和、端口号、分片。这些词后面会反复出现,但不会堆砌,而是跟着实际场景走。
2. 整体设计思路:为什么协议要分成一层一层的报文
2.1 分层不是学术洁癖,是为了各管各的事
很多人学网络第一课就是OSI七层或TCP/IP四层模型,背得滚瓜烂熟,但没想过为什么要分层。我用一个生活化的类比:寄快递。你写一张快递单(应用层数据),装进一个纸箱(传输层加TCP或UDP首部),纸箱外面再套一层物流袋并贴上运单(网络层加IP首部),最后装车运输(链路层加帧头帧尾)。每一层只关心自己那部分信息,快递员不需要知道你箱子里装的是什么,他只认运单上的地址。
这样设计的好处是解耦。传输层不用管底层是以太网还是别的链路,网络层不用管上层是网页还是文件传输。每层在自己的报文里加上自己需要的控制信息,收方对应层再逐层剥掉。理解了这一点,你再看TCP、IP、UDP的报文格式,就不会觉得那些字段是凭空冒出来的——每一个字段都是为了解决某一层的某个具体问题。
2.2 三个协议各自解决什么问题
UDP最简单,它只做一件事:把数据从一个端口送到另一个端口,加上一个可选的校验和。它不保证顺序、不保证到达、不重传,所以首部只有8个字节。适合对实时性要求高、能容忍少量丢包的场景,比如音视频流、某些查询类请求。
TCP要复杂得多,它要解决可靠传输的问题:数据不能丢、不能乱序、不能重复,还要控制发送速度别把对方压垮。所以它的首部最少20字节,塞满了序列号、确认号、窗口、标志位这些控制字段。
IP是网络层的核心,负责把数据包从源主机路由到目的主机,跨越多个网络。它要处理寻址、分片、生存时间等问题,首部固定20字节,选项可变。
提示:很多人分不清“TCP报文”和“IP报文”的关系。记住一句话:TCP报文是IP报文的数据部分。也就是说,IP首部后面跟着的“数据”,很可能就是一整个TCP或UDP报文。抓包时看到的“帧”是链路层的,剥掉帧头才是IP包,再剥掉IP首部才是TCP/UDP段。
2.3 字段设计背后的取舍逻辑
拿“首部长度”这个字段举例。IP首部有个“首部长度”(IHL),占4位,单位是4字节,所以最大能表示15×4=60字节。为什么是4位而不是8位?因为IP首部固定部分才20字节,选项很少用,4位足够表达,省下来的位可以给别的字段。这就是协议设计里的位级精打细算,每一个bit都要花在刀刃上。
再看TCP的“窗口大小”字段,16位,最大65535字节。早期网络这个值够用,后来带宽大了不够用,就引入了“窗口缩放选项”来扩大。这说明协议不是一成不变的,字段不够用时会通过选项扩展,而不是推翻重来。理解这些取舍,你在看报文时就能明白哪些字段是“历史遗留”,哪些是“后来补丁”。
3. IP报文格式逐字段拆解与实操验证
3.1 IP首部整体布局
IPv4首部固定部分20字节,加上可选选项最多60字节。我按顺序把字段列出来,然后挑重点讲。
| 字段 | 位数 | 作用 |
|---|---|---|
| 版本 | 4 | 值为4表示IPv4 |
| 首部长度 | 4 | 单位4字节,最小5(即20字节) |
| 服务类型 | 8 | 用于QoS标记,一般不用 |
| 总长度 | 16 | 首部+数据的总字节数,最大65535 |
| 标识 | 16 | 分片时用于重组同一数据报 |
| 标志 | 3 | DF(禁止分片)、MF(还有分片) |
| 片偏移 | 13 | 分片在原数据报中的位置,单位8字节 |
| 生存时间TTL | 8 | 每经过一个路由器减1,减到0丢弃 |
| 协议 | 8 | 上层协议类型,6是TCP,17是UDP |
| 首部校验和 | 16 | 只校验首部,不校验数据 |
| 源IP地址 | 32 | 发送方IP |
| 目的IP地址 | 32 | 接收方IP |
| 选项 | 可变 | 一般不用,如记录路由、时间戳 |
3.2 总长度、标识、片偏移:分片三兄弟
这三个字段要连起来看。总长度告诉你这个IP包一共多大。标识是一个计数器,每发一个数据报加1,分片后所有片的标识相同,收方靠它判断哪些片属于同一个原始数据报。片偏移告诉你这个片在原始数据里的位置,单位是8字节,所以每个分片的数据长度必须是8的倍数(除了最后一片)。
为什么需要分片?因为链路层有MTU(最大传输单元),以太网默认1500字节。一个IP包如果超过MTU,要么在IP层分片,要么在传输层就避免。TCP通常通过MSS协商避免IP分片,UDP则不管,大了就分。
我实测过一个场景:用ping发一个超大包,指定不分片,看会发生什么。
ping -s 2000 -M do 192.168.1.1-s 2000表示数据部分2000字节,加上IP首部20字节和ICMP首部8字节,总共2028字节,超过1500。-M do表示禁止分片。结果会收到“Frag needed and DF set”的ICMP错误。这个实验能让你直观感受到MTU和分片的关系。
注意:分片是有代价的。只要有一个分片丢了,整个数据报都要重传。而且分片会增加路由器负担。所以现代网络里,TCP会通过MSS协商尽量避免分片,UDP应用则应该自己控制包大小。
3.3 TTL:一个包能活多久
TTL占8位,最大255。每经过一个路由器减1,减到0就丢弃,并回一个ICMP超时消息。这个设计是为了防止路由环路导致包无限循环。你可以用traceroute(Windows上是tracert)来观察TTL的变化,它本质上就是发一串TTL从1开始递增的包,看每个路由器返回的超时消息。
traceroute 8.8.8.8每跳显示的就是路径上的路由器。TTL默认值不同系统不一样,Linux常见64,Windows常见128。这个值在抓包时能帮你判断包的来源系统,也算一个小技巧。
3.4 协议字段:IP包里的“暗号”
协议字段告诉收方,IP首部后面的数据是什么。6是TCP,17是UDP,1是ICMP,2是IGMP。抓包工具就是靠这个字段决定怎么解析后面的内容。如果你看到协议号是6,但后面按UDP解析,那肯定乱码。这个字段只有8位,所以最多256种上层协议,够用了。
3.5 首部校验和:只查头不查身
IP首部校验和只校验首部20字节(或含选项),不校验数据。为什么?因为数据校验交给上层TCP/UDP做,IP层不重复劳动。计算方法是把首部按16位分组,求反码和,再取反。每经过一个路由器,TTL变了,校验和要重新算。这也是为什么抓包时看到的校验和有时显示“incorrect”,因为网卡可能做了校验和卸载,抓包抓到的是还没算好的值。
4. TCP报文格式:可靠传输的全部秘密
4.1 TCP首部整体布局
TCP首部固定20字节,加上选项最多60字节。字段比IP多,因为要管的事多。
| 字段 | 位数 | 作用 |
|---|---|---|
| 源端口 | 16 | 发送方端口 |
| 目的端口 | 16 | 接收方端口 |
| 序列号 | 32 | 本报文数据第一个字节的编号 |
| 确认号 | 32 | 期望收到的下一个字节编号 |
| 数据偏移 | 4 | 首部长度,单位4字节 |
| 保留 | 3 | 保留未用 |
| 标志位 | 9 | URG、ACK、PSH、RST、SYN、FIN等 |
| 窗口 | 16 | 接收窗口大小 |
| 校验和 | 16 | 校验首部+数据+伪首部 |
| 紧急指针 | 16 | 配合URG使用 |
| 选项 | 可变 | MSS、窗口缩放、SACK等 |
4.2 序列号和确认号:可靠传输的基石
这两个32位字段是TCP最核心的设计。序列号不是“第几个包”,而是“本报文数据第一个字节在整个字节流中的编号”。比如你发100字节,序列号是1000,那下一个报文的序列号就是1100。确认号是“我期望你下次发来的第一个字节编号”,等于“我已经收到了确认号之前的所有字节”。
这种设计叫字节流编号,而不是包编号。好处是即使包乱序、重传,收方也能按字节重组。我举个例子:发送方发了序列号1000(100字节)、1100(100字节)、1200(100字节)三个包,收方先收到1100,再收到1000,再收到1200。收方收到1100时,发现1000没到,不能确认1100,只能重复确认1000。等1000到了,收方可以一次性确认到1300。这就是累积确认。
提示:抓包时看序列号和确认号,能快速判断丢包和重传。如果看到同一个序列号出现多次,基本就是重传了。
4.3 标志位:TCP的状态机全靠它
标志位有9个,常用的6个:
- SYN:建立连接时用,同步序列号。
- ACK:确认号有效。
- FIN:发送方没有数据要发了,请求关闭。
- RST:重置连接,通常表示异常。
- PSH:告诉收方尽快把数据交给应用层,不要缓存。
- URG:紧急指针有效,很少用。
三次握手就是SYN、SYN+ACK、ACK。四次挥手是FIN+ACK、ACK、FIN+ACK、ACK。抓包时过滤tcp.flags.syn==1就能看到所有连接建立请求。
4.4 窗口:流量控制的阀门
窗口字段16位,表示“我现在还能接收多少字节”。发送方根据这个值调整发送速度,不能超过接收方的窗口。如果窗口变成0,发送方就停止发送,直到收到窗口更新。这个机制叫滑动窗口。
窗口大小会动态变化。接收方应用读取数据慢了,缓冲区满了,窗口就变小。抓包时看窗口值的变化,能判断是发送方发太快还是接收方处理太慢。
4.5 选项字段:TCP的扩展空间
选项字段让TCP能不断进化。常见的选项:
- MSS:最大报文段长度,只在SYN包里出现,告诉对方“我最多能收这么大的段”。
- 窗口缩放:把窗口字段左移N位,扩大窗口上限。
- SACK:选择性确认,告诉对方“我收到了哪些不连续的数据块”,提高重传效率。
- 时间戳:用于计算RTT和防止序列号回绕。
抓包时在SYN包里能看到MSS和窗口缩放,这些值决定了后续传输的效率。
5. UDP报文格式:简单到极致
5.1 UDP首部只有8字节
UDP首部就四个字段,每个16位:
| 字段 | 作用 |
|---|---|
| 源端口 | 发送方端口,可选 |
| 目的端口 | 接收方端口 |
| 长度 | 首部+数据的总长度,最小8 |
| 校验和 | 校验首部+数据+伪首部,可选 |
就这么简单。没有序列号、没有确认号、没有窗口、没有标志位。发出去就不管了,收不收得到看运气。
5.2 校验和:可选的保护
UDP校验和是可选的,IPv4里如果校验和字段为0,表示不校验。IPv6里强制校验。校验和计算要用到伪首部,包括源IP、目的IP、协议号、UDP长度。伪首部只是计算时临时拼出来的,不实际传输。
为什么UDP校验和可选?因为有些应用自己会做校验,比如某些实时音视频,丢一两个包无所谓,校验反而增加开销。但大多数情况下还是开着好,能发现数据损坏。
5.3 UDP的适用场景
UDP简单,所以快。适合:
- 实时音视频:丢包就丢包,重传反而卡顿。
- DNS查询:一问一答,包小,重传成本低。
- 广播/组播:TCP不支持。
- 游戏:对延迟敏感。
但UDP不保证顺序,应用层要自己处理。比如DNS用UDP,如果响应超时就重发请求。
6. 抓包实操:把报文格式看个透
6.1 工具选择与环境准备
抓包工具我用得最多的是Wireshark,跨平台,解析能力强。命令行下用tcpdump,适合服务器环境。安装就不说了,重点说怎么用。
先看本机有哪些网卡:
tcpdump -D选一个网卡抓包:
tcpdump -i eth0 -nn -vv-nn表示不解析主机名和端口名,-vv表示详细输出。
6.2 抓一个TCP三次握手
开一个终端抓包,另一个终端去访问一个HTTP服务:
tcpdump -i eth0 -nn 'tcp port 80'然后curl一个地址。你能看到三个包:
- 客户端发SYN,序列号假设是X。
- 服务端回SYN+ACK,序列号Y,确认号X+1。
- 客户端回ACK,确认号Y+1。
在Wireshark里展开TCP首部,能看到每个字段的值。重点看序列号和确认号的变化,以及标志位。
6.3 抓一个UDP包
DNS查询是典型的UDP:
tcpdump -i eth0 -nn 'udp port 53'然后dig一个域名。你能看到查询和响应两个UDP包。展开看长度字段和校验和。
6.4 观察IP分片
发一个超大ping包:
ping -s 3000 192.168.1.1在Wireshark里过滤ip.flags.mf==1 or ip.frag_offset>0,能看到分片。第一个片偏移0,MF=1;中间片偏移非0,MF=1;最后一片偏移非0,MF=0。标识字段相同。
注意:有些系统默认禁止ping发大包,或者防火墙拦截,可能看不到分片。可以在局域网内两台机器之间试。
7. 常见问题与排查技巧实录
7.1 抓包看到校验和错误,正常吗
经常有人问:Wireshark里IP或TCP校验和显示“incorrect”,是不是包坏了?大多数情况下不是。因为现代网卡支持校验和卸载,发送时网卡负责算校验和,抓包工具在网卡算之前就抓到了,所以看到的是没算好的值。接收端网卡会验证并剥离。如果你在接收端抓包还看到错误,那才可能是真错了。
7.2 为什么TCP有时会重传
重传的原因很多:丢包、超时、乱序、接收方窗口满。排查时先看序列号,如果同一个序列号出现多次,就是重传。再看时间间隔,如果间隔很短,可能是快速重传;如果间隔是RTO,可能是超时重传。结合确认号看,如果确认号一直不前进,说明接收方没收到。
7.3 UDP丢包怎么排查
UDP没有重传,丢了就是丢了。排查思路:
- 看发送端是否真的发出去了(抓包确认)。
- 看接收端是否收到(在接收端抓包)。
- 如果发送端发了接收端没收到,可能是网络中间设备丢弃,或者目的端口没监听,内核回ICMP端口不可达。
- 如果接收端收到了但应用没处理,那是应用层缓冲区满了。
7.4 端口号被占用怎么办
TCP和UDP的端口是独立的,同一个端口号可以同时被TCP和UDP使用。如果启动服务报“Address already in use”,先查谁占用了:
netstat -tulnp | grep 8080或者用ss:
ss -tulnp | grep 8080找到进程后决定是杀掉还是换端口。
7.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| TCP连接建立失败 | 防火墙拦截、端口未监听 | 抓包看SYN是否有响应 |
| 数据传输慢 | 窗口小、丢包重传 | 看窗口值和重传次数 |
| UDP收不到 | 端口未监听、网络丢弃 | 两端抓包对比 |
| 校验和错误 | 网卡卸载、真损坏 | 接收端抓包确认 |
| IP分片丢失 | MTU不匹配、中间设备 | 看分片标志和偏移 |
8. 几个容易踩的坑和我的实操心得
第一个坑:把序列号当成包编号。很多人以为序列号是1、2、3这样递增的包序号,其实它是字节编号。一个包可能携带1000字节,序列号就从1000跳到2000。理解这一点,看抓包时就不会困惑为什么序列号跳得那么快。
第二个坑:忽略伪首部。TCP和UDP校验和计算都要用伪首部,包含IP地址。这意味着如果NAT改了IP地址,校验和必须重算,否则收方校验失败。有些NAT设备处理不好这个,导致包被丢弃。排查时如果看到校验和错误且经过NAT,要考虑这个因素。
第三个坑:MTU和MSS混淆。MTU是链路层最大传输单元,以太网1500。MSS是TCP最大报文段长度,等于MTU减去IP首部和TCP首部,通常1460。TCP协商MSS是为了避免IP分片。如果你把MSS设得比MTU还大,就会分片,性能下降。
第四个坑:抓包位置不对。在发送端抓包看到包发出去了,不代表接收端收到了。中间可能被防火墙、路由器、负载均衡丢弃。排查网络问题一定要两端都抓,或者至少在关键节点抓。
第五个坑:UDP校验和可选导致的问题。有些系统默认关闭UDP校验和,如果数据在传输中损坏,收方发现不了,应用层拿到错误数据。对数据完整性要求高的UDP应用,应该确保校验和开启。
我个人的习惯是,遇到任何网络问题,先抓包,先看报文格式,先确认包到底有没有到、字段对不对。十次里有八次,问题在报文层面就能定位。报文格式这东西,背下来没用,用起来才有价值。你可以在自己机器上开两个终端,一个抓包一个发请求,把TCP、UDP、IP的每个字段都对照看一遍,比看十遍书都管用。后续如果想深入,可以研究TCP拥塞控制算法、IP选项的实际应用、UDP在QUIC里的改造,这些都是从报文格式延伸出去的方向。