zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

三、TCP

2023-04-18 15:35:54 时间

一、TCP协议

TCP是一种面向连接的、可靠的字节流服务。

1.1 面向连接

面向连接: 两个使用TCP的应用(通常是一个客户端和一个服务器)也彼此交换数据之前必须先建立一个TCP连接。在一个连接中,仅有两方进行彼此通信,所以广播和多播不能用于TCP。

1.2 可靠传输

TCP通过多种方式确保可靠性的机制
  1. 应用数据被分割成TCP认为最适合发送的数据块。
  2. 当TCP发布一个段后,它启动一个定时器,等待目的端确认收到这报文段。如果不能及时收到一个确认,将重发 这个报文段。
  3. 当TCP收到来自发送端的数据,它将发送一个确认。这个确认不是立即发送,一般推迟几分之一秒(大概200ms)
  4. TCP将保持它的首部和数据的校验和
  5. 既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。如果 必要,TCP将对收到数据进行重新排序,将收到的数据以正确的顺序交给应用层。
  6. TCP还能提供流量控制,TCP连接的每一方都有固定的缓冲空间。TCP的接收端只允许另一端发送接收到缓冲区所 能接纳的数据。这将防止较快主机致使减慢主机的缓冲区溢出

1.3 TCP的字节流

1. 两个应用程序通过TCP连接交换 8 bit 字节构成字节流。TCP不在字节流中插入记录标识符。我们将这称为字节流服务(byte stream service)。如果另一方的应用程序先传 10 byte,又传输 20 byte, 再传 50 byte, 连接的另一方将无法了解发送方每次发送了多少个字节。收方可以分4次发送这80个字节,每次接收20字节。一端将字节流放到TCP连接上,同样的字节流将出现在TCP连接的另一端。
2. 另外,TCP对字节流的内容不做任何解释。TCP不知道传输的数据字节流是二进制数据还是ASCII字符、EBC、DIC字符或者其他类型数据。对字节流的解释由TCP连接双方的应用层解释。

二、TCP协议报文头格式(20 bytes)

2.1 源端口和目的端口(4 byte)

源端口和目的端口,各占2个字节,分别写入源端口和目的端口。

每个TCP段都包含源端口和目的端口号,用于寻找发端和收端应用进程。他两个值加上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接

一个IP地址和一个端口号也称为一个插口(socket)插口对(socketpair)

2.2 序号(4 byte)

序号用来标识从TCP发端向TCP收端发送的数据字节流,它标识在这个报文段中的的第一个数据字节的序号。如果将字节流看作在两个应用程序间的单向流动,则TCP用序号对每个字节进行计数。序号是32bit的无符号数,序号到达2的32次方减1后又从0开始。SYN标志消耗了一个序号

2.3 确认号(4 byte)

确认号,占4个字节,是期望收到对方下一个报文的第一个数据字节的序号。例如,B收到了A发送过来的报文,其序列号字段是701,而数据长度是200字节,这表明B正确的收到了A发送的到序号700为止的数据。因此,B期望收到A的下一个数据序号是701,于是B在发送给A的确认报文段中把确认号置为701;

发送ACK无需占用任何序号,因为32bit的确认序号字段和ACK标志一样,总是TCP首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。

TCP为应用层提供全双工服务。这意味数据能在两个方向上独立进行传输。因此连接的每一端必须保持每个方向上的传输数据序号。

TCP可以表述为一个没有选择确认和否认的滑动窗口协议。我们说TCP缺少确认是因为TCP首部中的确认序号所表示发方收到字节,但还不包括确认序号所指的字节。当前还无法对数据流中选定的部分进行确认。例如: 如果在 1~1024字节已经成功收到,下一报文段中包含序号从2049~3072的字节,收端并不能确认这个新的报文段。它所能做的就是发挥一个确认序号为1025的ACK,它也无法对一个报文段进行否认。例如: 如果收到包含1025~2048字节的报文段,但它的检验和错,TCP接收端所能做的就是发回一个确认序列号为1025的ACK。

2.4 首都长度 (4 bit)

首部长度给出首部中32bit字的数目。需要这个值是因为任选字段的长度是可变的。这个字段占 4 bit,因此TCP最多有60个字节的首部。然而,没有任选字段,正常的长度是20个字节。

2.5 保留字段 (6 bit)

保留今后使用,但目前应都位0;

2.6 紧急比特URG (1 bit)

当URG=1,表明紧急指针字段有效。告诉系统此报文段中有紧急数据;

2.7 确认比特ACK (1 bit)

当ACK=1时,确认号字段才有效。TCP规定,在连接建立后所有报文的传输都必须把ACK置1;

