计算机网络 —— TCP/IP

📅 2026/7/12 0:01:08 👁️ 阅读次数 📝 编程学习
计算机网络 —— TCP/IP

1 为什么学习网络协议

1.1 日常开发中的网络问题

即使你写的业务代码大部分是CRUD,网络问题也会冷不丁找上门:

  • 接口超时:调用下游服务,偶尔报read timeout,但下游明明很快。排查后发现是 TCP 连接池不够,大量连接在排队等待释放
  • 连接断开:客户端长连接莫名其妙断开,日志里出现Connection reset by peer。可能是服务端 CLOSE_WAIT 堆积,也可能是防火墙 idle timeout 踢掉了连接
  • 慢请求:同样的 API,有的用户快有的慢。抓包发现 DNS 解析耗时 2 秒,或者 TCP 握手阶段丢包导致重传
  • 数据乱序或者丢包:用 UDP 传日志,偶尔少了几条。不了解 UDP 不可靠性,就会以为是代码 bug

这些问题背后全是网络协议的知识。不懂协议,你只能重启大法、加日志盲猜;懂了协议,你能通过tcpdumpnetstat精准定位根因。

1.2 网络协议的分层思想

计算机网络的复杂性决定了它必须分层设计。每一层只关心自己职责范围内的事,下层为上层提供服务,上层无需关心下层实现细节。

OSI 七层模型(理论标准)

层次

名称

功能

典型协议/设备

7

应用层

为用户提供网络应用

HTTP、FTP、SMTP、DNS

6

表示层

数据格式转换、加密、压缩

SSL/TLS、JPEG、ASCII

5

会话层

建立、管理和终止会话

NetBIOS、RPC

4

传输层

端到端可靠或不可靠传输

TCP、UDP

3

网络层

路由选择、分组转发

IP、ICMP、OSPF

2

数据链路层

相邻节点间的帧传输

Ethernet、WiFi、ARP

1

物理层

比特流传输

光纤、双绞线、集线器

七层网络可以记为:“物链网输会示用”(物联网叔会使用)

TCP/IP 四层模型(事实标准)

层次

名称

对应 OSI 层

主要协议

4

应用层

5~7

HTTP、DNS、FTP、SSH

3

传输层

4

TCP、UDP

2

网络层

3

IP、ICMP、IGMP

1

网络接口层

1~2

Ethernet、ARP、PPP

四层网络模型可以记为:“接网输用”(接网叔用)

2 TCP/IP基石 —— IP与路由

IP协议是TCP/IP协议的核心,所有的TCP,UDP,IMCP,IGMP的数据都以IP数据格式传输。要注意的是,IP不是可靠的协议,这是说,IP协议没有提供一种数据未传达以后的处理机制,这被认为是上层协议:TCP或UDP要做的事情。

2.1 IP地址、子网掩码、CIDR

IP 地址是网络层的“门牌号”,用于唯一标识一台主机。IPv4 地址长 32 位,通常写成点分十进制,如192.168.1.1

子网掩码(Subnet Mask)​ 用来划分 IP 地址中的网络号主机号。例如:

IP 地址: 192.168.1.10 → 11000000.10101000.00000001.00001010 子网掩码: 255.255.255.0 → 11111111.11111111.11111111.00000000 网络号: 192.168.1.0 (IP & 掩码) 主机号: 0.0.0.10 (IP & ~掩码)

CIDR(无类别域间路由)​ 用IP/前缀长度表示,例如192.168.1.0/24等价于子网掩码255.255.255.0,表示前 24 位是网络号,后 8 位是主机号。

  • /24可用主机数:2^(32-24) - 2 = 254(去掉全 0 的网络地址和全 1 的广播地址)
  • 常见前缀:/8(A 类)、/16(B 类)、/24(C 类)

面试可能会问10.0.0.0/8有多少可用 IP?答案是 2^24 - 2 = 16,777,214。

2.2 数据包如何在网络跳跃

