TCP、IP协议知识点梳理

前言

本文知识点梳理借鉴了很多小林coding的图文,有兴趣查看更加细致完整的资料可以直接跳转查看。

TCP协议基本概念

传输层最主要的协议之一。TCP报文的格式如下。协议格式对其中包含的内容大概有个印象即可。主要的内容其实很好记忆,比如因为是传输层协议,所以肯定有源端、目标端口号。创建可靠连接握手时需要的ack、seq以及一些状态控制位SYN、FIN。另外拥塞流量控制需要的窗口以及包含具体数据的消息体。

image.png

学习TCP协议的三次握手流程对于排查一些TCP链接的问题时是否有用。下面两幅TCP时序图和状态机值得记忆下:
image.png

image.png
可以通过几个问题来判断自己理解是否到位:

TCP协议的核心机制

TCP连接的创建和断开流程对于工程应用的问题排查比较有用,而进一步了解TCP的核心机制对于排查一些更加深层次的疑难杂症和理解网络包具体内容会非常有帮助。

重传机制

重传的时机:

  • 数据包丢失的时候
  • ACK丢失的时候

重传的间隔:

满足超时时间会进行重传。超时重传时间RTO(retransmission timeout)由RTT(round trip time)确定。超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。如果连续多次超时则2的指数回退,每次RTO加倍。

重传的机制

  • 快速重传Fast transmission与选择性确认SACK(selective acknowledgement):接收方对于未接收到的seq会连续发3次ack让发送方识别到然后直接进行重传,不用等RTO。快速重传会配合SACK工作从而确定重传时需要重传哪些报文(因为有几个报文已经提前发出去了,如果被接收方接收了就不需要重发了)

image.png

  • duplicate sack: sack是应对发送方发丢数据的情况,而d-sack是应对接收方ack丢失的情况。 接收方用于告诉发送方我收到了重复的seq,这样发送方就知道是接收方返回的ack丢了,不是自己发送的报文丢了,避免重复发送。

image.png

滑动窗口与流量控制

累积确认和应答

不用一个seq一个ack来交互,窗口为3则允许连续发3个seq直接返回3个ack,并且3个ack只要最后一个被接收到就认为前面的也全部接收到了,是一种累积确认、累积应答的方式。

窗口滑动

此外滑动窗口的一个重要作用就是流量控制,通过窗口协商避免发送方发送的过快导致接收方来不及处理。

滑动的理解:滑动是发送方和接收方通过交互协商不断调整窗口大小的过程。发送方收到ACK则调整窗口后移,可以发送更多的数据。下面图演示窗口size变化的过程
image.png
滑动窗口的大小由接收方指定。内核缓冲区的数据没有被应用取走则会导致窗口缩小甚至win=0。

抓包时的win时告诉对方自己的接收窗口。实际的真实窗口大小是:

1
「Window size value」 * 「Window size scaling factor」 = 「Caculated window size 」

image.png

窗口探测

发送方需要定时探测窗口,否则接收方从win=0恢复成win>0的ack丢失时会导致死锁(双发不进行任何收发)
image.png

糊涂窗口综合征与Nagle算法

窗口糊涂指的是如果win非常小,每次交互还要带上TCP+IP header的40个字节,很浪费。简而言之,就是不引入额外机制的情况下滑动窗口会发送不经济的小报文。Nagle算法开启就会等到满足条件发送发才发送数据,属于延迟处理。条件如下:

  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS;
  • 条件二:收到之前发送数据的 ack 回包;
1
2
3
4
5
6
7
8
9
10
11
if 有数据要发送 {
if 可用窗口大小 >= MSS and 可发送的数据 >= MSS {
立刻发送MSS大小的数据
} else {
if 有未确认的数据 {
将数据放入缓存等待接收ACK
} else {
立刻发送数据
}
}
}

Nagle算法需要保证接收方不通告小窗口(可以看TCP_CORK选项)才能真的生效,否则ACK回得快仍然是无效的。Nagle默认是打开的,关闭的话可以socket设置TCP_NODELAY
当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

拥塞控制

通过拥塞窗口控制发送量避免向已经负担过重的网络继续发送数据包。实际的发送窗口swnd=min(cwnd,rwind)。总体过程如下,关于拥塞算法可以看我另外一篇专门讨论拥塞算法的文章拥塞算法CUBIC和BBR

收到3个ACK执行快重传,说明遇到了网络拥塞,乘法减小然后快速恢复(TCP reno)。值得注意的是
image.png
Linux系统上查看TCP链接初始化win拥塞算法之类的内部信息可以用命令:

1
ss -nil

