Linux高性能服务器编程 学习笔记 第三章 TCP协议详解

与IP协议相比,TCP协议更靠近应用层,因此在应用程序中有更强的可操作性。一些重要的socket选项都和TCP协议相关。

本章从以下方面讨论TCP协议:
1.TCP头部信息。TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口号、目的端端口号,管理TCP连接,控制两个方向的数据流。

2.TCP状态转移过程。TCP连接的任意一端都是一个状态机,在TCP连接从建立到断开的过程中,连接两端的状态机将经历不同的状态变迁。

3.TCP数据流。有两种TCP数据流:交互数据流和成块数据流。TCP数据流中有一种特殊的数据,称为紧急数据。

4.TCP数据流的控制。为保证可靠传输和提供通信质量,内核需要对TCP数据流进行控制,我们会讨论TCP数据流控制的两方面:超时重传和拥塞控制。

传输层协议主要有TCP和UDP,TCP相对于UDP的优点是面向连接、字节流、可靠传输。

使用TCP协议通信的双方必须先建立连接,然后才能开始数据的读写,双方都必须为该连接分配必要的内核资源,以管理连接状态和连接上数据的传输。TCP连接是全双工的,即双方的数据读写可通过一个连接进行。完成数据交换后,通信双方必须断开连接以释放系统资源。

TCP协议的这种连接是一对一的,所以基于广播和多播(目标是多个主机地址)的应用程序不能使用TCP服务,而无连接的协议UDP则适合广播和多播。

字节流服务和数据报服务的区别对应到实际编程中,则体现为通信双方是否必须执行相同次数的读、写操作(只是表现形式)。当发送端应用进程连续执行多次写操作时,TCP模块先将这些数据放入TCP发送缓冲区中,当TCP模块真正开始发送数据时,发送缓冲区中的这些等待发送的数据可能被封装成一个或多个TCP报文段发出。因此,TCP模块发送出的TCP报文段的个数与应用进程执行的写操作次数之间没有固定的数量关系。

当接收端收到一个或多个TCP报文段后,TCP模块将它们携带的应用程序数据按照TCP报文段的序号依次放入TCP接收缓冲区中,并通知应用进程读取数据。接收端应用进程可以一次性将TCP接收缓冲区中的数据全部读出,也可以分多次读取,这取决于用户指定的应用程序读缓冲区的大小,因此,应用进程执行读操作次数和TCP模块接收到的TCP报文段个数之间也没有固定的数量关系。

综上,发送端指定的写操作次数和接收端执行的读操作次数之间没有任何数量关系,这就是字节流的概念:应用进程读数据的发送和接收时没有边界限制的。UDP则不然,发送端应用进程每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送之,接收端必须针对每个UDP数据报执行读操作(通过recvfrom系统调用),且必须及时读取,否则就会丢包(常出现在较慢的服务器上),并且,如果用户没有指定足够的应用进程缓冲区来读取一个UDP数据报,该UDP数据报将被截断。
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第1张图片
TCP传输是可靠的,首先,TCP采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功;其次,TCP协议采用超时重传机制,发送端在发出一个TCP报文段后启动定时器,如果定时时间内未收到应答,它将重发该报文段;最后,由于TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP还会对接收到的TCP报文段重排、整理,再交给应用层。

UDP协议和IP协议一样,提供不可靠服务,它们都需要上层协议来处理数据确认和超时重传。

TCP头部出现在每个TCP报文段中,用于指定通信的源端端口、目的端端口、管理TCP连接等,以下是TCP头部结构,其中字段为管理TCP连接和控制数据流提供了足够的信息:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第2张图片
16位端口号告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。进行TCP通信时,客户端通常使用系统自动选择的临时端口号,而服务器使用知名服务端口号。所有知名服务使用的端口号都定义在/etc/services文件中。

32位序号为一次TCP通信(从TCP连接建立到断开)过程中某个传输方向上的字节流中的每个字节编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系统初始化为某个随机ISN(Initial Sequence Number,初始序号值),在该传输方向上(从A到B),后续的TCP报文段中序号值被系统设置为ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移,如某个TCP报文段传送的数据是字节流中第1025~2048字节,那么该报文段的序号值就是ISN+1025。另一个传输方向(从B到A)的TCP报文段的序号值也有相同含义。

32位确认号用作对另一方发送来的TCP报文段的响应,其值是下一个期望收到的数据包的序号值,即已经成功接收的数据的最后一个字节的序号值加1。假设主机A和主机B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,还包含对B发送的TCP报文段的确认号。反之,B发出的TCP报文段也同时携带自己的序号和对A发送来的报文段的确认号。

4位头部长度标识该TCP头部有多少32bit字(以4字节为单位),由于4位能表示的最大值为15,因此TCP头部长度最长是60字节。

6位标志位包含以下几项:
1.URG标志:表示紧急指针是否有效。

2.ACK标志:表示确认号是否有效,我们称携带ACK标志的TCP报文段为确认报文段。