2.8 推送比特PSH (1 bit)

当两个应用进程进行交互式通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应,这时候就将PSH=1;

2.9 复位比特RST (1 bit)

当RST=1,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接。

一般来说,无论何时一个报文段发往基准的连接出现错误,TCP都回发出一个复位报文段。

1. 发送到不存在的端口的连接请求,IP存在,但是PORT不存在的时候,服务器就会发出RST包
2. 异常终止一个连接的时候,差不多强制断开了,没有四次挥手的过程了。
  1) 终止一个连接的正常方式是一方发送FIN。有时这也称为有序释放。
  2) 有可能发送一个复位报文段而不是FIN来中途释放一个连接。有时候称这为异常释放。
  3) 异常终止一个连接对应用程序有两个优点:
    ①: 丢弃任何代发数据并立即发送复位报文段。
    ②: RST的接收方会区分另一端执行的异常关闭还是正常关闭。应用程序使用的API必须提供产生异常关闭而不是正常 关闭的手段

2.10 同步比特SYN (1 bit)

同步SYN,在连接建立时用来同步序号。当SYN=1,ACK=0,表明是连接请求报文,若同意连接,则响应报文中应该使SYN=1,ACK=1;

2.11 终止比特FIN (1 bit)

终止FIN,用来释放连接。当FIN=1,表明此报文的发送方的数据已经发送完毕,并且要求释放;

2.12 窗口 (2 byte)

TCP流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,这个值是接收方控制发送方可以连续发送未经确认的报文的数量。窗口大小是一个16bit字段,因而窗口大小最大为 65535字节,

2.13 校验和 (2 byte)

校验和覆盖了整个TCP报文段: TCP首部和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由手段进行验证。TCP校验和的计算和UDP的校验和极端相似,都会使用一个伪首部。

2.14 紧急指针 (2 byte)

只有当URG标志置为 1 时的紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的晋级方式是发送端向另一端发送紧急数据的一种方式

2.15 选项

最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size)。每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志的那个段)中指明这个选项,它指明本段所能接收的最长字段的报文段

  在某些书中,将它看做是可"协商"选项。它并不是任何条件下都可以协商的。当建立一个连接时候,每一方都有用于通告它期望接收到MSS选项(MSS选项只能出现在SYN报文段中)。如果一方不接收来自另一方的MSS值,则MSS就定为默认值536字节。
  一般来说,如果没有分段发生,MSS还是越大越好。报文段越大允许每个报文段传送的数据就越多,相对IP和TCP首部有更高的网络利用率。当TCP发送一个SYN时,或者是因为一个本地应用进程向发起一个连接,或者是因为另一端的主机收到了一个连接请求,它能将MSS值设置为外出接口上的MTU长度减去固定的IP首部和TCP首部的长度。对于一个以太网,MSS值可达到1460字节。
  如果目的IP地址为"非本地的(nonlocal)",MSS通常默认值536字节。而区分地址是本地还是非本地是很简单的,如果目的IP地址的网络号和子网号都和我们相同,则是本地的,如果目的IP地址的网络号与我们是不相同的,则是非本地的; 如果目的IP地址的网络号与我们的相同而子网号与我们不同,则可能是本地的,也有可能是非本地的。大多数TCP实现版都提供了一个配置选项,让系统管理员说明不同的子网是属于本地的还是非本地的,这个选项的设置将确定MSS可以选择尽可能的大(达到外出接口的MTU长度)或者默认值536。

 

 

 

2.16 填充

这是为了使整个首部长度是 4 字节的整数倍。

三、三次握手与四次挥手

 

 

 

3.1 三次握手

3.1.1 三次握手的含义

三次握手:为了对每次发送的数据量进行跟踪与协商,确保数据段的发送和接收同步,根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系,并建立虚连接。

3.1.2 三次握手的过程原理

1. TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;

2. TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,这是报文首部中的同部位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。

如下图: 首部长度到终止比特FIN共占用2个字节,数据为 8002 (1000 0000 0000 0010),其中SYN为0x002,即二进制数据的倒数第二位,也就是 1。SYN=1, seq=0

3. TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号

如下图: SYN=1, ACK=1, seq=0

4. TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。

如下图: SYN=0, ACK=1, seq=1

5. 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。

3.2 四次挥手

3.2.1 四次挥手的含义

即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。

3.2.2 四次挥手的过程原理

1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。

FIN=1, seq=4, Ack=4

2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。

ACK=1, Ack=5, seq=4

3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。

4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。

FIN=1, Ack=5, Seq=4

