JavaEE(系列21) -- 传输层协议UDP 和 TCP

目录

1. 应用层和传输层的联系

2. UDP协议 

2.1 UDP简介

2.2 UDP格式

2.2.1 目的端口和源端口 

2.2.2 报文长度 

2.2.3 校验和 

3. TCP协议 

3.1 TCP简介

3.2 TCP格式 

 3.2.1 数据偏移和选项(option)

 3.2.2 保留项

3.2.3  6位控制位

3.2.4  32位序号和32位确认序号

3.2.5 16位窗口和滑动窗口

3.2.6 16 位紧急指针

4. TCP实现可靠传输的核心机制(重点)

4.1 确认应答机制

4.2 超时重传

4.3 连接管理(三次握手,四次挥手)

4.3.1 建立连接 -- 三次握手

4.3.2 断开连接 -- 四次挥手

5. TCP实现提高通信效率的核心机制(重点) 

5.1 滑动窗口

5.2 流量控制

5.4 拥塞控制

5.5  延时应答

5.6 捎带应答 

5.7 面向字节流

5.8 异常情况


1. 应用层和传输层的联系

        在网络通信的过程中, 应用层描述了应用程序如何理解和使用网络中的通信数据, 和程序员打交道最多的就是应用层了, 针对不同的业务场景, 很多时候程序员需要去自定义应用层协议, 自定义协议主要需要完成下面的两件事情:

  1. 结合业务场景和需求, 分析清楚, 客户端和服务器之间(请求/响应)要传递哪些信息.
  2. 明确传递的信息要以什么样的格式来组织, 可以是普通文本的方式, 也可以一些广泛使用的数据格式, 比如: XML, json, protobuffer.

        其中xml和json都是按照文本的方式来组织的, 优点是可读性好, 用户不需要借助其他工具, 肉眼就能看懂数据的含义, 缺点是要额外传很多的标签或键名, 占用较多的网络带宽, 影响效率; 而protobuffer会将文本数据压缩为二进制数据传输, 特点是肉眼无法解析, 但占用空间更小小, 传输占用的带宽也就降低了.

        应用层也有知名并广泛使用的成品协议, 就比如 : HTTP协议.

        除了最上层的应用层, 下面的传输层, 网络层, 数据链路层, 物理这四层都是已经在系统内核/驱动程序/硬件中已经实现好了, 不许要我们去实现, 传输层是紧接着应用层的一层, 虽然传输层是操作系统内核实现好了, 但是我们在写应用层代码的时候, 是要调用系统的socket API去完成网络编程, 所以需要我们了解这里传输层的一些关键协议UDP和TCP.

端口号:

        端口号是传输层协议的概念, TCP和UDP协议的报头中都会包含源端口和目的端口, 并且都是使用2个字节, 16bit来表示端口号, 范围也就是 0 -> 65535; 但是我们日常写的程序使用的端口号一般都是从1024开始的, 因为0 -> 1023这个范围的端口号也称为 “知名端口号/具名端口号”, 这些端口号系统已经分配给了一些知名并广泛使用的应用程序.

        这里我们并不是完全不能使用0 -> 1023这个范围的端口号, 只是建议使用, 虽然这些端口被分配给了特定程序, 但是这些程序是否在主机运行着, 主机上是否安装了这些程序都是不一定的, 要使用0 -> 1023这些端口, 需要注意2点 :

  1. 要确定这个端口没有和程序绑在一起.
  2. 要拥有管理员权限.

2. UDP协议 

2.1 UDP简介

UDP是User Datagram Protocol的缩写, UDP的特点:

  1. 无连接
  2. 不可靠传输
  3. 面向数据报
  4. 全双工

UDP使用起来简单高效, 但它的数据载荷较小, 一般适用于以下场景:

  1. 包总量较少的通信(DNS、SNMP等)
  2. 视频、音频等多媒体通信(即时通信)
  3. 限定于LAN()局域网)等特定网络中的应用通信
  4. 广播通信(广播、多播)

2.2 UDP格式

 img

也可以理解成下面的格式: 