3.PSH标志:提示接收端应用进程应立即从TCP接收缓冲区中取走数据,为接收后续数据腾出空间(如果应用进程不将接收到的数据读走,它们会一直留在TCP接收缓冲区中)。

4.RST标志:表示要求对方重新建立连接,我们称携带RST标志的TCP报文段为复位报文段。

5.SYN标志:表示请求建立一个连接,我们称携带SYN标志的TCP报文段为同步报文段。

6.FIN标志:表示通知对方本端要关闭连接了,我们称携带FIN标志的TCP报文段为结束报文段。

16位窗口大小字段是TCP流量控制的一个手段,窗口指的是接收窗口(Receiver Window, RWND),它告诉对方本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。

16位校验和字段由发送端填充,接收端对TCP报文段执行校验以确定TCP报文段在传输过程中是否损坏,这个校验不仅包括TCP头部,也包括数据部分。

16位紧急指针是一个正的偏移量,当URG标志生效时,它和序号字段的值相加表示最后一个紧急数据的下一字节的序号,即这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

TCP头部最后一个选项字段是变长的可选信息,这部分最多包含40字节,因为TCP头部最长是60字节,减去20字节固定部分。TCP头部选项结构:
在这里插入图片描述
选项的第一个字段kind说明选项的类型,有的TCP选项后没有后面两个字段,仅包含1字节的kind字段。第二个字段指定该选项的总长度,该长度包括kind字段和length字段占据的2字节。第三个字段info是选项的具体信息。常见的TCP选项有以下7种:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第3张图片
各个kind字段的说明:
1.kind=0:选项表结束选项。

2.kind=1:空操作(nop)选项,没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。

3.kind=2:最大报文段长度选项,TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segment Size,MSS),TCP模块通常将MSS设置为MTU-40字节(20字节TCP头部和20字节IP头部),这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不含选项字段,且通常也确实不含选项字段),从而避免本机发生IP分片。对以太网而言,MSS值为1500-40=1460字节。

4.kind=3:窗口扩大因子选项,TCP连接初始化时,通信双方使用该选项协商接收通告窗口的扩大因子。在TCP头部中,通告窗口字段是16位的,因此最大为65535字节,但为了TCP通信的吞吐量,TCP模块通过窗口扩大因子提高通告窗口上限,假设TCP头部中接收到的通告窗口大小为N,窗口扩大因子为M,那么TCP报文段的实际接收到的通告窗口大小为N*2 M ^M M,M的取值范围为0~14。我们可通过修改/proc/sys/net/ipv4/tcp_window_scaling内核变量来启用或关闭窗口扩大因子选项。

和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。当连接建立好后,每个数据传输方向的窗口扩大因子就固定不变了。窗口扩大选项的细节可参考RFC 1323。

5.kind=4:选择性确认(Selective Acknowledgement,SACK)选项。TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送(如传了3个TCP报文段,对方只收到了第1个和第3个,因此对方只会确认第1个报文段已收到,而本端会重传第2个和第3个,即使第3个对方已经收到了),从而降低TCP性能。SACK可使TCP模块只重新发送丢失的TCP报文段,而不用发送所有未被确认的TCP报文段。本选项只用于连接初始化时,表示是否支持SACK技术。我们可通过修改/proc/sys/net/ipv4/tcp_sack内核变量来启动或关闭选择性确认选项。

6.kind=5:SACK实际工作的选项,该选项告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此重传丢失的数据块。每个块边沿(edge of block)字段包含一个4字节的序号,其中块左边沿表示没收到的不连续块的第一个数据的序号,而块右边沿表示没收到的不连续块的最后一个数据的序号的下一个序号,这样一对参数之间的数据是没有收到的。一个块信息占用8字节,因此TCP头部选项中实际最多可以包含4个这样的不连续数据块(需要考虑2字节的选项类型和长度字段)。

7.kind=8:时间戳选项,它提供了计算通信双方回路时间(RTT,Round Trip Time)的方法,从而为TCP流量控制提供了信息。我们可通过修改/proc/sys/net/ipv4/tcp_timestamps内核变量来启用或关闭时间戳选项。

用tcpdump抓取一个包含TCP报文段的数据包:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第4张图片
tcpdump输出以上内容中,Flags [S]表示该TCP报文段包含SYN标志,因此它是一个同步报文段,如果TCP报文段中包含其他标志,则也会将其放在方括号中。

seq是序号值,因为这是第一个SYN段,因此此值是此次通信过程中该传输方向的ISN值,也因此它没有针对对方发来的TCP报文段的确认值,因为还没收到任何对方发送的TCP报文段。

win是通告窗口大小,由于这是一个同步报文段,因此此值反应的是最大窗口大小。

options是TCP选项,具体选项在方括号中。mss是发送端通告的最大报文段长度,通过ifconfig命令可查看接口的MTU为16436字节,因此TCP报文段的MSS为16436-40=16396字节。sackOK表示发送端支持并同意使用SACK选项。TS val是发送端的时间戳,ecr是时间戳回显应答,由于这是第一个SYN段,因此它针对对方的时间戳的应答为0。nop是一个空操作选项。wscale指出发送端使用的窗口扩大因子为6。

