TCP/IP五层模型栈
应用层
在应用层中,最重要的就是"设计并实现一个应用层协议"
举个栗子:
如果公司在开发一个项目,关于点外卖的软件,其中有个功能是这样的:
前端和后端是通过网络来进行交互的,在这个交互的构成中就需要约定好,前端发什么样的数据,后端回应什么样的
形如这样的工作,就是在设计一个应用层协议,上图的这部分工作就是在规划请求和响应之间要传递的信息.
但是由于应用层协议是可以随心所欲的来指定的,所以就会导致两极分化的情况非常严重 (大佬设计的非常好,菜鸟设计的协议就非常糟糕)对于次,大佬发明了一些比较好的协议的模板,可以让我们直接往上套.
xml
xml是属于一种比较老牌的数据格式了,虽然仍在使用,但是使用更多越来越少了
xml的格式是由标价构成的
<标签名> 内容 </标签名>
开始标签 要表示的值 结束便签
举个栗子:
发送请求的协议模板如下:
<request>
<userId>1234</userId>
<startTime>2022-04-01</startTime>
<finishTime>2022-04-12</finishTime>
<count>10</count>
</request>
可以将这看做k-v模型
通过这些便签,就更好的体现了这个数据的可读性,尤其是那部分是什么意思,一目了然.
xml现在很少会作为应用层协议的设计模板,现在使用xml作为一些配置文件
json模板
json是当下最流行的一种设计应用层协议的数据格式
格式:
{
键:值,
键:值,
.......
}
举个栗子:
{
userId:1234,
startTime:'2022-04-01',
finishTime:'2022-04-12',
count:10
}
json中表示字符串使用单引号或者双引号都是可以的(类似于SQL)最后一对个键值对,后面可以加逗号,也可以不加逗号(标准是没有的,但是一般的json解析器不会在意这个细节)
json要求key一定是字符串,因此key这里的引号可以省略,除非key包含了一些特殊符号,(比如空格或者-…)必须要加上引号
虽然json的传输速率比xml要高,但是仍然要多传递一些冗余信息,这一点在表示数组的时候,尤为明显
举个栗子:
用json表示响应:
{
ok:true,
reason:"",
data:[
{
name:'蛋炒饭',
price:12,
count:1,
totalPrice:12
},
{
name:'炒面',
price:14,
count:2,
totalPrice:24
}
]
}
当表示复杂的数据,比如数组的时候,此时的很多key就会重复出现N次,也就占用了更多的额外带宽
至此,protobuffer诞生
protobuffer
protobuffer 是一个二进制格式的数据,在protobuffer的数据中,不包含上述的key的名字了,而是通过顺序和一些特殊符号来区分每个字段的含义,同时传一个IDL文件来描述这个数据格式(每部分的意思),IDL只是起到一个辅助开发的效果,并不会真正的进行传输,传输的仅仅是二进制的纯粹的数据
简化版本如下:
蛋炒饭\212\21\212\3炒面\214\22\224
通过二进制的数据重新对这里的内容进行编排,甚至可能还会进行一些数据压缩
这样虽然传输效率会更高,但是也会让这个数据肉眼难以观察,调用起来不方便
综上所述:json的应用范围要比protobuffer更广
总结:
应用层协议要做的工作
- 明确传输的数据(根据需求)
- 明确传输的格式(参考模板json,xml,protobuffer
传输层
传输层是操作系统内核实现,程序猿不需要直接和传输层打交道,但是传输层对我们来说仍然意义重大!进行网络编程都要用到socket,一旦调用了socket代码就进入到传输层的范畴
端口号
端口号用于区分一台主机中接收到的数据报应该转交给哪一个进程进行处理。
端口号取值于 0 - 65525之间的整数
知名端口号:把0 - 1024 这些端口号,给划分出了一些具体的作用
比如:
传输层中的协议有很多,最常见的就是UDP和TCP
UDP
UDP的报文格式:
代码中写的端口号,就会被打包成这样的UDP数据报中(在报头中体现)
UDP客户端为例
此时的源端口就是操作系统自动分配的端口号
目的端口就是服务器端口
报文长度
数据报长度表示整个数据报(UDP首部+UDP数据)的最大长度
此处的报文长度是2个字节,范围是0 - 65525 ,0-64k
这也就让UDP使用中存在一个非常致命的缺陷,无法表达一个比较大的数据报,
校验和
用来验证网络传输的这个数据是否是正确的.
网络中传递的数据的本质是电信号和光信号,但是如果有一些外界干扰磁场之类的,就可能导致原有的一些传递出去的数据发生了改变,校验和就可以帮助我们发现数据中的错误
TCP
-
源/目的端口号:表示数据是从哪个进程来,到哪个进程去;
-
32位序号/32位确认号:后面详细讲;
-
4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15 * 4 = 60
-
6位标志位:
- URG:紧急指针是否有效
- ACK:确认号是否有效
- PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
- RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
- SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
- FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
-
16位窗口大小:后面再说
-
16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不
-
光包含TCP首部,也包含TCP数据部分。
-
16位紧急指针:标识哪部分数据是紧急数据;
-
40字节头部选项:暂时忽略;
TCP中的一些核心机制
上面的四个机制中,前三个可以在代码中体现出来,但是"可靠传输"虽然不能在代码中体现,但是这个确实TCP中最核心的机制,引入TCP的关键原因就是为了保证可靠传输.
因此TCP中的很多机制都是围绕着可靠传输来展开的.
一、 确认应答
可靠性:发送方发出去的数据之后,能够知道对方有没有收到
关键在于接收方收到消息之后,给发送方返回一个应答报文(ACK,acknowledge),表示自己已经收到了
举个栗子:
同学A有一道数学题不会做了,所以他发短信给同学B,想让同学B帮忙解答一下
如果同学B没有回复,那么我就不知道自己的消息有没有发出去,可能没有发出去就丢包了,只有B回复同学A的答案我才知道同学B看到了同学A的消息.
当下的确认应答有一些小问题,如果同学A给同学B发了多道题呢?
上图的情况应该是正常情况,但是网络中的环境很复杂的,可能会发生后发先至的情况
答案4明明时候发的但是却先到达,此时A就会收到错误顺序的答案.
后发先至的情况在网络中是很常见的情况,忘喽环境十分复杂,连续发的两个包,不一定走的是同一条路.
为了解决上述的后发先至的问题,很简单的办法就是对消息进行编号.
确认序号: 表示当前这个应答报文针对哪个消息进行的确认应答
序号和确认序号在TCP报文头中也有体现
TCP针对消息的序号,并不是按照"消息的条数"进行编号的,而是按照字节来进行编号的
此时第一个字节编号为序号1,第二个字节编号为序号2,以此类推
确认序号也不是上面举例所讲的那样
二、超时重传
相当于对确认应答进行了补充.确认应答是网络一切正常的时候,通过ACK通知发送方我收到了,如果出现了丢包的情况,超时重传的机制就起到效果了.
此时发送方发出信息,等待ACK。情况一对方没有收到消息,发出消息在传输到一半丢失了,接收方不可能发送ACK,发送方就必不可能收到ACK.情况二 接受方收到了信息,但是发送ACK的时候,却丢失了ACK,发送方也收不到ACK.
超时重传,重传的数据一定能成功吗?肯定不是100%
如果只是因为网络抖动了一下,这个时候重传还是很容易成功的,如果是网络遭受了严重的伤害,肯恶搞就没那么容易恢复,重传也成功不了。
重传如果失败,可能还会再尝试,但不会无休止的重传,连续几次的重传不成功,就认为这个网络可能遇到了严重的情况,此时再怎么重传都是不行的,就只能放弃(自动断开TCP的连接)
重传的时间间隔也不是固定的,一般来说是逐渐变大的(重传的频率会逐渐降低)
一次传输失败的概率本身是很低的,连续两次传输都失败,概率更是小上加小,这个时候TCP就不太指望可以重传成功了。
以上两个机制,TCP的可靠性得到了有效的保障。
三、连续管理(重点)
1)如何建立连接 (三次握手)
客户端和服务其之间,通过三次交互,完成建立连接到过程。“握手是形象的比喻”
客户端是主动发起连接请求的一端,客户端先发送一个SYN同步报文段,给服务器
建立连接就如同谈恋爱一样,是一个双向奔赴的过程,客户端想和服务器建立连接,就发送了SYN,服务其接收后,回应客户端就发送了ACK,同时服务器也想和客户端建立连接,就也发送了SYN,客户端收到后,就返回ACK,然后客户端和服务其就建立了连接。
虽然此时是三次交互,但是中间的两次可以合二为一。每次的传输的数据都要经过封装和分用,才能完成传输,封装两次不如封装一次更高效。这就好比淘宝店铺买两件衣服,商家肯定会打一个包裹给我。
此处的CLOSED、SYN——SENT其实就是TCP的状态。
2)如何断开连接(四次挥手)
三次握手,就让客户端和服务器之间建立好了连接.
建立好连接之后,操作系统内核中,就需要是用一定的数据结构来保存连接相关的信息,保存的信息其实最重要的就是五元组(源ip,源端口,目的ip,目的端口,tcp),保存的信息需要占用系统资源(内存)
如果有一天,连接断开了,那么之前保存的连接信息就没意义了,对应的空间也就释放了
如果两个操作之间的时间差比较大,是不可以合并的,但是如果两个操作之间的时间差比较小,是可以合并的(延时应答和捎带应答)
状态转换的详细情况
重要的两个状态:
CLOSE_WAIT:四次挥手挥了两次之后出现的状态,这个状态就是在等待代码调用socket.close()
方法来进行后来的挥手操作
TIME_WAIT:谁主动发起FIN,谁就进入TIME_WAIT,起到的效果就是给最后一次ACK提供机会.
如果,A最后发出ACK之后,可能会出现丢包的情况,这时候,B就会从新发送FIN给A;如果A在规定时间内没收到B的FIN,就直接销毁.
四、滑动窗口
使用滑动窗口就可以提高效率
当前是在等待1001,2002,3001,4001四组ACK,不需要等待4001才继续往下发,只要1001到了,就继续往下多发一组(4001-5000),此时等待ACK的范围就是2001,3001,4001,5001,如果2001到了.就继续往下多发一组(5001-6000),此时ACK的范围就变为3001,4001,5001,6001.
窗口大小越大,认为传输速度就越快,窗口大了,同一时间等待的ACK就更多了
在滑动窗口中,出现丢包情况
ACK丢了
此时虽然ACK丢失了,但是数据却实实在在的传输到了主机B,在发送4001-5000之前,收到了2001的ACK,表示的含义是2001之前的数据全部确认收到了,此时1001ACK有没有被收到已经无足轻重了,因为2001之前的所有数据全部被收到了
所以,如果ACK丢了,不需要做其他操作
数据丢了
五、流量控制
滑动窗口的延申,为了保证滑动窗口的可靠性
流量控制的关键就是能够衡量接受方的处理速度,此时这个可以使用接收方接收缓冲区的大小,来衡量当前的处理能力
这个过程也可以看做是一个生产者,消费者模型
A就是生产者 , B就是消费者 B的接受缓冲区就是交易场所
接收缓冲区有一个总大小,随着A发送数据,B的接受缓冲区剩余空间就越来越小
如果剩余空间比较大,就会认为B的处理能力比较强,就可以让A多发些
如果剩余空间比较小,就会认为B的处理能力比较弱,就可以让A少发些
通过ACK就可以拿到接受缓冲区的大小,可以看到TCP报头中,有16位窗口大小,里面保存的就是接收缓冲区的剩余空间大小,发送方就可以通过这个16位窗口大小来衡量发送速度.
虽然这里窗口大小只有16位,但其实并不止,在TCP首部40字节的可选项中包含了窗口扩大因子M,实际的窗口大小是窗口字段的值左移M位
六、拥塞控制
拥塞控制也是滑动窗口的延申,也是限制滑动窗口的发送的速率
拥塞控制衡量的是,发送方到接收方,这整个链路之间的拥塞情况(处理能力)
七、延时应答
流量控制的延申,流量控制想法于踩了刹车,使发送方发的不要太快
延时应答,就是再这个基础上,尽量让窗口一更大一些
举个栗子:
一个水池,一边进水,一边出水。
每一次进水,都会查询当前水池里剩余空间有多少。此时采取的策略就是不立即回答,而是稍微晚一会回答,迟一点回答,就会意味着延迟时间里会出更多的水。此时水池中的剩余空间就更大了。
如果立即回答,可能回答:水池剩余空间20吨水;如果延时一会回答:水池剩余空间30吨。(延时时间里,多出了10吨水)
这个操作就是再有限的情况下,又尽可能的提高了一点传输速度。
八、捎带应答
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 “一发一收” 的。意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”;那么这个时候ACK就可以搭顺风车,和服务器回应的 “Fine,thank you” 一起回给客户端
因为延时应答的存在,导致ACK并不会立刻返回的,如果当时的延时应答,导致ACK的返回时机和应用层中返回的响应时机重合了,就可以把这个ACK和响应数据合二为一
九、面向字节流–粘包问题
TCP粘包指的是 粘 的是应用层数据,再TCP接收缓冲区,若干个应用层数据包混在一起了,分不清是谁的了
这些数据报到达B之后,就会进行分用,,分用意味着把TCP数据进行解析了,取出的应用层数据放在接收缓冲区,以备应用程序来取,如果没有额外的限制,其实就很难区分了,归根到底就是没有明确包之间的边界
解决方案:关键就是要在应用层协议这里,加上包之间的边界
例如:约定每个包以 ;结尾
有了边界,就可以很明确的区分每个包了
十、TCP的异常处理
- 进程终止
在进程毫无防备的情况下,突然结束进程,这个时候该进程的TCP连接是通过socket来建立的,socket本质上是进程打开一个文件,文件其实就是存在于进程的PCB里面有个文件描述符表,每次打开一个文件(包括socket),都在文件描述符表里,增加一项,每次关闭一个文件,都在文件描述符表里,进行删除一项.
瑞国直接杀死进程,PCB也就没了,里面的文件描述符表也就没了,此时的文件相当于自动关闭了,这个过程其实和调用socket.close()
一样,都会触发4次挥手
- 机器关机
按照操作系统约定的正常流程关机,会杀死所有进程,然后在关机
- 机器掉电/网络断开
TCP vs UDP对比
- 啥时候使用TCP?
对可靠性有一定要求(日常开发中的大多数情况,都是基于TCP)
- 啥时候用UDP?
对可靠性要求不高,对于效率要求更高
典型面试题:
基于UDP如何实现可靠传输??(考TCP)
本质就是在应用层基于UDP复刻TCP的机制