TCP协议-TCP连接管理

一、TCP概述

TCP协议是 TCP/IP 协议族中一个非常重要的协议。它是一种面向连接、提供可靠服务、面向字节流的传输层通信协议。

TCP(Transmission Control Protocol,传输控制协议)。

1.1 TCP协议的特点

(1)TCP 是面向连接的传输层协议。这就是说,通信双方在使用TCP协议进行通信之前,必须先建立TCP连接。在通信结束后,必须释放已经建立的TCP连接。这就好比打电话,通话前要先拨号建立连接,通话结束后要挂机释放连接。

(2)TCP 是点对点(一对一)的连接。每一条TCP连接只能有两个通信端点(endpoint)。所以基于广播和多播(通信目标是多个主机地址)的应用程序不能使用TCP连接。

(3)TCP 提供可靠交付的通信服务。通过TCP连接传递的数据,无差错、不丢失、不重复,并且按序到达。

(4)TCP 提供全双工通信。TCP允许通信双方在任何时候都能发送和接收数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。在发送时,应用程序在把数据发送给TCP的发送缓存后,就可以做自己的事,而TCP在合适的时候把数据通过网卡发送出去。在接收时,TCP把收到的数据先放入接收缓存,应用层的应用进程在合适的时候再读取缓存中的数据。

(5)TCP 是面向字节流的。TCP中的“流”(stream)指的是流入到进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把上层应用程序交下来的数据仅仅看成是一连串无结构的字节序列(就像水流一样)。TCP并不知道所传送的字节流的含义。TCP不保证接收方应用程序所收到的数据块和发送方应用程序所发出的数据块具有对应大小的关系(例如,发送方应用程序交给发送方的TCP共10个数据块,但接收方的TCP可能只用了4个数据块就把收到的字节流交付上层的应用程序)。但是,接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全相同。因此,接收方的应用程序必须有能力识别接收到的字节流,把它还原成有意义的应用层数据。下图的示意图解释了TCP面向字节流的含义:

TCP协议-TCP连接管理_第1张图片 TCP面向字节流的概念

TCP和UDP在发送报文时所采用的方式完全不同。TCP并不关心上层的应用程序一次把多长的报文发送到TCP的缓存中,而是根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应该包含多少个字节(UDP发送的报文段是上层的应用程序给出的)。如果应用程序传送到TCP缓存的数据块太大,TCP就可以把它划分短一些再传送。如果应用进程一次只发来一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。关于TCP报文段的长度问题,会在下面内容中进行讨论。

1.2 TCP的连接概念

每一条TCP连接都有两个端点。TCP连接的端点叫做套接字(socket)。根据RFC 793的定义:端口号拼接到(concatenated with)IP地址即构成了套接字。因此,套接字的表示方法为:

套接字 socket = (IP地址:端口号)

例如,若IP地址是 192.168.1.112,而端口号是 80,那么得到的套接字就是(192.168.1.112: 80)。

每一条TCP连接唯一地被通信两端的两个端点(即两个套接字)所确定。即:

TCP连接 ::= {socket1, socket2} = {(IP1: port1), (IP2: port2)}

 IP1 和 IP2 分别是两个端点主机的IP地址,而port1 和 port2 分别是两个端点主机中的端口号。TCP连接的两个套接字就是socket1 和 socket2。

二、TCP报文段的首部(头部)结构

TCP虽然是面向字节流的,但是TCP传送的数据单元却是报文段。一个TCP报文段分为首部和数据两部分,而TCP的全部功能都体现在它首部中各字段的作用。因此,只有弄清楚TCP首部各字段的作用才能掌握TCP的工作原理。

这里再讲一下“面向字节流”的含义,是指应用层上的应用程序将数据(这些数据可能是有结构层次的)传递给传输层的TCP时,TCP只把这些数据看做是一连串的字节流,而不会去关心这些数据是什么结构的。

TCP报文段首部的前20个字节是固定的,后面有4n字节是根据需要而增加的选项(n是整数)。因此TCP的首部的最小长度是20字节。

下图显示了TCP首部的数据格式。如果不计选项字段,TCP首部的大小通常为20字节。

每个TCP首部都包含源端和目的端的端口号,大小均为16bit,用于寻找发送端和接收端应用进程。这两个值+IP首部中的源端IP地址和目的端IP地址,就可以唯一确定一条TCP连接。

TCP协议-TCP连接管理_第2张图片 TCP报文段的首部格式

 2.1 TCP首部固定部分各字段的含义

(1)源端口和目的端口:各占2字节,分别写入源端口号和目的端口号。和UDP的首部类似。

(2)序号:占4字节。序号范围是 [0, 2^32-1],共2^32(即4 294 967 296)个序号。序号增加到(2^32-1)后,下一个序号就又回到0。也就是说,序号使用 mod 2^32 运算。TCP是面向字节流的,在一个TCP连接中传送的字节流中的每一个数据字节都按顺序编号。整个要传送的字节流的起始序号必须在TCP连接建立时设置。首部中序号字段值指的是本报文段所发送的数据的第一个字节的序号

例如,某个TCP报文段的首部序号字段值是301,而这个报文段携带的数据共有100字节,那么最后一个字节的序号是400。显然,下一个报文段(如果还有的话)的数据序号应当从401开始,即下一个报文段的序号字段值是401。这个字段的名称就叫做“报文段序号”。

(3)确认号:占4字节。是期望收到对方下一个报文段的第一个数据字节的序号值

例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节(序号501~700)。这表明B正确收到了A发送的到需要700为止的数据。因此,B期望收到A的下一个数据字节序号是701,于是B在发送给A的确认报文段中把确认号设置为701。请注意,现在的确认号不是501,也不是700,而是701。总之,应当记住:

若确认号 = N,则表明:到序号 N-1 为止的所有数据都已正确收到。

由于序号字段有32位长,可对4GB的数据进行编号。在一般情况下,可保证当序号重复使用,旧序号的数据早已通过网络到达终点了。