当你的电脑访问www.example.com时,数据包从你的网卡出发,经过多个路由器,最终到达目标服务器。这个过程依靠路由表下一跳机制。

路由表​ 每个路由器都有一张路由表,记录着目标网络、子网掩码、下一跳地址和出接口。关键点是最长前缀匹配:路由器会选择掩码最长的匹配项。

数据包跳跃过程(简化)

  1. 你的电脑查询路由表,发现目标不在局域网内,将包发给默认网关。
  2. 家用路由器查表,转发给运营商的接入路由器。
  3. 运营商骨干网路由器通过 BGP 协议逐跳转发,直到抵达目标服务器。
  4. 目标服务器所在局域网的交换机根据 MAC 地址将包交给服务器网卡。

每一跳,数据包的源/目的 IP 不变,但源/目的 MAC 地址会随链路改变。

这里我通过抓包软件抓取curl http://nginx.org/LICENSE的整个流程:

  • 阶段 1:DNS 解析(并行查询与响应竞赛)
    • 第 5254、5255 行(87.7435s):你的主机10.33.3.236向主 DNS222.200.115.251同时发起A(IPv4)AAAA(IPv6)域名解析请求,查询nginx.org
    • 第 5260、5261 行(87.7700s):仅隔26ms,你的主机可能因主 DNS 响应稍慢,又向备 DNS10.1.3.38重传/并行发起了同样的 A 和 AAAA 查询(glibc 的 resolver 常会并行发送)。
    • 第 5262、5263 行(87.7893s):备 DNS10.1.3.38迅速响应:
    • 返回 A 记录:nginx.org A 52.58.199.22(数据包长度 85,包含 IP)。
    • 返回 AAAA 记录:响应包长度 69(无 IPv6 地址,或因未设置而返回空)。
    • 第 5277、5278 行(88.0827s)主 DNS222.200.115.251此时才姗姗来迟地返回响应,且返回了一个不同的 IP3.125.197.172

关键决策:由于备 DNS 提前 300ms 返回了52.58.199.22,你的curl已经采用了该 IP 建立连接,因此后续主 DNS 返回的3.125.197.172被直接丢弃。

  • 阶段2TCP 三次握手(连接 52.58.199.22)
    • 第 5264 行(87.7991s):客户端发送SYN包(Seq=0),向52.58.199.22:80发起连接,带有 MSS=1460、窗口缩放等选项。
    • 第 5274 行(88.0698s):服务器回复SYN-ACK(Seq=0, Ack=1),确认连接。
    • 第 5275 行(88.0699s):客户端回复ACK(Seq=1, Ack=1)。三次握手完成,连接建立。
  • 阶段 3:发送 HTTP 请求
    • 第 5276 行(88.0701s):客户端发送HTTP GET请求,请求路径为/LICENSE,协议为 HTTP/1.1。数据包长度为 134 字节(包含 HTTP 头)。
  • 阶段4:ICMP 异常(迟到的 DNS 响应被拒绝)
    • 第 5279 行(88.0828s):当主 DNS222.200.115.251将迟到的 DNS 响应(包含3.125.197.172)发回给客户端时,客户端回复了一个ICMP 类型 3(目标不可达)代码 3(端口不可达)
  • 阶段 5:接收 HTTP 响应(数据分片与重组)

    • 第 5301 行(88.2836s):服务器回复纯ACK,确认收到客户端的 GET 请求。
    • 第 5302、5303 行(88.2836s):服务器发送 HTTP 200 OK 响应。由于响应内容较大(LICENSE文件),TCP 将其分为两个数据包发送:
      • 第 5302 行:发送1360 字节有效载荷(标记为[TCP PDU reassembled in 5303],表示待重组)。
      • 第 5303 行:发送剩余数据(约 361 字节),并在该包中附带 HTTP 状态行HTTP/1.1 200 OK (text/plain)
    • 第 5304 行(88.2836s):客户端发送ACK,确认已收到全部数据(Ack=1722,证明累计收到了 1721 字节数据,与 1360+361 吻合)。
  • 阶段 6:四次挥手(优雅关闭连接)

    • 第 5305 行(88.2838s):客户端发送FIN, ACK,表示数据已接收完毕,主动要求关闭发送方向。
    • 第 5306 行(88.4981s):服务器回复ACK,确认收到 FIN。
    • 第 5307 行(88.4982s):服务器发送自己的FIN, ACK(本数据包仅显示[ACK],但从序列号Ack=82Seq=1723推断,这实际是双向关闭的最后一环),客户端回复最终 ACK,连接彻底关闭