img 

2.2.1 目的端口和源端口 

 UDP会把从应用层拿到数据(就是网络编程中应用层调用socket API, send()发送的数据)的基础上再前面拼装上8个字节的报头.

 下面是各个部分占用的字节数.JavaEE(系列21) -- 传输层协议UDP 和 TCP_第1张图片

2.2.2 报文长度 

 UDP的包长度, 也叫报文长度(报头+载荷), 这个属性表示了一个UDP数据报的大小, 单位为字节, 2字节表示 0->65535这个范围, 也就是说一个UDP数据报最大不超过64KB.

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第2张图片 

那如果应用层的数据报大于64KB怎么办呢?

这里有两种方案:

  1. 一是可以在应用层的代码层面将应用层的数据报手动进行分包, 这样拆分成多个小的包通过多个UDP数据报进行传输.
  2. 第二种方案是就不用UDP协议了, 改用TCP协议, TCP没有这样的限制的, 后文会介绍到.

2.2.3 校验和 

检验和: 

像这样的现象是客观存在不可避免了, 我们能做的只是及时的识别出当前的数据是否出现了问题, 因此就引入了校验和来进行鉴别, 校验和是针对数据的内容进行一系类的运算(每一个比特位都会参与运算)得到一个比较短的结果, 我们可以认为, 数据内容一定, 得到的校验和就是相同的, 如果我们的数据变了, 那么的校验和就变了, 如此即可验证得到数据是否准确.

比如发送方要发送的数据是 “反射导弹”, 计算出来的校验和也会放到报头中发送, 当接收方拿到数据就会有两种情况了, 第一种情况是接收方拿到数据后重新计算一个校验和, 得到的结果如果和拿到的校验和相同, 就认为数据是正确的, 不同, 则认为数据不正确.

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第3张图片

 

 JavaEE(系列21) -- 传输层协议UDP 和 TCP_第4张图片

3. TCP协议 

3.1 TCP简介

 TCP, 即Transmission Control Protoco, TCP协议相比于UDP协议要更复杂,

TCP的特点:

  1. 有连接
  2. 可靠传输
  3. 面向字节流
  4. 全双工.

TCP对数据传输提供的管控机制,主要体现在两个方面:安全和效率。
这些机制和多线程的设计原则类似:保证数据传输安全的前提下,尽可能的提高传输效率。 

3.2 TCP格式 

 3.2.1 数据偏移和选项(option)

数据偏移和选项(option), 选项可有可无也可以有多个, 用于对TCP一些功能的扩展和TCP中的一些属性进行解释说明, 可能包括 “窗口扩大因子”, “时间戳” 等选项, 数据偏移表示TCP数据起始处与TCP报文起始处之间的距离, 也就是4个比特位(0到15)表示TCP首部报头的长度, 单位是4字节;

正是由于TPC当中有了数据偏移和选项这两个属性, 致使使TCP的报头长度是可变的, 不像UDP一样固定是8字节, 选项之前的部分是固定的长度(20字节), 选项长度 = 首部长度 - 20字节, 通过首部长度就可以去调节选项长度; 如果首部长度值是5, 表示整个TCP报头是20字节(相当于没有选项), 如果首部长度值是15, 表示整个TCP报头是60字节(选项部分就是40字节), 填充是为了保证选项为32比特的整数倍.

 3.2.2 保留项

数据偏移后面还有6位保留项, 这里保留项的存在是为了未来TCP协议的拓展升级准备的, 网络协议的拓展升级是一件成本极高的事情, 比如现在UDP协议报文长度是最大是2字节(64KB), 如果想要升级一下让UDP的报文支持更大的长度, 在技术上可以实现, 但实际上要想让世界上所有能上网的设备所安装的各式的操作系统都能够同步完成升级, 支持新的UDP, 这是不现实的; 一种系统升级了, 其他系统不升级, 就办法进行通信了…

而像TCP这样引入了 “保留位”, 如果在未来想要引入一些新的功能, 就可以使用这些保留位, 这样对于TPC本来的报头结构影响是比较小的, 针对不升级的设备也更容易兼容就版本的TPC.

3.2.3  6位控制位

 6位控制位从左至右分别为 , URG, ACK, PSH, RST, SYN, FIN, 含义如下:

  • URG(Urgent Flag):该位为1时, 表示包中有需要紧急处理的数据.
  • ACK(Acknowledgement Flag)该位为1时, 确认应答的字段变为有效, TCP规定除了最初建立连接时的SYN包之外该位必须设置为1.
  • PSH(Push Flag):该位为1时, 表示需要将受到的数据立刻传给上层应用协议, PSH为0时, 不需要立即传而是先进行缓存.
  • RST(Reset Flag):该位为1时表示TCP连接中出现异常必须强制断开连接.
  • SYN(Synchronize Flag):用于建立连接, SYN为1表示希望建立连接, 并在其序列号的字段进行序列号初始值的设定(Synchronize本身有同步的意思, 也就意味着建立连接的双方, 序列号和确认应答号要保持同步).
  • FIN(Fin Flag):该位为1时, 表示今后不会再有数据发送, 希望断开连接.

3.2.4  32位序号和32位确认序号

32位序号和32位确认序号和TCP确认应答机制有关

3.2.5 16位窗口和滑动窗口

16位窗口和滑动窗口, 流量控制机制有关;

3.2.6 16 位紧急指针

16 位紧急指针要配合URG控制位一起使用的, 用于指明紧急数据之后正常数据的起始位置; 

4. TCP实现可靠传输的核心机制(重点)

4.1 确认应答机制

确认应答是实现TCP可靠传输最核心的机制:这里的可靠并不是指发送方能够百分百能将数据发送到接收方, 可靠传输是要尽可能的把数据发过去, 发送方能够准确的知道接收方是否收到了数据.

1. 比如, 女神跟你关系是非常要好的, 你发的消息她一定立刻会回, 当你给你的女神发消息说要请她吃麻辣烫, 当你看到她的回复的时候, 你就知道消息她是收到了的.

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第5张图片

 这里女神回复的好啊好啊就称为 “应答报文”, 也叫ACK(acknowledge), TCP进行可靠传输最主要就是靠这个确认应答机制来保证的, A给B发一个消息, B收到之后就会返回一个ACK, A收到这个应答之后, 就知道了发的数据顺利到达了(没有丢包), 如果没有收到ACK就说明A发的数据消息大概率不见了(丢包).

2. 考虑更复杂的情况, 如果多条消息同时发送, 可能会发生 “后发先至” 的情况, 比如你问你的女神是否吃麻辣烫后紧接着又问一句: “女神, 女神, 你做我女朋友好吗?”, 就可能会是下面的情况:

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第6张图片

在网络通信中, 两个主机之间, 传输路线存在多条, 数据报1和数据报2可能走的都是不同路线, 而且设备的转发速率也是有快有慢, 受这样的网络环境的影响, 这种后发先至的情况是很常见也是无法避免的, 收到消息的顺序就会存在变数, 这样应答错乱后, 解析数据的含义就出现歧义了, 就如上图, 如果按照正常的逻辑解析的话就是 “吃麻辣烫->滚, 做女朋友->好啊好啊”, 而实际上女神没想做你女朋友…
3. 为了解决上述先发后至的问题, TCP中就引入了序号和确认序号.

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第7张图片

 

上面发送方的序号就是序号, 接收方的序号就是确认序号, 这样即使出现先发后至的情况导致消息的顺序错位了, 也能区分清楚应答报文是针对哪一条消息做出的应答.

这里的序号和确认序号就是对应与TCP报头中的序号与确认序号, 它们都是32位大小, ACK同样也在报头中占据1个比特位.

img 

要注意任何一条数据(包括应答报文)都是有序号的, 但确认序号只有应答报文有, 是否为应答报文取决于ACK这个标志位是否为1, 如果为1就表示是应答报文, 如果是0就表示不是应答报文.

