在网络原理中主要学习TCP/IP四层模型中的重点网络协议
1. 应用层
1.1 应用程序与协议
应用层是和程序员接触最密切的;
应用程序:在应用层这里,很多时候都是程序员自定义应用层协议(步骤:1、根据需求,明确要传输的信息,2、约定好信息按照什么样的格式来组织)的;所谓的网络协议其实就是约定,即程序员在代码中规划好,数据如何进行传输;
下面是自定义协议的例子:
1、点外卖
点开外卖软件,首先会看到商家列表, 这里就涉及到程序和服务器之间进行的网络通信交互
2、请求: 用户信息, 位置信息,
此处假设就使用简单的格式来组织,使用文本的方式.三个属性,使用""来分割
1000,100,30()--->代码中构造出一个这样的字符串,给写入到 Tcp socket 或者 Udp 的 socket 中
3、响应: 商家列表(多个商家),每个商家,包含 名称,图片,距离,简介,评分.
此处假设使用简单的格式来组织,使用文本的方式,每个商家信息占一行,每个属性使用","来分割
过桥米线,图片地址1,1.1km,评分5
张亮麻辣烫,图片地址2,2.3km,评分4.8
魏家凉皮,图片地址3,3km,评分4.9
上述这个过程,就是自定义协议.针对这里的情况,使用啥样的格式来组织,只要客户端和服务器双方能够对应上都是可以的;
1.2 开发中构造协议的格式
1.2.1 xml
(通过标签来组织数据)
早期的组织数据的格式,现在几乎很少用于网络通信,下图是代码展示:
xml 的优势:
让数据的可读性变的更好;
xml 的劣势:
标签写起来非常繁琐,传输的时候也占用更多网络带宽,(maven, 就会使用 xml 来管理项目配置)
1.2.2 json
(当下最流行的一种数据组织格式)
json格式使用键值对,通过{ }把所有的键值对给包裹起来,键值对之间,使用","来分割,键和值之间,使用 ": " 来分割,其中键固定就是 string 类型,值的话,可以是数字,可以是字符串,也可以是 json,还可以是数组;由于 json 的 key 固定就是字符串类型,很多时候也是可以把 key 的引号给省略的
下图是代码展示:
json 的优势:
可读性比较好的; 比 xml 更简洁.
json 的劣势:
同样也是会在网络传输中,消耗额外的带宽 (需要把 key 也进行传输的)
虽然如此,json 在网络通信中仍然非常流行,除非是些对于性能要求非常高的场景,不使用json 之外,其余的很多地方都可以使用 json
1.2.3 protobuffer
相比于json 和 xml 来说,protobufer (简称为 pb)使用二进制的方式来组织数据,可以保证带宽占用最低(相当于把要传递的信息按照二进制形式压缩了)
pb 的优势:.
占用带宽最低,传输效率最高.非常适合于对于性能要求比较高的场景,
pb的劣势
可读性不好(二进制结构,肉眼无法直接阅读),一定程度的影响开发效率,
应用层中也有一些"现成"的应用层协议,其中最知名的,最广泛使用的,就是 HTTP 协议了(超文本传输协议)~~>"超文本"不仅仅是文本,还有图片,视频,音频,字体..
2. 传输层
传输层中使用到的协议主要有TCP和UDP协议:
端口号:写一个服务器,必须手动指定一个端口号,通过端口来区分当前这个主机上的不同的应用程用,客户端在通信的时候也会有一个端口号(代码中感受不到),系统自动分配的一个客户端,
端口号,固定就是占2 个字节.表示的数据范围0->65535,一般来说,0是不用的.其中1-1023 称为"知名端口号,1024-65535 普通的端口号,平时自己写代码,用的端口还是普通的端口号为好一点;
2.1 UDP协议
2.1.1 udp报文格式
学习一个协议,其中最主要的工作,就是去理解协议报文格式。
udp协议的报文格式总体如下图所示:
udp协议的报文格式的详细分析图如下图所示:
源 ip, 目的 ip 不在传输层,而是在网络层 (IP 协议里);像UDP,TCP这样的报文格式,是最初一些大佬们设计并约定出来的内容,最后被整理成了一份标准文档,称为RFC标准文档;
2.1.2 报文案例分析
用户使用浏览器进行搜索原理如下图所示:
于业务的发展,搜集信息越来越多了,样式也越来越复杂了.导致商搜服务器和入口服务器之间交互的数据量,已经接近 64kb (udp报文长度就是64kb)了.如果超过上限,数据就会出现截断,最终展示在页面上的数据肯定就会出错
方案一:把数据拆分成多个包,使用多个 UDP 数据报进行传输.(开发成本,比较大)
方案二:直接使用 TCP.TCP 没有数据包的大小限制.
2.1.3 关于校验和
校验和是计算机中非常广泛使用的概念.由于在网络传输中,一些设备被外部干扰,就可能会出现数据传输出错的情况.在数据传输的过程中低电平被干扰之后扰下就成了高电平,因此, 就需要校验和这一方法来识别出出错的数据.
校验和,其实本质上也是一个字符串,体积比原始的数据更小,又是通过原始的数据生成的.
如果接收方收到的原始数据相同,得到的校验和就一定相同,;同理,校验和相同,原始数据大概率相同 (理论上会存在不同的情况, 实际的概率非常低,可以忽略不计)
如何基于校验和来完成数据校验呢
1、发送方,把要发送的数据整理好(称为 data1),通过一定的算法,计算出校验和 checksum1;2、发送方把data1和checksun1一同发送出去;
3、接收方收到数据,收到的数据称为 data2(数据可能和 data1 就不一样了),收到数据 checksum1;
4、接收方再根据 data2 重新计算校验和 (按照相同的算法),得到 checksum2
5、对比 checksum1 和 checksum2 是否相同.如果不同,则认为 data2 和 data1 一定不相同
如果 checksum1 和 checksum2 相同,则认为 data1 和 data2 大概率是相同的(理论上存在不同的可能性,概率比较低,工程上忽略不计)
关于校验和的计算方法:计算校验和,有很多种算法,
1、此处 UDP 中使用的是 CRC 算法(循环冗余算法)
把当前要计算校验和的数据,每个字节,都进行累加,把结果保存到这 两个字节的变量中,累加过程中如果溢出,也没关系;如果中间某个数据,出现传输错误,第二次计算的校验和就会和第一次不同
CRC 这个算法其实不是特别的靠谱,导致两个不同的数据,得到相同的 crc 校验和的概率比较大(前一个字节恰好少1,后一一个字节恰好多1.)
2、md5/sha1 算法 (就只介绍 md5)
这里有一系列的公式,来完成 md5 的计算,(咱们不需要考虑公式是啥样的,是一个
数学问题),但是咱们需要知道 md5 的特点:
1.定长,无论你原始数据多长,计算得到的 md5,都是固定长度->校验和本身就不应该很长,要不然不方便网络传输
2.分散,给定两个原始数据,哪怕绝大部分内容都一样,只要其中一个字节不同,得到的 md5 值都会差异很大(md5 也非常适合作为 hash 算法)
3.不可逆。给定一个原始数据, 计算 md5,非常容易。但是给定 一个md5,还原出原始数据, 计算量非常庞入,以至于超出了现有计算机的算力极限,理论上是不可行的.
2.1.4 关于udp的特点分析
通过之前所写的基于udp实现的通信代码来分析相关的特点:
1、无连接.UDP 协议本身不会存储对端的信息,要在发送数据的时候,显式指定要传输给谁
2、不可靠,代码中体现不出来
3、面向数据报
4、全双工 通过一个 socket,既可以 send, 又可以 receive
2.1.5 基于UDP的应用层协议
NFS:网络文件系统
TFTP:简单文件传输协议
DHCP:动态主机配置协议
BOOTP:启动协议(用于无盘设备启动)
DNS:域名解析协议
2.2 TCP协议
TCP协议最大的特点就是可靠性传输;
2.2.1 TCP报文格式
TCP报文格式如下图所示:
首先:数据报 = 首部(报头 header) + 载荷
四位首部长度:
tcp报头长度是不固定的,报头最短,是20字节(没有选项);报头最长是60字节(一般来说选项最长的是40字节),4bit=>(0,0XF)此处的单位是4字节
保留(6位):
UDP 有个问题,udp报文长度 64kb是固定的;所谓的保留位, 就是当前现在不用,但是先占个位置,后面如果有需要,再使用.(留下了扩展的余地~~)
16位检验和:
和 udp 一样的校验和一样;
2.2.2 关于可靠传输
可靠传输,是 TCP 最最核心的特性 (初心),并不是说,发送方把数据能够 100% 的传输给接收方,而是退而求其次:
1、发送方发出去数据之后,能够知道接收方是否收到数据.
2、一旦发现对方没收到,就可以通过一系列的手段来"补救.
下面是一些确保信息可靠传输的补救手段:
1、确认应答
发送方,把数据发给接收方之后,接收方收到数据就会给发送方返回一个应答报文(acknowledge, ack).发送方如果收到这个应答报文了,就知道自己的数据是否发送成功了;
但是每天请求能及时的收到正确的应答这个情况太理想了,实际上网络传输数据可能会出现(后发先至--->一个数据包在进行传输的过程中走的路径可能是非常复杂的.即不同的数据包,可能走不同的路线.)这样的情况';
TCP在此处要完成两个工作:
1、确保应答报文和发出去的数据, 能对上号,不要出现歧义
2、确保在出现后发先至的现象时,能够让应用程序这边仍然按照正确的顺序来理解数据
所谓的序号就是一个整数.大小关系,就描述了数据的先后顺序,准确的说,序号不是按照"一条两条"方式来进行编号的,而是按照字节来编号的,因为tcp是面向字节流的;
对于序列号并不是我们自己设定的,序列号的情况如下图所示:
通过特殊的 ack 数据包,里面携带的"确认序号"告诉发送方,哪些数据已经被确认收到了
此时发送方,就心中有数了,就知道了自己刚发的数据是到了还是没到.=>TCP 的初心,是为了实现可靠传输 =>达成可靠传输的最核心的机制, 就是确认应答q1:如何区分一个数据包是普通的数据,还是 ack 应答数据呢?
q2:为啥确认序号是现在这样的规则?即用收到的最后一个字节的序号+1来表示呢
主要是为了关联后面的知识->滑动窗口,
2、超时重传
确认应答,描述的是一个比较理想的情况。但是如果网络传输过程中,出现丢包,则发送方,势必就无法收到 ACK 了,所以使用超时重传机制,针对确认应答,进行补充.
为什么会丢包?
把网络想象成公路,即错综复杂的公路网,在公路上就会有很多的收费站。平时车流量很通畅,但是当节假日的时候,收费站这里经常会堵车好几个小时;
在网络中,"收费站"可以理解成是一些路由器/交换机,如果数据包太多了,就会在这些路由器/交换机上出现"堵车",但是路由器针对"堵车"的处理,不会把这些积压堵塞的数据包都保存好,而是会把其中的大部分数据包直接给丟弃掉.此时这个数据包就在网络上消失了,
真实的网络环境,是错综复杂的,我们不会知道哪个节点上会出现上述"堵车"情况,也不知道啥时候会出现丢包, 丟包是非常随机的事件.这个事情出现概率的大小,取决于网络基础设施施,同时也取决于网络环境.
综上所述,丢包是一个"随机"的事件,因此在上述 tcp 传输过程中,丢包就存在两种情况.
情况一:传输的数据丢了
情况二:返回的 ack 丢了
对于发送方的来说,无法区分这两种情况.无论出现上述哪种情况,发送方都会进行"重新传输"第一次是丢了,重传一下很大概率就能传过去;重传就是一个很好的丢包下的补救措施了
q3:发送方,何时进行重传?
引入等待时间,发送方发出去数据之后, 会等待一段时间, 如果这个时间之内, ack 来了,此时就自然视为数据到达;如果达到这个时间之后,数据还没到,就会出发重传机制
q4:关于等待时间
1、初始的等待时间,是可配置的.不同的系统上都不一定一样,也可以通过修改一些内核参数来引起这里的时间变化.
2、等待的时间,也会动态变化.每多经历一次超时,等待时间都全变长(变长,隐含的含义,就是对能够正确传输数据这件事,前面按照丢包概率 10% 计算,每次重传一次都会让传输成功的概率大幅度增加.)
eg:A->B发了一条数据,
第一次,A 等待 ACK 的时间,假设是 50ms,此时如果达到 50ms, 还没有 ack,A 就重传;
当 A 重传的数据,还是没有收到 ack,第二次等待的时间就会比第一次更长,拉长也不是无限拉长,重传若干此时,时间拉长到一定程度,认为数据再怎么重传也没用了, 就放弃 tcp 连接(准确的说是会触发 tcp 的重置连接操作)
q5:关于接受端收到两条一样的数据的bug处理
其实 TCP 已经非常贴心的帮我们把这个问题解决了;TCP 会有一个"接收缓冲区",就是一个内存空间,会保存当前已经收到的数据,以及数据的序号;
接收方如果发现,当前发送方发来的数据,是已经在接收缓冲区中存在的 (收到过的重复
数据了),接收方就会直接把这个后来的数据给丢弃掉,确保应用程序进行 read 的时候,
读到的是只有一条数据,
所谓的接受缓冲区,不仅仅是能进行去重,还能进行重新排序,确保发送的顺序,和应用程序读取的顺序是一致的;
3、连接管理
即建立连接 + 断开连接
建立连接:
握手,只是为了唤起对方的注意,tcp 这里的握手,也是类似给对方传输一个简短的,没有业务数据的数据包,并通过这个数据包,来唤起对方的注意,从而触发后续的操作.
TCP 的三次握手.TCP 在建立连接的过程中,需要通信双方一共"打三次招呼"才能够完成连接建立的,简单分析图如下所示:
A 想和 B 建立连接,A 就会主动发起握手操作;实际开发中,主动发起的一
方, 就是所谓的"客户端,被动接受的一方,就是"服务器"(同步报文段,就也是一个特殊的 TCP 数据包,没有载荷的 (不携带业务数据的))
q6:三次握手是要解决什么问题?通过四次握手,是否可行?通过两次握手,是否可行呢?
可以, 但是没必要.两个数据合并成一个数据效率更高.
q7:三次握手核心作用:
1、确认当前网络是否是畅通的,
2、要让发送方和接收方都能确认自己的发送能力和接收能力均正常
3、让通信双方,在握于过程中,针对一些重要的参数进行协商
握手这里要协商的信息,其实是有好几个的,至少大家要知道,tcp 通信过程中的序号从几开始,就是双方协商出来的(一般不是从1 开始的),每次连接建立的时候,都会协商出一个比较大的,和上次不太一样的值.
这种设定方式,是避免"前朝的剑,斩本朝的官"----->
有的时候,网络如果不太好,客户端和服务器之间可能会连接断开, 再重新建立连接;重连的时候, 就可能在新的连接好了之后,旧连接的数据姗姗来迟,这种迟到的数据,应该要丢弃掉的! 不应该让这个之前的的数据影响到本次的业务逻辑~
如何区分数据是否是来自于上个朝代?就可以通过上述序号的设定规则来实现->如果发现收到的数据序号和当前正常数据的序号差异非常大就可以判定为是上个连接的数据就可以直接丢弃了;
断开连接,四次挥手:
建立连接,一般都是客户端主动发起;断开连接,客户端和服务器都可以主动发起
四次挥手的简单分析如下所示:
q8:和 三次握手 不同,此处的四次挥手,能否把中间的两次交互合二为一?不一定!!
不能合并的原因是因为ACK 和 第二个 FIN 的触发时机是不同的.ACK 是内核响应的.B 收到 FIN,就会立即返回 ACK;第二个 FIN 是应用程序的代码触发.B 这边调用了 close 方法才会触发 FIN ->
从服务器收到 FIN(同时返回 ACK),再到执行到 close发起 FIN,这中间要经历多少时间,经历多少代码,是不确定的!!FIN 就会在 socket 对象 cose 的时候, 被发起.可能是手动调用 close 方法也可能是进程结束.故此综上所述:
象前面的三次握手, ACK 和 第二个 syn 都是内核触发的.因为是同一个时机,可以合并;但是这里的四次挥手, ACK 是内核触发的,第二个 FIN 是应用程序执行 close 触发的,二者的的触发时机不相同,所以不能合并.
是否意味着,如果我这边代码 close 没写/没执行到,是不是第二个 FIN 就一直发不出去??(有可能的),并非所有的连接都是好聚好散;但是,TCP 中还有一个机制->延时应答,(后面再说),能够拖延 ACK 的回应时间一旦 ACK 滞后了,就有机会和下一个 FIN 合并在一起了
q8:TIME_WAIT 状态主要存在的意义?
如果最后一个 ACK 丢了,站在 B 的角度,B 就会触发超时重传重新把刚才的 FIN 给传一遍,如果刚才 A 没有 TIME WAIT 状态, 就意味着 A 这个时候就已经真的释放连接了,此时重传的 FIN 也就没人能处理, 没人能返回 ACK 了.B 永远也收不到 ACK 了
A这边使用 TIME_WAIT 状态进行等待,等待的这个时间,就是为了处理后续 B 重传的 FIN此时有重传的 FIN 来了, 就可以继续返回 ACK 了.B 这边的重传 FIN 才有意义.
q9:TIME WAIT 等待多久呢?
ps:本篇的内容就到这里了,如果大家感兴趣的话就请一键三连哦!!!