大白话简记TCP

ISO 定的 OSI 七层模型

协议族五层

TCP 生命周期

tcp连接生命周期

  • 对于建链接的3次握手 主要是初始化Sequence Number的初始值。通信的双方要互相知道对方初始化的Sequence Number(缩写ISN: Initial Sequence Number),所以叫SYN,全称:Synchronize Sequence Number。这个同步序号作为此TCP连接后面数据通信的序号,以保证应用层能处理乱序、重复、丢失的问题。

  • 对于4次挥手 其实你仔细看是2次,因为TCP是全双工的,所以双方都会发出FINACK一次请求和应答。只不过有一方是被动的,所以看上去就像是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接到了clientSYN请求,然后应答了ACK-SYNclient掉线,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 = 63sTCP才会断开这个连接

  • 关于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发了30segment过去,但是网络断了,于是client重连,又用1ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,clientSequence 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

  • 关于MSLTIME_WAITTCP的状态图中,从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 的那些事儿