应用层协议包括请求报文和响应报文,一般应用层协议都是由开发这个软件的程序猿来约定的,设计一个应用层协议,主要包含两个工作
xml是用的比较久的数据格式,现在虽然也在用,但是用的越来越少,xml的格式非常有特点,由标签构成
格式:<标签名>内容标签名>
<标签名>,是开始标签
内容就是要表示的值
标签名>,是结束标签
优点很明显,通过这些标签,就更好的体现了数据的可读性,尤其是哪个部分是啥意思,一目了然。但是缺点也很明显,虽然xml
提高了可读性,但是又引入了太多的辅助信息,就导致传输相同数据的请求时,占用的网络带宽更高,而如果带宽固定,相同时间能传输的请求个数就是更少,值得一提的是,带宽很贵
综上所述,正是因为上面的优点和缺点,xml
现在很少作为应用层协议的设计模板,现在使用xml
主要是作为一些配置文件
当下最流行的设计应用层协议的数据格式就是json
,json
的格式比也很有特点,是键值对的格式
格式:
{
键:值,
键:值,
...
键:值
}
通过{}
构成了键值对结构,一个{}
中有很多个键值对,键值对之间用 逗号 分割,键和值之间用 冒号 分割,要求键必须是字符串类型,值允许很多种(数字,字符串,布尔,数组,甚至是另一个json…),json
中表示字符串,单双引号都可以,而又由于json
要求key
必须是字符串,因此key
这里的引号可以省略,除非key
中包含了一些特殊符号(空格,-…),则必须加上引号
可以看出,json
的传输效率比xml
高,但仍然要多传递一些冗余信息,就是key
的名字,这一点在表示数组时尤为明显,一个数组中的key将会重复很多份
因此protobuffer
应运而生,比json
更加精简,是一种二进制格式的数据,在protobuffer
的数据中,不再包含key
的名字,而是通过顺序以及一些特殊字符,来区分每个字段的含义,同时再通过一个 IDL 文件,来描述这个数据格式(说明每个部分是啥意思), IDL 只是起到一个辅助开发的效果,并不会真正的传输,传输的只是二进制的纯粹的数据
比如数据格式就是这种:
中国\北京\朝阳区\身份证号
通过二进制的数据重新对这里的内容进行编排甚至压缩,这样做之后传输效率更高,但是这个数据难以观察,调试起来不方便
开发效率包含开发和调试,如果线上环境出问题了,如果用json,出问题的请求和响应一目了然;如果是用protobuffer,二进制的数据我们无法直接通过肉眼观察,就得自己开发一个专门的程序来解析这个数据,分析问题,而开发效率 > 运行效率
设计应用层协议,是一件非常普遍的事情,设计应用层协议,要做的工作:
除此之外,业界也有一些现成的,已经被设计好,被广泛使用了的应用层协议,我们稍加修改,就可以使用,最知名的就是HTTP协议
HTTP (全称为 “超文本传输协议”) 是一种应用非常广泛的应用层协议
HTTP协议的重要性
HTTP/1和HTTP/2在传输层是基于TCP的,而HTTP/3是基于UDP的,而现在主流的还是HTTP1.1版本的,由于HTTP是一个文本格式的协议,我们可以不需要理解具体的二进制位,而只需要理解文本的格式,我们可以通过抓包工具来看它的报文结构
抓包工具,其实就是一个第三方程序,而fiddler就是一个专门抓HTTP的抓包工具,我们可以直接在官网下载。在这个网络通信的过程中,类似于“代理”,如下图所示:
Fiddler 使用方式:
打开 Fiddler,然后打开你要访问的网站,访问该网站的 HTTP 请求和响应就会显示在 Fiddler 上
当我们选择好我们具体要查看的 HTTP/HTTPS 请求和响应时,双击左边栏具体的报文,右上栏就会显示具体的请求报文内容,右下角就会显示具体的响应报文内容(需要点击 Raw 标签来查看详细的数据格式)
标签页的选项,就表示当前使用啥样的格式来显示HTTP请求,我们用的最多的就是row选项(原始的),这样我们看到的就是HTTP请求数据的本体
如果觉得直接在fiddler看的累,我们可以通过右下角的 view in Notepad 键通过记事本打开请求或响应,通过记事本来看
响应的正文往往是被显示在浏览器上,最常见的响应格式就是 html,很多网站返回的 html 是压缩过的(因为压缩之后,网络传输的数据量就变少了,即节省了网络带宽),此时在我们看来就是一段乱码,所以需要进行解压缩 decode
为了方便我们抓取我们自己想查看的抓包结果,可以通过 ctrl + a 全选左侧的抓包结果,delect 删除选中的所有抓包结果,再进行网页的刷新就行
注意: 直接安装的 Fiddler 是无法查看 HTTPS 的请求的,需要进行下面的配置才能查看HTTPS的包:
HTTP请求和响应的报文格式并不相同
和具体报文对照着看
请求报文
请求分为四个部分:
请求行:
a)HTTP 的方法,描述请求想干啥。GET 就是想从服务器获取到某个东西。
b)URL,描述了要访问得到网络上的资源具体在哪儿。
c)版本号,HTTP/1.1 表示当前使用的 HTTP 版本号是 1.1。1.1 是当下最主流的版本。
请求头,包含了很多行:
a)每一行都是一个键值对,键和值之间使用:空格来分割
b)键值对的个数是不确定的,不同的键值对,表示的含义是不一样的
空行:相当于请求的结束标志
请求正文(body):可选的,有的有,有的没有。空行后面的就是正文
首行:包含了 3 个部分:
a)版本号:HTTP/1.1
b)200:状态码,描述了这个响应,是一个表示 ”成功“ 还是 ”失败“ ,以及其他不同的状态码,描述了失败的原因。
c)200:状态码描述,通过 一个/一组 简单的单词,来描述当前的状态码的含义
响应头:也是键值对结构,每个键值对占一行,每个键值对之间使用 : 和 空格 来分割。响应头当中的键值对个数,也是不确定的,不同的键值对表示不同的含义
空行:表示响应头的结束
响应正文:就是服务器返回给客户端的具体数据。内容可能有各种格式,最常见的就是 HTML
URL的含义就是“网络上唯一资源的地址符”,既要明确主机是谁,又要明确主机上的哪个资源,而URL都要遵守一个基本模板:
通过这些IP地址,端口,带层次的文件路径,就描述了一个网络上具体的资源
小结:对于URL来说,虽然里面的结构很复杂,但是最重要的,与开发最紧密的,主要就是四个部分:(1)ip地址/域名,(2)端口号(经常省略),(3)带层次结构的路径,(4)query String 查询字符串
URL 的 encode/decode
当 query string 中如果包含了特殊字符,就需要对特殊字符进行转义。转义的过程就叫做 URLencode,反之,把转义后的内容还原回来,就叫做URLdecode。
因为一个 URL 里面是有很多特殊的含义的符号的:/ : ? & = +…这些符号都是在 URL中具有特定含义的。万一,queryString 里面也包含这类特殊符号,就可能导致 URL 被解析失败!所以要进行 encode
方法 | 说明 | 适用版本号 |
---|---|---|
GET | 获取资源 | HTTP 1.0、HTTP 1.1 |
POST | 传输实体主体 | HTTP 1.0、HTTP 1.1 |
PUT | 传输文件 | HTTP 1.0、HTTP 1.1 |
HEAD | 获得报文首部 | HTTP 1.0、HTTP 1.1 |
DELETE | 删除文件 | HTTP 1.0、HTTP 1.1 |
OPTIONS | 访问支持的方法 | HTTP 1.1 |
TRACE | 追踪路径 | HTTP 1.1 |
CONNECT | 要求用隧道协议连接代理 | HTTP 1.1 |
LINK | 建立和资源之间的联系 | HTTP 1.1 |
UNLINE | 断开连接关系 | HTTP 1.1 |
上述方法只罗列到1.1,后续的2,3版本并不考虑,而其中最最常用的就是GET
和POST
方法
浏览器发送GET请求的情况
浏览器发送POST请求的情况
GET 和 POST 的区别:
GET 和 POST 没有本质区别。具体来说,相当于是 GET 使用的场景,也能换成 POST。POST 使用的场景,也能换成 GET。但是细节上还是有区别的:
- GET 通常用来取数据。POST 通常用来上传数据。
- 通常情况下,GET 没有 body,GET 是通过 query string 向服务器传递数据的。通常情况下,POST 是有 body 的,通过 body 向服务器传递数据。
- GET 请求一般是幂等(每次相同的输入,得到的输出结果是确定的)的,POST 请求一般是不幂等(每次相同的输入,得到的结果是不确定的)的。
- GET 可以被缓存,POST 不能被缓存(承接第三点,幂等才可以缓存,如果不幂等,就没有缓存的必要,毕竟结果不确定,需要实时计算)。
header 的整体格式是键值对结构,每个键值对占一行,键和值之间使用 冒号+空格
进行分割
表示服务器主机的地址和端口
表示 body 的数据长度,长度单位是字节
关于 ConTent-length,HTTP 也是基于 TCP 协议的,TCP 是面向字节流,有粘包问题,所以这里也有,就可以通过下面这两种方法来解决:
表示 body 的数据格式,介绍三种请求中的数据格式
application/x-www-form-urlencoded
这是 form 表单提交的数据格式,此时 body 的格式就类似于 query string(是键值对的结构,键值对之间使用 & 分割,键与值之间使用 = 分割
multipart/form-data
这是 form 表单提交的数据格式(需要在 from 标签上加上 enctyped=“multipart/form-data”),通常用于 HTML 提交图片或者文件
application/json
此时 body 数据为 json 格式,json 格式就是源自 js 的对象的格式。用一个 { } 括住,里面有多个键值对,键值对之间使用逗号分割,键和值之间使用冒号分割
这里表示的是,当前用户是拿一个什么样的东西来上网,比如:
现在 User-Agent 的作用,更多的是用来区别电脑和手机,毕竟屏幕尺寸不同
表示当前的页面,是从哪个页面跳过来的
表示当前页面是从搜狗转过来的。Referer 的一个作用就是,可以看到广告的点击跳转从哪里来的,广告主就可以通过Referer来区分是哪个广告平台导入过来的
Cookie 是什么?
Cookie 是浏览器给页面提供的一种纽扣持久化存储数据的机制,把一些信息写在特定的文件里面。最典型的应用场景就是存储当前用户的身份信息,就像我们登录 CSDN 之后,刷新页面,或者重新打开 CSDN 网站,仍然是已登录状态
为什么需要 Cookie?
如果没有 Cookie,直接将要存储的数据保存在客户端浏览器所在的主机的硬盘上,那么就会出现很大的安全风险,比如当你不小心打开某个不安全的网站,该网站就可以在你的硬盘上写一个病毒程序,那么你的电脑就挂了!因此浏览器为了保证安全性,就禁止网页中的代码访问主机的硬盘(无法在 JS 中读写文件),因此也就失去了持久化存储的能力,故 Cookie 就很重要!
具体的组织形式:
Cookie 来自哪里,如何往 Cookie 中存储数据?
Cookie 这个数据可能是客户端(网页)自行通过 JS 写入的,也可能来自于服务器在 HTTP 响应的 header 中通过 Set-Cookie 字段给浏览器返回数据。
Cookie 在浏览器这边是按照域名维度来存储的,例如我们打开 CSDN 的首页,点击网址栏左边的一把小锁就能找到 Cookie,我们就可以看到打开这个网页时,系统按照不同域名设置了 Cookie
Session
就是会话,就像微信消息列表,就相当于是会话列表。聊天记录,就相当于是用户的详细信息,是储存在服务器上的,Cookie传过来一个会话id,服务器通过会话id找到用户对应的会话,进一步获取到用户的详细情况
状态码表示访问一个页面的结果(如访问成功、失败,还是其它一些情况等等),它是一个3位的整数,从 1xx、2xx(成功)、3xx(重定向)、4xx(客户端错误)、5xx(服务器错误),分为五个大类,每个大类的含义都不同。以下介绍一些常见的状态码及它的状态码解释
有的页面通常需要用户有一定的权限才能访问,如未登录
一般是服务器的代码执行过程中遇到了一些特殊的情况,造成服务器崩溃可能会产生这个状态码
重定向相当于手机呼号的呼叫转移功能,如果我们换了一个手机号,就可以去办理该呼叫转移业务,使朋友拨打你的旧号码时,自动跳转到新号码
I am a teapot
,我是一个茶壶,来自程序猿的幽默 响应报头的基本格式和请求报头的格式基本一致,下面介绍下响应报头的 Content-Type 参数
Content-Type
Content-Type 表示 body 的数据格式,以下介绍三种响应中的数据格式
text/html
表示数据格式是 HTML
text/css
表示数据格式是 CSS
application/javascript
表示数据格式是 JavaScript
application/json
表示数据格式是 JSON
form 是 HTML 中的一个表单标签,可以用于给服务器发送 GET 或者 POST 请求。
form 的重要参数:
input 的重要参数在 form 标签中的含义:
当我们用 form 表单构造好了 HTTP 请求,点击 submit 提交按钮,就可以将请求发送出去
发送get请求
<form action="https://www.sogou.com" method="get">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="提交">
form>
发送post请求
<form action="https://www.sogou.com" method="post">
<input type="text" name="username">
<input type="password" name="password">
<input type="submit" value="提交">
form>
ajax(Asynchronous Javascript And XML) 是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。传统的网页(不使用 ajax)如果需要更新内容,必需重载整个网页面。
这里的同步和异步与多线程的同步异步不同。这里的同步是调用者发送请求之后,一直等待被调用者发送的响应,异步调用者发送请求之后,就不管了,直到被调用者来通知才处理响应
这里不使用 JavaScript 中原生的 ajax,而是用第三方库中 jQuery 里面提供的对 ajax 封装好的一个更简便的版本
注意:
如何安装使用 jQuery 第三方库?
在 Javascript 中安装第三方库,只要在代码中引入对应库的 CDN 链接即可
CDN 是啥?
CDN 的全称是 Content Delivery Network。即内容分发网络。CDN 是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率
jQuery 中的重要参数:
测试代码:
<script src="jQuery.js"></script>
<script>
$.ajax({
// 请求方法
type:'get',
// 请求url层次文件路径
url:'https://baidu.com',
success: function(body) {
// success对应一个回调函数
// 这个函数会在正确获取到 HTTP 响应之后,来调用
// "异步"
// 回调函数的参数,就是 HTTP 响应的body部分
console.log('获取到响应数据!' + body);
},
error: function(body) {
// error也对应一个回调函数
// 这个函数会在请求失败后触发
// "异步"
console.log('获取响应失败!');
}
});
</script>
结果:
浏览器打开之后,却是空白页面,打开控制台发现回调函数执行为error:
原因就是因为跨域访问,浏览器是禁止 Ajax 进行跨域访问的。就是禁止跨越多个域名/服务器
HTTPS 相当于 HTTP 的孪生兄弟。就是在 HTTP 的基础上,引入了一个加密层。就是对数据加密,防止运营商劫持。就像在很多时候,我们在网页上下载软件,下载之后却发现下载下的软件和我们想要的软件不一样。
主要就是读密码进行加密,但是加密之后,也可以破解,但是破解的成本很高。HTTPS 当中引入的加密层,称为 SSL/TLS,主要的加密方式有两种:
对称加密
就像图片这样:
客户端和服务器持有同一个密钥,客户端传输的数据(HTTP 请求的 header 和 body)都通过这个密钥进行加密。然后再网络上传输的就是密文了。服务器在收到密文之后,接下来就可以根据密钥进行解密,拿到明文了。
但是!一个服务器有很多客户端,如果都用同一个密钥的话,那黑客也接入客户端,就把其他客户端都破解了。所以一个客户端对应一个密钥。所以就要么是客户端生成密钥,要么就是服务器生成密钥,然后通过网络传送。我们假设是客户端生成密钥:
客户端生成密钥,然后再通过网络告诉服务器,但是密钥如果用明文传输,也会让黑客拿到,加密也就没有意义了。所以使用对称加密,最大的问题就是:密钥无法安全传输到服务器,明文传输是不行的,所以必须继续对密钥加密,就成了套娃操作了。所以还得使用 :非对称加密。
非对称加密
非对称加密,有两个密钥:公钥和私钥。公钥:人人都知道。私钥:只有自己才知道。使用公钥加密,私钥解密。或者私钥加密,公钥解密。
基于非对称加密,服务器就可以自己生成一对公钥和私钥,公钥人人都能拿到。客户端使用公钥,对数据加密,然后把数据传给服务器,服务器再用私钥解密,但是非对称加密资源消耗很高,因此更常用的应该是用非对称密钥传递对称密钥,再用对称密钥进行消息传递,如下图。
那么接下来问题又来了:
客户端获取公钥时遇到的问题。客户端刚开始会问服务器要公钥,服务器返回公钥和私钥的时候,黑客拦截到服务器的公钥,然后黑客自己也生成一副公钥,私钥。把自己生成的公钥发给客户端。客户端加密之后,把密文发出去的时候,黑客再进行解密拦截解密。黑客为了隐藏自己,再用自己拿到的服务器的公钥加密,之后发给服务器,此时客户端,服务器之间使用的对称密钥黑客就截获到了,还是有问题。如下图所示:
解决中间人攻击:引入证书
关键就是让客户端确认,当前的公钥是来自于服务器还是黑客。验证的时候,就通过第三方公信机构来验证,关键在于证书不容易伪造,证书都是有着校验机制,并且客户端也能向公信机构求证。就像是住酒店的时候,刷身份证,这也是通过公安局来验证的
虽然传输层,是操作系统内核实现的,程序猿不需要直接和传输层打交道,但是传输层对我们来说仍然意义重大,在编写代码时,如果一切顺利,就还好,一旦代码出现一些bug,为了解决这些bug,传输层的一些知识就是必要的了
UDP报头结构
问题:
上述的介绍中,一个UDP数据报能表示的长度很有限,因此如果要传一个比较大的数据,就要进行分包,然后再通过多个UDP数据报分别发送,接收方再把收到的数据报重新拼接成完整的数据,但是这种做法太麻烦了,拆包组包的代码很麻烦,还要考虑很多种情况(比如包丢了,包的顺序)
TCP报头结构
- 源端口和目的端口与UDP一致
- 序列号:表示当前是哪一条消息(具体理解可参照确认应答机制)
- 确认号:表示当前是针对哪条消息进行应答
- 4位首部长度:表示TCP报头的长度,是可变的,这里的单位是4字节,意思是4位首部长度可以指定报头长度最大可以是60个字节
- 6位保留位:表示预留的空间,现在暂时不用,但是可能未来升级协议时就会用到
- 6个标志位:是TCP报文的核心字段,ACK为1,表示该报文是应答报文;SYN为1,表示该报文是同步报文段;FIN为1,表示该报文是结束报文段
- 16位窗口大小:说明接收方自己的缓冲区剩余容量大小,实际窗口大小不止64K,在选项中还包含一个窗口扩大因子M,实际窗口大小是窗口字段的值再左移M位
- 16位校验和:与UDP校验和作用一样
- 16位紧急指针
- 选项:可以有,也可以没有,可以有一个,也可以有多个
这是保证可靠传输的核心机制
核心逻辑就是接收方收到消息之后,需要给发送方返回一个应答报文(ACK, acknowledge),表示自己已经收到
如上图,发送方在发送数据时,TCP将每个字节的数据都进行了编号,即序列号,1 ~ 666表示报头序号为1,报文长度为666,而每一个ACK都带有对应的确认序列号,意思是告诉发送方,“我已经收到了哪些数据,让发送方下一次从哪里开始发”
在上述的确认应答机制中,A给B发数据报,B收到并且返回应答报文,这种情况是正常情况下的过程,但是可能存在两种情况,导致这个过程有问题:
问题一:我们该如何确定是哪种情况呢?又该如何处理
其实,我们不用确认是哪种情况,当主机A在规定时间内没有收到应答报文,那么主机A将会再次发送数据报
问题二:如果是ACK
丢失,那么意味着主机B将会收到很多重复数据,此时又该如何处理
那么就需要TCP协议能够识别哪些包是重复的包,并且把重复的包丢掉,在确认应答机制中的序列号就起到了作用,接收方收到数据会先放到操作系统内核的“接收缓冲区”,接收缓冲区可以看作是一个内存空间,并且是阻塞队列。当TCP收到新的数据的时候,就会根据序号检查这个数据是不是在接收缓冲区中已经存在了,如果不存在,就放进去,如果存在,就丢弃,保证应用程序调用socket api拿到的数据不重复。而主机B会隔一段时间向主机A发送应答报文,向主机A说明下面该传递哪些数据,因此不用担心主机A收不到应答报文,除非是网络异常
问题三:如果主机A一直收不到应答报文怎么办
重传如果失败,TCP可能还会进行尝试,但是不会无休止的重传,连续几次重传都不行,就会认为网络可能出现了严重问题,就会选择断开连接。而重传的时间间隔也不是固定的,一般来说会逐渐变大(即重传频率降低),因为一次传输失败的概率本身比较小,连续几次丢包的概率小之又小
基于以上两个机制,TCP的可靠性,就得到了有效的保证,但是TCP为了保证网络信号传输更高的可靠性,因此有了连接管理机制
三次握手的可靠性解释:
三次握手的过程是为了检测网络是否满足可靠传输的基本条件,第一次握手,服务器收到了客户端的SYN报文,服务器就知道了客户端的网络满足可靠传输,第二次握手,客户端收到了SYN、ACK报文,说明客户端就知道了自己和服务器的网络都满足可靠传输,第三次握手,是客户端告诉服务器,他自己的网络满足可靠传输,至此,双方都知道自己和对方的网络正常,那么就正式建立连接了
建立好连接之后,客户端和服务器就会在操作系统内核使用一定的数据结构来保存连接的相关信息,比如最重要的就是“五元组”
认识几个重要的连接过程的状态
问题一:为什么第二次挥手和第三次挥手不能合并在一起
不能合并的原因在于另一方发送ACK和发送FIN的时机是不同的;在三次握手中,ACK和SYN都是操作系统内核负责的,是同一时机;而四次挥手中,ACK是操作系统内核负责的,收到对方发的FIN之后,立即准备发送ACK,而FIN是用户代码负责的(只有用户代码中调用了socket.close()
方法,才会触发 FIN),如果这俩操作时间差比较小,那么可能会合在一起发,如果时间差比较大,那么大概率不会合在一起
认识几个重要的断开连接过程的状态
socket.close()
方法,来进行后续的挥手过程,正常情况下,一个服务器不会存在大量的CLOSE_WAIT,如果存在,大概率是代码有bug,导致close
没有被执行到滑动窗口的意义就是在保证可靠性的前提下,尽量提高传输效率
如图一这种发送方式,由于确认应答机制的存在,导致当前每次执行一次发送操作,都要等待ACK的到达,然后才能继续发送,大量的时间都花在了等ACK上,传输效率极低
如图二,就是滑动窗口机制,本质就是“批量的发送数据”,然后一起等一波ACK,一份等待时间等待了多份ACK,如果一次批量发送数据量为N,那么就称N为“窗口大小”
问题一:能不能不等,一直传输呢
必须得等,TCP是得保证可靠传输的,可靠传输的核心就是确认应答,如果不处理ACK,那么可靠传输就形同虚设
问题二:我们是等待所有ACK都到了才发,还是只要一个ACK到了,我们就继续发
“滑动”的意思就是并不用把N组数据的ACK都等到,才继续往下发送,而是收到了一个ACK之后,就继续往下发一个。比如上图,只要1001的ACK收到了,主机A就会再发送一个4001 ~ 5000的数据报,此时等待ACK的范围就是(2001,3001,4001,5001),如下图,滑动窗口的范围就是需要等待的ACK的范围:
问题三:因为数据是批量发送,如果丢包,该如何重传
第一种情况:数据报丢了
如图,在这种情况下,接收方收到1 ~ 1000之后,返回1001的ACK,然后发送5001 ~ 6000,而1001 ~ 2000丢了,那么虽然后面接收方收到了2001 ~ 6000,并且把这些数据都保存到接收缓冲区了,接收方还是一直在向发送方索要1001 ~ 2000的数据,多次同样的ACK,就会触发发送方的重传机制,然后发送方就会只发送1001 ~ 2000,当接收方收到数据报之后,由于后面2001 ~ 6000都已经接收到了,因此直接向发送方发送6001的ACK应答报文,告诉发送方,从6001开始发送即可。即重传只是需要把丢了的那一块数据重传即可,其他已经到了的数据就不必重传了,整体的传输效率比较高,被称为快重传
第二种情况:ACK丢了
在这种情况下,主机A会收到2001的ACK,而2001的ACK的意思是2001之前的数据都收到了,这种情况下,即使1001的ACK丢包或者比2001到的慢,也没关系,主机A就知道2001之前的数据主机B都收到了,滑动窗口向后移动两个单元
流量控制是在滑动窗口的基础上,目的是为了保证可靠性,在滑动窗口中,不仅要考虑发送方,还得考虑接收方,如果发送方发的太快,接收方处理不过来,就会丢包,还是要重新发送,不如调整发送速度,如图:
问题一,如何控制发送速度
如上图,接收方有一个接收缓冲区,我们可以将它理解为阻塞队列,而阻塞队列是一定有一个大小的,如果传过来的数据太多,势必会造成丢包,那么我们该如何控制发送速度呢,就需要接收方在收到发送的数据后,通过ACK报文来告知发送方自己剩余的容量大小,而这个数据就保存在TCP报头的16位窗口大小中,当窗口容量降为0的时候,就不会再发送了
问题二,当容量为0的时候,发送方之后该如何得知什么时候能继续发送数据
发送方会隔一段时间向接收方发送一个窗口探测报文,这个报文中没有数据,只是为了触发ACK,了解如今接收方的剩余容量大小,如果不为0了,那么就继续发送数据
拥塞控制也是在滑动窗口的基础上来保证可靠性,因为两台主机之间发送数据,可能需要经历很多的设备,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起大量丢包问题,得不偿失
因此为了摸清网络状况,TCP引入了慢启动机制,先发少量的数据,测测网速,再决定按照多大的速度传输数据
拥塞控制图分析
如上图,就是通过拥塞窗口的大小,来制约滑动窗口的大小,滑动窗口的大小 = min(流量控制窗口,拥塞窗口)。最开始的时候,拥塞窗口取的非常小,只有1个单位,然后先进行指数增长,让拥塞窗口快速增长到快要丢包但是还没丢包的大小,提高效率;指数增长到一定程度就进入了线性增长阶段,慢慢增长进行探测;而一直增长增长,势必会达到网络拥塞的地步,就会引发丢包,一旦丢包,拥塞窗口就直接降为最小
问题一:阈值的作用和更新
刚开始16就是当前的阈值,当拥塞窗口的大小达到阈值之后,拥塞窗口的增长就由指数增长改为线性增长;并且阈值也不是一成不变的,当出现丢包之后,阈值就降为了原来最大拥塞窗口的一半
问题二:出现丢包之后为什么直接降为最小
原因就是出现丢包,可能是网络不好,而如果网络不好了,我们只降一点点解决不了问题,甚至会造成更多的丢包,于是干脆直接降到最低,来保证传输的可靠性
延时应答是在流量控制的基础上进行的优化,流量控制是控制传输的速度,当窗口小的时候让速度慢下来;而延时应答,则是在这个基础上,尽可能的让发送方多发一些数据
延时应答图说明
当主机A向主机B发送了1 ~ 1000之后,它不会立即应答,而是稍微等一下,有可能在这等待的一段时间里,接收缓冲区的剩余容量就可能增加一些,此时再返回ACK报文,告知剩余容量,来让主机A发送更多的数据,因此这个操作就是在有限的情况下,尽可能的提高传输速度
可以采取下面的延时应答方法:
服务器和客户端之间的通信主要是下面几种模型:
而捎带应答机制,就可以提高一问一答这种模型的效率,如下图,ACK是内核响应的,会立即执行,而“响应”是应用程序执行到相关的代码才会发送的,这两个报文是不同时机触发的,理论上不应该合并,但是因为延时应答机制,导致ACK不一定是立即返回的,而ACK响应推迟,那么ACK报文就有可能和应用程序的“响应”报文的返回时机重合,这时,就可以把ACK和响应数据合二为一
不仅仅TCP存在粘包问题,其他的面向字节流的机制,也存在该问题,比如读文件,这里的粘包,粘的是应用层的数据报,在TCO接收缓冲区中,若干应用层数据报混在了一起
粘包问题举例
比如客户端向服务器发了好几份数据报,这些数据报在到达服务器之后,就会进行分用,而当内核把TCP的报文分用之后,把数据放在缓冲区供应用层读取,但是由于我们已经分用了,我们就分不清哪些数据是一个包里面了,该读多少我们并不清楚
粘包问题解决方案
本身粘包问题对TCP的传输没有任何影响,但是会影响到应用层读取数据,因此我们可以在应用层约定一种规则,明确包的边界,然后按照规则把数据进行切分,那么我们就可以知道一份数据是从哪到哪了,比如-数据-,使用“-”把数据进行分割
TCP异常情况:
这里说的是正常的终止,比如在任务管理器关闭进程,或者在桌面手动关闭页面和后台程序。
异常处理
实际上,TCP连接,是通过 socket 建立的,socket本质上是进程打开的一个文件,文件保存在进程PCB的文件描述符表中,每次打开一个文件(包括socket),都会在文件描述符表中增加一项,如果close文件,那么就会在文件描述符表中删除一项,而如果直接终止进程,PCB就没了,里面的文件描述符表也就没了,此时的所有文件都会进行关闭,而关闭的过程就和手动close一样,都会触发四次挥手过程,因此进程终止执行的是正常的TCP中断连接过程P
这里的关机是指点击操作系统的关机键,让系统正常关机,而这种操作和进程终止操作一毛一样
这里的机器断电以台式机为例,台式机被直接拔掉电源,进程就根本没有反应时间,无法进行正常的TCP四次挥手断开连接过程
导致的结果
比如客户端和服务器正在进行网络传输
服务器断电关机,那么客户端发过去的数据报就再也收不到ACK了,此时就会触发超时重传机制,重传了几次之后,依然没有收到ACK,客户端就会认为这个连接出现了严重故障,接着就会尝试重新建立连接,尝试失败,就会放弃连接(删除之前与服务器相关的连接信息)
客户端断电关机,那么由于服务器是被动接收数据的一方,因此就不清楚客户端是暂时没有数据还是挂了,因此就会隔一段时间发送一个探测报文(这种探测报文又称为“心跳包”),不带有实际数据,只是为了触发客户端的ACK,重复几次,服务器发现客户端没有传过来ACK应答报文,那么就会认为A出现了问题,因此也会尝试重传,尝试失败,放弃链接
什么时候用TCP,什么时候用UDP
IP协议主要完成两方面工作
- 4位版本号:IP协议的版本号,当前只有两个取值,4(0100,IPv4)和6(0110,IPv6)
- 4位头部长度:IP报头和TCP类似,是可变的,带有选项,单位也是4字节
- 8位服务类型(TOS):只有四位有效,包括最小延时、最大吞吐量、最高可靠性、最小成本(同一时刻只能选择一种),这四个选项都是为了选择最优路径,但是最优路径也分什么最优
- 16位总长度:与UDP类似,IP的数据报最大长度也是不能超过64K,但是IP协议自身是实现了分包组包的操作,就是下面三个标识符
- 16位标识:被拆出的同一个TCP数据报的16位标识的值是相同的
- 13位片偏移:对同一个TCP的多个包进行标记,描述谁先谁后
- 3位标志:标识当前是否是TCP分成的最后一个包,标记为1,表示这是最后一个包(结束标记),为0,说明后面还有
- 8位生存时间(TTL):表示一个IP数据报,在网络上还能存在多久,单位是转发次数,IP数据报每经过一个路由器,TTL - 1,如果TTL耗尽,那么收到这个包的路由器就把这个包丢掉(原因就是有些包里的IP地址永远也到不了,又不能让它无休止的在网络上转发)
- 8位协议:表示传输层是使用的哪种协议,TCP和UDP都有不同的取值
- 16位头部校验和:用来校验数据是否正确
- 32位源IP和目的IP:源IP就表示发送方地址,目的IP就表示接收方地址,对于IPv4,由于32位比特位人不好记,于是使用“点分十进制”的方式显示,把32位分成4部分,每个部分1个字节,范围就是0 ~ 255
- 选项:不定长,最多40字节
衍生出一个面试题:如何基于UDP,在应用层实现分包组包,参考IP协议的操作
IP地址分成两个部分,网络号和主机号,比如“127.0.0.1”中“127.0.0”就是网络号,最后的“1”就是主机号
要求:同一个局域网里,主机之间的网络号是相同的,主机号不同,两个相邻的局域网之间(同一个路由器之间连接),网络号也不同
值得一提的是,路由器也有IP地址,并且至少有两个,LAN口有一个自己负责的局域网的IP地址,称为内网IP(网络号是自己的局域网里的网络号),WAN口和其他路由器相连,也对应一个IP地址,称为外网IP(网络号是相邻局域网的网络号)
网络号和主机号的位数不一定就是24位和8位,具体的位数需要看子网掩码
子网掩码也是一个32位,点分十进制的整数,子网掩码的左侧全为1,右侧全为0,左侧的1就表示哪些位是网络号,右侧的0就表示哪些位是主机号
最常见的子网掩码就是255.255.255.0,如果一个局域网设备多了,子网掩码就会出现一些其他值,需要根据实际需要来确定网段如何划分(每个设备的IP都是可配置的,包括子网掩码也可修改)
当前IPv4协议,使用的 IP 地址是32位整数,能表示的范围是42亿9千万,但是随着网络发展,世界上的设备早已经超过42亿9千万,尤其是移动互联网的兴起,手机也要分配 IP 地址,因此,让每个设备都有唯一的 IP 地址,不现实
解决方案:
路由选择,也就是路径规划,两个设备之间,要找到一条通道,能够完成传输的过程
IP数据报中的目的地址,表示了这个包要发到哪里,如果传到一个路由器,这个路由器就认识这个目标地址,就会直接说明该怎么走,如果当前路由器不认识,就会说明大致方向,让数据报到下一个路由器再看,一直走,一直在接近目标,总会到达一个知道路的路由器,那么就可以具体的转发过去了,可能有的路由器不光认识,还知道多条路径,那么就可以选择一条更合适的路线
路由器如何认识IP地址,在路由器内部维护了一个数据结构,路由表,它是有一系列专门的路由表生成算法,自动生成的,也可以手动配置,而路由表里面就记录了一些网段信息(网络号,目的IP就在这些网络号中匹配)以及每个网络号对应的网络接口(网络接口就对应到路由器里面的具体端口)
数据链路层主要的协议,叫做“以太网”,平时插的网线,就叫做“以太网线”,以太网这个协议不仅仅规定了数据链路层的内容,也规定了物理层的内容
以太网数据帧结构
- 源地址和目的地址:6个字节,比IPv4长了六万多倍,被称为“mac地址”,IP地址没做到的唯一,mac地址做到了每个设备唯一(每个网卡都是唯一的,在网卡出厂时就写死了)
mac地址和IP地址的作用其实有些重复了,因此现状就是mac地址和IP地址同时使用,表示不同的功能,IP地址用来表示传输的起点和终点,mac地址用来表示传输过程中,任意两个相邻节点之间的地址- 类型:如上图所示,
0800
是一般情况,0808,8035
都是特殊情况,0800
就是表示后面的数据报是IP数据报- CRC:帧尾,是一个基于CRC算法(循环冗余算法)的校验和
- 中间数据的1500字节:被称为MTU,是指一个以太网数据帧能够承载的数据范围,这个范围取决于硬件设备。以太网是和硬件密切相关的,不同的硬件设备,对应的数据链路层协议,可能就不一样,MTU也不相同。数据链路层需要考虑的是相邻设备之间的传输,因此就要考虑不同设备之间的承载量,而IP的分包,不只是给IP的报头64k准备,更多的是为了适应不同的MTU
换句话说,虽然TCP是面向字节流,但是数据量还是受限于MTU,而MTU又分为三部分,IP长度,TCP长度,数据长度(MSS),因此我们说,当数据长度不超过MSS时,是最高效的
ARP协议
ARP报文不是用来传输数据的,而是根据协议建立起IP -> mac地址的映射关系。由于路由器会分用到网络层,根据IP地址来规划接下来的路线,因此就需要这样的映射关系,在封装以太网数据帧的时候,明白目的mac是啥
做法:当设备启动时,会向局域网中广播ARP报文,每个设备收到后,给出应答,应答中就包含了自己的IP和mac,发起广播的设备就能根据这些应答建立起映射表了