这里有关主备DNS做一个解释:

  • 你的电脑 (10.33.3.236)同时向两个DNS服务器发起了查询:
    • 主DNS (222.200.115.251):这个IP很可能是你的网络运营商(如电信、联通)提供的官方DNS
    • 备DNS (10.1.3.38):这是一个内网IP,很可能是你的路由器或局域网内的另一台设备。
  • “竞赛”与切换:这一次,备DNS (10.1.3.38) 响应速度更快,先返回了IP地址 (52.58.199.22)。你的电脑采用了这个结果,并忽略了主DNS稍后才返回的另一个IP (3.125.197.172)。
  • ICMP错误:当主DNS迟到的响应到达时,电脑发现用于询问的“窗口”已经关闭,于是它发回一个ICMP错误,相当于告诉主DNS:“这个问题我已经解决了,不用再回复了。”

2.3 MTU与IP分片

MTU(Maximun Transmission Unit)是数据链路层一帧能承载的最大数据量。以太网的MTU通常是1500字节。如果IP包大于MTU,就需要分片

分片的弊端

  • 增加开销:每个分片都要加 IP 头。
  • 放大丢包影响:只要一个分片丢失,整个原始报文都得重传。
  • 安全风险:分片重叠攻击等。

路径 MTU 发现(PMTUD)​ 是现代 TCP 避免分片的关键机制:发送方在 IP 头设置DF标志,如果某个链路 MTU 太小,路由器会丢弃并回复 ICMP 错误,发送方据此减小包大小。

这就是为什么 TCP 的 MSS(Maximum Segment Size)通常设为 1460 字节(1500 - 20 字节 IP 头 - 20 字节 TCP 头)。

2.4 ARP 协议与地址解析

ARP 是根据IP地址获取MAC地址的一种协议。

ARP(地址解析)协议是一种解析协议,本来主机是完全不知道这个IP对应的是哪个主机的哪个接口,当主机要发送一个IP包的时候,会首先查一下自己的ARP高速缓存(就是一个IP-MAC地址对应表缓存)。

如果查询的IP-MAC值对不存在,那么主机就向网络发送一个ARP协议广播包,这个广播包里面就有待查询的IP地址,而直接收到这份广播的包的所有主机都会查询自己的IP地址,如果收到广播包的某一个主机发现自己符合条件,那么就准备好一个包含自己的MAC地址的ARP包传送给发送ARP广播的主机。

而广播主机拿到ARP包后会更新自己的ARP缓存(就是存放IP-MAC对应表的地方)。发送广播的主机就会用新的ARP缓存数据准备好数据链路层的的数据包发送工作。

3 TCP核心机制

3.1 连接管理:三次握手与四次挥手

三次握手

  • 第一步:客户端发送 SYN,初始化序列号 x,进入SYN_SENT
  • 第二步:服务端回复 SYN+ACK,初始化序列号 y,确认号 ack=x+1,进入SYN_RCVD
  • 第三步:客户端回复 ACK,双方进入ESTABLISHED

为什么是三次,不是两次?