实际上的TCP序号并不是上面举例那样简单的以1, 2这种方式编号, TCP是面向字节流的, 在实际的TCP传输中, 是针对每一个字节都进行了编号, 假设我们的一条数据长度是1000个字节, 传输的数据第一个字节的序号是1, 由于这1000个字节都是属于同一TCP报文的, 因此就将一条数据中的第一个字节作为了TCP报头里记录的序号也就是1, 代表要发送的这一条数据.

img

 

确认序号的取值是收到的数据的最后一个字节的序号+1, 接收方收到数据后会返回一个1001做为确认序号, 这里的确认序号表示着两个含义:

  1. 小于1001的数据都已经确认收到了
  2. 发送方接下来应该从1001这个序号开始发送数据(接收放向A索要1001开始的数据).

img 

由于网络通信是有很多情况,就会导致数据发送的顺序和接收顺序不一致,TCP自身有整队的功能,根据序号进行整队.

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第8张图片

 

 这里总的来说就是, TCP可靠传输的能力最主要是通过确认应答机制保证的, 通过应答报文不仅可以让发送方清楚的知道是否传输成功, 而且通过序号和确认序号对多组数据的应答对应关系进行了详细的区分.

4.2 超时重传

 上面说的确认应答只是讨论了数据顺利传输的情况, 那如果出现意外了呢? 比如网络原因可能会导致发送数据或者返回的ACK丢包了又该如何呢?

1. 这里不得不提一个概念:丢包

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第9张图片

 

出现了丢包的情况, 发送方重新再发一个就行了, 这就是TCP引入的超时重传机制, 当一个数据发送后, 如果在一个时间阈值内没有收到ACK, 就认为是丢包了, 就会重发一份同样的数据.

触发重传有两种情况:

  • 一是数据包丢了, 此时发送方等待一段时间没有收到ACK就会重新发送.
  • 第二种情况是返回的ACK丢包了, 对于发送方是不知道是自己这边发送的消息丢包了还是对方返回的ACK出问题了, 此时就会按照最坏的情况, 重发数据, 这就可能导致接受方重复收到多次同样的消息, 但TCP针对这种重复数据的传输是有特殊处理的, TCP实现了一个去重机制TCP中存在一个发送缓冲区和接收缓冲区(操作系统内核里面的一段内存), 接收方拿到数据后是将数据放到了接收缓冲区(有阻塞队列功能, 但不限于阻塞队列)中, 缓冲区会根据数据的序号进行排序, 并且根据数据的序号, TCP很容易识别当前接收缓冲区里的这两条数据是否是重复的, 如果重复就把后来的这份数据就直接丢弃了, 然后重新返回一个与之前相同的ACK.

img

 2. 超时重传的数据有没有可能继续发生丢包?

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第10张图片

TCP针对超时重传发生丢包做出的应对 

 JavaEE(系列21) -- 传输层协议UDP 和 TCP_第11张图片

 上面的确认应答机制和超时重传机制这两者相互配合, 共同支撑整体的TCP可靠性.

4.3 连接管理(三次握手,四次挥手)

TCP要完成通信是需要先建立连接的, 所以就有了连接管理的机制, 连接管理一定程度上也可以体现TCP的可靠性, 但保证可靠传输最核心的机制还是上面介绍的确认应答和超时重传.

TCP这里的连接指的是由一个四元组(源IP, 源端口, 目的IP, 目的端口)来标识, 一个连接建立完成就表示通信双方知晓对方的IP和端口信息, 就是通信双方各自都维护着连接这样的一个数据结构, 双方把对方的地址信息都保存下来就是完成了连接, 而断开连接就是把各自存储的连接删除掉.

对于TCP的连接管理就是建立连接(三次握手), 断开连接(四次挥手)了.

4.3.1 建立连接 -- 三次握手

客户端与服务器之间进行三次交互建立连接的过程被形象地称为 “三次握手”, 在这三次交互中, 通信双方要完成对彼此信息的记录.

  • SYN(Synchronize Flag)(同步报文段):用于建立连接, SYN为1表示希望建立连接, 并在其序列号的字段进行序列号初始值的设定(Synchronize本身有同步的意思, 也就意味着建立连接的双方, 序列号和确认应答号要保持同步).
  • ACK(Acknowledgement Flag)(应答报文段)该位为1时, 确认应答的字段变为有效, TCP规定除了最初建立连接时的SYN包之外该位必须设置为1.

img 

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第12张图片

 

1.还是你和你的女神聊天, 你想要让女神做你的女朋友, 于是就有了下面的对话.

 img

        再从实际上TCP的三次握手来说, 当发送方发出SYN后, 接收方都到发送方的SYN后, 此时接收方就能够确定发送方的发送能力和接收方的接收能力是正常的, 然后接收方回应ACK和SYN, 当接收方收到ACK和SYN后, 就知道了发送方和接收方的发送能力, 接收能力都是正常的, 最后发送方回应ACK给接收方, 此时接收方也确定了接收方和发送方的接收能力, 发送能力都是正常的. 

所以, 总结一下三次握手的意义就是:

  1. 让通信双方各自建立对对方的 “认同”.
  2. 验证通信双方各自的发送能力和接收能力是否正常
  3. 在握手的过程中, 双方还会 “协商” 配置一些重要的参数(完成一些数据的同步).

2.  在建立连接的过程中, 服务器与客户端是存在着不同的状态的, 不同的状态体现了TCP当前的工作, 具体如下

img

  • CLOSED 表示客户端或服务器处于关闭状态.
  • LISTEN 表示服务器已经准备就绪, 等待客户端连接的状态.
  • SYN_SENT 表示客户端连接请求已发送, 此时客户端进入阻塞等待服务器确认应答状态, 一般此状态的存在时间很短.
  • SYN_RCVD 表示服务器已经收到客户端的连接请求, 发送ACK和SYN并进入阻塞等待客户端连接状态, 一般此状态存在时间很短.
  • ESTABLISHED 表示客户端或服务器已经建立成功连接, 随时可以进行通信, 要注意理解, 两次握手后, 从客户端来看, 客户端已经把该发送的和该接收的都完成了, 此时客户端就认为进行成功建立连接; 而对于服务器, 当第三次握手后才能认为成功建立连接.

4.3.2 断开连接 -- 四次挥手

三次握手类似, 客户端与服务器通过四次交互断开连接的过程称为 “四次挥手”, 通信双方向对方发起一个断开连接的请求FIN, 再各自给对方一个回应ACK

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第13张图片

1. ack和fin可以在一起发送吗? 

 JavaEE(系列21) -- 传输层协议UDP 和 TCP_第14张图片

 2. 第二个fin客户端会收不到吗?

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第15张图片

 img

  • FIN_WAIT_1 出现在主动发起断开连接的一方, 主动方发送FIN后, 进入等待被动方确认断开响应状态.
  • FIN_WAIT_2 1 出现在主动发起断开发起连接的一方, 收到被动方的ACK, 进入等待被动方FIN状态
  • CLOSE_WAIT 出现在被动发起断开连接的一方, 当被动方收到主动方发送的FIN请求后, 被动方响应ACK, 然后等待关闭连接( 等待socket调用close方法).
  • LAST_ACK 出现在被动发起断开连接的一方, 被动方FIN发送后, 进入等待最后一个ACK状态.
  • TIME_WAIT 出现在主动发起断开发起连接的一方, 收到被动方FIN, 发送最后一次ACK, 然后继续保持当前的TCP状态, 再等一会儿后释放连接.

这里重点要理解的是TIME_WAIT这个状态, 从上图来看在客户端看来它的将最后一次ACK发出去后, 四次挥手就已经是完成了? 那为什么TIME_WAIT这里还要等待一会而不是立即释放连接, 这是因为最后一次客户端发送ACK后是可能存在丢包的情况的, 在三次握手和四次挥手的过程中, 同样是存在超时重传的, 如果丢包了, 服务器就会以最坏情况认为自己的FIN丢了, 会重发FIN, 此时客户端就需要等待以预防服务器重发FIN的这种情况, 因此使用TIME WAIT状态保留一定的时间, 就是为了能够处理最后一个ACK丢包的情况, 能够在收到重传的FIN之后, 进行ACK响应.