(4)数据偏移:占 4 bit。这个字段用于记录TCP报文段的首部长度。由于TCP报文段的首部结构中还有长度不确定的选项字段,因此数据偏移字段是必要的。当应注意的是,“数据偏移”的单位是32位字(即为4字节的字长为计算单位)。由于4位二进制能够表示的最大十进制数是15,因此数据偏移的最大值是60字节,这也是TCP报文段首部的最大长度(即选项字段长度不能超过40字节)。一般情况下,TCP首部的长度为20字节。

(5)保留:占6位。保留为今后使用,但目前应置为0。

(6)6个控制位,用来说明本报文段的性质,它们各自的含义如下:

  • 紧急URG:(Urgent Pointer)紧急指针字段。当URG=1时,有效。它告诉系统此报文段有紧急数据,应尽快传送(相对于高级优先的数据),而不要按原来的排队顺序来传送。当 URG置1时,发送应用进程就告诉发送方的TCP有紧急数据要传送。于是发送方TCP就把紧急数据插入到本报文段数据部分的最前面,而在紧急数据后面的数据仍是普通数据。这时要与首部中紧急指针(Urgent Pointer)字段配合使用。

例如,已经发送了很长的一个程序要在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行。因此用户从键盘发出中断命令(ctrl + c),如果不使用紧急数据,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有的数据都被处理完毕后这两个字符才被交付给接收方的应用进程,这样做就浪费了许多时间。

  • 确认ACK:(ACKnowledgment) 当 ACK=1时,确认号字段才有效。当ACK=0时,确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置1。
  • 推送PSH:(Push) 当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP就可以使用推送(push)操作。这时,发送方TCP把 PSH 置 1,并立即创建一个报文段发送出去。接收方TCP收到 PSH=1 的报文段后,就尽快地交付给接收方的应用进程,而不再等到整个缓存都填满后再向上交付。

虽然应用程序可以选择推送操作,但推送操作很少使用。

  • 复位RST:(ReSeT) 当RST=1时,表明TCP连接出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立TCP连接。RST置1,还可用来拒绝一个非法的报文段或拒绝打开一个TCP连接。RST也可称为重建位或重置位。我们将携带RST标志的TCP报文段称为复位报文段
  • 同步SYN:(SYNchronization)  在TCP连接建立时用来同步序号。当 SYN=1,ACK=0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应报文段中使用 SYN=1,ACK=1。因此,SYN置为1,就表示这是一个连接请求或连接接受报文段。我们将携带SYN标志的TCP报文段称为同步报文段。关于TCP连接的建立和释放,会在下面的部分进行详细的讨论。
  • 终止FIN:(FINish)  原来关闭一个TCP连接。当 FIN=1 时,表明此报文段的发送方的数据已经发送完毕,通知对方本端要关闭连接了。我们将携带FIN标志的TCP报文段称为结束报文段

(7)窗口:占2字节。窗口值是 [0, 2^16-1]之间的整数值。窗口字段指的是发送本报文段的一方的接收窗口(Receiver Window, RWND)(而不是自己的发送窗口)。窗口值告诉对方:本端的TCP接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。总之,窗口值是接收方目前允许发送方发送的数据量(以字节为单位),作为发送方设置其发送窗口的依据。

例如,发送了一个TCP报文段,其确认号是701,窗口字段值是1000。这就是告诉对方:从701号算起,我方(即发送此报文段的一方)的接收缓存空间还可接收1000个字节的数据(字节序号是701~1700),你在给我发送数据时,必须考虑到这一点。

总之,记住:窗口字段明确指出了 现在允许对方发送的数据量。窗口值经常在动态变化着。

(8)校验和:占2个字节。由发送方填充该字段,校验和字段检验的范围包括首部和数据这两部分。接收端对TCP报文段执行CRC算法以检验TCP报文段在传输过程是否有损坏,校验和检测也是TCP提供可靠传输的一个重要保障。和UDP数据报一样,在计算校验和时,要在TCP数据报的前面加上12字节的伪首部。TCP伪首部的格式和UDP用户数据报的伪首部格式一样,但应把伪首部第4个字段中的17改为6,(TCP的协议号是6),把第5个字段的UDP长度改成TCP长度。接收方收到此报文段后,仍要加上这个伪首部来计算校验和。若使用IPV6,则相应的伪首部也要改变。

TCP协议-TCP连接管理_第3张图片 UDP用户数据报的首部和伪首部

(9)紧急指针:占2个字节。紧急指针字段仅在 URG=1 时才有意义。它指出本报文段中紧急数据的字节数(紧急数据都是放在TCP报文段数据部分的前面,紧急数据结束后才是普通数据)。该字段值和序号字段相加表示最后一个紧急数据的下一个字节的序号值。当所有紧急数据都处理完时,TCP就告诉应用程序恢复到正常操作。值得注意的是,即使窗口值为0时也可发送紧急数据。

(10)选项:TCP头部最后一个选项(options)字段是可变长的可选信息,最长可达40字节,因此TCP首部最长是60字节。当没有使用“选项”字段时,TCP首部长度是20字节。

2.2 TCP首部选项字段结构

TCP首部结构的最后一个选项(Options)字段是一个可变长的可选信息。

上面我们已经知道,在TCP报文段的首部有一个“数据偏移”字段,占 4 bit位,最大能表示的十进制数为 15,单位为32位字(也就是4字节),因此数据偏移字段的最大值是60字节。该字段的含义是TCP报文段的首部长度。

因此,TCP首部选项字段的最大长度 = 60字节 - 首部固定大小的20字节 = 40字节

典型的TCP首部选型字段的结构示意图如下所示:

TCP首部选项字段的一般结构
  •  kind:占1个字节。该字段是说明选项的类型。有的TCP选项没有后面两个字段,仅包含1字节的kind字段。
  • length:占1个字节。指定该选项的总长度。该长度包括kind字段和length字段占据的2个字节。
  • info:是选项的具体信息。常见的TCP选项有7种,如下图所示:
