TCP协议详解——Linux高性能服务器

TCP协议较IP协议更为接近应用层,在应用程序中有更强的可操作性。
传输层协议主要有两个:TCP、UDP。

TCP服务的特点

面向连接、字节流和可靠传输
面向连接:使用TCP协议通信双方要先建立连接才能开始数据的读写。连接双方都要为连接分配必要的内核资源,以管理连接的状态和连接上数据的传输。完成数据交换后通信双方必须断开连接以释放资源。
TCP连接是全双工的,双方的数据读写可以通过一个连接进行。
TCP协议的连接是一对一的,基于广播和多播的应用程序不能使用TCP服务。
字节流:应用程序对数据的发送和接收没有边界限制。
可靠的:TCP协议采用发送应答机制,发送端发送的TCP报文段都必须得接收到应答才认为这个报文传送成功;采用超时重传机制,发送端在发送一个报文后启动定时器,一定时间内没收到应答将重发报文段;TCP报文段最终以IP数据报发送【IP数据报可能乱序、重复】,TCP协议对接收到的TCP报文段重排、整理后才交给应用层。
与UDP区别

  • UDP适合多播和广播发送端
  • 应用程序每执行一次写操作,UDP模块就将其封装成一个UDP数据报并发送。接收端必须及时对每一个UDP数据报执行读操作,否则就会丢包(较慢服务器)且如果用户没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。
  • UDP与IP协议一样,提供不可靠服务,需要上层协议进行数据确认和超时重传

TCP字节流服务:

UDP数据报服务:

TCP头部结构

用于指定通信的源端端口号、目的端口号,管理TCP连接,控制两个方向的数据流。

固定头部


16位端口号:告知主机该报文段的源端口以及目的端口。进行TCP通信时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知名服务端口号
32位序号:一次TCP通信过程中某一个传输方向上的字节流的每个字节的编号。假设主机A和主机B进行TCP通信,A发送给B的第一个TCP报文段中,序号值被系统初始化为某个随机值ISN (初始序号值)。那么在该传输方向上,后续的TCP报文段中序号值将被系统设置成ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移。
32位确认号:用作对另一方发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1.假设主机A和B进行TCP通信,那么A发送出的TCP报文段不仅携带自己的序号,而且包含对B发送来的TCP报文段的确认号。
4位头部长度:标识TCP头部有多少个4字节。 因为4位最大能表示15,所以TCP头部最长是60字节。
6位标志位包含如下几项:

  • URG标志,表示紧急指针是否有效。
  • ACK标志,表示确认号是否有效。称携带ACK标志的TCP报文段为确认报文段。
  • PSH标志,提示接收端应用程序应该立即从TCP接收缓冲区中读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在TCP接收缓冲区中)。
  • RST标志,表示要求对方重新建立连接。称携带RST标志的TCP报文段为复位报文段。
  • SYN标志,表示请求建立一个连接。称携带SYN标志的TCP报文段为同步报文段
  • FIN标志,表示通知对方本端要关闭连接了。称携带FIN标志的TCP报文段为结束报文段

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

16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。这个校验不仅包括TCP头部,也包括数据部分。这也是TCP可靠传输的一个重要保障
16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。

头部选项

TCP头部最后的选项字段是可变长的可选信息,这里最多包含40字节【60字节-20字节的固定部分】
TCP头部选项结构:

kind:说明选项的类型
length:指定该选项的总长度(包括kind字段&length字段的两字节)
info:选项的具体信息
常见的7种TCP选项:

  • kind=0:选项表结束选项
  • kind=1:空操作的选项,一般用于将TCP总长度填充为4的整数倍
  • kind=2:最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(MSS). TCP模块通常将MSS设置为(MTU-40)字节(这40字节包括20字节的TCP头部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不包含选项字段),从而避免本机发生IP分片。对以太网而言,MSS值是1460 (1500-40)字节。
  • kind=3:窗口扩大因子选项。TCP连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因子。在TCP的头部中,接收通告窗口大小是用16位表示的,故最大为65535字节,但实际上TCP模块允许的接收通告窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子解决了这个问题。窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段头部的接收通告窗口大小就是该TCP报文段的实际接收通告窗口大小。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。
  • kind=4选择性确认(SACK)选项。TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生的,它使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。
  • kind=5:SACK实际工作的选项。该选项的参数告诉发送方本端已经收到并缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。每个块边沿参数包含一个4字节的序号。其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不连续块的最后一个数据的序号的下一个序号。这样一对参数之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块。
  • kind=8是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(RTT) 的方法,从而为TCP流量控制提供重要信息。

TCP连接的建立和关闭

客户端上执行登录服务器端口
TCP连接的建立和关闭时序图:
ernest-laptop【客户端】、Kongming20【服务端】

TCP三次握手(建立连接的三个步骤)

  1. 第一个报文段含SYN【为同步报文段】,客户端向服务器发起连接请求,同时含ISN值为535734930的序号。
  2. 第二个报文段也是同步报文段,表示服务器同意与客户端建立连接,同时发送自己的ISN值2159701207的序号。对第一个报文段进行确认(确认值535734930),第一个同步报文段序号值加1【同步报文段即使不携带任何应用程序也会占用一个序号值】
  3. 第三个报文段是客户端对第二个同步报文段的确认

关闭连接:
第4个TCP报文段包含FIN标志【是结束报文段】,客户端要求关闭连接。结束报文段占用一个序号值。
服务器用TCP报文段5来确认该结束报文段。紧接着服务器发送自己的结束报文段6, 客户端则用报文段7给予确认。
仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确认报文段5是否出现在连接断开的过程中,取决于TCP的延迟确认特性。在连接的关闭过程中,因为客户端先发送结束报文段,主动关闭,而称服务器被动关闭。

一般而言,TCP连接是由客户端发起,并通过三次握手建立(特殊情况:同时打开)TCP连接的关闭过程相对复杂一些。可能是客户端执行主动关闭,比如前面的例子:也可能是服务器执行主动关闭,比如服务器程序被中断而强制关闭连接(特殊情况:同时关闭)

扫描二维码关注公众号,回复: 9358247 查看本文章
半关闭状态

TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭。换言之,通信的一端可以发送结束报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送结束报文段以关闭连接。TCP 连接的这种状态称为半关闭状态。

服务器和客户端应用程序判断对方是否已经关闭连接的方法是:read系统调用返回0 (收到结束报文段)。当然,Linux 还提供其他检测连接是否被对方关闭
socket网络编程接口通过shutdown函数提供了对半关闭的支持。
注意:使用半关闭的应用程序很少见。

连接超时

若客户端访问一个距离很远的服务器,或是因网络繁忙使服务器对客户端发送的同步报文段没有应答,客户端程序会先进行重连,多次重连无效后通知应用程序连接超时。

TCP状态转移

TCP连接的任一端都是状态机,在TCP连接从建立到断开过程中状态机会经历不同状态变化。当前状态可用netstata命令查看。状态转移图:

图中虚线为典型服务端连接的状态转移;实线为典型客户端连接的状态转移。这里的CLOSED是假想的起始点。
过程

  • 服务器的典型状态转移过程
    服务器通过listen系统调用进入LISTEN状态,被动等待客户端连接,因此执行的是所谓的被动打开。服务器一旦监听到某个连接请求(收到同步报文段),就将该连接放入内核等待队列中,并向客户端发送带SYN标志的确认报文段。此时该连接处于SYN_RCVD状态。如果服务器成功地接收到客户端发送回的确认报文段,则该连接转移到ESTABLISHED状态。ESTABLISHED状态是连接双方能够进行双向数据传输的状态。
    客户端主动关闭连接时( 通过close/shutdown系统调用向服务器发送结束报文段),服务器通过返回确认报文段使连接进人CLOSE_WAIT状态。这个状态的含义:等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接后,也会立即给客户端发送一个结束报文段来关闭连接。这将使连接转移到LAST_ACK状态,以等待客户端对结束报文段的最后一次确认。一旦确认完成,连接就彻底关闭了。
  • 客户端的典型状态转移过程
    客户端通过conneet系统调用主动与服务器建立连接。conneet 系统调用首先给服务器发送一个同步报文段,使连接转移到SYN_ SENT状态。此后,connet 系统调用可能因为两个原因失败返回【1.conneet连接的目标端口不存在(未被任何进程监听),或者该端口仍被处于TIME_WAIT状态的连接所占用,则服务器将给客户端发送一个复位报文段,connect 调用失败;2.目标端口存在,但coonect在超时时间内未收到服务器的确认报文段,则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状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文段,再收到结束报文段)。
    处于FIN_WAIT_2状态的客户端需要等待服务器发送结束报文段,才能转移至TIME_WAIT状态,否则它将一直停留在这个状态。如果不是为了在半关闭状态下继续接收数据,连接长时间地停留在FIN_ WAIT_2状态并无益处。
    连接停留在FIN_WAIT_2状态的情况可能发生:客户端执行半关闭后,未等服务器关闭连接就强行退出了。此时客户端连接由内核来接管,可称之为孤儿连接( 和孤儿进程类似)。Linux 为了防止孤儿连接长时间存留在内核中,定义了两个内核变量: tcp_max_orphans和tcp_fin_timeout. 前者指定内核能接管的孤儿连接数目,后者指定孤儿连接在内核中生存的时间。
    对应TCP连接断开的客户端、服务器状态转移图:
TIME_WAIT状态

客户端连接在收到服务器的结束报文段后并没有直接CLOSED,而是转移到TIME_WAIT状态。
在这个状态,客户端连接要等待一段长为2MSL (Maximum Segment Life,报文段最大生存时间)的时间,才能完全关闭。
存在原因

  • 可靠地终止TCP连接。如果上图中用于确认服务器结束报文段6的报文段7丢失,服务器会重发结束报文段,客户端要停留以处理重复收到的结束报文段。负责客户端会回复服务器一个复位报文段。
  • 保证让迟来的TCP报文段有足够时间被识别并丢弃。如果没有该状态,应用程序可以立刻建立一个与刚关闭连接相似的连接(IP地址、端口号),被称为原来连接的化身。这化身可能收到属于原来连接的迟来的TCP报文段。

TCP报文段最大生存时间为MSL,在2MSL时间后网络上两个传输方向报文段都消失(中转服务器丢弃),这时可以安全的建立新的连接。
避免TIME_WAIT状态情况:程序退出后想立即重启【如果TIME_WAIT状态连接占用端口,程序将无法在2MSL内启动】。可以用socket选项REUSEADDR强制进程立即使用处于TIME_WAIT状态的连接占用的端口。

复位报文段

某些情况下,TCP连接一端会向另一端发送携带RSL标志的报文段【复位报文段】,通知对方关闭连接或重新建立连接。
产生复位报文段的三种情况:

访问不存在的端口

客户端访问一个不存在的端口时,目标主机返回一个复位报文段。收到复位报文段不能回应,只能关闭连接/重新连接。
当客户端程序向服务器某个端口发起连接,而该端口被处于TIME_WAIT状态连接占有时,客户端程序也会收到复位报文段。

异常终止连接

TCP提供了异常终止一个连接的方法,即给对方发送一-个复位报文段。一旦发送了复位报文段,发送端所有排队等待发送的数据都将被丢弃。
应用程序可以使用socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。

处理半打开连接

服务器关闭或者异常终止了连接,而对方没有接收到结束报文段(eg:发生了网络故障),此时,客户端还维持着原来的连接,而服务器即使重启,也已经没有该连接的任何信息了。我们将这种状态称为半打开状态,处于这种状态的连接称为半打开连接。如果客户端往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。

TCP交互数据流

TCP报文段所携带的应用程序数据按照长度分为两种:交互数据和成块数据。交互数据仅包含很少的字节。使用交互数据的应用程序(或协议)对实时性要求高,比如telnet、ssh等。成块数据的长度则通常为TCP报文段允许的最大数据长度。使用成块数据的应用程序(或协议)对传输效率要求高,比如ftp。

必如此了。广域网上的交互数据流可能经受很大的延迟,并且,携带交互数据的微小TCP报文段数量一般很多(一个按键输人就导致一个TCP报文段),这些因素都可能导致拥塞发生。解决方法:Nagle算法。
Nagle算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发送其他TCP报文段。并且,发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时以一个TCP报文段将它们全部发出。这样就极大地减少了网络上的微小TCP报文段的数量。该算法的另一个优点在于其自适应性:确认到达得越快,数据也就发送得越快。

TCP成块数据流

用FTP协议传输一大文件。当传输大量大块数据的时候,发送方会连续发送多个TCP报文段,接收方可以一次确认所有这些报文段。那么发送方在收到上一次确认后,能连续发送多少个TCP报文段呢?这是由接收通告窗口(含拥塞窗口)的大小决定的。
服务器每发送4个TCP报文段就传送一个PSH标志给客户端,以通知客户端的应用程序尽快读取数据。不过这对服务器来说显然不是必需的,因为它知道客户端的TCP接收缓冲区中还有空闲空间( 接收通告窗口大小不为0)。

带外数据

有些传输层协议具有带外(OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据(带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据。
带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。实际应用中,带外数据的使用很少见,已知的仅有telnet、ftp 等远程非活跃程序。
UDP没有实现带外数据传输,TCP也没有真正的带外数据。不过TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种紧急方式。TCP 的紧急方式利用传输普通数据的连接来传输紧急数据。这种紧急数据含义与带外数据类似。
TCP发送带外数据的过程。假设一个进程已经往某个TCP连接的发送缓冲区中写入了N字节的普通数据,并等待其发送。在数据被发送前,该进程又向这个连接写了3字节的带外数据“abc".此时,待发送的TCP报文段的头部将被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一字节(进一步减去当前TCP报文段的序号值得到其头部中的紧急偏移值),如图:

发送端发送多字节带外数据时仅最后一个字节被当作带外数据。
如果TCP模块以多个TCP报文段来发送图中TCP发送缓冲区中的内容,则每个TCP报文段都将设置URG标志,并且它们的紧急指针指向同一个位置( 数据流中带外数据的下一个位置),但只有一个TCP报文段真正携带带外数据
TCP接收带外数据的过程:TCP 接收端只有在接收到紧急指针标志时才检查紧急指针,然后根据紧急指针所指的位置确定带外数据的位置,并将它读入一个特殊的缓存中。这个缓存只有1字节,称为带外缓存。如果上层应用程序没有及时将带外数据从带外缓存中读出,则后续的带外数据( 如果有)将覆盖它。
如果给TCP连接设置了SO_OOBINLINE选项,则带外数据将和普通数据一样被TCP模块存放在TCP接收缓冲区中。此时应用程序需要像读取普通数据一样来读取带外数据。这种情况下如何区分带外数据和普通数据?紧急指针可以用来指出带外数据的位置,socket编程接口也提供了系统调用来识别带外数据。

TCP超时重传

为了确保TCP服务能够重传超时时间内未收到确认的TCP报文段。TCP模块为每个TCP报文段都维护一个重传定时器,该定时器在TCP报文段第一次被发送时启动。如果超时时间内未收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就是TCP的重传策略。
每次重传超时时间都会增加一倍(与TCP超时重连一样)。Linux有两个重要的内核参数与TCP超时重传相关:tep_retries1和tcp_retries2。前者指定在底层IP接管之前TCP最少执行的重传次数,默认值是3。后者指定连接放弃前TCP最多可以执行的重传次数,默认值是15。 在多次重传均失败的情况下,底层IP和ARP开始接管直到客户端放弃连接为止。

拥塞控制

概述

TCP模块还有一个重要的任务, 就是提高网络利用率,降低丢包率,并保证网络资源对每条数据流的公平性。这就是所谓的拥塞控制。
拥塞控制的最终受控变量是发送端向网络一次连续写人(收到其中第一个数据的确认之前)的数据量,我们称为SWND (发送窗口)。不过,发送端最终以TCP报文段来发送数据,所以SWND限定了发送端能连续发送的TCP报文段数量。这些TCP报文段的最大长度(仅数据部分)称为SMSS (发送者最大段大小),其值一般等于MSS。
发送端需要合理地选择SWND的大小。如果SWND太小,会引起明显的网络延迟;如果SWND太大,则容易导致网络拥塞。
接收方可通过其接收通告窗口(RWND)来控制发送端的SWND.但这显然不够,所以发送端引入了一个拥塞窗口(Congestion Window, CWND)的状态变量。实际的SWND值是RWND和CWND中的较小者。拥塞控制的输入和输出图:(是一个闭环反馈控制)

慢启动和拥塞避免

慢启动
TCP连接建立好之后,CWND将被设置成==初始值IW ,其大小为2~4个SMSS【新的Linux内核提高了该初始值,以减小传输滞后】此时发送端最多能发送IW字节的数据。此后发送端每收到接收端的一个确认,其CWND就按照CWND+= min (N, SMSS)==增加。
N——此次确认中包含的之前未被确认的字节数。
CWND将按照指数形式扩大,这就是所谓的慢启动。慢启动算法的理由是,TCP模块刚开始发送数据时并不知道网络的实际情况,需要用一种试探的方式平滑地增加CWND的大小。
如果不施加其他手段,慢启动必然使得CWND很快膨胀并最终导致网络拥塞。因此TCP拥塞控制中定义了另一个重要的状态变量:慢启动门限(ssthresh)。当CWND的大小超过该值时,TCP 拥塞控制将进入拥塞避免阶段。
拥塞避免
拥塞避免算法使得CWND按照线性方式增加,从而减缓其扩大。实现方式:

  • 每个RTT时间内按照式CWND+= min (N, SMSS)计算新的CWND,而不论该RTT时间内发送端收到多少个确认。
  • 每收到对新数据的确认报文段,就按照CWND+=SMSS*SMSS/CWND来更新CWND

拥塞发生时(慢启动阶段/拥塞避免阶段)拥塞控制的行为。
发送端判断拥塞发生的依据:

  • 传输超时,或者说TCP重传定时器溢出。
  • 接收到重复的确认报文段。

对第一种情况仍然使用慢启动和拥塞避免。对第二种情况则使用快速重传和快速恢复。
注意:第二种情况如果发生在重传定时器溢出之后,则也被拥塞控制当成第一种情况

如果发送端检测到拥塞发生是由于传输超时,即第1种情况,那么它将执行重传并做以下调整:
ssthresh=max( FlightSize/2, 2*SMSS)
FlightSize——已经发送但未收到确认的字节数。
调整后,CWMD将小于SMSS,那么也必然小于新的慢启动门限值ssthresh,它一定不小于SMSS的2倍,故而拥塞控制再次进入慢启动阶段。

快速重传和快速恢复

在很多情况下,发送端都可能接收到重复的确认报文段,比如TCP报文段丢失,或者接收端收到乱序TCP报文段并重排之等。
拥塞控制算法需要判断当收到重复的确认报文段时,网络是否真的发生了拥塞,或者说TCP报文段是否真的丢失了。
具体做法:发送端如果连续收到3个重复的确认报文段,就认为是拥塞发生了。它会启用快速重传和快速恢复算法来处理拥塞,过程:

  1. 当收到第3个重复的确认报文段时,按照【ssthresh=max ( FlightSize/2, 2SMSS)】计算sthresh,然后立即重传丢失的报文段,并按照【CWND=ssthresh+3SMSS】设置CWND
  2. 每次收到1个重复的确认,设置CWND= CWND+SMSS。此时发送端可以发送新的TCP报文段( 如果新的CWND允许的话)。
  3. 当收到新数据的确认时,设置CWND= stresh。

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

发布了48 篇原创文章 · 获赞 4 · 访问量 4094

猜你喜欢

转载自blog.csdn.net/qq_41403559/article/details/104447037
今日推荐