一些加强理解的问题

TCP协议建连为什么要3次握手?

握手阶段主要是为了初始化收发两端的seq。3次握手本质是说至少3次交互才能保证双方建立一个可靠的信道。因为信道本身是不可靠的会丢包。

  • 假设只握手1次:client发包给server,server如果不回ack,client以为没收到就一直发sync,没ack是显然不行的。
  • 假设握手2次:client发给server,然后server再发给client ack,这样client就知道server是否收到从而避免无限发SYNC,解决了1次握手的问题。但是这样仍然不够。RFC 793中提到三次握手主要是为了避免旧的SYNC请求晚到导致的链接重复创建。下图是小林coding中的图,演示了旧的SYNC先到,在2次握手时服务端直接会变成ESTABLISHED并且发送数据,直到client发现seq不匹配才能终止链接,这就造成了资源浪费

image.png

  • 假设握手3次:3次握手的关键是server有个中间状态SYNC_RECV,不会直接变成ESTABLISHED。在这个状态下旧的SYNC先到不会导致server直接变成ESTABLISHED,server会等到client发现seq不匹配发过来的RST及时关闭连接避免创建无效链接。这里关键是只有client收到sync后才知道seq是否匹配然后及时中断连接,因此server一定要通过3次握手等到client这个RST信号。

TCP协议为什么要四次挥手

image.png
TCP是全双工的,四次挥手断开连接的本质是client和server都需要告诉对方——我没有数据再要发送了。四次挥手可以当成两轮交互。第一轮交互client告诉server我要断开了,不要再给我发数据,即client发送FIN1然后server ack,这里server会通知应用程序不要给client再发数据了。第二轮交互server告诉client,我也要断开了,别再给我发数据,即server发送FIN给client,然后client ack。

四次挥手为什么要等待2MSL

理解2MSL( Maximum Segment Life 报文最大生存时间)的含义即可理解为何要等待2MSL。首先等待2MSL是为了解决最后第四次挥手的ACK没有被server收到的情况。一个报文在网络中存在的最大时间是一个MSL(Linux默认30秒)。假如现在 A 发送 ACK 后,最坏情况下,这个 ACK 在 1MSL 时到达 B;此时 B 在收到这个 ACK 的前一刹那,一直在重传 FIN,这个 FIN 最坏会在 1MSL 时间内消失。因此从 A 发送 ACK 的那一刹那开始,等待 2MSL 可以保证 A 发送的最后一个 ACK,和 B 发送的最后一个 FIN 都在网络中消失后续新建的连接不会收到这种老的包。另外值得注意的是处于TIME_WAIT状态的一端在收到重传的FIN时会重新计时(rfc793 以及 linux kernel源代码tcp_timewait_state_process函数)。

不过2MSL也不能绝对保证网络中不存在老的包。在前面的解释中我们思考一个场景,就是第一次FIN成功发送给A,然后A的ACK发出去了,这时候马上断网会发生什么?这时候会产生以下情况。

  • B满足RTO时间后不断重发FIN包(因为收不到ACK),一直处于LAST_ACK
  • A等待2MSL后进入CLOSED

这时候会产生一个问题,就是B断网后不断重发FIN,A接收到第一个有效FIN之后直接CLOSED了,这种情况下A复用端口的话可能会收到网络中旧的FIN包。

四次挥手每次失败时的行为

  • 第一次挥手失败:按照RTO时间2指数回退进行一定次数重试。重试次数由tcp_orphan_retries决定
  • 第二次挥手失败:ACK不会重传,所以是client重发FIN包打到最大重试次数后CLOSE。
  • 第三次挥手失败:内核自动回复ACK后,等待进程调用CLOSE函数使得server进入LAST_ACK。如果这个ACK发送失败,client端FIN_WAIT1等待超时后会直接close。超时时间由tcp_fin_timeout内核参数控制。
  • 第四次挥手失败:过程如下,服务端超时没收到第四次的ACK,则服务端会重发FIN包。重发次数由tcp_orphan_retries控制。注意收到服务端FIN包会重置2MSL定时器延长等待时间。

image.png

三次握手和四次挥手固定吗

不固定,主要考虑同时打开或者同时发起关闭的情况:
创建连接如果同时发SYNC过程就会变成4次握手
image.png

connect reset by pear和Broken pipe

例如收到一些不符合预期的信号,就会返回RST(比如SYNC_RECV阶段收到一个FIN包),应用层收到报错一般是connect reset by peer。broken pipe则是收到RST之后再接收数据写入就会有该报错。

什么是tcp fast open