TCP协议-TCP连接管理_第4张图片 7种TCP选项

(1)kind=0:是选项表结束选项。

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

(3)kind=2:是最大报文段长度(Maximum Segment Size,MSS)选型。MSS是每一个TCP报文段中的数据部分的最大长度。数据部分加上TCP首部才等于整个的TCP报文段,所以MSS并不是整个TCP报文段的最大长度,而是TCP报文段长度减去TCP首部长度。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP首部长度和20字节的IP首部长度)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP报文段首部和IP数据报首部都不包含选项字段,当然这是一般情况),从而本机发生IP分片。对于以太网而言,MSS的值是1460(1500-40)字节。

为什么要规定一个最大报文段长度MSS呢?这并不是考虑接收方的接收缓存可能放不下TCP报文段中的数据,实际上,MSS与接收窗口值没有关系。主要原因是为了提高网络的利用率。因为TCP在传送数据时,是以报文段为单位发送数据的。而待传送的数据是以 MSS 的大小为单位进行分割的,然后加上TCP首部,就组装成一个完整的TCP报文段。进行重发时,也是以 MSS 为单位。

如果MSS设置太低了,网络的利用率就会降低;如果MSS设置太高了,又会增加网络处理IP分片的开销。因此,设置一个合理的MSS值是很有必要的,MSS的值应尽可能大些,只要在IP层传输时不需要再分片就行,最理想的情况是,最大TCP报文段长度正好是IP数据报中不会被分片处理的最大数据长度。

在建立TCP连接建立的过程中,双方都把自己能够支持的MSS填入这一字段,以后就按照这个数值传送数据,两个传送方向可以有不同的MSS值。若主机未填写这一项,则MSS的默认值是536字节长度。因此,所有在互联网上的主机都应能接受报文段长度是 536+20(TCP报文段固定首部长度)=556字节。

<备注1> MTU(Maximum Transmission Unit,最大传输单元) 它属于数据链路层上的概念,表示的是数据链层帧的数据部分的最大长度。

<备注2> 有一种流行的说法:在TCP连接建立的阶段“双方协商MSS值”,但这是错误的,因为这里并不存在任何的协商,而只是一方把MSS值设定好以后通知另一方而已。

(4)kind=3:窗口扩大因子选项。该选项是为了扩大窗口。我们知道,TCP首部的窗口字段长度是16位,因此最大的窗口大小为 64K(65 535) 字节。由上图可知,窗口扩大选项占3个字节,其中有一个字节表示移位值M。假设TCP首部中的接收窗口字段值为 N,窗口扩大选项中的移位值为 M,那么TCP报文段的实际接收窗口大小为:N * 2^M,或者说N左移M位。注意,M的取值范围是[0, 14]的整数值。我们可以通过修改 Linux系统中的 /proc/sys/ipv4/tcp_window_scaling 内核变量来启动或关闭窗口扩大因子选项。

和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段首部的接收窗口字段值就是该TCP报文段的实际接收窗口大小。当TCP连接建立好之后,每个数据传输方向的窗口扩大因子就确定下来了。如果连接一方实现了窗口扩大,当它不再需要扩大其窗口时,可发送M = 0的选型,使窗口大小回到16。

(5)kind=4:选择确认(Selective Acknowledgment, SACK)选项。TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被接收方确认的TCP报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK选项正是为了改善这种情况而产生的,它使TCP模块只重新发送丢失的TCP报文段,而不用发送所有未被确认的TCP报文段。选择确认选项用在TCP连接建立过程中,表示是否支持SACK选项。我么可以通过修改Linux系统的 /proc/sys/net/ipv4/tcp_sack 内核变量来启用或关闭选择确认选项。

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

(7)kind=8:是时间戳选项,占10个字节。时间戳选项有以下两个功能:

第一,用来计算通信双方之间的回路时间(Round Trip Time,RTT)。发送方在发送TCP报文段时把当前时间值放入时间戳字段,接收方在确认该报文段时把时间戳字段值复制到时间戳回显应答字段。因此,发送方在收到确认报文后,可以准确地计算出RTT来。

第二,用于处理TCP序号超过2^32(4,294,967,296)的情况,这又称为防止序号绕回PAWS(Protect Against Wrapped Sequence numbers)。我们知道,TCP报文段的序号只有32位,而每增加2^32个序号后就会重新从0开始编号。当使用高速网络时,在一次TCP连接的数据传送中序号很可能会被重复使用。例如,当使用 1.5Mbit/s的速率发送TCP报文段时,序号重复要6小时以上。但若使用2.5Gbit/s的速率发送数据报时,则不到14秒钟序号就会重复。为了使接收方能够把新的报文段和迟到很久的报文段(序号相同的情况下)区分开,可以在报文段中加上时间戳选项。我们可以通过修改Linux系统的 /proc/sys/net/ipv4/tcp_timestamps 内核变量来启用和关闭时间戳选项。

示例1,我们使用Wireshark软件查看一个TCP连接的SYN同步报文段信息。

TCP协议-TCP连接管理_第5张图片 SYN同步报文段信息

 由上图可以看到,TCP报文段首部各个字段的信息,该报文段的首部长度是40字节,其中包括固定的首部长度20字节+选项字段20字节长度。标志位Flags中,只有 SYN=1,其他标志位均为0。初始序号值为0,确认号为1,接收窗口值为5840,检验和字段为0x5574,紧急指针字段为0。

 我们展开其Options选项的内容,如下图所示:

TCP协议-TCP连接管理_第6张图片 SYN同步报文段Options选项信息

 由上图可知,在Options选项中,设置了如下的几种TCP选项:

  • kind=2,最大报文段MSS选项,MSS的值为1460。
  • kind=4,选择确认SACK选项
  • kind=8,时间戳timestamps选项
  • kind=1,无操作NOP选项
  • kind=3,窗口扩大(Window scale)选项,移位值为7,可以扩大2^7=128倍。