TIME_WAIT这里等待的时间为2MSL , MSL表示报文最大生存时间(通常是60s), 也就是在两个节点进行网络传输过程中消耗的最大时间, 如果TIME_WAIT维持了2MSL都没用收到重传的FIN, 就认为最后一个ACK顺利到达了, 服务器与客户端就完全断开连接了.

5. TCP实现提高通信效率的核心机制(重点) 

5.1 滑动窗口

上面介绍的TCP机制都是再给TCP的可靠性提供支持, 但保证了可靠性其实就牺牲了一定的效率, 滑动窗口做的事情就是在保证传输的可靠性的基础上, 尽量地去提高传输效率.

在进行IO操作的时候, 时间成本主要是两个部分, 一是等, 二是数据传输, 大多数情况下, IO花的时间成本大头都是在等上面, 滑动窗口本质上就是降低了等待确认应答ACK消耗的时间.

对于基本的确认应答机制来说, 每发送一次数据, 都需要等待ACK返回后才能进行下一次发送, 这样大部分的时间都用在等ACK上了.

1. 正常的滑动窗口(不包含丢包的情况)

img

 

滑动窗口的本质就是不进行等待发送多条数据, 然后使用一份时间来等待多个ACK返回.

img

 把不需要等待, 就能直接发送的最大数据量, 称为 “窗口大小”, 上图中窗口的大小就是4000, 客户端发送了4条数据之后并不是等到4个ACK都都返回后才能继续发送, 而是每收到一次ACK就继续发下一条数据, 这样就让客户端这里等待ACK的数据始终始终都是4条, 就如上图, 客户端发出1-1000,1001-2000, 2001-3000, 3001-4000这四条数据后, 客户端收到1001, 紧接着就发送4001-5000这条数据, 收到2001, 就继续发送5001-6000…
img

 

这里图中本来等待ACK是1001-5000, 接下来, 收到了2001这个ACK, 就说明2001之前的数据(1001-2000)已经被确认了, 此时就可以立即发送5001-6000的数据, 此时意味着等待ACK的范围就是2001-6000, 这就相当于一个大小始终不变的窗口, 但窗口框住的数据变了, 相当于窗口向右滑动了一格, 所以这里就形象的称为 “滑动窗口”.

2. 上述是正常传输的情况, 那如果丢包了应该如何处理呢? 下面就来分析一下

 

情况1, ACK丢了: 这种情况下是不用做任何的处理的, 数据还是能够正常传输, 比如1001丢了, 但实际上1-1000的数据是服务器是收到了的, 当客户端收到2001时就表明2001之前的数据都已经确认到达服务器, 就会接着再发送两条数据, 所以只要大部分的ACK没有丢, 客户端可以通过下一次或者后面的确认应答序号来进行确认, 不处理也没事.

img

 

情况2, 数据丢了: 数据都丢了, 这就必须得处理了, 比如1-3000的数据中, 其中1001-2000的数据丢了, 那服务器每收到一个数据, 都会返回1001, 表示让客户端重传1001-2000这个数据, 当客户端收到若干个个相同的确认应答序号时, 就明白了, 数据丢了, 就会对丢失的数据进行重传, 直到服务器收到1001-2000的数据, 就会返回最新的确认应答序号, 当然, 如果中间还有数据都丢包, 返回的就是新丢的包的序号了, 然后还是上述操作.

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第16张图片

这种丢包重传的方式被称作 “快速重传”, 让重传操作只重传了丢失的数据, 可以视为是超时重传机制在滑动窗口下的变形; 如果当前传输数据密集, 按照滑动窗口的方式来传输, 此时按照快速重传来处理丢包; 如果当前传输数据稀疏, 就不再按照滑动窗口方式了传输了, 此时还是按照之前的超时重传处理丢包.

5.2 流量控制

滑动窗口机制是在提高TCP的传输效率, 窗口越大, 传输效率就越高, 但是窗口也是不能无限大下去的.