5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗ *∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。

ACK=1, Ack=5, Seq=5

6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。

3.2.3 为什么客户端最后还要等待2MSL?

3.2.3.1 2MSL存在的意义

MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。

第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

说明: 客户执行主动关闭并进入TIME_WAIT是正常的。服务器通常执行被动关闭,不会进入TIME_WAIT状态。这暗示我们终止一个客户程序,并立即重新启动这个客户程序,则这个新客户程序将不能重用相同的本地接口。这不会带来什么问题,因为客户端使用本地端口,而并不关心这个端口号是什么。 对于服务器,情况有所不同,因为服务器使用的指明端口。如果我们终止一个已经建立连接的服务器程序,并试图立即重新启动这个服务器程序,服务器程序将不会把它的者指明端口复制给它的断电没因为那个端口是处于2MSL连接的一部分。在这个重新启动服务器程序前,需要等待 1~4分钟。

3.2.3.2 平静时间的概念

1. 对于来自某个连接的焦躁替身的迟到报文段,2MSL等待可防止将它解释称使用相同插口对的新的连接部分。但是这只是在处于2MSL等待连接中的主机处于正常工作状态时间才有效。
2. 如果使用处于2MSL等待端口的主机出现故障,它会在MSL秒内重新启动,并立即使用故障前仍处于2MSL的插口对来建立一个新的连接吗?如果是这样,在故障前从这个连接发出而迟到的报文段会被错误地当做属于重启后新连接的报文段。无论如何选择重启后新连接的初始信号,都会发生这种情况。
3. 为了防止这种情况, RFC 793指出TCP在重启动后端 MSL秒内不能建立任何连接。这就是平静时间。
4. 只是极少的显现版遵守这一原则,因为大多数主机重启动的事件都比MSL秒要长。

3.2.4 FIN_WAIT_2状态

1. 在 FIN_WAIT_2 状态我们已经发出了PIN,并且另一端也已对它进行确认。除非我们实现半关闭,否则将等待另一端的应用层意识到它已收到一个文件结束符的说明,并向我们发一个FIN来关闭另一端的连接。只有当另一端的进程完成这个关闭,我们这端才会从FIN_WAIT_2状态进入到TIME_WAIT状态。
2. 这意味着我们这端可能永远保持这个状态。另一端也将处于CLOSE_WAIT状态,并一直保持这个状态知道应用层决定进行关闭。

3.3 为何是三次握手,四次挥手

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

3.4 建立连接,客户端突然宕机

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75秒发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

3.5 TCP的半关闭

TCP提供了连接的一端在结束它的发送后还能接收来自另一端的数据的能力,我们称之为半关闭。
出现半关闭的原因可以理解为: 没有半关闭,需要其他的一些技术让客户通知服务器客户端已经完成了它的数据发送,但是仍然要接收来自服务器端的数据,这是一个全双工的状态,使用两个TCP连接可以作为一个选择。

四、TCP的数据传输

约一半的TCP报文段包含成块数据(如FTP、电子邮件),另一半包含交互数据(如telnet、rlogin),如果按字节计算,成块数据与交互数据的比例约为9:1,这是因为成块数据的报文段基本上都是满长度的(通常为512字节的用户数据),而交互数据小得多

4.1 TCP的交互数据流

4.1.1 TCP的交互式输入

通常每一次按键都会产生一个数据分组,每个数据分组会产生4个报文段
(1)来自客户的交互按键
(2)来自服务器的按键确认
(3)来自服务器的按键回显
(4)来自客户的按键回显确认

 

 我们一般可以将报文段2和3进行合并,称其为经受时延的确认。因为有时候发送的字节会很小,比如说只有10个字节,但是IP首部和TCP首部已经占据40个字节了,这样多发送ACK包出来就会浪费资源了。当我们发送的字节数很大的时候,如1000个字节,就远远大于TCP前面的40个字节,这样的话服务器的ACK包就会发送出来了。

4.1.2 经受时延的确认

通常TCP在接收到数据时并不立即发送ACK,它推迟发送以便将ACK与需要沿该方向发送的数据一起发送,有时称其为数据捎带ACK,绝大多数实现采用的时延为200ms。也就是说,TCP将以最大200ms的时延等待是否和数据一起发送。

4.1.3 Nagle算法

Nagle算法要求一个TCP连接上最多只能有一个未被确认的小分组,在该小分组的确认到达之前不能发送其他小分组,这些未被发送的小分组在确认到来时以一个分组的方式发出去,该算法的优越之处在于确认到达得越快,数据发送得也越快。在希望减少小分组数目的低速广域网上,该算法会发送更少的分组。

 

 

