1、概述

  • 主要特点:
  1. 面向连接:应用程序在使用TCP之前,必须建立TCP连接,传送数据完毕之后必须释放已经建立的TCP连接
  2. 一条TCP连接是点对点的
  3. 全双工通信:允许通信双方随时发送信息。TCP的两端设有发送缓冲区和接受缓冲区,应用程序把数据交给缓冲区后就可以做其他事情了。
  4. 可靠的交付:数据报无差错、不丢失、不重复、按序到达
  5. 面向字节流:应用程序交给TCP的是数据块,但是TCP只把它看作无结构的字节流。接收方收到的字节流和发送方发送的字节流要完全相同。而不用关注数据块的大小、数量是否一致。
  • 一点注意:
(1)、TCP的连接时一条虚连接,而不是真正的物理连接
(2)、TCP并不关心应用进程一次发送到缓存的报文有多大,而是根据接收方的窗口值和当前网络的拥塞程度来决定一个报文应该含有多少字节。如果传入缓冲区的数据过长,就划分的短一些再发送,如果太短,也可以等待积累足够长再发送。这也是它面向字节流的具体体现了,只关注最终字节的发送接收情况,而不是发送接收方的数据块。这些数据块完全可以合并或者划分
  • TCP的连接
  1. 连接是TCP最基本的抽象,面向连接是TCP的基本特性之一。而且这个连接是点对点的。
  2. 每个端点是一个套接字,即 (ip:port)

2、可靠传输的工作原理

  • 基本思想
  1. 理想传输条件的两个特点:传输信道不产生任何差错;无论传输方速度多快,接收方始终来得及接受。
  2. 实际传输不具备这样的条件,TCP想让传输可靠要做的事:当传输出错时,让接收方及时重传数据;当接收方来不及处理收到的数据时,及时告知发送发适当降低发送数据的速度。

2.1、可靠传输的简单实现:停止等待协议

  • 操作方式:每发送完一个分组就停止发送,等待接收方的确认,收到确认后再继续发送数据。
  • 如果出现差错:比如发送的报文中途丢失了或者报文出错,被接收方丢弃了,停等协议并不会使接收方做什么。只需要在发送方设置一个超时计时器,如果超过这个计时器的时间还没有收到回复,就会重传一次数据报来保证传输的可靠性。需要注意的是:
(1). 发送方发送数据时必保留副本以防止需要重传
(2). 发送分组和确认分组都要编号,明确是那个分组没有收到确认。(这点可能不是太需要,不是只有收到确认才会发送下一条报文嘛。。。但是对于TCP的滑动窗口时必须的)
(3). 超时计时器的时间必须要比数据在分组传输的平均往返时间长一些。
  • 确认丢失和确认迟到:接收方对报文的确认丢失或者迟到了。这种情况下:比如发送方发送了 M 报文
  • 发送方:如果确认丢失和确认迟到了,那么之前就已经重新发送这条报文了,在收下迟到的确认之后什么也不做。
  • 接收方:之前已经确认过 M 报文了,但是给发送方的对 M 的确认丢失了,对于重新发送的 M 直接丢弃,再次发送确认报文,接收方收到确认报文之后才能继续发送下一个报文。(我觉得这才是要编号的原因,接收方得知道这条报文是否之前已经确认过了,如果已经确认了就得丢弃了)

java双工通信通道 tcp双工通信_TCP

  • 这样就实现了一种简单的可靠传输(相比TCP的可靠传输而言,这里只是举例介绍可靠传输的必要条件),这种被称为ARQ(自动重传请求),即:重传的请求时自动进行的,接收方不需要请求发送方重传某个出错的分组(TCP却不是这样)。有兴趣可以了解下连续ARQ协议。

3、TCP报文的首部格式

  • 简述:TCP虽然时面向字节流的,但传输的数据单元仍是报文段。TCP首部的前 20 个字节是固定的,后面 4*n 个字节是根据需要增加的选项,所以,首部最小长度是 20 个字节。TCP 的各个功能都与它的首部密不可分。