就是在第一次建链之后,后续新创建的请求,可以直接根据客户端缓存的cookie直接通过1个RTT完成第一次请求的收发。Linux内核参数通过net.ipv4.tcp_fastopen打开。
image.png

区别nagle和tcp延迟确认

nagle算法主要针对发送方,不满足条件不发送,囤数据,避免发送小的包。TCP延迟确认主要指的是回ack的时候不直接回,而是等一会儿再回,两个能力不要弄混,控制的参数也不同。nagle关联的socket参数是TCP_NODELAY,而延迟确认关联的socket参数是TCP_QUICKACK

为什么全连接队列是链表,半连接队列是哈希表

  • 全连接队列都是established的链接,按序被accept用链表比较合理
  • 半连接队列里的都是SYNC_RECV的链接在等ack,用hash匹配过来的ack时间复杂度比较好

没有listen可以创建链接么?

可以的,参考如下情况,两个客户端同时sync sent。不过客户端此时没链接队列,但是会有个全局hash表存放相关信息。
image.png

关联的诊断问题

sync flood攻击

三次握手的ACK如果服务端一直收不到,服务端链接变成SYNC_RECV进入Linux内核的SYN队列(半连接队列)。
image.png
攻击者发送大量SYNC会导致服务器产生大量SYNC_RECV的链接。
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列;
  • 全连接队列,也称 accept 队列;

image.png
解决方式主要是:

  • 调大 netdev_max_backlog;
  • 增大 TCP 半连接队列;
  • 开启 tcp_syncookies;(不用担心accept队列满,攻击者只发sync,不进行第三次握手建立连接)。不过这个也不是万能的,如果攻击者发大量ack包和下边的cookies信息回导致服务端编码解码cookies CPU开销剧增影响服务器稳定性
  • 减少 SYN+ACK 重传次数

如何排查全连接、半连接队列溢出

全连接的通过netstat -s | grep overflowed命令以及查看LISTEN状态连接的RECV-Q情况即可知晓。半连接可以看SYNC_RECV的tcp连接数量即可。半连接队列最大值和tcp_max_syn_backlog和somaxconn都相关,具体和内核实现相关。

大量TIME_WAIT连接

产生TIME_WAIT的原因主要有:

  • HTTP使用短连接导致大量TIME_WAIT连接。现代WEB实现,HTTP短连接断开都是有服务端发起的
  • HTTP长连接超时设置不合理,超时后断开产生TIME_WAIT连接
  • HTTP长连接数量最大值设置不合理,打到最大值后关闭连接产生大量TIME_WAIT连接

解决方案主要有:

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项:开启了该功能,虽然不能马上减少TIME_WAIT的链接,但是重用TIME_WAIT的链接可以使得client能够进行接入(前提是client有做端口绑定的启动)。开启后在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。注意用户态SO_REUSEADDR和这个reuse内核参数无关,用户态SO_REUSEADDR是主动告诉内核TIME_WAIT的这个端口可以被重用。
  • 打开tcp_tw_recycle 和 tcp_timestamps(默认打开)两选项启用快速回收: 不要在NAT环境下使用,NAT下多机器系统时间戳误差会导致新链接直接被拒绝。内核4.12改参数已移除,不建议使用。
  • net.ipv4.tcp_max_tw_buckets: 超过阈值后直接RST重置链接,这个调节下可以直接清理掉超过这个阈值的TIME_WAIT连接。
  • 程序中使用SOCKER选项 SO_LINGER ,应用强制使用 RST 关闭(不推荐,过于暴力):如果l_onoff为非 0, 且l_linger值为 0,那么调用close后,会立该发送一个RST标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT状态,直接关闭。

大量CLOSE_WAIT连接

被动关闭方例如server,如果没法正确执行完close(),就没法从CLOSE_WAIT转变成LAST_ACK状态。这种基本上是由于socket实现的代码BUG导致。以下流程中实现有问题都有可能导致大量CLOSE_WAIT。问题排查案例可以看参考资料netty的那个问题排查。
一个普通的 TCP 服务端的流程:

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口
  2. 将服务端 socket 注册到 epoll: 遗漏socket注册到epll会导致后续无法正常close。
  3. **epoll_wait 等待连接到来,连接到来时,调用 accpet 获取已连接的 socket:**这步遗漏会导致client关闭连接时找不到要释放的对象。这个可以通过ss查看LISTEN状态的链接的RECV-Q值。确认是否有没有被用户态accept的链接。如果这个值小于SEND-Q,说明内核态网络包都被用户态取走了。
  4. **将已连接的 socket 注册到 epoll:**会导致服务端无法感知client FIN包
  5. epoll_wait 等待事件发生
  6. 对方连接关闭时,我方调用 close: 代码有BUG导致没执行close