Nagle算法的代价是产生了时延,有时候我们需要关闭Nagle算法。一个典型的例子就是X窗口系统服务器: 小消息(鼠标移动)必须无延时的发送,以便为进行某种操作的交互使用提供实时的反馈。

在一个交互注册过程中键入中断的一个特殊功能键。这个功能键通常可以产生多个字符序列。通常从ASCII码的转移(escape)字符开始。如果TCP每次得到一个字符,它很可能会发送序列中的第一个字符,然后缓存其他字符并等待对该字符的确认,这就会经常触发服务器的经受延时的确认算法,表示剩下的字符没有在200ms内发送。这对交互用户而言,

4.2 TCP的成块数据流

4.2 1 正常数据流

假设 A -> B,B要对A的数据进行确认,当有一个分组来的时候,B不立即确认,而是启动一个定时器(RFC规定要小于500ms,很多都是200ms),等待200ms,如果在这期间A又来了其他的数据就可以一起确认了 。或者A要发数据给B了,就顺带把之前的也确认了。比如下面这样,对1025的确认放到了跟2049一起。

4.2.2 滑动窗口协议

使用滑动窗口协议,接收方不必确认每一个收到的分组。在TCP中,ACK是累积的—它们表示连接方已经正确收到了一直到确认号减1的所有字节。比如上面的2049,就表示我收到了2048个字节。

 

 

1. 窗口左边沿向右边沿靠近为窗口合拢 ,该现象发生在数据被发送和确认时。
2. 窗口右边沿向右移动时允许发送更多的数据,称为窗口张开,该现象发生在另一端的接收进程读取已经确认的数据并释放了TCP的接收缓存时。
3. 窗口右边沿向左移动时称为窗口收缩。
4. 窗口左边沿不会向左移动,因为窗口左边沿受另一端发送的确认序号的控制,如果接收到一个这样的指示的话会被认为是一个重复ACK被丢弃。

注意:
  1. 发送方不必发送全窗口大小的数据
  2. 接收方在发送一个ACK前不必等待窗口被填满

 

1. 发送方不必发送一个全窗口大小的数据
2. 来自接收方的一个报文段确认数据并把窗口左边沿向右滑动。这是因为窗口的大小是现对于确认序号的。
3. 窗口的大小可以减小,但是窗口的右边沿却不能够向左移动。
4. 接收方在发送一个ACK前不必等待窗口被填满。我们有时候可以看见许多实现每收到两个报文段就会发送一个ACK。

4.2.3 窗口大小

由接收方提供的窗口的大小由接收进程控制,会影响TCP性能。
如: 发送和接受缓冲区的大小为4096个字节。

4.2.4 PUSH标志

用途:发送方用该标志通知接收方将所收到的数据(包括与PUSH一起传送的数据以及接收方TCP已经为接收进程收到的其他数据)全部提交给接收进程。
现状:大部分API不向应用程序提供通知其TCP设置PUSH标志的方法,因为一个好的TCP实现能够自行决定何时设置该标志。

4.2.5 慢启动

发送方一开始变向网络发送多个报文段,直至达到接收方通告的窗口的大小为止。当发送方和接收方处于同一个局域网时候,这种方式是可以的。但是如果在发送方和接收方之间存在多喝路由器和速率较慢的链路时,就有可能出现一些问题,一些中间路由器必须缓存分组,并由可能耗尽存储器的空间,然后发生丢包。

TCP采用慢启动算法来降低一开始就发送过多的数据到网络。

1. 慢启动为发送方的TCP增加了一个另一个窗口: 拥塞窗口,记为 cwnd。当与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为 1 个报文段(即另一端通告的报文段大小)。没收到一个ACK,拥塞窗口就增加一个报文段(cwnd以字节为单位,但是慢启动以报文段为单位进行增加)。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
2. 发送方开始时发送一个报文段,然后等待ACK。当收到该ACK时,拥塞窗口侧泳1增加到2,即可以发送两个报文段。当收到这两个报文段的ACK时,拥塞窗口就增加到4.这是一种指数增加的关系。
3. 在某些点可能达到了互联网的容量,于是中间路由器开始丢弃分组,发送方检测到丢包,相当于得到通知: 发送方的拥塞窗口开得过大,需要调整。这就是TCP的重传机制。

通过观察到新分组进入网络的速率应该与另一端返回确认的速率相同而进行工作。
拥塞窗口(cwnd) 初始化为1个报文段,每收到一个ACK,拥塞窗口增加一个报文段(cwnd以字节为单位,但慢启动以报文段为单位进行增加),发送方取拥塞窗口与通告窗口中的最小值作为发送上限。