三、TCP的连接管理

TCP是面向连接的传输层协议。TCP连接的建立和释放是每一次面向连接的通信中必不可少的过程。因此TCP通信过程有3个阶段,即:连接建立、数据传输和连接释放。这里我们主要将TCP连接的建立和释放的过程。

3.1 TCP连接的建立

TCP连接的建立采用客户-服务器方式。主动发起连接建立的应用进程叫做客户端(client),而被动等待连接建立的应用进程叫做服务器(server)。

TCP建立连接的过程叫做握手,握手的过程需要在客户端和服务器之间交换3个TCP报文段,因此我们形象地将TCP连接的建立过程称为“三次握手”。

下图是TCP三次握手建立连接的过程,假定主机A运行的是TCP客户程序,主机B运行的是TCP服务器程序。最初两端的TCP进程都处于关闭(Closed)状态。注意:A客户端主动打开连接,B服务器被动打开连接。

TCP协议-TCP连接管理_第7张图片

 描述整个过程

1. 服务器初始化状态

主机B的TCP服务器进程先创建传输控制块(TCB),这时socket(),bind() 函数已经执行完毕,服务器进程准备接受客户进程的连接请求。然后服务器进程调用listen()函数,此时服务器进程处于监听(listen)状态,紧接着调用accept()函数,等待客户的连接请求到来。如有,即作出响应。

服务端进程调用函数顺序:socket—>bind—>listen—>accept。当执行到accept()函数时,服务器进程会一直处于阻塞状态,直到有客户连接请求到达才返回。

2. 客户端发起连接请求,发送SYN同步报文段,第一次握手

主机A的客户进程也是首先创建传输控制块(TCB),然后向主机B发出连接请求报文段,这时请求报文段的首部的同步位SYN=1,同时选择一个初始序号seq=x,这个初始序号x就是随机产生的整数ISN。TCP规定,SYN报文段(即SYN=1的TCP报文段)不能携带数据,但要消耗一个序号。这时,TCP客户进程进入 SYN-SENT(同步已发送) 状态

客户进程调用函数顺序:socket——>connect。当客户进程调用connect()函数时,客户进程就会向服务器进程发出连接请求的SYN同步报文段。

前面我们已经讲过,携带SYN标志的TCP报文段称为同步报文段。此时,同步标志位 SYN = 1。

ISN(Initial Sequence Number) 初始序列号

3. 服务器同意建立连接,回复确认信息,第二次握手

主机B的服务器进程收到连接请求报文段后,如同意建立连接,则向主机A的客户进程发送确认报文段。在确认报文段的首部中,SYN=1,ACK=1,确认号=x+1(没有数据,所以长度为0,直接seq+1即可),同时也为自己选择一个初始序号seq = y。请注意,这个确认报文段也不能携带数据,但同样要消耗一个序号。这时,TCP服务器进程进入 SYN-RCVD(同步收到) 状态

4. 客户端确认连接,发送确认连接信息,第三次握手

主机A的客户进程收到主机B的服务器进程的确认报文段后,还要向主机B的服务器进程的SYN报文段给出确认。在确认报文段的首部中,ACK=1,确认号ack=y+1,而自己的序号seq=x+1。TCP的标准规定,ACK报文段可以携带数据。但如果不携带数据,则不消耗序号。在这种情况下,客户进程的下一个数据报文段的序号仍是seq=x+1。这是,TCP连接已经建立,主机A的客户进程也进入 ESTABLISHED(已建立连接)状态当主机B的服务器进程接收到主机A的客户进程发来的确认报文段后,也进入ESTABLISHED(已建立连接)状态

上面给出的TCP连接建立过程叫做“三次握手”。请注意,在上图中,B发送给A的报文段,也可以拆成两个报文段分两次发送。即先发送对A的连接请求同步报文段的确认报文段(ACK=1, ack=x=1),接着再发送B自己的连接请求的同步报文段(SYN=1, seq=y)给A。A收到B的同步报文段后,再给B回复一个确认报文段。那么,这样的过程就变成了“四次握手”,但效果是一样的。

 3.1.1 TCP连接建立相关问题

问题1:TCP连接的建立为什么需要3次握手,而不是两次握手?

在谢希仁编写的《计算机网络》一书中,给出的理由是:防止已失效的连接请求报文段突然又传送到了服务器端,因而产生错误。但是这个解释是不够准确的,不是最根本原因。首先,我们要清楚一点,TCP是全双工通信的三次握手的一个重要作用是客户端和服务端交换彼此的ISN,以便让对方知道接下来接收数据的时候如何按序列号重新组装TCP报文段。如果只有两次握手,至多只有连接发起方的起始序列号被确认,而另一方的起始序列号却得不到对方的确认。因为只有TCP通信双方的SYN同步报文段的首部中有本端的起始序列号,所以每一方都要对对方的SYN同步报文段回复一个ACK确认报文段,用来表明我方已经确认收到你方发送的同步报文段了。

结论:TCP连接的建立过程中的三次握手,握的就是通信双方的起始序列号。三次握手实质上是三报文握手。

下面,我们来讨论一下谢希仁先生给出的理由的这种情况的分析。

(1)“已失效的连接请求报文”是如何产生的。现假定出现一种异常情况,即 客户端A发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间滞留了,以致延误到连接释放以后的某个时间才到达B。本来这是一个早已失效的连接请求报文段,但服务端B收到此失效的连接请求报文段后,就误以为是客户端A又发出一次新的连接请求。于是就向A发出确认报文段,同意建立连接。如果只有两次握手,那么只要B发出确认报文段,B就认为新的连接建立了,B进入ESTABLISHED(已建立连接)状态。

(2)由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的连接是已经建立了的,并一直在等待A发来数据。也就是说,B此时一直处于忙等状态,其结果是浪费了B的资源开销。

结论:两次握手,可能导致服务器需要维护许多不成功的TCP连接,造成服务器资源的浪费。