注意:

  • 处于 LISTEN 状态的 socket,Recv-Q 表示当前 socket 的完成三次握手等待用户进程 accept 的连接个数,Send-Q 表示当前 socket 全连接队列能最大容纳的连接数
  • 对于非 LISTEN 状态的 socket,Recv-Q 表示 receive queue 的字节大小,Send-Q 表示 send queue 的字节大小

查看tcp全连接队列是否溢出除了看listen连接的RECV-Q也可以用以下命令,看到overflowed就说明全连接队列溢出。

1
2
3
netstat -s | egrep "listen|LISTEN"
243 times the listen queue of a socket overflowed
243 SYNs to LISTEN sockets dropped

总体上大量CLOSE_WAIT可以关注close是否被正常调用,代码BUG、java程序OOM、http client的不合理使用(client处理时间过长,server timeout提前关闭连接就会产生CLOSE_WAIT的连接)都是导致这类情况的根因之一。

tcpdump抓不到包

网络包进入主机后的顺序如下:

  • 进来的顺序 Wire -> NIC -> tcpdump -> netfilter/iptables
  • 出去的顺序 iptables -> tcpdump -> NIC -> Wire

如果iptables有OUTPUT的规则,被限制的包会抓不到。

nagle和delay ack一起开启导致的40ms延迟问题

10.48.159.165 运行的是 Delayed ACK,10.22.29.180 运行的是 Nagle 算法。10.22.29.180 在等 ACK,而 10.48.159.165 触发了 Delayed ACK,这样傻傻等了 40ms。
image.png
如果因为同时开启nagle和delay ack导致这种延迟问题,建议关闭一个nagle或者tcp delay。

优化tcp缓冲区大小

系统空闲内存大的话,可以适当增加下TCP内核缓冲区大小。注意如果设置socket参数SO_SNDBUF和SO_RCVBUF则TCP内核缓冲期的动态调整功能会被关闭。一般而言,应当保证缓冲区的动态调整的最大值达到带宽时延积,而最小值保持默认的 4K 不变即可。而对于内存紧张的服务而言,调低默认值是提高并发的有效手段。比如万兆带宽,RTT=10MS,动态范围最大值可以是1000MB/s(换成字节)*0.01=100MB,像Linux默认6MB的最大动态范围在万兆带宽、内存充足的情况下其实都是可以适当增加些的。

1
2
3
## 比如最大带宽是 100 MB/s,网络时延(RTT)是 10ms 时,意味着客户端到服务端的网络一共可以存放 100MB/s * 0.01s = 1MB 的字节。
带宽时延积(BDP,bandwidth delay product)=RTT*带宽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#查看接收缓冲区,三个参数分别是min initial-default max的值,注意不能设置超过内存大小
cat /proc/sys/net/ipv4/tcp_rmem
4096 131072 6291456

## 查看发送缓冲区,注意不能设置超过内存大小
cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304

## 发送缓冲区自动开启,接收一半也默认开启,可以看参数
cat /proc/sys/net/ipv4/tcp_moderate_rcvbuf

## 查看tcp内存分配的page数,实际内存占用注意乘以page size,一般4K。
## 当 TCP 内存小于第 1 个值时,不需要进行自动调节;
## 在第 1 和第 2 个值之间时,内核开始调节接收缓冲区的大小;
## 大于第 3 个值时,内核不再为 TCP 分配新内存,此时新连接是无法建立的;
## 创建tcp connection都会占用tcp mem,如果是网络I/O密集型应用可以根据自己内存适当调节大些
cat /proc/sys/net/ipv4/tcp_mem
9744 12992 19488

对内核参数调优有兴趣可以看下文章:Performance Tuning on Linux — TCP

IP协议

基础知识

记住一些关键基础知识即可:

  • 分类地址、无分类地址差异
  • 子网掩码与子网的划分
  • 单播、广播和组播(多播)的差异
  • IPv6和IPv4的差异
    • 采用128位
    • 取消了首部校验和字段,交给数据链路和传输层校验就够了
    • 取消了分片/重组相关字段:没有必有在网络寻址时组装分片,直接在源端或者目标端组装即可
    • 取消选项字段:选项字段不再是标准 IP 首部的一部分了,但它并没有消失,而是可能出现在 IPv6 首部中的「下一个首部」指出的位置上。删除该选项字段使的 IPv6 的首部成为固定长度的 40 字节

image.png

相关协议

  • DNS
  • ARP和RARP
  • DHCP
  • NAT
  • ICMP
  • IGMP

参考资料