4.2.6 成块数据的吞吐量

 

 

 

 

发送一个分组的时间取决于两个因素:
1. 传播时延,由光速、传输设备等待时间引起
2. 发送时延,取决于媒体速率(媒体每秒可传输的比特数)对于给定的两个接点之间的通路,传播时延固定,发送时延取决于分组大小。

4.2.7 拥塞

会导致拥塞的情况:
1. 数据从大管道(如快速局域网)流向小管道(较慢的广域网)
2. 多个输入流到达一个路由器,而路由器的输出流小于这些输入流的总和

 

 

4.2.8 紧急方式(URG)

1. TCP提供了紧急方式,它使一段可以告诉另一端有些具有某种方式的 "紧急数据" 已经放置在普通的数据流中。另一端被通知这个紧急数据已被放置在普通数据流中,由接收方决定如何处理。可以通过设置TCP首部中的两个字段来发出这种从一端到另一端的紧急数据已经被防止在数据流中的通知。URG比特被置为1,并且一个16bit的紧急指针被置为一个正的偏移量,该偏移量必须与TCP首部中的序号字段相加,以使得出紧急数据的最后一个字节的序号。
2. TCP必须通知接收进程,已接收到一个紧急数据指针。接收进程读取数据流,并被告知合适碰到紧急数据指针。只要从接收方当前读取位置到紧急数据指针之间有数据存在,确认为应用程序处于紧急方式。在紧急指针通过之后,应用程序便转回到正常方式。
3. TCP本身对紧急数据知之甚少。没有办法指明紧急数据从数据流的何处开始。TCP通连接传送的唯一信息就是紧急方式已经开始(TCP的首部中的URG比特)和只想晋级数据最后一个字节的指针。其他的事情留给应用程序去处理。
4. 紧急方式有什么用?比较常见的是 Telnet 和 Rlogin。 当交互用户键入中断键时候,就会使用紧急方式来完成这个功能,另一个是 FTP,当用户放弃一个文件的传输时,也使用紧急方式。Telnet 和 Rlogin从服务器到客户使用晋级方式是因为在这个方向上的数据流很可能要被客户的 TCP停止(即通告了一个大小为0的窗口)。但是如果服务器进程进入了紧急方式,尽管它不能够发送任何数据,服务器TCP也会立即发送紧急指针和URG标志。当客户TCP接收到这个通知时就会通知客户进程,于是客户可以从服务器读取其输入、打开窗口并使数据流动。

五、TCP的粘包问题

在socket网络程序中,TCP和UDP分别是面向连接和非面向连接的。因此TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端就难于分辨出来了,必须提供科学的拆包机制。

保护消息边界:把数据看成一条独立的消息在网上传输,接收端也只能接收独立的消息;而流则是无保护消息边界的,它可以一次发送多个数据,接受端也能一次接收多个数据,依据缓冲区而定。

如:有三个数据大小分别为2k,4k,6k,如果是TCP流传输,则只要缓冲区大小为12k以上,就能一次发送完毕,但如果是UDP传输,则要分3次才能传输完成

5.1 什么是TCP的粘包问题

发送端发送的数据包到达接收端时,前一个数据包的尾部和下一个数据包的头部粘在了一起,变成了一包。

5.2 什么原因造成的

1、发送方:发送使用了Nagle算法,将多个数据整合在一起发出,就有可能出现粘包

2、接收方:接收方收到发送方的数据,需要传到应用层处理,应用层没来得及处理,导致缓冲区的数据包粘在了一起

5.3 所有的粘包都需要处理吗?

1、如果发送的数据正好是同一文件里数据,那就不需要处理

2、如果发送的数据没有任何关系,就需要处理

5.4 如何处理粘包?

1、发送方:主动选择关闭Nagle算法
2、接收方:接收方不能处理这个问题,得交给应用层处理
3、应用层:只要可以在接受时,知道每个数据的长度就可以了
  a. 每条数据的首位加上特殊标记,接收方可以分辨
  b. 发送数据时附带上数据的长度
  c. 发送固定长度的数据

5.4.1 禁用Nagle算法

# 在发送方加上
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
    
# 经过测试,不对,具体该怎么处理后续解决。

5.4.2 应用层处理(知道数据长度)

https://www.cnblogs.com/wangyong123/articles/12934371.html#_lab2_3_1

六、TCP的超时重传机制

