【读书笔记】Linux高性能服务器编程(第一篇 第三章)

第三章 TCP协议详解

TCP头部信息:TCP头部信息出现在每个TCP报文段中,用于指定通信的源端端口号,目的端端口号,管理TCP连接,控制两个方向的数据流。

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

TCP数据流:通过分析TCP数据流,我们就可以从网络应用程序外部来了解应用层协议和通信双方交换的应用程序数据。

TCP数据流的控制:为了保证可靠传输和提高网络通信质量,内核需要对TCP数据流进行控制(超时重传 和 拥塞控制)。


3.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数据报执行读操作,否则就会丢包(发生在较慢的服务器上)

并且如果用户没有指定足够的应用程序缓冲区来读取UDP数据,则UDP数据将被截断。

两者区别如图:



TCP传输时可靠的。

1.TCP协议采用发送应答机制,即发送端发送的每个TCP报文段都必须得到接收方的应答,才认为这个TCP报文段传输成功。

2.TCP协议采用超时重传机制,发送端在发送出一个TCP报文段之后启动定时器,如果在定时时间内未收到应答,它将重发该报文段。

3.因为TCP报文段最终是以IP数据报发送的,而IP数据报到达接收端可能乱序,重复,所以TCP协议还会对接收到的TCP报文段重排,整理,再交付给应用层。 

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

    

3.2 TCP头部结构

3.2.1 TCP固定头部结构

如图:


16位端口号:告知主机该报文段是来自哪里(源端口)以及传给哪个上层协议或应用程序(目的端口)的。客户端一般使用系统自动选择的临时端口号,服务端一般使用知名服务端口号(/etc/services 文件中可查看)

32位序号:一次TCP通信(从TCP连接建立到断开)过程中某一个传输方向上的字节流的每个字节编号,在通信的第一个TCP报文段中,序号值为某个随机值ISN,确认号为0。(例如:某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是ISN+1025。另一个传输方向同理)。

32位确认号:用作对另一方发送来的TCP报文段的响应。其值是收到的TCP报文段的序号值加1。

4位头部长度:标识该TCP头部有多少个4 字节 。 TCP头部最长是60字节。

6位标志位包含如下几项:

URG标志:表示紧急指针是否有效。

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

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

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

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

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

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

16位校验和:由发送端填充,接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意,这个校验不仅包括TCP头部,也包括数据部分,这也是TCP可靠传输的一个重要保障。

16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为紧急偏移。TCP的紧急指针是发送端向接收端发送紧急数据的方法。


3.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是最大报文段长度选项。

kind=3是窗口扩大因子选项。

kind=4是选择性确认(SACK)选项。

kind=5是SACK实际工作的选项。

kind=8是时间戳选项。

 

分析tcpdump抓取的数据包:


Flag[S]表示该TCP报文段包含SYN标志,是一个同步报文段。

若含有其他标志则显示在方括号中。

seq是序号值,这是传输方向上的第一个报文段,所以没有针对对方发送来的TCP报文段的确认值。

win是接收通告窗口的大小。因为这是一个同步报文段,所以win值反映的是实际的接收通告窗口大小。

 

3.3 TCP连接的建立和关闭

如图:


第1个TCP报文段包含SYN标志,因此它是一个同步报文段,即ernest-laptop(客户端)向kongming20(服务器)发起连接请求。同时,该同步报文段包含一个ISN值为535734930的序号。

第2个TCP报文段也是同步报文段,表示kongming20同意与ernest-laptop建立连接。同时它发送自己的ISN值为2159701207的序号,并对第一个同步报文段进行确认。确认值是535734931

 

3.3.2 半关闭状态

TCP连接是全双工的,所以它允许两个方向的数据传输被独立关闭。

 

3.4 TCP状态转移

TCP状态转移图:


图中的粗虚线表示典型的服务器端连接的状态转移;粗实线表示典型的客户端连接的状态转移。CLOSED是一个假想的起始点,不是一个实际的状态。


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

