上周帮人看一个后台系统一个列表接口在本机跑 30 毫秒部署到线上稳定 1.2 秒。他一开始死磕数据库索引把慢查询日志翻了个底朝天什么也没找出来。我让他换成按请求逐个计时结果发现真正耗掉那一秒的地方是每次请求都新建一次 TCP 连接再加上服务端一个配置不当的 Nagle 参数。这个问题追到根上就是TCP/IP 协议、HTTP 协议和 FTP 协议这三样东西各自负责什么、边界在哪里没理清楚。很多人对这三个协议的理解停留在HTTP 是网页协议、FTP 是传文件的、TCP/IP 是底层的这种认知在写业务代码时够用一旦碰到性能问题、连接异常、防火墙拦截、抓包看不懂就完全接不上了。我写这篇东西是想把三者放回同一张网图里讲清楚 TCP/IP 是怎么一层层把字节送出去的HTTP 在这个基础上额外约定了什么语义FTP 又为什么会长出一个在今天看来相当别扭的双连接结构。文中会带具体的抓包命令、报文逐行解读、参数计算过程和几个我实打实踩过的坑做后端、做运维、做客户端、或者纯粹想搞明白点一下网页到底发生了什么的人都能从里面拿到能直接用的东西。1. 三个协议不是并列关系先把它们在链路里的位置摆清楚刚接触网络的人容易把 TCP/IP、HTTP、FTP 当成三个平行的东西去背这会导致后面所有知识都是散的。它们其实是套娃关系TCP/IP 是整套通信规则的地基HTTP 和 FTP 是建在这块地基上的两种应用约定。地基管的是字节怎么可靠地从 A 送到 B应用约定管的是这些字节拼起来是什么意思。1.1 TCP/IP 是一整套协议族不是两个协议TCP/IP这个名字有很强的误导性听起来像只有 TCP 和 IP 两个协议实际上它是一整个协议族的统称包含几十上百个协议。IP 负责寻址和路由把数据包从一台机器送到另一台机器TCP 在 IP 之上补上可靠性保证数据不丢、不重、按序到达此外还有 UDP、ICMP、ARP、DNS 用的协议等等都在这个族里。真正需要记住的是分工IP 层只保证尽力而为包丢了它不管包乱序了它也不管TCP 层才负责把这些坑填上。所以你在写应用层代码时感受到的连接可靠是 TCP 拿重传、确认、排序这些机制硬撑出来的不是网络本身保证的。这个认知很关键。当你看到抓包里出现大量重传包那不是网络坏了而是网络在正常工作——它在用重传对抗丢包。真正要排查的是丢包率为什么这么高而不是重传机制本身。三层之间的关系可以这样对照IP 关心送到哪台机器TCP 关心这段字节流完整有序HTTP 关心这段字节流是一个请求还是一段图片。每往上一层就多一层语义也多一层需要自己处理的边界情况。1.2 懂 HTTP 不懂 TCP 的人排障时会卡在哪我见过太多人把 HTTP 的全部问题都当成后端接口慢然后去查代码逻辑。实际上一个 HTTP 请求的耗时是由几段拼起来的DNS 解析、TCP 握手、TLS 握手如果是 HTTPS、请求发送、服务端处理、响应传输。只有服务端处理这一段是代码的事前面几段全是协议层的事。举几个典型现象。页面首次打开要 300 毫秒刷新一下变成 50 毫秒八成是握手和 DNS 的固定开销不是接口变快了。接口偶尔超时但服务端日志显示处理只花了 10 毫秒那问题可能在 TCP 重传或者连接被中间设备掐断。上传大文件到 99% 卡住不动很可能是 TCP 窗口满了在等确认也可能是服务端读缓冲没及时消费。把耗时按协议层拆开是最有效的一种排障思路。下面这张表是我平时用的归因清单遇到慢先对号入座能省掉大量无效排查。现象大概率层级优先核查手段首次访问慢、二次访问快DNS / TCP 握手换 IP 直连对比、看 DNS 缓存请求发出后长时间无响应TCP 重传 / 服务端阻塞抓包看是否有重传、看服务端线程栈大响应传输到中途卡顿TCP 窗口 / 应用读取速度看窗口更新、检查读取端缓冲连接随机被重置中间设备 / 连接空闲超时看 RST 包出现位置、查空闲超时配置请求头特别大时失败服务端头部大小限制看返回码 431 或 400这张表不是万能钥匙但它能帮你把慢这个模糊感受快速收敛到某一层去。2. TCP/IP 分层每层丢掉的细节都是上一层必须自己补回来的分层的本质是每一层只对上一层负责把下层的能力包装成一个更简单的接口。这个设计的好处是解耦代价是每一层都会丢失一些信息上层如果想用这些信息就得自己想办法绕过去。2.1 四层模型里每一层到底解决了什么问题从下往上数链路层管的是同一根网线或同一个无线信道上两台设备怎么把帧发出去它关心 MAC 地址、帧格式、差错检测。网络层IP管的是跨网段寻址它关心 IP 地址和路由表把数据包一跳一跳地转发到目标网络。传输层TCP/UDP管的是端到端的进程间通信它关心端口号把数据交给正确的应用程序。应用层HTTP、FTP、DNS管的是数据本身的含义。四层各司其职每层的头部都是给对端同层看的。IP 头是给对端的 IP 层看的TCP 头是给对端的 TCP 层看的HTTP 头是给对端的应用看的。抓包的时候按这个规律去读一层层剥开思路就不会乱。有一组数字值得记住因为它直接影响你能传多大的数据。以太网的标准 MTU 是 1500 字节也就是一个帧最多承载 1500 字节的 IP 数据报。IP 头最少 20 字节TCP 头最少 20 字节剩下给应用数据的最大长度就是 1460 字节这个值叫 MSS最大报文段长度。超过这个长度的应用数据会被 TCP 切成多段发送。这个 1460 不是随便定的它是 MTU 减去两层头部得来的。如果你在链路上启用了额外封装比如某些隧道MTU 会变小MSS 也跟着变小这时候如果没做好 MSS 协商就会出现小请求正常、大请求卡死的经典故障。我遇到过一次特别隐蔽的某台机器访问外部服务时几十字节的请求一切正常几百 KB 的响应就永远收不全。最后定位是链路 MTU 比标准值小而路径 MTU 发现机制被中间设备把 ICMP 报文丢了导致大包一直被丢弃、一直重传。改法是在系统层面把 MSS 上限调小。这种问题的特征就是小数据通、大数据不通看到这个现象可以直接往 MTU 方向想。2.2 一次数据发送的完整封装过程你在代码里调用一次写操作数据往下走的过程是这样的应用层把 HTTP 报文请求行 头部 正文交给 TCPTCP 在它前面加上自己的头部填上源端口、目标端口、序号、确认号、窗口大小等字段形成报文段IP 层再在前面加上 IP 头填上源 IP、目标 IP形成数据报链路层再加帧头和帧尾填上源 MAC、目标 MAC形成帧最后由网卡把电信号打出去。接收端是反过来的网卡收帧、剥掉帧头交给 IP 层IP 层核对目标 IP剥掉 IP 头交给 TCPTCP 核对端口和序号剥掉 TCP 头把有序的字节流交给应用应用按 HTTP 的格式解析出请求行和头部。这个加头—剥头的过程就是抓包工具能按层展示报文的原因。抓包工具看到的就是链路上的帧它自己按协议规范一层层解析所以你能在同一个包里同时看到以太网头、IP 头、TCP 头和 HTTP 内容。理解这一点有个实际好处你能判断某条信息到底应该在哪一层去找。比如这个请求是从哪个端口发出的去看 TCP 头这个请求想要什么资源去看 HTTP 请求行这个包有没有被分片去看 IP 头里的标志位和片偏移。2.3 分层是有代价的跨层调优为什么会让人上瘾分层让各层解耦但也意味着上层很难直接影响下层的行为。想让 TCP 表现更好得通过套接字选项去设参数想绕过 TCP 的某些机制得用 UDP 自己在应用层实现可靠性——HTTP/3 就是这么干的它把可靠传输从 TCP 搬到了基于 UDP 的 QUIC 上。跨层调优之所以让人上瘾是因为它往往能带来数量级的改善但风险也很高。比如关掉 Nagle 能让小包立刻发出降低交互延迟但会增加网络上的小包数量调大发送缓冲区能提升大文件吞吐但会占用更多内存还可能加剧延迟。我的经验是跨层调优必须建立在你确知瓶颈在哪一层的前提上。不知道瓶颈就调参数等于闭着眼睛拧旋钮。先用抓包和耗时统计把问题定位到具体一层再动那一层的参数改完必须重新测别凭感觉。3. TCP 的可靠性靠什么撑起来握手、挥手、重传与窗口TCP 的所有复杂度都围绕一个目标在一条会丢包、会乱序、会拥塞的链路上给上层伪装出一条可靠、有序的字节流。理解它的关键是搞清楚它在防什么。3.1 三次握手每一步都在确认什么握手的过程是这样的客户端发 SYN带上自己的初始序号 seqx进入 SYN_SENT 状态。服务端回 SYNACK带上自己的初始序号 seqy同时确认 ackx1进入 SYN_RCVD 状态。客户端回 ACKacky1双方进入 ESTABLISHED。为什么是三次而不是两次核心原因是只有三次才能让双方都确认我的发送能力、你的接收能力、你的发送能力、我的接收能力这四件事都正常。两次握手的话服务端发完 SYNACK 就认为连接建立了但客户端的接收能力还没被验证过。更实际的理由是防止历史重复连接。设想一个很早以前发出的 SYN 在网络里绕了很久才到达服务端如果只有两次握手服务端会直接建立一个连接并分配资源然后一直等一个永远不会来的数据白白浪费。三次握手让客户端有机会用最后一个 ACK 否掉这种历史连接——如果客户端没发这个 ACK服务端就不会进入 ESTABLISHED。还有个细节容易被忽略SYN 包会消耗一个序号所以确认号是 x1 而不是 x。这个序号占一位的规则在挥手时也一样适用FIN 同样占一个序号这也是为什么挥手的确认号有时看起来多了一个。握手阶段的常见问题有两个。一是 SYN 队列被打满表现为客户端连接超时但服务端 CPU 不高通常是短时间大量新建连接导致的可以通过调大队列长度或者加连接复用缓解。二是握手耗时占比过高尤其在跨地域访问时一次握手可能就吃掉几十甚至上百毫秒这种情况下复用连接带来的收益非常明显。3.2 四次挥手与 TIME_WAIT 的取舍关闭连接比建立连接麻烦因为 TCP 是全双工的两个方向要分别关闭主动关闭方发 FIN进入 FIN_WAIT_1。被动关闭方回 ACK进入 CLOSE_WAIT主动方进入 FIN_WAIT_2。被动关闭方处理完剩余数据发自己的 FIN进入 LAST_ACK。主动关闭方回 ACK进入 TIME_WAIT等待 2MSL 后彻底关闭。中间那两步之所以分开是因为被动关闭方收到 FIN 时可能还有数据没发完它得先把数据发完才能发自己的 FIN。这就形成了两个方向各关一次的四次挥手。TIME_WAIT 是最容易被误解的状态。它不是 bug它有两个明确作用一是保证最后一个 ACK 能到达对端如果 ACK 丢了对端会重发 FIN处于 TIME_WAIT 的一方还能再回一次 ACK二是让本次连接的所有重复报文在网络中消散避免它们被后续用相同四元组建立的新连接误收。代价是TIME_WAIT 期间这个四元组不能被复用。如果一台机器作为主动关闭方大量短连接就会堆积大量 TIME_WAIT最终耗尽本地临时端口。这时候新连接会报无法分配请求的地址。解决方向有三个各有代价手段原理代价让服务端主动关闭TIME_WAIT 转移到服务端客户端端口快速回收服务端积累 TIME_WAIT需评估端口容量启用端口复用允许新连接复用处于 TIME_WAIT 的四元组极低概率收到旧连接的残留报文一般可接受使用长连接 / 连接池从根上减少连接建立次数需要处理空闲连接保活和过期我个人的偏好是优先走连接池因为它同时解决了握手开销和 TIME_WAIT 两个问题。只有当连接数实在压不下来时才去调系统参数。另外提醒一句网上流传的一些激进回收 TIME_WAIT的配置方式在较新的内核里已经被移除或者不建议使用会带来连接异常别照抄老文章。3.3 重传、滑动窗口与拥塞控制的三段式TCP 保证可靠的核心手段是确认 重传。接收方收到数据后回 ACK发送方在一定时间内没收到 ACK 就重传。超时时间不是固定的是根据往返时延动态估算的链路越慢超时越长。但纯靠发一个等一个效率太低于是有了滑动窗口。发送方可以在没收到确认的情况下连续发多个报文段窗口大小就是还能发多少。这个窗口取两个值中的较小者接收方通告的接收窗口防止把对方缓冲区撑爆和发送方的拥塞窗口防止把网络撑爆。拥塞控制的经典实现分四个阶段。慢启动连接刚建立时拥塞窗口从一个很小的值开始每收到一个确认就翻倍指数增长。拥塞避免窗口涨到阈值后改成线性增长每个往返只加一点。快重传收到三个重复 ACK 就立刻重传丢失的段不等超时。快恢复重传后把窗口降到阈值的一半再继续线性增长而不是直接回到起点。这套机制解释了很多反直觉的现象。比如为什么跨洋链路刚开始传得慢跑一会儿就快了——慢启动在爬坡。为什么网络一拥塞吞吐断崖式下跌——触发拥塞控制窗口被砍半。为什么丢一个包不一定慢丢一串包就明显慢——零星丢包可以快重传补救连续丢包只能等超时。我在调大文件传输时最常用的一招是确认瓶颈到底是窗口还是带宽。算法很简单用带宽乘以往返时延得到管道容量再和实际窗口对比。如果窗口明显小于管道容量那吞吐就被窗口卡住了加大缓冲区通常有效如果窗口已经大于管道容量那就已经跑满带宽再怎么调也没用。这个计算能帮你省掉很多瞎试。3.4 Nagle 与延迟确认撞车制造小包延迟的经典组合回到开头那个案例。Nagle 算法的作用是减少网络上的小包如果还有未确认的数据在路上就不发新的小包先攒着。延迟确认的作用也是减少包收到数据后不立刻回 ACK等一小会儿看看有没有数据要捎带一起回。这两个机制单独看都是优化凑到一起就出事发送方在等确认才发下一段接收方在等更多数据才回确认双方互相等直到延迟确认的定时器通常几十毫秒到期才打破僵局。表现出来就是每次交互多几十毫秒。识别这个问题的特征很明确单次小请求的耗时总是落在某个固定量级比如稳定在 40 毫秒左右而且无论服务端代码怎么优化都不变。抓包能直接看到一个数据包发出后等了几十毫秒才有一个纯 ACK 回来紧接着下一个数据包才发出去。解决办法按场景选交互型短连接比如命令行式接口关掉 Nagle让数据立刻发出。批量传输场景保留 Nagle它能显著减少小包数量提升有效带宽。应用层自己做攒批把多次小写合并成一次大写入从源头减少小包。关 Nagle 是通过套接字选项设置的服务端和客户端都可能需要。但要注意关掉之后小包变多在丢包率高的链路上反而可能更糟所以要在真实网络环境下测。4. HTTP 协议语义层的事情TCP 一概不管TCP 把一段连续的字节流送到对端但它完全不知道这段字节流是几个请求、每个请求从哪开始到哪结束。HTTP 要做的第一件事就是定义一种能让接收方切分消息的格式。4.1 请求报文逐行拆解一个最朴素的 HTTP 请求长这样GET /api/list?page2 HTTP/1.1 Host: 127.0.0.1:8080 User-Agent: demo-client/1.0 Accept: application/json Connection: keep-alive第一行是请求行三段方法、请求目标、协议版本。方法表达意图。GET 取数据POST 提交数据PUT 整体替换PATCH 局部更新DELETE 删除HEAD 只要头部不要正文OPTIONS 询问服务端支持哪些能力。这些语义是约定不是强制——你完全可以用 GET 去删数据但那样会让缓存、重试、日志全部失灵因为所有中间环节都按约定在理解这些方法。请求目标通常就是路径加查询串。协议版本现在基本是 HTTP/1.1少数老系统还在 1.0。从第二行开始是头部字段每行名字: 值的格式到空行为止。空行之后是正文GET 一般没有正文POST 有。这个空行分隔头部和正文的约定就是 HTTP/1.x 能被解析的基础。响应报文结构对称第一行是状态行包含协议版本、状态码、原因短语然后是响应头空行之后是响应正文。需要特别提醒的是头部字段名不区分大小写但值通常是区分大小写的同名头部可以出现多次某些场景下等价于用逗号合并。这些细节在写自己的解析器时才重要用现成库的话基本不用操心。4.2 状态码里那些最容易被误用的状态码分五类1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务端错误。分类大家都知道但具体到某个码该不该用误用非常普遍。几个高频误用200 和 201。创建资源成功应该返回 201并带上新资源的位置。很多系统一律返回 200导致客户端没法判断到底是查到了还是创建了。301 和 302 和 307 和 308。区别在于是否允许改变请求方法以及是否允许缓存。301/302 在历史上允许客户端把 POST 改成 GET307/308 则严格要求保持原方法。301 和 308 是可缓存的永久重定向302 和 307 是临时重定向。做接口迁移时如果用错了可能导致客户端把本该 POST 的请求发成 GET。401 和 403。401 是你没有提供有效凭证会引导客户端去认证403 是凭证我认了但你没权限。给 401 加上错误的认证头会让客户端反复弹认证框。400 和 422。400 是请求本身格式有问题422 是格式没问题但语义校验没过。这个区分不是所有系统都做但做了会让前端错误提示更准确。502 和 504。502 是上游返回了无效响应504 是等上游超时。这两个码在多层架构里非常有诊断价值看到 502 说明上游挂了或者协议不匹配看到 504 说明上游太慢方向完全不同。429。限流时应该返回 429 并带Retry-After而不是笼统返回 503。客户端能据此做退避重试。状态码是给机器看的不是给人看的。用它来表达语义比在响应体里塞一个{code: 500, msg: 参数错误}要规范得多——虽然后者在很多国内团队里已经成了习惯但至少 HTTP 状态码本身要选对否则监控和网关层根本没法正确识别。4.3 从 1.0 到 3.0每一代在修什么具体问题HTTP/1.0 的最大问题是每个请求一个连接用完就关。这意味着每个请求都要付一次 TCP 握手甚至一次慢启动。HTTP/1.1 引入持久连接keep-alive解决了这个问题连接可以复用一个连接上串行发多个请求。但 HTTP/1.1 有个硬伤串行。虽然定义了管道化pipelining允许连续发多个请求不等响应但响应必须按请求顺序返回。只要第一个请求慢后面的全被堵住这就是应用层的队头阻塞。所以浏览器干脆放弃管道化改用每个域名开 6 个左右并发连接的土办法绕过去。HTTP/2 的核心改动是把消息拆成二进制帧给每个帧打上流 ID这样多个请求的帧可以在一条连接上交错传输接收方按流 ID 重组。这解决了应用层队头阻塞同时用 HPACK 压缩头部HTTP/1.1 的头部冗余非常严重一堆 Cookie 重复传还支持服务端主动推送。HTTP/3 则更进一步把传输层从 TCP 换成了基于 UDP 的 QUIC。原因还是队头阻塞——HTTP/2 解决了应用层的但没解决 TCP 层的一个连接上的所有流共享同一个 TCP 字节流只要有一个包丢了TCP 就得等重传所有流一起等。QUIC 把流的概念下移到传输层每个流独立管理丢失恢复一个流丢包不影响其他流。用一个类比HTTP/1.1 是一条单车道车必须排队HTTP/2 是同一条路上画了多个车道但所有车道共用一套红绿灯TCP 重传一处事故全线等待HTTP/3 是每个车道自己独立的信号系统互不干扰。实际选型上内网服务用 HTTP/1.1 加连接池通常就够了对外提供大量并发静态资源HTTP/2 收益明显对弱网和移动端场景HTTP/3 的改善最显著。但 HTTP/3 依赖 UDP 通路某些网络环境下 UDP 会被限制需要有降级方案。这一点在做架构决策时一定要先验证。4.4 消息边界Content-Length、chunked 和 keep-alive 的三角关系这是 HTTP/1.1 里最容易出问题的一块。既然连接要复用接收方就必须能准确判断这个响应到哪结束否则下一个响应就没法定位。规则有三条如果有Content-Length就按这个长度读。如果是Transfer-Encoding: chunked就按分块格式读直到读到长度为 0 的块。两者都没有那就只能读到连接关闭为止——这种情况下连接没法复用。第三条是很多老系统的默认行为也是 keep-alive 失效的常见原因。如果你发现连接复用没生效先检查服务端有没有正确输出长度信息。chunked 的价值在于边生成边发送。服务端如果不知道最终响应多大比如实时输出日志、流式生成大文件就可以用分块编码一段段发不用先在内存里攒出完整内容再算长度。这两者不能同时使用。同时出现的话规范要求忽略Content-Length但实际中不同实现的处理不一致可能被用来构造请求走私。所以网关层通常会直接拒绝这种请求自己写服务端时也要注意不要输出这种组合。还有一个隐藏坑头部字段和正文之间的那个空行。某些手写的拼接代码少打了一个换行服务端就会一直等头部结束表现为请求卡住直到超时。这种问题用抓包一眼就能看出来——报文在某个位置戛然而止。5. FTP 的双连接设计一个把控制和数据彻底分开的老协议FTP 诞生在算力极其宝贵的年代它的设计思路和今天的协议很不一样。最特别的一点是它一次会话要开两条 TCP 连接一条传命令一条传数据。5.1 为什么非要开两条连接控制连接固定走服务端的 21 端口全程保持用来传用户名、密码、切换目录、发起传输这些命令以及服务端的响应码。数据连接专门传文件内容和目录列表传完就关。这么设计的直接好处是命令通道永远不会被数据堵住。传一个几十 G 的文件期间你还能在控制连接上发中止命令服务端能立刻收到并中断传输。如果只有一条连接数据洪流会把命令淹掉中止命令得排在几十 G 数据后面等于没法中止。另一个好处是灵活。数据连接可以走完全不同的网络路径可以设置不同的传输模式ASCII 或二进制可以指定不同的端口。这种控制面与数据面分离的思想在今天看其实相当先进很多现代系统设计里也能见到类似思路。但代价也很明显。两条连接意味着两套状态防火墙要放行两个方向的连接NAT 设备要处理内网地址在协议正文里的明文传递。这些在当年的局域网环境里都不是事到了互联网时代就成了大麻烦。5.2 主动模式与被动模式227 响应背后的地址难题FTP 有两种数据连接建立方式这是所有 FTP 问题的源头。主动模式PORT客户端先在一个本地端口上监听然后用 PORT 命令把自己的 IP 和端口告诉服务端服务端从自己的 20 端口主动连过来。问题在于客户端通常躲在 NAT 后面它告诉服务端的是内网地址服务端根本连不上就算地址是对的客户端所在网络的防火墙也会拦掉这个从未请求过的入站连接。被动模式PASV客户端发 PASV 命令服务端回一个 227 响应里面写明你来连我这个地址和端口客户端主动去连。这样就变成了客户端发起的出站连接NAT 和防火墙都好处理。227 响应的格式长这样227 Entering Passive Mode (192,168,1,10,195,80)括号里前四个数字是 IP 的四段后两个数字要换算成端口第一个乘 256 加第二个。上面这个例子就是 195×25680 50000。主动模式和被动模式的关键差异对比如下维度主动模式被动模式谁发起数据连接服务端客户端服务端使用的端口固定 20每次协商的随机高位端口客户端防火墙友好度差入站被拦好服务端防火墙友好度好差需开放端口段NAT 环境可用性差内网地址暴露好227/PORT 命令依赖PORTPASV / EPSV实际部署里几乎都默认用被动模式因为客户端环境不可控。但被动模式会在服务端打开一大片随机端口如果服务端有防火墙就得限定端口范围并放行。很多FTP 能登录、能列目录但下载卡住的故障根因就是控制连接的 21 端口通了数据连接的端口段没放行。排查这类问题的标准动作是先确认当前用的是哪种模式再看数据连接卡在哪一步。抓包时如果看到 PASV 命令发出、227 响应回来然后就没有下文了基本可以断定是数据端口不通。还有一个现代替代方案值得一提如果环境允许用基于 SSH 的文件传输协议或者 FTP over TLS 会省心很多它们只需要一个端口绕开了双连接带来的所有麻烦。但如果是维护老旧系统还是得把主动被动这套搞明白。5.3 手敲一遍 FTP 会话把每条响应码对上想真正理解 FTP最快的办法是手动走一遍。用 telnet 或 nc 连上 21 端口一条条敲命令看服务端怎么回。nc 127.0.0.1 21 220 FTP server ready. USER anonymous 331 Please specify the password. PASS guest 230 Login successful. PWD 257 / is the current directory. PASV 227 Entering Passive Mode (127,0,0,1,200,100). TYPE I 200 Switching to Binary mode.到这一步控制连接就绪数据连接地址也拿到了200×256100 51300。接着可以用另一个终端去连这个数据端口或者在支持完整 FTP 的客户端里继续。LIST 150 Here comes the directory listing. 226 Directory send OK. QUIT 221 Goodbye.响应码的第一位表示类别2 开头成功3 开头需要更多信息4 开头临时失败可重试5 开头永久失败。这里几个码值得记住220 服务就绪331 需要密码230 登录成功227 进入被动模式150 数据连接即将打开226 传输完成425 无法打开数据连接。425 这个码是排查 FTP 的金钥匙。看到它就说明控制连接没问题否则你不会收到响应问题出在数据连接建立失败方向立刻明确检查端口放行、检查 PASV 返回的地址是不是客户端可达的地址。顺便说一句用二进制模式TYPE I传文件几乎总是正确的选择。ASCII 模式会做换行符转换传文本可能没事传压缩包、图片、可执行文件必坏。我见过有人用 ASCII 模式传了一个 tar 包解压时报校验失败查了半天以为是传输丢包其实是内容被悄悄改了。6. 抓包对照让三个协议在同一份时间线里现形看再多文字描述不如自己抓一次包。抓包的价值在于它把抽象的分层变成了可以逐字节核对的事实。6.1 过滤表达式怎么写才是有效过滤抓包最忌一上来就全量抓几秒钟就是几十万条。有效的做法是先按端口或主机圈定范围再逐步收窄。# 只抓 80 端口的流量不解析域名显示完整包内容 tcpdump -i any -nn -s0 -A tcp port 80 # 只抓 SYN 包观察谁在发起连接 tcpdump -i any -nn tcp[tcpflags] tcp-syn ! 0 and tcp[tcpflags] tcp-ack 0 # 抓 FTP 控制连接看清命令和响应 tcpdump -i any -nn -A tcp port 21 # 把抓到的包存文件后续用图形化工具慢慢看 tcpdump -i any -nn -s0 -w /tmp/cap.pcap host 127.0.0.1参数里-s0表示抓完整包长不加这个可能只抓前 96 字节HTTP 正文就看不全了。-nn表示不做域名和端口名解析抓到什么显示什么速度快且不会因为反解而引入额外流量。如果用的是图形化工具显示过滤表达式的思路类似只是语法不同按协议过滤写协议名按端口过滤写端口号按字段过滤用字段名。掌握几个常用组合就够了没必要背全套语法。一个实用技巧抓包位置决定了你能看到什么。在客户端抓看到的是客户端视角包括重传在服务端抓看到的是服务端视角在中间设备抓能看到两边的差异。排查客户端觉得慢、服务端觉得快这类问题时同时抓两端再对比时间戳几乎立刻就能定位延迟发生在哪一段。6.2 一次 HTTP 请求的逐段耗时怎么算用抓包结果算耗时比任何监控面板都精确因为它精确到每一个包的时间戳。分析步骤是这样的找到 SYN 包的时间戳这是 t0找到 SYNACK 的时间戳这是 t1t1-t0 就是一次单向的握手延迟找到最后一个 ACK 的时间戳 t2t2-t0 是完整握手耗时找到第一个携带 HTTP 请求的包 t3这中间是客户端处理时间找到第一个携带响应数据的包 t4t4-t3 是服务端处理加首字节延迟找到响应最后一个包 t5t5-t4 是传输耗时。这套拆解下来你就能明确回答这 1.2 秒到底花在哪。开头那个案例拆出来是握手 40 毫秒、请求发送立即、服务端处理 10 毫秒、响应传输里有两段明显的 40 毫秒间隙。那两段间隙就是 Nagle 和延迟确认互相等待的结果和数据库一点关系都没有。还有一个容易被忽略的指标连续两次请求之间的间隔。如果连接是复用的第二个请求应该紧接着第一个响应发出如果看到每次都重新握手说明连接复用没生效可能是客户端没启用也可能是服务端返回了Connection: close。6.3 FTP 在抓包里长什么样FTP 的抓包特别直观因为它的控制命令是纯文本明文可见。打开 21 端口的抓包你会看到一来一回的对话C: USER anonymous S: 331 Please specify the password. C: PASS guest S: 230 Login successful. C: PASV S: 227 Entering Passive Mode (127,0,0,1,200,100). C: LIST S: 150 Here comes the directory listing. 此时另有一条连接在 51300 端口上建立并传数据 S: 226 Directory send OK.主动模式的抓包就完全不一样你会看到客户端先发 PORT 命令然后服务端从 20 端口向客户端发 SYN方向是反的。这个反向 SYN是主动模式最明显的特征也是它在 NAT 环境下失败的直观证据。对照两种模式的抓包你会立刻明白为什么被动模式成了默认选择它的所有连接方向都是客户端发起NAT 和防火墙处理起来最省事。7. 排障时最容易判断错的几个现象最后这部分是我这些年反复遇到、也见过别人反复判断错的几类问题写下来供参考。把重传当成故障。抓包里看到重传就断定网络有问题这是最常见的误判。少量重传是正常现象链路上总会有丢包。判断标准是重传率低于千分之一基本可以忽略超过百分之一才值得深入查。而且要先看丢包发生在哪一段是客户端到服务端还是服务端到上游。把 TIME_WAIT 当成内存泄漏。看到几千个 TIME_WAIT 就慌了其实这个状态本身不占多少内存真正的问题是端口耗尽。判断方法是看本地临时端口的使用率而不是看 TIME_WAIT 的绝对数量。把 502 当成后端代码出错。502 是网关认为上游响应无效可能是上游进程重启、端口没监听、协议版本不匹配也可能是响应头格式错误。直接去看后端业务日志往往什么都找不到应该先看上游进程是否存活、端口是否可达。把 HTTP 超时当成网络超时。这两者差别很大。HTTP 客户端的超时通常包含连接建立、等待响应、读取正文几个阶段任何一个阶段超时都会抛同一个异常。查的时候必须分开设超时并分别记录否则永远不知道卡在哪一段。忽略头部大小限制。请求带上大量 Cookie 或者超长自定义头时可能触发服务端或网关的头部大小限制返回 431 或者直接断开。这类问题在本地测不出来因为本地往往没有网关层。判断方法是逐步删减头部字段做二分。把 ASCII 模式当默认安全选项。前面提过传二进制文件必须用二进制模式。这个坑每年都有人踩而且症状很有迷惑性——文件大小看着差不多就是内容不对。如果非要从这些经验里提炼一条最有用的先确认问题在哪一层再动那一层的参数。分层的意义不只是让协议好实现也是让排障有章可循。看现象能收敛到某一层就已经解决了一半问题剩下的无非是抓包核对和逐项排除。我自己的习惯是任何时候都先抓一份包存着哪怕当下看不出问题等想明白了再回头翻往往能找到关键证据。
阅读完成 · 觉得有帮助?