需要注意的是,在TCP连接建立的过程中,只要服务器进程收到了连接请求的SYN同步报文段,服务器就会为这个连接请求创建传输控制块TCB数据结构,这就需要开辟内存空间来维护这个数据结构。

问题2:在TCP连接建立过程中,如果服务器一直收不到客户端的ACK确认报文段,会发生什么?

操作系统会给每个处于 SYN-RCVD状态的服务器进程都设定一个定时器,如果超时时间还没有收到客户端第三次握手的ACK确认报文段,将会重新发送第二次握手的报文段,直到重发达到一定次数时才会放弃。

问题3:初始序列号ISN为什么要随机初始化?

seq序号表示的是发送的TCP报文段数据部分的起始字节位置,服务器/客户端可以通过序号正确读取数据。如果不是随机分配起始序列号,那么黑客就会很容易获取到客户端与服务器之间TCP通信的初始序列号,然后通过伪造序列号让通信主机读取到携带病毒的TCP报文段,发起网络攻击。

问题4:在TCP连接建立的过程中,可能会出现什么攻击?如何解决?

SYN flood 攻击:又称为SYN泛洪攻击。下面介绍一下泛洪攻击是如何发生的。

(1)攻击者在短时间内伪造大量不存在的IP地址,向服务器不断地发送连接请求的SYN同步报文段,当服务端收到这些连接请求的报文段后,就会为该连接请求创建传输控制块来保存客户端的信息。当有大量的连接请求时,服务器会消耗掉大量的内存资源,直至内存资源被耗尽。

(2)服务器同时需要为每条连接请求回复ACK确认报文段,并等待客户端的确认。但是客户端的IP地址是虚假的,也就不会向服务器回复确认报文段,那么服务器需要不断地重发第2次握手的报文段直至超时。同时,这些伪造的连接请求SYN同步报文段还将长时间占用未连接队列(Linux默认的限制一般是256个),导致正常客户端的连接请求SYN同步报文段被丢弃,目标系统运行缓慢,严重者引起网络阻塞甚至服务器系统瘫痪。

我们可以通过下面这个图来直观了解一下SYN泛洪攻击的过程:

TCP协议-TCP连接管理_第8张图片 SYN泛洪攻击

 解决的办法:

方法1:缩短SYN Timeout 时间。由于 SYN Flood 攻击的效果取决于服务器上保持的半连接数,这个值=SYN攻击频度 x SYN Timeout,所以通过缩短从接收到SYN报文段到确定这个报文段无效并丢弃该连接的时间。例如,设置为20秒以下(过低的SYN Timeout 设置可能会影响客户的正常访问),可以成倍地降低服务器的载荷。

方法2:设置 SYN Cookie。就是给每一个连接请求的IP地址分配一个Cookie,如果短时间内连续收到某个IP地址的大量重复SYN报文段,就认定是收到了攻击。以后从这个IP地址来的报文段会被丢弃。

方法3:使用防火墙。SYN Flood攻击很容易就能被防火墙拦截。

3.1.2 连接超时问题

前面介绍的是TCP连接正常建立的过程。如果客户端访问一个距离它很远的服务器或者由于网络繁忙,导致服务器对于客户端发出的同步报文段没有应答,此时客户端程序将产生什么样的行为呢?显然,对于提供可靠服务的TCP来说,它必然是先进行重连(可能执行多次),如果重连仍然无效,则通知应用程序连接超时,建立TCP连接失败。

发起TCP连接请求的客户端,当发送了同步报文段后,会开启一个重传定时器,当超时时间到达后,如果没有收到确认报文段,会重发一次同步报文段,并将重连的超时时间增加一倍,直到达到规定的重连次数为止。TCP重连次数是由 /proc/sys/net/ipv4/tcp_syn_retries 内核变量定义的,默认值是6,它表示的含义是建立TCP连接时SYN同步报文段重发的次数。

可以通过sysctl 命令来查看:(Linux系统:CentOS-8.3)

# sysctl -a | grep tcp_syn_retries
net.ipv4.tcp_syn_retries = 6

 在Linux系统中,连接超时典型为2分7秒,而对于一些Client来说,这是一个非常长的时间,所以在实际网络编程中,可以使用非阻塞模式来实现。例如:使用 select(2)、poll(2)、epoll(2)等系统调用来实现多路复用。

下来来分析一下,这个2分7秒的时间间隔是怎样来的。

2分7秒 即 127秒,刚好是 2 的 7 次幂减1,如果TCP连接建立的SYN报文段超时时间间隔是按照2的幂来递增的话,那么:

第 1 次发送 SYN 报文后等待 1s(2 的 0 次幂),如果超时,则重发
第 2 次发送后等待 2s(2 的 1 次幂),如果超时,则重发
第 3 次发送后等待 4s(2 的 2 次幂),如果超时,则重发
第 4 次发送后等待 8s(2 的 3 次幂),如果超时,则重发
第 5 次发送后等待 16s(2 的 4 次幂),如果超时,则重发
第 6 次发送后等待 32s(2 的 5 次幂),如果超时,则重发
第 7 次发送后等待 64s(2 的 6 次幂),如果超时,则超时失败

综上,1+2+4+8+16+32+64=127

 上面总的超时时间刚好是127秒。也就是说,Linux内核在尝试建立TCP连接时,最多会尝试7次,重发6次。

修改重连次数和超时时间

可以通过以下命令修改该值,例如将其改为5。

sysctl -w net.ipv4.tcp_syn_retries=5

如果希望重启系统后仍生效,可以在 /etc/sysctl.conf 文件中添加如下内容:

net.ipv4.tcp_syn_retries=5

在应用程序中,我们可以通过设置socket选项中的 SO_SNDTIMEO 选项,然后调用connect()函数。具体代码如下:

