Linux 高性能服务器编程——TCP协议详解
问题聚焦:
本节从如下四个方面讨论TCP协议:
TCP头部信息:指定通信的源端端口号、目的端端口号、管理TCP连接,控制两个方向的数据流
TCP状态转移过程:TCP连接的任意一端都是一个状态机
TCP数据流:两种主要类型:交互数据流,成块数据流
TCP数据流的控制:保证可靠传输和提高网络通信质量,两个方面:超时重传,拥塞控制
1 TCP服务的特点
传输层协议:TCP协议、UDP协议
TCP协议相对于UDP协议的特点:面向连接、字节流和可靠传输。
使用TCP协议通信的双方必须先建立连接,才能开始读写数据。 双方都必须为该连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。TCP连接是全双工,即双方的数据读写可以通过一个连接进行。完成数据交换之后,通信双方都必须断开连接,以释放资源。
TCP协议的这种连接是一对一,所以不适合基于广播和多播的应用程序(UDP适合)。
当发送端应用程序连续执行多次操作时,TCP模块先将这些数据放入TCP发送缓冲区中。当TCP模块真正开始发送数据时,发送缓冲区中这些等待发送的数据可能被封装成一个或多个TCP报文段发出。因此TCP模块发送出的TCP报文段的个数和应用程序执行的写操作次数之间没有固定的数量关系。
当接收端收到了一个或多个TCP报文段后,TCP模块将它们携带的应用程序数据按照TCP报文段的序号依次放入TCP接收缓冲区中,并通知应用程序读取数据。接收端应用程序可以一次性将TCP接收缓冲区中的数据全部读出,也可以分多次读取,这取决于用户指定的应用程序读缓冲区的大小。因此应用程序执行的读操作次数和TCP模块接收到的TCP报文段个数之间也没有固定的数量关系。
综上所述,发送端执行的写操作次数和接收端执行的读操作次数之间没有任何数量关系,这就是字节流的概念:应用程序对数据的发送和接收是没有边界限制的。UDP则不然。发送端应用程序每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送之。接收端必须及时针对每一个UDP数据报执行读操作(通过recvfrom系统调用),否则就会导致丢包(这经常发生在较慢的服务器上)。并且,如果用户没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。
TCP字节流服务和UDP数据报服务的工作流程区别如下图所示:
TCP传输是可靠的:
- 发送应答机制:发送端发送的每个TCP报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功。
- 超时重传机制:发送端在发送出一个TCP报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。
- 重排,整理:TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序、重复,所以TCP协议还会对接收到的TCP报文段重排、整理,再交付给应用层。
2 TCP头部结构
如图:
16位端口号:告知主机该报文段是来自哪里(源端口号),以及传给哪个上层协议或应用程序(目的端口)。对于客户端来说,端口号常为系统自动选择。所有知名服务使用的端口号都定义在/etc/services文件中。
32位序号:一个TCP通信过程中某一个传输方向上的字节流的每个字节的编号。如某个TCP报文段传送的数据是字节流中的第1025到2048字节,那么该报文段的序号值就是ISN+1025,ISN为初始序号值。
32位确认号:用作对另一方发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1。
4位头部长度:标识TCP头部有多少个4字节。因为4位最大能表示15,所以TCP头部最长是60字节。
6位标志位:6位标志位包含如下几项:
- URG标志 :表示紧急指针是否有效。
- ACK标志 :表示确认号是否有效。
- PSH标志:提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间。(如果应用程序不将接收到的数据读走,它们就会停留在TCP接收缓冲区中)
- RST标志:表示要求对方重新建立连接。我们称携带RST标志的TCP报文段为复位报文段。
- SYN标志:表示请求建立一个连接。我们称携带SYN标志的TCP报文段为同步报文段。
- FIN标志:表示通知对方本端要关闭连接了,我们称携带FIN标志的TCP报文段为结束报文段。
16位窗口大小:TCP流量控制窗口。这里说的窗口指的是接收通告窗口。它告诉对方本端的TCP缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
16位校验和:CRC算法校验。由发送端填充。这个校验不仅包括TCP头部,也包括数据部分。
16位紧急指针:发送端向接收端发送紧急数据的方法。一个正的偏移量,它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。
TCP头部选项:最多40字节。常见7种类型。
结构:
常见的7种选项:
Demo 使用tcpdump观察TCP头部信息
需要注意的一点是,tcpdump输出的TCP报文段描述信息和TCP头部二进制信息是完全对应的,只是省略了一些不常用的信息,所以只是在这里具体分析TCP头部的二进制信息,而在后面只使用tcpdump的描述信息进行过程的分析。
首先来看一下tcpdump的描述信息的含义:
Flags [S]: 表示该TCP报文段包含SYN标识,因此它是一个同步报文段。如果TCP报文段包含其他标识,则tcpdump也会将该标志的首字母显示在Flags后的方括号中。
seg:序号值。
win:接收通告窗口的大小。这是一个同步报文段,所以win值反映的是实际的接收通告窗口的大小。
options:TCP选项字段。mss(发送端声明的最大报文长度),sackOK(发送端支持并同意使用SACK选项),TS val(发送端的时间戳),ecr(时间戳回显应答,第一个报文,所以时间戳的应答位0),nop(空选项操作),wscale(发送端使用的窗口扩大因子位6)。
接下来分析二进制信息:从第21字节开始
十六进制数 | 十进制表示 | TCP头部信息 |
0xa295 | 41621 | 源端口号 |
0x0017 | 23 | 目的端口号 |
0xd099e103 | 3499745539 | 序号 |
0x00000000 | 0 | 确认号 |
0xa | 10 | TCP头部长度为10个4字节,即40字节 |
0x002 | 设置了SYN标志位,即SYN=1 | |
0x8010 | 32792 | 接收窗口大小 |
0xfe30 | 头部检验和 | |
0x0000 | 没设置URG位,所以紧急指针无效 | |
0x0204 | 最大报文段长度选项的kind值和length值 | |
0x400c | 16396 | 最大报文长度 |
0x0402 | 允许SACK选项 | |
0x080a | 时间戳选项的kind值和length值 | |
0x026e44d9 | 40781017 | 时间戳 |
0x00000000 | 0 | 回显应答时间戳 |
0x01 | 空操作选项 | |
0x0303 | 窗口扩大因子选项的kind值和length值 | |
0x06 | 6 | 窗口扩大因子为6 |
3 TCP连接的建立和关闭
Demo 使用tcpdump观察的数据来分析TCP连接的建立和关闭
当执行telnet命令登录和退出另外一台电脑时,会触发TCP连接的的建立和关闭。抓取的包筛选出TCP连接部分的包,如下图所示(为了方便观察,并没有给出对应的二进制信息)
分析:因为整个过程并没有发生应用层数据的交换,所以TCP报文段的数据部分的长度总是0。
时序图表示如下:
TCP三次握手过程
报文段1:包含SYN标志位,同步报文段,请求发起连接,初始报文序号ISN为535734930(同步报文段占了一个序号值)。
报文段2:包含SYN标志位,同步报文段,初始报文序号ISN为2159701207(同步报文段占了一个序号值)。
包含ACK标志位,对报文段1进行确认,确认值为535734931,即第1个同步报文段的序号值加1。
报文段3:包含ACK标志位,对报文段2进行确认,确认值为2159701208,即第2个同步报文段的序号值加1。
TCP四次挥手过程
报文段4:包含FIN标志位,结束报文段。占用一个序号值。
报文段5:确认结束报文段4。实际上,仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确认报文段5是否出现在连接断开的过程中,取决于TCP的延迟确认特性。
报文段6:发送自己的结束报文段。
报文段7:确认结束报文段6。
特殊地,半关闭状态:
TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭。
半关闭状态:通信的一段发送结束报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送结束报文段以关闭连接。
判断对方是否已经关闭连接的方法:read系统调用返回0。
socket网络编程接口通过shudown函数提供了对半关闭的支持。
连接超时
对于提供可靠的TCP服务来说,当对方没有应答时,它必然先进行重连,如果重连仍然无效,则通知应用程序连接超时。
TCP模块一共执行多少次重连操作,这是由/proc/sys/net/ipv4/tcp_syn_retries(默认为5)内核变量所定义的,每次重连的超时时间都增加一倍(1s、2s、4s、8s和16s)。
4 TCP状态转移
下图是TCP完整的状态转移图。描绘了所有的TCP状态以及可能的状态转换。
- CLOSED是一个假想的起始点,并不是一个实际的状态。
- 粗虚线表示典型的服务器端连接的状态转移。
- 粗实线表示典型的客户端连接的状态转移。
服务器通过listen系统调用进入LISTEN 状态,被动等待客户端连接,因此执行的是所谓的被动打开。服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中,并向客户端发送带SYN标志的确认报文段。此时该连接处于SYN_RCVD状态。如果服务器成功的接收到客户端发送的确认报文段,则该连接转移到ESTABLISHED状态。ESTABLISHED状态是连接双方能够进行双向数据传输的状态。
当客户端主动关闭连接时(通过close或者shutdown系统调用向服务器发送结束报文段),服务器通过返回确认报文段使连接进入CLOSE_WAIT状态。这个状态的含义很明确:等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接后,也会立即给客户端发送一个结束报文段来关闭连接。这将使连接转移到LAST_ACK状态,以等待客户端结束报文段的最后一次确认。一旦确认完成,连接就彻底关闭了。
客户端通过connect系统调用主动与服务器建立连接。connect系统调用首先给服务器发送一个同步报文段,使连接转移到SYN_SENT状态。此后,connect系统调用可能因为如下两个原因失败返回:
- 如果connect连接的目标端口不存在(未被任何进程监听),或者该端口仍被处于TIME_WAIT状态的连接所占用,则服务器将给客户端发送一个复位报文段,connect调用失败。
- 如果目标端口存在,但connect在超时时间内未收到服务器的确认报文段,则connect调用失败。
connect调用失败将使连接立即返回到初始的CLOSED状态。如果客户端成功收到服务器的同步报文段和确认,则connect调用成功返回,连接转移到ESTABLISHED状态。
当客户端执行主动关闭时,它将向服务器发送一个结束报文段,同时连接进入FIN_WAIT_1状态。若此时客户端收到服务器专门用于确认目的的确认报文段,则连接转移至FIN_WAIT_2状态。当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这一对状态是可能发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认并进入TIME_WAIT状态。
上图看到客户端从FIN_WAIT_1状态直接进入TIME_WAIT状态的一条线路(不经过FIN_WAIT_2状态),前提是处于FIN_WAIT_1状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文段,再收到结束报文段)。如图3-6中的服务器不发送报文段5。
前面说过,处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段,才能转移到TIME_WAIT状态,否则它将一直停留在这个状态。如果不是为了在半关闭状态下继续接收数据,连接长时间的停留在FIN_WAIT_2状态并无益处。连接停留在FIN_WAIT_2状态的情况可能发生在:客户执行半关闭后,未等服务器关闭连接就强行退出了。此时客户端连接由内核来接管,可称为孤儿连接。Linux为了防止孤儿连接长时间存留在内核中,定义了两个内核变量:/proc/sys/net/ipv4/tcp_max_orphans 和/proc/sys/net/ipv4/tcp_fin_timeout 。 前者指定内核能接管的孤儿连接数目;后者指定孤儿连接在内核生存的时间。
TIME_WAIT状态:
在主动关闭流程中,客户端接收到服务器的结束报文段之后,并没有直接进入CLOSED状态,而是转移到TIME_WAIT状态。
在这个状态要等待2MSL(MSL, 报文段最大生存时间)的时间,才能完全关闭。原因:
- 可靠地终止TCP连接:图3-9中用于确认服务器结束报文段6的TCP报文段7丢失,那么服务器将重发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文段)。否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是一个像TCP报文段7那样的确认报文段。
- 保证让迟来的TCP报文段有足够的时间被识别并丢弃:在Linux系统上,一个TCP端口不能被同时打开多次(两次及以上)。当一个TCP连接处于TIME_WAIT状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接。反过来思考,如果不存在TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,是指它们具有相同的IP地址和端口号)。这个新的、和原来相似的连接被称为原来的连接的化身。新的化身可能接收到属于原来的连接、携带应用程序数据的TCP报文段(迟到的报文段),这显然是不应该发生的。这就是TIME_WAIT状态存在的第二个原因。
TIME_WAIT状态持续等待2MSL原因:
因为TCP报文段的最大生存时间是MSL,所以坚持2MSL时间的TIME_WAIT状态能够确保网络上两个传输方向上尚未被接收到的、迟到的TCP报文段都已经消失(被中转路由器丢弃)。因此,一个连接的新的化身可以在2MSL时间之后安全建立,而绝对不会接收到属于原来连接的应用程序数据,这就是TIME_WAIT状态持续2MSL时间的原因。
有时候当程序退出后,我们希望能够立即重启它,但是由于处于TIME_WAIT状态的连接还占用着端口,程序将无法启动(直到2MSL超时时间结束)。对于客户端程序来说,我们通常不用担心上面描述的重启问题,因为客户端程序一般使用系统自动分配的临时端口号建立连接,而由于随机性,临时端口一般和程序上一次使用的端口号(还处于TIME_WAIT状态的那个连接使用的端口号)不同,所以客户端程序一般可以立即重启。但是如果服务器主动关闭连接后异常终止,则因为它总是使用同一个知名服务器端口号,所以连接的TIME_WAIT状态将导致它不能立即重启。不过,我们可以通过socket选项SO_REUSEADDR 来强制进程立即使用处于TIME_WAIT状态的连接占用的端口。
在某些特殊条件下,TCP连接的一端会向另一端发送携带RST标志的报文段,即复位报文段,以通知对方关闭连接或重新建立连接。
三种产生复位报文段的3种情况:
- 访问不存在的端口:当客户端程序访问一个不存在的端口时,目标主机将给它发送一个复位报文段。实际上,当客户端程序向服务器的某个端口发起连接,而该端口仍被处于TIME_WAIT状态的连接所占用时,客户端程序也将收到复位报文段。
- 异常终止连接:TCP提供了一场终止一个连接的方法,即给对方发送一个复位报文段。一旦发送一个复位报文段,发送端所有排队等待的数据都将被丢弃。应用程序可以使用socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。
- 处理半打开连接:服务器(或客户端)关闭或者异常终止了连接,而对方没有收到结束报文段(比如发生了网络故障),此时,客户端(或服务器)还维持着原来的连接,而服务器(或客户端)即使重启,也已经没有改连接的任何信息了,我们将这种状态称为半打开状态,处于这种状态的连接称为半打开连接。如果客户端(或服务器)往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。
5 TCP数据流
在前面的小节中,我们讨论了TCP的连接及其状态,从本节开始,我们开始讨论通过TCP连接交换的应用程序数据。
按照数据长度分为两种:
- 交互数据(实时性):
- 成块数据(传输效率):
- 当传输大量大块数据的时候,发送方会连续发送多个TCP报文段,接收方可以一次确认所有这些报文段。
- 服务器每发送4个TCP报文段就传送一个PSH标志给客户端,以通知客户端的应用程序尽快读取数据。
6 带外数据
UDP没有实现带外数据传输,TCP也没有实现真正的带外数据。不过TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种紧急方式。TCP的紧急方式利用传输普通数据的连接来传输紧急数据。这种紧急数据的含义和带外数据类似,因此后文也将TCP紧急数据称为带外数据。
假设一个进程已经往某个TCP连接的发送缓冲区写入了N字节的普通数据,并等待其发送。在数据被发送前,该进程又向这个连接写入了3字节的带外数据“abc" 。此时,待发送的TCP报文段的头部被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一字节。
有图3-10 可见,发送端一次发送的多字节的带外数据中只有最后一个字节被当作带外数据(字母c),而其他数据(字母a和b)被当作了普通数据。如果TCP模块以多个TCP报文段来发送图3-10 所示TCP发送缓冲区中的内容,则每个TCP报文段都被设置URG 标志,并且它们的紧急指针指向同一位置(数据流中带外数据的下一个位置),但只有一个TCP报文段真正携带带外数据。
TCP接收端只有再接收到紧急指针标志时才检查紧急指针,然后根据紧急指针所指向的位置确定带外数据的位置,并将它读入一个特殊的缓存中。这个缓存只有1个字节,称为带外缓存。 如果上层应用程序没有及时将带外数据从带外缓存中读走,则后续的带外数据(如果有的话)将覆盖它。
上面说的是带外数据的接收过程是TCP模块接收带外数据的默认方式。如果我们给TCP设置了SO_OOBINLINE 选项,则带外数据将和普通数据一样被TCP模块存放在TCP接收缓冲区中。此时应用程序需要像读取普通数据一样来读取带外数据。
7 TCP超时重传
异常网络状况下,TCP控制数据传输以保证其可靠服务的措施。
Point:
- TCP模块为每个TCP报文段都维护一个重传定时器,该定时器在TCP报文段第一次被发送时启动。
- 如果超时时间内未收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。
- 每次重传超时时间增加一倍。
- Linux内核有两个重要的内核参数与TCP超时重传相关:/proc/sys/net/ipv4/tcp_retries1和proc/sys/net/ipv4/tcp_retries2。 前者指定在底层IP接管之前TCP最少执行的重传次数,默认值为3 。后者指定连接放弃前TCP最多可以执行的重传次数,默认值是15(一般对应13-30min)。
- TCP报文段的重传可以发生在超时之前,即快速重传,下一节讨论。
8 拥塞控制
目的:提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。
四种拥塞控制:
- 慢启动(slow start)
- 拥塞控制(congestion avoidance)
- 快速重传(fast retransmit)
- 快速恢复(fast recovery)
- reno算法
- vegas算法
- cubic算法
SWND限定了连续发送的TCP报文段数量。
TCP报文段的最大长度(仅指数据部分)称为SMSS。
影响:发送端需要合理地选择SWND的大小。如果SWND太小,会引起明显的网络延迟:反之,如果SWND太大,则容易导致网络拥塞。
窗口大小:接收方通过比较两个值:RWND(接收通告窗口)和拥塞窗口(CWND),取其较小值,作为SWND的值。
如图所示:
关于TCP还剩下拥塞控制的详细介绍,将在后续作为专题仔细学习。
To be continued ......
参考资料:
《Linux高性能服务器编程》
from:http://blog.csdn.net/zs634134578/article/details/19346609