java双工通信通道 tcp双工通信_数据_02

  • 一些重要的首部字段(源端口和目的端口就不说了,占16位(注意是端口不是IP,IP 是在 IP 数据包的首部的,TCP 封装在IP数据包里面传输))。
  1. 序号:占32位,在增加到 2^31-1之后,会重新归为0(我们对这个数据的要求是在一次TCP连接中尽量不要出现重复的序号,防止新报文和迟到的报文混淆。在2.5G/S的高速网络中,不到14秒就会用尽序号,加上路径的延迟,可能使两者混淆。后来在长度可变的选项字段加上了时间戳,用以区分可能的混淆和计算往返时间)。这是传输的每一个字节的序号,按序编号的。
  2. 确认号:占32位,是期望收到对方的下一个报文段的第一个数据字节的序号。比如 A->B 长度位200字节(500-700)的数据,那么 B->A 的报文确认号为701
  3. 数据偏移:占4位,标识数据部分距离头部的偏移值,前面说过头部长度至少是20字节,还有可变部分。单位是32位,即4字节。因此最大偏移值是60字节,即头部的可选部分最大长度是40字节。
  4. 紧急URG字段:1位,优先传送。当URG=1 时,发送方把紧急数据插入到报文段数据的最前面。注意,接收方并没有对它做特殊处理。
  5. 确认 ACK 字段:1位。仅当ACK=1时,确认号有效,当TCP连接建立后,ACK字段必须是1。注意这里是一个TCP连接建立的相关字段。
  6. 复位RST : 1位,RST=1时表示TCP连接中出现了严重差错,必须释放连接重新建立。也用来拒绝一个非法报文或者拒绝打开一个连接。注意这里是一个 TCP释放连接的相关字段。
  7. 同步SYN:SYN=1 表示一个连接请求或者连接接受报文。SYN=1 ACK=0 时,表示连接请求,SYN=1 ACK=1 时,连接接受。
  8. 终止 FIN:用来释放一个连接。FIN=1 要求释放连接。注意是要求。
  9. 窗口:16位,这个值告诉接收方 发送方的接收窗口的缓存的剩余大小,以作为接收方再次给发送方发送数据时窗口之设置的依据(应该还有考虑网络拥塞情况)。总之,就是告诉对方,你下次给我发送数据窗口值要设置的大小,作为依据。
  10. 紧急指针:指出紧急字段末尾在数据报中的位置。后面都是普通数据了。仅当 URG=1 时有效。即使窗口值=0 ,紧急数据仍可发送。

4、TCP 可靠传输的实现

  • 滑动窗口以字节为单位, 下面的讨论只考虑数据的单向传输,即只考虑发送方的发送窗口和接收方的接受窗口而不管发送方的接收窗口和接收方的的发送窗口。

4.1、发送窗口和接收窗口

java双工通信通道 tcp双工通信_缓存_03

  • 发送窗口的机制
  1. 发送窗口将A 的报文分为四部分:已发送并收到确认的(不需要保留了)、发送窗口内的、不允许发送的(接收方未为这部分数据保留缓存空间)
  2. 发送窗口内部的数据又可以分为 已发送未确认的、允许发送但尚未发送的(可用窗口)
  3. 发送窗口的位置由发送前沿和后沿共同确定。前沿是右边,后沿是左边。后沿只能不动(未收到新的确认)或者前移(收到新的确认)。前沿只能不动(1、没有收到新的确认并且窗口值大小不变 2、收到新的确认并且窗口值变小了)或者前移(除了不动的两种情况,比如收到新的确认并且窗口值不变或者变大),也可以后移,但这样显然容易造成错误
  4. 上图中发送窗口有三个指针将数据分为四段,各个区域的划分前面已经说过了。显然窗口越大,发送数据效率越高。
  • 接收窗口的机制
  1. 只能对按序接收到的数据进行确认,比如上图32、33 号字节未按序收到,B-> A 的应答报文中序号仍是31(按序接收的最大值是30)。
  2. 当31号报文正确收到后,B的接收窗口的后沿向前移动3个字节,接着 B ->A 的确认报文中 序号变为 34。A 收到 B 的确认后,把整个窗口向前移动3个字节,也就是把发送窗口的后沿和前沿都向前移动3个字节,但是P2指针的位置暂时并不变,接着A 继续发送报文直至 P2 和P3重合,即可用窗口的值为0,停止发送,等待 B 的确认,B确认后又回到这一点的开头位置。
  3. 如果确认丢失或者迟到了。像前面的ARQ一样,A会设置一个超时计时器,超过时间还没有收到确认的话就会从P1 的位置重发。依次来保证数据传输的绝对可靠。

4.2、缓存和窗口的关系

  • 数据传输流程:发送方的应用进程把字节流写入TCP的发送缓存,接收方从TCP的接收缓存里面读取字节流
  • 需要明确的点:缓存空间和序号都是有限的,并且是循环使用的。他的数据结构应该是首尾相连的,至于具体是什么,请大佬赐教。

java双工通信通道 tcp双工通信_TCP_04

4.2.1、发送窗口和发送缓存
  • 发送缓存存放的数据:
  1. 发送应用程序传送给发送方TCP准备发送的数据
  2. TCP 已发送但是没有收到确认的数据
  • 小结:从上面两条来看,发送窗口和发送缓存好像没有啥区别。通常来说,发送窗口只是发送缓存的一部分,而且被确认的数据会从缓存区删除,所以两者的后沿是重合的。另外发送应用程序必须控制写入缓存的速度,不能太快,否则发送缓存就会没有存放数据的空间。
4.2.2、接收窗口和接收缓存
  • 接收缓存存放的数据:
  1. 按需到达但未被接收方应用程序读取的数据(已经确认的数据,但未交付主机)
  2. 未按序到达的数据(未确认的数据,就是接收窗口了)
  • 小结:从上面两条来看,接收窗口也是接收缓存的一部分,两者前沿重合。如果应用程序处理的太慢了,接收缓存就会被 上面1的数据填满,窗口之就会变为0,那么返回给发送方的窗口值就是0,可以达到流量控制的效果。
4.2.3、几点注意:
  • 发送方的窗口值会受到接收方的确认报文中窗口值的影响,但不会总是和接收方的一致,一方面是网络延迟,如果接收方的窗口值变化了,不会立即引起发送方的窗口值变化,另一方面,发送方还可能要根据网络拥塞情况适当减小自己的窗口值大小。
  • TCP要求接收方必须有累计确认功能以减小传输开销。接收方可以选择合适的时间发送确认消息(等待未按序达到的数据到达,确认最大值),也可以把确认消息放在自己要发送的消息中,顺带捎给发送方。但是推迟确认的时间不宜过长,TCP规定不超过 0.5s,防止发送方以为丢失了重新发送。

4.3、选择确认 SACK

  • 我们也能发现,上面说的传输机制只会确认已到达的最大值,而不会管未按序到达的数据,这样显然会造成资源浪费。而SACK 能做到只传送缺少的数据 而不重传已经正确到达但是不按序的数据。
  • 需要在连接建立时,在TCP的可选字段加上 SACK,并在可选字段中加入每两个不连续的数据块的边界。由于可选字段最多40个字节,而SACK要占用一个,指明这个选项长度要占用一个字节,所以留给边界字段的长度仅有38个字节,一个边界序号是两个字节(就是那个TCP头部的那个序号),两个边界能定位一个数据块。所以最多添加4个数据块的边界(8个值,占32个字节)。

java双工通信通道 tcp双工通信_数据_05

5、 拥塞控制基础

5.1、拥塞控制的基本原理

  • 拥塞:网络中的带宽、交换机中的缓存和处理机等都是网络中的资源。如果在某段时间内,如果对某一资源的需求量超过了该资源所能提供的可用部分,网络的性能就会变坏。这种情况叫做拥塞。而且拥塞常常趋于恶化,由于拥塞引起的重传会加剧拥塞程度。
  • 拥塞控制:拥塞控制是防止过多的数据注入到网络中,使网络中的路由或者链路不至于过载。拥塞控制的前提是网络能够承受现有的网络负荷。
  • 流量控制和拥塞控制的区别:
  • 拥塞控制是一个全局性的过程,涉及所有主机、路由器以及其他会使网络性能降低的因素。拥塞的原因多种多样,不容易找出来。但是流量控制往往☞点到点的通信量的控制,是个端到端的问题。流量控制要做的是抑制发送端的发送速率,使得接收端来得及接收,目标很明确。

5.2、拥塞控制的基本方法

从控制理论来看,分为两个大的方面:开环控制和闭环控制。

  • 开环控制:设计之初就将各种因素考虑周到,力求运行时不会产生拥塞,运行起来之后就不再更改了。(显然不适合互联网的拥塞控制,影响因素太多)
  • 闭环控制:基于反馈环路,主要有以下几种措施:
  1. 监测网络系统发生的拥塞,以及在何时何处发生。检测方法:第一种:检测一些网络性能指标,通常包括:因缓存不足丢弃的分组百分比、超时分组数、平均分组时延、平均队列长度等,这些指标上升代表着拥塞程度增加。第二种:在路由转发的分组中保留一个标识网络拥塞状况的字段。三:由主机或者路由周期性发出探测信号来询问拥塞状况。
  2. 把拥塞发生的信息传送到可以采取行动的地方(比如发送方,减小发送窗口值等...)
  3. 调整网络系统的运行以解决可能出现的问题

6、TCP拥塞控制

  • TCP的拥塞控制使用的算法有四个:慢开始、拥塞避免、快重传、快恢复。发送方维持一个状态变量:拥塞窗口(cwnd)。还需要设置一个 慢开始门限(ssthresh)。下面通过一个例子来介绍这几个算法的使用情景。
  • 在下面的论述中,不考虑双向数据传输和接收方窗口值的影响。

6.1、一个例子

  • TCP连接初始化时,令 cwdn=1,本例中假设 慢开始门限(ssthresh)=16。

java双工通信通道 tcp双工通信_TCP_06

  1. (0->1)开始传输时执行慢开始算法,每经过一个传输轮次(后面说),cwnd*2
  2. (1->2) 直到 cwnd 值达到了慢开始门限值16,开始执行拥塞避免算法,这时拥塞窗口值随着传输轮次开始线性增加
  3. (2->3->4)在 2 处出现超时,判断出现拥塞,cwnd=1,ssthresh= 24/2 =12。重新进入慢开始阶段,直到 再次 cwnd=ssthersh,再次开始执行拥塞避免算法
  4. (4->5---> ...)在 4 处收到3个对同一个字节的确认报文,说明接收方执行了快重传算法发送方执行快恢复算法而不是慢开始,调整 ssthresh = cwnd/2,接着令 cwnd=ssthresh。然后直接从该cwnd值开始执行避免拥塞。

6.2、完整过程

  • 上面的例子并不全面,比如没有说明如果慢开始阶段就产生了超时该怎么办?下面的流程图更加完整。

java双工通信通道 tcp双工通信_数据_07

这里就清楚多了,慢开始和拥塞避免阶段出现超时和重复确认的处理方式时一致的。

6.3、一些问题

  • cwnd 的初始值和递增策略:
    协议标准规定:初始拥塞窗口值是2-4个发送方最大报文段(SMSS)的长度。这个最大报文长度是再连接建立时双方协商确认的,主要是为了减少数据传输时可能出现的分片。
  • 当 smss > 2190字节 ,cwnd=2*smss 字节
  • 当 1095>smss >2190 字节, cwnd=3*smss 字节
  • 当 smss <1095 字节 ,cwnd=4*smss 字节

递增策略:每收到一个新的报文段的确认后,cwnd += min ( N,SMSS )

  • 其中 N 是原先未被确认,现在刚好被确认的报文段的字节数。就是上一个给被确认的报文段..吧..。好像没啥规律。
  • 上述拥塞窗口的传输单位:报文段而不是字节,方便以较小的值来描述传输过程。每收到一个报文段的确认后 cwnd 会++。
  • 传输轮次:每次将拥塞窗口内的报文段全部发送完成(第二轮次是将m2、m3发送出去)算是一个传输轮次。