//设置connect超时时间
int timeout_connect(const char *ip, int port, int timeout)
{
    int ret = 0;
    struct sockaddr_in cli_addr;
    
    bzero(&cli_addr, sizeof(cli_addr));
    cli_addr.sin_family = AF_INET;
    inet_pton(AF_INET, ip, &cli_addr.sin_addr);
    cli_addr.sin_port = htons(port);
    
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    assert(sockfd >= 0)
    //通过选项SO_SNDTIMEO所设置的超时时间类型是timeval,这和select()系统调用的超时参数类型相同
    struct timeval timeout;
    timeout.tv_sec = time;
    timeout.tv_usec = 0;
    socklen_t len = sizeof(timeout);
    ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);
    assert(ret != 1);
    
    ret = connect(sockfd, (struct sockaddr*)&cli_addr, sizeof(cli_addr));
    if(ret == -1){
        //超时对应的错误号是EINPROGRESS。下面这个条件如果成立,我们就可以处理定时任务了
        if(errno == EINPROGRESS){
            printf("connection timeout, process timeout logic\n");
            return -1;
        }
        printf("error occur when connecting to server\n");
        return -1;
    }
    return sockfd;
}

3.2 TCP连接的释放

TCP连接的释放可以用“四次挥手”的过程来描述。数据传输结束后,通信的双方都可释放连接。现在 客户端A和服务器B都处于 ESTABLISHED 状态。释放连接的过程如下图所示:

TCP协议-TCP连接管理_第9张图片

 描述整个过程

1. A客户端主动断开连接,发送释放连接的FIN报文段,第一次挥手

A客户端进程先向B服务器进程发送释放连接的FIN结束报文段,并停止再发送数据,主动关闭TCP连接。在结束报文段的首部中,终止控制位FIN=1,其序号字段seq=u,它等于前面已发送过的数据的最后一个字节的序号加1。此时,A客户端进程进入 FIN-WAIT-1(终止等待1)状态,等待B服务器进程的确认。请注意,TCP规定,FIN报文段即使不携带数据,它也要消耗掉一个序号。这点和SYN报文段是一样的。

2. B服务器收到A客户端的结束报文段,发出确认报文段,第二次挥手

B服务器进程收到A客户端进程发来的释放连接的FIN结束报文段后,立即发出确认报文段,确认号ack=u+1,而这个报文段自己的序号seq=v,等于B服务器前面已发送过的数据的最后一个字节的序号加1。然后B服务器进程进入 CLOSE-WAIT(关闭等待)状态。TCP服务器进程这时通知高层的应用进程,那么从 A 到 B 这个方向的连接就释放了,此时TCP连接处于半关闭(half-close)状态,即A客户端已经没有数据要发送了,但B服务器若发送数据,A客户端仍要接收。也就是说,从 B 到 A 这个方向的连接并未关闭,这个状态可能会持续一段时间。

A客户端收到B服务器的确认报文段后,就进入 FIN-WAIT-2(终止等待2)状态,等待B服务器发出的FIN结束报文段。

3. B服务器释放连接,发出连接释放的结束报文段,第三次挥手

当B服务器已经没有要向A客户端发送的数据时,其应用进程就通知TCP释放连接,向A客户端发送释放连接的结束报文段。在这个结束报文段的首部中,终止控制位FIN置1,假定其序号字段为w(在半关闭状态中,B服务器可能又发送了一些数据),同时还必须重复上次已发送过的确认号ack=u+1。这时,B服务器进程就进入 LAST-ACK(最后确认)状态,等待A客户端的确认。

4. A客户端收到B服务器释放连接的结束报文段,发出确认报文段,第四次挥手

A客户端在收到B服务器的释放连接的结束报文段后,必须对此发出确认,即向B服务端发送一个确认报文段。在确认报文段的首部中,控制位ACK=1,确认号字段ack=w+1,而自己的序号字段seq=u+1(根据TCP标准,前面发送过的FIN报文段是要消耗一个序号的)。然后A客户端进入到TIME-WAIT(时间等待)状态

请注意,此时TCP连接还没有释放掉,必须经过时间等待计时器(TIME-WAIT timer)设置的时间2MSL后,A才进入到 CLOSED 状态。时间MSL(Maximum Segment Lifetime,最长报文段寿命) 即一个TCP报文段存活的最长时间。RFC793建议设为2分钟,现在可以根据情况使用更小的MSL值。因此从A客户端进入到 TIME-WAIT 状态后,要经过4分钟才能进入到CLOSED状态,才可以建立下一个新的连接,当A客户端撤销相应的传输控制块TCB后,就结束了这次的TCP连接。

B服务器只要收到了A客户端发出的确认报文段,就进入 CLOSED状态。同样,B服务器在撤销相应的传输控制块TCB后,就结束了此次的TCP连接。

可以发现,B服务器结束TCP连接的时间要比A客户端早一些。

上述内容就是TCP连接释放的过程,俗称“四次挥手”过程,其实质上是四报文挥手。

3.2.1 TCP连接释放相关问题

问题1:为什么建立连接是三次握手,而关闭连接却是四次挥手?

在建立TCP连接时,当服务器收到客户端发来的连接请求的SYN同步报文段后,可以直接发送一个ACK+SYN的报文段给客户端,其中ACK控制位是用来确认的,SYN控制位是用来同步的。但是在关闭连接时,当服务器收到客户端发来的FIN结束报文段时,自己这边可能还有数据没有发送完,因此只能先回复一个ACK确认报文,告诉客户端,“你发来的FIN报文我收到了”。只有等到服务器所有的数据都发送完了,服务器才会向客户端发送一个FIN结束报文段,最后客户端回复一个确认报文,总共就是四次挥手过程。也就是说,在关闭连接的第二次挥手阶段,服务器不能将控制位ACK+FIN 同时放在一个报文段中回复给客户端。

注意:发送了FIN结束报文段,只是表示本端不能再继续发送数据了,但是还可以接受数据。TCP通信它是全双工的,收到一个FIN报文段,只是关闭了一个方向上的连接,而另一个方向仍能发送数据,此时TCP处于半关闭状态。