接下来分析tcpdump输出的字节码中TCP头部对应的信息,它从上图中的21字节开始(除去IP首部):
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第5张图片
可见TCP报文段头部的二进制码和tcpdump输出的TCP报文段描述信息完全对应。

在ernest-laptop上执行telnet命令登录Kongming20的80端口,我们抓取这一过程中客户和服务器交换的TCP报文段,操作如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第6张图片
执行telnet命令并在两台通信主机之间建立连接后(telnet输出Connected to 192.168.1.109),键入Ctrl+]调出talnet程序的命令提示符,然后在telnet命令提示符后输入quit以退出telnet客户程序,从而结束TCP连接。整个过程中tcpdump的输出如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第7张图片
由于整个过程没有发生应用层数据的交换,所以TCP报文段的数据部分的长度(上图中length字段)总是0。我们将以上输出绘制成时序图:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第8张图片
第一个TCP报文段包含SYN标志,因此它是一个同步报文段,即ernest-laptop(客户端)向Kongming20(服务器)发起连接请求,同时,该同步报文段包含一个ISN值为535734930的序号。第二个TCP报文段也是同步报文段,表示Kongming20同意与ernest-laptop建立连接,同时发送自己的ISN值为2159701207的序号,并对第一个同步报文段进行确认,确认值是535734931,即第一个同步报文段的序号值加1。序号值是用来标识TCP数据流中的每一字节的,但同步报文段比较特殊,即使它没有携带任何应用进程数据,也要占用一个序号值。第三个TCP报文段时ernest-laptop对第二个同步报文段的确认,至此,TCP连接就建立起来了,建立TCP连接的这三个步骤被称为TCP三次握手。

从第三个TCP报文段开始,tcpdump输出的序号值和确认值都是相对初始ISN值的偏移,我们可以使用tcpdump的-S选项来打印序号的绝对值。

后面4个TCP报文段时关闭连接的过程。第四个TCP报文段包含FIN标志,因此它是一个结束报文段,即ernest-laptop要求关闭连接,结束报文段和同步报文段一样,也要占用一个序号值。Kongming20用TCP报文段5来确认该结束报文段。紧接着Kongming20发送自己的结束报文段6,ernest-laptop则用TCP报文段7给予确认。实际上,仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确认报文段5是否出现在连接断开过程,取决于TCP的延迟确认特性。

在连接关闭过程中,因为ernest-laptop先发送结束报文段,故称ernest-laptop执行主动关闭,而Kongming20执行被动关闭。

一般TCP连接是由客户端发起,并通过三次握手建立(特殊情况是同时打开)的,而TCP连接的关闭可能是客户端执行主动关闭,也可能是服务器执行主动关闭,也可能是同时关闭(和同时打开一样,很少见)。

TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭,即通信的一端可以发送结束报文段给对方,告诉它本端已经完成了数据发送,但允许继续接收来自对方的数据,直到对方也发送结束报文段以关闭连接。TCP连接的这种状态称为半关闭(half close)状态:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第9张图片
由上图,服务器和客户端应用进程判断对方是否已关闭连接的方法是read系统调用返回0(收到结束报文段)。Linux还提供其他检测连接是否被对方关闭的方法。

socket网络编程接口通过shutdown函数提供对半关闭的支持。使用半关闭的应用程序很少见。

以上是很快建立连接的情况,如果客户访问一个距离它很远的服务器,或由于网络繁忙,导致服务器对于客户端发出的同步报文段没有应答,对于提供可靠服务的TCP来说,此时客户端程序会先进行重连(可能执行多次),如果重连仍无效,则通知应用进程连接超时。

为观察连接超时,我们模拟一个繁忙的服务器环境,在ernest-laptop上执行以下操作:
在这里插入图片描述
iptables命令用于过滤数据包,-F选项用于清空当前所有规则链中的规则,即重置为默认设置;-I INPUT含义为将规则插入名为INPUT的规则链中;-p tcp表示只匹配TCP协议的数据包;-i eth0表示只匹配从eth0网卡接收到的数据包;-j DROP表示匹配到的包全部丢弃,不做任何处理。这样客户端就无法得到SYN报文段的ACK。

接下来在Kongming20上执行telnet命令登录到ernest-laptop,并用tcpdump抓取这个过程中双方交换的TCP报文段,具体操作如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第10张图片
从两次date命令的输出可以看出,Kongming20建立TCP连接的超时时间是63秒,本次tcpdump的输出如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第11张图片
此次抓包我们保留了tcpdump输出的时间戳(没有使用-t选项)。

我们一共抓到6个TCP报文段,它们都是同步报文段,且具有相同的序号值,这说明后5个同步报文段都是超时重连报文段。这些TCP报文段被发送的时间间隔分别为1、2、4、8、16秒(由于定时器精度问题,这些时间间隔都有一定偏差),可以推断第6个TCP报文段的超时时间是32秒(63-16-8-4-2-1=32),因此TCP模块一共执行了5此重连操作,这是由/proc/sys/net/ipv4/tcp_syn_retries内核变量所定义的。每次重连的超时时间都增加一倍,在5此重连都失败的情况下,TCP模块放弃连接并通知应用程序。

