Fluge Site

TCP是一个非常复杂的面向连接的协议,在很30多年来,各种优化变种争论和修改不断,所以我先从连接的建立和终止开始写TCP。后面应该还有几篇文章写TCP的另外几个特别重要的特性。
TCP最开始被我知道就先从很有特点的链接建立和终止—三次握手和四次挥手,基本上TCP协议的可靠性就是从保证连接的可靠性开始的。

TCP链接的建立—三次握手

对于三次握手,其实是TCP比较著名的东西了,在完全不了解这个TCP的时候就知道有这个东西了,但是开始的时候总有一点让我非常的疑惑:TCP为什么是三次握手,为什么不是两次或四次?

TCP 为什么是三次握手,为什么不是两次或四次?

要解释这个问题,首先明白TCP出现的价值和思路:是为了在不可靠的互联网络上提供一个可靠的端到端字节流而设计的,并且一个TCP连接是全双工。这是TCP很重要的一个设计理念:提供了一种可靠的,面向连接的字节流运输层服务,并且是双全工的。这里需要理解双全工的意思:就是两端之间进行通信,这两端既可以是数据的接收方,也可以是数据的发送方。
1、 可靠模型:但是为了数据的安全送达,就必须在发送数据前向另一个端口进行通信

数据发送端A:嘿,我想发送数据了,可以么。
数据接收端B:好的,这边允许接受。

然后数据的发送端就可以发送数据了,这里就基本保证你发的在接收方会正常的接受并不会发错。这是发送数据的基本可靠模型
2、 连接模型:在TCP的要求中,需要一种面向连接的通信:连接在我理解中就是相当于有一根空水管,连接两个水池(为两个水池传输东西),在水管中传输东西的效率肯定会高于用桶去一桶桶的装,来的方便。

这两个模型联系起来:就是当两个水池之间要进行交换东西的时候,需要有一个水管去保持两边的交换的效率。这个时候就把水管的一边和一个水池相连(用水管的另一边进行可靠模型的验证),如果验证通过在把另一边进行相连,然后再去用可靠模型验证。都通过就说明整个链接水管工作完成了。可以正常的工作。
在上面的水管是虚拟的不存在的,抽象出整个可靠模型,然后简化整个过程:A,B连个端口

A:我想向你发送数据,可以么。
B:可以啊。(第一次可靠验证结束)
B:我想向你发送数据,可以么。
A:可以啊。 (第二次可靠验证结束)

然后上面的过程其实可以进一步的优化就是把两次的可靠验证结合在一起:中间两次的B的发送数据显然可以合并为一次数据发送。
上面就是我对为什么是三次握手,为什么不是两次或四次的理解的一个方向。两次握手只能让链接的一端发送的数据的是可信的,四次握手就显得有点多余。

TCP的三次握手具体过程

TCP的三次握手除了要建立可靠的连接外还有就是初始化SYN(传输数据的包的序号)。也就下图中的xy。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。

基本过程:

  1. 第一次握手:客户端发送syn包(syn=x)到服务器,并进入SYN_SEND状态,等待服务器确认;
  2. 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
  3. 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。

在握手中值得注意的细节是:

  1. 关于建连接时SYN超时。如果服务端接到了客户端发的SYN后回了ACK后client掉线了,服务器端没有收到客户端回来的ACK(第二次握手完成),那么,这个连接处于一个中间状态,即没成功,也没失败。于是,服务器端如果在一定时间内没有收到的TCP会重发ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻倍,5次的重试时间间隔为1s,2s,4s,8s,16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
  2. 关于SYN Flood攻击。一些恶意的人就为此制造了SYN Flood攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。于是,Linux下给了一个叫tcp_syncookies参数来应对这个事——当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是:tcp_synack_retries可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大SYN连接数;第三个是:tcp_abort_on_overflow处理不过来干脆就直接拒绝连接了。

TCP链接的终止—四次挥手

挥手过程就很好理解了,TCP是双全工的。发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。

A:兄弟我的数据传送完了
B:收到了—-过了一会B的数据也传完了
B:兄弟我数据传完了
A:好的收到了


在里面需要注意的是:关于MSLTIME_WAIT。我们注意到,在TCP的状态图中,从TIME_WAIT状态到CLOSED状态,有一个超时设置,这个超时设置是 2*MSL(RFC793定义了MSL为2分钟,Linux设置成了30s)为什么要这有TIME_WAIT?为什么不直接给转成CLOSED状态呢?主要有两个原因:1)TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL,2)有足够的时间让这个连接不会跟后面的连接混在一起(你要知道,有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。

关于TCP的长链接与短连接

TCP的链接都是通过三次握手和四次挥手建立的,在一般情况下一般TCP是建立的短连接,一次数据传完就断开,这样也方便链接的管理。但是在某些需要频繁的交换数据的场景,这样就会浪费很多时间和资源在链接的建立和断开,所以这个时候就适合使用长连接进行通信。

短连接

一般短连接是client向server发起连接请求,和server通过三次握手,建立连接。client和server两边进行一次通信,然后一次读写就完成了。通常这时候双方都可以发送FIN进行关闭连接,但是一般情况下都是client先进行发送,一般server不会回复完ACK后立即关闭连接,server端会在数据发送完后发送FIN进行关闭,一般一个链接进行一次通信,这样链接的管理比较方便,不需要额外的控制手段。

长链接

长链接的情况比短连接复杂,client和server建立链接后,进行一次通信,在通信完成后,双方都不会发送FIN关闭链接,后续通信也可以继续使用该链接。而且链接的保活一般都是在server端进行的,在不进行通信的时候,server是处于一个半链接的状态,这个时候的server应该通过某种办法去获取client的状态,用来判断这个链接是否该关闭,不然对server资源占用是非常严重的,而且也非常没有必要,这个也叫TCP的保活。
TCP的保活一般有两种办法:
1、在应用层建立心跳机制:client在隔一段时间向server发送一次心跳包(一般都是很小的包,或者只包含包头的一个空包),让server知道此时client的状态,server可以不对心跳包进行处理。
2、TCP协议的KeepAlive机制:当建立一个TCP连接时设置后keepalive后,就会将一系列的定时器与该连接相关联。这些定时器中某些用于处理keepalive过程。当keepalive定时器变为0时,client会发送一个keepalive 探针包(probe packet)到server,server在收到包后,会回应一个ACK。


参考:TCP 的那些事儿(上)