每一次传输数据时,TCP报文段的首部都会标明本报文段的起始序列号,以便对方确认。当接收方收到数据后,会去查询数据报文段首部中的序列号和数据的长度,将自己下一步应该接收的起始序号作为确认应答回复给发送方。就这样,通过序列号和确认应答号,TCP就可以实现可靠传输。但是数据和确认都有可能丢失。TCP通过再发送端发送数据时设置一个定时器来解决这种问题。如果当定时器溢出时间还没有收到接收方确认,发送端就重传该数据。对任何实现而言,关键之处就在于超时和重传的策略,及怎样决定超时时间间隔和重传的间隔。

6.1 TCP的不同定时器

对每个连接,TCP管理4个不同的定时器。
1. 重传定时器: 用于当发送一个报文时,在规定时间内,发送方需要收到另一端发送的接收报文确认。
2. 坚持(persist)定时器: 使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。
3. 保活(keepalive)定时器: 用于检测一个空闲连接的另一端是否依然还保持连接。
4. 2MSL定时器: 测量一个连接处于 TIME_WAIT 状态的时间。

6.2 异常数据的的传输过程

6.2.1 发送数据丢包

当发送端将数据发出后,会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。否则,数据可能已经丢失。

在一定时间内没有等到确认应答,发送端会认为数据已经丢失,并进行重发。这样,即使有丢包,仍能保证数据达到对端,实现可靠传输。

6.2.2 确认数据丢失

发送端未收到确认应答,不一定是数据丢失。也可能是对端已经收到数据,只是返回的确认应答在途中丢失。这种情况也会导致发送端因没有收到确认应答,而认为数据没有到达目的地,从而进行重发。

6.2.3 其他原因导致的丢包

此外,也有可能是因为一些其他原因导致确认应答延迟到达,在发送端重发数据后才收到。此时,源发送方只要按照重发机制重发数据即可,但是对于接收方来说,它会反复收到相同的数据。而为了对上层应用提供可靠的传输,必须得放弃重复的数据报文段。

6.3 确认号

TCP首部有一个确认字段,该字段实现TCP传输过程中的确认功能,该字段有两个作用:
1)表示确认字段之前的序列号的字节流均已被成功接收。
2)表示期望收到的下一个报文段的序列号,即下一个报文段的数据部分的第一个字节的序号。

6.4 TCP超时重传

超时重传指的是在发送数据报文段后开始计时,到等待确认应答到来的那个时间间隔。如果超过这个时间间隔,仍未收到确认应答,发送端将进行数据重传。这个等待时间称为RTO(Retransmission Time-Out,超时重传时间)。

还有一个时间叫RTT(Round Trip Time,报文段的往返时间),这个时间间隔是指数据报文段发出的时间戳与收到确认应答的时间戳的时间之差。

6.4.1 超时重传时间的确定

  超时重传的概念很简单,但是重传时间的选择却是TCP最复杂的问题之一。因为网络环境的不同,时间会有差异。如果把超时重传的时间间隔设置得太短,就会引起很多报文段的不必要的重传,使网络负荷增大;如果把超时重传设置得多长,则又使网络的空闲时间增大,降低了传输效率。

  那么,TCP的超时计时器的超时重传时间究竟应设置为多大呢?

  TCP采用了一种自适应的算法,它记录一个报文段发出的时间戳,以及收到相应的确认应答的时间戳。这两个时间戳之差就是该报文段的往返时间RTT。

算法步骤如下:

(1)计算一个加权平均往返时间RTTs(这也称为平滑的往返时间,s 表示 smoothed。因为进行的是加权平均,因此得到的结果更加平滑)。
  第1次测量得到的RTT样本值,作为RTTs的初始值。从第2次开始,新的RTTs值使用如下的公式计算得到

    新的RTTs = (1 - α) × (旧的RTTs) + α × (新的RTT样本)
    <说明1> 新的RTT样本需要重新测量得到。
    <说明2> 系数 α 的取值范围为[0, 1)。
    <说明3> RFC 6298文档推荐的 α 值为 1/8,即 0.125。

 

(2)基于计算得到的RTTs时间设置超时重传时间RTO。显然,超时计时器设置的超时重传时间RTO应略大于上面得到的加权平均往返时间RTTs。

  RFC 6298 建议使用下式计算RTO:
    RTO = RTTs + 4 × RTTd
    <说明> RTTd 是 RTT的偏差的加权平均值,它与 RTTs 和 新的 RTT 样本之差有关。

  RFC 6298 建议这样计算RTTd。当第一次测量时,RTTd的值取为测量到的RTT样本值的一半。在以后的测量中,则使用下式计算加权平均的RTTd:
    新的RTTd = (1 - β) × (旧的RTTd) + β × |RTTs - 新的RTT样本值|
    <说明1> 系数 β 的取值范围为[0, 1),它的推荐值为 1/4,即 0.25。

6.4.2 RTT样本的测量

上面所说的RTT样本的测量,实现起来相当复杂。示例如下:

如下图所示,发送方发出一个报文段后,设定的重传时间到了,还没有收到确认应答。于是重传报文段。经过一段时间后,收到了确认报文段。现在的问题是:如何判定此确认报文段是对先发送的报文段的确认还是对后来重传的报文段的确认?由于重传的报文段和原来的报文段完全一样,因此源主机在收到确认后,就无法做出正确的判断,而正确的判断对确定加权平均RTTs的值关系很大。

 

若收到的确认是对重传报文段的确认,但却被源主机当成是对原来的报文段的确认,则这样计算出的RTTs和超时重传时间RTO就会偏大。若后面再发送的报文段又是经过重传后才收到确认报文段,则按照此方法得出的超时重传时间RTO就会越来越长。

同样的道理,若收到的确认是对原来的报文段的确认,但被当成是对重传报文段的确认,则由此计算出的RTTs和RTO都会偏小。这就必然导致报文段过多地重传。这样就有可能使RTO的值越来越短。

根据以上所述,Karn 提出了一个算法:在计算加权平均 RTTs 时,只要报文段重传了,就不采用其往返时间样本。这样得出的加权平均 RTTs 和 RTO就比较准确。

但是,这又可能引起新的问题。设想出现这样的情况:报文段的时延突然增大了很多。因此,在原来得出的重传时间内,不会收到确认报文段。于是就重传报文段。但根据 Karn 算法,不考虑重传的报文段的往返时间样本。这样,超时重传时间就无法更新。

因此需要对 Karn 算法进行修正。方法是:报文段每重传一次,就把超时重传时间RTO增大一些。典型的做法是取新的重传时间为旧的重传时间的 2倍。当不再发生报文段的重传时,再根据上面给出的公式计算超时重传时间RTO的值。实践证明,这种策略较为合理。

总之,Karn 算法能够使TCP区分开有效的和无效的往返时间样本,从而改进了往返时间RTT的估测,使超时重传时间的计算更加合理。

当然,数据不会被无限、反复地重发。当达到一定重发次数后,如果仍然没有任何确认应答返回,就会判断为网络或对端主机发生了异常,TCP模块就会强制关闭连接,并且通知上层应用通信异常强行终止。

6.5 选择确认SACK

问题:如果接收方收到的报文段无差错,只是未按序到达,中间还缺了一些序号的字节数据,那么能否设法只重传缺少的数据而不重传已经正确到达接收方的数据呢?

答案是可以的,选择确认就是一种可行的处理方法。下面我们用一个例子来说明选择确认的工作原理。
当TCP的接收方在接收对方发送过来的数据字节流的序号不连续时,结果就形成了一些不连续的字节块,如下图所示。

 

 

可以看出,序号 1 ~ 1000 已收到了,但序号 1001 ~ 1500 没有收到。接下来的字节流又收到了,可是又缺少了 3000 ~ 3500。再后面从 4501 起又没有收到。也就是说,接收方收到了和前面的字节流不连续的两个字节块。如果这些字节块都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据。

从上图可以看出,和前后字节不连续的每一个字节块都有两个边界:左边界和右边界。因此在图中用四个指针标记这些边界。请注意,第一个字节块的左边界 L1=1501,而右边界 R1 = 3001 而不是 3000。这就是说,左边界指出字节块的第一个字节的序号,但右边界减 1 才是字节块的最后一个字节序号。同理,第二个字节块的左边界 L2=3501,而右边界 R2 = 4501。

我们知道,TCP报文段的首部没有哪个字段能够提供上述这些字节块的边界信息。RFC 2018 规定,如果要使用选择确认 SACK 功能,那么在建立TCP连接时,就要在TCP 首部的选项字段(即Options)中加上“允许SACK”的选项,而通信双方必须都事先约定好。如果使用选择确认,那么原来首部中的“确认号”字段的用法仍然不变。只是以后在TCP报文段的首部中的“选项”字段中都增加了SACK选项,以便报告收到不连续的字节块的边界。

由于TCP首部的“选项”可选字段的长度最多只有 40 字节,而指明一个边界就要用掉4字节(因为序号有32位,需要使用4个字节表示),因此在选项中最多只能指明4个字节块的边界信息。这是因为4个字节块有8个边界,因为需要用 8 * 4 =32个字节来描述。另外还需要两个字节,一个字节用来指明是 SACK 选项,另一个字节是指明这个SACK选项要占用多少字节。如果要报告五个字节块的边界信息,那么至少需要 42 个字节。这就超过了选项字段的40字节的上限。具体是如何规定的,可以参考 RFC 2018。示例表示如下:

 

