文章目录
- 应用层
- 自定义协议
- 传输层
- udp协议
- TCP协议
- 1.确认应答
- 2.超时重传
- 3.连接管理
- 建立连接, 三次握手
- 断开连接, 四次挥手
- tcp的状态
- 4.滑动窗口
- 5.流量控制
- 6.拥塞控制
- 7.延时应答
- 8.携带应答
- 9.面向字节流
- 10.异常情况
应用层
自定义协议
客户端和服务器之间往往要进行交互的是“结构化数据”,网络传输的数据其实是字符串/二进制bit流。约定协议的过程中,就是把结构化数据转成字符串/二进制bit流的过程。
- 把结构化数据,转成字符串/二进制比特流这个操作,称为序列化
- 把字符串/二进制比特流还原成结构化数据这个操作,称为反序列化
序列化/反序列化具体要组织成什么样的格式,这里包含哪些信息,约定这两件事的过程就是 自定义协议的过程。
自定义协议,要约定好两方面内容
- 服务器和客户端之间要交互哪些信息
- 数据的具体格式
客户端按照约定发送请求,服务器按照约定来解析请求。
服务器按照约定构造响应,客户端按照约定来解析请求。
传输层
udp协议
UDP的特点是无连接,不可靠传输,面向数据报,全双工的。
研究一个协议主要是研究它的报文格式。下面是大学课本中常见的报文格式。
下面是比较准确的报文格式
- udp报头中一共有四个字段,每个字段2个字节。由于一个报头中使用2个字节表示端口号,端口号的取值范围就是0~65535,即64kb。同理一个数据报长度也是64kb,不能在长了。然而在当今互联网时代,64kb是非常小的。一旦数据报长度超过64kb,后面的数据可能会出现截断。如果我们需要传输的数据超过64K,就需要在应用层手动的分包,多次发送,并在接收端手动拼装。
- 校验和起到的效果,就是去尝试检查当前传输的数据是否存在问题?是否出现了bit翻转?如果发现出现问题,就可以把这个错误的数据报丢掉,避免将错就错。
校验和可以采用下面几个方法
- CRC算法,即循环冗余算法。UDP 数据报发送方,在发送之前,先计算一遍 CRC,把算好的 CRC 值放到 UDP 数据报中,(设这个 CRC 值为value1)。接下来这个数据包通过网络传输到达接收端,接收端收到这个数据之后,也会按照同样的算法,再算一遍 CRC的值,得到的结果是value2. 比较自己计算的 value2 和收到的 value1 是否一致 如果是一致的,就说明的数据是 ok的.如果不一致,传输过程中发生了比特翻转了. 上述 CRC 算法中,如果只有一个 bit 位发生翻转,此时100%能够发现问题。如果有两个/多个bit位发生翻转,有可能恰好校验和和之前一样!!这样的情况概率比较低,可以忽略不计。
- md5算法/sha1算法。这两个算法类似,都具有下面几个特点。
1.定长。无论原始数据多长, 算出来的 md5 的最终值都是固定长度
常见的 md5 有 16 位版本(2字节),32位版本(4字节),64位版本 (8字节)。
2.分散。计算 md5 的过程中, 原始数据,只要变化一点点,算出来的 md5 值就会差异很大.网络传输中,如果出现 bit 翻转, 意味着只是极少的 bit 翻转了即使就只是翻转 1 bit, 最终得到的 md5 都会差异非常大。
3.不可逆。给你一个源字符串,计算 md5 值很简单。但给一个计算好的md5值计算源字符串是很难的。也常用于加密。
TCP协议
TCP的特点是有连接的,可靠传输,面向字节流和全双工。下面先看看tcp的报文格式。
- 四位首部长度:即报头的长度。tcp报头的前20字节都是固定不变的,而后面的选项长度是可变的。可以有也可以没有。4个比特位表示的范围是0~15,这里设置的单位是4字节,而不是字节。这里的4位表示的就是15*4为60字节。报头的长度是60字节,选项的长度是20字节。
- 保留(6位):在udp中数据报的长度是2个字节,无法在进行扩展,tcp在设置报头的时候保留6位,在扩展的时候可以使用保留位避免扩展不兼容的问题。
- 六位标志位,tcp最核心的部分。
1.确认应答
- tcp中核心的功能是可靠传输,而在网络通信中,不可能100%把数据发送出去,只能尽量将数据发送出去,发送方能够知道对方是否收到数据,就认为是可靠了。
- 为了确保可靠性,最核心的机制就是“确认应答”,发送方发送一条消息,接收方收到消息回复一条应答消息。但在网络通信中经常会出现发送方或者接收方连续发送多条消息,出现“后发先至”的情况。
- 出现“后发先至”的原因。在网络通信中,网络通信环境错综复杂,两台设备通过多个路由器交换机连接,一个数据报从发送方到接收方走的路径可能是不一样的,可能出现后发的先到达的情况。
- 为了解决“后发先至”的情况,引入了序号和确认序号,对于数据进行编号,应答报文就可以告诉发送方,我这次应答的是那个数据。tcp是面向字节流的,是以字节位单位进行传输的,tcp的序号和确认序号都是以字节位单位进行编号的。
序号:针对tcp的每一个字节进行编号 > 确认序号:应答报文中的确认序号是发送过去的最后一个序号加1 >
在确认应答中,通过应答报文反馈给发送方。**应答报文也叫做ack报文 **,平时报头中ack值为0,如果当前报文是应答报文,此时报头中ack的值为1tcp可靠性的核心机制是有确认应答,而不是“进行了三次握手”
2.超时重传
- 在网络通信中,若传输顺利,通过应答报文就可以告诉发送方,当前数据是否收到,但在网络上可能出现 “丢包” 的情况,如果数据丢了,没有到达接收方,也不会有Ack报文。
- tcp的可靠性就是在对抗丢包,发送方在发送数据之后会等待一定的时间,若等了好久(超时),ack还没有等到,此时发送方认为出现丢包,当认为丢包后,就会把刚才的数据重新发送一次(重传)
- 出现 “ 丢包” 的原因:在网络中,可能存在某个时刻,某个路由器/交换机的负载量过高,短时间内有大量数据要经过这个设备进行转发,当高负载超过这个设备的极限的时候就会出现丢包。
- 上面的过程中,认为没有收到ack就是丢包了,但可能是应答中的数据报丢了,也会重新发送。 Tcp socket在内核中存在接收缓冲区(一块内存空间)发送方发来的数据会先放到缓冲区中,然后应用程序调用read/sanner.next才能读到数据。当数据到达缓冲区中
接收方就会先判定当前缓冲区是否已有这个数据(或者是否曾经存在过),如果有或曾经存在直接把这个重复发来的数据丢弃,就能确保应用程序在调用read/scanner.next出现重复数据。接收缓冲区不仅能够进行去重,还能进行排序。接收方如何判断这个数据是不是重复数据
- 数据还在缓冲区中,还没有被read走。 此时就拿着新收到的序号和缓冲区中的序号对一下,看有没有一样的,若有,就是重复的数据,把新收的的数据丢掉。
- 数据在缓冲区中已经被read走,此时新来的数据无法再缓冲区中查到。应用程序在读取数据的时候是按照序号的先后顺序读取的,先读1-1000,1001-2000, 2001-3000一定是先读序号小的在读序号大的,比如上一次读到的是3000,新收到的数据是1001,则这个1001一定是已经被读过的树,此时可以判定这个新的数据包是“重复的包”,可以直接丢弃。
超时会重传,也不是无限重传
- 重传次数也是有上限的,重传到一定程度还没有ack,就尝试重置连接,如果重置也失效,就放弃连接。
- 重传的超时时间的阈值,会随着重传次数的增加而增大。
3.连接管理
建立连接, 三次握手
tcp是有连接的,在建立连接的过程中,应用程序只是调用socket api,而真正建立连接的过程是由操作系统的内核来完成的。操作系统完成建立连接的操作叫做 “三次握手” 。建立连接的目的是让通信双方保存对方的相关信息。下面是三次握手的具体过程。
- 第一次握手是向服务器发送一个syn同步报文段,所谓的syn是一个特殊的tcp数据报。此时的作用是告诉服务器我要和你建立连接。
- 没有载荷,不会携带应用层数据,但有ip报头,以太网数据帧头,以及tcp报头。tcp报头中就保存了客户端自己的端口,IP报头中就包含了客户端自己的IP。
- 六个标志位中的第五位,为1.
- 服务器在收到syn后,会返回ack(确认应答),接下来还会在返回syn,此时的作用是告诉客户端我收到请求并同意建立连接。
- 最后客户端向服务器返回一个确认应答。
所谓的建立连接就是通信双方各自给对方发送一个syn,各自给对方回应一个ack。
- 在上图的建立连接过程中,是有四次交互,但服务器给客户端发送到的两条数据可以合并. syn是第五个标志位为1, ack是第二个标志位为1,这样完全可以用一个tcp数据包就可以发送,既能应答上个请求,也能发起syn,网络传输设计到封装回和分用,两个合并既提高了效率,有降低了成本,最终形成三次握手.
为什么要加进行三次握手
- 可以先针对通信路径进行投石问路,初步确认通信链路是否正常(可靠性的前提条件)
- 可以验证通信双方,发送数据的能力和接受数据能力是否正常(为啥要进行三次握手?两次行不行(服务器无法确认自己的发送能力和客户端的接受能力)?四次行不行(多余了))
- 三次握手的过程中也会协商一些必要的参数
tcp中有很多参数需要协商,都是在数据包 “选项” 中体现的. 其中,TCP的 “序号” 和 “确认序号” 还是听重要的,tcp的序号 和 确认序号的初始值都不是从0 或 1开始的,而是从一个较大值开始往后计算的.即使是同一个客户端和服务器每次连接的初始值都是不一样的.
断开连接, 四次挥手
在建立连接的过程中,是通信双方保存对端的信息,断开连接的目的是将对端的信息从数据结构中删除/释放掉. 下面是四次挥手的具体过程.
四次挥手和三次握手的方式大致相同,只是发送的数据不同,但四次挥手不能像三次握手一样将中间的两步合并,三次握手中间两不触发机制完全相同,和应用程序代码无关,可以合并,但四次挥手服务器调用ack是内核来完成的,而服务器发送fin是服务器代码调用close()方法才能发送fin,在这个时间段,间隔时间可能很长,也有可能很短,不一定能将两步合并为一步.
总结三次握手和四次挥手的相同之处和不同之处
- 相同之处: 都是通信双方给对方发送一个syn/fin, 给对方回应一个ack.
- 不同之处: 三次握手可以合并,四次挥手不一定能合并.
三次握手,必须是客户端主动,四次挥手,客户端/服务器都可以主动发起.
tcp的状态
tcp服务器和客户端之间通过一定的数据结构来保存对端的信息, 在这个数据结构中有一个属性叫 “状态”, 操作系统内核根据当前状态的不同,
决定了当前应该干什么.下面是具体的tcp状态图.
上面的图只需要简单了解就可以,下面看看几个重要的状态.
三次握手中重要的状态
- LISTEN状态: 表示服务器这边已经创建好seversocket了,并且已经绑定好端口号. 此时表示可以开始建立连接了.
- ESTABLISHED状态: 表示客户端和服务器建立连接完成,即三次握手完成.
四次挥手中的重要状态
- CLOSE_WAIT状态:谁主动断开连接就进入CLOSE_WAIT状态,表示接下来代码中药调用close状态来发起fin. (收到对方发起的fin之后进入这个状态)
- TIME_WAIT转态:表示 本端发起fin之后,对端也发起fin之后,进入TIME_WAIT状态,这个状态的作用是给最后一个ack重传留有一定的时间.
TIME_WAIT的等待也不是无休止的等待,最多等待2MSL(MSL是一个系统内核的配置项,表示客户端到服务器消耗的最长时间,常见设置有2min),超过这个时间都不重传,意思是不会在重传了.双方都进入CLOSED状态.
4.滑动窗口
在确认应答,超时重传和连接管理的三个机制中, 虽然保证了可靠传输, 但等待ack的过程消耗的时间是挺多的, 滑动窗口就在保证可靠传输的前提下, 让消耗的时间成本也降低了.
上述图中把多次请求等待ack的时间, 使用同一份时间来等了, 减少了总的等待时间. 当有一个请求的ack返回以后, 窗口的大小不变, 但位置向右移动, 就出现了滑动的效果, 就叫做滑动窗口.
但上述操作也会出现丢包的情况, 具体处理情况如下.
- ack丢了
像ack丢了的情况,就无需进行处理,对于可靠性没有任何影响,也不需要进行重传.后面的ack会覆盖前面ack中的信息,如果是2001先到/或者1001丢了,说明1-1000,1001-2000都是已经到达了的,滑动窗口会直接往后移动两个格子.- 数据丢了
数据都是按顺序排序发送的,当接收方发现某个数据丢失后,会反复向发送方索要这个数据的ack, 当发送方在多次受到某个ack的确认报文后,则认为是这个数据丢了,于是重传这个数据. 反复索要的目的就是再给这个数据留有等待时间,多次索要还没等到,应该是丢了,就相当于进行了 "超时时间"判定. (没有丢包的数据已经拍好序列放在接受缓冲区中)
在上述过程中,这里的重传做到了 “针对性” 的重传,那个数据丢了就重传那个,已经收到的数据,不必重复发送,整体的效率没有额外的损失,把这种方式叫做"快速重传"
当短时间传输的数据较多是,会使用快速重传,短时间数据较少还是会按照超时重传进行发送.
5.流量控制
通过滑动窗口可以提高传输的效率, 窗口大小越大, 更多的数据复用同一块时间等待,效率就越高. 但窗口的大小也并不是越大越好,当发送数据过快,接收方处理能力低,接受缓冲区满了,就出现丢包情况,这种情况,即使重传,也还是会丢包.通过流量控制,让发送方发送数据速度和接收方处理数据的速度保持一致,避免出现丢包的情况.
在ack报头中中有一个 16位窗口大小,通过这个字段来给发送方反馈发送速度.接收方会按照自己接受缓冲区剩余空间的大小,作为ack中窗口大小的数值,发送方根据这个数值调整自己窗口的大小.
当窗口大小为0时, 说明接收缓冲区已经满了, 此时发送方就暂定发送数据.发送方会周期性的发送 " 窗口探测包", 这个包不携带载荷,只是为了触发ack, 当查询结果非0就继续发送数据.
6.拥塞控制
流量控制是站在接收方来控制发送方发送速率,拥塞控制是控制数据传输路径上发生拥堵的问题. 针对这种情况,核心思路是把中间路径经过的所有设备视为一个整体,然后通过" 实验 "的方法找到一个合适的传输速率.
如何进行实验的?
- 慢启动: 刚开始传输数据的时候,速率较小,采用较小的拥塞窗口,此时不知道网络路径的具体情况,一上来就太大,可能导致网络路径更加拥挤
- 若慢启动没有出现丢包,就增大拥塞窗口的大小,按指数增长来增大拥塞窗口.
- 当指数增长到达一定 “阈值” 的时候,指数增长变为现行增长.
- 当现行增长出现丢包的情况,就把拥塞窗口的阈值设置成较小的值,回到最初的慢启动,并且此时会重新设置指数增长的 " 阈值".
上述过程可以用图展示
现在更新之后有新的版本,一个是当发生丢包后重新回到新订的阈值不在进行慢启动,直接进入指数增长的模式,这样更加提高了传输的速率.
7.延时应答
延时应答也是基于滑动窗口在提高一些效率,结合滑动窗口和流量控制,能够延时应答ack的方式,把反馈的窗口大小弄大一些.接收方在接受数据放到接受缓冲区之后,不会立刻返回ack,而是等一段时间, 让应用程序将这个数据使用掉, 这样, 接受缓冲区的空间就变大, 返回的窗口大小就是一个更大的值了.
在滑动窗口中,ack丢了, 不会影响可靠信, 正常情况每个数据都有ack,
这样可以每隔几个数据在返回一个ack(每隔几个数据也能起到延时应答的效果),另外也能减少ack传输的数量,起到节省空间的效果.
8.携带应答
携带应答是尽可能把能合并的数据包进行合并,从而起到提高效率的效果.
在延时应答的机制下, ack会延迟发送, 就可能在返回响应的时候和响应的数据包一起返回, 本身ack也不携带载荷, 只是在报头中ack标志位设置为1, 并且设置确认序号以及窗口大小. 这几个属性,在reponse报文中也用不到, 不会发生冲突.
在携带应答的加持下, 后续每次传输请求响应,都有可能把传输的业务数据和上次的ack合二为一.
9.面向字节流
粘包问题 , 此处的包是"TCP载荷中的应用层数据包"
TCP是面向字节流的,站在应用层的角度,每次收到的数据是一串连续的字节数据,不知道从哪一部分到那一部分是一个完整的应用层数据包,多个数据包混淆不清,像这种情况就是" 粘包问题".
如何解决粘包问题?
核心方法就是 “明确包之间的边界”.
- 通过特殊符号, 作为分隔符, 见到分隔符就视为一个包结束了.
- 指定出包的长度,比如在包开始的位置, 加上一个特殊的空间来表示整个数据的长度.
UDP存在粘包问题吗?
UDP传输的基本单位是udp数据包, 在udp这一层就已经分开了,只需约定好, 每个udp数据包值承载一个人应用层数据包, 不需要额外的手段来区分,就不存在粘包问题.
10.异常情况
考虑的情况是考虑比丢包更严重的情况, 如网络直接出现故障等情况,该如何处理?
- 接收方或发送方有一方出现 进程崩溃.
进程无论是正常结束还是异常崩溃,系统都会自动触发回收资源文件, 关闭文件这样的效果,就会触发四次挥手. tcp的连接是有生命周期的,可以比进程更长一些,虽然进程退出,但tcp连接还在,任然可以通过四次挥手断开连接- 接收方或发送方出现关机(正常关机)
关机操作会终止系统所有的进程,和上面一样会触发四次挥手的操作,若挥手速度够快,就能删除本端和对端的信息, 但可能存在四次挥手还没有结束系统就已经关闭了,此时fin已经发送,无法收到ack,此时会进入超时重传的流程中,若还是没有收到ack, 就会单方面释放连接信息.- 其中一方断电(更突然的关机)
- 断电的是接收方: 发送方突然出现没有ack了,就会进入超时重传,重传之后还是不行, 就会进入 "复位连接 ".(相当于清除原来tcp中的临时数据,重新开始). 此时会用到tcp中的复位报文段(RST),
当rst也收不到ack,就当方面释放连接.- 断电的是发送方: 当接收方一段时间没有收到对方的信息,就会触发心跳包,来判断发送方是挂了,还是当前没有发送数据,如果发现对端没有心跳了,本端也会尝试复位连接并且当方面释放连接.
心跳包是不携带应用层数据的特殊数据包,是周期性的,没有心跳视为对端挂了.
- 网线断开.
网线断开,数据就无法发送出去,此时发送方收不到ack就会超时重传并且触发复位连接,从而单方面释放连接,接收方收不到数据,就会触发心跳包判断对方是不是挂了,若没有心跳就会当方面释放连接.