在应用程序中,我们可以修改连接超时时间。

TCP连接的任意一端在任一时刻都处于某种状态,TCP连接当前状态可通过netstat命令查看。我们接下来讨论TCP连接从建立到关闭的整个过程中通信两端状态的变化,以下是完整的状态转移图,它描绘了所有TCP状态以及可能的状态转换:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第12张图片
上图中粗虚线表示典型的服务器端连接的状态转移,粗实线表示典型的客户端连接的状态转移。CLOSED是一个假想的起始点,并不是一个实际的状态。

先讨论服务器的典型状态转移过程。

服务器通过listen系统调用进入LISTEN状态,被动等待客户端连接,因此执行的是被动打开。服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中,并向客户端发送带SYN标志的确认报文段,此时该连接处于SYN_RCVD状态。如果服务器成功接收到客户端发送回的确认报文段,则该连接进入ESTABLISHED状态。ESTABLISHED状态是连接双方能进行双向数据传输的状态。

当客户端主动关闭连接时(通过close或shutdown系统调用向服务器发结束报文段),服务器通过返回确认报文段使连接进入CLOSE_WAIT状态,含义为等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接后,也会立即给客户端发送一个结束报文段来关闭连接,这将使连接转移到LAST_ACK状态,以等待客户端对结束报文段的最后一次确认,一旦确认完成,连接就彻底关闭了。

下面讨论客户端的典型状态转移过程。

客户端通过connect系统调用主动与服务器建立连接,connect系统调用首先给服务器发送一个同步报文段,使连接转移到SYN_SENT状态,此后,connect系统调用可能首先因为下面两个原因返回失败:
1.如果connect连接的目标不存在(未被任何进程监听),或该端口被处于TIME_WAIT状态的连接所占用,则服务器将给客户端发送一个复位报文段,connect调用失败。

2.如果目标端口存在,但connect函数在超时时间内未收到服务器的确认报文段,则connect调用失败。

connect调用失败将使连接立即返回初始的CLOSED状态。如果客户端成功收到服务器的同步报文段和确认,则connect调用成功返回,连接转移至ESTABLISHED状态。

当客户端执行主动关闭时,它向服务器发送一个结束报文段,同时连接进入FIN_WAIT_1状态,若此时客户端收到服务器该FIN报文段的ACK,则连接转移至FIN_WAIT_2状态。当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这一对状态时半关闭状态。如果服务器随后也关闭连接(发送结束报文段),则客户端将给予确认并进入TIME_WAIT状态。

图3-8中还给出了客户端从FIN_WAIT_1状态直接进入TIME_WAIT状态的一条线路,没有经过FIN_WAIT_2状态,前提是处于FIN_WAIT_1状态的服务器收到的ACK报文中还带有FIN,这种情况对应于图3-6中服务器把TCP报文段5和6合成一个TCP报文段发送。

处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段,才能转移到TIME_WAIT状态,否则它将一直停留在这个状态。如果服务器端不是为了在半关闭状态下继续接收数据,长时间地使客户停留在FIN_WAIT_2状态并无益处。客户执行半关闭后,可能未等服务器关闭连接就强行退出了,此时客户端连接由内核来接管,可称之为孤儿连接(与孤儿进程类似),Linux为了防止孤儿连接长时间存留在内核中,定义了两个内核变量/proc/sys/net/ipv4/tcp_max_orphans和/proc/sys/net/ipv4/tcp_fin_timeout,前者指定内核能接管的孤儿连接数,后者指定孤儿连接在内核中生存的时间(孤儿连接处于FIN_WAIT_2状态的最长时间)。
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第13张图片
图3-8中还描绘了其他非典型的TCP状态转移路线,如同时关闭、同时打开。

从上图看,客户端连接在收到服务器的结束报文段(报文段6)后,并没有进入CLOSE状态,而是转移到TIME_WAIT状态,此状态下,客户端要等待2MSL(Maximum Segment Life,报文段最大生存时间)的时间,才能完全关闭。MSL是TCP报文段在网络中的最大生存时间,RFC 1122的建议值是2分钟。

TIME_WAIT状态存在的原因:
1.可靠地终止TCP连接。假设上图中报文段7丢失,那么服务器将重发结束报文段,因此客户端需要停留在某个状态以处理重复收到的结束报文段,否则,客户端将以复位报文段来回应服务器,服务器会复位连接(而非优雅地关闭连接,因为它期望的是一个对于FIN的ACK)。

2.保证让被延迟的TCP报文段有足够的时间被识别并丢弃。在Linux系统上,一个TCP端口不能被同时打开两次及以上,当一个TCP连接处于TIME_WAIT状态时,我们无法使用该连接占着的端口来建立一个新连接。如果不存在TIME_WAIT状态,则应用能立即建立一个与刚关闭的连接相似的连接(相似指它们有相同IP地址和端口号),这个新的与原来连接相似的连接被称为原来连接的化身(incarnation),新的化身可能接收到属于旧连接的、携带应用数据的TCP报文段(被延迟的报文段),这是不应该发生的。

