1、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数据将被截断。
- TCP传输是可靠的。TCP协议采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功。TCP协议采用超时重传机制,发送端在发送出一个TCP报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。因为TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序,重复,所以TCP协议还会对接收到的TCP报文段重排,整理,再交付给应用层。
2.TCP头部结构
TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口,目的端端口,管理TCP连接等。
2.1 TCP固定头部结构
- 16位端口号:告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。客户端一般使用系统自动选择的临时端口号,服务端一般使用知名服务端口号
- 32位序号:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节编号,在通信的第一个TCP报文段中,序号值为某个随机值ISN,确认号为0。后续的TCP报文段中序号值将被设置成ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移。
- 32位确认号:用作对另一方发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1。
- 4位头部长度:标识该TCP头部有多少个4 字节 。 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报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分,这也是TCP可靠传输的一个重要保障。
- 16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。
2.2 TCP头部选项
TCP头部的最后一个选项字段是可变长的可选信息,这部分最多包含40字节,因为TCP头部最长是60字节(包含20字节的固定部分)。
选项的第一个字段kind说明选项的类型(有的TCP选项没有后两个字段,仅包含1字节的kind字段)。第二个字段length字段占据的2字节。第三个字段info(如果有的话)是选项的具体信息。
常见的TCP选项有7种:
- kind=0是选项表结束选项。
- kind=1是空操作(nop)选项,没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍
- kind=2是最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segment Size, MSS),TCP模块通常将MSS设置为(MTU-40)字节(减去了20字节的TCP头部和20字节的IP头部)。
- kind=3是窗口扩大因子选项。只能出现在同步报文段中。
- kind=4是选择性确认(SACK)选项。它使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。
- kind=5是SACK实际工作的选项。该选项参数告诉发送方本端已经收到并不缓存的不连续的数据块,从而让发送端可以据此检查并重发丢失的数据块。
- kind=8是时间戳选项。提供了计算通信双方之间的回路时间(RTT)的方法。
3.TCP连接的建立和关闭
3.1TCP连接的建立和关闭
三次握手:
报文段1:包含SYN标志位,同步报文段,发起连接请求,初始报文序号ISN为535734930(同步报文段占了一个序号值)。
报文段2:包含SYN标志位,同步报文段,初始报文序号ISN为2159701207(同步报文段占了一个序号值)。 (收到对端的SYN,发送自己的SYN,同时发送对对端SYN的ACK)
包含ACK标志位,对报文段1进行确认,确认值为535734931,第1个同步报文段的ISN加1。
报文段3:包含ACK标志位,对报文段2进行确认,确认值为2159701208,第2个同步报文段的ISN加1。
四次挥手:
报文段4:包含FIN标志位,结束报文段。占用一个序号值。
报文段5:确认结束报文段4。实际上,仅用于确认目的的确认报文段5是可以省略的,因为结束报文段6也携带了该确认信息。确认报文段5是否出现在连接断开的过程中,取决于TCP的延迟确认特性。
报文段6:发送自己的结束报文段。
报文段7:确认结束报文段6。
3.2半关闭状态
TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭。还言之,通信的一端可以发送结束报文段给对方,告诉它本端已经完成数据的发送,但允许继续接收来自对方的数据,直到对方也发送结束报文段以关闭连接。这种状态被称为半关闭。
ps:服务器和客户端应用程序判断对方是否已经关闭连接的方法是read系统调用返回0(收到结束报文段)。
4.连接超时
【1】如果客户端访问一个距离它很远的服务器,或者由于网络繁忙,导致服务器对客户端发送处的同步报文段没有应答,此时客户端程序如果是提供可靠服务的TCP,它必然是先进行重连,如果重连仍无效,则通知应用程序连接超时。
【2】TCP重连操作次数,是由/proc/sys/net/ipv4/tcp_syn_retries(默认为5)内核变量所定义的,每次重连的超时时间都增加一倍(1s、2s、4s、8s、16s)。
5.TCP状态转移
粗虚线表示典型的服务器端连接的状态转移。粗实线表示典型的客户端连接的状态转移。
服务器:
- 服务器通过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状态。
【1】当客户端执行主动关闭时,它将向服务器发送一个结束报文段,同时连接进入FIN_WAIT_1状态。若此时客户端收到服务器专门用于确认目的的确认报文段,则连接转移至FIN_WAIT_2状态。当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这一对状态是可能发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认并进入TIME_WAIT状态。
【2】上图看到客户端从FIN_WAIT_1状态直接进入TIME_WAIT状态的一条线路(不经过FIN_WAIT_2状态),前提是处于FIN_WAIT_1状态的服务器直接收到带确认信息的结束报文段(而不是先收到确认报文段,再收到结束报文段)。
【3】处于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 。 前者指定内核能接管的孤儿连接数目;后者指定孤儿连接在内核生存的时间。
6.TIME_WAIT状态
客户端连接在收到服务器的结束报文段之后,并没有直接进入CLOSED状态,而是转移到TIME_WAIT状态。这个状态,客户端连接要等待一段长为2MSL(报文段最大生存时间)的时间,才能完全关闭。MSL是TCP报文段在网络中的最大生存时间,建议值是2min 。
TIME_WAIT状态存在的原因有两点:
- 可靠地终止TCP连接。
- 保证让迟来的TCP报文段有足够的时间被识别并丢弃。
为什么要等待2MSL?
因为TCP报文段的最大生存时间是MSL,所以坚持2MSL时间的TIME_WAIT状态能够确保网络上两个传输方向上尚未被接收到的,迟到的TCP报文段都已消失(被中转路由器丢弃)。因此,在同一个端口的新连接可以在2MSL时间之后安全地建立,而不会收到属于原来连接的应用程序的数据。
7.复位报文段
在某些特殊条件下,TCP连接的一端会向另一端发送携带RST标志的报文段,即复位报文段,以通知对方关闭连接或重新建立连接。
【1】访问不存在的端口:当客户端程序访问一个不存在的端口时,目标主机将给它发送一个复位报文段。实际上,当客户端程序向服务器的某个端口发起连接,而该端口仍被处于TIME_WAIT状态的连接所占用时,客户端程序也将收到复位报文段。
【2】异常终止连接:TCP提供了一场终止一个连接的方法,即给对方发送一个复位报文段。一旦发送一个复位报文段,发送端所有排队等待的数据都将被丢弃。应用程序可以使用socket选项SO_LINGER来发送复位报文段,以异常终止一个连接。
【3】处理半打开连接:服务器(或客户端)关闭或者异常终止了连接,而对方没有收到结束报文段(比如发生了网络故障),此时,客户端(或服务器)还维持着原来的连接,而服务器(或客户端)即使重启,也已经没有改连接的任何信息了,我们将这种状态称为半打开状态,处于这种状态的连接称为半打开连接。如果客户端(或服务器)往处于半打开状态的连接写入数据,则对方将回应一个复位报文段。
8.TCP交互数据流
TCP报文段所携带的应用程序数据按照长度分为:
交互数据:包含很少的字节,对实时性要求高
成块数据:长度通常为TCP报文段允许的最大数据长度,对传输效率要求高。
9.带外数据
【1】带外数据:用于迅速通告对方本端发生的重要事件。因此带外数据比普通数据有更高的优先级,他应该总是被立即发送,而不论发送缓冲区中是否有排队等待发送的普通数据。带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。
【2】 UDP没有实现带外数据传输,TCP也没有实现真正的带外数据。不过TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供了一种紧急方式。TCP的紧急方式利用传输普通数据的连接来传输紧急数据。这种紧急数据的含义和带外数据类似,因此后文也将TCP紧急数据称为带外数据。
【3】TCP发送外带数据的过程
假设一个进程已经往某个TCP连接的发送缓冲区写入了N字节的普通数据,并等待其发送。在数据被发送前,该进程又向这个连接写入了3字节的带外数据“abc” 。此时,待发送的TCP报文段的头部被设置URG标志,并且紧急指针被设置为指向最后一个带外数据的下一字节。
![]()
发送端一次发送的多字节的带外数据中只有最后一个字节被当作带外数据(字母c),而其他数据(字母a和b)被当作了普通数据。如果TCP模块以多个TCP报文段来发送图3-10 所示TCP发送缓冲区中的内容,则每个TCP报文段都被设置URG 标志,并且它们的紧急指针指向同一位置(数据流中带外数据的下一个位置),但只有一个TCP报文段真正携带带外数据。
【4】TCP接收带外数据的过程
TCP接收端只有再接收到紧急指针标志时才检查紧急指针,然后根据紧急指针所指向的位置确定带外数据的位置,并将它读入一个特殊的缓存中。这个缓存只有1个字节,称为带外缓存。 如果上层应用程序没有及时将带外数据从带外缓存中读走,则后续的带外数据(如果有的话)将覆盖它。上面说的是带外数据的接收过程是TCP模块接收带外数据的默认方式。如果TCP设置了SO_OOBINLINE 选项,则带外数据将和普通数据一样被TCP模块存放在TCP接收缓冲区中。此时应用程序需要像读取普通数据一样来读取带外数据。这种情况下,紧急指针可以用来指出带外数据的位置。
10.TCP超时重传
【1】TCP服务必须能够重传超时时间内未收到的TCP报文段。为此,TCP模块为每个TCP报文段都维护一个重传定时器,该定时器在TCP报文段第一次被发送时启动。如果超时时间内为接收到接收方的应答,TCP模块将重传TCP报文段并重置定时器。至于下次重传的超时时间如何选择,以及最多执行多少次重传,就是TCP的重传策略。
【2】Linux有两个重要的内核参数与TCP超时重传相关:/proc/sys/net/ipv4/tcp_retries1和/proc/sys/net/ipv4/tcp_retries2。前者指定在底层IP接管之前TCP最少执行的重传次数,默认值是3。后者指定连接放弃前TCP最多可以执行可以执行的重传次数,默认值是15(一般对应13~30min)。
11.拥塞控制
11.1拥塞控制概述
【1】拥塞控制:提高网络利用率,降低丢包率,保证网络资源对每条数据流的公平性。
【2】四个部分:
慢启动(slow start)
拥塞避免(congestion avoidance)
快速重传(fast retransmit)
快速恢复(fast recovery)
【3】算法:
reno算法
vegas算法
cubic算法
【4】
- 拥塞控制的最终受控变量是发送端向网络一次连续写入(收到其中第一个数据确认之前)的数据量,称为SWND(发送窗口)。不过,发送端最终以TCP报文段来发送数据,所以SWND限定了发送端能连续发送的TCP报文段数量。这些TCP报文段的最大长度(仅指数据部分)称为SMSS(发送者最大段大小),其值一般等于MSS。
- 发送端需要合理的选择SWND的大小。如果SWND太小,会引起明显的网络延迟;如果SWND太大,则容易导致网络拥塞。前文提到,接收方可通过其接收通告窗户(RWND)来控制发送端的SWND。发送端还引入了一个称为拥塞窗户(CWND)的状态变量。实际的SWND值是RWND和CWND中的较小者。
11.2慢启动和拥塞避免
慢启动:
【1】TCP连接好之后,CWND将被设置成初始值IW(Initial window),其大小为2~4个SMSS。此时发送端最多能发送IW字节的数据。此后发送端每收到接收端的一个确认,其CWND就按照下式增加:
CWND+=min(N, SMSS) ——————————————————–(a)
其中N是此次确认包含的之前未被确认的字节数。CWND将按照指数形式扩大,这就是所谓的慢启动。
【2】慢启动算法的理由:TCP模块刚开始发送数据时并不知道网络的实际情况,需要以一种试探的方式平滑的增加CWND的大小。
拥塞避免:
【1】如果不施加其他手段,慢启动必然使得CWDN很快膨胀(可见慢启动实际不慢)并最终导致网络拥塞。因此TCP拥塞控制中定义了另一个重要的状态变量:慢启动门限(ssthresh)。当CWDN的大小超过该值时,TCP拥塞控制进入拥塞避免阶段。
【2】拥塞避免算法使得CWDN按照线性方式增加,从而减缓其扩大。有如下两种实现方式:
每个RTT时间内按照a式计算新的CWND,而不论该RTT时间内发送端收到多少个确认。
每收到一个对新数据的确认报文段,就按照(b)来更新CWND
CWND+=SMSS*SMSS/CWND —————————————-(b)
判断拥塞发生的依据
【1】传输超时,或者说TCP重传定时器溢出
【2】接收到重复的确认报文段
对第一种情况仍然使用慢启动和拥塞避免。对第二种情况则使用快速重传和快速恢复(如果是真的发生拥塞的话)。第二种情况如果发生在重传定时器溢出之后,则也被拥塞控制当成第一种情况。
当发生第一种情况时,将执行重传并做以下调整:
ssthresh = max(FlightSize/2, 2*SMSS)
CWND <= SMSS
FlightSize是已经发送但未收到确认的字节数。
11.3快速重传和快速恢复
【1】发送端如果连续收到3个重复的确认报文段,就认为是拥塞发生了。
【2】启用快速重传和快速恢复算法来处理拥塞:
- 当收到第3个重复的确认报文段时,按照ssthresh = max(FlightSize/2, 2*SMSS)计算ssthresh ,然后立即重传丢失的报文段,并按照 CWND = ssthresh + 3*SMSS 设置CWND。
- 每次收到1个重复的确认时,设置CWND = CWND + SMSS。此时发送端可以发送新的TCP报文段(如果新的CWND允许的话)。
当收到新数据的确认时,设置CWND = ssthresh(ssthresh是新的慢启动门限值,由第一步计算得到)。
快速重传和快速恢复完成之后,拥塞控制将恢复到拥塞避免阶段