TCP复习笔记(一) 连接的建立与终止

一. TCP首部

首部长度以4字节为单位,4位首部长度最多可表示60字节,其中有20字节的固定长度。由于封装在在一个IP报文中,并且可根据首部长度得到数据起始位置,因此TCP首部没有数据或总长度字段,这和IP首部不一样,IP首部同时有首部长度和总长度字段,前者用于得到数据起始位置,后者用于得到结束位置,因为以太网要求桢的数据部分最小长度为46字节,IP报文小于46字节将在后面添加Padding,因此需要总长度来区分哪些是数据,哪些是Padding。这一点对TCP来说不存在,因为得到IP报文数据的结束位置,也就得到了TCP数据的结束位置:

首部中的序号对传输字节进行计数,在建立连接时,SYN置1,此时序号字段为该主机选择的初始序列号(ISN)。另外,SYN和FIN分别占用一个序列号,这样才能唯一标识一个SYN或FIN包,用于确认或超时重传。

二. 连接建立与终止

2.1 MSS

在建立连接时,双方会在TCP首部加入MSS选项用于通告己方能接收的最大报文段长度(不包括TCP和IP首部),MSS只能出现在SYN报文中,如果没有指明,则默认为536(实际上是576的IP报文长度)。在发送SYN报文时,根据MSS=MTU-固定的IP首部-固定的TCP首部长度来确定MSS大小,对于以太网,理想的MSS为1500-20-20=1460,而实际上大部分的MSS为1024,因为许多BSD的实现版本需要MSS为512的倍数。

通告双方的MSS主要是为了避免分段。分段(分片)发生在IP层,由于物理链路层限制了每次发送的数据帧最大长度,因此IP层会在必要的时候对数据进行分段,并在到达目的地时重组,这一切对传输层的TCP是透明的,因此TCP可以认为它的每份交给IP的字节流,都会以正确的形式到达目的地。

避免分片主要有两个好处:

  • 效率更高,因为IP层的分片可能由中间路由器完成
  • 在有IP分片丢失时,将重传整个IP报文(IP报文没有确认重传机制)。基于这一点,TCP避免分片将使得重传更为高效

2.2 ISN

在建立连接的SYN报文中的序列号字段即为初始序列号(ISN),这个值不能是硬编码的,否则可能会出现重新建立连接后,新连接将旧连接的包误认为是有效包的情况(每次建立连接的ISN是一样的)。ISN的初始化应该是动态的,防止新旧包交叠。RFC的建议是每4微妙ISN加1,这样用完32位需要4.55小时,然后ISN又从0开始。4.55小时远远大于TCP包在网络中的最大生存时间,是比较安全的。

2.3 半关闭

半关闭是指TCP一端在结束发送后,还能收到来自另一端的数据。半关闭不建议被应用程序利用,但是我们至少应该理解shudown(1)close()的区别,确保TCP连接得到正确关闭。

2.4 SYN超时

在尝试主动连接服务器时,如果服务器不可达,客户端将尝试重发SYN包。比如:telnet 11.11.11.11,可通过tcpdump tcp port 23查看重试次数和重试间隔,也可以通过date; telnet 11.11.11.11; date来查看总超时时间。

2.5 SYN-ACK超时

当服务器收到客户端的SYN包后,会回复SYN-ACK包,如果此时客户端迟迟不回ACK包,那么服务器将超时重发SYN-ACK包,重发次数默认为5次,重发间隔依次为1s,2s,4s,8s,16s,加之最后确认超时的32s,一共是63s。这63秒中,该连接占用了服务器的SYN队列,当SYN队列满时,新的连接请求将不能得到处理。SYN Flood攻击就是利用这一点,在发完第一个SYN之后,就下线,耗尽服务器的SYN队列,使其它的正常连接请求得不到处理。

三. 状态转变

其中比较重要的状态有:

3.1 TIME_WAIT

在主动发起关闭的一方,发送完最后一个ACK后,需要等待2MSL的时间,这样做的目的有两个:

  • 确保最后一个ACK正确到达,2MSL可让TCP再次发送最后的ACK以防这个ACK丢失(另一端超时重发FIN)
  • 确保在建立新的连接前,任何老的重复报文在网络中超时消失

MSL(Maximum Segment Lifetime):报文最大生存时间,用于限制TCP包在网络中最大留存时间,超过这个时间,包将被丢弃。IP层有个类似的TTL跳数来决定IP报文的去留,MSL和TTL共同限制了TCP包的生存时间。RFC建议MSL为2分钟,而Linux下为30秒。

在2MSL这段时间内,定义这个连接的套接字对将不可用,任何迟到的报文都将被丢弃。而在伯克利套接字等实现版本上,有更严格的限制:在2MSL时间内,套接字所使用的本地端口默认情况下都不能再使用。

对于客户端来说,这通常没有什么影响,因为客户端一般都使用临时端口与服务器进行连接。因此客户端主动断开连接并重启后,尽管之前使用的端口会处于TIME_WAIT状态而不能复用(bind()),但connect()会选择一个新的临时端口。

而对于服务器来说,由于服务器通常使用已知端口(监听端口),如果我们终止一个已经建立连接的服务器程序,并重启它,服务器程序将不能把这个已知端口赋给新的套接字(bind()),会得到EADDRINUSE的错误,只有在2MSL之后,端口才能被再次使用。

我们可以使用SO_REUSEADDR选项来重用处于2MSL状态的端口,它的原理是需要新的连接的初始序列号(ISN)大于旧连接的最后序号。这样就可以根据序列号区分哪些是旧连接的迟到报文,哪些的新连接的报文,但是仍然是有遗留风险的。

3.2 FIN_WAIT_2

在主动发起关闭的的一方发送完FIN并收到另一方的ACK后,进入FIN_WAIT_2状态。此时连接处于半关闭状态,正常情况下,如果我们的应用不利用半关闭这个特性,那么对方的应用层在收到FIN后,也会发送一个FIN关闭另一个方向上的连接。本端收到该FIN后,才进入TIME_WAIT状态。

这意味着主动关闭端将可能永远处于FIN_WAIT_2状态,被动端也一直处于CLOSE_WAIT状态。为了防止这种无限等待,当应用层执行全关闭(close)而不是半关闭(shutdown)时,多数套接字实现会生成一个定时器,定时器超时后,连接将进入CLOSED状态。这也进一步提醒我们应该确保正确关闭TCP连接。

四. TCP状态机

网络上的传输的没有连接的,TCP的所谓的”连接”是依靠一个状态机来维护当前的连接状态。理解这一点是非常重要的,因为网络是随时会有异常,连接在任何阶段都可能被异常终止。通常情况下,如果状态机收到了意料之外的包,将回复一个RST重置包,对方会据此重置连接状态。具体的状态机如下: