七层模型背得滚瓜烂熟,可真遇上“数据库连不上”,还是只能重启试试——这是我带过的新人在计算机网络入门阶段最典型的卡点。问题不在记性,而在于大多数人学网络基础时,拿到手的是一堆孤立的术语:IP、掩码、网关、TCP、DNS,每一条都能背,但彼此之间没有串成一条线。真出故障时,脑子里的知识调不出来,因为不知道从哪一层开始查。
这篇东西想干的事情很具体:把网络基础里那些最常被讲成“概念题”的内容,还原成一条能跑通的路径——从你在地址栏敲下回车,到页面出现,中间到底发生了什么;从一条网线插上,到两台机器能互相说话,中间要满足哪些条件。写完之后,你手里应该有一套自己的排错坐标系,而不是一堆背诵材料。适合刚入行的开发、刚接触运维的同学,也适合工作几年但一直靠“重启大法”混过来的朋友——网络的底层逻辑其实不难,难的是没人给你按正确的顺序讲一遍。
1. 从敲下回车到页面出现:一条请求走过的完整路径
我习惯把这一章放在最前面,因为它能把后面所有零散的知识点串成一根绳。很多人学网络是“自底向上”的,先学物理层、再学链路层、再学网络层,学到最后脑子里全是协议名字,却不知道它们各自在哪一步出场。反过来走一遍“自顶向下”的实际路径,回头再看那些协议,位置感会清晰得多。
1.1 域名解析:先把好记的名字换成能路由的地址
浏览器拿到www.example.com这种字符串是没法直接发包的,网络设备只认 IP 地址。所以第一件事是解析。这个过程有个很多人忽略的细节:解析不是一步到位的,而是一层一层往下问,且每一层都可能直接短路返回。
完整的顺序大致是这样:先查浏览器自身的 DNS 缓存,再查操作系统的 DNS 缓存和 hosts 文件,都没有命中才会把请求发给配置的递归解析服务器。递归服务器如果本地也没有缓存,就会从根域名服务器开始,逐级问到顶级域服务器,最后问到该域名自己的权威服务器,拿到结果后逐级缓存再返回给你。这个“逐级”的过程听起来慢,实际上因为各级都有缓存,绝大多数请求在第一步或第二步就返回了。
这里有个特别容易踩的坑,值得单独拎出来说:TTL。解析结果在各级缓存里都有一个存活时间,单位是秒。你在域名服务商后台把 A 记录从旧 IP 改成新 IP,改完之后自己电脑上访问还是旧地址,八成不是没生效,而是本地或中间某一级缓存的 TTL 还没到期。这时候用命令行工具查一下就能确认:
# 指定公共解析服务器查询,绕开本地缓存 dig @8.8.8.8 www.example.com +short # 查看完整的解析链路和 TTL dig www.example.com # Windows 下清本地缓存 ipconfig /flushdns # macOS 下清本地缓存 sudo dscacheutil -flushcachedig的输出里,ANSWER SECTION后面那个数字就是剩余 TTL。如果你在域名服务商那边看到刚改的记录,而本地 dig 出来的 TTL 还是几千秒,那就是缓存没到期,等或者换一个还没缓存过的解析服务器验证就行。我一般改解析的时候会提前一天把 TTL 调小到 300 秒,改完再调回去,这样切换窗口能压到五分钟以内。
另外几个常见记录类型也得心里有数:A记录指向 IPv4 地址,AAAA指向 IPv6,CNAME是别名,指向另一个域名而不是 IP,MX是邮件路由,NS指定这个域名由哪几台服务器负责解析。新手最容易搞混的是 CNAME 和 A:CNAME 的目标必须是一个域名,你不能把 CNAME 直接指向一个 IP,这个是硬性约束,配错了在后台通常直接报错。
1.2 三次握手:连接是怎么“谈”成的
拿到 IP 之后,如果用的是 TCP,接下来要先建立连接。教科书上讲三次握手通常配一张图,画三条箭头,写 SYN、SYN+ACK、ACK。图看懂了,但为什么非得三次,两次行不行,很多人答不上来。
用生活化的说法:两个人打电话确认线路。A 说“喂,你能听到我吗”(SYN,同时带上自己的初始序号),B 说“能听到,你能听到我吗”(SYN+ACK,带上自己的初始序号并确认 A 的序号),A 说“能听到”(ACK,确认 B 的序号)。到这里双方都确认了“我发得出、你收得到、你发得出、我收得到”这四件事。
为什么两次不够?核心在于 TCP 是全双工的,双方各自要同步自己的初始序列号,并且要确认对方收到了。两次握手只能让发起方确认自己的发送能力,被动方无法确认自己的发送能力是否正常。还有一个更实际的原因:网络上可能存在滞留的、早已失效的历史连接请求报文。如果只用两次握手,一个延迟很久的旧 SYN 到达服务端,服务端回复确认后就直接认为连接建立了,然后傻等一个永远不会来的数据,白白占着资源。三次握手下,最后那个 ACK 发不出来,服务端就知道这是个无效请求,超时后释放。
序列号这件事必须强调一下,它不是随便编的,而是后续所有可靠性机制的地基。TCP 靠“序号 + 确认号”来判定哪些字节收到了、哪些要重传。握手阶段双方交换的就是各自的初始序号(ISN),之后每个字节都按序编号。所以抓包的时候你看到的seq和ack数值,理解成“字节流上的位置指针”就对了,不是包编号,这是新手最常见的误解之一。
建连本身是有成本的,一次完整握手至少要一个往返时延(RTT)。跨地域访问时,RTT 动辄几十毫秒,这几十毫秒就纯纯花在建连上,什么数据都没传。这也是为什么长连接、连接池在高并发场景下几乎是标配——省下来的不是带宽,是时间。
1.3 请求发出与响应回来:HTTP 报文拆开看
连接建好之后,应用层的数据就可以往下走了。以 HTTP 为例,一个请求报文的结构非常朴素,就四块:请求行、请求头、一个空行、可选的请求体。
POST /api/login HTTP/1.1 Host: www.example.com User-Agent: Mozilla/5.0 Content-Type: application/json Content-Length: 42 Connection: keep-alive {"username":"demo","password":"******"}第一行是请求行,包含方法、路径、协议版本。方法里 GET、POST、PUT、DELETE、PATCH 是最常见的,GET 的语义是“取”,理论上不该有副作用,POST 是“提交”,其余几个更贴近“对资源做某种操作”。这套语义不是为了好看,缓存机制、重试机制都是靠它来判断的——一个 GET 请求失败了可以放心重试,一个 POST 请求自动重试就可能重复下单,这是实际开发里真会出事的地方。
请求头里几个必须认识的:Host决定服务端用哪个站点配置来处理这个请求,同一台机器上跑多个站点全靠它区分;Content-Type告诉对方请求体的格式,是 JSON 还是表单还是文件流;Content-Length或者Transfer-Encoding决定对方怎么判断请求体读完了;Connection控制连接是用完就关还是复用。
响应报文结构对称,第一行是状态行,包含协议版本、状态码、状态短语。状态码按首位数字分五类:1xx 是信息性,2xx 成功,3xx 重定向,4xx 客户端侧问题,5xx 服务端侧问题。记住这个分类比背具体码值重要得多,遇到一个没见过的 4xx,你至少能立刻判断“大概率是我这边传的东西有问题”,而不是一头扎进服务端日志里翻。
Connection: keep-alive这个头部值得多聊两句,因为它直接关系到性能。HTTP/1.0 默认是短连接,一个请求一个连接,用完就关,下一个请求重新握手。HTTP/1.1 默认改成复用,一次握手之后连接保持一段时间,后续请求接着用。这个改动在当年带来的性能提升非常明显,尤其是页面里要加载几十个静态资源的场景——省掉几十次握手,效果立竿见影。
1.4 渲染之前的最后一公里
数据回到浏览器之后,事情还没完。如果走的是 HTTPS,在建连和数据传输之间其实还夹着一层加密协商,需要额外的往返来交换参数、验证证书、协商出会话密钥。这也是 HTTPS 比 HTTP 慢一点的原因,不过现代协议版本已经把这个开销压缩得很小了,首次连接也就多一两个 RTT,后续会话复用可以做到几乎零额外开销。
接下来是浏览器解析响应内容:HTML 解析器开始构建文档树,遇到外链的样式、脚本、图片就发起新的请求(这些请求可能复用同一条连接,也可能并行开新连接,取决于协议版本和浏览器策略),最终合成页面渲染出来。所以你在开发者工具的网络面板里看到的请求数量,往往远大于你实际“打开一个页面”的心理预期。
这条路径走完,才算一次完整访问。后面几章我会把这条路径上每一个关键节点单独拆开,讲清楚它的判断条件和出错表现。你如果能把这四个小节的顺序记住,遇到问题时至少知道该从哪一步开始怀疑。
2. 分层模型的价值不在考试,而在排错的坐标系
分层的概念被讲烂了,很多人对它的印象停留在“考试要默写七层名字”。我见过不少人真的能一字不差背出来,但问“网页打不开可能是哪一层的问题”,还是答不上。这说明分层在他们脑子里只是知识点,没有变成工具。
分层的真正价值在于:它把一个巨大的排查空间切成了几个互不重叠的小空间。一旦你把故障定位到某一层,就可以把其他层的可能性全部排除掉,排查范围瞬间缩小。这才是我认为入门阶段最该建立的思维习惯。
2.1 七层与四层:两套说法的对应关系
OSI 七层是教学模型,TCP/IP 四层是工程实践里真正在用的划分。两者的对应关系必须搞清楚,否则看文档时会一直对不上号。
| OSI 七层 | TCP/IP 四层 | 典型协议/设备 | 关注的问题 |
|---|---|---|---|
| 应用层 | 应用层 | HTTP、DNS、SMTP、SSH | 业务数据长什么样 |
| 表示层 | 应用层 | 编码、压缩、加密 | 数据怎么表示 |
| 会话层 | 应用层 | 会话管理 | 谁来维持对话 |
| 传输层 | 传输层 | TCP、UDP | 端到端可靠性、端口 |
| 网络层 | 网络层 | IP、ICMP、路由 | 跨网段寻址与转发 |
| 数据链路层 | 网络接口层 | 以太网、ARP、MAC | 同一链路上的交付 |
| 物理层 | 网络接口层 | 网线、光纤、无线信号 | 比特怎么变成电信号 |
看这张表要注意,OSI 的上三层在实际工程里基本被合并处理,因为它们都由应用程序自己负责,操作系统不关心你在应用层聊什么。真正需要分清的是下面四层,因为每一层的故障现象差别很大。
还有个常见误解是把“会话层”理解成 TCP 连接。不是的,TCP 连接属于传输层,会话层管的是应用之间对话的组织方式,比如断点续传、对话恢复这类逻辑,实际实现里往往是应用代码自己做的。
2.2 封装与解封装:数据包像套娃
发送方从应用层往下走,每经过一层就加一个头部,这个过程叫封装;接收方从下往上走,每层拆掉自己认识的头部往上交,叫解封装。
具体加什么,举个例子最清楚。你要发一段 JSON 数据:
- 应用层把它包装成 HTTP 报文,加了请求行和请求头;
- 传输层加上 TCP 头(源端口、目的端口、序号、确认号、窗口大小等),变成一个 TCP 段;
- 网络层加上 IP 头(源 IP、目的 IP、TTL、协议号等),变成一个 IP 包;
- 链路层加上以太网头(源 MAC、目的 MAC、类型),变成一个帧;
- 物理层把帧变成电信号或光信号发出去。
接收方反过来拆:链路层看 MAC 地址是不是发给自己的,是就拆掉头部往上交;网络层看目的 IP 是不是自己,是就拆掉头部往上交;传输层看端口号对应哪个进程,交上去;应用层按 HTTP 协议解析,拿到 JSON。
这个过程中有个参数必须知道:MTU(最大传输单元)。以太网的 MTU 通常是 1500 字节,意思是链路层一次最多能带 1500 字节的载荷。减去 IP 头 20 字节、TCP 头 20 字节,留给应用数据的空间是 1460 字节,这个值叫MSS(最大报文段长度)。
MTU 1500 - IP 头 20 - TCP 头 20 = MSS 1460为什么这个数字值得记?因为它是很多“诡异问题”的根源。比如你在本地测得好好的接口,部署到某个云环境就卡住不动,或者大文件传一半断掉,很可能就是路径上某一跳的 MTU 比预期小(常见于隧道场景),而两端没有正确处理分片,导致大包被丢弃。这类问题的典型特征是:小请求正常,大请求卡死。遇到这种症状,第一反应就该想到 MTU。
2.3 用分层思维切分故障域
我把排查顺序总结成一个从下往上、逐层验证的流程,非常土但极其有效:
先看物理和链路这一层——网线插了没、网卡是不是启用状态、同一个交换机下能不能互通。这一层的判断工具就是看链路状态和 ARP 表,ip link看网卡状态,ip neigh或者arp -a看邻居缓存。如果连 ARP 都解析不出来,说明二层就不通,往上查全是浪费时间。
再看网络层——两台机器 IP 配得对不对、掩码是不是一致、网关通不通。ping网关是最快的一步。同网段内互相 ping 不通,先怀疑掩码配错了;跨网段 ping 不通,先怀疑网关或路由。
然后看传输层——端口有没有在监听、防火墙有没有拦、连接能不能建立。ss -lntp看监听,nc -zv或者curl -v看能不能连上。
最后看应用层——服务本身有没有报错、返回内容对不对、证书有没有过期。
这个顺序的价值在于它不允许你跳步。我见过太多人一上来就翻应用日志,翻了两个小时,最后发现是网线松了。也见过一上来就怀疑防火墙,实际是服务根本没起来。按层走虽然看起来慢,实际上是最快的路径,因为它保证了每一步的结论都是确定的。
3. IP、掩码、网关这三件套,算一遍才算真懂
这三个概念被讲得太多,反而容易让人产生“我懂了”的错觉。判断标准很简单:给你一个地址和掩码,你能否在一分钟内算出网络地址、广播地址和可用主机范围。算不出来就是没懂,这跟会不会背定义没关系。
3.1 IP 地址与 CIDR 写法
IPv4 地址是 32 位,写成四段十进制,每段 0 到 255,中间用点分开。这是给人看的,机器看的是二进制。理解这一点非常关键,因为后面所有的计算都在二进制层面进行。
比如192.168.1.130,拆成二进制是:
192 → 11000000 168 → 10101000 1 → 00000001 130 → 10000010合起来就是 32 位。地址本身分成网络部分和主机部分,哪几位是网络、哪几位是主机,由掩码决定。CIDR 写法是把掩码用“前缀长度”表示,比如/24表示前 24 位是网络部分。
历史上还有 A/B/C 类的说法,A 类默认 /8,B 类默认 /16,C 类默认 /24。这套分类早就被 CIDR 取代了,现在看到它基本只在老教材和老配置里。实际工作中一律用前缀长度表达,因为它能表达任意长度的掩码,而分类地址只能表达三种。
3.2 掩码的本质是与运算
掩码就是一串连续的 1 后面跟一串连续的 0。判断一个 IP 属于哪个网络,做法是把 IP 和掩码做按位与运算,结果就是网络地址。
拿192.168.1.130/26走一遍:
掩码 /26 → 前 26 位是 1 255.255.255.192 二进制:11111111.11111111.11111111.11000000 IP: 192.168.1.130 → 11000000.10101000.00000001.10000010 掩码:255.255.255.192 → 11111111.11111111.11111111.11000000 与运算结果: 11000000.10101000.00000001.10000000 转回十进制: 192.168.1.128所以网络地址是192.168.1.128。这个网段的主机位是 6 位(32 减 26),可表示的地址数是 2 的 6 次方等于 64 个,范围从192.168.1.128到192.168.1.191。其中第一个是网络地址,最后一个是广播地址,都不能分配给主机,所以可用主机数是 62 个,范围是192.168.1.129到192.168.1.190。
这个计算必须练到条件反射。因为实际排查中经常遇到这种情况:两台机器看起来在同一个网段,实际掩码不一致,导致一台认为对方在同网段直接 ARP,另一台认为对方在别的网段要交给网关,结果就是单向通、双向不通。这种问题的现象特别迷惑人,但只要把两边的掩码拿出来算一遍,立刻水落石出。
再补一个容易搞混的点:/31和/32。/31只有两个地址,按传统规则没有可用主机,但在点对点链路里现在允许两个地址都当主机用;/32只有一个地址,通常用于给单个主机配一个独立地址,或者做回环路由。这两个是特例,别拿通用公式硬套。
3.3 网关:出得去与回得来
网关本质上就是“当我发现目的地址不在我所在的网段时,把包交给谁”。每台主机上都有一张路由表,最简单的路由表只有两条:一条是“我所在的网段,直接投递”,另一条是“其他所有地址,交给网关”,后者叫默认路由,写作0.0.0.0/0。
# 查看路由表 ip route # 典型输出 default via 192.168.1.1 dev eth0 192.168.1.0/24 dev eth0 scope link这两行的意思是:目的地址在192.168.1.0/24范围内,直接从 eth0 发出去(靠 ARP 找到对方的 MAC);其他所有地址,都发给192.168.1.1。
这里有个反直觉的事实:ping 通网关不代表能上网,ping 不通网关也不代表一定上不了网。如果目标在同一个网段,压根不需要网关,网关挂了照样互通。反过来,网关能 ping 通,但如果网关到自己上游的路由有问题,或者做了访问控制,你依然出不去。所以“ping 网关”只是一个中间检查点,不是终点。
另一个高频坑是回程路由。A 能发给 B,不代表 B 能回给 A。如果 B 的默认网关指向了一个不知道 A 网段怎么走的设备,B 的响应包就会迷路。表现就是 A ping B 超时,但 B 上抓包能看到请求进来了、响应也发出去了。这类问题在有多个网段、多条出口的机房里特别常见,抓包是唯一靠谱的定位手段——你需要在两端同时抓,看包到底在哪一段消失。
3.4 私有地址与地址转换的实际影响
有三段地址被规定为私有地址,只能在内网使用,不会在公网上被路由:
| 地址段 | 前缀 | 可用地址数量(约) | 常见用途 |
|---|---|---|---|
| 10.0.0.0 - 10.255.255.255 | /8 | 1677 万 | 大型内网、云上 VPC |
| 172.16.0.0 - 172.31.255.255 | /12 | 104 万 | 中型内网 |
| 192.168.0.0 - 192.168.255.255 | /16 | 6.5 万 | 家庭、小型办公 |
内网机器要访问公网,必须经过网络地址转换。做法是把内网源地址替换成出口设备的公网地址,同时记录一条映射关系,等响应回来再按映射改回去。这个机制解决了两件事:公网地址不够用,以及内网结构对外不可见。
但它带来了一个必须知道的副作用:外网无法主动连内网。因为转换设备上没有对应的映射记录,不知道把包转给谁。如果确实需要外部访问内网某个服务,就得手工配置一条端口映射规则,把公网某端口固定映射到内网某台机器的某端口。
这个机制还影响日志和限流。你在服务端看到的来源地址,往往是转换设备的地址,而不是真实客户端地址。所以需要靠X-Forwarded-For这类头部来传递原始地址,而这个头部是可以伪造的,服务端不能无条件信任它——只能信任自己链路上那一层设备追加的内容。这是个安全细节,但实际项目中踩坑的不少。
4. TCP 与 UDP 的取舍,别只记“可靠/不可靠”
“TCP 可靠,UDP 不可靠”这句话本身没错,但它几乎不能帮你做任何决策。真正需要判断的是:这个场景能不能承受重传带来的延迟抖动?能不能容忍丢一点数据?连接状态带来的额外开销值不值?这些问题想清楚了,选型是自然的结果。
4.1 TCP 的可靠性是靠什么堆出来的
TCP 保证可靠的手段就三样,但每一样都有代价。
第一是序号与确认。每个字节都有编号,接收方收到后回复确认号,表示“这个位置之前的我都收到了”。发送方据此判断哪些需要重传。代价是每个包都要带确认信息,双向都有额外流量。
第二是超时重传。发出去一段时间没收到确认,就重发。这个“一段时间”不是固定值,而是根据实测的往返时延动态估算的,还要加上一定的余量。估算得太激进会误判重传,估算得太保守会让故障恢复变慢。代价是重传本身会消耗带宽,而且会进一步加剧拥塞。
第三是校验和。每个段都带校验和,接收方算一遍,对不上就丢弃,等着对方重传。代价很小,但它只能发现错误,不能纠正。
这三样东西叠在一起,才有了“可靠”这个效果。但要注意,TCP 保证的是“字节流按序、无差错、不重复地送达”,它不保证“送达时间”,也不保证“对方应用处理成功”。一个请求发出去,对方内核收到了,但应用进程崩溃了没处理,TCP 层面依然是成功的。这是很多人混淆的地方——应用层的可靠性得应用自己保证。
4.2 窗口、拥塞控制与真实网络里的表现
如果每发一个包都要等确认,那效率会低到无法接受。所以 TCP 引入了滑动窗口:发送方可以连续发若干个段而不必逐个等待确认,窗口大小就是“允许多少数据在途”。
窗口大小不是固定的,接收方会在每个确认里带上自己还能接收多少(接收窗口),发送方据此调整。同时发送方还维护一个拥塞窗口,根据网络状况动态变化。实际发送量取两者较小值。
拥塞控制的经典过程分四个阶段:慢启动、拥塞避免、快重传、快恢复。慢启动阶段每收到一个确认就把拥塞窗口加一倍,呈指数增长;涨到一个阈值后进入拥塞避免,改为线性增长;一旦检测到丢包,窗口大幅回退。
这套机制解释了很多实际现象。比如为什么一个新连接刚开始传输时速度偏慢,要过一会儿才提上来——就是慢启动在起作用。比如为什么跨地域传输大文件,前几秒的吞吐明显低于后面的稳态值。再比如为什么在高丢包链路上,即使带宽很充裕,TCP 的实际吞吐也上不去——因为丢包被当成拥塞信号,窗口一路回退。
理解这一点对做性能优化很有帮助。你在应用层看到“接口慢”,有可能是网络本身带宽不够,也有可能是 RTT 太大导致窗口涨不起来,还有可能是链路有轻微丢包把拥塞窗口压住了。这三种情况的优化手段完全不同,靠猜是猜不出来的,得看指标。
顺便提一个常被忽略的参数:Nagle 算法。它会把小包攒起来合并发送,减少小包数量,提升带宽利用率。但它会和延迟确认机制叠加,产生几十毫秒的额外延迟。对于交互式的低延迟场景(比如实时的操作反馈),通常会在 socket 上关掉它。这个开关在很多语言的标准库里都能设,配置项名字里带TCP_NODELAY。
4.3 UDP 被误解的地方
UDP 的头部只有 8 个字节:源端口、目的端口、长度、校验和。就这些,没有序号,没有确认,没有窗口,没有连接状态。所以它快,快在没有任何额外机制。
但“不可靠”不等于“不能用”。很多场景恰恰需要 UDP 的这种特性。比如实时音视频,一帧数据丢了,重传回来也过了显示时间,重传毫无意义,不如直接丢掉让解码器做补偿。比如游戏里的位置同步,宁可丢掉旧的位置包,也要保证最新的位置能尽快到达。再比如 DNS 查询,一次请求一次响应,用 UDP 省掉握手开销,超时了应用自己重发就行。
现在很多基于 UDP 的协议会在应用层自己实现可靠传输和拥塞控制,把需要的机制按需加上去,不需要的一律不要。这个思路其实很值得学习:不是“UDP 不可靠所以不能用”,而是“我只需要一部分可靠性,那就只实现那一部分”。
4.4 选型对照与实际案例
| 维度 | TCP | UDP |
|---|---|---|
| 连接 | 需要握手建立 | 无连接,直接发 |
| 有序性 | 保证按序 | 不保证 |
| 可靠性 | 重传保证不丢 | 不保证送达 |
| 头部开销 | 20 字节起 | 8 字节 |
| 传输方式 | 字节流 | 数据报,一次一个边界 |
| 典型场景 | 接口调用、文件传输、数据库连接 | 音视频、游戏、DNS、指标上报 |
有个细节容易忽略:TCP 是字节流,没有消息边界。你发两次,对方可能一次收到,也可能分三次收到。所以应用层协议必须自己定义边界——要么用长度前缀,要么用分隔符。很多“粘包”问题本质上就是没处理好边界,跟 TCP 本身没关系。UDP 则天然有边界,一个数据报就是一个整体,收的时候要么完整收到,要么完全收不到。
实际选型时我的判断顺序是:先看能不能容忍丢数据,不能就 TCP;能容忍丢,再看延迟要求是否严格,严格就 UDP;两者都行的话,看生态和运维成本,TCP 的工具链和排查手段成熟得多,出问题更好定位,这本身也是一个加分项。
5. 抓一次包,把抽象概念落到字节上
看再多图解,不如自己抓一次。抓包的意义在于把“听说”变成“看见”——你能亲眼看到三次握手的三个包,看到序号是怎么递增的,看到连接是怎么关的。这种印象一旦建立,后面看任何协议文档都会有画面感。
5.1 实验环境怎么搭最省事
最省事的方案是单机自测:本机起一个简单的 HTTP 服务,然后自己访问自己。省掉了网络环境的变量,专注看协议本身。
# 起一个最简单的 HTTP 服务,监听 8000 端口 python3 -m http.server 8000 # 另一个终端发起请求 curl -v http://127.0.0.1:8000/抓包用命令行工具就够,不用折腾图形界面。需要注意权限,抓包通常要 root 或者具备相应能力:
# 查看有哪些网卡 ip link show # 抓 8000 端口的包,-n 表示不做名字解析,-S 显示绝对序号 sudo tcpdump -i lo -n -S port 8000-i lo表示抓回环网卡。这一步是新手最容易翻车的地方:访问127.0.0.1走的是回环,抓物理网卡(比如 eth0)是抓不到的。同理,在容器环境里,进容器内部的流量走的是虚拟网桥,在宿主机默认网卡上也抓不到,得指定对应的网桥接口或者进容器命名空间里抓。
如果要做多机实验,两台虚拟机接同一个虚拟网络就行,配好 IP 和掩码,确保能互相 ping 通,然后再开始抓。环境越简单越好,变量越少,出问题时越容易判断。
5.2 抓取并读懂三次握手
访问一次之后,抓包输出大概长这样(做了简化):
IP 127.0.0.1.54321 > 127.0.0.1.8000: Flags [S], seq 1001, win 65495, length 0 IP 127.0.0.1.8000 > 127.0.0.1.54321: Flags [S.], seq 2001, ack 1002, win 65483, length 0 IP 127.0.0.1.54321 > 127.0.0.1.8000: Flags [.], ack 2002, win 65535, length 0解读一下这三个包。第一行Flags [S]是 SYN,seq 1001是客户端选的初始序号。第二行Flags [S.]是 SYN+ACK,点号表示 ACK 标志置位,seq 2001是服务端的初始序号,ack 1002表示“我期待你从 1002 开始发”,也就是确认了 1001 这一个字节。第三行Flags [.]纯 ACK,ack 2002确认了服务端的 SYN。
注意这里的序号是相对的,抓包工具默认会做偏移处理让你好读。-S参数就是让它显示绝对序号,这样你能看到真实的随机初始值——每次连接都不一样,这是安全设计,防止被预测。
再看关闭过程,会看到四个包:
Flags [F.] 客户端发 FIN Flags [.] 服务端 ACK Flags [F.] 服务端发 FIN Flags [.] 客户端 ACK四次挥手的原因是 TCP 全双工,两个方向要分别关闭。服务端收到 FIN 之后先回 ACK,表示“我知道你要关了”,但它自己可能还有数据没发完,所以不能立刻发 FIN,要等数据处理完。这就导致中间多了一次交互。
关闭之后主动关闭方会进入一个叫TIME_WAIT的状态,持续 2 倍的最大报文段生存时间。这个状态的目的是确保最后那个 ACK 能到达对方(如果丢了,对方会重发 FIN,还能再回一次),同时让本次连接的残留报文在网络中自然消失,不干扰后续使用相同端口的新连接。
这个状态在高并发短连接场景下会积累大量条目,把端口占满。见过不少“服务跑一段时间就连不上”的案例,根子就是短连接太多、TIME_WAIT堆积。解决办法要么减少短连接改用连接池,要么在允许的范围内调整相关参数。不过调参数是治标,治本还是得看连接管理策略。
5.3 看一次完整 HTTP 交互
握手之后就是数据包。用-A参数可以按 ASCII 打印内容:
sudo tcpdump -i lo -n -A port 8000你会看到明文的请求行、请求头、空行,然后是响应。因为 HTTP 没有加密,内容全裸。这也是为什么 HTTP 时代在公共网络上传输敏感信息很不安全——同一链路上的人只要开到混杂模式就能看到。
看到这里可以顺手验证一下前面讲的边界问题:如果响应体比较大,你会看到它被拆成多个 TCP 段,每个段的长度受 MSS 限制。在本地回环上 MSS 通常很大(因为 MTU 大),但走真实网络时就会被切成 1460 左右的小块。
5.4 抓包时最容易踩的几个坑
抓不到包。第一查网卡选对没,第二查权限够不够,第三查流量是不是根本没走你以为的那条路。容器、虚拟网卡、隧道这些场景,包的实际路径和直觉往往不一致,用ip route和ip link一步步确认。
抓到了但看不懂。通常是没加-n,工具在做反向解析,输出被主机名和端口名搅乱了。加上-n一律显示数字,清爽很多。
丢包。高流量下抓包工具本身可能来不及处理,内核缓冲区满了就丢。可以调大缓冲区,或者用文件写入模式先存下来再分析:
sudo tcpdump -i eth0 -n -w capture.pcap -s 0-s 0表示抓完整包不截断,-w写入文件。事后慢慢看,比实时盯着刷屏的输出高效得多。
时间戳对不上。不同机器抓的包,时间基准不一致,跨机对比时会算错延迟。这种情况要么先做时钟同步,要么只看单机的相对时间差。
6. 网络故障的排查链路,按顺序来别乱撞
前面几章讲的是原理,这一章讲的是把原理用起来。我给一套自己常用的固定流程,按顺序走,绝大多数问题能在十分钟内定位到层,剩下的就是细查。
6.1 先确认通不通:分层验证法
第一步永远是确认“能不能通”,而不是“为什么不通”。这两个问题的排查方向完全不同。
# 1. 本机回环是否正常 ping -c 3 127.0.0.1 # 2. 本机地址是否配好 ip addr show # 3. 同网段其他机器是否可达 ping -c 3 192.168.1.20 # 4. 网关是否可达 ping -c 3 192.168.1.1 # 5. 外部地址是否可达 ping -c 3 8.8.8.8 # 6. 域名解析是否正常 dig www.example.com +short这六步是有顺序的,每一步的失败都指向不同的层。回环不通说明协议栈本身有问题;本机地址没有说明网络配置没生效;同网段不通多半是链路或掩码问题;网关不通看交换机和网关设备;外部地址不通但网关通,看出口路由和地址转换;能 ping 通 IP 但解析不出域名,那就是 DNS 问题。
这个顺序的好处是把“网络不通”这个大问题拆成了若干个可以独立回答的小问题。我见过太多人一上来就 ping 一个外网地址,不通,然后开始抓瞎——他连自己到底卡在哪一步都不知道。
还有一个实用技巧:ping 不通不代表服务不可用。有些设备或云环境会屏蔽 ICMP,这时候要用针对端口的探测:
# 探测端口是否开放,-z 只扫不传数据,-v 显示详情 nc -zv 192.168.1.20 3306 # 用 curl 直接测应用层 curl -v --max-time 5 http://192.168.1.20:8080/healthcurl -v的信息量非常大,它会告诉你 DNS 解析用了多久、TCP 连接用了多久、TLS 握手用了多久、首字节等了多久。这几段时间分别对应不同的层,哪一段异常就查哪一层,效率极高。
6.2 再看快不快:路径与延迟
如果连通性没问题但速度慢,就要看路径上的每一跳。
# 逐跳探测,看在哪一跳延迟突然升高 traceroute -n www.example.com # 持续探测并统计每跳的丢包率与延迟分布,比单次探测靠谱 mtr -n --report www.example.com单次traceroute的参考价值有限,因为每一跳的响应可能来自不同的路径,而且某些设备对探测包做了限速。用mtr跑一段时间看统计,才有意义。重点看的是:哪一跳的延迟突然从个位数跳到几十上百毫秒,哪一跳开始出现持续的丢包。
要注意,中间某一跳不响应不代表链路断了。很多设备默认不回探测包,但转发正常。判断标准是:如果后面几跳能正常响应,那这一跳的“不响应”可以忽略。
延迟高但丢包为零,通常是物理距离或者链路拥塞;延迟不高但丢包明显,通常是链路质量差或者中间设备限速。这两种情况的处理方式完全不同,一个只能优化架构(比如就近部署),另一个可以找链路提供方排查。
还有一种情况是延迟看起来正常但应用响应慢,这时候瓶颈大概率不在网络上,而在服务端处理逻辑或者数据库查询。判断方法很简单:看首字节时间。如果连接建立很快、请求发出很快,但等响应等了很久,那就是服务端的问题,网络这边不用再查了。
6.3 端口与连接状态:谁占着不放
服务起不来,最常见的两个原因:端口被占,或者根本没监听在你以为的地址上。
# 查看所有监听端口及对应进程 ss -lntp # 只看某个端口 ss -lntp | grep 8080 # 统计各状态连接数,排查堆积 ss -s ss -ant | awk '{print $1}' | sort | uniq -css比老牌的netstat快很多,在连接数多的时候差别明显,建议养成用ss的习惯。
一个高频陷阱是监听地址。服务配置成监听127.0.0.1:8080,那就只有本机能连,其他机器一律连不上。日志里看不出任何异常,因为服务确实起来了。这种情况的唯一判断方法就是看ss输出里的本地地址列——显示127.0.0.1:8080还是0.0.0.0:8080或者*:8080,含义完全不同。
另一个常见现象是连接数堆积。CLOSE_WAIT大量出现,说明应用收到了对方的关闭请求但没有执行关闭,通常是代码里没正确释放连接,这是应用层 bug,跟网络无关。TIME_WAIT大量出现,是主动关闭方留下的正常状态,短连接多的时候必然出现,优化方向是减少短连接。SYN_RECV堆积,说明有大量半连接没完成握手,可能是握手包被丢弃,也可能是遭遇了异常流量。
能把状态码和原因对应起来,排查速度会快一个数量级。
6.4 常见报错对照表与处理思路
| 现象 | 最可能的原因 | 下一步动作 |
|---|---|---|
| Connection refused | 端口没有监听,或被明确拒绝 | ss -lntp确认监听状态 |
| Connection timed out | 中间有设备丢弃了包 | 逐跳探测,确认丢在哪一段 |
| No route to host | 本机路由表没有匹配项 | ip route查看路由 |
| Name or service not known | 域名解析失败 | dig确认解析链路 |
| 大请求卡死、小请求正常 | 路径 MTU 不匹配 | 检查隧道配置与分片设置 |
| 能连上但很快断开 | 连接空闲超时或对方主动关闭 | 查中间设备的超时配置 |
| 5xx 错误但连接正常 | 应用层异常 | 查服务日志,网络层不用再查 |
| 部分机器能访问部分不能 | 网段、掩码或访问控制差异 | 对比两边网络配置 |
这张表不是万能答案,但能帮你快速判断该往哪个方向走。核心思路还是那个:先确定问题在哪一层,再在这一层里找具体原因。跨层乱猜是最耗时间的做法。
最后分享一个我自己养成的习惯:凡是遇到“时好时坏”的网络问题,第一件事是记录时间点和现象,然后在两端同时抓包。因为这类问题往往和负载、时段、特定路径有关,靠单次复现很难抓到,但有了两端对照的抓包记录,包到底在哪一段消失、延迟到底加在哪一跳,一目了然。我靠这个习惯定位过好几次“只在高峰期出现”的问题,比反复猜要高效得多。