如果服务器成功地接收到客户端发送回的确认报文段,则该连接转移到ESTABLISHED状态。

ESTABLISHED状态是连接双方能够进行双向数据传输的状态。

 

当客户端主动关闭连接时,服务器通过返回确认报文段使连接进入CLOSED_WAIT状态。这个状态的含义是:等待服务器应用程序关闭连接。通常,服务器检测到客户端关闭连接后,也会立即给客户端发送一个结束报文段来关闭连接。

这时连接转移到LAST_ACK状态,以等待客户端对结束报文段的最后一次确认。一旦完成,连接就彻底关闭。


客户端通过connect系统调用主动与服务器建立连接。Connect系统调用首先给服务器发送一个同步报文段,使连接转移到SYN_SENT状态。此后,connect系统调用可能因为如下两个原因失败返回:

1.如果connect连接的目标端口不存在(未被任何进程监听),或者该端口仍处于TIME_WAIT状态的连接所占用,则服务器将给客户端发送一个复位报文段,connect调用失败。

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

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

当客户端执行主动关闭时,它将向服务器发送一个结束报文段,同时连接进入FIN_WAIT_1状态。若此时客户端收到服务器专门用于确认目的的确认报文段,则连接转移至FIN_WAIT_2状态。

当客户端处于FIN_WAIT_2状态时,服务器处于CLOSE_WAIT状态,这一对状态时可能发生半关闭的状态。此时如果服务器也关闭连接(发送结束报文段),则客户端将给予确认进入TIME_WAIT状态。

 

连接停留在FIN_WAIT_2状态的情况可能发生在:客户端执行半关闭后,未等服务器关闭连接就强行退出了。

此时客户端连接由内核来接管,可称之为孤儿连接(和孤儿进程类似)。LINUX定义了两个内核变量:/proc/sys/net/ipv4/tcp_max_orphans和/proc/sys/net/ipv4/tcp_fin_timeout。前者指定内核能接管的孤儿连接数目,后者指定孤儿连接在内核中生存的时间。

客户端和服务器的状态转移如图:


 

3.4.2 TIME_WAIT状态

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

MSL是TCP报文段在网络中的最大生存时间,建议值是2min 。


TIME_WAIT状态存在的原因有两点:

1.可靠地终止TCP连接。

解释:假设图3—9中用于确认服务器结束报文段6的TCP报文段7丢失,那么服务器将重发结束报文段。因此客户端需要停留在某个状态以处理重复收到的结束报文段(即向服务器发送确认报文段),否则,客户端将以复位报文段来回应服务器,服务器则认为这是一个错误,因为它期望的是一个像TCP报文段7那样的确认报文段。

2.保证让迟来的TCP报文段有足够的时间被识别并丢弃。

解释:在LINUX系统上,一个TCP端口不能被同时打开多次(两次及以上),当一个TCP连接处于TIME_WAIT状态时,我们将无法立即使用该连接占用着的端口来建立一个新连接,反过来思考,如果不存在TIME_WAIT状态,则应用程序能够立即建立一个和刚关闭的连接相似的连接(这里说的相似,指它们具有相同的IP地址和端口号),这个新的连接可能接收到属于原来的连接的,携带应用程序数据的TCP报文段(迟到的报文段),这显然是不应该发生的。

另外,因为TCP报文段的最大生存时间是MSL,所以坚持2MSL时间的TIME_WAIT状态能够确保网络上两个传输方向上尚未被接收到的,迟到的TCP报文段都已消失(被中转路由器丢弃)。

因此,在同一个端口的新连接可以在2MSL时间之后安全地建立,而不会收到属于原来连接的应用程序的数据,这是TIME_WAIT状态持续2MSL时间的原因。

 

3.5 复位报文段

3.5.1 访问不存在的端口

当客户端程序访问一个不存在的端口时,目标主机将给它发送一个复位报文段。

 

3.5.2 异常终止连接

TCP提供了异常终止一个连接的方法,即给对方发送一个复位报文段,一旦发送了复位报文段,发送端所有排队等候发送的数据都将被丢弃。

 

