1. 先想清楚:TCP 实验为什么必须靠抓包来学
标题写着 Wireshark 与 TCP,说白了就是一件事:把一条 TCP 连接从建立到断开的全过程,一个字节一个字节地摊在屏幕上。教科书里的三次握手、四次挥手、滑动窗口、拥塞控制,全是状态机图和公式,看完合上书,脑子里其实还是空的。但只要你在 Wireshark 里亲手点开一个 SYN 包,看到Flags: 0x002 (SYN)、Sequence Number: 0、Window Size: 64240、Maximum Segment Size: 1460,那些抽象概念立刻就落到了具体字段上。这就是抓包实验不可替代的地方:它把协议规范变成了可以点开、可以对照、可以验证的物证。
我最早做这类实验的时候,最大的困惑其实在于不知道该看什么。抓包一开,几十万行报文哗啦啦往下滚,80% 是各种后台心跳、系统更新、域名解析,真正想看的那条连接藏在里面根本找不到。后来才明白,抓包这件事的核心能力在于“裁剪”,而非“抓”:先把无关流量砍掉,再针对剩下的一小撮做逐字段分析。所以这篇内容我会从实验设计讲到环境搭建,再讲过滤器体系、握手拆解、序号分析、窗口与吞吐估算,最后把翻车实录和交付物整理都过一遍。适合刚接触网络协议分析的同学,也适合已经能抓包但总在“抓到了却看不懂”这一步卡住的人。
1.1 抽象状态机与真实报文之间的鸿沟
TCP 协议规范里,状态迁移图有十一个状态:CLOSED、LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、CLOSING、LAST_ACK、TIME_WAIT。只看图,你很难理解为什么需要一个 CLOSING 状态,也很难体会到 TIME_WAIT 为什么必须存在两倍报文寿命那么久。而在抓包里,CLOSING 状态对应的是“双方几乎同时发 FIN”这种极端情况:你能看到两个方向的 FIN 交叉出现,随后各自回 ACK,而不是标准的 FIN-ACK-FIN-ACK 顺序。这种场景在实验环境里需要刻意构造,但一旦构造出来,那个状态就再也不会忘了。
同样的鸿沟还体现在“可靠传输”上。教材说 TCP 用序号和确认号保证有序到达,听起来理所当然。但你在抓包里看到[TCP Previous segment not captured]、[TCP Fast Retransmission]、[TCP Retransmission]、[TCP Out-Of-Order]这几种标注时,才会真正意识到:所谓可靠,是靠在不可靠的链路上不断试错换来的,而每一种异常标注背后都对应一套不同的判定逻辑。Wireshark 的 TCP 分析器本质上是在帮你做一遍协议推理,它标注“快速重传”,是因为它看到三个重复 ACK 之后紧跟一个回退序号的段;它标注“乱序”,是因为它看到序号跳跃但后续又补上了前面的缺口。把这些标注和实际报文对照着看,比读十遍 RFC 都管用。
1.2 抓包点的三种选法与代价对比
实验设计的第一件事,是决定在哪里抓。位置选错,后面所有分析都白费。常见的三种选法,各自的成本和能看到的视野完全不同:
| 抓包位置 | 能看到什么 | 代价与局限 |
|---|---|---|
| 端点本机抓包 | 完整的双向数据流,包含应用层载荷 | 看不到中间设备行为;本机回环需要额外适配 |
| 交换机镜像口 | 多台主机的全部流量,适合观察并发连接 | 需要设备权限,镜像口容易丢包,时钟与主机不同步 |
| 中间主机代理转发 | 可以人为制造延迟、丢包、乱序 | 引入了额外一跳,时序被改写,分析结论需谨慎 |
我通常优先选端点抓包。原因很实际:实验的目的是理解协议行为,不是复现真实生产链路。端点抓包拿到的报文,序列号、确认号、窗口值都是最原始的,不会被中间设备改写;而经过代理或镜像的流量,往往会出现时序被压缩、窗口被调整、甚至 MSS 被重写的情况,初学者很容易把中间设备的行为误认为协议本身的特性。如果确实要做丢包与重传的实验,再考虑引入可控的中间环节,并且明确记录“这一段是我们人为加进去的”。
提示:不要在实验里同时改动多个变量。既换抓包点又加人为延迟,最后分析出的异常根本归因不了。
2. 环境准备:让抓到的每一个字节都可信
环境这一步,网上教程大多一笔带过,但它决定了后面所有分析的可信度。我见过太多人抓着抓到的包来问“为什么校验和全是错的”“为什么时间戳差了 3 秒”“为什么只抓到一半的流量”,最后发现全是环境问题。先把工具链理清楚:抓包引擎负责从网卡把帧捞上来,分析器负责解析和呈现。两个环节都可能出问题。
实际动手之前建议做三件事:确认网卡型号与驱动是否支持需要的模式;确认抓包权限,普通用户往往只能看到自己进程的流量;确认磁盘空间,一个满速千兆链路抓一小时,文件能到几十 GB 的量级。这三件事任何一件不到位,实验都会在中途崩掉,而且崩得很莫名。
2.1 安装、权限与驱动那一堆坑
抓包工具有两个部件需要分清:底层的抓包库和上层的图形界面。抓包库负责和网卡驱动打交道,部分平台需要单独安装,安装时会顺带装一个虚拟的回环网卡,用来抓本机内部进程之间的通信。这里第一个坑就来了:如果你的实验是“本机浏览器访问本机上的服务”,流量根本不经过物理网卡,只走回环,而回环网卡的名称往往不叫常见的那个名字,你得在接口列表里专门找带 “Loopback” 字样的那一项,否则抓半天一个包都没有。
第二个坑是权限。在类 Unix 系统上,抓包需要对网卡做原始访问,普通用户默认没这个权限,通常需要把用户加入特定用户组,或者用提权方式启动。但提权启动之后,配置文件、插件、最近打开的文件列表都跟着变了,你会觉得“怎么和我平时用的不一样”。我个人的做法是固定用非提权的图形界面加已配置好的权限组,需要提权的批量处理另走命令行工具,两边互不干扰。
第三个坑是驱动与网卡的卸载计算。现代网卡为了性能,会把校验和计算、分段重组这些活交给硬件做。结果就是:抓包库在数据离开网卡之前把它截下来了,此时校验和字段还是空的或者错的,TCP 分段也还没切。于是你在分析器里看到一片“校验和错误”和超大尺寸的 TCP 段,吓得以为链路出问题了。这属于典型的抓包位置导致假象,处理办法是关闭对应网卡的卸载功能,或者在分析器的协议设置里关掉校验和验证。做实验时我倾向于前者,因为关掉验证只是不报警,报文本身还是错的,容易误导后续判断。
2.2 时间戳、缓冲与文件格式的取舍
时间精度这件事,在分析重传和延迟时特别关键。默认情况下抓包库给的时间戳精度是微秒级,普通实验足够用了;但如果要观察微秒以下级别的间隔(比如高速链路下的短突发),就需要显式开启纳秒精度,而且文件格式必须选支持高精度时间戳的那种。老式的抓包文件格式精度有限,转存的时候精度会被截断,这个细节经常被忽略,导致两组数据对比时“时间对不上”。
缓冲策略也得提前想。抓包库默认会攒一批数据再一次性写入磁盘,减少 I/O 次数。好处是性能高,坏处是一旦程序崩溃或者机器断电,缓冲区里那部分就没了。实验时如果只是想抓几十秒的握手过程,可以把缓冲关掉或者设得很小,保证数据实时落盘;如果要做长时间的大流量采集,就必须开缓冲,并且定期切分文件,避免单个文件过大导致后续打开和分析都卡死。切分文件除了控制体积,还有一个隐性好处:每段时间对应一个明确的实验动作,事后定位特别快。
文件命名建议带上前缀、时间、场景三个要素。我见过太多“新建文件夹 (3)”里的“capture.pcapng”,过两天自己都不知道哪个是握手、哪个是重传。
2.3 造流量:可控的对端比真实公网更好用
实验里最忌讳的就是拿真实公网服务当唯一的流量源。原因很简单:你控制不了对端。对端的窗口大小、拥塞算法、是否启用选择性确认、是否中途重置连接,全在别人手里,你只能被动观察。一旦分析结论和对端行为耦合,实验就失去了可重复性。
可控的对端有几种做法。第一种是在同一台机器上起一个简单服务,用回环地址访问,好处是零外部依赖,坏处是回环链路的往返时间极短,很多和时间相关的特性(重传超时、延迟确认)根本触发不出来。第二种是在局域网内的另一台机器上起服务,这样有真实的链路往返,能观察延迟确认和窗口更新。第三种是用脚本精确控制服务端行为,比如故意在握手中不接受某个选项、故意只发一半数据就停住、故意不回 ACK,用来构造实验需要观察的异常场景。
我自己做重传实验时,通常用一个很小的自定义服务端,配合发送端的分段逻辑,人为制造“接收方不回 ACK”的状态,然后在发送端抓包,等重传超时自己触发。这样抓到的重传包时间间隔接近协议实现里的初始超时值,而且不受外界干扰,非常适合写进报告。
3. 过滤器体系:先把 99% 的噪声砍掉
过滤器是整个实验里最有性价比的技能。一条连接可能只有几百个包,但网卡上同时跑着几百条连接,总量几十万包。如果你不知道过滤语法,就只能靠肉眼在滚动的列表里找,效率低到无法接受。过滤体系分两层:抓包时用的底层过滤,和抓包后用的显示过滤。前者决定哪些包被写进文件,后者决定哪些包被显示出来。这两层的语法完全不同,混淆是初学者最常见的错误之一。
我的建议是:抓包过滤尽量宽松,显示过滤尽量精细。理由很充分,抓包过滤一旦写错,丢掉的包永远找不回来,只能重抓;显示过滤写错了,改一下立刻生效,成本几乎为零。所以除非流量量大到磁盘扛不住,否则不要在抓包阶段做过度裁剪。
3.1 抓包过滤与显示过滤,别用反了
抓包过滤用的是经典的底层过滤语法,关键词是host、port、net、tcp、udp这类,运算符是and、or、not,取反还可以写成!。它的特点是粒度粗,只认地址和端口,不认协议字段。比如想只抓某个测试地址的 8080 端口,写成host 192.0.2.10 and tcp port 8080,这就够了。
显示过滤走的是另一套语法,能深入到每一个协议字段。同样是端口条件,要写成ip.addr == 192.0.2.10 && tcp.port == 8080。运算符用&&、||、!或者and、or、not都行,但字段名必须完全正确,写错一个字母就会变红。它最强大的地方是能判断标志位和解析状态,比如“只看 SYN 包”、“只看被标注为重传的包”、“只看某一方向的数据段”,这些用抓包过滤根本表达不出来。
记住一个判断标准:语法里出现点号加字段名的,是显示过滤;语法里只有地址、端口、协议名的,是抓包过滤。
3.2 常用的 TCP 显示过滤速查
下面这张表是我做实验时最常用的一批,基本覆盖了从握手到收尾的全部观察需求:
| 观察目标 | 显示过滤表达式 |
|---|---|
| 只看某条连接的握手首包 | tcp.flags.syn == 1 && tcp.flags.ack == 0 |
| 只看握手回应包 | tcp.flags.syn == 1 && tcp.flags.ack == 1 |
| 只看纯确认包(无载荷) | tcp.flags.ack == 1 && tcp.len == 0 && tcp.flags.syn == 0 && tcp.flags.fin == 0 |
| 只看结束包 | tcp.flags.fin == 1 |
| 只看重置 | tcp.flags.reset == 1 |
| 只看所有异常标注 | tcp.analysis.flags |
| 只看重传 | tcp.analysis.retransmission |
| 只看快速重传 | tcp.analysis.fast_retransmission |
| 只看重复 ACK | tcp.analysis.duplicate_ack |
| 只看零窗口通告 | tcp.analysis.zero_window |
| 只看带载荷的数据段 | tcp.len > 0 |
| 只看某个端口 | tcp.port == 8080 |
| 只看某个方向 | ip.src == 192.0.2.10 && tcp.dstport == 8080 |
表格里最值得单独说的是tcp.analysis.flags。它是个聚合条件,只要分析器给这个包打上了任何异常标记,它就被选中。做初步筛查时先跑这一条,能快速定位到整条连接里所有可疑位置,然后再用更精确的条件逐个展开,效率高得多。
3.3 用 tcp.stream 把一条连接单独拎出来
真正好用的是流编号。分析器会自动把属于同一条连接的报文归到一个编号下,从 0 开始递增。你只要在任意一个包上右键,选“追踪 TCP 流”,就能看到这条连接的全部内容按方向分色显示,同时过滤栏里会自动填入tcp.stream eq 3这样的表达式。这一步是分水岭:从“面对几十万包”变成“面对几百包”,后面所有分析都在这几百包里做。
流编号还有一个隐藏用途:当你需要对比多条连接的行为差异时,可以先把候选流号提取出来,再用tcp.stream eq 3 || tcp.stream eq 7这样的条件同时显示,横向比较两条连接的握手选项、窗口变化、重传次数。做实验报告时这个技巧非常省事,能直观看出“同样的服务,为什么一条快一条慢”。
提示:流编号是分析器在本次会话里动态分配的,换一个文件重新打开,编号可能变化。写报告时如果引用了流号,务必同时写下连接的四元组,否则将来别人无法复现你的定位。
4. 逐字段拆三次握手与四次挥手
握手和挥手是实验的必修段,但很多人只看了标志位就结束,其实信息密度远不止于此。真正有价值的字段藏在选项区:最大段长度、窗口缩放因子、选择性确认许可、时间戳。这几个选项决定了整条连接后续的传输性能上限。可以说,连接性能的上限在握手那一刻就已经被谈妥了,后面所有的窗口调整都是在这个谈判结果里做文章。
我的习惯是把握手三个包并排看,重点盯三个东西:双方各自声明的最大段长度是否一致、是否都带了窗口缩放、时间戳选项是否存在。这三项里任何一项缺失,后面的分析都要相应调整预期。
4.1 握手三包:选项字段才是重点
第一个包由客户端发出,标志位只有 SYN,序号是随机初始值,我们记作 x。这个包的选项区一般包含最大段长度、窗口缩放因子、选择性确认许可、时间戳。最大段长度告诉对方“我最多能接收这么大的 TCP 载荷”,以太网环境下典型值是 1460 字节,算法很简单:链路最大传输单元 1500 减去 IP 头 20 字节、再减去 TCP 头 20 字节。如果链路上有额外封装(比如隧道),这个值会相应变小,你在抓包里看到的可能就是 1400 甚至更小。
第二个包由服务端回,标志位是 SYN 加 ACK,序号是服务端自己的随机初始值 y,确认号是 x+1。注意这个 +1:SYN 虽然不携带数据,但它占用一个序号,所以确认号必须加一。这一点在分析挥手时也一样,FIN 同样占用一个序号。第二个包的选项区是服务端对自己能力的声明,注意它是独立的,不用和客户端保持一致。实践中经常出现客户端声明缩放因子 7、服务端只回 5 的情况,最终生效的是双方各自收到的那个值,且两个方向可以不同。
第三个包是客户端回 ACK,确认号 y+1,序号 x+1,通常不带选项,也不带数据。到这一刻连接建立完成。整个握手在抓包里耗时通常只有几毫秒,但如果这个时间明显偏大(几十毫秒甚至上百毫秒),就说明链路往返时间不小,这直接决定了后续吞吐上限,值得单独记录。
4.2 挥手四包与 TIME_WAIT 的观察
标准的关闭过程是四个包。主动关闭方先发 FIN 加 ACK,此时它进入等待状态,还能继续接收数据但不能发送;被动关闭方回一个 ACK,进入对方的等待状态,此时它仍可以继续发送剩余数据;等它把数据发完,再发自己的 FIN 加 ACK;主动方回最后一个 ACK,然后进入一个持续两倍报文寿命的等待状态。
这个等待状态在抓包里是看不到包的,因为它是内核里的一个计时器。但你能通过后续行为间接观察到:如果在这个等待期内同一个四元组又被复用发起新连接,抓包里会出现异常现象,比如新连接的 SYN 收到 RST,或者出现序号错乱。做实验时我一般会分别观察两种情况:客户端主动关闭(等待状态出现在客户端)、服务端主动关闭(等待状态出现在服务端)。两者的差别会直接影响高并发服务的端口耗尽风险,这一点在网络编程里是经典的踩坑点。
还要注意半关闭的观察。当一方发了 FIN 之后,另一方还没有发 FIN 的这段时间里,连接处于半关闭状态,此时数据只能单向流动。抓包里表现为:某一个方向上连续出现带载荷的数据段,另一个方向上只有确认,直到数据发完才出现 FIN。这个时间段在实验里可以刻意拉长,比如让服务端在收到关闭请求后延迟几秒再关闭,就能清楚地看到单向传输阶段。
4.3 抓不到握手?先别怀疑工具
很多人第一次做实验,抓了半天找不到 SYN 包,第一反应是工具坏了。实际上常见原因就那么几个,按概率排序:
一是抓包开始时连接早就建立了,你抓到的是连接中段。解决办法是先开抓包,再发起访问。二是抓包点选错,比如你抓的是无线网卡,但流量走的是有线网卡,或者你抓的是物理网卡,但目标是本机回环。三是中间存在代理,客户端连的是代理,代理再连目标,于是你看到的握手是客户端和代理之间的,真正的目标连接在另一台机器上。四是过滤条件写得太窄,把握手包本身过滤掉了,比如你写了tcp.len > 0,而握手包载荷长度为零。
排查顺序我一般是:先清空过滤看全量,再确认接口选对,最后确认抓包时序。这三步能解决九成以上的“抓不到”。
5. 序号、确认号:可靠性是怎么被“看出来”的
序号和确认号是 TCP 分析的主线。所有的重传、乱序、重复、丢包判断,最终都归结为“这个包的序号是不是预期的那个”。但直接看绝对序号很难受,因为初始值是随机的大数,动辄十亿级别,两个包差 1460 你根本看不出来。所以分析器默认开启了相对序号,把每条连接的初始序号映射成 0,后续按偏移量显示。这个开关在协议设置里,实验时建议保持开启,除非你要研究初始序号生成策略。
开启相对序号之后,一切都变得直观:握手第一个包的序号是 0,第二个包是 0,第三个包是 1,第一个数据段是 1,长度 1460,那么下一个期望的序号就是 1461。如果抓到的下一个包序号是 1461,正常;如果是 2921,说明中间有段没抓到;如果是 1461 但被标注为乱序,说明它晚到了。
5.1 相对序号开关与“看起来不合理”的 seq
相对序号的映射是按方向独立计算的。也就是说,客户端到服务端方向的 0,和服务端到客户端方向的 0 是两个不同的基准,各自对应各自的初始序号。这一点必须记牢,否则看到两个方向的序号都是小数字时会误以为哪里算错了。
还有一个容易误导的现象:当连接中途开始抓包时,分析器拿不到初始握手包,就无法建立映射基准,它会退而把看到的第一个包当作基准。这时候显示的相对序号虽然也是从 0 或 1 开始,但它和真实偏移无关,做跨连接的序号比较会出错。判断方法很简单:看第一个包有没有 SYN 标志。如果没有,就说明这次抓包是不完整的,任何基于序号连续性的结论都要打折扣。
另外,当出现重传时,你会看到同一个相对序号出现两次。这不是错误,这正是重传的定义。分析器会给后出现的那个打上重传标记,并且在备注里说明“这是第几次重传”。三次以上的重传通常意味着链路真的有问题,值得进一步查。
5.2 重传、快速重传与 SACK 的报文特征
普通的超时重传,特征是“等”。发送方发出一个段,迟迟收不到确认,等到计时器到期,再发一遍完全相同的段。抓包里表现为:两次相同序号的段之间隔了一段明显偏长的时间,而且中间没有任何来自接收方的确认。初始超时值通常在几百毫秒到一秒之间,重传后如果还失败,超时值会翻倍,所以你会看到重传间隔一次比一次长。
快速重传不需要等计时器。它的触发条件是发送方连续收到三个重复的确认号,说明某个段大概率丢了,但后续的段都到了。此时发送方立刻重发缺口那个段,不用等超时。抓包里的特征是:先出现若干个确认号完全相同的 ACK(重复确认),紧接着一个序号回退的段,这个段被标注为快速重传。恢复速度比超时重传快得多,通常只损失一个往返时间。
选择性确认是配合重传来的。接收方在确认包里带上一个选项,告诉发送方“我收到了哪几段”,这样发送方就不用把缺口之后的所有段全部重发一遍,只补缺口就行。抓包里看到确认包的选项区有“选择性确认”条目,说明双方都支持这个能力。启用之后,一次丢包往往只需要重传一个段,而不是一大片,这个差异在长肥管道上非常明显。
5.3 乱序、零窗口、保活的识别
乱序和丢包在抓包里看起来很像,都会导致“序号跳跃”。区别在于:乱序的段最终会到达,只是顺序颠倒了,分析器会把它标注为乱序而不是丢包;真正丢了的段永远不会出现,只能靠重传来补。判断方法是看后续有没有那个缺失序号的包,有就是乱序,没有就是丢包。乱序在链路存在多条路径时很常见,实验里可以用中间设备人为制造。
零窗口是接收方主动喊停。当接收缓冲被填满,接收方在确认包里把窗口字段置零,发送方看到之后必须停止发送,进入持续探测状态,定期发一个小包问“现在能收了吗”。抓包里表现为:某个确认包的窗口值为 0,之后一段时间内出现若干个载荷长度为 1 的探测包,直到窗口恢复。这个现象在接收方处理速度跟不上发送方时非常典型,做实验时可以故意让服务端读得慢一点来复现。
保活报文则完全不同,它是空闲连接的定期探测,载荷通常为零或一字节,序号是发送方已发送的最后一个字节减一。看到这种“不合常理”的序号别慌,那是协议设计的刻意为之。
6. 从窗口到吞吐量:抓包能不能算出真实带宽
这条连接最快能跑多少,是实验里最有价值的结论之一。而这个结论不需要做压测,光靠握手包和几个数据包就能估算出来。核心变量有三个:接收窗口大小、往返时间、是否启用窗口缩放。三者组合起来,就能算出一个理论上限,再和实测吞吐对比,差距大就说明有瓶颈。
先把概念说清楚。窗口是接收方告诉发送方“我还能接收多少字节”,发送方在收到确认之前最多只能发这么多。所以单位时间内能发多少,取决于窗口多大除以一次往返需要多久。这个商就是吞吐上限。公式写出来很简单:吞吐 = 窗口 / 往返时间。但实际要用的时候,窗口要乘上缩放因子,往返时间要从抓包里实测,两个细节一错,结果能差两个数量级。
6.1 窗口缩放因子到底怎么算
TCP 头里的窗口字段只有 16 位,最大表示 65535 字节。这个值在上世纪八十年代够用,放在今天千兆链路上就是瓶颈:假设往返时间 40 毫秒,65535 字节除以 0.04 秒,只有约 13 Mbps,链路再快也跑不上去。所以后来加了个选项,允许双方在握手时协商一个移位因子,实际窗口等于字段值左移这个因子位。
这个因子是握手时双方各自在 SYN 和 SYN-ACK 里声明的,约定俗成的取值上限是 14。计算方式很直接:实际窗口 = 窗口字段值 × 2^因子。举例来说,抓包里看到窗口字段 64240,因子是 7,那么实际可接收窗口就是 64240 × 128 = 8,222,720 字节,约 7.8 MB。这个量级才能支撑起千兆链路上的长肥管道传输。
需要注意的是,因子只在握手阶段协商一次,之后不变。而窗口字段值在每个确认包里都可能变化,所以要算某个时刻的实际窗口,必须用当时的字段值乘上握手时定下的因子,不能混用。分析器的协议设置里有个选项可以自动做这个换算,把缩放后的值直接显示出来,省去手工乘法。做实验时建议开启,避免算错。
6.2 三步估算法与一个完整算例
我常用的估算流程分三步。第一步,从握手包里取出双方声明的窗口缩放因子,确认是否启用。第二步,找到连接稳定传输阶段中一个较大的窗口通告值,乘以缩放系数得到实际窗口。第三步,从抓包里量出往返时间,用实际窗口除以往返时间得到吞吐上限。
举个具体例子。假设某次实验抓到的握手包里,客户端声明因子 7,服务端声明因子 7,稳定阶段某个确认包的窗口字段是 65535,因子 7,那么实际窗口是 65535 × 128 = 8,388,480 字节。再从抓包里量往返时间:找到一个数据段和它对应的确认,两者时间差 40 毫秒。那么理论上限就是 8,388,480 ÷ 0.04 = 209,712,000 字节每秒,换算成比特要乘 8,约 1.68 Gbps。
这个数字只是上限,实际能不能达到还取决于发送方的拥塞窗口和应用层供给速度。所以做实验时要同时看两个量:接收窗口决定“对方能收多快”,拥塞窗口决定“发送方敢发多快”,两者取小才是实际速率。拥塞窗口在抓包里不直接显示,但可以通过“在途字节数”间接观察:任一时刻,已发送但未确认的字节总量,就是当前的发送窗口,它不会超过拥塞窗口和接收窗口中的较小者。分析器的 TCP 流图里有一条“在途字节数”曲线,看那条曲线在什么水平上稳定,就知道实际生效的窗口是多大。
另外一个常见算例是反过来的:已知链路带宽和往返时间,算需要多大窗口才能跑满。带宽 100 Mbps,往返 20 毫秒,那么管道容量是 100,000,000 ÷ 8 × 0.02 = 250,000 字节。也就是说窗口至少要 25 万字节才能填满这条管道。如果抓包里看到接收方只通告了 65535 字节,那这条连接最多只能跑到约 26 Mbps,链路再宽也没用。这类结论在排查“带宽上不去”的问题时特别有说服力。
6.3 TCP Stream Graphs 该看哪几张图
分析器自带的流图功能,能把上面的计算可视化。我常看四张:
时间-序列号图,横轴时间纵轴序号,正常情况下是一条条斜向上的平行线,斜率就是瞬时速率。出现水平段说明在等确认,出现垂直段说明有重传。这张图最能一眼看出“卡在哪”。
吞吐量图,纵轴是单位时间字节数。看它的稳定平台在哪里,和上面算出的理论上限对不对得上,差多少。
往返时间图,每个数据段和它的确认之间的时间差。这条曲线的抖动程度直接反映链路质量,突然抬高的位置往往对应队列积压或者路径切换。
窗口缩放图,把接收窗口和拥塞窗口随时间的变化画出来。看窗口是不是长期贴在小值上,如果一直很小,说明接收方应用读得慢,不是网络问题。
这四张图配合着看,基本能在几分钟内给一条连接定性。做实验报告时把这几张图截下来,比贴一堆报文列表有说服力得多。
7. 排错实录:实验里最容易翻车的几种情况
这一节全是踩过的坑。有些问题看起来像协议故障,实际上是工具环境的问题;有些看起来像工具问题,实际上是自己的过滤条件写错了。区分这两类很重要,否则会把大量时间浪费在错误的方向上。
7.1 抓不到包 / 只抓到半边
只抓到单向流量是极常见的现象,尤其在虚拟化环境和无线网络里。表现是:你能看到发出的包,看不到回来的包,或者反过来。原因通常是抓包点的位置不对,比如流量实际走了另一块网卡,或者无线环境下部分管理帧被驱动过滤掉了。
排查方法很朴素:先看接口列表里每块网卡的实时流量计数,哪个在涨就抓哪个。如果所有网卡都不涨,但应用确实在通信,那大概率走了回环,去抓回环接口。还有一种情况是抓到了包但被显示过滤藏起来了,清空过滤栏再确认一次。
另一个隐性原因是时间。如果两台机器的时钟不同步,你又把两个抓包文件合并分析,就会看到确认包早于数据包这种荒谬的时序。做多机实验前务必校时,或者至少记录下各机器的时钟偏差。
7.2 校验和报错的真相
满屏“校验和错误”是新手最容易慌的。前面提过,这通常是网卡的卸载计算导致的:抓包发生在硬件计算校验和之前,所以字段里是错的或者空的。判断方法很直接:如果几乎所有包都报同一个错误,而且链路工作正常,那基本就是卸载问题,不是真故障。
处理方式有两种。彻底一点的是在系统层面关闭该网卡的校验和卸载功能,让软件来计算,这样抓到的包校验和就是对的。省事一点的是在分析器的协议设置里关掉校验和验证,眼不见为净。我做实验时选前者,因为关掉验证之后,万一真有校验和错误也发现不了,等于自废一项检查能力。
顺带说一下 TCP 分段卸载。开启之后,抓包库里看到的可能是远大于最大段长度的“巨包”,因为分段还没发生。这类包在分析器里会显示为一个超大的 TCP 段,序号分析也会跟着乱。同样通过关闭网卡卸载功能解决。
7.3 显示与解析层面的假象
还有一类问题纯属显示层的误导。比如分析器默认开启 TCP 重组,会把多个小段拼成一个完整应用层消息显示,这时候你看到的“一个包”其实是多个包拼出来的,序号分析要以原始分段为准。排查重传问题时建议临时关掉重组,看最原始的段。
再比如时间显示格式。默认是相对本次抓包开始的秒数,看起来是 0.001、0.002 这种小数字,容易误以为是绝对时间。分析长连接时切换到“当天时间”格式更直观,定位“几点几分出的问题”更快。
最后一类假象来自分析器的启发式标注。它对“乱序”和“丢包”的判断是基于启发式规则的,在某些场景下会误判,比如超时重传和乱序重传难以区分。遇到标注和实际不符时,不要盲信标签,回到序号和时间的原始数据自己推一遍。
8. 把实验报告写成能交付的东西
做完分析不等于完成任务,能不能把结论清楚地交付出去才是。我见过太多实验记录,通篇是报文字段截图,最后一句“分析完毕”,看的人完全不知道结论是什么。一份有价值的记录,应该让没参与实验的人也能顺着读下来,并且能复现你的操作。
报告里最关键的不是截图数量,而是结论和证据的对应关系。每一条结论后面都要跟一个能定位到具体报文的证据,比如“窗口字段长期停留在 65535,未启用缩放,因此吞吐上限约 26 Mbps”,证据是某个流编号下第几个包。这种写法别人能验证,也经得起追问。
8.1 一份分析记录的骨架
我自己的记录固定分五块。第一块是实验目标与假设,写清楚这次要看什么现象,预期是什么。第二块是环境与抓包点,写清楚在哪抓、什么时候开始、用了什么过滤条件,这部分决定可复现性。第三块是关键报文摘录,只贴和结论相关的包,每个包标注它在流中的位置和作用。第四块是量化结论,包括往返时间、窗口大小、重传次数、吞吐上限,全部给数字。第五块是异常与未解释现象,把没搞清楚的也记下来,这块往往是最有价值的部分,因为它指出了下一步要研究的方向。
第三块最容易被写成流水账。控制方法是:先想清楚结论,再倒推需要哪几个包作为证据,只贴这几个。一条连接几百个包,真正需要展示的通常不超过十个。
8.2 用命令行工具把手工活压到最低
图形界面适合探索,批量统计适合命令行。比如要统计所有连接里哪些出现了重传,用一条命令就能把结果拉出来,不用一条条点开看。基本套路是指定读取文件、用显示过滤表达式筛选、只输出需要的字段。
tshark -r capture.pcapng \ -Y "tcp.analysis.retransmission" \ -T fields -e frame.number -e ip.src -e tcp.srcport -e tcp.dstport -e tcp.seq再比如要列出所有连接的首包,快速了解这次抓包里到底有多少条独立会话:
tshark -r capture.pcapng \ -Y "tcp.flags.syn == 1 && tcp.flags.ack == 0" \ -T fields -e frame.number -e ip.src -e tcp.srcport -e ip.dst -e tcp.dstport需要把大文件按连接拆开时,可以先提取流编号,再逐条导出。这类批处理的好处是可重复:换一份抓包文件,命令原样再跑一遍,结果格式完全一致,方便横向对比。做多组对照实验时,我基本都用这个方式生成汇总表,最后再把少数几条典型连接拿到图形界面里做详细讲解。
提示:批处理输出的字段顺序、分隔符尽量固定下来,并写进报告附录。别小看这个细节,同一个人隔两周回来,往往也记不住当时用的是哪套字段。
我个人在实际操作中的体会是,TCP 抓包实验真正的门槛从来不在语法和字段,而在于“先想清楚要验证什么,再去抓”。漫无目的地抓一大堆包,然后盯着屏幕找异常,是最低效的做法。反过来,先写下三个具体问题,比如“这条连接的窗口缩放谈成了多少”“握手耗时是不是异常”“重传是超时触发还是快速重传触发”,带着问题去过滤、去定位,通常十几分钟就能出结论。另一个小技巧是给自己建一个过滤器收藏夹,把常用表达式按场景归类存好,用的时候直接点,比每次重新敲快得多,也能避免手误打错字段名导致的误判。