由于TCP报文段的最大生存时间为MSL,所以坚持2MSL时间的TIME_WAIT状态能确保网络上两个传输方向上的TCP报文段都已经消失(被中转路由器丢弃),因此,一个连接的新的化身可以在2MSL时间后安全地建立,不会收到属于原来连接的应用数据,这就是为什么TIME_WAIT状态要维持2MSL时间。

有时我们希望避免TIME_WAIT状态,因为当程序退出后,我们希望能够立即重启它,但由于处在TIME_WAIT状态的连接还占用着端口,程序将无法启动(直到2MSL超时时间结束)。考虑以下例子,在测试机器ernest-laptop上以客户端方式运行nc命令(可以进行TCP/IP连接的创建、监听、传输数据等操作的工具),登录本机Web服务,且明确指定客户端使用12345端口与服务器通信,然后从终端输入Ctrl+C终止客户端程序,接着又启动nc程序,以完全相同的方式再次连接本机Web服务,具体操作如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第14张图片
nc的-p选项指定本地端口。netstat的-n选项以数字形式列出IP地址;-a选项表示列出所有连接(包括在LISTEN状态的连接);-t表示列出TCP连接。

上图我们使用netstat命令查看连接的状态,其输出显示,客户端程序被中断后,连接进入TIME_WAIT状态,12345端口仍被占用,所以客户重启失败。

对客户端程序来说,我们通常不用担心上面描述的重启问题,因为客户端一般使用系统自动分配的临时端口号来建立连接,该临时端口号是操作系统选择的一个未被占用的端口号,以确保不会与其他正在使用的端口号冲突,因此客户端程序一般可以立即重启。上例中只是为了说明问题,我们强制客户使用12345端口,才导致重启客户端失败。

但如果是服务器主动关闭连接后异常终止,则因为它总是使用同一个知名服务端口号,所以连接的TIME_WAIT状态将导致它不能立即重启,但我们可以通过socket选项SO_REUSEADDR来强制进程立即使用处于TIME_WAIT状态的连接占用的端口。

某些特殊条件下,TCP连接的一端会向另一端发送携带RST标志的报文段,即复位报文段,以通知对方关闭连接或重新建立连接。

当客户端访问一个不存在的端口时,目标主机将给它发送一个复位报文段。我们在Kongming20上执行telnet命令登录ernest-laptop上一个不存在的54321端口,并用tcpdump抓取该过程中两台主机交换的TCP报文段,具体操作如下:
在这里插入图片描述
telnet程序的输出显示连接被拒绝了,因为这个端口不存在,tcpdump抓取到的TCP报文段内容如下:
在这里插入图片描述
可见ernest-laptop对于Kongming20的连接请求(同步报文段)回应了一个复位报文段(tcpdump输出了R标志)。收到复位报文段的一端应关闭连接或重新连接,而不能回应这个复位报文段。上图中复位报文段的通告窗口也为0,但复位报文段也不需要通告窗口字段。

当客户进程向服务器的某个端口发起连接,而该端口被处于TIME_WAIT状态的连接所占用时,客户程序也将收到复位报文段。

通过交换结束报文段而终止一个TCP连接是正常的终止方式,TCP还提供了异常终止一个连接的方法,即给对方发送一个复位报文段,一旦发送了复位报文段,发送端所有排队等待发送的数据都将被丢弃。

应用进程可使用socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。

考虑以下情形:TCP连接的本端所在主机崩溃(没有发出FIN),对方接收不到FIN报文段,此时对端还维持着原来的连接,而本端机器崩溃并重启后,就没有该连接的信息了。我们称这种状态为半打开状态,处于这种状态的连接称为半打开连接,如果对端往这个处于半打开状态的连接写入数据,则本端将回应一个复位报文段。

例如,我们在Kongming20上使用nc命令模拟一个服务器程序,使其监听12345端口,然后在ernest-laptop上运行telnetl命令登录到Kongming20的12345端口上,接着拔掉ernest-laptop的网线,并重启机器以中断Kongming20上的服务器程序,此时,ernest-laptop上运行的telnet客户端程序维持着一个半打开连接。然后接上ernest-laptop的网线,然后客户端进程向该半打开连接中写入1个字节的数据a。我们运行tcpdump抓取整个过程交换的TCP报文段,具体操作如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第15张图片
上图中,我们输入字符a后,telnet的输出显示,连接被服务器关闭了。tcpdump抓取到的内容如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第16张图片
由上图,前3个TCP报文段时正常建立TCP连接的3次握手过程。第4个TCP报文段由客户端发送给服务器,它携带了3字节的应用数据,这3字节依次是a\r\n,但由于服务器程序已被中断,所以Kongming20对客户端发送的数据回应了一个复位报文段5。

