ISO 定的 OSI 七层模型
协议族五层
TCP 生命周期
对于建链接的3次握手 主要是初始化
Sequence Number
的初始值。通信的双方要互相知道对方初始化的Sequence Number
(缩写ISN
:Initial Sequence Number
),所以叫SYN
,全称:Synchronize Sequence Number。这个同步序号作为此TCP连接后面数据通信的序号,以保证应用层能处理乱序、重复、丢失的问题。对于4次挥手 其实你仔细看是2次,因为TCP是全双工的,所以双方都会发出
FIN
、ACK
一次请求和应答。只不过有一方是被动的,所以看上去就像是4次挥手。如果两边同时发出断开的请求,那就会进入到CLOSING
的状态,然后到达TIME_WAIT
状态。下图是双方同时段连接的示意图:
因为双方同时发起了FIN
,都认为自己是FIN发起的主动方
,各自都进入FIN_WAIT_1
状态,都在等待对方被动
发出FIN
请求,同时双方都收到对方的FIN
请求,于是各自应答对方ACK
,对方进入CLOSING
状态,这个CLOSING
都是在等待前面提到等待对方发出被动的FIN请求
,都在互相等待,那就像死锁
了,这里的解决方案就是进入CLOSING
状态时同时开启TIME_WAIT
等待指定的时机后会自动进入CLOSED
状态。
下面我们用电话通话
的例子来比拟下这里的建立连接3次握手,断开链接4次挥手。
打电话的3次握手
A: 你听到我说话么? – 1. 发往
B
的一个握手请求。发出问题让对方回答,目的是为了验证自己说的话对方能听到,即验证自己的话筒
->对方的听筒
这条链路。
B: 我听到你说话,你能听到我说话吗? –2. 发往
A
的一个握手请求。回答对方的问题,让对方知道他的验证通过。自己再发出问题,目的是为了验证自己说的话对方能听到,也就是:B的话筒
->A的听筒
这条链路是通的。
A: 我也能听到你说话 – 3. 发往
B
的一个握手请求。回答B
的问题,让B
知道他说的话自己也能听到。
4次挥手挂电话
A: 我的话说完了,要挂电话了,你说完了么? (发起断开
A -> B
的请求)
B: 好吧(应答下对方的挂掉意思本方已经收到, 这里意味着
A -> B
不会再发起新的请求,只有应答)
….
B: 我也说完了,我也要挂电话了 (发起断开
B -> A
链路的请求)
A: 好的 (应答对方,意味
B -> A
的链路也断开了,整个连接完整断开)
这里要解释的一点是,上面B
的连在一起的两句话,为什么不能像建立连接时候的两句话再一次请求里一次说掉呢? 因为bye bye
前可能还有话正在说,而hello
是对话的开始,之前没有别的话。
几个需要注意的事情
关于建连接SYN超时 试想一下,如果
server
接到了client
的SYN
请求,然后应答了ACK-SYN
后client
掉线,server
端没有收到client
回来的ACK
,那么连接处于一个中间的状态,既没成功,也没失败。于是server
端如果在一定时间内没有收到ACK
,TCP
回重发ACK-SYN
。在Linux
下,默认重试次数是5次,5次重试的时间间隔分别是:1s, 2s, 4s, 8s, 16s, 32s
,所以总共需要1s+2s+4s+8s+16s+32s = 2^6 -1 = 63s
,TCP
才会断开这个连接关于SYN Flood攻击 一些恶意的人就为此制造了
SYN Flood
攻击——给服务器发了一个SYN
后就下线,于是服务器默认等63s才会断开连接,这样攻击者重复这个过程就可以把服务器SYN连接队列
耗尽,让正常的连接请求不能处理。于是,Linux
下给了一个叫tcp_syncookies
的参数来应对这个事——当SYN连接队列
满了以后,TCP
会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number
发回去(又叫cookie
),如果是攻击者则不会有响应,如果是正常连接,则会把这个SYN cookie
发回来,然后服务端可以通过cookie
来建连接(即使你不在SYN队列中
),请注意请先千万别用tcp_syncookies
来处理正常的大负载连接的情况。因为syncookies
是妥协版的TCP
协议,并不严谨。对于正常的请求有三个参数可供调整,第一个是:tcp_synack_retries
可以减少重试次数;第二个是:tcp_max_syn_backlog
,可以增大SYN
连接数;第三个是:tcp_abort_on_overflow
处理不过来就直接拒绝连接。关于
ISN
的初始化ISN
是不能用hard code
的,不然会有问题——如果连接建立好始终使用1
来作ISN
,client
发了30
个segment
过去,但是网络断了,于是client
重连,又用1
做ISN
,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client
的Sequence Number
可能是3
,而server
端认为client
的这个号是30了。全乱了。RFC793中说,ISN
会和一个假的时钟绑在一起,这个时钟会在每4微秒
对ISN
做加一操作,知道超过2^32
,又从0
开始。这样,一个ISN
的周期大约是4.55个小时
。因为,我们假设我们的TCP segment
在网络上的存货时间不会超过Maximun Segment Lifetime
(MSL),所以,只要MSL
的值小于4.55小时
,那么我们就不会重用到ISN
关于
MSL
和TIME_WAIT
在TCP
的状态图中,从TIME_WAIT
状态到CLOSED
状态,有一个超时设置是2*MSL
(RFC793定义MSL
为分钟
,Linux
设置为30s
),为什么要有TIME_WAIT
,不直接转成CLOSED
状体呢?主要有两个原因:1)TIME_WAIT
确保有足够的时机让对方收到ACK
,如果被动关闭的那方没有收到ACK
,就会触发被动端重发FIN
,一来一去正好2个MSL
2) 有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些路由器会缓存IP数据包
,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)关于
TIME_WAIT
数量太多 从上面的描述我们可以知道,TIME_WAIT
是一个很重要的状态,但是如果在大并发的短链接下,TIME_WAIT
就会太多,这也会消耗很多系统资源。只要搜一下,就会发现多数方式会教你设置两个参数,一个tcp_tw_reuse
,另一叫tcp_tw_recycle
,这两个参数默认是关闭的后者recycle
比前者更激进,reuse
要温柔一些。另外,如果使用tcp_tw_reuse
,必须设置tcp_timestamps=1
,否则无效。这里,你一定要注意,打开这两个参数会有比较大的坑——可能会让TCP
连接出一些诡异的问题- 关于
tcp_tw_reuse
官方文档上说tcp_tw_reuse
加上tcp_timestamps
可以保证协议角度上的安全,但是你需要tcp_timestamps
在两端都被打开 - 关于
tcp_tw_recycle
如果tcp_tw_recycle
打开的话,会假设对端开启了tcp_timestamps
,然后回去比较时间戳,如果时间戳变大,就可以重用。但是,如果对端是一个NAT
网络的话(如:一个公司只用一个IP出公网)或是对端的IP
被另一台重用了,这个事就复杂了。建链接的SYN
可能被直接丢掉(你可以看到connnection time out
的错误) - 关于
tcp_max_tw_buckets
这个是控制并发TIME_WAIT
的数量,默认值是180000
,如果超限,那么系统会把多的destory掉,然后在日志里打上警告(如:time wait bucket table overflow
),官网文档说这个参数用来对抗DDOS
攻击的。也说的180000
默认值并不小。这个还要根据实际的情况考虑。
- 关于
TIME_WAIT
意味是你主动断开连接的,如果对方断连接,那么这个破问题就是对方的了。另外如果你的服务器是HTTP
服务器,那么设置一个HTTP的KeepAlive
相当重要(浏览器会重用一个TCP
连接处理多个HTTP
请求),让客户端去断链接。
数据传输中的 Sequence Number
参考自:TCP 的那些事儿