目录
应用层
传输层
udp 协议
端口号
报文长度(udp 长度)
校验和
TCP 协议
确认应答
超时重传
链接管理
滑动窗口
流量控制
拥塞控制
延时应答
捎带应答
总结
我们第一章让我们对网络有了一个初步认识,第二章和第三章我们通过代码感受了网络通信程序。
而本章的 通信原理 进一步了解网络是如何实现工作的,本章主要以理论为主,本章的理论非常多,面试常考,工作中也会常用,同时也非常抽象。
我们之前提到过的:由于复杂的网络环境催生出了复杂的网络协议,我们将这些复杂的协议拆分成 多种小协议;再将这些小协议进行分类可以分成不同的层级。
我们这一章将重点介绍应用层和传输层,其他层了解即可。
应用层
我们这里简单介绍,后面介绍 http 协议的时候我们会重点介绍。
应用层直接和代码相关;直接决定了数据需要传输什么,接收方需要拿到那些数据,拿到后如何使用。
应用层这一层中现存着一些现有的协议,比如HTTP协议,也可以是自己写的协议;
我们最开始就聊到过微信发送消息的那个栗子,
这就是简单的看一看,实际上远比这个复杂的多,具体的如何实现呢?
需要根据需求去写:
比如我们的微信小程序:美团外卖
需要传输哪些信息(根据需求)
具体需求按照啥格式来组织(随意约定)
具体的我们留在http 协议来细说。
传输层
再传输层中我们已经认识了两个协议 udp 和 tcp 协议。
我们先挑个软柿子捏,捏完软的我们再捏硬的。
udp 协议
我们先来看看udp 报文结构:
去网上copy 几张下来:
这个是大部分计算机网络教材上的图,事实上这个图其实并不准确,我们来画一张实际的图:
- 伪头部 : 只是为了提取 IP 数据报中的源IP,目的IP信息并加上协议等字段构造的数据。在实际传输中并不会发送,仅起到校验和计算使用,因此称之为伪首部。
- 源端口号 : 一般是客户端程序请求时,由系统自动指定,端口号范围是 0 ~ 65535,0~ 1023为知名端口号。
- 目的端口 : 一般是服务器的端口,一般是由编写程序的程序员自己指定,这样客户端才能根据ip地址和 port 成功访问服务器
- UDP 长度 : 是指整个UDP数据报的长度 , 包括 报头 + 载荷,
- UDP校验和 : 用于检查数据在传输中是否出错,是否出现bit反转的问题,当进行校验时,需要在UDP数据报之前增加临时的 伪首部。
我们看到上述的报头都是两个字节:都是 0 ~ 65535;
端口号
源端口、源IP 表示数据来自哪里
目的端口、目的IP 表示数据要去哪里;
就像唐僧说的那样:贫僧自东土大唐而来,到西天拜佛取经。
我们的端口号一般小于 1024 的都别那些有名的大公司软件用了;像我们耳熟能详的MySQL 它也才是 3306 ;如果非要用其实也没事。
报文长度(udp 长度)
2个字节长度,也是 0 ~ 65535 也就是 64k 的大小,说大不大,说小也不算小。
为什么但是设计的时候,不设计大一些呢,这就是时代的局限了,换成当年,64k 已经非常大了,当时的电脑内存那才多少 M 啊;但是对于现在我们来说,64k 太小了,随便一个小插件都不知64k。
因为电子元件的发展,64k 对我们来说太小了,随便发一些数据都可能超过这个数字。
所以我在使用 udp 编程的时候,需要注意了,需要控制大小,不然超出这个大小就会出现一些不必要的问题。
校验和
在我们的网络传输过程中,并非那么的稳定,随时都会出现问题,例如:受强磁产的作用,辐射,或者其他物理环境影响,都会造成网络传输的不稳定。
我们都知道,路由器、交换机之间传输的是高、低电平,转换为 01 的二进制,那么在这些特殊情况就可能造成: 0 - 1 变为 : 1 - 0 ;我们把这种现象称之为 比特翻转。
所以,我们在此基础上提出了:校验和,就是用来判断一下目前传输的数据时候发送了错误;但是这个校验和不是 100% 正确的。
例如:
我们要去买菜:番茄、土豆、黄瓜、青菜; 一共是四种菜,我们买回来四种不一定正确,但是买回来不是 四种一定不正确。
- 校验和正确,数据不一定是正确的,
- 但是检验和如果不正确,数据一定是不正确的
校验和更多的用处不是“证实”,而是为了证伪,判断数据是不是错的
ok;说到这我们算是吧 udp 这个软柿子捏完了,接下来到了难啃的硬骨头:TCP / IP。
TCP 协议
我们之前讲到过:
TCP 协议的特点:
- 有链接
- 可靠传输
- 全双工
- 面向字节流
我们本章的重点在于 "可靠传输"
可靠不是说百分之百可靠,我们的可靠是尽可能传输过去,哪怕没有传输过去,起码需要让对方知道自己没有传输过去。
这个核心机制就来了:确认应答、超时重传 这个是确保可靠的核心!!!
确认应答
这个机制是实现可靠传输的核心;
举个栗子🌰:
我现在是个舔🐕 ,我向我女神发出一个邀请:能不能让我陪她一起去游乐园玩玩。
那么她会回答一个 好 或者是 不好 。这就是答复,让我知道她到底去不去。
接收方也一样,接收方会返回一个 ACK报文(acknowledge)【应答报文】;
但是在过去 发短信常常会出现一种状况: 后发先至。
什么叫后发先至,举个栗子🌰:
我向女神发出了两个邀请:
邀请1:周六能不能让我陪她去游乐园玩玩?
邀请2: 能不能做我女朋友?
她回答:
可以;
滚。
我接收到的 不一定是对方按顺序发的;我收到的可能就是:滚 ; 可以。
这种情况就叫做后发先至。
在过去,这种情况也算是很普遍的,现在这类情况减少了,我们给 头tcp 数据报添加了一个序号和确认序号,女神对应的回答就是: 回答1:可以,回答2:滚!
那么此时我也是按顺序接收到女神发送的消息。
真实的TCP 协议中也是有 序号和确认序号。
TCP 针对每个字节都进行了编号 。
我们来可靠 TCP 的报文结构:
我们先不用去了解其他的结构是什么意思,我们一步步来理解 。
注意:
我们接受方返回的序列号与发送方无关,不是说你发送过来啥就返回啥;
而是返回发送过来的所有数据的下一个序号。
可以将这个返回的序号理解为:前n 个字节我已经收到了,现在需要你从 n + 1 个字节开始发送。
为啥网络传输中会出现这种后发先至的情况呢?
在网络传输中不是两个机器直接发送数据的,而是中间通过了多台交换机和路由器,具体在这传输过程中是如何传输的,这就不得而知了。
但是没关系,对我们的 tcp 来说,它本身自带了一个 " 整队 " 的任务,tcp 有个内存缓冲区(本质是内核上的一块地址),tcp 就可以按照序号针对收到的数据进行 " 整队 " ;
这样做只有一个目的:让应用程式读到的和发送方有一样的顺序。
如果上述过程可以完整进行,那么网络传输的可靠性确实可以保证,但是在网络传输过程中还有一个问题非常常见,那就是 " 丢包 "
为啥会丢包呢?
我们在发送数据的整个过程中,会经历多个交换机和路由器,这些交换机和路由器不只是接收、发送这两个设备的数据,还会有其他机器传输需要经过它。
那么当某个设备的流量达到峰值,就可能出现丢包现象。
这种情况下,丢谁的包,不知道,什么时候丢包,不知道!
这时,接收方没收到包,就不会返回一个 ack ,那么这时就触发了 tcp 协议的的第二个机制:超时重传
超时重传
发送方迟迟拿不到ack ,那么它也就反应过来,可没可能是发生丢包了呢?
不管是不是,发送方过一段时间就再发送一个,丢包只是个概率性事件,如果多次重传,我们发生方还是没有拿到ack ,那么大概率就是严重的网络事故;此时我们发送方也不傻,每次重传的延迟发送时间增加,增加到一定界限时,那么我们就会不发送,断开网络链接。可靠性指的是尽可能的传输过去。
我们丢包的情况有两种,第一种发送的时候丢包,第二种返回ack 的时候丢包:
第一种情况,我们超时重传就没事了;
如果是第二种,那么就会出现问题,加入我们去用微信付款,把钱付过去以后,返回时出现丢包,我们还需要付第二次款,那么此时:一份商品的价格我们付款了两次,就会出现大问题。
主机B 出现大量重复数据,那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。
这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。
TCP 帮我们处理好了这个问题,在接收缓存区中,我们根据收到的数据的序号,自动去重。 保证程序读到的数据只有唯一一份。
我们保证可靠性的最主要的机制就是: 确认应答 和 超时重传 !!!!
千万不要说是:三次握手和四次挥手!!!!
这是双方建立建立连接的机制,而不是保证可靠性的机制。
链接管理
建立链接:三次握手
断开链接:四次挥手
我们来讲讲什么是三次握手,什么是四次挥手
三次握手:
握手(handshake)指的是通信双方,进行一次网络交互;三次握手就是通信双方进行三次交互,从而建立链接。这里的链接就是双方各自记录对方的信息。
syn 表示同步报文段,意思就是客户端要向服务器申请建立链接。
那么服务需要给个回信(应答报文):ack ,
这就完成了客户端对服务器的链接,但是服务器还需要对客户端进行链接;
服务器向客户端发送一个 syn ;然后客户端要给服务器一个回信 ack。
我们一看,不对啊,这不是四次交互吗?为什么要叫三次握手啊?
我们其实可以把 服务器发送给客户端的 ack 和 syn 合并到一起,如:
我们可以拆分成四次,为什么需要将其何必为三次呢?
我们在前面说过,数据传从发送端发送,要经过层层封装,接收端接收到数据要经过层层分用,我们两次发送和接收较于一次而言 效率更加低,耗时较长。
故此,最终成了三次握手了。
啥样的报文叫做syn 报文呢?
我们回到之前讲到的 TCP 报文结构:
这六个特殊的比特位默认是 0 ,如果变为了 1 有其特殊的含义;
比如我们的ack 为1,就表示当前TCP 报文是一个应答报文。
如果 syn 为1,就表示当前 TCP 报文是一个 同步报文。
那么我们三次握手的中间一次: 即使一个ack 又是一个 syn,就可以把这两位 同时设为 1;
那么我们三次握手起到了什么作用呢?
三次握手的本质就是投石问路,验证了客户端和服务器之间各自的发送能力和接收能力是否正常。
四次挥手
三次握手的作用是建立链接,四次挥手的作用是断开链接。
通讯双方各自给对方发送一个fin(结束报文),再各自返回ack;
如图:
建立链接一定是由客户端主动的,断开连接双发都可以主动,服务器有时也是会崩溃的。
我们这里四次挥手很像三次握手啊,这里中间的 ack 和 fin 是否可以像三次握手一样合并为一次呢?
三次握手中 fin 和 ack 都是由内核态来完成的(同一时刻);
而四次挥手是不同时机触发的:
服务器第一次收到了 fin 立马就返回了一个 ack ,而要向客户端发送fin 则要看代码执行(执行close 方法时才触发发送fin)。
如果是客户端断开连接了,服务器立马也断开连接(执行close 方法),那么乘着 服务器还没有发送 ack ,此时可以合并。
具体的得看代码怎么写。
来看看三次握手和四次挥手的中间传输的过程:
这种图网上一搜一大把,
我们如果面试要问的时候就画中间的部分:
其他的画对了不加分,画错了就扣分,吃力不讨好
我们 tcp 到这里就结束了吗?只能说这种想法太天真了,我们才刚刚开始。
我们目前认识了三个:确认应答,超时重传 ,这两个特性保证了 tcp 传输的可靠性;链接管理就是投石问路,也和可靠性有点关系,但关系不大。
我们接下来要在保证可靠性的前提下,尽可能地提高效率!!
滑动窗口
按照我们先前提到的方式去传输数据,每次都只传输几个字节,这样效率就非常低了。
所以我们的 tcp 还需要在保证可靠的情况下,还需要尽可能的保证效率。
那么如何提高效率呢?
如上图,我们的主机A 一直在等待主机B 传输回来的 ack 。
想要提高效率可以从缩减等待时间下手:批量发送数据(一次性发送多条数据,一次等待多个ack)。
上图这个过程就称之为滑动窗口。我们这里用一次等待时间接收了四个 ack 。
收到这个 ack 就发送下一组数据,这样等待时间减少了,整体的效率就提高了。
相比之下,其实 udp 的效率还要快,但是 udp 是牺牲性能的条件下提高的效率。
下图是我copy的一张动图演示:
ok,我们说过,滑动窗口是要保证可靠性的,那么之前发生的情况,滑动窗口也会发生:丢包问题;
这种情况下啥事没有,即使丢了这么多ack 都没有关系。
我们 1001 序列号丢了,但是还有 2001 这个序列号收到了,我们这里的 2001 可以涵盖前面的 1001 序列号,主机 A 就会明白主机 B 已经收到了,但是 1001 这个ack 包丢了。
但如果是6001 这个包丢了,就触发超时重传。
上面的是返回的 ack 丢了,这里是发送的数据没有被主机 B 收到,那么主机 B 就会反复索要 这个数据。
主机 A 连续几次收到这个索要,就知道这个 数据 估计是丢了。于是就触发了超时重传。
最后就返回一个 7001 ack ,为什么不是 2001 呢,因为之前的 ack 都被 主机 A 收到了。
如果中间缺了两块,那么重传完 1001 之后就会再重传中间缺失的那个。
上述的重传过程是没有冗余的,缺失了就重传,没有缺失就不重传,整个过程速度是非常快的,所以也被称之为 : 快速重传。
我们目前说的是传输大量数据的情况,如果是少量数据,就那么几条不会使用滑动窗口和快速重传。
流量控制
滑动窗口是加快了效率,但是速度是越快越好吗?
有疑问那么就说明有些问题,我们的速度并非是越快越好的,我们别忘了这一切的前提是保证可靠! 这里的流量控制也是保证可靠性的一种。
我们的接收方是有个缓冲区的,如果我们滑动窗口发太快了,一下子就把这个缓冲区打满了,余下的发送过来这个缓冲区也接收不到,那么就出现丢包了;所以我们不如发送的慢一点。
流量控制的本质就是控制速度;如何控制这是个重点!
我们可以看到这里是有一个 16 位的窗口大小的,我们可以让 ack 报文返回的时候携带一个窗口大小,这个值得意义就是用来建议 主机 A 下次该发送多大得窗口,这里说的是建议,并不绝对。
那么我们主机 B 又是如何来确认这个 窗口大小的呢?
简单粗暴,直接拿缓冲区中剩余的空间大小作为窗口大小。
例如:
当我们的缓冲区满了以后,我们的数据还没有发送完,过了一段时间以后,我们就发送几个数据,进行试探,看看缓冲区是否有空闲的位置,
这就有点像个阻塞队列。
图中还出现一个叫做 拥塞控制的东西..
拥塞控制
滑动窗口大小 = 流量控制 + 拥塞控制
流量控制:制衡了接收方的处理能力
拥塞控制:制衡了传输路径的处理能力
我们都知道两个远程的机器进行数据传输要经过很多交换机和路由器,·很明显这个路径上任何一台设备遇到瓶颈都会影响到这整个传输过程。
这就是个木桶效应,一个木桶能装多少水取决于最短的木板。
而这里的拥塞控制存在的意义就是衡量中间节点的传输能力。
具体任务就是:衡量传输过程有多少太结点,每个结点当前的情况,甚至是每次传输走的路径都不同,通过实验找到一条合适的路线。
怎么试验?
我们来看下图:
我们来一个个解释:
最开始的时候我们会给一个非常小的速度,也叫做慢开始,当发现路径空旷,就开始指数形式增长,当发现达到一个阈值就开始成线性增长,一直增长到发现:出现丢包情况;
此时,又从慢开始出发,然后指数形式增长,直到达到阈值,此时的阈值为上一次丢包的一半,其他不做出改变,从此往复。
这个过程是动态变化的,目的就是:为了防止过多的数据注入到网络中,这样就可以使网络中的路由器或链路不致过载。
延时应答
我们 tcp 的可靠核心是: 确认应答 和 超时重传。
而确认应答的 ack 不是要立刻返回,而是要等一会再返回,这是为什么呢?
我们知道 tcp 决定传输效率的关键在于滑动窗口,而窗口大小又是由 拥塞控制 和 流量控制的, 流量控制又是来接收缓冲区的大小。
关键就在这里:缓冲区的大小,只要缓冲区里还有数据,那么我们主机 B 就会不断地消费缓冲区中的数据。
假设我们我们立刻返回的 ack 是 n ,那么稍微等一会,等缓冲区消费一些数据,返回的ack 大概率会比 n 大。
延迟应答的作用就再此:通过这个延迟,让接收方乘机多消费一些数据,以达到返回的窗口会大一丢丢,以此来增加发送方发送的速率。
捎带应答
捎带应答是基于延时应答,它是作用于服务器 与 客户端 之间的;
我们服务器与客户端之间存在 四种通信模型:
一问一答:绝大部分网络通信
多问一答:上传大文件
一文多答:下载大文件
多问多答:游戏串流
而捎带应答通常是作用于 一问一答 的模型。
例如:
我们客户端发送一个请求给客户端,我们 服务器会返回一个 ack,我们的服务器通过 write 这个方法写的数据,通过一些代码执行到才返回一个响应;这里的 ack 和 响应 本来是不同时机返回的,但是我们通过捎带应答这个机制,让 ack 等会再发送,那么 ack 和 响应 就可能会同时发送。
我们的四次挥手也有可能这样成为" 三次挥手 " 就结束。
TCP 远不止这些机制,还有其他很多核心的机制,这里只是介绍了比较核心的机制,可以参考 TCP RFC 标准文档。
总结
我们介绍了 udp 协议,和 tcp 协议
tcp 中我们介绍了:
- 确认应答,超时重传(确保可靠性的核心机制)
- 链接管理(网络链接的核心机制,三次握手和四次挥手面试常考)
- 滑动窗口(优化手段,增加效率)
- 流量控制,拥塞控制(组成滑动窗口的机制,也是优化手段,其中流量控制也是保证可靠性的一种)
- 延时应答,捎带应答( 提升效率的机制 )
tcp 和 udp 的区别:
tcp 是可靠传输,效率并没有那么高
udp 是不可靠传输,效率很高
为什么我们tcp 协议既要保证可靠又要追求效率呢?
我们现实生活中的确存在很多需要用到 tcp 的场景,例如各类对抗性激烈的网游:王者农药,lol,csgo 等等, 我们既要保证可靠的情况,又要 尽可能追求效率。
我们的传输层之间不仅仅只有这两个协议,还存在很多其他协议,也可以自己写协议,但这两个协议是用的最多,最权威的协议。