TCP报文段结构
p154《计算机网络》
MSS限制了报文段数据字段的最大长度。当TCP发送一个大文件时,TCP通常会将该文件划分成长度为MSS的若干块,此数据长度加上TCP首部长度才等于整个TCP报文段的长度。
MSS只出现在SYN=1的报文段中。
序号和确认号
一个报文段的序号是该报文段首字节的字节流编号。
假定数据流由一个500000字节的文件组成,其MSS为1000字节,数据流的首字节编号是0。
则该TCP将为该数据流构建500个报文段。给第一个报文段分配序号为0,第二个报文段分配序号为1000,第三个报文段分配序号为2000,以此类推,每一个序号被填入到相应的TCP报文段首部的序列号字段中。
确认号
主机A填充进报文段的确认号是主机A期望从主机B收到的下一字节的序号。
eg:主机A收到一个来自主机B的包含0-535的报文段,以及另一个包含字节900-1000的报文段。由于某种原因,主机A还没有收到字节536-899的报文段。
此时主机A为了重新构建主机B的数据流,仍在等待字节536-899,因此,A到B的下一个报文段在确认字段中包含536。
因为,TCP只确认该流中至第一个丢失字节为止的字节,所以TCP被称为提供累积确认。
确认应答机制
ACK(确认号)就是主机正在等待的数据的下一个字节序号。
eg:Client向Server发送了1005字节的数据,Server返回给Client的确认号是1004,则说明服务器只收到1- 1003的数据,1004 和 1005都还没收到。
超时重传机制
TCP采用超时重传机制来处理报文段的丢失问题。显然,超时时间间隔必须会大于该连接的往返时间RTT,即从一个报文段发出到它被确认的时间,否则会造成不必要的重传。但是这个时间间隔应该设置多长呢?
超时时间的确定
- 最理想的情况下,找到一个最小的时间,保证“确认应答一定在这个时间内返回”
- 但是这个时间的长短,随着网络环境的不同有所差异
- 如果时间设置的太长,会影响整体的重传效率
- 如果时间设置的太短,有可能会频繁发送重复的包。
TCP为了保证无论何时都能高性能的通信,因此会动态计算这个最大的超时时间
- 超时以500ms为一个单位进行控制,每次判定超时重发的时间都是500ms的整数倍;
- 如果重发一次之后,仍然得不到应答,等待2 * 500ms后再次进行重传
- 如果仍得不到应答,等待4 * 500ms进行重传,以此类推,以指数形式递增;
- 累积到一定的重传此次数,TCP认为该网络或者对端主机出现异常,强制关闭连接。
滑动窗口
【窗口概念】
窗口大小指无需等待确认应答就可以继续发送数据的最大值
下图的窗口大小就是4000个字节(四个段)
- 发送前4个段的时候,不需要等待任何ACK,直接发送;
- 收到第一个ACK确认应答后,窗口向后移动,继续发送第5678段的数据…
因此,这个窗口不断向后移动,叫做滑动窗口。
操作系统内核为维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有得到应答,只有ACK确认应答过的数据,才能从缓冲区删掉。
如果出现丢包,应该如何进行重传?
【情况1:数据包已经收到,但确认应答ACK丢失了】
部分ACK丢失并无大碍,因为可以通过后续的ACK来确认对方已经收到了哪些数据包。
(后续的ACK,即窗口范围内发送数据的“ACKS”中被丢失的ACK后面的ACK。能够证明接收端已经接收到此确认号之前的数据的原因是:假如接收端未收到发送端某一段的数据,接收端就会发送此段他想要接收到的数据的第一个字节编号,连发三次,发送端就会重发该数据,直到接收端接收到,标有正确想接收序号的ACK才能正常发送)
【数据包丢失】--------- 快速重传 p162
-
当TCP接收方收到一个具有这样序号的报文段时,即其序号大于下一个所期望的、按序的报文段(2000 > 1001)此时他检测到了数据流中的一个间隔,这就是说明有报文段丢失。 -
此时接收端将其接受到的最后一个按序字节数据进行重复确认(产生一个冗余ACK:1001),如果发送方接受到对相同数据的3个冗余ACK,就说明这个报文段(1001-2000)已经丢失,TCP就快速执行重传(在该报文段的定时器过期之前重传丢失的报文段)
因为2001-7000接收端已经收到了,被放到接收端操作系统内核的接收缓冲区中。
- 完成重传,接收端接收到1001-2000的数据后,接收端发送ACK的值y(7001:表示已接收1-7000字节的数据),接收端接收后,由于y值>SendBase(发送端最早未被确认的字节的序号:1001),则更新SendBase(SendBase = 7001)。【由于TCP采用了累积确认,所以y确认了字节编号在y之前的所有字节都已经收到】
流量控制
接收端处理数据的速度是有限的,如果发送的太快,导致接收端的缓冲区被填满,这个时候如果发送端继续发送,就会导致丢包,进而引起丢包重传等一系列连锁反应。
因此,TCP支持根据接收端的处理能力来决定发送端的发送速度,这个机制就叫做:流量控制
-
接收端将自己可以接受的缓冲区大小放入TCP首部中的”窗口大小“字段; -
通过ACK通知发送端 -
窗口越大,说明网络的吞吐量越高; -
接收端一旦发现自己的缓冲区快满了,就会将窗口的大小设置成一个更小的值通知发给发送端; 发送单就收到这个窗口的大小后,就会减慢自己的发送速度; -
如果接收端缓冲区满了,就会将窗口设置为0; 这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,让接受端把窗口大小再实时告诉发送端。
拥塞控制
由于网络上有很多计算机,可能当前的网络状态比较拥堵,在不清楚网络状态的情况下,如果一开始就发送大量的数据,很有可能引发一系列问题。
因此,TCP引入慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态后,再决定按照多大的速度传输数据。
在此引入概念:拥塞窗口
- 发送的时候,定义的拥塞窗口大小为1;
- 每次收到一个ACK应答,拥塞窗口加1;
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小值作为实际发送窗口;
拥塞窗口的增长速度,是指数级的;
“慢启动”只是指初始时慢,但是增长速度非常快;
为了不增长的那么快,此处引入一个名词叫做慢启动阈值,当拥塞窗口的大小超过这个阈值的时候,就不按照指数方式增长,而按照线性方式增长。
- 当TCP开始启动时,慢启动阈值等于窗口最大值
- 每次超时重发的时候,慢启动阈值都会变成原来的一半,同时拥塞窗口置回1。
少量的丢包,我们仅仅是触发超时重传;
大量的丢包,我们就认为是网络拥塞;
当TCP通信开始后,网络吞吐量会逐渐上升;
随着网络发生拥堵,吞吐量会立即下降。
通塞控制:归根结底是TCP协议想尽可能把数据传输给对方,但是又避免给网络造成太大压力的折中方案。
连接管理机制
三次握手
-
第一步:Client的TCP生成【不包含应用层数据的报文段(报文段中SYN被置为1,序号字段(随机选择一个初始序号client_isn))------ SYN报文段】将该报文段封装在一个IP数据报中,并发送给Server。 -
第二步:假设包含TCP SYN报文段到达Server主机,Server会从该数据报中提取出SYN报文段,并为该TCP连接分配TCP缓存和变量。并想Client发送允许连接的报文段【该报文段同样不包含应用层数据,但却在报文段首部包含三个重要信息:SYN:1;确认号字段被置为 client_isn + 1; Server选择自己的初始序号server_isn】------------SYNACK报文段
这个允许连接的报文段表明了:我收到了你发起建立连接的SYN分组,该分组带有初始序号client_isn。我同意建立此连接,我自己的初始序号是server_isn
-
第三步:在收到SYNACK报文段后,Client要给该连接分配缓存和变量。Client主机向Server发送另一个报文段,这个最后一个报文段对服务器允许连接的报文段做出了确认【该报文段的确认字段:server_isn + 1,因为连接已经建立了,所以SYN被置回为0。
该三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据
注意两端状态的变换时机。
【问题1】为什么不用两次?
主要是为了防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送的第一个请求连接并且没有丢失,只是因为在网络中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时之前滞留的那一次请求连接,因为网络通畅了, 到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的费。 如果采用的是三次握手,就算是那一次失效的报文传送过来了,**服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。**由于服务器收不到确认,就知道客户端并没有请求连接。
【问题2】为什么不用4次?
因为三次已经满足需要了,四次就有点多余了。
四次挥手
数据传输完后,双方都可以释放连接,此时Client和Server都是出于ESTABLISHED状态,假设Client主动断开连接,Server被动断开连接。
-
Client进程发出连接释放报文段,并停止发送数据。释放报文段首部,FIN = 1 , 其序列号seq=u (等于前面已经传送过的数据的最后一个字节序号+1),此时Client进入FIN-WAIT-1(终止等待1)状态。 -
Server收到连接释放报文段,发出确认报文段,ACK= 1 ,确认序列号为u+1 ,并且带上自己的序列号seq = v ,此时Server进入了CLOSE-WAIT状态
TCP服务器通知高层的应用程序,Client向Server的方向就释放了,这时候处于半关闭状态,即Client已经没有数据要发送了,但是Server若发送数据,Client依然要接收,这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间
-
Client收到Server的确认请求后,此时Client进入**FIN-WAIT-2(终止等待2)**状态,等待Server发送连接释放报文段(在这之前还要接收Server发送的最终数据) -
Server最后的数据发送完毕后,就像Client发送连接释放报文段,FIN=1 ,确认序列号为v+1 ,由于在半关闭状态,Server很可能又发送了一些数据,假定此时的序列号为seq=w ,此时,Server就进入了LAST-ACK(最后确认)状态,等待Client的确认。 -
Client收到Server的连接释放报文段后,必须发出确认,ACK=1,确认序列号为w+1,而自己的序列号是u+1,此时Client进入了TIME-WAIT状态。
注意此时的TCP连接还没有释放,必须经过2*MSL(最长报文段寿命)的时间后,当Client撤销相应的TCB后,才进入CLOSED状态。
-
Server只要收到了Client发出的确认,就立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。
可以看到,Server结束TCP连接的时间要比Client早一些。
TCB,也叫传输控制块的数据结构把发给不同设备的数据封装起来,我们可以把该结构看做是信封。一个TCB数据块包含了数据发送双方对应的socket信息以及拥有装载数据的缓冲区。在两个设备要建立连接发送数据之前,双方都必须要做一些准备工作,分配内存建立起TCB数据块就是连接建立前必须要做的准备工作。
【问题1】为什么最后Client还需要等待2*MSL的时间呢?
- MSL:他是任何报文在网络上可以存在的最长时间,超过这个时间的报文将被丢弃。
- 第一,保证Client发送的最后一个ACK报文能够到达Server,因为这个ACK可能会丢失,站在Server的角度来看,我已经发送了FIN报文请求断开了,Client还没给我回应,应该是我发送的请求断开报文没有收到,于是我在重新发送一次,而Client就能在这2MSL时间段内收到这个重传报文,接着给出回应报文,并且重启2MSL计时器。
- 第二,Client发送完最后一个确认报文后,在这个2*MSL时间中,就可以使本连接持续的时间内所产生的所有报文都从网络中消失,这样新的连接就不会出现旧连接的请求报文。
【问题2】为什么建立连接是三次握手,关闭连接是4次挥手?
关闭连接时,Server 收到了 对方的FIN报文段时,仅仅表示对方不再发送数据了(但还能接收数据),而且Server自己也还未把全部的数据都发送给了对方。所以Server此时可以立即关闭,也可以将未发送的数据发送完后,再发送FIN报文段给对方,表示同意现在的关闭连接。
因此,己方ACK和FIN一般都会分开发送。
【问题3】如果已经建立了连接,但是Client突发故障了怎么办?
TCP设置有一个保活计时器。Server每收到一次Client的请求后都会重新复位这个计时器,时间通常是设置为2小时,如果达到了超时时间,Server就会主动发送一个探测报文段,以后每隔75分钟就发送一次。如果连续发送10个探测报文仍然没有反应,则说明Client端出现了故障,接着就关闭连接。
理解TIME_WAIT状态
主动关闭连接的一方,在双方都发过FIN后要处于TIME-WAIT状态,等待2*MSL的时间后才能回到CLOSED状态。
假定ACK丢失,TIME-WAIT状态使得主动关闭的一方重传确认报文。在TIME-WAIT状态中所消耗的时间就是与具体实现有关的,典型的值有20秒、1分钟、2分钟。经过等待后,连接就正式关闭,之前处于TIME-WAIT状态端的所有资源将被释放。
提高网络利用率的规范
延迟确认应答
接收数据的主机如果每次都立即回复ACK的话,可能会返回一个较小的窗口(因为刚才接收完数据,此时的缓冲区比较满)。
当某个发送端收到这个小窗口的通知以后,会以此窗口的大小为上限发送数据,从而又降低了网络的利用率。
因此引入一个方法,那就是收到数据以后并不立即返回ACK,而是延迟一段时间的机制。
- 在没有收到2x最大段长度的数据为止不做确认应答(收到两个包就立即返回确认应答的情况)
- 其他情况下,最大延迟0.5秒发送确认应答。
事实上,大可不必每一个数据段都要进行一次确认应答,TCP采用滑动窗口的控制机制,因此通常确认应答少一些也无妨。TCP文件传输中,绝大多数是每两个数据段发送返回一次确认应答。
捎带应答
在同一个TCP包中及发送数据又发送确认应答的一种机制。由此,网络的利用率会提高,计算机的负荷也会减轻。
不过,确认应答必须要等到应用处理完数据并将其作为回执的数据返回为止,才能进行捎带应答。
面向字节流
-
创建一个TCP的socket,同时在内核中创建一个发送缓冲区 和 一个接受缓冲区; -
调用write时,数据会先写入发送缓冲区中; -
如果发送的字节数太大,会被拆分成多个TCP数据包发出; -
如果发送的字节数太少,就会先在缓冲区里等,等到缓冲区大小差不多了,或者到了其他适合的时机再发送出去; -
接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区; -
然后应用程序可以调用read从接收缓冲区中拿数据; -
另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,对于这样一个连接,既可以读数据也可以写数据,这个概念叫做全双工。
粘包问题
粘包问题中的“包”是指应用层的数据包
在TCP协议头中,没有如同UDP一样的“报文长度”字段,但是有一个序号字段。
那么从应用层看到的这一连串字节数据,就不知道从哪个部分开始到哪个部分是一个完整的应用数据包。
此时数据之间因为没有了边界,产生了粘包问题。
如何避免粘包问题?
应该明确两个包之间的边界
-
对于定长的包 应确保每次都按照固定的大小读取即可 -
对于变长的包 可以在数据包的头部,约定一个数据包总长度的字段,从而就知道了包的结束位置 还可以在包和包之间使用明确的分隔符来作为边界(应用层协议,是程序员自己来定义的,只要保证分隔符不和正文冲突即可)
TCP异常情况
【问题一】程序打开连接后,没有执行关闭,进程就结束了。被这个进程创建链接后续会怎么办?
答:会被关闭;OS在进程结束之后,会统一回收分配给进程的资源。所以OS会关闭连接时正常走四次挥手流程。
【问题二】如果进行了电脑的重启/关机操作呢?这台电脑上的所有打开的连接,有四次挥手么?
答:有的;
【问题三】强制关机(断电)
答: 如果断电强制关机,这台主机的连接就没有了,没有关闭过程。
【问题续】对方看到的是什么情况?
- 对方在尝试写数据 —— 必然要等待应答
- 如果关机后没有重启:则应答永远收不到。
- 关机后又重启了:会受到一个Reset消息 —— 异常结束
- 对方在尝试读取数据 —— 永远收不到消息,无法辨别是没有消息过来还是对方已经死
创建链接后续会怎么办?
答:会被关闭;OS在进程结束之后,会统一回收分配给进程的资源。所以OS会关闭连接时正常走四次挥手流程。
【问题二】如果进行了电脑的重启/关机操作呢?这台电脑上的所有打开的连接,有四次挥手么?
答:有的;
【问题三】强制关机(断电)
答: 如果断电强制关机,这台主机的连接就没有了,没有关闭过程。
【问题续】对方看到的是什么情况?
- 对方在尝试写数据 —— 必然要等待应答
- 如果关机后没有重启:则应答永远收不到。
- 关机后又重启了:会受到一个Reset消息 —— 异常结束
- 对方在尝试读取数据 —— 永远收不到消息,无法辨别是没有消息过来还是对方已经死
(所以业务上要做的调整,读消息的时候增加超时时间,达到超时时间之后,主动给对方发一个消息,一般把这种没有业务信息,只是定时给对方发的数据,为了确认双方安全的消息,称为心跳信息)
|