java双工通信通道 tcp双工通信_java双工通信通道_08

  • 接收方快重传的触发:如果个别报文段在传输过程中丢失了,但网络并不拥塞,为了避免发送方重新慢开始,接收方要执行快重传。如果M1、M2按序收到,接着收到了M4,本来接收方可以什么都不做,等待发送方超时重传,但是快重传要求接收方立马发送一个对 M2 的重复确认报文,接着接收方收到 M5、M6,继续发送对 M2 的确认。当发送方一连收到三个重复确认就会立马重新传送 M3 然后执行快恢复算法。综上:快重传是需要收发双方都执行相应动作的算法。
  • 我们之前没有考虑接收方窗口值(rwnd)的影响,综合考虑,显而易见: 发送方窗口值 = MIN( rwnd , cwnd)

6.4、主动队列管理(AQM)

  • 为防止TCP的全局同步,AQM 实现提前对路由器队列的随即丢弃。

7、TCP 运输连接管理

  • TCP运输连接的三个阶段:建立连接、传输数据、释放连接

7.1、建立连接

  • 连接建立的三个问题:
  1. 使每一方明确知道彼此的存在
  2. 允许双方协商一些参数(窗口值、是否使用可选字段比如SACK等),这些在连接建立阶段就要明确
  3. 能够对运输实体资源(如缓存的大小,TCP需要缓存来收发数据)进行分配。
  • 连接建立过程:
  1. 服务器(以下简称 S )首先要创建一种名为传输控制块TCB的数据结构(传输控制块把发给不同设备的数据封装起来,我们可以把该结构看做是信封。一个TCB数据块包含了数据发送双方对应的socket信息以及拥有装载数据的缓冲区。在两个设备要建立连接发送数据之前,双方都必须要做一些准备工作,分配内存建立起TCB数据块就是连接建立前必须要做的准备工作)
  2. 第一次握手:SYN=1,seq=x 。连接请求报文。前面说头部字段的时候说过,SYN会参与连接请求和连接接受报文。这个报文不能携带数据,但是要消耗一个序号。发送 x
  3. 第二次握手:SYN=1,ACK=1,seq=y,ack=x+1。收到了 x ,要 x+1 。给你 y。不能携带数据,要消耗一个序号
  4. 第三次握手:ACK=1,seq=x+1,ack=y+1。收到了y,要y+1,给你 x+1。可以携带数据。如果携带数据,消耗一个序号,否则不。

java双工通信通道 tcp双工通信_缓存_09

  • 为什么需要第三次握手?
    主要是为了防止已失效的请求报文突然又传送到了服务器。如果不需要第三次,服务器接收到这个失效的请求后就会直接建立连接,而客户端并不知情,认为自己没有要求建立连接,不予理会。就会白白浪费服务器的资源。

7.2、连接释放

  • 第一次挥手(C):客户端发送终止报文 FIN=1,seq=u。你要的u给你(seq=u),我要结束了 ( FIN=1 )。可以携带数据也可以不带,都要消耗一个序号。客户端进入CLOSE-WAIT状态,TCP连接进入半关闭状态,即:客户端已经不再发送数据了,但是仍接收来自服务器的数据。注意是数据,不是报文,之后客户端仍要发送挥手报文。
  • 第二次挥手(S):ACK=1(序号有效)seq=v(上一个数据序号+1),ack=u+1(收到 u 了,要u+1)。任然可以发送数据这时候。进入 FIN-WAIT2 ,等待服务器发出连接释放报文。
  • 第三次挥手(S):FIN=1(要结束了) seq=w(刚可能又发送了一些数据)ACK=1,ack=u+1(注意还是 u+1)。服务器进入 LAST-ACK状态,等待客户端的最后确认。
  • 第四次挥手(C):ACK=1,seq=u+1,ack=w+1。服务器接收报文之后关闭连接。之后客户端进入 TIME-WAIT 状态,等待 2*MSL(报文最长寿命) 事件后才算真正释放了连接。

java双工通信通道 tcp双工通信_数据_10

总的来说,就是:

Client: 我跟你没话说了,断了吧咱俩 (FIN=1)
Service: 别呀,我还没说完呢。。。继续哔哔 N 句----->>>>>
Service:好了,我也没话说了,断了吧咱俩(FIN=1)
Client:好的,拜拜您嘞