答:“从表面看,三次握手是为了验证双方的收发能力是否正常。第一次握手表明确认客户端的发送能力和服务端的接收能力;第二次握手确认服务端的发送能力和客户端的接收能力;但此时服务端还不知道客户端是否收到了自己的回应,所以需要第三次握手来最终确认。
但更深层的核心原因有两点:
第一,为了防止‘已失效的连接请求报文段’突然又传到服务端造成资源浪费。如果没有第三次握手,服务端收到一个过期的SYN就会直接建立连接,导致服务端白白消耗资源。
第二,这是为了同步双方的初始序列号(ISN)。三次握手让双方明确地交换并确认了各自的起始序号,这是后续TCP可靠传输和拥塞控制的基础。
所以,三次握手是保障TCP可靠性的最小且必要步骤,两次握手无法规避历史连接的隐患,而四次握手则冗余没必要。”

四次挥手

注意:这里的抓包软件Wireshark 为了方便用户阅读,自动隐藏了一次挥手过程。Wireshark 的显示逻辑是:如果 ACK 包紧接着上一个包发出(或者是为了响应上一个包),它会尝试将它们合并显示。

  • 第一步:主动关闭方发送 FIN,进入FIN_WAIT_1
  • 第二步:被动关闭方回复 ACK,进入CLOSE_WAIT;主动方进入FIN_WAIT_2
  • 第三步:被动方发送 FIN,进入LAST_ACK
  • 第四步:主动方回复 ACK,进入TIME_WAIT(等待 2MSL 后关闭)。

TIME_WAIT 两大作用

  1. 保证最后一个 ACK 可靠到达。
  2. 让旧报文在网络中自然消失。

TIME_WAIT 过多怎么办?

  • 开启tcp_tw_reuse(需配合tcp_timestamps)。
  • 使用长连接减少连接次数。
  • 服务端改为被动关闭。

⚠️tcp_tw_recycle已在 Linux 4.12 之后移除,且 NAT 环境下有严重 bug,不要再使用。

为什么是四次挥手?

TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

为什么客户端收到 FIN 后还要等 2MSL(Time-Wait)?

这是为了确保服务端收到了最后的 ACK。如果客户端发送完最后一个ACK后直接关闭,万一这个ACK在网络中丢失了,服务端会因为迟迟收不到ACK而认为自己的FIN没送到,于是超时重传FIN。此时客户端如果已经关闭了,收到这个重传的FIN就会回复RST(复位),导致服务端报错。所以客户端必须等待2MSL(最大报文生存时间的两倍),确保如果服务端重传FIN,自己还能收到并重新发送ACK,从而确保被动关闭方能正常进入CLOSED状态,这是TCP可靠性的最后一道保险。”

3.2 可靠传输机制

  • 序列号与确认应答:每个字节都有序列号,接收方回复 ACK 表示期望收到的下一个字节序号(累计确认)。
  • 超时重传(RTO):动态计算 RTT,超时重传,指数退避。
  • 快速重传:收到三个相同 ACK 立即重传,不等超时。
  • SACK:选择性确认,只重传丢失的部分。
  • 滑动窗口:发送窗口 = min(cwnd, rwnd),接收窗口由接收方缓冲区决定。

3.3 流量控制 vs 拥塞控制

流量控制

拥塞控制

关注点

接收方的处理能力

网络的承载能力

作用对象

发送方与接收方之间

全网路径上的路由器/交换机

实现方式

滑动窗口(rwnd)

拥塞窗口(cwnd)+ 算法

3.3.1 流量控制

如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据的丢失。所谓 流量控制 就是让发送方的发送速率不要太快,要让接收方来得及接收。

利用滑动窗口机制可以很方便地在TCP连接上实现对发送方的流量控制。

设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window)。因此,发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不是报文段。假设每一个报文段为100字节长,而数据报文段序号的初始值设为1。大写ACK表示首部中的确认位ACK,小写ack表示确认字段的值ack。

从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1,只有在ACK=1时确认号字段才有意义。

TCP为每一个连接设有一个持续计时器(persistence timer)。只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口控测报文段(携1字节的数据),那么收到这个报文段的一方就重新设置持续计时器。

3.3.2 拥塞控制