TCP报文段所携带的应用数据按长度可分为交互数据和成块数据。交互数据仅包含很少的字节,使用交互数据的应用或协议对实时性要求高,如telnet、ssh等。使用成块数据的应用或协议对传输效率要求高,如ftp。

考虑以下情况:在ernest-laptop上执行telnet命令登录自己机器,然后在shell命令提示符后执行ls命令,同时tcpdump抓取这一过程中telnet客户端和telnet服务器(都在一台机器上)交换的TCP报文段,具体操作如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第17张图片
以上过程引起服务器和客户端交换很多TCP报文段,下面仅列出我们感兴趣的、执行ls命令产生的tcpdump输出:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第18张图片
TCP报文段1由客户端发送给服务器,它携带1个字节的应用数据l。TCP报文段2是服务器对TCP报文段1的确认,同时回显字母l。TCP报文段3是客户端对TCP报文段2的确认。第4~6个TCP报文段是针对字母s的上述过程。TCP报文段7传送的2字节数据分别是客户键入的回车符\n和流结束符EOF(本例中是0x00)。TCP报文段8携带服务器返回的客户查询的目录的内容(ls命令的输出)。TCP报文段9是客户端对TCP报文段8的确认。TCP报文段10携带的也是服务器返回给客户端的数据,包括一个回车符、一个换行符、客户端登录的用户的PS1环境变量(第一级命令提示符)。TCP报文段11是客户端对TCP报文段10的确认。

上述过程中,客户端针对服务器返回的数据所发送的确认报文段(TCP报文段6、9、11)都不携带任何应用程序数据(长度为0),而服务器每次发送的确认报文段(TCP报文段2、5、8、10)都包含它要发送的应用进程数据。服务器采用了延迟确认,即它不马上确认上次收到的数据,而是在一段延迟后查看本端是否有数据需要发送,如果有,则和确认信息一起发出。因为服务器对客户请求处理地很快,所以它发送确认报文段的时候总有数据一起发送。延迟确认可以减少发送TCP报文段的数量。由于用户的输入速度明显慢于客户端进程的处理速度,所以客户端的确认报文段总是不携带任何应用数据。在TCP连接的建立和断开过程中,也可能发生延迟确认。

上例是在本地环回上运行的结果,在局域网中也能得到基本相同的结果,但广域网中就未必如此了,广域网上的交互数据流可能经受很大延迟,且由于携带交互数据的小TCP报文段数量一般很多(一个按键输入就导致一个TCP报文段),因此很可能发生拥塞。解决该问题的一个有效方法是使用Nagle算法。

Nagle算法要求在任意时刻最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达前不能发送其他TCP报文段,除非等待过程中要发送的数据报长度满足MSS的长度。发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时将它们(未满MSS,如果已满MSS,则不需等待确认到来)以一个TCP报文段全部发出,这样就极大地减少了网络上的微小TCP报文段的数量。该算法的另一个优点在于其自适应性,确认到达得越快,数据也就发送得越快。

下面考虑用FTP协议传输一个大文件。在ernest-laptop上启动一个vsftpd服务器程序(升级的、安全版的ftp服务器程序),并执行ftp命令登录到该服务器上,然后再ftp命令提示符后输入get命令,从服务器下载一个几百兆的大文件,同时用tcpdump抓取这一过程中ftp客户端和vsftpd服务器交换的TCP报文段,具体操作如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第19张图片
以下是上图过程的部分tcpdump输出:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第20张图片
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第21张图片
上图中,客户端发送的TCP报文段17、18分别是对TCP报文段2和16的确认(从序号值和确认值来判断),由此可见,当传输大量大块数据时,发送方会连续发送多个TCP报文段,接收方可以一次确认所有这些报文段。在接收方两个相邻的ACK间,发送方能发送多少个TCP报文段取决于接收方的通告窗口(还需考虑拥塞窗口)大小。报文段17中客户报告的通告窗口大小为30084*64=1925376字节(窗口扩大因子为6),而在TCP报文段18中,客户的接收通告窗口大小为27317*64=1748288字节,即客户能接收的数据量变少了,这说明客户端的TCP接收缓冲区有更多的数据未被应用程序读取而留在其中了,这些数据来自TCP报文段3~16。在服务器收到TCP报文段18后,它至少还能连续发送未被确认的报文段数量为1748288/16384=106个,其中16384是成块数据的长度(见报文段1~16的length值),它小于但接近MSS规定的16396字节。

上图中的服务器每发送4个TCP报文段就传送一个PSH标志(tcpdump输出标志P)给客户端,以通知客户应用进程尽快读取数据,但这对服务器来说不是必需的,因为它知道客户的TCP接收缓冲区中还有空间(客户报告的通告窗口不为0)。

我们修改上例中客户和服务器的TCP发送和接收缓冲区大小,然后重新执行以上操作,此次tcpdump的部分输出如下:
在这里插入图片描述
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第22张图片
从SYN报文段(上图中没有列出)可以看到,客户端和服务器的窗口扩大因子都为0,因此客户端和服务器通告的窗口大小都是3072字节。因为每个成块数据的长度为1536字节,所以服务器在收到上一个成块TCP报文段的确认前最多还能再发送1个成块TCP报文段。

有些传输层协议具有带外(Out Of Band,OOB)数据的概念,用于迅速通告对方本端发生的重要事件,因此,带外数据比普通数据(也称为带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。实际应用中,带外数据的使用很少见,已知的仅有telnet、ftp等程序使用。

UDP没有实现带外数据传输,TCP也没有真正的带外数据,但TCP利用其头部的紧急指针标志和紧急指针字段,给应用提供了一种紧急方式。TCP的紧急方式利用传输普通数据的连接来传输紧急数据,这种紧急数据的含义与带外数据类似,因此也将TCP紧急数据称为带外数据。

先介绍TCP发送带外数据的过程,假设一个进程已经往某个TCP连接的发送缓冲区中写入了N字节的普通数据,并等待发送,在数据被发送前,该进程又向这个连接写入了3字节abc,其中最后一个字节为带外数据,此时,待发送的TCP报文段的头部将被设置URG标志,且紧急指针被置为指向带外数据的下一字节,紧急指针的值为带外字节相对于当前TCP段数据部分开始处的偏移值。
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第23张图片
由上图,发送端一次只能发送一个字节(字母c)作为带外数据。如果TCP模块发送完以上报文段后,又发送一个带有带外数据的报文段,则后面的带外数据会覆盖前面的带外数据,即一个TCP连接只能有一个带外数据。

TCP接收端只有在接收到紧急指针标志时才检查紧急指针,然后根据紧急指针所指的位置确定带外数据的位置,并将其读入一个特殊的缓存中,这个缓存只有1字节,称为带外缓存。如果上层应用没有及时将带外数据从带外缓存中读出,则后续的带外数据将覆盖它。

上述的带外数据接收过程是TCP模块接收带外数据的默认方式,如果我们给TCP连接设置了SO_OOBINLINE套接字选项,则带外数据将和普通数据一样被TCP模块存放在TCP接收缓冲区中,此时应用进程需要像读取普通数据一样来读取带外数据,这种情况下,socket接口提供了系统调用来识别带外数据。

以下讨论异常网络状况下(出现超时和丢包),TCP如何控制数据传输以保证其承诺的可靠服务。

TCP必须能重传超时时间内未收到确认的TCP报文段,为此,TCP模块为每个TCP报文段维护一个重传定时器,该定时器在一个TCP报文段第一次被发送时启动,如果超时时间内没有收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。我们下面通过一个例子来研究一下Linux下的超时重传策略。

在ernest-laptop上启动iperf服务器程序,然后在Kongming20上执行telnet命令登录该服务器程序,接下来telnet客户发送一些数据给服务器,然后断开服务器的网线并再次从客户端发送一些数据给服务器,同时用tcpdump抓取这一过程中客户端和服务器交换的TCP报文段,以下是具体操作:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第24张图片
iperf是一个测量网络状况的工具,-s选项表示将其作为服务器运行,它默认监听5001端口,并丢弃该端口上接收到的所有数据,相当于一个discard服务器。上述操作的部分tcpdump输出如下:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第25张图片
报文段1~3是三次握手建立连接的过程。报文段4~5是客户端发送的数据1234\r\n及服务器确认的过程。报文段6是客户端第一次发送12\r\n的过程,由于服务器的网线被断开,所以客户端无法收到报文段6的确认报文段。此后,客户端对报文段6进行了5次重传,它们是报文段7~11,这可从报文段的序号得知。此后,数据包12~23都是ARP模块的输出内容,即Kongming20查询ernest-laptop的MAC地址。

我们保留了tcpdump输出的时间戳,观察TCP报文段6~11被发送的时间间隔,它们分别为0.2、0.4、0.8、1.6、3.2秒,由此可见,一共进行了5次重传,每次超时时间都增加一倍(和TCP连接建立时的重传策略相似)。在5次重传均失败的情况下,底层的IP和ARP开始接管,直到telnet客户端放弃连接为止。

Linux有两个内核参数与TCP超时重传有关:/proc/sys/net/ipv4/tcp_retries1和/proc/sys/net/ipv4/tcp_retries2,前者指定在底层IP接管前TCP最少执行的重传次数,默认值为3,后者指定连接放弃前TCP最多可以执行的重传次数,默认值为15(一般对应15~30分钟)。上例中,TCP重传发生了5此,连接坚持的时间是15分钟(可用date命令测量)。

虽然超时会导致TCP报文段重传,但TCP报文段的重传可发生在超时前,即快速重传。

TCP模块的另一个任务是提高网络利用率,降低丢包率,保证网络资源对每条数据流的公平性,这就是所谓的拥塞控制。

TCP拥塞避免的文档是RFC 5681,其中介绍了拥塞控制的四个部分:慢启动(slow start)、拥塞避免(congestion avoidance)、快速重传(fast retransmit)、快速恢复(fast recovery)。拥塞控制算法在Linux上有多种实现,如reno算法、vegas算法、cubic算法等,它们或部分或全部地实现了以上四部分。/proc/sys/net/ipv4/tcp_congestion_control文件指示机器当前使用的拥塞控制算法。

拥塞控制的最终受控变量是发送端向网络一次连续写入(收到其中第一个数据的确认前)的数据量,我们称为SWND(Send Window,发送窗口)。报文段的数据部分的最大长度称为SMSS(Sender Maximum Segment Size,发送者最大段大小),其值一般等于MSS。

发送端需要合理地选择SWND的大小,如果SWND太小,会引起明显的网络延迟(当已发送的、未收到ACK的数据已满SWND,则需要等到确认才能再发下一个报文段),如果SWND太大,则容易导致网络拥塞(一次连续发送可以发非常多报文段)。接收方可通过其通告窗口(RWND,Receive Window)来控制发送端的SWND,但这不够,因此发送端还引入了拥塞窗口(Congestion Window,CWND)。实际的SWND是RWND和CWND中的较小者。

TCP连接建好后,CWND被设为初始值IW(Initial Window),其大小为2~4个SMSS,但新的Linux内核提高了该初始值,以减小传输滞后。此时发送端最多能发送IW字节的数据,此后发送端每收到接收端的一个确认,其CWND就按下式增加:
在这里插入图片描述
N是此次ACK确认了多少字节的数据,这样,CWND就按指数形式扩大,这就是所谓慢启动。使用慢启动是由于,TCP模块刚开始发送数据时不知道网络的实际情况,需要用一种试探的方式增加CWND的大小。

如果不施加其他手段,慢启动必然使CWND很快膨胀(可见慢启动并不慢),并最终导致网络拥塞,因此TCP拥塞控制定义了慢启动门限(slow start threshold size,ssthresh),当CWND超过该值时,TCP拥塞控制将进入拥塞避免阶段。

拥塞避免算法使得CWND按线性方式增加,从而减缓其扩大,RFC 5681中提到了以下两种实现方式:
1.每个RTT时间按式3-1计算一次新的CWND,而不论该RTT时间内发送端收到多少个确认。

2.每收到一个对新数据的确认报文段,就按下式来更新CWND:
在这里插入图片描述
对式3-2的解释:每收到一个对大小为SMSS的报文段的ACK,CWND就增加SMSSSMSS/CWND倍大小,这样,当我们收到的确认大小满一个CWND时,就增加1个SMSS大小,即慢启动阶段每发送一个CWND大小的数据(如2个SMSS大小),CWND就变为原来的二倍(即4个SMSS大小),而拥塞避免阶段每发送一个CWND大小的数据,只增加一个SMSS的大小,因此该公式使CWND按线性增加。

下图描述了慢启动和拥塞避免发生的时机和区别:
Linux高性能服务器编程 学习笔记 第三章 TCP协议详解_第26张图片
上图中,我们以SMSS为单位来显示CWND(实际上它是以字节为单位的),以次数为单位显示RTT,这是为了方便讨论问题。上图我们假设当前ssthresh大小为16个SMSS(实际的ssthresh远比这个大)。

发送端以下面的两种情况来判断拥塞发生了:
1.传输超时(TCP重传定时器溢出)。

2.在传输超时前,接收到重复的确认报文段。

当拥塞发生时(可能在慢启动阶段或拥塞避免阶段),对以上两种情况有不同的处理方式。第一种情况发生时,通过调整一些参数使连接再次进入慢启动和拥塞避免。第二种情况发生时,则使用快速重传和快速恢复。

如果发送端检测到传输超时,即上述第一种情况,它将执行重传并做如下参数调整:将ssthresh减少为当前值的一半(不同实现减少的幅度略有差异);将CWND设为一个较小值,以便重新开始慢启动。

发送端可能会接收到重复的确认报文段,如TCP报文段丢失,或接收端收到乱序的TCP报文段。拥塞避免算法需要判断当收到重复的确认报文段时,网络是否真的发生了拥塞(即报文段是否真的丢失了),具体做法是,如果发送端连续收到3个重复的确认报文段,就认为是拥塞发生了,然后它将使用快速重传和快速恢复算法来处理拥塞,过程如下:
1.当收到第3个重复的确认报文段时,将ssthresh减为当前值的一半,然后立即重传丢失的报文段,然后按下式重新设置CWND:
在这里插入图片描述
2.每次收到一个重复的确认时,设置CWND=CWND+SMSS,以便允许发送端在等待非重复的ACK时可以发送新的TCP报文段,而不是保持空闲,我们收到重复的确认就意味着接收端还在接收后面的报文段,只不过乱序了,可能只丢了一个报文段,其他报文段还能到达,因此我们应该继续发送。

3.当收到新数据的确认时,设置CWND=ssthresh。

在快速重传和快速恢复完成后,拥塞控制将恢复到拥塞避免阶段。

你可能感兴趣的:(Linux高性能服务器编程,服务器,linux,学习)