《Linux高性能服务器编程》学习总结(三)——TCP协议详解
第三章 TCP协议详解
TCP协议是TCP/IP协议族中另外一个重要的协议,作为传输层上的协议,它更靠近应用程序,所以具有更强的可操作性,许多socket选项都是针对TCP协议设置的。
我们在第一章提到过,传输层协议主要有两个,TCP协议和UDP协议,并说明了两者的区别,简单来说,TCP协议双方再数据传输之前需要先建立连接,并为该连接分配必要的内核资源,完成数据交换后需要断开连接并释放资源。所以TCP的连接是一对一的,基于广播和多播的服务则不能使用TCP协议。
两者另外一个重要区别在于,TCP是基于字节流的服务,而UDP是基于数据报的服务,而这种区别在编程过程中的体现就在于通信双方是否必须执行同样次数的读写操作。当发送端的应用程序执行了多次写操作时,TCP模块先将这些数据放入TCP发送缓冲区,当真正发送时,发送缓冲区中的数据可能被封装成一个或多个TCP报文段发出,因此TCP模块发送的报文段数量和应用程序执行的写操作次数没有固定的数量关系。而UDP不同,发送端应用程序每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送之,接收端也应针对每一个数据报执行读操作,否则就会丢包。
在TCP首部中,有几个重要的字段,首先是序号字段,主机A和主机B进行TCP通信双方各自的第一个TCP报文段,序号值会被系统初始化为某个随机值ISN,那么在该方向上的后续TCP报文段的序号值就是该报文段数据部分第一个字节相对于整个字节流头部的偏移量加上这个ISN值,例如某个TCP报文段的数据是第1025-2048字节,那么其序号值为ISN+1025。确认序号是对连接另一方发来的报文段的响应,假设B收到了A发送的序号值为1000,那么他给A发送的确认报文的确认字段应为1001。4为首部长度和IP首部一样,单位依旧是32bit。下面的6位标志位包括:URG标志位表示紧急指针是否有效,ACK标志位表示确认序号是否有效,PSH标志位表示接收端应用程序应立即从TCP缓冲中读走数据,RST标志位表示要求对方重新建立连接,SYN标志表示请求建立一个连接,FIN标志表示通知对方本端即将关闭。2字节的窗口大小是TCP用来控制流量的,它告诉对方本端的TCP缓冲区还能接受多少字节的数据,最后的紧急指针是一个偏移量,它和序号的相加值表示一个紧急数据的下一字节的序号,用来指示紧急数据。可选的选项部分有很多,在后面的流量控制和拥塞避免会详细说明。
TCP是面向连接的,所以其建立和关闭连接就是很重要的一环。TCP的连接建立又称三次握手,过程是:1)主机A向主机B发送一个序号为x,并且SYN置1的同步报文段;2)主机B收到A的报文回复一个序号为y,确认序号为x+1,并且ACK和SYN置1的同步报文段,对上一个报文进行确认并请求与对方连接;3)A向B发送一个序号为x+1,确认序号为y+1并且ACK置1的确认报文段。这样双方的连接就建立了起来。
而TCP的关闭连接过程又称四次挥手,过程是:1)主机A向主机B发送一个序号为x,确认序号为上一次收到的数据报序号+1,即y,并且FIN位置1的包表示本端请求关闭连接;2)B向A回复序号为y+1,确认序号为x+1,ACK置1的报文,此时A到B的链路中断,但是B仍可以向A继续发送数据,A也能正确接收;3)B发送完数据后向A发送一个序号为z,确认序号为x+1,FIN位置1的报文请求关闭本方连接;4)A收到后回复序号为x+1,确认序号为z+1,ACK置1的报文,并等待2MSL时间后结束连接。
当TCP连接建立的时候,如果服务器端由于网络状态或连接数量过多等原因没有对SYN同步报文段进行应答,那么客户端会如何做呢?通过实验我们会发现客户端会连续发送6个序号一致的同步报文段,其间隔为1s、2s、4s、8s、16s,一共发起了5次重连,最后一次等待32s后连接失败,所以TCP重连的超时时间每次增加一倍。
上图中红色为服务器端正常连接的过程,蓝色为客户端正常连接的过程,黄色为各种意外因素导致的状态转换,绿色为因各种原因双方回到CLOSE状态。
红色和蓝色的过程上文已经说过,此处不再赘述,第①种意外情况是当服务器收到客户端的TCP连接请求时发现对方请求连接的端口未打开,此时会向对方回复一个复位报文段;第②种情况是同时打开,就是说当TCP连接双方几乎同时向对方发送SYN同步报文段请求连接,双方收到后都进入到SYN_SEND状态,接到对方的SYN后各自返回一个ACK并进入SYN_RCVD状态,后收到对方的ACK进入ESTABLISHED,同时打开的时候没有服务器和客户端的概念,并且一共会发送4个报文段,而不是正常三次握手中的3个;第③种意外情况是同时关闭,和同时打开类似;第④种情况是当A端关闭连接后,B端发送的ACK和FIN报文同时到达A端,此时A可以不必经过FIN_WAIT_2状态而直接进入TIME_WAIT。
需要重点解释的是,当客户端发送ACK给服务器端后要进入2MSL的TIME_WAIT时间,MSL是报文段最大生存时间,RFC1122中定义这个时间一般为2min。等待这端时间的目的是防止由于网络原因服务器端未收到这个ACK回复,重传了上一个FIN报文段,如果不进入TIME_WAIT状态则服务器端一旦接收不到ACK,就会维持连接,消耗内核资源。所以在实际编程过程中,我们会经常发现多次测试重复绑定socket的时候会出现端口被占用的错误,这就是端口仍处于TIME_WAIT状态不能被使用,可用socket选项SO_REUSEADDR来强制立即使用端口。
在上述状态变迁图中,并没有完全说明另外一种特殊情况,即当某些时候,TCP连接的一端会向另一端发送携带RST标志的复位报文段以通知对方关闭连接或重新建立连接。一般有三种情况:1)访问不存在的端口,这点在上面的①中已经说明;2)异常中止连接,当一方向另一方发送复位报文段时,发送端所有排队等待发送的数据皆被丢弃,在编程过程中,可以使用socket选项的SO_LINGER来发送复位报文段以异常终止一个连接;3)处理半打开连接,当通信双方建立连接后,若其中一方网络断线,而另一方此时并不知情,此时断线方网络重连,没有了该连接的信息,即处于了半打开状态,另一方继续向对方发送数据,此时由于对面连接已经被关闭,所以对方会返回一个复位报文段以重新连接。
TCP在传输数据的时候,根据数据的长度分为交互数据流和成块数据流。交互数据流一般仅仅包含很少的字节,我们以telnet举例,在最原始的情况下,我们每键入一个字符,就会向服务器端发送这个字符,服务器端再回复一个确认报文段后,再发送这个字符的回显,而后客户端再对于回显报文进行确认,也就是说键入一个字符就会产生4个报文段,当有大量数据键入的时候产生的TCP报文段数量将极其庞大。所幸我们有了延迟确认机制,意思就是当服务器端收到客户端报文段时会延迟发送确认报文段,而会等待一段时间看是否有需要回送的数据,如果有则与数据一同发送,这样一来就减少了网络中的TCP报文段。但是这种机制仍旧没有从根本解决大量TCP报文段可能在网络上产生的拥塞问题,所以我们有了Nagle算法。Nagle算法要求一个TCP连接的双方在任意时刻最多只能发送一个未被确认的报文段,在该TCP报文段的确认到达之前不能发送其他报文段,与此同时,发送方在等待确认时收集本段要发送的微量数据,待确认到来时一并发送,该算法还有个优点是自适应性,确认到达得越快,数据发送也就越快。显然,这样的算法可以使得网络上的TCP报文段大量减少。TCP在发送成块数据流的时候,也不会每收到一个报文段就发送一个确认,而是通常使用“隔一个报文段确认”策略。
TCP的超时重传机制和之前说过的超时重连很像,默认一共执行多次重传,每次重传的超时时间增加一倍,分别是0.2s、0.4s、0.8s、1.6s、3.2s等。
TCP还有拥塞控制的功能,其最终受控变量为发送端向网络一次连续写入的数据量,我们称之为发送窗口SWND,由于我们每次TCP发送数据时有最大报文段长度MSS,所以SWND控制了能连续发送TCP报文段的数量,发送端应合理选择SWND的大小,如果太小则会有明显延迟,太大则可能造成网络拥塞。虽然接收方可以通过通告窗口RWND控制发送端的SWND,但这明显不够,所以我们引入了一个拥塞窗口CWND。拥塞控制算法包括慢启动、拥塞避免、快速重传和快速恢复,慢启动算法的原理是先设定一个较小的初始SWND,然后慢慢提高这个值以试探网络的实际情况,但是如果不施加其他手段限制,则SWND会很快膨胀。所以在拥塞控制中我们定义了一个重要的变量,慢启动门限ssthresh,当CWND的大小超过这个门限的时候,就进入拥塞避免阶段。拥塞避免算法会使得CWND线性增大。而发送端是如何判断网络上是否发生拥塞呢?依据有两条,一是传输超时,或者说TCP重传定时器溢出,二是接收到重复的确认报文段。对第一种情况,TCP仍然使用慢启动和拥塞避免算法,将ssthresh调整为2MSS和未收到确认的字节数/2的最大值,而对第二种情况则会使用快速重传和快速恢复算法,当发送端收到3个重复的确认报文段,将CWND设置为ssthresh+3*SMSS并重传缺失的报文段,而后每次收到一个重复确认,设置将CWND加一个SMSS,当收到新数据的确认时将CWND设置为之前的ssthresh,而后重新回到拥塞避免状态。