文章目录
- 1. 应用层和传输层的联系
- 2. UDP协议
- 3. TCP协议
- 3.1 TCP报头介绍
- 3.2 TCP实现可靠传输的核心机制
- (1)确认应答
- (2)超时重传
- (3)连接管理
- 建立连接(三次握手)
- 断开连接(四次挥手)
- (4)滑动窗口
- (5)流量控制
- (6)拥塞控制
- (7)延时应答
- (8)捎带应答
- (9)面向字节流(粘包问题)
- (10)异常情况
1. 应用层和传输层的联系
在网络通信过程中,应用层描述了应用程序如何理解和使用网络中的通信数据。
而程序猿在应用层这里最主要的工作就是自定义协议,自定义协议主要做两件事:
- 结合业务场景和需求,明确协议数据要传递哪些信息。
- 明确数据组织格式。(比如:可以按照纯文本方式,也可以使用
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点 :
- 要确定这个端口没有和程序绑在一起.
- 要拥有管理员权限.
2. UDP协议
UDP
是User Datagram Protocol
的缩写, UDP
的特点是无连接, 不可靠传输, 面向数据报, 全双工, UDP
使用起来简单高效, 但它的数据载荷较小.
UDP协议报文结构(教材中):
但实际中,以下方式更为合理:
UDP
就会把载荷数据(就是通过UDP socket
,也就是send()
方法拿来的)的基础上再前面拼接上几个字节的报头。此处的拼接相当于字符串拼接。(此处是二进制的,不是文本的)
UDP
报头中就包含了一些特定的属性,就携带了一些重要的信息。不同的协议,功能不同,报头中带有的属性信息就不同。
对于UDP
来说,报头一共就是8
个字节,分为4
部分(每个部分2字节)。2个字节表示的范围是0-->65535
,换算单位为64KB
。也就是一个UDP
数据报,最大只能传输64KB
的数据。
在当下的时代,64KB是一个很小的量。如果应用层数据报超过了64KB,怎么办呢?
- 需要在应用层,通过代码的方式针对应用层数据报进行手动的分包,拆成多个包通过多个UDP数据报进行传输。(本来要send一次,现在需要send多次)。举个🌰:我要搬家,但是我的行李比较多。此时有两种方式:方法1:多叫几辆小面包车。方法2:直接叫一辆大卡车。显然,方法2更便捷!
- 不用UDP,换成TCP。TCP没有这样的限制。
校验和:作用是验证传输的数据是否是正确的, 网络传输, 本质上是在传输光信号/电信号, 在传输过程中, 可能会受到一些物理环境的干扰等, 在这些干扰下就可能出现 “比特翻转” 的情况, 0会变成1, 1会变成0.
一旦数据变了, 对于数据含义的解析可能就致命的, 举一个典型的例子, 程序中经常使用1表示某个功能开启, 0表示关闭, 本来网络数据报是想开启功能, 结果因为翻转, 就导致变成了关闭了.
像这样的现象是客观存在不可避免的, 我们能做的只是及时的识别出当前的数据是否出现了问题, 因此就引入了校验和来进行鉴别, 校验和是针对数据的内容进行一系类的运算(每一个比特位都会参与运算)得到一个比较短的结果, 我们可以认为, 数据内容一定, 得到的校验和就是相同的, 如果我们的数据变了, 那么的校验和就变了, 如此即可验证得到数据是否准确.
举个🌰:
比如发送方要发送的数据是 “反射导弹”, 计算出来的校验和也会放到报头中发送, 当接收方拿到数据就会有两种情况了, 第一种情况是接收方拿到数据后重新计算一个校验和, 得到的结果如果和拿到的校验和相同, 就认为数据是正确的, 不同, 则认为数据不正确.
还有一种情况:数据传输过程出错了, 但是可能计算得到的校验和和之前的校验和恰好─样, 这种情况理论上是存在的, 但在工程实践中出现这种情况的概率极小, 就忽略不计了.
针对网络传输中, 生成校验和的算法有很多种, 这里简单介绍一下下面几个比较知名的:
CRC
(循环冗余校验): 这种方法简单粗暴, 就是报数据的每个字节循环往上累加, 如果累加溢出, 高位就不要了, 这种方法比较好算, 但是校验的效果不够理想, 万一你的数据同时变动了两个bit位(前一个字节少1, 后一个字节多1), 就会出现内容变了, CRC没变这样的情况.MD5
: MD5是使用一系列公式来进行更复杂的数学运算, MD5算法得到的结果具有下面几个特点:- 定长:无论原始数据多, 得到的MD5值都是固定长度(4/8字节).
- 冲突概率很小: 原始数据即使变动了一点点, 算出来的MD5值都会差别差别很大(让MD5结果更分散了).
- 不可逆: 通过原始数据计算出MD5比较容易, 但是通过MD5还原成原始数据很难, 理论上是不可实现的(计算量极大).
基于MD5的这些特点, 让MD5适用于更多场景, 计算校验和, 作为哈希函数计算Hash值, 用于加密等; 网上会看到一些解密MD5的方法, 都不是真正的解密, 因为真正的的想将数据还原回去凭借现在的计算机是做不到的, 这里的解密实质上是将一些常见的字符串的MD5值进行汇总成一张表, 解密的过程相当于查表.
SHA1
: SHA1和MD5是类似的, 只是运算过程不一样。
3. TCP协议
3.1 TCP报头介绍
TCP
, 即Transmission Control Protoco
, TCP协议相比于UDP协议要更复杂, TCP的特点是有连接, 可靠传输, 面向字节流, 全双工.
端口号:与UDP相像。此处不详细介绍。
32位序号,32位确认序号:后文详解。
数据偏移和选项(option), 选项可有可无也可以有多个, 用于对TCP一些功能的扩展和TCP中的一些属性进行解释说明, 可能包括 “窗口扩大因子”, “时间戳” 等选项, 数据偏移表示TCP数据起始处与TCP报文起始处之间的距离, 也就是4个比特位(0到15)表示TCP首部报头的长度, 单位是4字节。
正是由于TPC当中有了数据偏移和选项这两个属性, 致使使TCP的报头长度是可变的, 不像UDP一样固定是8字节, 选项之前的部分是固定的长度(20字节), 选项长度 = 首部长度 - 20字节, 通过首部长度就可以去调节选项长度; 如果首部长度值是5, 表示整个TCP报头是20字节(相当于没有选项), 如果首部长度值是15, 表示整个TCP报头是60字节(选项部分就是40字节), 填充是为了保证选项为32比特的整数倍.
保留项, 数据偏移后面还有6位保留项, 这里保留项的存在是为了未来TCP协议的拓展升级准备的, 网络协议的拓展升级是一件成本极高的事情, 比如现在UDP协议报文长度是最大是2字节(64KB), 如果想要升级一下让UDP的报文支持更大的长度, 在技术上可以实现, 但实际上要想让世界上所有能上网的设备所安装的各式的操作系统都能够同步完成升级, 支持新的UDP, 这是不现实的; 一种系统升级了, 其他系统不升级, 就办法进行通信了。
而像TCP这样引入了 “保留位”, 如果在未来想要引入一些新的功能, 就可以使用这些保留位, 这样对于TPC本来的报头结构影响是比较小的, 针对不升级的设备也更容易兼容就版本的TPC.
剩下的属性TCP内部的工作机制有关, 在后文的内容中会详细介绍。
3.2 TCP实现可靠传输的核心机制
我们来看个段子doge
哈哈哈哈哈嗝~
(1)确认应答
确认应答是实现TCP可靠传输最核心的机制, 这里的可靠并不是指发送方能够百分百能将数据发送到接收方, 可靠传输是要尽可能的把数据发过去, 发送方能够准确的知道接收方是否收到了数据.
比如说,男神和我的关系是非常好的,我发的消息他一定会回复我。当我给我的男神发消息说我要请他吃麻辣烫,当你看到他的回复的时候,你就知道他的消息是收到了的。
这里男神回复的“好啊好啊”就称为“应答报文”。也称作ACK
(acknowledge),TCP进行可靠传输最主要的就是靠这个确认应答机制来保证得到。A给B发一个消息,B收到之后就会返回一个ACK,A收到 这个应答之后,就知道了发的数据顺利到达B(没有丢包)如果没有收到ACK
就说明A发的消息大概率不见了(丢包了)。
我们考虑更复杂一点的情况:
注意:此处我可能连发了两条消息。我发第二条消息,是不需要等待第一个消息的回应的。男神是收到消息就会立即回应~
**网络上可能存在“后发先至”,在这个情况下,收到消息的顺序是可能存在变数的!**就会出现以下情况:
由于“后发先至”,我先收到的滚,后收到的好啊好啊。
很明显,这里的应答就错乱。此时表示的含义就出现歧义了!
结论:网络后发先至这个现象是可观存在的,无法避免的。因此应答报文到达的顺序也是可能发生变动的,此时就需要考虑如何规避这种顺序错乱带来的歧义。
那么,如何解决上述后发先至问题呢?
**编号。**给传输的数据和应答报文,都进行编号,即可。
当我们引入序号之后,此时就不怕顺序乱了。即使顺序乱了,也可以通过序号来区分当前应答报文是针对哪个数据进行的了。
任何一条数据(包括应答报文)都是有序号的。确认序号,则是只有应答报文有(普通报文确认序号字段里的无意义)
这一条报文是否是应答报文,取决于ACK标志位是否为1。如果ACK为1,表示是应答报文;如果为0表示不是应答报文。
注意:应答报文的序号仅仅是一个身份表示。不需要应答应答报文。
实际上TCP的序号并不是按照“一条两条”这样的方式来编号的。TCP是面向字节流的。TCP的序号是按照字节来编号的。
黄色数据是1000个字节。假设是从1开始编号,此时第一个字节序号就是1.第二个字节序号就是2…但是由于这1000个字节都属于同一个TCP报文。此处报头的序号写的就是1.
接下来如果发送蓝色数据。此时第二个TCP数据报的头一个字节序号就相当于是1001.如果蓝色数据长度是1000,此时最后一个字节序号是2000.由于1001-2000都是属于一个TCP数据报。报头里只需要填写1001即可。
TCP的字节的序号是依次累加的。这个依次累加的过程对于后一条数据来说,起始字节的序号就是上一个数据的最后一个字节+1.每个TCP数据报报头填写的序号只需要写TCP数据的头一个字节的序号即可。TCP知道了头一个字节的序号。再根据TCP报文长度,就很容易知道每个字节的序号。
小结:TCP可靠传输能力,最主要就是通过确认应答机制来保证的。通过应答报文,就可以让发送方清楚的直到传输是否成功。进一步的引入了序号和确认序号。针对多组数据进行详细的区分。
(2)超时重传
在上述对确认应答的讨论中,只讨论了顺利传输的情况。如果丢包了怎么办?
丢包,会涉及两种情况:
- 发送的数据丢了
- 返回的ack丢了
发送方看到的结果,就是没有收到ack,无法具体区分是哪种情况。这两种情况一视同仁,都认为是丢包了。
丢包是一个概率性事件。通常情况下,丢包的概率是比较小的。因此,如果重新发送一下这个数据报,还是有很大可能传输成功的。
因此TCP就引入了重传机制,也就是在丢包的时候,就要重新发送一次同样的数据。
(到底当前的这次传输,是丢包了,还是ack走的慢?在路上?)
TCP引入了一个时间阈值。
发送方发送了一个数据之后,就会等待ack,此时开始计时。如果在时间阈值之内,也没有收到ack,此时不管ack是否在路上,还是彻底丢了,都认为是丢包了!
超时重传,超过一定时间,还没有响应,就重新传输。
这个超时时间,具体是多少ms。这个问题是有待研究的。因为这个时间是可配置的,并且在不同系统上面的默认值都可能存在差别。
由于重传机制,就有可能导致接收方重复的消息收到了多次。
上图第二种情况中,对于主机B来说。1-1000就收到了两次。
TCP对于这种重复数据的传输,是有特殊处理的。去重。TCP存在一个“接收缓冲区”这样的存储空间(接收方操作系统内核里的一段内存)。每个TCP的socket对象,都有一个接受缓冲区。(其实也有一个发送缓冲区)。主机B收到主机A的数据。其实是B的网卡读到数据了,然后把这个数据放到B的对应的socket的接受缓冲区中。此处的接收缓冲区可以想象成为一个阻塞队列(优先级队列或者有序队列)。根据数据的序号,TCP很容易识别当前接收缓冲区里的这两条数据是否是重复的。如果重复,就把后来的这份数据直接丢弃了。保证了应用程序调用read读到的数据,一定是不重复的。后续应用使用getInputStream,进一步的使用read,就是从接受缓冲区来读数据的。
小结:由于去重和重新排序机制的存在,发送方只要发现ack没有按时到达,就会重传数据,即使重复了,即使顺序乱了,都没事,接收方都能很好地处理好。(去重和排序都依赖于TCP报头的序号)
TCP的可靠传输就是通过确认应答和超时重传来体现出来的, 其中确认应答描述的是传输顺利的情况, 超时重传描述的是传输出现问题的情况, 这两者相互配合, 共同支撑整体的TCP可靠性.
(3)连接管理
TCP要完成通信是需要先建立连接的, 所以就有了连接管理的机制, 连接管理一定程度上也可以体现TCP的可靠性, 但保证可靠传输最核心的机制还是上面介绍的确认应答和超时重传.
TCP这里的连接指的是由一个四元组(源IP, 源端口, 目的IP, 目的端口)来标识, 一个连接建立完成就表示通信双方知晓对方的IP和端口信息, 就是通信双方各自都维护着连接这样的一个数据结构, 双方把对方的地址信息都保存下来就是完成了连接, 而断开连接就是把各自存储的连接删除掉.
对于TCP的连接管理就是建立连接(三次握手), 断开连接(四次挥手)了.
建立连接(三次握手)
客户端与服务器之间进行三次交互建立连接的过程被形象地称为 “三次握手”, 在这三次交互中, 通信双方要完成对彼此信息的记录.
还是我和我的男神聊天, 你想要让男神做我的男朋友, 于是就有了下面的对话.
首先是我向男神表白,男神接受了就表明, 你和男神就有了一个认同, 男神是你的唯一, 但此时我和男神还没有成为男女朋友的关系, 因为男神不知道你是不是他的唯一(万一我脚踏好几只呢~~ (剧情需要,狗头保命。doge) ~~) , 然后男神也对你表白, 我有接收了, 此时就说明我俩互为唯一了, 这就真正建立了男女朋友关系, 相当于客户端和服务器建立成功了。
把每次通信形象的称为一次握手, 上面的过程就是四次握手了, 但在实际TCP的建立连接过程上是三次握手, 其实就相等于于把上面男神的两条消息合并成了一条消息.
小结:所谓的三次握手,本质上是“四次交互”。通信双方,各自要向对方发起一个“建立连接”的请求。同时,再各自向对方回应一个ack。这里一共是有四次信息交互。但是中间两次交互可以合成一次交互的。因此就构成了“三次握手”。
我们知道客户端是主动发出请求的一端, 服务器是被动响应的, 所以, TCP建立连接需要让客户端向服务器发送一个连接请求, 即SYN, 然后服务器收到请求后会给客户端响应一个ACK和SYN, 客户端收到服务器的SYN后会立即发送一个ACK给服务器, 服务器收到客户端的确认应答后,客户端与服务器就连接成功了.
值得注意的是,服务器给客户端发送的ACK和SYN这两次通信是必须要进行合并的, 因为封装分用两次一定是比一次成本要高的, 而建立连接的这四次信息交互是在纯内核中完成的, 服务器的系统内核是可以做到收到SYN之后就立即同时发送ACK和SYN的, 也就必须是这样做。
以上是三次握手建立连接的作用。除此之外,三次握手另外一个重要作用:**验证通信双方各自的发送能力和接收能力是否正常。**三次握手,也是一定程度的保证了TCP传输的可靠性。(起到的不是关键作用,辅助作用)
举个🌰:
我要和男神来局和平精英。我俩交流需要连麦。我给男神发了个系统文字消息:请打开麦克风交流!然后男神打开了麦克风。此时:
第一次通信,当男神听到了我的声音,男神就知道我的麦克风和他的耳机是ok的,此时我什么都不知道;第二次通信,当我听到了男神的回复,我就知道男神的麦克风和我的耳机是好的。但是男神还不知道自己的麦克风和我的耳机是否ok;第三次通信,当男神再次听到我的回复,我们就知道了双方的耳机和麦克风都ok。这里的麦克风就对应发送能力, 耳机就对应接收能力。
再从实际上TCP的三次握手来说, 当发送方发出SYN后, 接收方都到发送方的SYN后, 此时接收方就能够确定发送方的发送能力和接收方的接收能力是正常的, 然后接收方回应ACK和SYN, 当接收方收到ACK和SYN后, 就知道了发送方和接收方的发送能力, 接收能力都是正常的, 最后发送方回应ACK给接收方, 此时接收方也确定了接收方和发送方的接收能力, 发送能力都是正常的.
三次握手的意义:
- 让通信双方各自建立对对方的 “认同”.
- 验证通信双方各自的发送能力和接收能力是否正常
- 在握手的过程中, 双方还会 “协商” 配置一些重要的参数(完成一些数据的同步).
发起建立建立连接请求的报文就称为SYN, 也叫同步报文段, SYN是TCP首部控制控制位当中的一位, 这个标志位为1就表示是请求建立连接的报文, 其他控制位同样如此, 用0和1来进行控制.
在建立连接的过程中, 服务器与客户端是存在着不同的状态的, 不同的状态体现了TCP当前的工作, 具体如下:
CLOSED
表示客户端或服务器处于关闭状态.LISTEN
表示服务器已经准备就绪, 等待客户端连接的状态.SYN_SENT
表示客户端连接请求已发送, 此时客户端进入阻塞等待服务器确认应答状态, 一般此状态的存在时间很短.SYN_RCVD
表示服务器已经收到客户端的连接请求, 发送ACK和SYN并进入阻塞等待客户端连接状态, 一般此状态存在时间很短.ESTABLISHED
表示客户端或服务器已经建立成功连接, 随时可以进行通信, 要注意理解, 两次握手后, 从客户端来看,客户端已经把该发送的和该接收的都完成了, 此时客户端就认为进行成功建立连接; 而对于服务器, 当第三次握手后才能认为成功建立连接.
三次握手这里也能一定程度上保证TCP的可靠传输, 但只是辅助做作用, 真确保可靠传输个关键机制还是上面介绍的确认应答和超时重传.
断开连接(四次挥手)
与三次握手类似, 客户端与服务器通过四次交互断开连接的过程称为 “四次挥手”, 通信双方向对方发起一个断开连接的请求FIN, 再各自给对方一个回应ACK, 这个时候我和男神的缘分就到了尽头了, 出现了下面的场景:
断开连接的请求也被称为FIN, 在TCP报文中也是一个控制位, 断开连接的请求可以是客户端先发起, 也可以是服务器先发起, 这里以客户端主动断开连接为例继续介绍.
FIN_WAIT_1
出现在主动发起断开连接的一方, 主动方发送FIN后, 进入等待被动方确认断开响应状态.FIN_WAIT_2
出现在主动发起断开发起连接的一方, 收到被动方的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顺利到达了, 服务器与客户端就完全断开连接了.
(4)滑动窗口
上面介绍的TCP机制都是再给TCP的可靠性提供支持, 但保证了可靠性其实就牺牲了一定的效率, 滑动窗口做的事情就是在保证传输的可靠性的基础上, 尽量地去提高传输效率.
在进行IO操作的时候, 时间成本主要是两个部分, 一是等, 二是数据传输, 大多数情况下, IO花的时间成本大头都是在等上面, 滑动窗口本质上就是降低了等待确认应答ACK消耗的时间.
对于基本的确认应答机制来说, 每发送一次数据, 都需要等待ACK返回后才能进行下一次发送, 这样大部分的时间都用在等ACK上了.
滑动窗口的本质就是不进行等待发送多条数据, 然后使用一份时间来等待多个ACK返回.
把不需要等待, 就能直接发送的最大数据量, 称为 “窗口大小”, 上图中窗口的大小就是4000, 客户端发送了4条数据之后并不是等到4个ACK都都返回后才能继续发送, 而是每收到一次ACK就继续发下一条数据, 这样就让客户端这里等待ACK的数据始终始终都是4条, 就如上图, 客户端发出1-1000,1001-2000, 2001-3000, 3001-4000这四条数据后, 客户端收到1001, 紧接着就发送4001-5000这条数据, 收到2001, 就继续发送5001-6000…
这里图中本来等待ACK是1001-5000, 接下来, 收到了2001这个ACK, 就说明2001之前的数据(1001-2000)已经被确认了, 此时就可以立即发送5001-6000的数据, 此时意味着等待ACK的范围就是2001-6000, 这就相当于一个大小始终不变的窗口, 但窗口框住的数据变了, 相当于窗口向右滑动了一格, 所以这里就形象的称为 “滑动窗口”。
那么,如果在上述情况下丢包了怎么办?
-
ack丢了。
不需要做任何处理。数据可以正常传输。
比如上述图中1001丢了。但实际上1-1000的数据是服务器是收到了的, 当客户端收到2001时就表明2001之前的数据都已经确认到达服务器, 就会接着再发送两条数据, 所以只要大部分的ACK没有丢, 客户端可以通过下一次或者后面的确认应答序号来进行确认, 不处理也没事.(2001这个ack其实覆含了上一个1001这个ack的信息。) -
数据丢了。
数据丢了是需要处理的。比如上述1-3000的数据中, 其中1001-2000的数据丢了, 那服务器每收到一个数据, 都会返回1001, 表示让客户端重传1001-2000这个数据, 当客户端收到若干个个相同的确认应答序号时, 就明白了, 数据丢了, 就会对丢失的数据进行重传, 直到服务器收到1001-2000的数据, 就会返回最新的确认应答序号, 当然, 如果中间还有数据都丢包, 返回的就是新丢的包的序号了, 然后还是上述操作。
上述丢包重传的方式称为“快速重传”(重传操作只重传了丢失的数据)可以看做超时重传在滑动窗口下的变形。如果当前传输数据密集, 按照滑动窗口的方式来传输, 此时按照快速重传来处理丢包; 如果当前传输数据稀疏, 就不再按照滑动窗口方式了传输了, 此时还是按照之前的超时重传处理丢包。
(5)流量控制
滑动窗口是一种干预发送的窗口大小的机制。
滑动窗口,窗口越大,传输效率就越高(即一份时间内等待的ack越多)但是窗口不能无限大。有一下几个方面:
- 完全不等ack,数据的可靠性就无法保证了。
- 窗口太大,会消耗大量的系统资源。
- 发送速度太快,接受方处理不过来,发了也没用。
接收方的处理能力,是一个很重要的约束依据。发送方发的速度,不能超出接收方的处理能力。流量控制要做的工作,就是根据接收方的处理能力,协调发送方的发送速率。
那么如何衡量接收方的处理能力?
我们可以通过看接收方缓冲区的剩余大小。
接收方的处理能力是通过接收方缓冲区的剩余容量来衡量的, 接收方缓冲区的容量剩余多少, 下次发送方的窗口大小就是多少, 可以接收方的缓冲区想象成一个蓄水池, 那么发送方的工作就是注水, 接收方的工作就是使用水池中的水, 当水位比较低(剩余空间大)那就注水的时候就快一点, 水位比较高(剩余空间小)那就注水的时候就慢一点, 池子满了就暂时先停止注水.
当发送方的数据到达接收方的时候, 接收方都会返回一个ACK,这个ACK除了确认能够确认应答, 还能告知接收方缓冲区的剩余容量, 然后发送方就会根据接收方缓冲区的剩余容量来控制发送速度(窗口大小), 当接收方得知接收方缓冲区空间满了的时候, 就暂时不会发送数据了, 而是会定期去给接收方发送一个探测窗口报文, 这个报文不携带具体的业务数据, 只是为了触发ACK查询接收方缓冲区的剩余容量.
由于接受方缓冲区剩余空间是一直在动态变化的。所以每次返回ack带的窗口大小都在变换。发送方也是在动态调整的。
(6)拥塞控制
流量控制和拥塞控制共同决定发送方的窗口大小是多少。流量控制考虑的是接收方的处理能力。拥塞控制描述的是传输过程中中间结点的处理能力。同样的如果中间转发过程中链路的拥堵了, 那接收方的处理能力再快也是白搭的(木桶效应)。
拥塞控制本质上就是通过实验的方式来逐渐找到一个合适的窗口大小(合适的发送速率).
接收方处理能力是好量化衡量的, 但是由于设备众多, 数据每次传输路线也大概率是不相同的… 众多影响因素导致中间节点的处理能力是不好量化衡量的, 因此拥塞控制采取了 “测试实验” 的方式逐渐调整不同情况下合适的发送速度.
初始的时候接收方会以较小的窗口进行发送(0轮, 窗口大小是1, 但不是一个字节), 由于初始窗口比较小(发送速率慢), 每一轮不丢包都会使窗口大小扩大一倍(指数增长), 当增长速率达到阈值之后, 指数增长就成为了线性增长, 再当窗口达到一定的大小, 就会出现丢包的情况, 这就意味着链路就出现了 “拥堵”, 说明此时发送的速率已经接近网络的极限的; 此时就会减小窗口的大小(速度很快, 立马缩成很小的值), 因为如果出现丢包减小窗口大小的速度不够大, 可能会出现持续性的丢包, 对网络通信的质量会造成很大的影响, 然后就是重复刚才指数增长和线性增长的过程了.
拥塞窗口不是固定数值, 而是一直动态变化的, 随着时间的推移, 逐渐达到一个动态平衡的过程, 使窗口大小随着网络的动态变化而动态变化.
实际上的窗口大小是拥塞控制和流量控制共同决定的, 取的是拥塞窗口和流量控制窗口的较小值.
(7)延时应答
延时应答也是提升效率的机制。也是在滑动窗口的基础之上加以改进。
滑动窗口的关键,窗口大一点,传输速度快一点。因此,要做的是,在接收方能够处理的前提下,尽肯能把窗口大小放大一点。即延时。
收到数据之后,不是立即返回ack,而是等一会再返回。等待的时间里,接受方的应用程序,就能把接收缓冲区的数据消费一下。此时剩余的空间就大了。
实际上延时应答采用的方式,就是在滑动窗口下,ack不再每一条数据都返回ack了,而是隔一条返回一个ack。实际上剩余空间大小,变化是一个复杂的过程,既取决于发送方的发送,也取决于接收方的处理。
(8)捎带应答
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 “一发一收” 的。意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”;
那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine,thank you” 一起回给客户端。
我康康——ack是内核立即返回的。
16.00——这个是业务上的响应。
这两个本来是不同的时机,但是TCP存在延时应答机制。就导致等待ack的过程中,B就要给A发送业务数据了。就可以让这个业务数据捎上这个ack一起发过去:
(9)面向字节流(粘包问题)
首先要明确,粘包问题中的 “包” ,是指的应用层的数据包。
在TCP的协议头中,没有如同UDP一样的 “报文长度” 这样的字段,但是有一个序号这样的字段。
站在传输层的角度,TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。
站在应用层的角度,看到的只是一串连续的字节数据。
那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。
如何避免粘包问题呢?明确两个包之间的边界。
- 对于定长的包,保证每次都按固定大小读取即可;
- 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;
- 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可);
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层。就有很明确的数据边界。
站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况。
(10)异常情况
进程终止:进程终止会释放文件描述符,仍然可以发送FIN。和正常关闭没有什么区别。
机器重启:和进程终止的情况相同。
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会
进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对
方不在,也会把连接释放。
TCP重置连接的报文的是通过复位报文段来判断的, 即RST, 也是TCP报头中控制位中的一位。
再考虑发送方掉电的情况, 此时接收方会发现, 发送方很长时间没有数据发送过来了, 但从接受方的角度来看, 接受方不知道是发送方挂了还是发送方在组织数据, 所以针对这种情况, 接受方会周期性的给发送方发送一个探测报文, 触发服务器的ACK, 如果没有反应, 就说明是发送挂了.
这样的探测报文也被形象的叫做 “心跳包”, 用来确认通信双方是否处在正常的工作状态中, 因为心跳是周期性的, 如果心跳没了, 说明就挂了, 心跳包是非常常见并且经常用到的保活机制。
最后在这里对比一下UDP和TCP, TCP优势在于可靠传输, 在绝大部分场景中都需要进行的是可靠传输; 而UDP优势在于高效率, 如果有些场景对于性能要求更苛刻使用UDP就很合适, 比如同一个机房内部的服务器之间通行就可以使用UDP, 因为这种场景下的网络结构相对简单, 网络带宽也是比较充裕的, 转发设备也是比较好的设备, 整体丢包的可能性就比较小了, 这里就可以要求以更高的效率进行传输; UDP还有一个天然的优势就是支持广播, IP地址中有一种特殊的地址叫 “广播IP”, 通过UDP往广播IP上发送数据报, 此时该局域网内所有的设备都能收到数据.
TCP是个非常复杂的协议, 上面所介绍的十大特性只是TCP中比较核心的特性, 其他的就不在这里介绍了, 传输层的协议也不只有UDP与TCP这两个, 比如还有KCP, QUIC等, 在游戏场景中经常使用.