流量控制只考虑接收方的处理能力,但网络本身也可能拥堵——路由器缓存溢出、链路带宽不足。如果所有发送方都按自己的最大能力发送,网络很快就会瘫痪(拥塞崩溃)

拥塞控制分为4个阶段:

慢启动(Slow Start)
  • 连接刚建立时,发送方并不知道网络的容量,因此保守地从一个很小的拥塞窗口(cwnd)开始。
  • 初始 cwnd 通常为 10 个 MSS(约 14600 字节,Linux 3.0 以后)。
  • 每收到一个 ACK,cwnd 增加 1 个 MSS(实际上是指数增长:每个 RTT 内 cwnd 翻倍)。
  • 当 cwnd ≥ ssthresh(慢启动阈值)时,转入拥塞避免阶段。

抓包特征:慢启动阶段,发送方发出的数据包序列呈“爆炸式”增长,Wireshark 可以看到连续的[ACK]后跟着越来越多的数据段。

拥塞避免(Congestion Avoidance)
  • 进入拥塞避免后,cwnd 的增长变为线性:每经过一个 RTT,cwnd 增加 1 个 MSS(实际实现中,每收到一个 ACK,cwnd += MSS / cwnd)。
  • 这个阶段持续到发生丢包(超时或收到三个重复 ACK)。
快速重传(Fast Retransmit)
  • 当发送方收到三个重复的 ACK(即同一个 seq 被确认了四次),说明很可能发生了丢包,不必等待超时,立即重传丢失的报文段。
  • 同时,发送方认为网络可能轻度拥塞,进入快速恢复阶段。
快速恢复(Fast Recovery)
  • 收到三个重复 ACK 时:
    • ssthresh = cwnd / 2(但不能小于 2 个 MSS)。
    • cwnd = ssthresh + 3(加 3 是因为已经收到的三个重复 ACK 表明有三个报文段离开了网络)。
  • 随后每收到一个重复 ACK,cwnd 增加 1 个 MSS(允许发送新的数据)。
  • 当收到新的 ACK(确认了新数据)时,cwnd 设置为 ssthresh,退出快速恢复,进入拥塞避免。

注意:如果发生超时(RTO),则 ssthresh = cwnd / 2,cwnd = 1,重新进入慢启动。超时意味着网络可能严重拥塞。

4 UDP与TCP的选择

4.1 UDP与TCP的区别

  • 是否面向连接
    • TCP是面向连接的,通讯前需要三次握手,断开时需要四次挥手。是端对端的连接,支持全双工通讯
    • UDP是面向无连接的,不需要三次握手和四次挥手,支持一对一、一对多、多对一和多对对的通讯
  • 数据传输方式
    • TCP面向字节流。不保留应用层的消息边界,数据像水流一样连续传输,接收方需要自行解析数据块。(存在粘包问题)
    • UDP面向数据报。每个udp数据包都是独立的,有明确的边界,接收方收到的就是发送方发出的完整数据单元
  • 是否可靠
    • TCP是可靠的连接。提供确认机制(ACK)、重传机制、序列号、丢包重发、乱序重组、流量控制和拥塞控制,确保数据完整、有序、不丢失地到达。
    • UDP是不可靠传输。不保证数据送达,也不重传、不排序。数据包可能丢失、重复或乱序,由上层应用自行处理。
  • 传输效率
    • TCP由于有连接建立、确认、重传、流量控制等机制,开销大,延迟较高但是稳定
    • UDP头部小(8个字节),无连接开销,传输速度快、延迟低,适合对实时性要求高的场景

4.2 应用场景

场景推荐协议原因

网页浏览(HTTP/HTTPS)、文件传输(FTP)、电子邮件(SMTP)

TCP

要求数据完整、准确,不能丢包

视频直播、在线游戏、语音通话(VoIP)、DNS查询

UDP

追求低延迟,少量丢包可接受,实时性优先

广播/组播(如网络发现、IPTV)

UDP

TCP不支持广播和组播