3.5.3 处理半打开连接

服务器(或客户端)关闭或者异常终止了连接,而对方没有接收到结束报文段(例如发生了网络故障),此时,客户端(或服务器)还维持着原来的连接,而服务器(或客户端)即使重启,也已经没有该连接的任何信息了。这种状态称为半打开状态

如果客户端(或服务器)往处于半打开状态的连接写入数据,则对方回应一个复位报文段。

 

3.6 TCP交互数据流

TCP报文段所携带的应用程序数据按照长度分为两种:交互数据和成块数据。

交互数据仅包含很少的字节。使用交互数据的应用程序(或协议)对实时性的要求高(例如telnet,ssh等)。

成块数据的长度则通常为TCP报文段允许的最大数据长度。使用成块数据的应用程序(或协议)对传输效率要求高。(例如ftp)

延迟确认:不马上确认上次收到的数据,而是在一段延迟时间后查看本端是否有数据需要发送,如果有,则和确认信息一起发出。

延迟确认可以减少发送TCP报文段的数量。

在TCP连接的建立和断开过程中,也可能发生延迟确认。

Nagle算法:算法要求一个TCP连接的通信双方在任意时刻都最多只能发送一个未被确认的TCP报文段,在该TCP报文段的确认到达之前不能发送其他TCP报文段。另一方面,发送方在等待确认的同时收集本端需要发送的微量数据,并在确认到来时以一个TCP报文段将它们全部发出。

优点:极大地减少了网络上的微小TCP数据报文段的数量,确认到达得越快,数据也就发送的越快

 

3.7 TCP成块数据流

 

3.8 带外数据

有些传输层协议具有带外(Out Of Band , OOB)数据的概念,用于迅速通告对方本端发生的重要事件。因此,带外数据比普通数据(带内数据)有更高的优先级,它应该总是立即被发送,而不论发送缓冲区中是否有排队等待发送的普通数据

带外数据的传输可以使用一条独立的传输层连接,也可以映射到传输普通数据的连接中。

UDP没有实现带外数据传输,TCP也没有真正的带外数据,不过TCP利用其头部中的紧急指针标志和紧急指针两个字段,给应用程序提供一种紧急方式。

TCP的紧急方式利用传输普通数据的连接来传输紧急数据,这种紧急数据的含义和带外数据类似(后文称其为带外数据)。

 

3.9 超时重传

TCP服务必须能够重传超时时间内未收到确认的TCP报文段,为此,TCP模块为每个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。

 

3.10 拥塞控制

 

3.10.1 拥塞控制概述

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

拥塞控制的四个部分:慢启动,拥塞避免,快速重传和快速恢复。

拥塞控制算法在Linux下有多种实现,/proc/sys/net/ipv4/tcp_congestion_control文件指示机器当前所使用的拥塞控制算法。

拥塞控制的最终受控变量是发送端向网络以此连续写入(收到其中第一个数据的确认之前)的数据量,我们成为SWND(Send Window,发送窗口)。不过发送端最终以TCP报文段来发送数据,所以SWND限定了发送端能连续发送的TCP报文段数量,这些TCP报文段的最大长度(仅指数据部分)成为SMSS(Sender Maximum Segment Size,发送者最大段大小),其值一般等于MSS。

发送端需要合理地选择SWND的大小,如果SWND太小,会引起明显的网络延迟;反之,如果SWND太大,则容易导致网络拥塞。

前文提到,接收方可通过其接收通告窗口(RWND)来控制发送端的SWND,而且发送端还引入了一个称谓拥塞窗口(Congestion Window,CWND)的状态变量。实际的SWND值是RWND和CWND中的较小者。

如图显示了拥塞控制的输入和输出:(闭环反馈控制)

 

3.10.2 慢启动和拥塞避免

慢启动:

TCP连接建立好之后,CWND将被设置成初始值IW(Initial Window),其大小为2-4个SMSS,此时发送端最多能发送IW个字节的数据。