kind = 4:占1字节,表示选项类别为 SACK。length:占1字节,表示SACK选项总共占用的字节数。该长度包括了kind字段和length字段占据的2个字节。

kind = 5:占1字节,表示这是SACK实际工作的选项。legnth:N 表示字节块数量;每个字节块有两个边界信息,因此需要8字节;+2 表示加上kind和length这两个字节。

6.6 TCP的坚持定时器

ACK的传输并不可靠,也就是说,TCP不对ACK报文段进行确认,TCP只确认哪些包含有数据的ACK报文段

6.6.1 坚持定时器的由来

  TCP通过让接收方指明希望从发送方接受的窗口大小来进行流量控制。设置窗口大小为0可以组织发送方传送数据,直至窗口变为非0为止。
  如果接收方向发送方通告了一个为0的接口,然后又向发送方通告了窗口更新,恰好这个确认丢失了,那么接收方等待接收数据,发送方等待允许他继续发送数据的窗口更新,就会形成死锁。为了防止这种死锁,发送方使用一个坚持定时器来周期性地向接收方查询,以便发现窗口是否增大。这些从发送方发出的报文段称为窗口探查。
  计算坚持定时器的定时时间使用了普通的TCP指数退避。窗口探查包含一个字节的额数据,TCP总是允许在关闭连接前发送一个字节的数据。返回的窗口为0的ACK不是确认该字节,因此该字节被持续重传。

6.6.2 糊涂窗口综合症

接收方通告一个小窗口,发送方通过这个小窗口发送少量的数据,这个数据量甚至小于报文段的长度,TCP的传输效率低到了极点。

避免措施
  接受方:
    ①接受方不通告小窗口。通常的做法是除非窗口可以增加一个报文段或者可以增加接受方缓存的一半,否则不予通告窗口更新。

  发送方-满足下述条件之一再发送数据:
    ① 可以发送一个满长度的报文段
    ②可以发送至少是接受方通告窗口大小一半的报文段
    ③ 可以发送任何数据并且不希望接收ACK(没有未被确认的数据)或者该连接上不能使用Nagle算法

6.6.3 坚持定时器的工作流程

1. 发送端收到0窗口通告后,就启动坚持定时器,并在定时器溢出的时候向客户端查询窗口是否已经增大。
2. 在定时器未到,就收到非零通告,则关闭该定时器,并发送数据。
3. 若定时器已到,还没有收到非零通告,就发探查报文。
4. 如果探查报文ACK的通告窗口为0,就将坚持定时器的值加倍,TCP的坚持定时器使用1,2,4,8,16……64秒这样的普通指数退避序列来作为每一次的溢出时间,重复1、2、3步,如果通告窗口非零,发送数据,关闭定时器。

6.7 TCP的保活定时器

6.7.1 保活定时器的由来

现实中可能存在一种空闲的TCP连接--连接的双方都没有向对方发送数据,则在两个TCP模块之间不交换任何信息,这意味我们可以启动一个客户和服务器建立连接,然后离去很长时间,而连接依然保持。而且中间的路由器可以崩溃或重启,只要两端的主机没有重启,则依然保持连接建立。

服务器为了知道客户机是否崩溃或关机等情况,从而引入了保活定时器来探查这种情况。

6.7.2 工作原理

如果一个给定的连接在2小时内没有任何动作,那么服务器就向客户发送一个探查报文段。客户主机必须处于以下4个状态之一:

1. 客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方的正常工作的,服务器在2小时内将保活定时器复位。
2. 客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应,服务器将不能收到对探查的响应,并在75秒后超时,总共发送10个探查,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
3. 客户主机崩溃并已经重新启动。这是服务器将收到一个对其保活探查的响应,但这个响应是一个RST复位,使得服务器终止这个连接。
4. 客户主机正常运行,但是从服务器不可达。这与状态2相同,因为TCP不能够区分状态4与2之间的区别,它所能发现的就是没有收到探查的响应。

服务器不用关注客户主机被关闭或者重新启动的情况。当客户机被关闭之后,所有的应用进程也被终止,这会使客户的TCP在连接上发出一个FIN。接收到FIN会使服务器的TCP向服务器进程报告文件结束,从而服务器检测到了这种情况。

七、名词解释

LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
  1.可靠的实现tcp全双工连接的终止;
  2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)