3.2 transport 层
TCP和UDP
3.2.1 TCP Transmission Control Protocol
3.2.1.1 TCP特性
工作在传输层
面向连接协议
全双工协议
半关闭
将数据打包成段,排序
确认机制
数据恢复,重传
错误检查
流量控制,滑动窗口
拥塞控制,慢启动和拥塞避免算法
更多关于tcp的内核参数,可参看man 7 tcp
3.2.1.2 TCP包头结构
TCP包头
- 源端口、目标端口:计算机上的进程要和其他进程通信是要通过计算机端口的,而一个计算机端口某个时刻只能被一个进程占用,所以通过指定源端口和目标端口,就可以知道是哪两个进程需要通信。源端口、目标端口是用16位表示的,可推算计算机的端口个数为2^16个,即65536
- 序列号:表示本报文段所发送数据的第一个字节的编号。在TCP连接中所传送的字节流的每一个字节都会按顺序编号。由于序列号由32位表示,所以每2^32个字节,就会出现序列号回绕,再次从0 开始
- 确认号:表示接收方期望收到发送方下一个报文段的第一个字节数据的编号。也就是告诉发送方:我希望你(指发送方)下次发送的数据的第一个字节数据的编号为此确认号数据偏移:表示TCP报文段的首部长度,共4位,由于TCP首部包含一个长度可变的选项部分,需要指定这个TCP报文段到底有多长。它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。该字段的单位是32位(即4个字节为计算单位),4位二进制最大表示15,所以数据偏移也就是TCP首部最大60字节
- URG:表示本报文段中发送的数据是否包含紧急数据。后面的紧急指针字段(urgent pointer)只有当URG=1时才有效
- ACK:表示是否前面确认号字段是否有效。只有当ACK=1时,前面的确认号字段才有效。TCP规定,连接建立后,ACK必须为1,带ACK标志的TCP报文段称为确认报文段PSH:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间。如果为1,则表示对方应当立即把数据提交给上层应用,而不是缓存起来,如果应用程序不将接收到的数据读走,就会一直停留在TCP接收缓冲区中
- RST:如果收到一个RST=1的报文,说明与主机的连接出现了严重错误(如主机崩溃),必须释放连接,然后再重新建立连接。或者说明上次发送给主机的数据有问题,主机拒绝响应,带RST标志的TCP报文段称为复位报文段
- SYN:在建立连接时使用,用来同步序号。当SYN=1,ACK=0时,表示这是一个请求建立连接的报文段;当SYN=1,ACK=1时,表示对方同意建立连接。SYN=1,说明这是一个请求建立连接或同意建立连接的报文。只有在前两次握手中SYN才置为1,带SYN标志的TCP报文段称为同步报文段
- FIN:表示通知对方本端要关闭连接了,标记数据是否发送完毕。如果FIN=1,即告诉对方:“我的数据已经发送完毕,你可以释放连接了”,带FIN标志的TCP报文段称为结束报文段窗口大小:表示现在允许对方发送的数据量,也就是告诉对方,从本报文段的确认号开始允许对方发送的数据量,达到此值,需要ACK确认后才能再继续传送后面数据,由Window size value * Window size scaling factor(此值在三次握手阶段TCP选项Window scale协商得到)得出此值
- 校验和:提供额外的可靠性
- 紧急指针:标记紧急数据在数据字段中的位置
- 选项部分:其最大长度可根据TCP首部长度进行推算。TCP首部长度用4位表示,选项部分最长为:(2^4-1)*4-20=40字节
TCP包头常见选项:
-
最大报文段长度MSS(Maximum Segment Size),通常1460字节指明自己期望对方发送TCP报文段时那个数据字段的长度。比如:1460字节。数据字段的长度加上TCP首部的长度才等于整个TCP报文段的长度。MSS不宜设的太大也不宜设的太小。若选择太小,极端情况下,TCP报文段只含有1字节数据,在IP层传输的数据报的开销至少有40字节(包括TCP报文段的首部和IP数据报的首部)。这样,网络的利用率就不会超过1/41。若TCP报文段非常长,那么在IP层传输时就有可能要分解成多个短数据报片。在终点要把收到的各个短数据报片装配成原来的TCP报文段。当传输出错时还要进行重传,这些也都会使开销增大。因此MSS应尽可能大,只要在IP层传输时不需要再分片就行。在连接建立过程中,双方都把自己能够支持的MSS写入这一字段。 MSS只出现在SYN报文中。即:MSS出现在SYN=1的报文段中
MTU和MSS值的关系:MTU=MSS+IP Header+TCP Header
通信双方最终的MSS值=较小MTU-IP Header-TCP Header
-
窗口扩大 Window Scale
为了扩大窗口,由于TCP首部的窗口大小字段长度是16位,所以其表示的最大数是65535。但是随着时延和带宽比较大的通信产生(如卫星通信),需要更大的窗口来满足性能和吞吐率,所以产生了这个窗口扩大选项 -
时间戳 Timestamps
可以用来计算RTT(往返时间),发送方发送TCP报文时,把当前的时间值放入时间戳字段,接收方收到后发送确认报文时,把这个时间戳字段的值复制到确认报文中,当发送方收到确认报文后即可计算出RTT。也可以用来防止回绕序号PAWS,也可以说可以用来区分相同序列号的不同报文。因为序列号用32为表示,每2^32个序列号就会产生回绕,那么使用时间戳字段就很容易区分相同序列号的不同报文
范例:
五要素
协议
IP 源 目的
端口 源 目的
TCP 10.0.0.100:12345--->1.1.1.1:80
[root@rocky8 ~]# cat /etc/services
3.2.1.3 TCP协议PORT
传输层通过port号,确定应用层协议,范围0-65535
*:https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
IANA互联网数字分配机构负责域名,数字资源,协议分配
- 0-1023:系统端口或特权端口(仅管理员可用) ,众所周知,永久的分配给固定的系统应用使用,22/tcp(ssh), 80/tcp(http), 443/tcp(https)
- 1024-49151:用户端口或注册端口,但要求并不严格,分配给程序注册为某应用使用,1433/tcp(SqlServer), 1521/tcp(oracle),3306/tcp(mysql),11211/tcp/udp (memcached)
- 49152-65535:动态或私有端口,客户端随机使用端口,范围定义:/proc/sys/net/ipv4/ip_local_port_range
范例:调整客户端的动态端口范围
[root@rocky8 ~]# cat /proc/sys/net/ipv4/ip_local_port_range
32768 60999
[root@rocky8 ~]# echo 20000 62000 > /proc/sys/net/ipv4/ip_local_port_range
[root@rocky8 ~]# cat /proc/sys/net/ipv4/ip_local_port_range
20000 62000
范例:
[root@rocky8 ~]# dnf -y install man-pages
[root@rocky8 ~]# man 2 socket
[root@rocky8 ~]# dnf -y install nc
[root@rocky8 ~]# nc -l 9527
[root@centos7 ~]# nc 172.31.1.8 9527
-bash: nc: command not found
[root@centos7 ~]# yum -y install nc
[root@centos7 ~]# nc 172.31.1.8 9527
[root@rocky8 ~]# ss -nt
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 52 172.31.1.8:22 172.31.0.1:63293
ESTAB 0 0 172.31.1.8:22 172.31.0.1:63772
ESTAB 0 0 172.31.1.8:9527 172.31.0.7:47226
[root@rocky8 ~]# nc -l 9527
[root@centos7 ~]# nc 172.31.1.8 9527
[root@centos7 ~]# nc 172.31.1.8 9527
i am centos8
[root@centos7 ~]# nc 172.31.1.8 9527
i am centos8
[root@centos7 ~]# nc 172.31.1.8 9527
i am centos8
i am centos7
[root@rocky8 ~]# nc -l 9527
i am centos8
i am centos7
[root@rocky8 ~]# lsof -i :9527
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nc 1833 root 3u IPv4 31658 0t0 TCP *:9527 (LISTEN)
nc 1833 root 4u IPv4 31659 0t0 TCP rocky8:9527->172.31.0.7:47230 (ESTABLISHED)
#9527 端口使用
#一旦建立连接,就释放了端口,不在占用端口,别人就可以继续访问
范例:找到端口冲突的应用程序
[root@rocky8 ~]# nc -l 22
nc: Address already in use #提示22端口被占用
[root@rocky8 ~]# nc -l 2222
[root@rocky8 ~]# ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 1 0.0.0.0:2222 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 [::]:22 [::]:*
[root@rocky8 ~]# lsof -i :22
-bash: lsof: command not found
[root@rocky8 ~]# dnf -y install lsof
[root@rocky8 ~]# lsof -i :22 #查看端口被谁占用
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sshd 774 root 4u IPv4 26024 0t0 TCP *:ssh (LISTEN)
sshd 774 root 6u IPv6 26035 0t0 TCP *:ssh (LISTEN)
sshd 1266 root 5u IPv4 27742 0t0 TCP rocky8:ssh->172.31.0.1:63293 (ESTABLISHED)
sshd 1278 root 5u IPv4 27742 0t0 TCP rocky8:ssh->172.31.0.1:63293 (ESTABLISHED)
sshd 1505 root 5u IPv4 29340 0t0 TCP rocky8:ssh->172.31.0.1:63772 (ESTABLISHED)
sshd 1507 root 5u IPv4 29340 0t0 TCP rocky8:ssh->172.31.0.1:63772 (ESTABLISHED)
[root@rocky8 ~]# ss -ntlp #-p 选项可以查看每个端口被哪个应用程序使用
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 1 0.0.0.0:2222 0.0.0.0:* users:(("nc",pid=1504,fd=3))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=774,fd=4))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=774,fd=6))
[root@rocky8 ~]# ss -ntlp |grep 22
LISTEN 0 1 0.0.0.0:2222 0.0.0.0:* users:(("nc",pid=1504,fd=3))
LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=774,fd=4))
LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=774,fd=6))
范例:判断端口是否正在打开
[root@rocky8 ~]# < /dev/tcp/127.0.0.1/22
[root@rocky8 ~]# echo $?
0
[root@rocky8 ~]# < /dev/tcp/127.0.0.1/80
-bash: connect: Connection refused
-bash: /dev/tcp/127.0.0.1/80: Connection refused
[root@rocky8 ~]# echo $?
1
TCP端口号通信过程
TCP序列和确认号
TCP确认和固定窗口
TCP滑动窗口
#ACK 3 表示期望你下次发3,变相告诉你3没收到,让你重发
TCP 滑动窗⼝是什么?
TCP 是每发送⼀个数据,都要进⾏⼀次确认应答。只有上一个收到了回应才发送下一个,这样效率会非常低,因此引进了滑动窗口的概念.
其实就是在发送方设立一个缓存区间,将已发送但未收到确认的消息缓存起来,假如一个窗口可以发送 5 个 TCP 段,那么发送方就可以连续发送 5 个 TCP 段,然后就会将这 5 个 TCP 段的数据缓存起来,这 5 个 TCP 段是有序的,只要后面的消息收到了 ACK ,那么不管前面的是否有收到 ACK,都代表成功,窗⼝⼤⼩是由接收方决定的。
窗⼝⼤⼩就是指不需要等待应答,还可以发送数据的大小。
3.2.1.4 三次握手和四次挥手
建立连接
TCP三次握手
TCP 建立连接的过程是怎样的?
- 第一次握手:A 的 TCP 进程创建一个 传输控制块 TCB ,然后向 B 发出连接请求报文段。之后将同步位 SYN 设置为 1,同时选择一个初始序列号 seq=x,这时客户端 A 进入到 SYN-SENT(同步已发送)状态。
- 第二次握手:B 收到连接请求报文段,如果同意建立连接,则向 A 发送确认。在确认报文段中 同步位 SYN=1、确认位 ACK=1、确认号 ack=x+1,同时也为自己选择一个初始序列号 seq=y,这时服务器 B 进入 SYN-RCVID 状态。
- 第三次握手:A 收到 B 的确认以后,再向 B 发出确认。确认报文 ACK=1、确认号ack=y+1。这时A进入到 ESTAB-LISHED 状态。当B接收到A的确认后,也进入 ESTAB-LISHED 状态。连接建立完成
为什么是三次握手???
-
1.为了防止已经失效的连接请求报文段突然又传到服务端,因而产生错误
-
- 如果客户端连续发送多次 SYN 建⽴连接的报⽂,如果出现了网络拥堵,可能会有旧连接先于新连接到达的情况,就可能会出现连接覆盖,要避免这种情况,最少需要三次握手
-
2.三次握⼿正好避免资源浪费
-
- 三次握⼿就已经是理论上建立可靠连接的最小次数了,所以不需要更多的连接
-
3.同步双⽅初始序列号
-
- 同步序列号(可以鉴别重复数序,按序接受等)其实并不要三次握手,只要一来一回两次就可以了
范例: 三次握手
#wireshark抓包分析
(ip.src == 10.0.0.101 and ip.dst == 10.0.0.102) || (ip.dst == 10.0.0.101 and ip.src == 10.0.0.102)
TCP四次挥手
MSL :Maximum Segment Lifetime
TCP 断开连接的过程是怎样的?
-
第一次挥手:A 先发送连接释放报文段,段首部的终止控制位 FIN=1,序号seq=u(等于A前面发送数据的最后一个序号加1);然后 A 进入 FIN-WAIT-1(终止等待1)状态,等待 B 的确认。
-
第二次挥手:B 收到 A 的连接释放报文段后,立刻发出确认报文段,确认号 ack=u+1,序号 seq=v(等于 B 前面发送数据的最后一个序号加1);然后 B 进入 CLOSE-WAIT(关闭等待)状态。
-
第三次挥手:A 收到 B 的确认报文段后进入到 FIN-WAIT-2(终止等待2)状态,继续等待 B 发出连接释放报文段;
-
- 若 B 已经没有数据要发送,B 就会向 A 发送连接释放报文段,段首部的终止控制位 FIN=1,序号 seq=w(半关闭状态可能又发送了一些数据),确认号 ack=u+1,这时B进入 LAST-ACK(最后确认)状态,等待A的确认。
-
第四次挥手:A收到B的连接释放报文段并发出确认,确认段中 确认位 ACK=1,确认号 ack=w+1,序号 seq=u+1;然后 A 进入到TIME-WAIT(时间等待)状态。当 B 再接收到该确认段后,B 就进入 CLOSED 状态。
第四次挥手为什么要等待2MSL(60s)
首先 2MSL 的时间是从客户端(A)接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端(A)的 ACK 没有传输到服务端(B),客户端(A)又接收到了服务端(B)重发的 FIN 报文,那么 2MSL 时间会被重置。等待 2MSL 原因如下
-
1.得原来连接的数据包消失
-
- 1)如果B没有收到自己的ACK,会超时重传FiN那么A再次接到重传的FIN,会再次发送ACK
- 2)如果B收到自己的ACK,也不会再发任何消息,
- 在最后一次挥手后 A 并不知道 B 是否接到自己的 信息
包括 ACK 是以上哪两种情况,A 都需要等待,要取这两种情况等待时间的最大值,以应对最坏的情况发生,这个最坏情况是:去向ACK消息最大存活时间(MSL) + 来向FIN消息的最大存活时间(MSL)。这刚好是2MSL,这个时间,足以使得原来连接的数据包在网络中消失。
-
2.保证 ACK 能被服务端接收到从而正确关闭链接
-
- 因为这个 ACK 是有可能丢失的,会导致服务器收不到对 FIN-ACK 确认报文。假设客户端不等待 2MSL ,而是在发送完 ACK 之后直接释放关闭,一但这个 ACK 丢失的话,服务器就无法正常的进入关闭连接状态。
为什么是四次挥手?
因为 tcp 可以在发送数据的同时也能接受数据,要实现可靠的连接关闭,A 发出结束报文 FIN,收到 B 确认后 A 知道自己没有数据需要发送了,B 知道 A 不再发送数据了,自己也不会接收数据了,但是此时 A 还是可以接收数据,B 也可以发送数据;当 B 发出 FIN 报文的时候此时两边才会真正的断开连接,读写分开。
范例: wireshark 查看四次挥手
范例:
[root@rocky8 ~]# ss -ant
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 1 0.0.0.0:9527 0.0.0.0:*
ESTAB 0 0 172.31.1.8:22 172.31.0.1:63293
ESTAB 0 0 172.31.1.8:9527 172.31.0.7:47230
ESTAB 0 52 172.31.1.8:22 172.31.0.1:63772
LISTEN 0 128 [::]:22 [::]:*
[root@centos7 ~]# ss -nat
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:*
LISTEN 0 100 127.0.0.1:25 *:*
FIN-WAIT-1 0 1 172.31.0.7:47230 172.31.1.8:9527
ESTAB 0 0 172.31.0.7:22 172.31.0.1:63812
ESTAB 0 0 172.31.0.7:22 172.31.0.1:63813
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 100 [::1]:25 [::]:*
[root@rocky8 ~]# iptables -A INPUT -s 172.31.0.7 -j DROP
[root@rocky8 ~]# iptables -vnL
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DROP all -- * * 172.31.0.7 0.0.0.0/0
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
[root@centos7 ~]# ssh 172.31.1.8
[root@centos7 ~]# ss -nta
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:*
LISTEN 0 100 127.0.0.1:25 *:*
SYN-SENT 0 1 172.31.0.7:56896 172.31.1.8:22 #这里有 SYN-SENT 状态
ESTAB 0 0 172.31.0.7:22 172.31.0.1:50521
FIN-WAIT-1 0 1 172.31.0.7:47230 172.31.1.8:9527
ESTAB 0 52 172.31.0.7:22 172.31.0.1:50520
ESTAB 0 0 172.31.0.7:22 172.31.0.1:63812
ESTAB 0 0 172.31.0.7:22 172.31.0.1:63813
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 100 [::1]:25
[root@rocky8 ~]# iptables -F
[root@centos7 ~]# ssh 172.31.1.8
root@172.31.1.8's password:
[root@centos7 ~]# ss -nta
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:*
LISTEN 0 100 127.0.0.1:25 *:*
ESTAB 0 0 172.31.0.7:22 172.31.0.1:50521
ESTAB 0 0 172.31.0.7:56900 172.31.1.8:22 #7和8已经 ESTAB 状态
ESTAB 0 52 172.31.0.7:22 172.31.0.1:50520
ESTAB 0 0 172.31.0.7:22 172.31.0.1:63812
ESTAB 0 0 172.31.0.7:22 172.31.0.1:63813
TIME-WAIT 0 0 172.31.0.7:56898 172.31.1.8:22
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 100 [::1]:25 [::]:*
[root@rocky8 ~]# ss -ant
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 1 0.0.0.0:9527 0.0.0.0:*
ESTAB 0 0 172.31.1.8:22 172.31.0.7:56900
ESTAB 0 0 172.31.1.8:22 172.31.0.1:63293
ESTAB 0 0 172.31.1.8:9527 172.31.0.7:47230
ESTAB 0 52 172.31.1.8:22 172.31.0.1:63772
LISTEN 0 128 [::]:22 [::]:*
[root@centos7 ~]# iptables -A INPUT -s 172.31.1.8 -j DROP
[root@centos7 ~]# iptables -vnL
Chain INPUT (policy ACCEPT 65 packets, 4848 bytes)
pkts bytes target prot opt in out source destination
0 0 DROP all -- * * 172.31.1.8 0.0.0.0/0
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
Chain OUTPUT (policy ACCEPT 43 packets, 4468 bytes)
pkts bytes target prot opt in out source destination
[root@centos7 ~]# ssh 172.31.1.8
[root@rocky8 ~]# ss -ant
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 1 0.0.0.0:9527 0.0.0.0:*
SYN-RECV 0 0 172.31.1.8:22 172.31.0.7:56902 #现在就是SYN-RECV状态
ESTAB 0 0 172.31.1.8:22 172.31.0.1:63293
ESTAB 0 0 172.31.1.8:9527 172.31.0.7:47230
ESTAB 0 52 172.31.1.8:22 172.31.0.1:63772
LISTEN 0 128 [::]:22 [::]:*
#两次握手只实现了单次确认,只有第三次才能双向确认
[root@centos7 ~]# iptables -F
范例: 查看 MSL
[root@rocky8 ~]# sysctl net.ipv4.tcp_fin_timeout
net.ipv4.tcp_fin_timeout = 60
[root@rocky8 ~]# cat /proc/sys/net/ipv4/tcp_fin_timeout
60
3.2.1.5 有限状态机 FSM:Finite State Machine
虚线:服务器 实线:客户端
- CLOSED 没有任何连接状态
- LISTEN 侦听状态,等待来自远方TCP端口的连接请求
- SYN-SENT 在发送连接请求后,等待对方确认
- SYN-RECEIVED 在收到和发送一个连接请求后,等待对方确认
- ESTABLISHED 代表传输连接建立,双方进入数据传送状态
- FIN-WAIT-1 主动关闭,主机已发送关闭连接请求,等待对方确认
- FIN-WAIT-2 主动关闭,主机已收到对方关闭传输连接确认,等待对方发送关闭传输连接请求
- TIME-WAIT 完成双向传输连接关闭,等待所有分组消失
- CLOSE-WAIT 被动关闭,收到对方发来的关闭连接请求,并已确认
- LAST-ACK 被动关闭,等待最后一个关闭传输连接确认,并等待所有分组消失
- CLOSING 双方同时尝试关闭传输连接,等待对方确认
客户端先发送一个FIN给服务端,自己进入FIN_WAIT_1状态,这时等待接收服务端报文,该报文会有三种可能:
- 只有服务端的ACK
- 只有服务端的FIN
- 基于服务端的ACK,又有FIN
1、只收到服务器的ACK,客户端会进入FIN_WAIT_2状态,后续当收到服务端的FIN时,回应发送一个ACK,会进入到TIME_WAIT状态,这个状态会持续2MSL(TCP报文段在网络中的最大生存时间, RFC1122标准的建议值是2min).客户端等待2MSL,是为了当最后一个ACK丢失时,可以再发送一次。因为服务端在等待超时后会再发送一个FIN给客户端,进而客户端知道ACK已丢失
2、只有服务端的FIN时,回应一个ACK给服务端,进入CLOSING状态,然后接收到服务端的ACK时,进入TIME_WAIT状态
3、同时收到服务端的ACK和FIN,直接进入TIME_WAIT状态
客户机端的三次握手和四次挥手状态转换
服务器端的三次握手和四次挥手状态转换
3.2.1.6 sync半连接和accept全连接队列
/proc/sys/net/ipv4/tcp_max_syn_backlog 未完成连接队列大小,默认值128,建议调整大小为1024以上
/proc/sys/net/core/somaxconn 完成连接队列大小,默认值128,建议调整大小为1024以上
范例:修改半连接队列
[root@rocky8 ~]# cat /proc/sys/net/ipv4/tcp_max_syn_backlog
128
[root@rocky8 ~]# echo 1024 > /proc/sys/net/ipv4/tcp_max_syn_backlog
[root@rocky8 ~]# cat /proc/sys/net/ipv4/tcp_max_syn_backlog
1024
范例:修改全连接队列
[root@rocky8 ~]# cat /proc/sys/net/core/somaxconn
128
[root@rocky8 ~]# echo 1024 > /proc/sys/net/core/somaxconn
[root@rocky8 ~]# cat /proc/sys/net/core/somaxconn
1024
TCP 半连接队列和全连接队列是什么?
服务端收到客户端发出的 SYN 请求后,会把这个连接信息存储到半链接队列(SYN 队列)。
服务端收到第三次握⼿的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到全连接队列(accept 队列),等待进程调⽤ accept 函数时把连接取出来。
这两个队列都是有大小限制的,当超过容量后就会将链接丢弃,或者返回 RST 包。
3.2.1.7 TCP超时重传
异常网络状况下(开始出现超时或丢包),TCP控制数据传输以保证其承诺的可靠服务
TCP服务必须能够重传超时时间内未收到确认的TCP报文段。为此,TCP模块为每个TCP报文段都维护一个重传定时器,该定时器在TCP报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就是TCP的重传策略
与TCP超时重传相关的两个内核参数:
- /proc/sys/net/ipv4/tcp_retries1,指定在底层IP接管之前TCP最少执行的重传次数,默认值是3
- /proc/sys/net/ipv4/tcp_retries2,指定连接放弃前TCP最多可以执行的重传次数,默认值15(一般对应13~30min)
范例:
#最小超时重传次数
[root@rocky8 ~]# cat /proc/sys/net/ipv4/tcp_retries1
3
#最大超时重传次数
[root@rocky8 ~]# cat /proc/sys/net/ipv4/tcp_retries2
15
粘包/拆包是怎么发生的?怎么解决这个问题?
TCP 发送数据时会根据 TCP 缓冲区的实际情况进行包的划分,一个完整的包可能会被 TCP 拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是 TCP 粘包和拆包问题。
发生 TCP 粘包的原因:
- 1.发送的数据小于 TCP 缓冲区大小,TCP将缓冲区中的数据(数据属于多条业务内容)一次发送出去可能就会发生粘包。
- 2.接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
发生 TCP 拆包的原因:
- 1.待发送数据大于最大报文长度,TCP 在传输前将进行拆包。
- 2.发送的数据大于 TCP 发送缓冲区剩余空间大小,将会发生拆包。
解决方案:
- 1.发送端给每个数据包添加包首部,首部中包含数据包的长度,这样接收端在接收到数据后,通过该字段就可以知道每个数据包的实际长度了。
- 2.发送端将每个数据包设置固定长度,这样接收端每次从读取固定长度的数据把每个数据包拆分开。
- 3.可以在数据包之间设置边界,如添加特殊符号,接收端可以通过这个特殊符号来拆分包。
3.2.1.8 拥塞控制
网络中的带宽、交换结点中的缓存和处理机等,都是网络的资源。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可承受的能力,网络的性能就会变坏。此情况称为拥塞
TCP为提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。即所谓的拥塞控制
TCP拥塞控制的标准文档是RFC 5681,其中详细介绍了拥塞控制的四个部分:慢启动(slow start)、拥塞避免(congestion avoidance)、快速重传(fast retransmit)和快速恢复(fast recovery)。拥塞控制算法在Linux下有多种实现,比如reno算法、vegas算法和cubic算法等。它们或者部分或者全部实现了上述四个部分
当前所使用的拥塞控制算法
/proc/sys/net/ipv4/tcp_congestion_control
范例:
#拥塞控制算法
[root@rocky8 ~]# cat /proc/sys/net/ipv4/tcp_congestion_control
cubic
发送方一直发送数据,但是接收方处理不过来怎么办?(流量控制)
如果接收方处理不过来,发送方就会触发重试机制再次发送数据,然而这个是有性能损耗的,为了解决这个问题,TCP 就提出了流量控制,为的就是让发送方知道接受方的处理能力。
也就是说,每次接收方接受到数据后会将剩余可处理数据的大小告诉发送方。
比如接受方滑动窗口可用大小为400字节,发送方发送过来100字节的数据,那么接收方剩余可用滑动窗口大小就为300字节,这是发送方就知道下次返送数据的大小范围了。
但是这里有一个问题,数据会存放在缓冲区,但是这个缓冲区是操作系统控制的,当系统繁忙的时候,会缩减缓冲区减小,可能就会造成丢包的问题。
如: 发送方接收方窗口大小各为200字节,发送方发送100字节的给接收方,此时双方各剩100字节,但是此时操作系统非常忙,将接收方的缓存区减少了50字节,这时接收方就会告诉发送方,我还有50字节可用,但是在接收方发送到达之前,发送方是不知道的,只会看到自己还有100字节可用,那么就继续发送数据,如果发送了80字节数据,那么接收方缓存区大小为50字节,就会丢失30字节的数据,也就是会发生丢包现象。
我们会发现,这个问题发生的原因就是减少了缓存,又收缩了窗口大小,所以 TCP 是不允许同时减少缓存⼜收缩窗⼝的。
3.2.1.9 内核TCP参数优化
参看帮助: man tcp
编辑文件/etc/sysctl.conf,加入以下内容:然后执行 sysctl -p 让参数生效。
net.ipv4.tcp_fin_timeout = 2
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_keepalive_time = 600
net.ipv4.ip_local_port_range = 2000 65000
net.ipv4.tcp_max_syn_backlog = 16384
net.ipv4.tcp_max_tw_buckets = 36000
net.ipv4.route.gc_timeout = 100
net.ipv4.tcp_syn_retries = 1
net.ipv4.tcp_synack_retries = 1
net.ipv4.tcp_max_orphans = 16384
net.core.somaxconn = 16384
net.core.netdev_max_backlog = 16384
作用说明:
- net.ipv4.tcp_fin_timeout 表示套接字由本端要求关闭,这个参数决定了它保持在FIN-WAIT-2状态的时间,默认值是60秒。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_fin_timeout 60
- net.ipv4.tcp_tw_reuse 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认值为0,表示关闭。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_tw_reuse 0
- net.ipv4.tcp_tw_recycle 表示开启TCP连接中TIME-WAIT sockets的快速回收。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_tw_recycle,默认为0,表示关闭。 提示:reuse和recycle这两个参数是为防止生产环境下Web、Squid等业务服务器time_wait网络状态数量过多设置的。
- net.ipv4.tcp_syncookies 表示开启SYN Cookies功能。当出现SYN等待队列溢出时,启用Cookies来处理,可防范少量SYN攻击,这个参数也可以不添加。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_syncookies,默认值为1
- net.ipv4.tcp_keepalive_time 表示当keepalive启用时,TCP发送keepalive消息的频度。默认是2小时,建议改为10分钟。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_keepalive_time,默认为7200秒。
- net.ipv4.ip_local_port_range 该选项用来设定允许系统打开的端口范围,即用于向外连接的端口范围。 该参数对应系统路径为:/proc/sys/net/ipv4/ip_local_port_range 32768 61000
- net.ipv4.tcp_max_syn_backlog 表示SYN队列的长度,即半连接队列长度,默认为1024,建议加大队列的长度为8192或更多,这样可以容纳更多等待连接的网络连接数。 该参数为服务器端用于记录那些尚未收到客户端确认信息的连接请求最大值。 该参数对象系统路径为:/proc/sys/net/ipv4/tcp_max_syn_backlog
- net.ipv4.tcp_max_tw_buckets 表示系统同时保持TIME_WAIT套接字的最大数量,如果超过这个数值,TIME_WAIT套接字将立刻被清除并打印警告信息。 默认为180000,对于Apache、Nginx等服务器来说可以将其调低一点,如改为5000~30000,不通业务的服务器也可以给大一点,比如LVS、Squid。 此项参数可以控制TIME_WAIT套接字的最大数量,避免Squid服务器被大量的
TIME_WAIT套接字拖死。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_max_tw_buckets - net.ipv4.tcp_synack_retries 参数的值决定了内核放弃连接之前发送SYN+ACK包的数量。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_synack_retries,默认值为5
- net.ipv4.tcp_syn_retries 表示在内核放弃建立连接之前发送SYN包的数量。 该参数对应系统路径为:/proc/sys/net/ipv4/tcp_syn_retries,默认值为6
- net.ipv4.tcp_max_orphans 用于设定系统中最多有多少个TCP套接字不被关联到任何一个用户文件句柄上。 如果超过这个数值,孤立连接将被立即被复位并打印出警告信息。 这个限制只有为了防止简单的DoS攻击。不能过分依靠这个限制甚至认为减少这个值,更多的情况是增加这个值。该参数对应系统路径为:/proc/sys/net/ipv4/tcp_max_orphans ,默认值8192
- net.core.somaxconn 同时发起的TCP的最大连接数,即全连接队列长度,在高并发请求中,可能会导致链接超时或重传,一般结合并发请求数来调大此值。 该参数对应系统路径为:/proc/sys/net/core/somaxconn ,默认值是128
- net.core.netdev_max_backlog 表示当每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许发送到队列的数据包最大数。 该参数对应系统路径为:/proc/sys/net/core/netdev_max_backlog,默认值为1000