慢启动,是传输控制协议使用的一种阻塞控制机制。

慢启动也叫做指数增长期。

慢启动是指每次TCP接收窗口收到确认时都会增长,增加的大小就是已确认段的数目。这种情况一直保持到要么没有收到一些段,要么窗口大小到达预先定义的阈值。

如果发生丢失事件,TCP就认为这是网络阻塞,就会采取措施减轻网络拥挤,一旦发生丢失事件或者到达阈值,TCP就会进入线性增长阶段,这时,每经过一个RTT(往返时延,表示从发送端发送数据开始,到发送端收到来自接收端的确认(接收端收到数据后便立即发送确认),总共经历的时延。)窗口增长一个段。


慢启动为发送方的TCP增加了另一个窗口:拥塞窗口(congestion window),记为cwnd。当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为 1个报文段(即另一端通告的报文段大小)。每收到一个ACK,拥塞窗口就增加一个报文段(CWND以字节为单位,但是慢启动以报文段大小为单位进行增加),

公式为:CWND += min(N,SMSS),N是此次确认中包含的之前未被确认的字节数。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。发送方开始时发送一个报文段,然后等待 ACK。当收到该ACK时,拥塞窗口从1增加为2,即可以发送两个报文段。当收到这两个报文段的 ACK时,拥塞窗口就增加为4。这是一种指数增加的关系。


慢启动算法的理由:TCP模块刚开始发送数据时并不知道网络的实际情况,需要一种试探的方式平滑地增加CWND的大小。

但是慢启动会使得CWND很快膨胀,并导致网络阻塞,因此TCP拥塞控制中定义了另一个重要的状态变量:慢启动门限。

当CWND的大小超过该值时,TCP拥塞控制将进入阻塞避免截断。


拥塞避免算法使得CWND按照线性方式增加,从而减缓其扩大。

有两种实现方式:

1.每个RTT时间内按照CWND += min(N,SMSS)计算新的CWND,而不论该RTT时间内发送端收到多少个确认。

2.每收到一个对新数据的确认报文段,就按照 

CWND += SMSS*SMSS/CWND 更新CWND 。

 

发送端拥塞发生的依据有如下两个:

1.传输超时,或者说TCP重传定时器溢出。

2.接收到重复的确认报文段。

拥塞控制对这两种情况有不同的处理方式,对第一种情况仍然使用慢启动和拥塞避免。

对第二种情况则使用快速重传和快速恢复(如果是真的发生拥塞的话)。注意,第二种情况如果发生在重传定时器溢出之后,则也被拥塞控制当成第一种情况。

若发生第一种情况,那么它将执行重传并做如下调整:

Ssthresh = max(FlightSize/2 , 2*SMSS)

CWMD <= SMSS

其中FlightSize 是已经发送但未收到确认的字节数。

调整之后,CWMD将小于SMSS,那么也必然小于新的慢启动门限值ssthresh,它一定不小于SMSS的2倍),故而拥塞控制再次进入慢启动阶段。

 

3.10.3 快速重传和快速恢复

拥塞控制算法需要判断当收到重复的报文段时,网络是否真的发生了拥塞,或者说TCP报文段是否真的丢失了,具体做法是:发送端如果连续收到3个重复的确认报文段,就认为是拥塞发生了。

然后它启用快速重传和快速恢复算法来处理拥塞,过程如下:

1)当收到第三个重复的确认报文段时,按照ssthresh = max(FlightSize/2 , 2*SMSS)计算ssthresh ,然后立即重传丢失的报文段,并按照 CWND = ssthresh + 3*SMSS 设置CWND。

2)每次收到1个重复的确认时,设置CWND = CWND + SMSS。此时发送端可以发送新的TCP报文段(如果新的CWND允许的话)。

3)当收到新数据的确认时,设置CWND = ssthresh(ssthresh是新的慢启动门限值,由第一步计算得到)。

快速重传和快速恢复完成之后,拥塞控制将恢复到拥塞避免阶段,由第三步可得知。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值