一)应用层协议简介:根据需求明确要传输的信息,明确要传输的数据格式;
应用层协议:这个协议,实际上是和程序员打交道最多的协议了
1)其它四层都是操作系统,驱动,硬件实现好了的,咱们是不需要管
2)应用层:当我们继续进行写应用程序的时候,尤其是应用程序负责网络通信的时候,往往是需要自定义应用层协议的,我们要根据需求,明确要进行传递的信息,我们还要明确传递的数据格式;
3)应用层这里面,最重要的事情,就是说自己实现一个应用层协议,比较简单也是在工作中经常做的事情;
FTP,SSH,TELNET,DNS(域名解析的协议)
1)使用现成的应用层协议来进行开发
2)程序员自己约定一个协议,来进行开发,只要客户端和服务器自己进行开发的;例如我在网页上面给服务器发送了一个请求,请求中包含着一个链接,然后服务器给浏览器返回了一个响应,这个响应就是一个网页;在这个过程中,使用的协议就是HTTP协议这时就可以
使用自定义协议,来约定好请求是什么格式,响应是什么格式,客户端和服务器就可以按照这样的约定来进行开发了;3)总而言之,自定义协议,其实是一个很简单的事情,只需要约定请求和响应的详细格式即可,越详细越好,要把各种细节都能够交代到,能够更好的表示当前的信息,咱们也可以自己来约定好格式,也可以基于xml,Json来约定数据格式,甚至还可以通过一些其他的二进制的方式来进行约定数据
1)例如我们要提供一个新的需求,要求在外卖软件的首页,显示一个优惠活动,用户参与活动,就可以参加抢红包的操作;
2)客户端:修改界面,可以显示优惠活动的详情;
3)服务器:修改后台逻辑,针对啥样的用户可以参加优惠,具体怎么做可以领到红包,红包金额是多少;
4)当的客户端启动的时候<就要向服务器进行查询,当前用户是否可以进行参加活动,服务器就要返回是或者否
5)这个时候在开发功能之前,就要约定好,客户端发的查询请求的功能是怎么样的(带有用户的身份信息),在约定好服务器返回的响应是什么样的(1表示可以表示能参与,0表示不能参与,true表示能参与,false表示不能参与)
1)假设现在自己实现一个功能叫做获取用户的订单历史(这个订单历史在数据库里面,我们要从服务器操作数据库来拿,这样的功能就会涉及到前后端的一个交互过程),咱们的前端和后端就是通过网络来进行交互的;
2)在这个交互的过程中我们就需要约定好,前端发送什么样子的数据,后端要返回什么样子的数据,本质上来说就是在进行规划请求和响应之间的信息,以及传数据的格式;
咱们进行查历史订单,查找谁的订单,以及查找哪一个时间段的订单,以及数据太多,你要不要截取一部分;
1)我们前端发送的请求类似于:
{
用户的ID,
查询的起始时间,
查询的结束时间,
显示的条数;
}
2)我们后端返回的响应:
2.1)查询是否成功?如果失败,原因都有哪些?
2.2)返回一个订单数组:
数组里面的每一个元素:
{
商品名,
商品单价,
商品数量,
店铺名称,
总的金额
}
像上面的过程就相当于是设计应用层协议,咱们设计应用层协议,本质上就是做两个工作
1)明确要传输的信息
2)明确数据的组织格式,请求的格式是啥样子的,响应是啥样子的
1)当前我们只是给出了一种可能的格式,此处我们这里面的数据的格式,也是可以进行随心所欲的约定的
2)相比之下我们只需要让我们的客户端和服务器都按照一定的格式来进行交互就可以了;
3)上面的这一种设计十分不好,咱们的输出传输效率比较低,这也就说明了是运行效率低;
4)咱们的这个代码可读性也不好,本身开发效率也比较低,正因为如此,大佬们发明了一些比较好的协议的模板,让我们向上套;
XML,JSON,protobuffer,前两种可读性比较好,但是运行效率不高,咱们的第三种可读性不好,但是运行效率比较高;
1)咱们的XML就是一种老牌的数据格式了,虽然现在也在用,但是越来越少了,咱们的这些标签本身占据的空间已经超过了数据本身所占用的空间,也就说引入了更多的辅助信息,我们程序的效率就会受到很大的影响,对于一个服务器程序来说最贵硬件资源就是网络带宽,对于XML来说,虽然提高了可读性,但是又引入了太多的辅助信息,比如说标签名字,这些辅助信息所进行占用的空间甚至已经超过了数据本身的一个空间,因为在XML里面要经常表示这些辅助信息,在传输相同的网络数据的时候,这就会导致传输相同数目的请求的时候,占用的网络带宽是更高的,如果说带宽固定,那么相同时间内能够传输的请求数目就是更少的,所以说我们程序的效率就会收到很大的影响;
2)咱们现在看这个XML标签,标签名就是key,标签值就是value,就构成了一个键值对,通过这些标签,就更好的体现了我们数据的可读性,尤其是哪一个部分代表是什么意思,非常一目了然,咱们的一个服务器程序最贵的硬件资源就是网络带宽,因此XML现在已经很少作为应用层协议的设计模板了,现在使用XML主要是为了做配置文件;
3)JSON的传输效率虽然要比XML要好一些,但是还是要多进行传递一些冗余信息,就是Key的名字,但是Json格式的数据也是有非常大的缺点,尤其是在进行表示数组的时候,这里面的Key是在不断重复的这也会占用大量的带宽资源但是咱们的JSON是一种最重要的设计模式
4)因此咱们的protobuffer应运而生,这是一种二进制格式的数据,在这种格式的数据里面,不会再包含上面的key的名字了,而是通过顺序或者是一些特殊的符号,来进行区分每一个字段的含义,同时在通过一个IDL文件,来进行描述这个数据格式(来进行描述每一个部分是啥意思),IDL只是起到一个辅助性开发的效果,并不会真正的进行传输,传输的只是二进制的纯粹的数据,似乎传输效率高了,但是开发效率低,可能是把对应的文字换成了对应的二进制数据,咱们通过二进制的数据对这里面的内容重新地进行了编排,甚至有可能做出一些数据压缩,只进行传递一些必要的信息,传输效率会更高,但是这些数据肉眼难以观察;
5)开发效率低,是包含了开发和调试,调试很不方便,如果说咱们的线上环境出现了问题,如果说咱们用JSON,出问题的请求和响应,一目了然(一看就知道哪里出现了问题),如果使用protobuffer,二进制的数据,根本没法用肉眼看,我们就需要自己开发一个专门的程序来进行解析这里面的数据,来进行分析这里面的问题,这就会比较麻烦;
咱们广论开发:咱们的protobuffer里面有IDL,这个就是说咱们的protobuffer约定的一种文件格式,类似于C语言结构体的写法
class message Response{ bool ok=1; string reason=2; repeated Data data=3; }
咱们来进行设计应用层协议,是一件非常普遍的事情,也是一种并不复杂的事情;
设计应用层协议,我们要做的工作就是说:
1)要根据需求,明确传输的信息
2)要进行明确数据传输的格式,要参考现有模板,比如说JSON,XML,protobuffer
端口号的用途:标识一个进程,就可以区分当前数据要交给哪个进程来处理
1)例如在开发服务器的时候,首先会让服务器提供一个业务端口,通过这个端口,就可以提供一些广告搜索的服务(上游服务器,就可以通过这个端口来获取到广告数据)
2)其次服务器还会提供一个调试端口,在服务器运行过程中,其实会涉及到很多很多的数据,有时候为了定位一些问题,就需要查看一些内存中的数据,通过这些调试端口给服务器发送一些调试请求,于是服务器解耦可以返回一些对应的结果;3)直接拿调试器打一些断点可以吗?
如果拿调试器来进行断住程序,此时整个进程就会处于一种阻塞的状态的,这就意味着这个服务器是无法响应正常的业务请求的4)咱们的很多网络服务是进行使用非常常用,非常广泛的服务,为了更好地进行管理,我们就给这些服务分配一些专门的端口号
80---->http服务器
443---->https服务器
23 ------>ssh
23------>ftp服务器,自己的部署http服务器,就可以把他绑到80,也可以把它绑到其他转口
当一个数据包到达了网卡以后,然后自己主机上面的电脑的所有进程都是通过同一个网卡来传输数据的,可以让每一个进程分别绑定不同的端口号,此时收到的网络的数据包中也会包含一个目的端口的字段,此时就会让这些目的端口找到对应的端口号的进程,从而把数据交给对应的进程,这个过程就是由操作系统内核来完成的
5)咱们的端口号的范围就是0-65535,这里我们就需要说一些常见的知名端口号,我们把0-1024这些端口号,给进行划分出了一些具体的应用;
6)因为咱们的很多网络服务是属于非常常见,非常应用广泛的一个服务,我们为了进行更好的管理,我们就需要给这些服务分配一个专门的端口号,一方面你见到了这个端口,就知道这是什么服务,另一方面我们把常见的服务的端口分配好,然后我们再进行部署一些服务的时候,这些服务之间的端口号就不会容易冲突;
7)我们并不是强制要求,而是建议,因为咱们的80端口是http服务器,咱们的443端口是http服务器,咱们的22端口是ssh服务器,咱们的23端口是ftp服务器;
端口号:端口号是一个整数,用来区分进程;但是PID(用于进程的身份标识)也是一个整数(PCB中的一个特性),用来区分进程,为什么在网络编程中,不直接使用PID,而是直接用到端口号这样的概念呢?
端口号是固定不变的,端口号我们可以手动指定,但是PID每次进程启动之后都会发生改变,这是系统自动分配的,我们无法控制;
1)例如客户端要连接一个服务器,客户端就要先知道服务器的IP与PID,一旦服务器进行重启,PID就会发生改变
中国移动:10086--类似于端口号,这是固定不变的
转换人工:工号xxx为您服务
他就类似于PID,每次接通电话可能都不一样
同样的,两个进程无法绑定到同一个端口号
2)系统提供的原生的socket API其中有一个方法就叫做bind,功能就是把端口号和一个socket关联起来,一个进程是可以绑定多个端口的3)端口号是传输层协议的概念, TCP和UDP协议的报头中都会包含源端口和目的端口, 并且都是使用2个字节, 16bit来表示端口号, 范围也就是 0 -> 65535; 但是我们日常写的程序使用的端口号一般都是从1024开始的, 因为0 -> 1023这个范围的端口号也称为 “知名端口号/具名端口号”, 这些端口号系统已经分配给了一些知名并广泛使用的应用程序
4)这里我们并不是完全不能使用
0 -> 1023
这个范围的端口号, 只是建议使用, 虽然这些端口被分配给了特定程序, 但是这些程序是否在主机运行着, 主机上是否安装了这些程序都是不一定的, 要使用0 -> 1023
这些端口, 需要注意2点 :
- 要确定这个端口没有和程序绑在一起.
- 要拥有管理员权限.
应用层和传输层的联系:除了最上面的应用层,下面的传输层,网络层和数据链路层,物理层这四层都已经在操作系统内核,驱动程序和硬件设备已经实现好了,不需要我们去实现,传输层是紧接着应用层的一层, 虽然传输层是操作系统内核实现好了, 但是我们在写应用层代码的时候, 是要调用系统的
socket API
去完成网络编程, 所以需要我们了解这里传输层的一些关键协议UDP
和TCP
.
二)UDP协议:适用于数据量比较小的场景了,数据载荷比较小
2.1)UDP抱头:
1)传输层,是基于操作系的内核进行实现的,咱们的程序员是不需要和传输层来进行打交道的,但是传输层意义非常重大,因为进行网络编程是需要用到Socket,也就是说一旦调用了Socket代码就进入到了传输层的范畴;
2)咱们一个完整的UDP数据包=UDP报头+UDP数据载荷,UDP数据载荷就是一个完整的UDP数据包;
3)所谓的把一个应用层数据报封装成UDP数据包,其实本质上也就是说加上了8个字节特定含义的数据
4)咱们在代码中所写的端口号,就会被打包到这样的UDP数据包里面,这是我们在报头中体现的,咱们以UDP客户端为例,源端口也就是操作系统自动默认给客户端分配的端口,目的端口也就是服务器的端口已经被指定;
5)为什么端口号的范围是0-65535,在UDP数据报头中最多也就给安排了两个字节的数据
6)咱们是否可以把UDP这里面的端口改成4个字节之类的呢?
1)不可以,一方面这个东西是操作系统内核实现的,不好意思,你都拿不到windows系统源码,就改不了内核
2)第二方面,就算你把自己的主机的UDP协议给改了,那还有别人的主机呢?要想改,就把所有的世界上的主机都进行修改;
1)咱们的入口服务器会根据请求分别从广告服务器和大搜索服务器来进行获取数据,并进行拼接,最终会得到一个完整的网页返回给浏览器
2)现实就是说咱们的广告服务器和入口服务器和咱们的自然搜索结果服务器和入口服务器要进行网络的传输和交互,就需要选定一个好的应用层协议,最开始的时候用的UDP协议
3)因为最开始的时候,咱们的所进行使用的就是UDP协议,但是随着时间的推移,随着业务的发展,咱们的广告服务器所进行吐出的业务数据越来越多,更复杂;
2.2)UDP校验和:
UDP校验和:是验证网络传输过程中这个UDP数据是否是正确的,因为咱们的网络上面的传递数据的本质是光信号和电信号
1)网络上传输的数据,有可能会发生故障,网络上的数据本质都是一些0/1的bit流,这些bit流是通过光信号和电信号来表示的(高频率表示1,低频率表示0)
2)如果在传输过程中出现了比特翻转(0->1,1->0)的情况,尤其是卫星上面的数据,要考虑比特翻转和太空上宇宙射线,就比如说磁场,就会使我们的原有的一些传递的信息发生了改变肯定会有一些特殊的情况会影响到我们的数据传输质量,再举一个例子,我们本来是想要发一组连续的高频信号,因为我们遇到了一个强磁场,我们就可能会导致其中的某些高频信号变成低频信号(从0变成1),在网络传输中,我们一定会遇到一种情况,会影响到我们的网络传输质量的,本来表示0,结果变成1了,数据就变错了;
3)校验和的本质上是检验当时数据的传输是否出问题,需要尽可能识别出数据是不是错的,校验和就可以帮发现数据是否错误,但是校验的效果不够理想, 万一你的数据同时变动了两个bit位(前一个字节少1, 后一个字节多1), 就会出现内容变了, CRC没变这样的情况.
比如说我想要去超市买菜,我妈妈让我买四样菜,土豆,西红柿,白菜,酸菜;
校验和:四样菜,我们就可以根据四样菜来进行判断买菜是否正确;
1)我买完之后,发现手里面有5样菜或者3样菜,就发现我买错了;但是此时我发现手里面有4样菜,就一定买对了吗?
就是有可能是菜有猪肉,羊肉,西兰花,韭菜
其实也不一定,这是仅仅靠检验数据来进行校验的,所以校验和正确不一定保证数据正确2)校验和不一定可以百分之百的进行校验,如果校验和正确,不一定可以保证数据100%一定对,但是如果校验和不正确,这时就可以直接判断数据一定是错的;
上面的一副对联就相当于是校验和,就是基于内容来计算出来的校验和,比如说最后一条变成了小李飞刀,对联就对不上了,比如说src,md5还有sha1;
2.3)UDP是如何进行数据校验的:
第一种方式:crc机制也叫做循环冗余检验,例如现在有一行二进制数据,依次按照字节为单位,将这些字节进行累加,如果说咱们的校验和数据出错,那么直接丢弃
short sum; for(byte b:数组) { sum=sum+b; } 这是如果出现溢出,溢出的数据就不要了
1)传输数据的时候,就会把数据和src校验和一起传输给目标,接收方同时收到了src校验和数据,接收方就需要验证一下当前的数据是否是正确的(在网络传输中可能会出现比特反转的情况,接收方再按照同样的算法,再根据数据部分进行校验和,把这个新计算的结果和收到的src校验和进行对比,看看结果是不是一致的;
2)本质上来说就是,在发送之前,针对要发送的数据算一遍src,接收到之后再次算一遍src,把这个新算出的结果与发送过来的src的结果进行对比看看他们俩相同,如果出现了比特翻转接收到的内容,和发送的内容就不一样了,此时算出的两份src就大概率是不会相同了;其实还是有一定的概率,两个不同的数据,算出的src是一致的,这个概率整体上来说是比较小的
3)md5算法,这种算法的应用场景非常多,用来做校验和,只是其中的一个场景;本质上是一个非对称的哈希算法;
3.1)首先回顾一下哈希表,它的核心是一个数组,可以做到O1复杂度的增删改查,借助了数组下标的随机访问能力,通过key进行哈希算法,转化成一个数组下标;
3.2)哈希算法有很多种,针对整数的哈希算法,都是比较简单的;md5算法本质上是针对数据进行一系列的数学变换,md5算法的特性:定长,分散,不可逆
1)定长:无论输入的字符串长度有多长,得到的md5的值都是固定的字符串的长度(32位),4个字节64位, 八个字节(128位)
2)分散:只要输入的字符串改变了一点点得到的md5的值都会差别巨大,如果两个字符串的md5的值一致,基本上是不可能的;
3)不可逆:给定源字符串,很容易算出md5的值,但是对于md5的值,很难算出字符串
可以作为哈希算法,也可以作为校验和,key是MD5,value是原始字符串4)例如在银行里面设置密码,后台的工作人员可不可以看到呢?
密码原文,小键盘输入,程序读取到这个密码之后,就可以通过MD5进行加密,然后数据库里面存的也是密文,由于MD5不可逆的特性,程序员无法感知
2.4)UDP总结:
1)无连接:知道对端的IP地址+端口号就可以直接进行传输,不需要建立连接;
2)不可靠:没有任何的安全机制,咱们的发送端发送数据包之后,如果说因为网络故障导致无法收到信息,UDP协议层也不会给应用层传递任何错误信息;
3)面向数据包:咱们的应用层交给UDP多么长的报文,UDP会原样进行发送数据,既不会拆分数据,也不会合并数据,如果说发送端一次发送100个字节,那么接收端也必须一次接受100个字节,而不是说循环接收10次,一次接受10个字节;
4)缓冲区:咱们的UDP只有接收缓冲区,没有发送缓冲区,咱们的UDP没有真正意义上面的发送缓冲区,我们进行发送的数据会直接交给操作系统的内核,由内核直接把数据交给网络层协议来完成后续的传输动作,但是咱们的UDP具有接收缓冲区,但是这个接收缓冲区并不能保证咱们的收到的UDP数据包的顺序和发送UDP数据包的顺序是一致的,如果缓冲区满了,再次到达的UDP数据就会被丢弃;
5)大小受限,咱们的UDP首部长度中有一个是16位的最大长度,也就是2个字节,也就是说一个UDP数据包的最大传输的数据范围是64K;
三)TCP协议:
1)16位源端口和16位目的端口:表示这个进程从哪里来到哪里去;
2)32位序号和32位确认序号:进行编号保证可靠性
3)4位首部长度:这里面是表示四个比特位,本质上来说就是表示TCP报头的长度,TCP的报头是在不断变长的,不像UDP一样就是固定8个字节,此处的表示该TCP头部有多少个32个bit位,都多少个四个字节,4位表示4个比特位,也就是说TCP头部的最大长度就是说4*15=60个比特位;
4)16位紧急指针标识哪些数据是紧急数据;
5)16位窗口大小标识滑动窗口的大小
6)16位校验和表示发送端填充,CRC校验,如果说接收端校验不通过,那么就认为数据有问题,此处的校验和不光包含TCP首部,还包含着TCP数据部分;
7)URG:表示紧急指针是否有效;
ACK:表示确认序号是否有效;
PSH:表示提醒接收端应用程序立刻从TCP接收缓冲区里面把数据读走;
RST:表示对方要求重新建立连接,我们把表示为RST标识的称之为复位报文段;
SYN:表示请求建立连接,我们把SYN标识的段叫做请求同步报文段;
FIN:表示本段要进行关闭了,我们把表示为FIN结束标识的段称之为结束报文段;
8)保留6位:保留位的意思就是说现在还不进行使用,但是保不齐以后会进行使用,也就是说咱们是为了未来的升级留点空间;
9)选项:主要是说这个选项,可以有,也可以没有,可以有一个,可以有多个;
1)TCP头部中的最后一个选项字段是可变长的可选信息,这一部分最多包含40个字节,因为TCP头部长度最多就是60个字节,还包括前面讨论的20个字节;
2)最重要的是窗口扩大因子;
可靠性:不是说发送出去的数据100%就能够接收到,我发出去的数据,到底对方收没收到,我心里有数,我可以知道我发送的数据成功还是失败(接通电话的时候,对方有反应,就知道可靠不可靠);
1)换一种角度来说,客户端发送出去的数据,客户端知道对方是接收到发送的数据,还是知道对方没有接收到我的数据;
2)发送出去之后就不管了,也不知道你有没有收到,这就叫做不可靠传输;
3)接收方收到消息之后,给发送方返回一个应答报文ACK,表示自己已经收到了;
1)确认应答机制(保证可靠传输的核心)(正常情况下,数据传输的可靠性)
咱们虽然在TCP代码上面看不出来,但是这个确实是TCP中最可靠的机制,引入TCP的关键原因就是为了可靠传输,因为TCP是有连接的,所以在客户端发送数据给接受方了,接收方就应该返回一个应答报文,当发送方收到了这个应答报文后就认为对方已经收到了,我打电话的时候,我进行在电话里面说话,对方说嗯嗯,我才表示已经收到了;
1)但是网络上的传输,顺序是不确定的,因此就不可以单纯的通过收到数据的顺序来判断和确定逻辑,你所进行发送的两个数据包,是不一定走同一条路的,先发后到;
2)例如:我给我的女神发送消息
第一次发送请求,你在吗?女神回复说在,这就是一个可靠传输
第二次发送了两个请求:
请求一:我想请你吃个麻辣烫,请求二:做我女朋友吧
3)这是我发送了两条消息,一个是好呀好呀,另一个是滚,由于网络延迟,我不知道他的回答是是针对我的那条请求来回答的,出现了后发先置的情况,我并不知道它的这两个响应报文分别对应着哪一次请求,可能这个吃麻辣烫是滚,对做我女朋友是好呀好呀;
1)这时女朋友和我可以来个序号(编号):
我:请求1:女神你陪我吃麻辣烫好吗?请求2:女神,你做我女朋友好吗?(序号)
女神作出回应:针对请求一是好呀好呀,针对请求二,是滚,确认序号,我们如果说使用编号,就不会再出现数据顺序混乱的情况了;
2)确认序号表示当前这个应答报文是针对哪一个消息进行的确认应答,TCP是依靠字节来进行编号,通过序号来进行表示当前发送的是哪一条消息,通过确认序号来进行表示针对哪一条消息来进行回应;
3)在网络通信中两个主机之间通信链路可能存在多条,数据包1和数据包2走的都是不同的路线,并且设备的转发速率也是非常慢的,受网络环境的影响,这种后发先到的情况是不可避免的,收到消息的变数有可能就会受到影响,这样应答错乱以后,解析数据的含义就出现歧义了
1)第一次请求,主机A给主机B发了个1000字节的数据,序号就是1-1000,每一个字节都是有一个序号的这个操作相当于是发了个TCP数据报,这个数据报的的序号是1,长度是1000
2)咱们的主机B会给主机A返回一个应答报文(ACK里面),里面会传输一个确认序号叫做1001,确认应答数据报,里面的确认数据是1001,意思就是1001之前的数据B已经被顺利收到了,另外也可以理解成接下来B在向A索要1001开始的数据;这是发送方就可以经过应答报文来确定对方是否收到了,接下来主机A也就是知道了,B已经收到了我发送的1到1000的数据包,接下来主机A要发送1001-2000之间的数据了;
3)接下来A收到B的确认应答报文之后,A会向B发送一个1001-2000的数据包,B会给A返回一个确认序号为2001的应答报文,表示200之前的数据已经收到了,同时也在催促A该发送2000以后的数据报文了,此时A就知道了,B已经收到了2000之前的数据,咱们要接下来发送2001-3000的数据;
4)TCP的核心是可靠性,可靠性的核心是确认应答;
5)发送1-1000的数据的意思是TCP报头中的序号是1,报文长度是1000,通过这个信息来表示明确的范围,就是表示时时刻刻确认序号之前的数据咱们已经收到了;
6)咱们的这个1-1000是一个TCP数据包,这一个TCP数据包通过层层的封装变成了一个以太网数据帧,进行传输,如果说有传输了多个数据包,分成了多个以太网数据帧,多个数据帧之间才会出现后发先致;
7)在生产者消费者模型里面,也会存在着确认应答机制,消息队列里面会保存着一些数据,这里面的数据也要是定期删除的,因为消息队列里面的存储空间也不是无限大的;那么我们要删除那些数据呢?
这个时候就要用到确认应答机制了,当消费者模型将元素进行消费的时候,会给消息队列发送一个ACK,这是消息队列就知道了哪些数据是被消费过的;
8)咱们的TCP针对每一个字节的数据进行编号,那么就是序列号,咱们的每一个ACK都是带有一定的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你要从哪里发
9)超时重传机制相当于是对确认应答机制进行了补充,上面说的确认应答是网络在一切正常的情况下,通过应答来进行确认可靠性,就是咱们的服务器会给客户端发送一个ACK通知发送方表示我收到了,但是如果出现了丢包的情况,超时重传机制就要起到效果了;
10)TCP可靠传输的能力最主要是通过确认应答机制保证的, 通过应答报文不仅可以让发送方清楚的知道是否传输成功, 而且通过序号和确认序号对多组数据的应答对应关系进行了详细的区分
2)超时重传机制(偶然丢包的时候,数据传输的可靠性)
为什么要进行重传?重传丢的数据是什么
1)第一种情况就是说主机A传输数据syn的过程中就出现了丢包,另一台主机没有接收到数据,自然也就没有办法给你返回ACK;
2)第二种情况就是说主机A成功的把数据发送到了主机B,主机B也确实收到消息了,但是主机B返回的ACK丢包了;
对于咱们的发送方来说根本是无法区分哪一种原因导致我收不到ACK,于是主机A就直接往坏了想,对方压根就没有收到,我们就重新再进行发送一次业务数据;
3)这里面的重发也不是立即就重发的,而是要等一会,给点反应时间,数据在网络传输是需要时间的,如果发送发10min没有等到ACK,超出了等待时间,就进行重传业务数据,正常情况下,连续发丢了两次数据包的概率是比较低的,因此就希望第二次重传发送是可以成功的,如果网络不是有太大的问题,一般重传都是可以成功,本来说丢包就是一个小概率时间,但是连续发送两次都进行丢包,那么概率就会变得更低了;
超时重传TCP的去重:
1)现在来假设试想一下,如果是接收数据方的返回的ACK丢了,此时触发了超时重传,就会导致接收方受到了重复的业务信息,如果说这里的重复发送的数据不是请吃麻辣烫,而是借钱,那就很尴尬了;
2)TCP内部就会有一个去重操作,接收方收到的数据会先存放到操作系统内核的接收缓冲区里面,接收缓冲区咱们可以认为是一个内存空间,也可以视为一个阻塞队列;
3)如果咱们收到了新的数据,TCP就会根据这个序号,来进行检查这个数据是否在接收缓冲区中已经存在了,如果不存在就直接放进去,如果存在,我们就直接丢弃,
4)这个时候咱们的操作系统内核就可以保证咱们的应用程序调用SocketAPI拿到的这个数据一定是不重复的,ACK和业务代码没啥关系,虽然说咱们的超时重传操作可能会使接收方收到重复的TCP报文,但是TCP在我们的操作系统内核里面已经完成了去重操作,这样就可以保证应用程序所拿到的数据一定是不重复的,应用程序是感知不到超时重传的过程的;
重新发送的数据是会进行重新封装还是将之前封装好的报文缓存下来之后再进行发送?
1)进行重新发送的数据这是TCP负责进行发送的,调用一个socket中的write操作,本质上是把这个数据写到TCP的发送缓冲区里面,由我们的操作系统内核从发送缓冲区中取出数据,通过网卡进行传输,封装的过程;
2)当触发了超时重传的时候,操作系统内核就会把刚才发送缓冲区里面的内容重新进行封装,在进行传输,如果顺利的收到了ACK,就可以把这个数据从发送缓冲区里面给删除掉,咱们的发送缓冲区也可以视为是一个阻塞队列;
超时重传的策略:动态重传时间+最大传输次数
1)linux中是以500ms为单位来进行控制,每一次判定超时重传的时间都是500ms的整数倍,如果说进行重新发送一次,得不到应答,就会等待2*500ms进行重传,如果说还收不到应答,就会等待4*500ms进行重传,以此类推,以指数形式进行倍增
2)进行重传的次数累积到一定的次数,那么TCP认为网络或者对端主机出现异常,就会强制进行关闭连接
只要重传还是一直失败,大概率是认为,这个传输时是通不了,咱们进行超时重传的数据是不一定会成功的?
1)假设一次丢包的概率是10%,那么两次都丢包的概率是1%,三次都丢包的概率是0.1%,如果两次三次都丢包了,就认为网络上面出现了严重的故障(挖掘机铲网线),没网费了,自动断开TCP连接,就尝试重新建立连接;2)重传如果失败,可能还会再进行尝试,也不是会无休止的重传,连续几次重传都不行,那么TCP就认为这个网络可能是约到了严重的情况,怎们重传恐怕都不行了,就只能放弃了,就会自动地进行断开TCP的连接,再去尝试建立连接;
3)咱们的重传的时间间隔不是确定的,不是一直不变的,而是在进行动态变化的,一般重传的时间间隔会变得越来越大,重新尝试发送数据的频率会越来越低;
4)一次传输失败的概率本身就比较小,连续两次都失败概率只会变得越来越小,小上加小,这个时候咱们的TCP本身就不会在指望咱们重传可以成功了,认为这个时候重传频率再高,大概率也是没啥用的,白费力气;
5)假设丢包概率是10%相当高了,那么连续两次丢包的概率就是1%,连续两次丢包就意味着当前丢包的概率已经高于10%了,接下来我们再次进行发送,TCP知道能够顺利发送过去的概率和可能性也是非常小的;
6)超时的时间的确定,也是非常困难的:
在最理想的情况下,需要找到一个最小的时间,保证确认应答一定可以在这个时间段内返回,但是这个时间的长短,随着网络环境的不同,是会有差异的,如果的超时时间设置的太长,那么会影响整体的重传效率,如果说我们的超时时间设置的太短,那么就会有可能说是频繁发送重复的包,所以说咱们的TCP保证无论在什么情况下都可以进行高性能的通信,会动态地进行计算这个超时时间
1)基于上述的两个机制,咱们的TCP可靠性就看基本得到了有效的保障,一种是针对数据传输正确的情况下是怎么进行处理的,确认应答
2)一种是基于丢报的情况下怎么进行处理的,超时重传
3)超时重传以500ms为单位,进行依次叠加,如果是发送的syn丢了,那就再发送一次SYN即可,如果返回的ACK丢了,那么此时隔一段时间重复发送SYN,就会在内核的接收缓冲区里面进行去重;
3)连接管理:也是保证TCP可靠性的一个机制,本质上是四次交互
一)三次握手:
1)A一定是服务器,是被动的那一方,但是B是客户端,是主动的那一方,开始建立连接的过程,是客户端主动建立连接的,但是在断开连接的时候,客户端和服务器都是可能先进行发送FIN的;
2)本质上就是说咱们的客户端和服务器通过三次交互,完成了相互连接的过程:
3)TCP这里的连接指的是由一个五元组来标识, 一个连接建立完成就表示通信双方知晓对方的IP和端口信息, 就是通信双方各自都维护着连接这样的一个数据结构, 双方把对方的地址信息都保存下来就是完成了连接, 而断开连接就是把各自存储的连接删除掉
1)为什么是三次握手?不是四次握手?
1)当A的syn报文到达B后,B的内核会在第一时间发出响应 ,就会立即应答,B会发出syn,尝试建立连接给A,同时会立即向A发出ack,确认刚才已经收到A发送出去的请求,这两件事是同时触发的,就没有必要进行四步握手协议,直接一次到位;
2)咱们在网络上面进行传输的数据,都是需要经过一系列的封装和分用,才可以完成传输,封装两次肯定不如封装一次更高效,因为到了网络层就需要加上IP数据报头,到数据链路层就需要加上以太网数据桢头桢尾,还要在网络上进行传输,所以就需要放在一起,提高传输效率;
3)把syn和ack合在一起,只需要把TCP报头中六个标志位中的的syn和ack标志位都设置成1就可以了;
2)为什么要进行三步握手协议?为什么要建立连接?和可靠性有啥关系;
1)投石问路:通过三步握手协议,来进行确定A和B之间的传输是否是通畅的,也就是说确保通信的链路畅通,检查这个网络的情况是否满足可靠传输的基本条件,如果你强行在网络环境特别差的情况下进行TCP传输,那么也是会涉及到大量的丢包问题,无法保证可靠传输,那么也就没有必要进行TCP的数据传输了,防止通信过程中浪费大量的时间;
2)尤其是确认A和B的发送数据能力和接受数据能力是否是正常的,保证可靠性的前提条件,所以说三次握手也是在检测通信双方发送能力和接受能力是否都正常;
3)协商参数,也就是说让通信双方协商一些重要的信息,通过三次握手,让A和B之间通通气,选择一些传输中合适的参数,例如TCP的序号从几开始,咱们的客户端和服务器是有一些共同的信息的,因此在三次握手的同时,也会相互之间交互一些必要的内容,双方按照商量好的一个方式来进行后续的工作;
1、对方报文发送的开始序号; 2、对方发送数据的缓冲区大小; 3、能被接收的最大报文段长度MSS; 4、被支持的TCP选项;
例如在一个打电话情境中,A和B不刚要知道自己的听筒和话筒是否正常,还要彼此知道对方的听筒和话筒是否正常,就是在进行验证通信双方的喇叭和听筒是否都好使
1)上面的这个环节接收方只是知道一半,只知道对方的麦克风和自己的听筒是好使的;
2)到了最后一个环节就知道了,咱们的四个设备双方都知道是好使的,咱们也就具备了可靠传输的基本条件;
如果网络中出现了问题,三次握手的过程都无法保证成功,那么也没有必要进行后续的传输,三次握手主要是判断一个主机是否有发送数据和接收数据的能力,如果连这些基本的要求都做不到,那这两台主机就没有什么必要再去进行沟通和通信了
3)TCP服务器中一些重要的状态:
1)listen:表示服务器启动成功,端口绑定成功,随时有人可以有客户端找来进行建立连接,手机开机,信号良好,随时可以有人可以给我打电话,创建好了ServerSocket的实例的时候,就进入了这个状态,等待客户端来进行进行连接;
2)establed:表名客户端和服务器已经建立连接成功了,随时都可以进行通信了
代码中accept返回,得到了一个socket对象,这是一个稳定的状态,说明连接已经建立好了,可以正式进行通信了,也就是说有人给我打电话,我接听了,接下来就可以说话了;
3)closed:表示客户端或者是服务器处于关闭状态
4)syn_sent:表示客户端连接请求已经发送,此时客户端进入阻塞等待服务器进入到确认应答状态,一般来说此状态存在的时间很短;
5)syn_rcvd:表示服务器已经收到了客户端的连接请求,发送ACK和SYN进入阻塞
4)四部握手可以吗?两次握手可以吗?
1)可以,但是没有什么必要,也可以得到投石问路的效果,这样比较麻烦,效率比较低,对多余的一次数据包进行层层封装和分用,传输的开销就会变大,中间的ACK和SYN是可以合在一起;
2)A-->B(syn),B--->(syn+ack)这样做可以吗?
3)B也无法知道自己的发送数据的能力是否正常,也无法知道A的接受数据的能力是否正常
4)两次传输意味着缺少最后一次,此时客户端知道两方的发送接收能力正常的情报是完整的,但是服务器这边是残缺的,服务器不知道自己的发送能力是否OK,也不知道客户端的接受能力是否OK;
5)也就是说此时此刻服务器对于当下是否满足可靠传输心里是没有底气的,这第三次交互,就是为了给服务器吃一个定心丸;
二)四次挥手:释放之前的系统资源
三次握手是为了进行可靠性传输之前的一个验证,四次挥手是为了释放资源,双方各自向对方发送了FIN,并且给对方一个ACK确认报文;
1)咱们的FIN叫做结束报文段,当我们标志位的FIN是1的时候,就意味着向对方发送了一个结束报文段;
2)三次握手过程,一定是客户端主动进行发起的,主动发起请求的一方叫做客户端;
3)但是咱们的四次挥手过程中,可能是客户端主动发起,也有可能服务器主动发起;
4)咱们的三次握手,中间两次是可以进行合并的,但是咱们的四次挥手,中间两次可能合并不了,也有可能可以进行合并,大概率是合并不了,结合上面的三次握手过程和四次挥手的过程,来进行理解一下;
1)为什么要进行四次挥手:
1)四次挥手是为了释放我们对应的资源,我们的三次握手过程,其实本质上来说就已经让客户端和服务器建立好了连接,但其实在建立好连接之后,我们在操作系统内核里面,我们就要使用一定的数据结构来保存有关于建立连接的信息,保存的信息最最重要的是,其实就是在保存咱们前面所说的五元组,占用系统资源;
2)咱们的客户端和服务器都是需要进行保存5元组的,源IP,源端口,目的IP,目的端口,协议类型,占用系统资源,你要是不进行保存,你都不知道自己自己到底和谁进行建立的连接,既然我们要保存信息就需要占用系统资源,比如说内存;
3)假设有朝一日,连接断开了,之前咱们保存的连接信息其实本质上就没有什么意义了,我们对应的空间也是可以进行释放掉了,也就是说把之前的记录进行销毁,就是为了释放咱们的系统资源,连接保存信息的内存就可以释放了,销毁了;
5)客户端和服务器都可以主动断开连接,三次握手主要是为了验证咱们进行可靠性传输的一个验证,而我们的四次挥手就是为了说释放资源;
6)咱们的三次握手的过程中,B发送的syn和ack是同一时机,因此就可以进行合并,此时B给A发送的syn和ack都是由操作系统的内核来进行负责运行的,是几乎同时进行发生的;
7)但是在四次挥手过程中,被动断开连接的那一方发送syn和ack的时机往往是不同的,B给A发送的syn,是操作系统的内核来进行实现的,在进行收到主机A发送过来的请求断开连接主机B就会立即做出响应,操作系统的内核就会将ACK返回给主机A;
8)但是B给A发送的FIN,是我们进行使用用户的代码来进行实现的,B的代码中调用了socket.close()方法,才会触发FIN,只有当我们的用户代码执行到close方法才会进行触发这完全取决于用户的代码是怎么写的,如果代码执行处理的逻辑比较短,那么就可能很快执行到close();
9)也有可能就是说代码可能会进行处理大量的逻辑,用户的代码可能出现了错误,造成了死循环之类的,这样我们的socket.close()可能就会永远无法执行了,可能FIN永远无法触发,从内核触发ACK和用户代码层面触发FIN这是两个不同的时机;
10)这两个操作,时间差比较大,就不能进行合并了,如果是时间差较短,这是有可能合并的延时应答和捎带应答;
1)A向B发送SYN(请求断开连接)
2)B只要收到A发出的FIN就会立即触发ACK(确认已经收到想要断开连接的请求),发送ACK,这是操作系统的内核可以快速做出响应,但是啥时候发送FIN是用户自己手动来决定,只要代码中出现了socket.close()这样的操作的时候,才会触发FIN;3)所以说,一个是内核触发的,一个是用户代码触发的,无法保证他们是在同时进行的,他们本质上没有关联关系;
就是之前咱们再写TCP客户端服务器代码的时候,咱们有一个循环代码
1)TCP服务器针对每一个TCP客户端都创建了一个clientSocket,单独的写一个procession来进行循环读取TCP客户端发送过来的请求
2)当客户端发送FIN的时候,咱们的服务器还在处于一个while循环里面,还想着客户端能够发送数据呢,里面有一个循环退出条件,while(!scan.hasNext()),当我们满足循环条件也就是说客户端断开链接了,进入到这个方法咱们的服务器就感知到了,客户端来给咱们发送syn了,所以说当咱们执行while(!scan.hasNext())方法的时候,就会立即返回一个ACK;
3)但是此时咱们服务器立即重传ack,这是操作系统内核进行完成的工作,可不会立即传输FIN,因为想要传输FIN,就需要执行climentSocket.close(),但是想要执行完这个方法,还需要处理完我们的if(!scan.hasNext())循环语句块里面的逻辑,调用break语句,才可以退出,万一在这一个循环里面出现了死循环,就无法发送FIN了
private void procession(Socket clientSocket) throws IOException { System.out.printf("我们这一次客户端请求的IP地址是%s 端口号是%d",clientSocket.getInetAddress().toString(),clientSocket.getPort()); try(InputStream inputStream=clientSocket.getInputStream()) { try (OutputStream outputStream = clientSocket.getOutputStream()) { Scanner scanner = new Scanner(inputStream); while (true) { if (!scanner.hasNext()) { System.out.printf("客户端断开连接%s %d", clientSocket.getInetAddress(), clientSocket.getPort()); break; } String request = scanner.next(); System.out.println(request); String response = process(request); PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); printWriter.flush(); System.out.printf("[客户端的端口号是%d 客户端的IP地址是%s],请求数据是%s,响应数据是%s", clientSocket.getPort(), clientSocket.getInetAddress(), request, response); } } catch (IOException e) { e.printStackTrace(); } finally { clientSocket.close(); //listenSocket.close();在这里面是不能进行关闭的 } }
1)但是实际上FIN的触发,本质上是内核里面释放了对应的PCB的文件描述符,比如你虽然没有调用socket.close(),但是Socket对象被GC回收了也是有可能会释放对应的文件描述符的
2)或者进程结束了,PCB都要被销毁,PCB的文件描述符表没了,进一步的文件描述符也就没了,这同样会触发FIN(停止客户端代码)
2)四次挥手过程中一些重要的状态:
1)ClLOSE_WAIT:谁被动接受FIN谁就进入到这个状态,B收到了FIN进入的状态,等待用户调用sockt.close,来发送FIN,四次挥手已经挥了一半了,在三次握手和四次挥手的过程中,同样是有可能会出现丢包的问题的
2)四次挥手过程已经过了一半了,是咱们的主机B在收到A请求的FIN做出ACK响应之后,没有发送FIN给A的这个状态,就是在等待代码中调用socket.close()方法,来进行后续的挥手过程,在咱们正常的情况下,一个服务器不应该有大量的CLOSE_WAIT状态,如果这个状态存在,那么就说明代码出现了BUG,close方法没有被执行到
3)如果在调试代码的过程中出现了大量的CLOSE_WAIT,这是说明socket.close()没有被及时地调用到;
2)TIME_WAIT:谁主动发起了FIN,谁就主动进入到TIME_WAIT状态,它所起到的效果,就是给最后一次ACK提供重传机会,为了处理最后一次ACK丢包;
如果是最后一个ACK丢了,B就收不到A传输的ACK了,因为咱们的B不知道是自己的发送的数据FIN丢了,还是应答报文ACK丢了,所以就会重新的进行传输业务数据,重传FIN;
1)四次挥手必须是四次吗?可以是三次吗? 有,延时应答和捎带应答(会将ACK和FIN是有可能合并在一起的)
2)四次挥手一定会执行吗? 不一定,四次挥手是一个正常TCP断开的流程,但实际上有的情况TCP也会异常断开,例如网线都断了
四)滑动窗口:提升效率
虽然上面的几种机制都是为了保证可靠性,但是TCP也要尽可能地保证效率,本质上来说,保证效率和保证可靠性是比较冲突的,要等待接收ack,这是需要时间的,但是TCP在保证可靠性的前提下,再来几种机制,努力的提升传输效率,提升性能;
咱们批量发送,一次发一波数据,一次等一波的ACK,这是就把等待多组ACK的等待时间重叠起来了,进一步缩小等待时间提高程序执行效率;
1)咱们滑动窗口的效果就是说在尽可能地保证可靠性的情况下,尽可能的提高传输效率
2)况且咱们进行发送滑动窗口的本质就是说进行批量的发送数据,咱们尽可能说是把等待ACK的时间总体进行缩短,咱们可以把等待一份ACK的时间变成等待多份ACK的时间,速度提升了,等待ACK的时间,甚至少等一些ACK;
3)可以看到,由于确认应答机制的存在,这就导致了当前每执行一次发送操作,都要等待上一个ACK的到达,只有ACK到了,发送方才可以进行发送下一次传输的数据,大量的时间都花在等ACK上面了,整体等待花的时间多了,那么在单位时间内传输的数据量就变少了;
4)ACK:确认应答机制里面的,接收者告诉发送者自己受到的那些数据,里面有序列号,发送方要花费很多时间来进行等待,这个过程就消耗了大量的时间,所以我们就要想办法批量去发送,批量去等待;
5)例如A向B发送了1-1000的数据,B返回一个确认序号,A就会进行等待,直到A收到这个确认序号,这个确认序号的意思就是说B告诉A我收到了1-1000B在向A索要1001-2000的数据,如果没有收到ACK,就会进行重传;
窗口大小:一次批量发的数据的长度,称为窗口大小,如果没有批量发送数据的长度限制,完全不等ACK就一通发,其实就没有可靠性而言,窗口越大,整体的效率就会越高,但是可靠性会非常低,但是可以窗口越小整体的效率就会越低,但是可靠性也会越高
1)当前窗口范围就是1001-4000,也就意味着发送方现在同时发送了1001-2000,2001-3000,3001-4000,发送这四组数据的过程中不会等待ACK,这四组都发完了,发送方统一等待4组数据的ACK,把一份等待一组ACK的时间,变成了一份等待多组ACK的时间,把等待多组ACK的时间给压缩成一份,TCP是要保证可靠传输的,而保证可靠传输的灵魂就是确认应答,如果说没有这个ACK,可靠传输就形同虚设,等,是必须的,只能让他少等一会,如果一次发送的批量数据是N,统一等待一波,此时这里的N就被称为窗口大小
2)滑动的意思是,并不需要把N组数据的ACK都等到了,才继续往后发送,而是收到一组ACK,就继续往下发送一组;
3)这时依然在等待四份数据的ACK,并不是在等待四份数据的ACK都到达了,才发新的数据,而是随时收到ACK,就随着向后发送;
此时向服务器发送的数据是1-4000的数据,进行批量发送,发送之后接收完数据,当前的客户端是在进行等待1001,2001,3001,4001四组ACK,咱们的客户端不需要等到4001来了之后才继续向后发送数据,等到一个之后就向后发送一组新的;
4)此时只要1001到了,我们就可以立即向后多发一组(4001-5000),此时等待ACK的范围就是2001,3001,4001,5001,如果是2001这个ACK到了,我们就认为1001-2000之间的数据我们已经收到了,我们也可以继续向后多发1组数据(5001-6000);
5)此时等待ACK的范围就是3001,4001,5001,6001,每一次等到一个ACK,就向后滑动,窗口向后滑动,咱们向后多进行发送一组数据,咱们要等待ACK的范围会发生改变;
1)滑动窗口,这时传输ACK会存在后发先致的情况?
1)ACK 1001 2001,3001,4001都在网络上运输呢不一定非得是1001先到呀,可能是3001,4001先到呀,这样也没有关系;
2)确认序号表示,从该序号之前,前面的数据已经接收到了,如果收到了4001这个ACK,说明0-1000,1001-2000,2001-3000,3001-4000的数据已经收到了,此时4001前面的这几组ACK收或不收已经不关键了,然后滑动窗口向后滑动三格,然后直接发送4001-5000,5001-6000,6001-7000的数据,7001-8000的数据,
3)当前这个滑动窗口越大,说明就可以认为传输速度越快,窗口大了,同一时间内需要等待的ACK就越多,总体等待ACK的时间就少了;
发送方连续进行发送了0-1000,1001-2000,2001-3000,3001-4000,4001-5000
4)咱们的接收方接收这5类数据之后,发送发就会进行批量等待5组数据的ACK,但是其实发送方们只需要进行等待5001这个确认序号就可以了;
5)发送方收到5001的时候,也就意味着1-5000的数据已经收到了,咱们的1001,2001,3001,4001被进行丢包是毫不影响的,只要我们收到了5001,就已经涵盖了3001和4001所进行表示的信息,我们的窗口就一下子向后面挪了5个格子;
2)滑动窗口过程中出现的丢包问题:
2.1)返回的ACK丢了:
现在发送0-1000,1001-2000,2001-3000,数据包已经抵达,ACK却丢了
1)这种情况不打紧,只要不是全丢了就好,发送方如果1001这个ACK丢了,但是3001这个ACK到了,这此时也就知道了确认序号3001之前的数据应该都到了,肯定0-1000的数据都已经收到了,此时3000前面的数据已经接收到了 ,这个时候再是否收到1001这个ACK数据包,其实已经没有必要了;
2)实际上有时TCP为了偷懒,滑动窗口下并不是每条数据都有ACK,有时会个几条才会发一个ACK;
2.2)直接发的数据就丢了 ,仍然需要重传(ACK不处理还可以)
1)假设主机A向主机B发送0-1000的数据包丢了,B返回的确认序号就永远是1001这个数据,即使A给B已经向后发送了若干个数据包,此时仍然是在索要1001;
2)索要若干次之后,A就明白了自己丢包了,就会触发超时重传机制,重新发送0-1000的数据包;
因为咱们在这里面的重传只是需要把丢了的那一部分在进行重传了即可,其他已经到了的数据就不需要进行重传了,这样咱们的整体的重传效率就会变得比较高了,这样的重传称之为快速重传,这个快速重传就是类似于看电视剧,以前大家看电视剧是真的是用电视看,每一天都是固定时间播放的,如果某一天有事了,其中某一集没有看成,只能继续往后看,过两天,发现另外一个电视台,也在放这个电视剧,就可以通过另外一个电视台,把缺了的那一集给补完,此时就可以了
这时1001-2000发的数据包丢了,因为1001丢包了,主机B返回的ACK就会一直索要1001,确认信号就一直是是1001,1001-2000丢了,B就会给A返回一个ACK,由于包丢了,就会一直返回,我收到0-1000的数据,我想要1001之后的数据
1)A向B发送了1-1000的数据,B返回一个ACK,表示收到了1-1000的数据,还向A索要1001之后的数据;
2)A发送1001-2000的数据,B没反应,不会返回ACK;
3)A在发送2001-3000的数据,B在A不丢包(1001-2000的数据)的情况下会返回一个已经接收到了2001-3000的数据,确认序号是3001的ACK;
4)但是此时A丢包了,B返回的ACK就变成了我已经收到了0-1000的数据,我还是索要1001-2000的数据,但是实际上2001-3000的数据确实是已经存放到接收方的接收缓冲区里面了;
5)这时A还会发送2001-3000的数据,B确实收到数据了,但返回的ACK确认序号还是1001,6)这时A还会发送3001-4000的数据,但此时B返回的ACK仍然是向A索要1001-2000的数据;2001-4000的数据已经让B存放在接收缓冲区里面了,但是此时的确认序号仍然是1001,但是0-1000的缓冲区和2001-3000之间还是有空位置的,此时因为1001-2000的数据没到,本属于他的缓冲区是空着的;
6)这时A就知道自己已经丢包了,就会给B再次发送1001-2000的数据,此时B收到了0-4000的数据,就会知道4001之前的数据已经收到了,这时返回的ACK中的确认序号就变成了4001,就是我已经收到了0-4000的数据,就会再次向A索要4001之后的数据了;
我们在这里面的重传只需要把丢了的那一块数据给重传了就可以了,其他已经到了的数据就不需要进行重传了;
五)流量控制:限定滑动窗口发送速率的大小,保证TCP的可靠性,16位窗口大小,里面就包含了接收缓冲区的剩余大小;
进行流量控制的关键,就是说可以进行衡量接收方的处理速度,咱们在此处就直接使用接收方剩余的接收缓冲区的剩余空间大小来进行衡量当前接收方的应用程序的处理能力和速度;
流量控制这件事情,就是根据接收方的处理能力(接收缓冲区的空余于空间大小),来动态决定发送方的发送速率(控制滑动窗口的大小);
1)接收方的处理能力是通过接收方缓冲区的剩余容量来衡量的, 接收方缓冲区的容量剩余多少, 下次发送方的窗口大小就是多少
2)当发送方的数据到达接收方的时候, 接收方都会返回一个ACK,这个ACK除了确认能够确认应答, 还能告知接收方缓冲区的剩余容量, 然后发送方就会根据接收方缓冲区的剩余容量来控制发送速度(窗口大小)
3)当接收方得知接收方缓冲区空间满了的时候, 就暂时不会发送数据了, 而是会定期去给接收方发送一个探测窗口报文, 这个报文不携带具体的业务数据, 只是为了触发ACK查询接收方缓冲区的剩余容量.
1)机制的本质目的是在控制滑动窗口的大小,是想要保证可靠性的,窗口的大小决定了传输的效率,窗口越大效率就会越高,单位时间批量等待ACK的时间就会越来越多;
2)窗口越大,资源开销就要更多,窗口越小,效率就得不到保证,发送方发送的贼快,接收方根本处理不过来了,接收方就会把新收到的包给丢了,发送方是不是还得重传;
就是怕咱们的接收缓冲区把数据挤满了,应用程序处理不过来,操作系统内核就把数据包给丢了,数据包一丢,咱们还需要进行重传,没有必要;
3)此时的流量控制就是基于接收方的一个处理数据的能力来限制窗口大小的
TCP这个数据的传输过程类似于生产者,消费者模型
1)主机A在给B发送数据,到达了B的接收缓冲区,此时主机A就是生产者,B的应用程序通过SocketAPI来读取数据,被SocketAPI读取的数据,就从缓冲区删掉了,此时应用程序就是消费者,接收缓冲区就相当于消费场所,就相当于是一个阻塞队列,缓冲区里面的数据可以随时被B里面的应用程序通过SocketAPI来进行读取);
2)此时说的窗口大小,就是发送方批量发送的数据有多少,例如主机A发的数据很多,窗口很大,此时接收缓冲区的数据也会增长很快,如果主机B的应用程序读的数据很慢,随着时间的推移,接收缓冲区逐渐满了,如果此时不加任何限制,主机A还是按照原来的速度发,不加任何限制,这时新来的数据没有地方保存了,就被内核丢了;
1)如果说剩余空间比较大,那么就认为B的处理数据的能力就比较强,就可以让A发送的快点
2)如果说剩余的空间比较小,就认为B的处理能力是比较强的,叫可以让A发送的慢一些
1)例如,接收缓冲区的大小现在是4000,当1-1000的数据到达的时候,缓冲区这里用了1000,还剩3000,返回的ACK就会把这个信息发回去,发送方再次发送数据的时候,就会按照3000这个滑动窗口来发送数据;
2)如果窗口大小变成0了(接收缓冲区已经满了),这是发送方就啥也不干了么?
不是的,发送方此时是不会再发送数据了,但是为了可以查询当前接收方的窗口大小,每隔一段时间还会发一个窗口探测包,通过这个包(不会传输具体的业务数据),主要目的是触发接收方的ACK,当发送方知道现在就知道当前窗口的大小了,然后再次发送数据
3)为什么TCP传输数据的大小不会受到限制呢?
正常来说,16为窗口大小最大的数字表示65535,那么TCP的窗口大小最大就是65535字节吗,64K,TCP是传输层协议,在TCP首部40字节选项(在选项中)中包含了一个窗口扩大因子 M,实际上的窗口大小是窗口字段的值左移M位;
1)流量控制是根据接收方的剩余缓冲区大小来进行确定滑动窗口的大小,但是拥塞控制是把中间的链路看成了一个整体,看看中间数据链路的拥堵情况,根据传输数据快慢以及效率来进行控制窗口大小;
2)虽然TCP有了滑动窗口这个大杀器,可以相当可靠的发送大量的数据,但是如果在刚开始的阶段就发送大量的数据可能会引发问题,网络上有很多的计算机,可能当时的网络状态很拥堵,若不清楚网络状态,贸然发送大量的数据,很可能会导致网络系统瘫痪
3)本质上是在另一个角度上来控制发送方的窗口大小,他是站在一个宏观角度来看待问题,他是把中间的链路都看成了一个整体,只看结果,是一个不断尝试的过程;因为我们的A和B的中间节点,有多少个,咱们也不知道,就很难对这些设备一一进行衡量
流量控制针对的是接收方一个元素针对的,拥塞控制针对的是整体路径进行限制,它们会动态变化的,真实的窗口大小取流量控制的窗口和拥塞控制的窗口他们的较小值;
正式的窗口取流量控制窗口和拥塞控制窗口的较小值,是希望滑动窗口的大小是出现在阈值和网络拥塞窗口(丢包窗口)之间,这样就做到了尽量少丢包,况且传输效率还特别高
六)拥塞控制:
数据传输的效率,不仅仅取决于接收方滑动窗口的大小和接收方接收方缓冲区处理数据的能力,还和传输路径以及中间链路连接有关;
1)先用一个较小的窗口来传输数据,看看是否丢包,如果不丢包,说明网络通畅;如果丢包,说明网络拥堵;当我网络通畅的时候,就加大传输速率;如果当前网络不丢包的情况下,就立即降低发送速率,通过这样的方式,就可以直接试出来当前最合适的窗口大小;
2)当A开始的时候会以一个较小的窗口来发送数据,如果说数据很流畅的就到达了就逐渐增大窗口大小,如果加大到一定程度之后,出现了丢包,就意味着通信链路出现拥堵了,这个时候我们在减小窗口;
3)通过反复的增大和减小过程,逐渐就摸到了一个合适的范围,拥塞窗口就在这个范围内不断地发生变化,达到动态平衡;
1)最期望的理想滑动窗口效果,就是说在阈值和丢包窗口之间,在这个窗口之间就可以保证既不进行丢包,传输速率也是比较快的;
2)咱们拥堵的值是啥,就是完全取决于这一次传输是否丢包,如果进行丢包了,那么就说明视为拥堵,也不一定,反正会出现多次重传;
3)咱们就直接可以通过拥堵的值来进行确定更新下一次阈值的这一个过程,不是阈值影响到了拥堵的值,而是我们拥堵的值影响到了阈值,下一次拥堵的值是如何进行改变的?拥堵的值就是完全取决于咱们网络上面的一个传输数据的效率,这个值还是和网络整体环境是有关系的,所以拥堵的值也是在实时变化的,因为我们一开始的网络传输的时候拥堵值就是不确定的,所以下一次的阈值也是不确定的;
4)刚开始启动的时候,窗口比较小,此时通过指数增长,就可以在很少的轮次(发送的次数)上把窗口大小给顶上去,刚开始的窗口比较小,慢慢达到增长到即将丢包的最大窗口,实际情况既希望速度快,又希望不丢包,我们只能增加滑动窗口接近丢包的极限,又没真丢;
初始情况下我们给定的窗口太小了,可能合适的值是一个更大的值,通过上述过程,我们就可能接近一个更合适的值,指数增长到一定程度就会变成线性增长,滑动窗口已经快打到极限了,此时如果指数级增长,可能来个2倍,就直接超过了最大范围,线性增长也是增长,增长到一定程度,就会出现丢包的问题;
5)一旦发生了丢包的情况,此时发送方就立即会让窗口变,回归到刚才的初始情况下的窗口大小,继续重复刚才的指数增长+线性增长,一大块就减没了;
1)这就类比于两个人谈恋爱,在一开始的时候两人的感情呈现指数级增长,之后呈现线性,稳步上升,两个在一起时间长了,一吵完架,感情迅速降到级点,对彼此很失望;
2)一段时间之后,两个人都彼此发现自己有不对的情况,感情又迅速升温,最终还是会线性增长,但是这一次增涨到极点的过程也就是阈值还是比较短的,至少比第一次短;
1)第二次滑动窗口从0开始增长的阈值就是第一次滑动窗口达到的最大值的一半;
2)这个阈值就决定了,咱们窗口大小啥时候从指数增长为线性,这个阈值也不是永远固定不变的,每一次出现丢包,阈值就变更新为当前出现丢包的窗口大小的一半,就想要通过阈值来进行描述一下线性增长的过程;
3)通过拥塞窗口来进行控制滑动窗口的大小;
4)如果达到阈值,就会从原来的指数增长变成线性增长,增长速度变慢,因为阈值已经快接近丢包的范围了,传输轮次是传输的次数,当传输伦次达到13次的时候 ,就会出现网络拥堵,然后窗口的大小又会变成最小值,阈值会发生变化;
7)延时应答:琢磨窗口大小,相当于是流量控制窗口的一个延申(16位窗口变大)
1)咱们的流量控制是踩了一下刹车,想让发送方,发送的不要太快,延时应答,就是在这个基础上面,能够让窗口变得更大一些,想让咱们的发送方多发送一些数据;
让窗口大小保证在可靠的基础上,效率再高一些,窗口争取再大一些,流量控制就是说,窗口大小是接收缓冲区的剩余空间大小,会随着时间的推移接收方的应用程序会不断地进行消耗缓冲区里面接收的数据;窗口越大,网络吞吐量就越大,传输效率就会越高,我们的目标是在保证网络环境不拥堵的情况下,尽量提高传输效率;
2)在A发数据的时候,本应会返回一个ACK,但是这时缓冲区的大小可能会发生变化,可能会减少很多,这时B就对数据的需求量更大了,所以我们可以让ACK等一会再发出去,在B向A发ACK的这个过程中,B可能会消费的更多,缓冲区的剩余就更多,这时下次A再发的数据就可能会让缓冲区填不满,所以说咱们就等一会再来发送ACK,延时应答让最后传输的ACK中的16为窗口大小就会变得更大;
咱们再进行举一个入水和出水的例子来进行理解一下:
1)我们在进行注水的同时也在出水,每一次在进行注入一波水的时候,就会进行询问水池中的剩余空间有多少;
2)此时我们采取的策略就是不立即进行回答,而是稍微晚一会再进行回答,我们稍微迟一点进行回答,就是为了在迟一点的这个延时时间里面,就会排出更多的水
3)如果立即进行回答,那么我们可能会回答一个剩余20吨水,但是我们稍微等一会回答,也就是说延时一会回答,就可以说剩余5吨,我们在借助延时的这个时间里面,又出了15吨水,所以说我们在下一次进水的时候就会进更多的水;
4)所以说我们的这个操作就实在有限的情况下,又尽可能地提升了一点运行传输速度,既想保证可靠性,也想保证效率,发送给多的数据;
咱们如何来进行延时应答呢?
1)每隔N个包就来进行应答一次
2)超过最大延时时间就进行应答一次
八)捎带应答:
捎带应答:捎带应答本身是一个概率性的机制,当前ACK的延长时间正好要比接下来发业务数据的时间要长一些,取决于代码具体的实现,内核中只要收到数据就会立即返回ACK,这是应用程序进行的响应,捎带应答本身就是延时应答的延申;
咱们用浏览器上网打开网页用的就是一问一答的模型,一个请求就是一个响应
1)内核返回的ACK和应用程序,用户代码返回的响应,是有一定的时间间隔的因为这两个东西是不同时机进行触发的,两种数据包本来进行返回的时间间隔是不同的,所以说在本质上就不应该进行合并;
2)但是有了延时应答的存在,导致我们的ACK是不会立即返回的,而是要稍微等一会返回,让我们的接收方的应用程序多消耗一下接收缓冲区里面的数据,才可以进行返回的16位窗口大小变得更大些;
3)ACK正好在这里等一会的过程中,服务器要返回业务上面的请求了,正好可以把这个ACK和response合成一个包,因为在网络通信过程中,对于数据涉及到大量的封装和分用,收到之后还要进行解析,时间成本也高,效率也低;
3)之前我们说过,四次挥手是有可能变成三次挥手的,但是具体的概率是不知道的
捎带应答是建立在延时应答的基础上的 ,他是有可能把中间的ACK和FIN合并到一起的,例如在服务器收到请求并返回响应,这个过程中消耗50ms,但是延时应答ACK最多等待20ms,这个时候就无法触发捎带应答了;
4)所以说它本质上是一个概率上的机制,但是此时假设延时应答ack最多是等60ms,第50ms的时候,此时处理完数据,代码执行逻辑完成响应,这个时候ACK就可以和FIN一起过去了,也就同时触发了延时应答和捎带应答;
这是延时应答的一种情况,假设左边是A,右边是B
1)正常来说A再给B发送业务数据的时候,B的操作系统内核会立即返回一个ACK,但是由于延时应答的问题,咱们的ACK要等一会发,等一会是为了让B消耗了更多的业务数据,让B的接收缓冲区变得更大,让返回的ACK里面的16位窗口大小变得更大些;
2)业务数据,用户操作代码执行逻辑实现的,但是在等待过程中,业务数据处理完成了,那么这个ACK就会和响应报文一起发送给对方,这就是演示应答的基础上发生了捎带应答
九)TCP面向字节流:
面向字节流,可能会出现粘包问题,粘包问题,主要指的是粘应用层的数据报,咱们的TCP数据报中的载荷里面放的就是应用层数据;
咱们不仅仅是TCP存在粘包问题,而是说所有的面向字节流的机制都是存在着粘包问题的,比如说读文件;
1)TCP粘包主要是粘的是应用层的数据包,在咱们的TCP缓冲区里面,若干个应用层数据包粘在一起了,分不出谁是谁了;
2)本质上就是说,指数据在进行传输的时候,在一条消息中读到了另一条消息的部分数据,这种现象就叫做粘包,咱们的TCP是面向字节流的传输协议,流是没有明确的开始和结尾边界的,所以会出现粘包问题;
1)接收方收到这些数据之后,进行分用,先把TCP数据包进行解析,解析过后,就会把这些数据部分放到接收缓冲区里面;
2)应用程序通过read方法从接收缓冲区中读数据的时候,就不知道从哪里到哪里是一个完整的应用层数据包了;应用层取的就是一个个完整的字节,应用程序此时只能看到接收缓冲区中的一个一个的字节;无法区分当前应用层有多少个应用层数据包,以及从哪里到哪里是一个完整的应用层数据报;他不是TCP特有的问题,而是面向字节流共有的问题;
3)TCP的报头没有长度的,当前我们的接收缓冲区,可以视为是分用后的数据(当前已经把TCP报头给去掉了,缓冲区里面是没有TCP报头的),或者SocketAPI拿不到,从应用程序角度来看就是一个一个的字节
4)那么如何解决这个粘包问题呢?应该通过再设定一个合理应用层协议来进行解决
1)不知道两个应用层数据之间的界限
2)解决方法的关键是要在应用层协议这里,加入包之间的界限
3)粘包问题虽然是在TCP这里面被提及,但是他从本质上来说还是一个应用层的问题,咱们的粘包问题本身对于TCP协议没有任何影响,但是这个粘包问题导致了说基于TCP的应用层协议会出现一些麻烦,应用程序就不知道一个应用层数据到另一个应用层数据之间的界限了,会对我们的应用层产生影响;
解决粘包问题: