TCP
概述
TCP 是一个传输层流协议(因此业务侧要处理"粘包"的问题)
请求头

控制位

TCP 选项
socket 选项总结(setsockopt) - tiny~~ - 博客园

其中 Kind 的枚举值

SACK
选择重传,在 TCP 头的选项中指定多个边界
TSOPT 时间戳选项
1)计算往返时延 RTT:
当 a 主机接收到 b 主机发送过来的确认 ack 报文 s2 时,a 主机此时内核时刻为 ta2.
a 主机从 s2 报文的 timestamp echo 选项中可以解析出该确认 ack 确认的报文的发送时刻为 ta1.
那么:RTT= 接收 ack 报文的时刻-发送报文的时刻 =ta2 -ta1.
ta2 和 ta1 都来自 a 主机的内核,所以不需要在 tcp 连接的两端进行任何时钟同步的操作。
2)防止回绕的序号:
我们知道 TCP 报文的序列号只有 32 位,而没增加 2^32 个序列号后就会重复使用原来用过的序列号。假设我们有一条高速网络,通信的主机双方有足够大的带宽涌来快速的传输数据。例如 1Gb/s 的速率发送报文,则不到 35 秒报文的序号就会重复。这样对 TCP 传输带来混乱的情况。而采用时间戳选项,可以很容易的分辨出相同序列号的数据报,哪个是最近发送,哪个是以前发送的。
连接队列

服务器端会维护两种队列,处于 SYN_RCVD 状态的半连接队列,而处于 ESTABLISHED 状态但仍未被应用程序 accept 的为全连接队列
半连接队列
在三次握手协议中,服务器维护一个半连接队列,该队列为每个客户端的 SYN 包开设一个条目(服务端在接收到 SYN 包的时候,就已经创建了 request_sock 结构,存储在半连接队列中),该条目表明服务器已收到 SYN 包,并向客户发出确认,正在等待客户的确认包。这些条目所标识的连接在服务器处于 Syn_RECV 状态,当服务器收到客户的确认包时,删除该条目,服务器进入 ESTABLISHED 状态。
目前,Linux 下默认会进行 5 次重发 SYN-ACK 包,重试的间隔时间从 1s 开始,下次的重试间隔时间是前一次的双倍,5 次的重试时间间隔为 1s, 2s, 4s, 8s, 16s, 总共 31s, 称为
指数退避,第 5 次发出后还要等 32s 才知道第 5 次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s, TCP 才会把断开这个连接。由于,SYN 超时需要 63 秒,那么就给攻击者一个攻击服务器的机会,攻击者在短时间内发送大量的 SYN 包给 Server(俗称 SYN flood 攻击),用于耗尽 Server 的 SYN 队列。对于应对 SYN 过多的问题,linux 提供了几个 TCP 参数:tcp_syncookies、tcp_synack_retries、tcp_max_syn_backlog、tcp_abort_on_overflow 来调整应对。

全连接队列
当第三次握手时,当 server 接收到 ACK 包之后,会进入一个新的叫 accept 的队列。
当 accept 队列满了之后,即使 client 继续向 server 发送 ACK 的包,也会不被响应,此时 ListenOverflows+1,同时 server 通过 tcp_abort_on_overflow 来决定如何返回,0 表示直接丢弃该 ACK,1 表示发送 RST 通知 client;相应的,client 则会分别返回 read timeout 或者 connection reset by peer。另外,tcp_abort_on_overflow 是 0 的话,server 过一段时间再次发送 syn+ack 给 client(也就是重新走握手的第二步),如果 client 超时等待比较短,就很容易异常了。而客户端收到多个 SYN ACK 包,则会认为之前的 ACK 丢包了。于是促使客户端再次发送 ACK ,在 accept 队列有空闲的时候最终完成连接。若 accept 队列始终满员,则最终客户端收到 RST 包(此时服务端发送 syn+ack 的次数超出了 tcp_synack_retries)。
服务端仅仅只是创建一个定时器,以固定间隔重传 syn 和 ack 到服务端

状态机

三次握手

一些问题
- 为什么需要握手?
- 三次握手的一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number), 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
- 为什么需要三次握手?
另一些问题
- 有关 ISN 生成规则:
ISN = M + F(localhost, localport, remotehost, remoteport)
如果 ISN 是固定的,攻击者很容易猜出后续的确认号。因此通过一个公式去计算,M 是一个计时器,每隔 4 毫秒加 1。 F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 hash 算法不能被外部轻易推算得出。
- syn flood 攻击
四次挥手

一些问题
- 为什么需要挥手?
- 为什么需要四次挥手?
- 为什么服务端的 FIN 不能跟 ACK 一起发送?
- 为什么客户端需要有等待 2MSL 的 TIME-WAIT?如果不等待会怎么样?
滑动窗口
Linux 执行步骤
当外部连接请求到来时,TCP 模块会首先查看 max_syn_backlog,如果处于 SYN_RCVD 状态的连接数目超过这一阈值,进入的连接会被拒绝。根据 tcp_abort_on_overflow 字段来决定是直接丢弃,还是直接 reset.
从服务端来说,三次握手中,第一步 server 接受到 client 的 syn 后,把相关信息放到半连接队列中,同时回复 syn+ack 给 client. 第三步当收到客户端的 ack, 将连接加入到全连接队列。
一般,全连接队列比较小,会先满,此时半连接队列还没满。如果这时收到 syn 报文,则会进入半连接队列,没有问题。但是如果收到了三次握手中的第 3 步(ACK),则会根据 tcp_abort_on_overflow 字段来决定是直接丢弃,还是直接 reset.此时,客户端发送了 ACK, 那么客户端认为三次握手完成,它认为服务端已经准备好了接收数据的准备。但此时服务端可能因为全连接队列满了而无法将连接放入,会重新发送第 2 步的 syn+ack, 如果这时有数据到来,服务器 TCP 模块会将数据存入队列中。一段时间后,client 端没收到回复,超时,连接异常,client 会主动关闭连接。
Linux 参数配置
一些内核参数配置,使用 sysctl
# 缓冲区配置
net.core.rmem_max # 所有类型读的最大套接字缓冲大小
net.core.wmem_max # 所有类型写的最大套接字缓冲大小
net.ipv4.tcp_moderate_rcvbuf = 1 # TCP接收缓存是否自动调整
net.ipv4.tcp_rmem = 4096 65536 16777216 # TCP读缓冲配置,可使用最小,默认,最大字节数
# tcp_rmem的最大字节数一定小于rmem_max,因此需要同步调整
net.ipv4.tcp_wmem = 4096 65536 16777216 # TCP写缓冲配置,可使用最小,默认,最大字节数
# 连接队列配置
net.ipv4.tcp_max_syn_backlog = 1048576 # 半连接队列长度(socket数量)
net.core.somaxconn = 65535# 全连接队列长度,实际的全连接队列长度=min(backlog, somaxconn)
# 网卡配置
net.core.netdev_max_backlog = 1048576 # 每CPU 网络设备积压队列长度,为了10GbE 的NIC,这可能需要增加到10 000。
# TCP选项配置
net.ipv4.tcp_sack = 1
net.ipv4.tcp_fack = 1
net.ipv4.tcp_tw_reuse = 1 # TIME-WAIT socket是否复用,依赖于时间戳选项的开启
net.ipv4.tcp_tw_recycle = 0 # 已废弃
网卡的一些参数配置
ifconfig eth0 txqueuelen 10000 # 网卡每CPU发送队列长度
一些问题
有关粘包
原因:
- 服务端会使用 tcp nagle 算法,将多个包合并在一起发送
- 客户端会一次取到达的多个包
解决:
应用层分包,比如设置请求头,里面标识包的长度
Linux 问题排查
netstat -i
Iface MTU RX-OK RX-ERR RX-DRP RX-OVR TX-OK TX-ERR TX-DRP TX-OVR Flg
ens192 1500 9970944800 0 0 0 8747927905 0 0 0 BMRU
lo 65536 2322753743 0 0 0 2322753743 0 0 0 LRU
RX 标识接收队列,TX 标识发送队列,ERR 标识错误,DRP 标识丢弃,OVR 标识溢出(队列满?)
连接队列满
netstat -s | grep TCPBacklogDrop # 特定于 TCP 连接请求丢弃时间,也就是全连接队列溢出,数字一直变大就是有溢出
netstat -s | grep overflowed # 直接各种缓冲区队列是否有溢出,数字一直变大就是有溢出
netstat -s|egrep "SYNs to LISTEN" # 直接查看tcp半链接队列是否有溢出,数字一直变大就是有溢出
查看全连接队列情况
ss -lnt
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 65535 127.0.0.1:10248 0.0.0.0:*
LISTEN 0 65535 0.0.0.0:32041 0.0.0.0:*
LISTEN 0 65535 127.0.0.1:10249 0.0.0.0:*
LISTEN 0 65535 192.168.1.15:7946 0.0.0.0:*
LISTEN 0 65535 0.0.0.0:31243 0.0.0.0:*
LISTEN 0 65535 127.0.0.1:9099 0.0.0.0:*
Send-Q, 表示全连接队列最大为 65535, Recv-Q,标识全连接队列当前是 0
查看半连接队列长度
netstat -natp | grep "127.0.0.1:8888" |grep SYN_RECV | wc -
# 127.0.0.1:8888 的半连接长度
当全连接队列已满
- 当 tcp_abort_on_overflow=0,服务 accept 队列满了,客户端发来 ack,服务端直接丢弃该 ACK,此时服务端处于【syn_rcvd】的状态,客户端处于【established】的状态。在该状态下会有一个定时器重传服务端 SYN/ACK 给客户端(不超过 /proc/sys/net/ipv4/tcp_synack_retries 指定的次数,Linux 下默认 5)。超过后,服务器不再重传,后续也不会有任何动作。如果此时客户端发送数据过来,服务端会返回 RST。(这也就是我们的异常原因了)
- 当 tcp_abort_on_overflow=1,服务端 accept 队列满了,客户端发来 ack,服务端直接返回 RST 通知 client,表示废掉这个握手过程和这个连接,client 会报 connection reset by peer。
如果半连接队列满了
- 没有开启 tcp_syncookies(net.ipv4.tcp_syncookies = 1),则会丢弃;
- 开启,???
获取 tcp 详细连接信息
ss --options --extended --memory --processes --info
uid:2016 ino:79654650 sk:204 cgroup:/user.slice/user-2016.slice/session-24913.scope <->
skmem:(r0,rb6944643,t0,tb2626560,f0,w0,o0,bl0,d0) cubic wscale:9,9 rto:204 rtt:0.026/0.007 ato:40 mss:65495 pmtu:65535 rcvmss:65495 advmss:65495 cwnd:10 bytes_sent:2044962 bytes_acked:2044963 bytes_received:90608190 segs_out:62394 segs_in:62355 data_segs_out:27046 data_segs_in:37627 send 202Gbps lastsnd:1948 lastrcv:2376 lastack:1948 pacing_rate 392Gbps delivery_rate 131Gbps delivered:27047 app_limited busy:49700ms reordering:5 rcv_rtt:9.318 rcv_space:154180 rcv_ssthresh:3471469 minrtt:0.014
tcp ESTAB 0 0
127.0.0.1:33076 127.0.0.1:4222
users:(("flowgame-open-a",pid=194253,fd=9)) timer:(keepalive,4.388ms,0) uid:2016 ino:19790933 sk:205 cgroup:/user.slice/user-2016.slice/session-2810.scope <->
skmem:(r0,rb2227527,t0,tb2626560,f0,w0,o0,bl0,d55525) cubic wscale:9,9 rto:204 rtt:1.454/2.082 ato:40 mss:22016 pmtu:65535 rcvmss:536 advmss:65495 cwnd:10 bytes_sent:49355 bytes_acked:49356 bytes_received:49604 segs_out:127450 segs_in:120312 data_segs_out:8199 data_segs_in:8199 send 1.21Gbps lastsnd:25732 lastrcv:25732 lastack:10612 pacing_rate 2.42Gbps delivery_rate 19.6Gbps delivered:8200 app_limited busy:14448ms reordering:5 rcv_rtt:110224 rcv_space:44450 rcv_ssthresh:43690 minrtt:0.364
最大文件数满
参考资料
为什么 TCP 协议有 TIME_WAIT 状态 - 面向信仰编程