TCP协议
TCP(Transmission Control Protocol,传输控制协议)是一种在计算机网络中常用的协议,它是一种面向连接的、可靠的、基于字节流的传输层协议。TCP协议主要负责对数据进行分段、组装、传输和确认,以保证数据的可靠传输。
特点
- 可靠性:TCP协议可以保证数据的可靠传输,通过数据分段、校验和、确认机制等手段来确保数据不会丢失、损坏或重复
- 面向连接:TCP协议在传输数据之前需要先建立连接,传输完成后再释放连接。这种连接方式可以保证数据的有序传输,避免数据混乱
- 流式传输:TCP协议是基于字节流的传输层协议,将数据按照字节流方式分段传输,不需要考虑数据的长度和格式
- 拥塞控制:TCP协议可以根据网络状态来控制数据的发送速度,避免网络拥塞导致数据丢失和延迟
TCP首部
- 源端口:发送方端口,长度为2个字节16位,因此最大值为
2的16次方
65536 - 1,故TCP的端口范围为0 ~ 65535
- 目标端口:另一方的端口,范围同上
- 序列号:代表着数据的位置,最大序号为
2的32次方-1
,每发一次数据就要累加一次该数据的长度,当序号超过最大值又会从0开始。序列号不一定从0或1开始,而是从建立连接的随机数作为其初始值,在建立连接和断开连接时发送的SYN包和FIN包虽然并不携带数据,但是也会作为1个字节增加对应的序列号。 - 确认号:指下一次应该收到的数据的序列号
- 数据偏移:字段长4位,单位为4字节,代表着数据包开始的位置,也可以理解成TCP首部总长度。从图中可以知道首部的固定长度为20字节,在没有可选数据时其最小值为5(二进制表示
0101
),最大值2的4次方减1
(二进制1111
)15再乘4为60字节,由此可知可选部分的最大长度为40字节 - 保留位:4位长度,以后扩展使用,到目前为止还没有任何用处
- 控制位:8位长度,由左到右分别是
CWR(Congestion Window Reduced)
、ECE(Explicit Congestion Notification)
、URG(Urgent Flag)
、ACK(Acknovledgement Flag)
、PSH(Push Flag)
、RST(Reset Flag)
、SYN(Synchronize Flag)
、FIN(Fin Flag)
,当他们的值为1时都表示一定的含义- CWR:减小拥塞窗口,发送方降低发送速率
- ECE:ECN回显,发送方接受到了一个更早的拥塞通知(ECN是一种拥塞控制机制,用于在发生拥塞时通知TCP发送方降低发送速率,从而避免网络拥塞)
- URG:表示当前数据需要紧急处理
- ACK:表示确认需要有效,也就是ACK为1时上图中的确认号才会有效
- PSH:表示数据需要立刻传给上层协议
- RST:表示连接出现非常严重的错误必须重新连接
- SYN:表示希望建立连接,并将当前的序号作为初始值
- FIN:表示希望断开连接不会有数据发送了
- 窗口:字段长为16位,用于通知从确认号开始能够接受的数据大小,如果窗口的值为0时表示可以发送窗口侦测,通常情况下TCP会根据窗口大小进行分段传输,每段最大传输数据大小(MSS)为1460,实际情况的最大长度为1448(减去12字节的时间戳选项)
- 校验和:端到端校验机制确保数据的正确性,由发送方通过头部、数据计算得到的校验和,接收方方会重新校验进行对比
- 紧急指针:在URG控制位为1时有效,该字段的数值表示数据中的紧急数据,也就是说从数据的开始到紧急指针的位置为紧急数据,因此紧急指针也代表了紧急数据的末尾
- 选项:选项类型(无操作、最大段大小MSS、窗口缩放因子、时间戳等等),每个选项的第一个字节为种类指明选项的类型,种类为0和1的选项仅占1字节,其他种类选项根据种类来确定字节数,种类1允许发送者用多个4字节组填充字段
注意
在老版本的TCP协议中可能只会存在6为控制位,而CWR、ECE控制位可能不存在,新版本都会有8位控制位
以上的字段明细都可以使用wireshark抓包工具捕获到,如果你对此工具还不熟悉可以参考我的「wireshark网络抓包」一文
连接与终止
TCP是可靠传输协议,其数据传输前通信双方必须建立一条连接,总体来说TCP通信是个复杂的过程(超时重传、流量控制、分包组装等等),整体通信大致过程示意图如下:
三次握手
关于TCP的三次握手和四次挥手是常问的考点,搞清楚它非常简单,这里总结下二者。TCP的连接需要三个步骤:发起端发送SYN、接收端发送SYN+ACK、发送端发送ACK。为什么需要三次呢?一次行不行?答案是否定的,三次正好符合可靠传输的特点。这就好比2人打电话,甲方打给乙方,甲方问你是乙方吗,乙方回答我是那你是谁呢,甲方再次回乙方说我是谁,这样下来双方都知道对方的身份并且知道电话已经打通了,可以收发数据。而TCP连接也是这样的,下方是三次握手连接的示意图:
- 刚开始Client处于close状态,Server处于Listen状态
- Client主动打开发送
SYN=1、Seq=x
请求表示主动连接(注意:seq在初始化时是个随机数,它会根据时间戳变化,超时重传时使用相同的seq,新的连接会生成新的序列号,来避免历史或其他连接的错误问题),此时Client进入SYN_SENT
状态 - Server接受到客户端的
SYN
请求后,需要应答Client并发送SYN=1、ACK=1、Seq=y
请求,其中确认号为x+1
且有效期望下一次客户端的发送序列号应为x+1
,而SYN
表示也想连接Client且序号为y
,此时Server进入SYN_RCVD
状态 - Client收到Server的
ACK
响应后进入ESTABLISHED
状态表示已连接(半连接状态),然后发送Seq=x+1、ACK=1
的请求,序列号为y+1
且有效,表示确认了Server的连接请求,并期望Server下一次发送的序号为y+1
- Server收到客户端的
ACK
响应后也进入ESTABLISHED
状态,双方进入全连接状态,三次握手完毕可以进行数据传输了
抓包结果:
TCP握手期间虽然没有真实的数据进行传输,但在这期间会进行相关重要数据的约定,如:窗口大小、MSS等有用的信息
重置连接
三次握手是TCP连接的正常过程,但还有其他非正常的连接出现严重的连接错误,此时就会通过标记RST来重置连接,比如主机A使用telnet连接主机B的某个未开放端口,就会被主机B拒绝:
# 使用主机A192.168.10.8登录 主机B192.168.10.9:8080
➜ telnet 192.168.10.9 8080
Trying 192.168.10.9...
telnet: connect to address 192.168.10.9: Connection refused
2
3
4
当主机B接受到TCP连接请求时被认为成错误的连接,此时主机B会主动发送一个RST的响应,表示连接错误,需要重新连接,可以使用抓包工具查看:
连接队列
上面的三次握手只是简单的概述了主要的连接过程,在真实环境中存在请求队列的概念,如同时并发多个TCP请求,就会将其排列成队列进行处理
在TCP连接中存在两个重要的队列SYN队列(半连接队列)
和Accept队列(全连接队列)
,它们分别用于处理连接请求和已经建立连接的数据传输
- SYN队列:用于存储SYN(同步)请求的队列。当一个客户端请求与服务器建立TCP连接时,它会向服务器发送一个SYN包。服务器在收到SYN包后,将在SYN队列中排队等待确认,并向客户端发送一个SYN-ACK包作为确认
- Accept队列:用于存储已经建立连接的队列(未被上层应用程序使用)。当服务器收到客户端第三次握手的ACK请求后,客户端和服务器之间的连接就建立了,此时连接会被添加到accept队列中,等待应用程序使用
大致处理过程示意图如下: 首先Client主动发送SYN
连接请求,Server收到后创建半连接对象将其放入SYN队列
,Server发送ACK+SYN
给Client后,直到收到Client的ACK
响应后,创建全连接对象将其连接放入Accept队列
,最后由应用程序接受处理
SYN队列的的最大限制通常是1000,Accept队列的最大限制通常是128,可以通过以下方式查看:
# 查看syn队列最大值
➜ cat /proc/sys/net/core/netdev_max_backlog
1000
# accept队列最大值
➜ cat /proc/sys/net/ipv4/tcp_max_syn_backlog
128
2
3
4
5
6
总的来说两个队列并不是很大,但实际情况中由于CPU、内存等相关因素的影响,我们的应用程序可能达不到很高的并发处理请求,因此TCP的请求就会被延时,或当SYN、Accept队列溢出时,Server也会忽略掉Client的SYN请求包,根据重传机制Client等待一段时间重新发送SYN,此时Server表现出繁忙
的状态
SYN泛洪
SYN泛洪也称SYN攻击,是TCP常见的网络攻击手段,其利用TCP连接三次握手的第一阶段,当Client向Server疯狂发送SYN请求,却不响应Server的ACK+SYN请求时,Server的SYN队列会很快被打满,从而主动丢弃后面的SYN请求,也就无法进行正常的TCP连接了,大量的SYN请求也会消耗Server的资源,产生不正常的消耗
如何避免SYN攻击呢,可以从防火墙
、SYN Cookies
、减小SYN队列
、SYN ACK重传次数
等着手:
开启SYN Cookies:
什么是SYN Cookies?当SYN队列溢出时,如果再收到SYN请求不会为其分配任何存储资源,而只有当SYN+ACK报文段被确认时才分配到Accept队列里。SYN Cookies是由Server通过一定的算法加随机值计算而来,再作为序列号发送给Client,Server收到Client的ACK报文后会校验合法性,如果合法将会放入Accept队列,否则直接丢弃,这样就绕过了SYN队列资源。
可以通过以下方式设置syncookies:
sh# 0 表示关闭 # 1 SYN队列溢出时开启 # 2 永久开启 ➜ cat /proc/sys/net/ipv4/tcp_syncookies 1
1
2
3
4
5服务器通常用下面的方法设置初始序列号:首5位是t模32的结果,其中t是一个32位的计数器,每隔 64秒增1;接着3位是对服务器最大段大小(8种可能之一)的编码值;剩余的24位保存了 4元组与t值的散列值,该数值是根据服务器选定的散列加密算法计算得到的。Server根据其中的t值可以计算出与加密 的散列值相同的结果,那么服务器才会为该SYN重新构建队列
减小SYN ACK重传次数
减小重传次数让其达到最大重传次数时,释放掉SYN资源断开连接,SYN ACK重传次数由linux内核决定的,可以通过以下方式进行查看和设置:
sh➜ cat /proc/sys/net/ipv4/tcp_synack_retries 5
1
2
四次挥手
TCP断开需要4步完成
- 假如Client想要释放连接将会发送
FIN
请求给Server端,接着进入FIN_WAIT_1
状态 - Server收到Client的
FIN
请求后,给Client回了一个ACK
响应,进入CLOSED_WAIT
状态 - Client收到Server的
ACK
响应后,进入FIN_WAIT_2
状态,等待Server发送FIN
请求 - Server处理完数据发送
FIN
请求给Client,进入LAST_ACK
状态 - Client终于等到了Server的
FIN
请求,赶紧给Server回了一个ACK
响应,紧接着进入TIME_WAIT
状态,在等待2MSL
时间后也会进入CLOSE
状态 - Server收到Client的
ACK
响应后进入CLOSE
状态,此时Server已经完全关闭
抓包结果:
💡为什么看到的是3次挥手?
不是说TCP断开需要4次挥手吗?为什么实际抓包的结果却是3次就完成了?这里就需要了解TCP的延时确认机制
TCP的延迟确认(Delayed ACK)
是一种优化TCP传输性能的机制。TCP协议默认采用延迟确认机制,即接收方不会立即发送确认消息,而是等待一定时间(通常是200ms),看是否需要返回数据,如果无需数据就会等待一会看后面的请求是否需要返回数据,便一起返回发送一个确认消息,以减少确认消息的数量和网络负载,否则需要数据就会立刻返回ACK
而三次回收的情况通常是接收端不需要再发送数据了,根据延时确认机制便会将2、3次挥手(ACK&FIN)合并成一次数据返回,这就是为什么实际网络中看到的是3次挥手的原因。可以通过修改TCP的延迟发送值来禁用此功能,这里不再展开
💡什么是MSL?为啥需要2MSL
MSL(Maximum Segment Lifetime)是报文最大生存时间,也就是说网络中最大的存活时间,否则将会丢弃。MSL的时间通常大于TTL的跳数时间,IP的传输存活时间是基于TTL的跳数,每过一个路由器TTL都会减去1,直到为0时将会丢弃报文,同时发送ICMP
报文通知源主机目标不可达
MSL默认值通常为30
,TTL的默认跳数为64
,系统认为30s内IP可以被转发64次。为什么是2MSL呢?2MSL是从Client接收到Server的FIN报文后发送ACK报文开始计算的,假如ACK报文由于网络原因在MSL时间内没有到达Server,Server会触发超时重传机制再次发送FIN报文给Client,所以2MSL是最保险的时间,当Client等待2MSL内没有收到任何Server的包就会被关闭掉
通过以下方式查看系统默认的TTL跳数和MSL时间
# TTL 默认跳数
➜ cat /proc/sys/net/ipv4/ip_default_ttl
64
# FIN默认超时时间(2MSL时间)
➜ cat /proc/sys/net/ipv4/tcp_fin_timeout
60
2
3
4
5
6
💡有趣的是处于TIME_WAIT(2MSL)
状态下的端口还不能重新被使用!虽然Client已经收到Server的FIN报文并发送了ACK报文确定要断开连接了,但在TIME_WAIT期间其端口还是不能被使用(尽管Server可能已经被关闭了),这个原因其实和为啥要等待2MSL道理一样,都是为了避免2MSL期间Server重传FIN报文。我们来验证下端口不可用:
# 访问本地nginx
➜ curl localhost
# 查看TCP的连接状态,可以很明显本地的 `33196` 端口关联了 80端口,即curl随机选用了 33196 端口访问nginx
➜ netstat -ant | grep 80
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:33196 127.0.0.1:80 TIME_WAIT
# 使用nodejs监听33196端口,抛出异常 端口被占用
➜ node index.js
events.js:377
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE: address already in use :::33196
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上述等再次查看33196已经被完全断开时就可以正常启动了
查看TCP状态
可以在主机上通过netstat
命令查看tcp的连接状态,netstat是一个用于显示网络状态和统计信息的命令行工具,可以用来查看系统的网络连接、路由表、接口状态等
➜ netstat -antp | more
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:111 0.0.0.0:* LISTEN 872/rpcbind
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1229/nginx: master
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1211/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1454/master
tcp 0 0 0.0.0.0:10010 0.0.0.0:* LISTEN 1229/nginx: master
tcp 0 196 192.168.10.9:22 192.168.10.1:65308 ESTABLISHED 1691/sshd: root@pts
tcp 0 0 127.0.0.1:33196 127.0.0.1:80 TIME_WAIT -
2
3
4
5
6
7
8
9
10
可以从上看出tcp监听的端口号、进程、连接状态等等。如:nginx监听了80端口、ssh监听了22端口,22端口和65308端口保持连接(这里我在主机用terminal连接了虚拟机)处于ESTABLISHED建立状态,33196端口和80端口已经处于断开但还在TIME_WAIT状态
半连接、半断开、半打开
要了解这几种状态含义需要明白TCP是个全双工连接协议,它提供了全双工的数据传输能力。全双工连接指的是通信双方可以同时发送和接收数据,而不受对方数据传输的影响。这意味着,在一个TCP连接中,数据可以沿着两个方向同时传输,而不需要等待对方的回复
半连接:在TCP三次握手时,Client如果不回复Server端ACK报文时,Server端就处于SYN_RCVD状态,消耗Server端的资源,常见的SYN攻击就是基于半连接漏洞产生的
半断开:在TCP四次挥手期间,Client发送了FIN报文后,却一直等不到Server的FIN报文,此时Client只能收到报文而不能发送报文
半打开:正常的TCP连接双方都可以收发数据报文,假如在某时间段两端不进行任何数据传输,而一端由于某种原因断开了且不发送任何FIN报文,另一端也不会知道是不是断开了,这时就处于半打开状态。而系统内核一般对TCP有一个保活机制(KeepAlive)来心跳检测是否还处于连接状态,当TCP在指定时间不发送任何数据,系统则认为已断开连接
注意
HTTP也有一个KeepAlive的概念,但与TCP的KeepAlive完全不同;HTTP的KeepAlive是开启长连接,在HTTP/1.1默认会开启,主要是提供HTTP请求可以在同一个TCP连接上完成,减小网络开支;而TCP的KeepAlive是个保活机制,在长时间没有进行数据传输时用来探测是否还处于连接状态
数据传输
我们知道TCP是个可靠传输协议,那么它是如何实现数据传输的可靠性的呢?通常情况下TCP会对传输的数据进行分段并进行编号,当接受方收到数据后重新进行组装。若在传输的过程中出现了丢包,发送端便会重传丢失的包,其具体实现看下一小节
分段传输
TCP协议的每段最大数据长度通常被称为MSS(Maximum Segment Size)
1460,这个值是根据IP层MTU(Maximum Transmission Unit)
最大传输单元计算的,MTU值一般为1500,如果过大会造成网络中路由器的缓冲压力,当IP层所传输的数据大小大于MTU时就会进行IP分片
如果IP分片传输中有一个IP片丢失了就会重传所有的IP片段,这无疑会浪费网络资源。所以TCP为了不让数据在IP分片,通常设置为每片IP最大数据承载大小也称为MSS。MTU的值为1500字节,IP头占用20字节,TCP头占用20字节,因此MSS的值通常为1500-20-20=1460字节
可以通过设备上的某个网卡查看MTU大小:
➜ cat /sys/class/net/ens160/mtu
1500
2
在实际中MSS的值通常都是1448比1460少了12个字节,这通常是TCP的头部选项部分会多12字节的时间戳等信息,其用来服务超时重传等功能
滑动窗口
TCP的数据可靠传输类似于一问一答的形式,每发送一个数据包会通过另一方的确认来判断是否丢包,但如果每次只能发送一个数据包将大大提高时延,因此TCP中引入了窗口的概念
什么是TCP窗口(Window)?窗口通常用来限制数据包的发送速率,是一个流量控制机制,通过动态改变窗口的大小进行控制发送速率。窗口的大小通常等于接收方的窗口大小,会根据接收方的确认消息动态调整,在三次握手期间虽然没有进行实质的数据传输,但通常都会协商一些有用的信息,其中就包含窗口的大小
窗口其实是个缓存空间,每一方都会在缓存空间中维护着一些有用的信息,如:已发送的数据、未发送的数据等等。假如发送方的窗口大小为3,那么就会连续发送3个数据包,而不需要等待一个一个确认,同时缓冲区会记录这些已发送和未发送的信息,从而改变窗口大小和位置
滑动窗口的概念则是调整发送数据的起始位置,TCP在传输数据时会将数据分成多段MSS大小的数据段并进行编号,然后便会以窗口大小为基本单位按顺序发送数据,因此窗口范围内的数据发送期必早于窗口后的数据。而窗口内通常会包含多个数据片段,每段数据被实际接收的时间会因为网络原因进行波动,所以发送方收到不同数据片段的ACK时间也会不同,当窗口的前部分被确认后,就会进行窗口滑动,这样后面未在窗口内的数据段就会被放入窗口内等待发送
大概的滑动窗口示意图如下:
窗口的大小会根据接收方的确认信息、发送时延、拥塞控制动态改变的,然后来控制发送速率,达到数据传输的可靠、高效,接下来了解下TCP如何完成数据传输的可靠性
重传机制
大家知道TCP是个可靠的传输协议,所以会有特殊的机制来保证传输数据的可靠性,而其中一个重要的特性就是超时重传。当发送方发送一个报文后在指定的时间内没有收到另一方的响应后,就会认为数据包已经丢失,便会根据重传机制重新发送数据包
由于网络波动等种种原因,超时重传是避不开的问题,重传有两种机制:基于时间和基于冗余ACK,通常后者比前者更高效
RTT、RTO
了解超时重传前需要知道RTT
、RTO
两个时间概念
RTT(Round-Trip Time)
是指TCP数据包从发送方发送到收到接收到另一方的ACK所需的时间
RTO(Retransmission Timeout)
是指TCP发送方在未收到确认的情况下等待重传的时间
超时重传是根据RTO的时间进行判断的,RTO是根据RTT进行动态计算的,如何确定RTO的时间是非常关键的,通常略大于RTT的时间,过小造成网络资源浪费,过大网络延时偏大,这里不具体展开其实现算法
超时重传
当TCP发送一个数据包时会启用一个定时器进行倒计时,如果在RTO的时间范围内收到了对方的ACK包则重置定时器不会重传报文,反之没有收到另一方的ACK,则会重新发送数据包,并重启定时器,当超时重传时会把定时器的时间设置为上一次的2倍,也称二进制指数避退(binary exponential backof)
,当然不会无线重传下去,会有重传的阈值,当超过了这个值就会断开TCP连接,默认操作系统的重传次数可以通过以下方式进行查看:
➜ cat /proc/sys/net/ipv4/tcp_syn_retries
6
2
快速重传
快速重传不是基于定时器,而是基于数据进行重传的,通常发生在没有延时的情况下。若TCP累积确认无法返回新的ACK,或者当ACK包含选择确认信息(SACK)表明出现失序报文段时,快速重传会推断出现丢包
通常来说,当发送端认为接收端可能出现数据丢失时,需要决定发送新(真正丢包的) 数据还是重传所有的问题,处理不好就会造成网络资源的浪费
TCP提供了SACK(Selective Acknowledgment)
方法来提高重传的效率。该机制允许接收方向发送方发送选择性确认(SACK),即确认接收到的连续数据块,同时告知发送方已经接收到丢失数据块的位置,发送方一般都会维护一个缓冲区用来标识已发送的和超时的,从而让发送方只重传丢失的数据块
拥塞控制
拥塞控制是一种网络流量控制机制,用于避免在网络中发生拥塞而导致网络性能下降或崩溃。该机制通过动态调整发送数据的速率来控制网络中的拥塞程度,并确保网络能够承载传输的数据量
TCP的拥塞控制算法通常基于网络拥塞的反馈信息,例如丢包、延迟等。当TCP发送方收到这些反馈信息时,它会采取一系列措施来减少发送数据的速率,以避免过多的数据流入网络中而导致拥塞。通过改变发送窗口进行速率的控制,发送窗口一般等于接收窗口,而有了拥塞控制后,发送窗口会取拥塞窗口和接收窗口的最小值
慢启动
在TCP连接建立时,发送方会将拥塞窗口的大小设置为一个较小的值,通常为2个最大分段大小(MSS)。发送方将拥塞窗口的大小逐渐增加,每发送一个数据段就将窗口大小加1,这样可以使得发送方逐渐探测网络的可用带宽。但不可能一直这样增长下去,当拥塞窗口大小达到一个阈值时,发送方将进入拥塞避免阶段,此时拥塞窗口的增加速率将变慢
假如拥塞窗口刚开始为1,当接受到1个ACK后,窗口大小加1可以同时发送2个SYN,然后接受到2个ACK后变成窗口大小变为4;接着发送4个SYN收到后大小变成8;就这样反复增加直到达到最大阈值,慢启动算法就是以指数的形式增加,增加速度比较快
拥塞避免
当达到慢启动的阈值时就会进入拥塞避免算法,以防止指数级的发送造成网络堵塞问题,拥塞避免算法当收到1个ACK时,拥塞窗口增加1/n
个大小,这样窗口的增长速率会越来越小
除了慢启动、拥塞避免算法外,还有快速恢复算法快速来恢复发送速率,这里不再展开了
总结
本文从TCP的首部、建立断开、传输过程、重传机制、拥塞控制等多方面讲述了TCP协议的工作方式和细节,而TCP远不止这么简单内容,作为非网络工程师对于更深的概念了解下即可,掌握这些通常足够了