问题2:为什么客户端在 TIME-WAIT 状态时,必须等待2MSL的时间才能进入到 CLOSED状态呢?

第一,为了保证客户端发送的最后一个ACK报文段能够到达对端,即保证可靠地终止TCP连接。因为如果出现网络拥塞,这个ACK报文段是有可能丢失的,因而使处于LAST-ACK状态的服务器收不到客户端对自己已发送过的FIN+ACK报文段的确认。那么,服务器会超时重传这个FIN+ACK报文段,而客户端就能在2MSL时间内收到这个重传的FIN+ACK报文段。接着客户端重传一次确认,重新启动2MSL计时器。最后,客户端和服务器都正常进入到CLOSED状态。如果客户端在 TIME-WAIT 状态时不等待一个2MSL时间,而是在发送完ACK确认报文段后立即释放连接,进入到CLOSED状态,那么就无法收到服务器重传的FIN+ACK报文段,因而也不会再发送一次确认报文段。这样,服务器就无法按照正常步骤进入到CLOSED状态。

第二,防止已失效的连接请求报文段出现在本次TCP连接中。客户端在发送完最后一个ACK报文段后,再经过2MSL的时间后,就可以使本次TCP连接持续的时间内所产生的所有报文段都从网络上消失。这样就可以使下一个新的TCP连接中不会出现之前旧的连接请求报文段。

问题3:为什么是2MSL,这个时间是如何得来的?

我们知道,MSL是TCP报文段的最大生存时间,2MSL的时间是从客户端发出最后一个ACK报文段开始计时的,考虑到了重传的因素。

客户端—[ACK报文段]—>服务器            1MSL

服务器—[FIN+ACK报文段]—>客户端    1MSL (如果服务器在超时时间内没有收到客户端的ACK报文段,就重传FIN+ACK报文段)

保证在TCP的两个传输方向上,那些尚未被接收或迟到的报文段都消失,理论上保证最后一个ACK报文段可靠到达。

问题4:TIME-WAIT 状态何时出现?TIME-WAIT会带来哪些问题?

TIME-WAIT状态是主动关闭连接的一方收到了对方发来的FIN结束报文段并且本端发送ACK确认报文段后的状态。

TIME-WAIT的引入是为了让TCP报文段得以自然消失,同时为了让被动关闭的一方能够正常关闭连接。

  • 服务器主动关闭连接,短时间内关闭了大量的客户端连接,会造成服务器上出现大量的 TIME-WAIT状态的连接,占据大量的tuple(源IP地址、目的IP地址、协议号、源端口、目的端口),严重消耗着服务器的资源。
  • 客户端主动关闭连接,短时间内大量的短连接,会大量消耗客户端主机的端口号,毕竟端口号只有65535个,断开耗尽了,后续就无法启用新的TCP连接了。

问题5:解决 TIME-WAIT 状态引起的bind()函数执行失败的问题?

问题场景:我在编写一个基于TCP连接的socket服务器程序并反复调试的时候,发现了一个让人无比心烦的情况:每次kill掉该服务器进程并重新启动的时候,都会出现bind错误:error:98,Address already in use。然而再kill掉该进程,再次重新启动的时候,就bind成功了。

问题原因:当我kill掉服务器进程的时候,系统并没有马上完全释放掉socket的TCP连接资源,此时socket处于TIME-WAIT状态,当我使用 netstat 命令查看该进程的端口号时,发现该进程处于TIME-WAIT状态,需要等待2MSL时间后,整个TCP连接才算真正结束。这就是我的服务器进程被杀死后,不能马上重新启动的原因(错误提示为:“Address already in use”)。Linux系统中,一个端口释放后需要等待两分钟才能再次被使用。

问题解决:我们可以使用setsockopt()函数设置socket描述符的SO_REUSEADDR选项,该socket选项可以让端口被释放后立即就能被再次使用,表示允许创建端口号相同但是IP地址不同的多个socket描述符。

代码描述如下:

int opt = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

问题6:TIME-WAIT 和 CLOSE-WAIT 的区别?

  • TIME-WAIT 状态是主动关闭TCP连接的一方在本端已经关闭的前提下,收到对端的关闭请求并将ACK确认报文段发送过去后所处的状态。

这种状态表示:通信双方都已完成工作,只是为了保证本次TCP连接能够顺利正常的关闭,即可靠地终止TCP连接。

  • CLOSE-WAIT 状态是被动关闭TCP连接的一方在接收到对端的关闭请求(FIN结束报文段)并且将ACK确认报文段发送出去后所处的状态。

这种状态表示:收到了对端关闭连接的请求,但是本端还没有完成工作,还未关闭本端的TCP连接。

问题7:半连接、半打开、半关闭的区别?

半连接:在TCP连接建立的三次握手过程中,主动发起连接请求的一方不发最后一次的ACK确认报文,使得服务器端阻塞在 SYN-RCVD(同步收到)状态。

半打开:如果TCP通信一方异常关闭(如断网、断电、进程被kill掉),而通信对端并不知情,此时TCP连接处于半打开状态,如果双方不进行数据通信,是无法发现问题的。解决的办法是引入心跳机制,设置一个保活计时器(keepalive timer),以检测半打开状态,检测到了就发送RST复位报文段,重新建立连接。

设想有这样的情况:客户端已主动与服务器建立了TCP连接。但是后来客户端的主机突然发生故障,客户端的TCP服务进程被kill掉了。显然,服务器以后就不能再收到客户端发来的数据。因此,应当有必要的措施使得服务器不要再白白地等待下去。解决的办法就是使用保活计时器。服务器每收到一次客户端的数据,就重新设置保活计时器,时间的设置通常是两小时。若两小时没有收到客户端发来的数据,服务器就发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个TCP连接。

半关闭:主动发起连接关闭请求的一方A发送了FIN结束报文段,对端B回复了ACK确认报文段后,B并没有立即发送本端的FIN结束报文段给A。此时A端处于FIN-WAIT-2(结束等待2)状态,A仍然可以接收B发送过来的数据,但是A已经不能再向B发送数据了。这时的TCP连接为半关闭状态。