首先如果窗口无限大了, 那么一个窗口就把数据都发完了, 也就是完全没有在等ACK了, 数据传输的可靠性就得不到保障了, 这就和TCP的初心背道而驰了, 而且窗口太大, 也会消耗大量的系统资源.

然后就是说, 窗口大了, 发送方的发送效率确实提高了, 但是接收方能接受得过来吗? 但是如果发送速度过快, 接收方的接缓冲区满了之后, 接收方就处理不过来了, 白发了…

所以并不是窗口大小越大, 传输效率就越高, 只有保证发送方发送与接收方接收的速率最大并保持一致时, 传输效率才是最高的, 而流量控制要做的工作就是根据接收方的处理能力, 动态协调发送方的发送速率.

接收方的处理能力是通过接收方缓冲区的剩余容量来衡量的, 接收方缓冲区的容量剩余多少, 下次发送方的窗口大小就是多少, 可以接收方的缓冲区想象成一个蓄水池, 那么发送方的工作就是注水, 接收方的工作就是使用水池中的水, 当水位比较低(剩余空间大)那就注水的时候就快一点, 水位比较高(剩余空间小)那就注水的时候就慢一点, 池子满了就暂时先停止注水.

当发送方的数据到达接收方的时候, 接收方都会返回一个ACK,这个ACK除了确认能够确认应答, 还能告知接收方缓冲区的剩余容量, 然后发送方就会根据接收方缓冲区的剩余容量来控制发送速度(窗口大小), 当接收方得知接收方缓冲区空间满了的时候, 就暂时不会发送数据了, 而是会定期去给接收方发送一个探测窗口报文, 这个报文不携带具体的业务数据, 只是为了触发ACK查询接收方缓冲区的剩余容量.

img

 JavaEE(系列21) -- 传输层协议UDP 和 TCP_第17张图片

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第18张图片 

5.4 拥塞控制

 流量控制是考虑到了接收方的处理能力来调节发送方的窗口大小, 而实际上数据传输不单单是简单的从发送方直接到了接收方, 往往还有复杂的中间转发过程(众多交换机和路由器等节点), 拥塞控制描述的是传输过程中中间节点的处理能力, 同样的如果中间转发过程中链路的拥堵了, 那接收方的处理能力再快也是白搭的(木桶效应).

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第19张图片

 

拥塞控制本质上就是通过实验的方式来逐渐找到一个合适的窗口大小(合适的发送速率).

接收方处理能力是好量化衡量的, 但是由于设备众多, 数据每次传输路线也大概率是不相同的… 众多影响因素导致中间节点的处理能力是不好量化衡量的, 因此拥塞控制采取了 “测试实验” 的方式逐渐调整不同情况下合适的发送速度.

JavaEE(系列21) -- 传输层协议UDP 和 TCP_第20张图片

 JavaEE(系列21) -- 传输层协议UDP 和 TCP_第21张图片

 实际上的窗口大小是拥塞控制和流量控制共同决定的, 取的是拥塞窗口和流量控制窗口的较小值. 

5.5  延时应答

延时应答也是提升TCP效率的机制, 流量控制是为了在接收方能够处理得了的前提下, 尽可能的把窗口大小放大一点, 延时应答相当于流量控制的延伸, 想要在此基础上, 让窗口的大小尽量再大一点.

就是接受方收到数据之后, 不是立即返回ACK了而是稍微等会再返回, 等待的时间里, 接收方的应用程序就能够把接收缓冲区的数据给再处理一波, 此时接收缓冲区的剩余容量就更大了.

实际上延时应答采取的方式, 就是在滑动窗口下, ACK不再每一条数据都返回了, 比如下图就是隔一条返回一个ACK.

img

 JavaEE(系列21) -- 传输层协议UDP 和 TCP_第22张图片

 

5.6 捎带应答 

