文章目录
- 1. 应用层
- 2. 传输层
- 2.1 端口号
- 2.2 UDP协议
- 2.2.1 UDP报文格式
- 2.2.2 UDP的特点
- 2.3 TCP协议
- 2.3.1 TCP报文格式
- 2.3.2 TCP的相关特性
- 2.3.4 确认应答
- 2.3.5 超时重传
- 2.3.6 连接管理
- 2.3.6.1 TCP三次握手
- 2.3.6.2 四次挥手
- 2.3.7 滑动窗口
- 2.3.8 流量控制
- 2.3.9 拥塞控制
- 2.3.10 延时应答
- 2.3.11 捎带应答
- 2.3.12 面相字节流
- 2.3.13 异常情况的处理
- 2.4 TCP / UDP对比
1. 应用层
应用层是和程序员接触最密切的
在应用层这里,有很多时候,都是程序员“自定义”用户层协议的
这里的自定义协议非常简单,就是程序员在代码中规定好,数据如何进行传输
- 根据需求,明确要传输的信息
- 约定好信息按照什么格式来组织
我们举个例子
当我们打开外卖软件,首先会看到商家列表
这里就涉及到程序和服务器之间的网络通信交互
请求:用户信息,位置信息
此处假设就使用简单的格式来组织,使用文本的方式,有三个属性,使用 “,” 来分割
100,100,30
代码中构造出这样的字符,写入到 Tcp socket 或者 Udp socket 中
响应:商家列表(多个商家)
包含 名称、图片、简介、评分
次数假设使用简单的格式来组织,使用文本的方式,每个商家信息占一行,每个属性适应 “,” 来分割
上述这个过程就是自定义协议
自定义协议,具体的方式非常灵活,只需要客户端和服务器能够对应
上述这种 通过文本方式构造的协议,属于比较粗糙的方式,在实际开发中比较少见
几种开发中更常见的格式
- xml
很少用于网络通信了
通过标签来组织数据
xml 是一个通用的数据格式,这里有什么标签,标签有什么含义都是程序员自定义的
xml 的优势:
让数据的可读性更好
xml 的劣势:
标签写起来非常繁琐,传输的低吼也占用更多网络带宽
(maven,就会使用 xml 来管理项目配置)
- json (当下最流行的一种数据组织格式)
json 使用键值对结构
{} 把所有的键值对给包裹起来
键值之间,使用 “,” 来分割
键和值之间,使用 “:”来分割
键固定就是 string 类型
值的话,可以是数组,可以是字符串,也可以是json,还可以是数组
json 的优势:
可读性比较好,比xml 更简洁
json 的劣势:
同样也是会在网络传输中,消耗额外的带宽(需要把 key 进行传输的)
虽然如此,json 在网络通信中仍然非常流行
除非是一些对于性能要求非常高的场景,不使用 json 之外其余的很多地方都可以使用 json
- protobuffer 相比于 json 和 xml 来说,protobuffer(简称为 pb)使用二进制的方式来组织数据
可以保证带宽占用最低(相当于把要传输的信息按照二进制形式压缩了)
pb 的优势:
占用带宽最低,传输效率最高,非常适合于对于性能要求比较高的场景
pb 的劣势:
可读性不好(二进制结构,肉眼无法直接阅读)
应用层中还是有一些“现成”的应用层协议
其中最知名的,最广泛使用的,就是 HTTP 协议了(超文本传输协议)
2. 传输层
传输层是面试的重点,也是工作中常用的内容
UDP:无连接,不可靠,面相数据报,全双工
TCP:有链接,可传输,面向字节流,全双工
2.1 端口号
写一个服务器,必须手动指定一个端口号,通过端口号来区分当前这个主机上的不同的应用程序
写一个客户端,客户端在通信的时候也会有一个端口号(代码中感受不到),系统自动分配的
端口号,固定就是占 2 个字节
表示的数据范围 0 - 65535
一般来说,0 不使用
1 - 1023 称为“知名端口号”
1024 - 65535 是普通的端口号
事实上,目前大部分的“知名”的服务器,现在大部分已经不再使用了
其中,还是有几个到现在还是比较知名的端口
22:ssh 服务器的端口号,ssh 协议是用来登录远程主机的
80:http 服务器的端口号
443:https 服务器的端口号
2.2 UDP协议
2.2.1 UDP报文格式
这个图不够严谨,可以看下面自己画的图
报头和荷载之间,可以认为是一个“字符串拼接”,是二进制的数据
UDP 报头:
分为四个部分,一个部分两个字节
UDP 报文长度:
两个字节(16位)表示的数据,表示的范围 0 - 65535
UDP 数据报,最长就是 64kb(如果数据量太大,还是使用 TCP)
校验和:
校验和是计算机中广泛使用的概念
网络传输中,由于一些外部干扰,就可能会出现数据传输出错的情况
因此,校验和就是 一种可以识别出 出错的数据的 检测手段
校验和,其实本质上也是一个字符串,体积比原始的数据更小,优势用过原始的数据生成的
只要原始数据相同,得到的校验和就一定相同
如何基于校验和来完成数据校验呢?
- 发送方,把要发送的数据整理好(称为 data1), 通过一定的算法,计算出校验和 checksum1
- 发送方把 data1 和 checksum1 一起通过网络发送出去
- 接收方收到数据,收到的数据称为 data2 (数据可能和 data1 就不一样了),收到数据 checksum1
- 接收方再根据 data2 重新计算校验和(按照相同的算法),得到 checksum2
- 对比 checksum1 和 checksum2 是否相同
如果不同,则认为 data2 和 data1 一定不相同
如果 checksum1 和 checksum2 相同,则认为 data1 和 data2 大概率是相同的
(理论上存在不同的可能性,概率比较低 工程上忽略不计)
通过上面的方式,就能发现数据传输出错
校验和是如何计算的呢?
计算校验和有很多算法,此处 UDP 中使用的是 CRC 算法(循环冗余算法)
把当前要计算校验和的数据,每个字节,都进行累加,把结果保存到这个 两个字节的 变量中
累加过程中如果溢出,也没关系
如果中间某个数据,出现传输错误,第二次计算的校验和就会和第一次不同
CRC 这个算法其实不是特别的靠谱,导致两个不同的数据,得到相同的 crc 校验和的概率比较大.
前一个字节恰好 少 1,后一个字节恰好 多 1
这种情况概率确实不大,但是确实也还是有这方面的风险
还有别的算法可以计算( md5 / sha1 算法)
这里只介绍 md5
md5 有一系列的公式,来完成 md5 的计算
我们需要知道 md5 的特点:
- 定长
无论你原始数据多长,计算到的 md5,都是固定长度
校验和本身就不应该长,要不然不方便网络传输 - 分散
给定另个原始数据,哪怕大部分内容都一样,只要其中一个字节不同,得到的 md5 值都会差异很大
md5 也非常适合作为 hash 算法 - 不可逆
给你一个原始数据,计算 md5,非常容易
给你 md5,还原出原始数据,计算量非常庞大
以至于超出了现有计算机的算力极限,理论上是不可行的
md5 也可以应用在密码学场景中
2.2.2 UDP的特点
-
无连接
UDP 协议本山不会存储对端的信息,要在发送数据的时候,显示指定要传输给谁
-
不可靠
代码中体现不出来 -
面向数据报
-
全双工
通过一个 socket,既可以 send,又可以 receive
2.3 TCP协议
TCP 是我们学习的重点
TCP 这个协议最大的特点就是 可靠传输
2.3.1 TCP报文格式
16 位源端口 与 16 位目的端口:
和 UDP 相同
4 位首部长度:
数据报 = 首部(报头) + 载荷
这是 TCP 报头的长度,TCP 报头长度是不固定的(变长的)
选项(option):
可以翻译为可选的(可以有,也可以没有)
选项也是报头的一部分
选项有,报头就更长;选项没有,报头就短
报头最短就是 20 字节(没有选项)
报头最长就是 60 字节(选项最多 40 字节)
保留(6 位):
UDP 有个问题,长度 64kb,无法改变
保留位可以扩展的位置,如果后面有需要,就可以直接扩展
16 位校验和:
和UDP一样
2.3.2 TCP的相关特性
-
有连接
-
可靠传输
-
面向字节流
-
全双工
可靠传输,是 TCP 最最核心的特性(初心)
什么是可靠传输:
1)发送方发出数据之后,能够知道接收方是否收到数据
2)一旦发现对方没有收到,就可以通过一系列的手段来“补救”
2.3.4 确认应答
发送方,把数据发给接收方之后,接收方收到数据就会给发送方返回一个 应答报文(acknowledge,ack)
发送方,如果收到这个应答报文了,就知道自己打的数据是否发送成功了
在网络传输数据的时候,可以会因为数据报走的路程不同,导致出现“后发先至”的情况
这个时候,数据就会发生错误
TCP 在此处就要完成两个工作
1、确保应答报文和发出去的数据,能够对应,不要出现歧义
2、确保在出现 后发先至 的现象是,能够让应用程序仍然按照正确的顺序来理解数据
这个时候,我们就需要用到
每一个 ack 都有一个确认序号,序号的大小关系,就描述了数据的先后顺序
序号不是按照“一条两条”方式来编写的,而是按照字节来编号的
假设最开始传输的第一个字节,序号是 1
从前往后进行累加
一个 TCP 数据包里一共有 1000 个字节的载荷数据
其中第一个字节的序号是 1,就在 TCP 报头的序号字段中,写"1"
由于一共是 1000 个字节,此时最后一个字节的序号自然就是1000 了
但是 1000 这样的数据并没有在 TCP 报头中记录
TCP 报头中记录的序号,是这一次传输的载荷数据中第一个字节的序号
剩下其他字节的序号,都需要依次的推出
在 应答报文中,就会在 确认序号 字段中填写 1001
因为收到的数据是 1-1000,所以 1001 之前的数据,都被 B 收到了或者也可以理解成,B 接下来向 A 索要 1001 开始的数据
这样通过特殊的 ack 数据白,里面携带的“确认序号”告诉发送方,哪些数据已经被确认收到了
此时发送发,就知道自己刚发的数据是到了还是没到,这就是“可靠传输”
TCP 的初心,就为了实现可靠传输
达成可靠传输的最核心的机制,就是确认应答
如何区分一个数据包是普通数据 还是 ack应答数据呢?
这一位为 1,表示当前数据包是一个应答报文,此时该数据包中的“确认序号字段”就能够生效
这一位为 0,表示当前数据包是一个普通报文,此时数据包中的”确认序号字段”是不生效
经典面试题:
TCP 是如何保证可靠传输的?
确认应答为核心,借助其他机制辅助,最终完成可靠传输
2.3.5 超时重传
确认应答,描述的是一个比较理想的情况
如果网络传输中,如果出现丢包,发送方,势必就无法收到 ack
这个时候我们就需要使用超时重传,针对确认应答,进行补充
由于丢包是一个“随机”的事件,因此在上述 tcp 传输过程中,丢包就存在两种情况
1、传输的数据丢了
2、返回的 ack 丢了
但是站在发送方的角度,无法区分这两种情况
无论出现上述那种情况,发送方都会进行“重新传输”
重传操作,大幅度提升了数据能够被传过去的概率
重传就是一个很好的丢包下的补救措施了
当引入“可靠性”的时候,是会付出代价的
最明显的代价,是两方面:
1、传输效率
2、复杂程度
发送方什么时候进行重传?
发送方,发出数据之后,会等待一段时间,如果这个时间之内,ack 来了,此时就自然十位数据到达
如果达到这个时间之后,数据还没到,就会触发等待机制
等待时间:
- 初始的等待时间,是可以配置的,不同的系统上都不一定一样
也可以通过修改一些内核参数来引起这里的时间变化 - 等待的时间,也会动态变化
每多经历一次超时,等待时间都会变长
但是时间拉长也不是无线拉长,重传若干次,时间拉长到一定的程度。任务数据再怎么重传也没用课,就放弃 tcp 连接(触发 tcp 的重置连接操作)
变长,隐含的意思,就是对能够正确传输数据这件事情,非常悲观
按照丢包概率 10% 计算,每次重传一次都会让传输成功的概率大幅度增加
反之,重传仍然失败的话,说明当前丢包的概率不止10%,应该是非常大的数值, 很可能网络上已经出现严重故障了
此时重传的再快,也没用,还不如重传的慢点,少传几次省点力气
如果你的数据丢了,是不会触发 ack 的
对端没有收到数据,自然不会返回 ack
(后续在滑动窗口机制下,可能会出现这种情况此时确实返回的 ack 是发送数据的第一个字节)
站在 B 的视角,收到了两条一样的数据
收到重复数据,是否会给程序带来一些 bug?
比如发的是一条数据,收的时候收到;两条数据(inputStream.read,读出来的是两条一样的数据)
在 TCP 中已经把这个问题解决了
TCP 中有一个“接收缓冲区”就是一个内存空间,会保存当前已经收到的数据,以及数据的序号
接收方如果发现,当前发送方发来的数据,是已经在接收缓冲区中存在的(收到过的重复数据了)
接收方就会直接把这个后来的数据给丢弃掉,确保应用程序进行 read 的时候
读到的是只有一条数据
接受缓冲区,不仅仅是能进行去重,还能进行重新排序
确保发送的顺序,和应用程序读取的顺序是一致的
2.3.6 连接管理
建立连接 + 断开连接
这也是面试中最经典的问题:
三次握手(建立连接)
四次挥手(断开连接)
握手(handshake):
打招呼的内容,没有实际意义,也比较简短,只是为了唤起对方的注意
tcp 这里的握手,也是类似,也就是给对方传输一个简短的,没有业务数据的数据包
通过这个数据包,来换起对方的注意,从而触发后续的操作
挥手和握手类似
握手这个操作,不是 TCP 独有的,甚至不是网络通信独有的
计算机中很多的操作,都会涉及到“握手”
2.3.6.1 TCP三次握手
TCP 的三次握手,TCP 在建立连接的过程中,需要通信双方一共“打三次招呼”才能够完成连接建立的
A 想和 B 建立连接
A 就会主动发起握手操作
实际开发中,主动发起的一方,就是所谓的“客户端”
被动接受的一方,就是“服务器”
同步报文段:
- 段 segment
- 包 packet
- 报 datagram
- 帧 frame
同步报文段,就是一个特殊的 TCP 数据包
不过没有载荷(不携带业务数据的)
此时,握手完成
此时,A 和 B 记录了对方的信息
(也就是构成了“逻辑”上的连接)
三次握手建立连接的过程,其实是,通信双方都要给对方发起 syn,也都要给对方反馈 ack
一共是 4 次握手,但是中间两次,恰好可以合并成一次
三次握手要解决什么问题?
通过四次握手,是否可行?两次呢?
- 四次,可以但是没有必要,两个接收方会不知道发送方的情况
TCP 是为了实现“可靠传输”
进行确认应答 和 超时重传有个大前提,当前的网络环境是基本可用的,是通畅的
三次握手的核心作用:
- 投石问路,确认当前网络是否是畅通的
- 要让发送方 和 接收方,都能确认自己的发送能力 和 接收能力均为正常
- 让通信双方,在握手过程中,针对一些重要的参数,进行协商
比如:
握手这里要协商的信息,其实是有好几个,但是此处不做过多讨论
但是至少要知道,,tcp 通信过程中的序号从几开始,就是双方协商出来的(一般不是从 1 开始的)
每次连接建立的时候,都会协商出一个比较大的,和上次不一样的值
有的时候,网络如果不太好,客户端和服务器之间可能会连接断开,再重新建立连接
重连的时候,就可能在新的连接好了之后,旧的连接的数据姗姗来迟
这种迟到的数据,应该要丢弃掉的,不应该让这个上次数据影响到本次的业务逻辑
如何区分数据是否是来自于上次传输?
就可以通过上述序号的设定规则来实现
如果发现收到的数据序号和当前正常数据的序号差异非常大,就可以判定为是上次的数据,就可以直接丢弃了
2.3.6.2 四次挥手
连接断开,四次挥手
建立连接,一般都是客户端主动发起的
断开连接,客户端和服务器都可以主动发起
FIN 就就叫做“结束报文段”
此时连接就断开了
这个时候,就相当于 A 和 B 都把对端的信息删除了
和三次握手不同,此处的四次握手,能否把中间的两次交互合二为一?
这个是不一定的
不能合并的原因,ACK 和 第二个 FIN 的触发时机是不同的
ACK 是内核响应的,B 收到 FIN 就会立即返回 ACK
第二个 FIN 是应用程序的代码触发,B 这边调用了 close 方法才会触发 FIN
从服务器收到 FIN(同时返回 ACK),再到执行到 close 发起 FIN,这中间要经历多少时间,经历多少代码是不确定的
FIN 就会在 socket 对象 close 的时候,被发起
可能是手动调用 close 方法,也可能是进程结束
像前面的三次握手,ACK 和第二个 syn 都是内核触发的,同一个时机,是可以合并的
这里的四次挥手,ACK 是内核触发的,第二个 FIN 是应用程序执行 close 处罚的,时机不同,是不可以合并的
是否一位置,如果我这边代码 close 没写/没执行到么是不是第二个 FIN 就一直发不出去?(有可能)
如果是正常的四次挥手,正常的流程断开的边界
如果是不正常的挥手(没有挥完四次),异常的流程断开连接(也是存在的)
但是,TCP 中还有一个机制,延时应答(能够拖延 ACK 的回应时间)
一旦 ACK 滞后了,就有机会和下一个 FIN 合并在一起了
哪一方,主动断开连接,哪一方就会进入 TIME_WAIT
TIME_WAIT 转台主要存在的意义,就是为了防止最后一个 ACK 丢失
如果最后一个 ACK 丢失,站在 B 的角度,B 就会触发超时重传
重新把刚才的 FIN 给传一遍
如果刚才 A 没有 TIME_WAIT 状态,就意味着 A 这个时候就已经真的释放连接了
此时重传的 FIN 也就没人能处理,没人能返回 ACK 了
B 永远也收不到 ACK 了
A 使用 TIME_WAIT 状态进行等待,等待的这个时间,就是为了处理后续 B 重传的 FIN
此时有重传的 FIN 来了,就可以继续返回ACK 了
B 这边的重传 FIN 才有意义
TIME_WAIT 等待多久呢?
假设网络上两个节点通信小号的最大时间为 MSL
(MSL:可配置的参数)
此时 TIME_WAIT 的时间就是 2MSL
(2MSL 已经是上限了,绝大部分的数据包不会达到这个时间)
2.3.7 滑动窗口
前三个机制,都是在保证 tcp 的可靠性
TCP 的可靠传输,是会影响传输的效率的
(多出了一些等待 ack 的时间,单位时间内能传输的数据就少了)
滑动窗口,就让可靠传输对性能的影响,更少一些
TCP 只要引入了可靠性,传输效率是不可控超过,没有可靠性的 UDP 的
TCP 这里的“效率机制”都是为了让 影响更小,缩短和 UDP 的差距
滑动窗口可以缩短确认应答的等待时间
这是没有滑动窗口的传输:
每一次收到一个应答报文,再发下一个数据
这个过程中,等待时间比较长
滑动窗口 的核心就是批量传输数据
不等 ack 回来,直接再发送下一个数据
批量传输,也不是“无限的”传输
批量传输也是存在一定的上限的,达到上限之后,再统一等待 ack
不等待的情况下,批量最多发送多少数据,这个数据量,称为“窗口大小”
当前 A 是给 B 批量的发送了四份数据
此时 B 也要给 A 回应 四组 ACK
此时 A 已经达到了窗口大小,再收到 ACK 之前,就不能继续往下发了
需要等待有 ACK 回来了之后,才能继续往下发
这四个 ACK 回来一个就继续发一个
这里看起来的直观效果,这个“窗口”就开始往后“滑动”
窗口越大,等待的 ack 越多,此时传输的效率就越高
上述滑动窗口中,确认应答是可以正常工作的
但是,如果出现丢包了怎么办?
这里的重传,相比于前面的超时重传,又有变化
情况一:ack 丢了
这种情况,不需要任何重传
因为确认序号就可以说明,确认序号表示,当前序号之前的数据,已经确认收到了
下一个应该从确认序号这里,继续发送
如果1001 这个 ack 丢了,但是 2001 ack 到了
2001 之前的数据都已经确认传输成功了(涵盖了 1001 的情况)
情况二:数据包丢了
主机 A 就需要知道是哪个数据丢了
主机 B 也就要告诉 A 是哪个数据丢了
由于前面的 1001-2000 这个数据没了
此处返回的 ack 仍然是索要 1001
无论当前传输的数据具体是几,都在索要 1001 这个数据
此时,主机 A 看到了 B 这边连续的几个 ack,都是索要 1001,A 就知道了,1001 这个数据就是球了,就重传了 1001
一旦把 1001-2001之后,顺利到达之后,B 索要的就是 7001
因为前面的数据都顺利传到,存储在 B 中
上述的重传过程中,并没有额外的冗余炒作,哪个诗句丢了,就重传哪个,没丢的数据则不需要重传
这整个过程都是比较快速的(快速重传)
(快速重传,是滑动窗口下,超时重传的变种)
如果通信双方,传输数据的量比较小,也不频繁,就仍然是普通的确认应答和普通的超时重传
如果通信双方,传输数据量更大,也比较复杂,就会进入到滑动窗口,按照快速重传的方式处理
通过滑动窗口的方式传输数据,效率是会提升的
窗口越大,传输效率就越大(一份时间,等待的 ack 更多了,总的等待时间更少了)
滑动窗口,设置的越大,越好吗?
如果传输的速度太快,就可能会使 接收方,处理不过来
此时,接收方也会出现丢包,发送方还得重传
TCP 前提是可靠性,可靠性的基础上,在提高传输效率
2.3.8 流量控制
站在接收方的角度,反向制约发送方的传输速率
发送方发送的速率,不应该超过接收方的处理能力
数据到达 B 的系统中
tcp socket 对象上带有接收缓冲区
A 给 B 发的数据,就会先到达 B 的接收缓冲区
B 这边还有应用程序,就会调用 read 这样的方法,把数据从接收缓冲区读出来,进一步的进行处理
(一旦数据被 read了,就可以从接收缓冲区删除了)
(这也是生产者消费者模型)
生产者:A
消费者:B 的应用程序
交易场所:B 的接收缓冲区(相当于一个阻塞队列)
消费速度,就是所谓的“处理能力”(取决于 B 的代码怎么写)
如何量化衡量这个处理速度?
直接通过接收方,缓冲区的剩余空间大小,作为衡量处理能力的指标
剩余空间越大,意味着消费速度越快,处理能力就越低
剩余空间越小,意味着消费速度越快,处理能力就越弱
接收方每次收到数据之后,都会把接收缓冲区剩余空间大小,通过 ack 返回给发送方
发送方就会按照这个数值来调整下一轮的发送速度
假设,接收缓冲区,总的空间是 4000
收到 1000 数据忠厚,还剩 3000
于是就把 3000 放到应答报文中,告诉发送方了
怎么告诉发送方呢?通过应答报文中的 16 位窗口大小
16 位窗口大小 中 是否最大只能表示 64k 呢?
TCP 报头中,选项部分里有一项是叫做“窗口扩展因子”
通过扩展因子,就可以让窗口大小表示一个更大的值
这个时候发送方依据收到的 3000 确实了这一轮发送的窗口大小
假设在这个过程中,接收方的应用程序还没来得及处理任何数据,此时收到一个数据,接收缓冲区的剩余空间就缩小一分
反馈 0,意味着告诉 A ,接收方已经满了,暂时别发送数据了
此时 A 就要暂停发送
需要暂停多久呢?
此时们虽然不传输业务数据了
仍然会周期性的发送一个“窗口探测包”,并不携带具体的业务数据
探测包就只是为了触发 ack,为了查询当前接收方这边的接收缓冲区剩余空间
2.3.9 拥塞控制
流量控制,是考虑的接收方的处理能力
但是网络中,不仅仅只有接收方,还有你整个的通信的路径
拥塞控制,就是考虑/衡量 通信过程中,中间节点的情况
关键问题:
接收方的处理能力,很方便进行量化
但是中间节点,结构更复杂,难以直接的进行量化,因此就可以使用“实验”的方式,来找到个合适的值
让 A 先按照比较低的速度先发送数据(小的窗口)
如果数据传输过程非常顺利,没有丢包
再尝试使用更大色窗口,更高的速度进行发送(一点一点变化)
随着窗口大小不停地增大,达到一定程度,可能中间节点就会出现问题了,此时这个节点就可能出现丢包
发送方发现丢包了,就把窗口大小调整小
此时如果发现还是继续丢包,继续缩小,如果不丢包了,就继续尝试变大
在这个过程中,发送方不停的调整窗口,逐渐达成“动态平衡”
这种做法,就相当于把中间节点,都视为“整体”,通过实验的凡是,来找到中间节点的瓶颈在哪里
不过上图是经典版本的拥塞控制
后面的 tcp 又对这里进行了改善
改进设施,不是一下让速度归零,而是仍然有一定的初始速度
流量控制 和 拥塞控制
都是在限制发送方的窗口的大小
最终时机发送的窗口大小,是取 流量控制 和 拥塞控制 中的窗口的较小值
2.3.10 延时应答
A 把数据传输给 B ,B 就会立即返回 ack 给 A(正常情况)
但也有时候,A 传输给 B,此时 B 等一会再返回 ack 给 A(延时应答)
本质上也是为了提升传输效率
发送方的窗口大小,就是传输效率的关键
流量控制这里,就是根据接受缓冲区的剩余空间,来决定发送速率
如果能够有办法,让这个流量控制得到的窗口更大,发送速率就更快
(大点的前提,能够让接收方还是能处理过来的)
延时应答这里,延时返回 ack,给接收方更多的时间,来读取接收缓冲区的数据
此时接收方读了这个数据之后,缓冲区剩余空间就变大了
返回的窗口大小也就更大了,传输速率也就更高了
比如:
初始情况下, 接收缓冲区剩余空间是 1kb
如果立即返回 ack,返回了 10kb 这么大的窗口
如果延时个 200ms 再返回,这 200ms 的过程中,接收方的应用程序的,又读了 2kb
此时,返回的 ack,就可以返回 12kb 的窗口了
延时应答,才促成了上面的四次挥手,能够三次挥完
2.3.11 捎带应答
捎带应答是在延时应答的基础上,进一步的提高效率
网络通信中,往往是这种“一问一答”这样通信模式
ack 也是内核立即返回的
response 则是应用程序代码来返回
这两者时机是不同的
由于 tcp 引入了延时应答,上面的 ack 不一定是立即返回,可能需要等待
在等待的过程中,B 就正好把 response 给计算好了
计算好了之后,就会把 response 返回
于此同时顺便就把刚才要返回的 ack 也带上了
本来就是要传输两个 tcp 数据包(封装分用两遍)
目前通过上述操作,就可以把两个包合并成一个了
2.3.12 面相字节流
这里有一个最重要的问题,粘包问题,面相字节流的机制都有类似的情况
此处的“包”是应用层数据包
如果有多个应用层数据包被传输过去,此时就容易出现粘包问题
目前,接收缓冲区中,这三个应用层数据包的数据,就是以字节的形式紧紧挨在一起的
接收方的应用程序,读取数据的时候
可以一次读一个字节,也可以读两个字节,也可以读 N 的字节
但是最终的目标是为了得到完整的应用层数据包
B 应用程序,就不知道,缓冲区里的数据,从哪里到哪里是一个完成的应用数据包了
相比之下,像 UDP 这样的面向数据报的通信方式,就没有上述的问题
UDP 的接收缓冲区中,相当于是一个一个的 DatagramPacket 对象
应用程序读的时候,就能明确只带哪里到哪里是一个完整的数据
如何解决粘包问题?
核心思路:通过定义好应用层协议,明确应用层数据包之间的边界
- 引入分隔符
- 引入长度
比如,用 \n 作为分隔符:
这个时候,应用程序,读取数据的时候
就可以一直持续读数据,直到读到 \n 为止
前面写的回显服务器,就是这样的方式
定义长度:
应用程序读程序的时候,就可以先读两个字节,得到数据包的长度
再根据这个长度,继续读取对应字节的个数
自定义应用层协议的格式
xml,json,protobuffer,本身都是明确了包的边界的
2.3.13 异常情况的处理
如果在使用 tcp 的过程中,出现意外,会如何处理?
(1)进程崩溃
进程异常终止了,这样文件描述符表,也就释放了,相当于调用 socket.close()
此时就会触发 FIN(三次握手四次挥手),对方收到之后,自然也就会返回 FIN 和 ACK,这边再进行 ACK(正常的三次握手四次挥手断开连接的流程)
TCP 的连接,可以独立于进程存在(进程没了,TCP 连接不一定没有)
(2)主机关机(正常流程)
在进行关机的时候,就是会先触发强行终止进程操作
此时就会触发 FIN,对方收到之后,自然也就会返回 FIN 和 ACK
不仅仅是进程没了,整个系统也可能关闭了
如果在系统关闭之前,对端返回的 ACK 和 FIN 到了,此时系统还是可以返回 ACK,进行正常的四次挥手的
如果系统已经关闭了,ACK 和 FIN 迟到了,无法进行后续 ACK 的响应
站在对端的角度,对端会以为自己的 FIN 丢包了,重传 FIN
重传几次都没有响应,自然就会放弃连接(把持有的对端信息就删了)
(3)主机掉电(非正常)
此时,是一瞬间的事情,来不及杀进程,也来不及发送 FIN,主机直接就停机了
站在对端的角度,对端也不知道怎么办
- 如果对端是在发送数据(接收方掉电)
发送的数据就会一直等待 ack,触发 TCP 连接重置功能,发起“复位报文段”
如果复位报文段 发过去之后,也没有效果,此时就会释放连接了 - 如果对端是在接收数据(发送方掉电)
对端还在等待数据到达,如果数据一直不到,此时是无法区分是对端没发小时,还是对方挂了
TCP 中提供了 心跳包 机制
接收方也会周期性的发送方发起一个特殊的,不携带业务数据的数据包,并且期望对方返回一个应答
如果对方没有应答,并且重复了多次之后,仍然没有,就视为对方挂了,此时就可以单方面释放连接了
(4)网线断开
网线断开 和 刚才的主机掉电非常类似
当前假设,是 A 正在给 B 发送数据,一旦网线断开
A 就相当于会触发超时重传,然后会连接重置,最后单方面释放连接
B 就会触发心跳包,如果发现对端没有响应,就单方面释放连接
TCP 的心跳机制
在我们的日常开发中,经常见到这种心跳机制(尤其是在分布式系统中)
如何识别某个机器是否挂了,一般都是通过心跳机制来检测
一般后续用到的心跳,都是在应用程序中,自主实现的
而不是直接使用 TCP 的心跳
因为 TCP 的心跳直接,周期比较长
而是希望是 秒级 甚至 毫秒级 就能检测出对端
2.4 TCP / UDP对比
TCP 优势在于 可靠传输,适用于绝大部分场景
UDP 优势在于 更高效率,更适用于,对于“可靠性不敏感”,“性能敏感”场景
(比如,局域网内部(同一个机房)的主机之间通信)
同一个机房内部,网络结构简单,带宽充足,交换机/路由器 网络设备负载程度也不是很高
出现丢包的概率就不大
往往也就希望机器之间数据传输能更快
如果传输比较大的数据包,TCP 更优先(UDP 有64kb 的限制)
如果要进行“广播传输”,优先考虑 UDP,UDP 天然支持广播,TCP 不支持
有一种特殊的场景,需要把数据发送给局域网的所有的机器
这个情况就是广播
比如:投屏功能
手机和电视得在同一个局域网下,手机这边触发投屏功能,就会往局域网发起广播数据包
电视此时就会回应,并把自己的 ip 端口告诉手机,这样手机就可以完成后续的投屏了
经典面试题:
如何基于 UDP 实现可靠传输?
这个问题本质上是考察 TCP
就是把 TCP 的可靠传输讲出来
- 确认应答
- 引入序号 确认序号
- 超时重传
- 滑动窗口
- …
这里主要还是要讲出来,并不是真的写代码