半打开和半关闭的区别:半打开是指TCP通信的一端由于异常关闭,通信双方已经无法进行正常的数据传输了;半关闭是指TCP通信的其中一个方向上的连接已经关闭了,而另一个方向的连接还是正常的,仍然可以进行数据传输。

四、TCP状态转换

为了更清晰地看出TCP连接的各种状态之间的关系,下图给出了TCP的状态转换示意图。

说明:紫色框框是TCP状态,红色是服务器进程的正常状态转换,蓝色是客户端进程的正常状态转换,黑色是异常变迁,即出现问题时的状态转换。

TCP协议-TCP连接管理_第10张图片 TCP的状态转换

 4.1 服务器正常状态转换

 服务器状态转换示意图如下所示

TCP协议-TCP连接管理_第11张图片

1. TCP连接建立的三次握手阶段

  • CLOSED —> LISTEN:服务器进程调用listen()后进入LISTEN状态,被动等待客户端发起连接。
  • LISTEN   —> SYN-RCVD:服务器进程一旦收到客户端发来的连接请求SYN同步报文段,就会将该连接放入内核中的等待连接队列中,并向客户端发送ACK+SYN确认报文段,服务器进程进入 SYN-RCVD状态。
  • SYN-RCVD —> ESTABLISHED:服务器一旦接收到客户端发来的ACK确认报文段,就进入 ESTABLISHED 状态,此时TCP连接建立成功,可以进行数据传输了。

2. TCP连接释放的四次挥手阶段

  • ESTABLISHED —> CLOSE-WAIT:当客户端主动发起连接关闭,服务器收到客户端发来的FIN结束报文段,服务器向客户端返回确认报文段,服务器就进入了 CLOSE-WAIT状态,此时TCP处于半关闭状态。
  • CLOSE-WAIT —> LAST-ACK:当服务器向客户端发送关闭连接的FIN结束报文段后,服务器就进入 LAST-ACK 状态。
  • LAST-ACK —> CLOSED:当服务器收到客户端对自己发出的FIN结束报文段的确认报文段后,服务器关闭TCP连接,进入 CLOSED 状态。

4.2 客户端正常状态转换

 客户端状态转换示意图如下所示

TCP协议-TCP连接管理_第12张图片

1. TCP连接建立的三次握手阶段

  • CLOSED —> SYN-SENT: 客户端调用connect()向服务器发送连接请求的同步SYN报文段,表示想与服务器建立TCP连接,自己进入 SYN-SENT 状态,等待服务器的响应。
  • SYN-SENT —> ESTABLISHED: connnect()调用成功,客户端收到服务器的ACK确认报文段,此时客户端进入 ESTABLISHED 状态。

2. TCP连接释放的四次挥手阶段

  • ESTABLISHED —> FIN-WAIT-1: 客户端主动调用close(),向服务器发送断开连接的FIN结束报文段,自己进入 FIN-WAIT-1 阶段,等待服务器的响应。
  • FIN-WAIT-1 —> FIN-WAIT-2: 客户端收到服务器对自己发出的FIN结束报文段的确认报文段后,进入 FIN-WAIT-2 阶段,等待服务器的结束报文段。
  • FIN-WAIT-2 —> TIME-WAIT: 客户端收到服务器发来的FIN结束报文段,发送ACK确认报文段给服务器,自己则进入 TIME-WAIT 状态。
  • TIME-WAIT —> CLOSED: 客户端要等待2MSL,即2个报文最大生存时间,才进入CLOSED状态。这是为了保证客户端发送的最后一个ACK确认报文段能够到达服务器,即保证可靠地终止TCP连接,以及防止已失效的连接请求报文段出现在本连接中。

 五、基于TCP协议的socket通信流程

        Socket 又称 ”套接字”,是系统提供的用于网络通信的方法。它并不是一种协议,没有规定计算机应当怎么传递信息,只是给开发人员提供了一个发送和接收消息的接口。开发人员能使用这个接口提供的方法,发送与接收消息。Socket描述了一个 IP 和 端口号。它简化了开发员的操作,知道了对方 IP 以及 port,就可以给对方发送消息,再由服务器处理发送过来的这些消息,Socket包含了通信的双方,即客户端与服务器。

TCP协议的socket通信流程,如图所示:

TCP协议-TCP连接管理_第13张图片

5.1 TCP网络编程—服务器编程步骤

1、socket():创建一个套接字,创建成功返回一个套接字文件描述符 listen_fd。

1-1、setsockopt():可选,设置socket属性。

2、bind():绑定本地的 IP 地址和端口号信息到socket上。

3、listen():开启监听。

4、accept():接收客户端的连接请求。连接成功,返回一个连接文件描述符 conn_fd。

5、recv()、send():收发数据。或者使用 read()、write()。

6、close(conn_fd):关闭网络连接。

7、close(listen_fd):关闭监听。

5.2 TCP网络编程—客户端编程步骤

1、socket():创建一个套接字,创建成功返回一个套接字文件描述符 client_fd。

1-1、setsockopt():可选,设置socket属性。

1-2、bind():可选,绑定本地的 IP 地址和端口号信息到socket上。

2、connect():请求连接到服务器端。

3、recv()、send():收发数据。或者使用 read()、write()。

4、close(client_fd):关闭网络连接。

参考

《计算机网络(第7版-谢希仁)》第5章

《Linux高性能服务器编程》第3章

《UNIX网络编程卷1:套接字联网API(第3版)》第1部分

四、TCP三次握手、四次挥手详解

TCP连接的状态详解以及故障排查

TCP 为什么三次握手而不是两次握手(正解版)

详解TCP三次握手/四次挥手

 近 40 张图解被问千百遍的 TCP 三次握手和四次挥手面试题

你可能感兴趣的:(#,计算机网络基础,计算机网络,TCP协议,传输层)