同样捎带应答也是提升TCP效率的机制, 是延迟应答的延伸, 由于延时应答的存在, 接收方并不是立即就返回响应ACK的, 而很多情况下, 客户端服务器在应用层也是 “一发一收” 的, 当服务器的应用层有业务数据要发送给客户端时, 就可以捎带的将ACK一起发送, 此时应用层代码需要响应的时机与ACK响应时机重合的, 就可以将这两个数据合二为一进行发送,

ACK是由系统内核返回的, 业务数据是由应用程序发送的, 这两条数据的发送本来是在不同的时机发送的, 由于延时应答机制的存在, 就导致等待ACK的过程中, 接收方就要发送业务数据给发送方了, 此时就可以让业务数据捎上这个ACK一起发过去就行了.

也就是说, 上面的ACK和业务数据本来是在不同的时机的, 但在延时应答的情况下是可能成为相同时机的, 然后就合并发送了, 延时应答是提高了这里合并的概率, 捎带应答就是针对这种能合并的情况进行的特殊处理.

5.7 面向字节流

 

TCP是面向字节流的, 在接收缓冲区其实是把多个数据都放到一起的, 这就导致应用层去使用read()读取缓冲区的数据时, 会出现分不清读到哪里才算是一个完整的应用层数据报, 由于TCP是面向字节流的, 那么一次读1个字节或者读N个字节, 都是可以的, 这就导致一次读到的数据可能是半个应用层数据报, 可能是一个应用层数据报, 也有可能是多个应用层数据报…

也就是说在TCP层次的socket API中是没有告诉我们应该读几个字节的, 具体怎么读, 完全是由程序员自己负责, 但我们所希望的是每次读的是一个完整的应用层数据报, 这就是需要程序员自己去解决了.

其实解决方案也很简单, 程序员是可以控制应用层协议的, 只需要在应用层代码中约定好应用层数据报和应用层数据报之间的边界就好了, 比如可以应用层数据报结尾约定一个分隔符, 这样在读取的时候, 就能区分出一个完整的应用层数据报了; 也可以约定好每个包的长度, 读取时先读取长度, 让然后再读取读到长度的字节数就能得到完整的数据报了.

5.8 异常情况

 

在进行TCP协议传输过程中会出现由于不可抗力导致的异常情况, 针对如下几种进行简单介绍:

进程崩溃了(进程终止)

TCP连接是通过socket来进行连接的, socket本质上是进程打开维护的一个PCB, 进程终止了, 对应的PCB就没了, 再对应在文件描述表中的位置就释放了, 就相当于文件自动关闭了, 这个过程和手动调用socket.close方法没有区别, 系统内核依然会完成四次挥手的过程, 此时其实还时一个正常断开连接的流程.

主机关机(按照正常流程关机)

主机关机首先终止的是进程, 就和上面一样的, 还是会触发四次挥手, 然后正式关机.

主机掉电

当电源或网络直接断开时, 是没有任何时间留给操作系统去反应的, 所以根本来不及去完成四次挥手.

假设是接收方掉电了, 此时发送方仍然是继续在发数据的, 发完数据要等待ACK返回, 接收方都挂了肯定时传不了了, 那么发送方就会进行超时重传, 但不管怎么重传, 都是收不到ACK的, 重传了几次, 还是没有收到ACK, 发送方就会尝试重置TCP连接, 显然这个重置也会失败, 然后发送方就会单方面放弃连接了.

TCP重置连接的报文的是通过复位报文段来判断的, 即RST, 也是TCP报头中控制位中的一位.

img

 

再考虑发送方掉电的情况, 此时接收方会发现, 发送方很差时间没有数据发送过来了, 但从接受方的角度来看, 接受方不知道是发送方挂了还是发送方在组织数据, 所以针对这种情况, 接受方会周期性的给发送方发送一个探测报文, 触发服务器的ACK, 如果没有反应, 就说明是发送挂了.

这样的探测报文也被形象的叫做 “心跳包”, 用来确认通信双方是否处在正常的工作状态中, 因为心跳是周期性的, 如果心跳没了, 说明就挂了, 心跳包是非常常见并且经常用到的保活机制.

你可能感兴趣的:(JavaEE,tcp/ip,网络,java-ee)