程序员最关注的也就是应用层,在应用层中程序员可以自定义协议,设计应用协议就是约定前后端的交互接口,写程序的时候,会涉及到前端和后端进行通信,那么我们就需要约定好前端发出什么样的请求,后端就需要返回啥样的数据,在http协议中就用到了Ajax和form标签,这两个都很重要,是前后端产生联系的接口。
设计一个应用程序可以自己从零开始设计,也可以进行二次开发。
如何设计应用层协议尼?
1、考虑清楚客服端和服务器之间要传递那些信息 (需求而定)
2、确定好信息之后,需要明确协议格式 (最好用已经发明好的协议模板往上面套)
1、端口号取值范围:
> 0~65535 -----> 64k
> 这里的64k在互联网程序中数据是非常小的,解决办法:1、手动对应用层进行拆分;2、直接使用TCP协议
校验和:☆
1、是用来验证数据是否正确的一种手段。
2、但在网络传输的过程中,受于自然干扰,导致可能会破坏原有要传输的数据信息 (光/电信号,太阳黑子/太阳风暴…),这样导致其中的bit发生改变(比特翻转),那为了检验出数据是不是有误,可以结束校验和来检查。
3、也有解决办法就是用ECCC内存,这个内存能够对bit翻转进行应对,一般用在服务器上,个体用就ECCC内存就太贵了。
4、校验和既验证数据个数,也验证数据内容。
如果最后结果是相同的,数据就没有发生变化;输入的数据相同,算法相同 ,得到的校验和结果也就相同;输入的数据不同,算法相同,得到的校验和结果大概是不同的。
校验和可以通过第三方工具进行计算,不需要自己手动的计算。
这里重点介绍TCP协议特性中的 可靠传输
。
TCP的核心就是解决“可靠传输”问题,而我们的UDP是无可靠传输
,(可以简单的理解TCP最后快递是否寄到目的地,我们是知道结果的,反而UDP在寄快递的时候,快递丢没丢失,是否到达目的地我们是不知道的)TCP会反映结果,UDP是不会反应结果。
TCP的可靠也不是说数据100%被对方收到,而是发的数据,对方收没收到数据,是会感知到的。
发送方又是如何知道接收方,到底是否收到数据尼???
当发送一条消息的时候,对方是会回复的。这样看似没有问题,但是这种确认机制,存在一个很大的问题,连续发消息会发生上什么??
正常的逻辑是会已上图所示,但是网络上传输的数据顺序性是不能保证的,可能也是后发先置的情况。
网络上的传输是复杂的 ,可能在回复第一条消息的时候进行了堵塞,延迟,导致第二条消息先送达,就会影响我们的正确理解。
为了解决这样的问题,就引入了一个重要的概念“序号”
加上编号之后,就不会影响理解正确的意思了;在编号中带上的编号,称为“序号”
,应答文中的编号,称之为“确认序号”
,他两之间存在对应的关系。
确认序号
表示,该序号之前的数据会被收到了,可以直接从1001序号开始发送数据。
有了确认应答,就更好的保证了TCP的可靠性,可以让发送方清楚的知道,对方是否收到,但是万一有某个数据丢了,还是要想办法处理一下的。
当发送的一条消息对方没有回复,可能出现两种情况:
1、我这边发的信息,直接发丢了;
2、对方看到了我的信息,给我回复信息的时候,信息丢了。
这两种情况,我们是区分不了的,既然区分不了,那就认为是我方的过错,等10分钟(超时),如果我放还是没收到对方的信息,那么我就重新再发一次!这就是所谓的“重传”。
此时这种情况对方就收到了两次一模一样的信息,对于人来说,这样的数据重复,是没有影响的,但是对于计算机来说,就会出现问题,例如发的是一个转账请求,如果产生超时重传,这不得发两次转账(达咩)。
但在TCP中也是会自动去重的功能,发送过去的数据,先放到接收方的内核中的缓冲区(是以阻塞队列数据结构实现的),每一段消息的都带有序号,如果新来的一个消息序号和之前缓冲区中的序号重复了,TCP直接就会进行去重操作。
应用程序从接受缓冲区中去数据的时候,可能不是一个重复的数据,调用 socket api 得到的数据,也是不重复的!
如果重传的数据也丢失了尼!!!
那么TCP有做了对应的处理:
1、重传不会无休止的进行,重传一定的次数,就会放弃重传,认为连接不可恢复,断开TCP连接。
2、重传的时间间隔,是不相同的,每次重传的时间间隔都会变长,再次丢失数据,那么认为恢复的概率就很小。
发生了两次数据丢包,意味着大概率的情况下,当前的网络丢包率非常高,可能网络遇到了严重的故障。
TCP在进行上诉的确认应答和超时重传机制上,可靠性就得到了良好的保证
这里下面就是面试的八股文了。都很重要的
这里的连接管理,是讨论TCP是如何建立连接的,TCP又是如何断开连接的。
这里就涉及到了TCP是经过三次握手建立连接,四次挥手断开连接。
TCP协议格式有6个特殊位:
这里的六个比特位,在TCP中是特殊的六个标志位:
1、ACK
:acknowledge(挥手) ,当这一位 为 1 时,表示当前发送的数据,“确认报文段”就表示是对刚才收到数据的应答。
确认应答机制中的“应答报文”同样也是ACK这一位为1.
2、SYN
:synchronize(同步),当这一位为1时,表示当前发送的这个请求,是“同步报文段”,是要和对方建立连接。
3、FIN
:结束报文段
A想和B建立连接(客服端主动发起):
1、SYN发送想和对方建立连接的请求;
2、B收到了A的SYN做出了应答ACK;
建立连接是需要“双向奔赴”的
3、B也发送SYN请求想和对方建立连接;
4、A收到了请求做出了ACK回应;
这是建立连接的整个过程,但是我们可以简化,把2,3给合并起来,就形成了三次握手。
ACK和SYN都是内核自己触发的,属于同一个时机。
❓❓❓ 为啥中间要合并???
1、分两次发送效率太低了;
2、分两条发,需要正对两条数据,分别进行封装和分用,实际上这两条数据刚好可以合并,也就合并了。
❔❔❔ TCP已经有了确认应答和超时重传保证了可靠性,为啥还要三次握手?握手有什么用?
1、三次握手其实是一个前提条件,先确认当前网络环境是否具备连接,进行可靠的传输。
2、同时也能协商一些重要的参数(通信双方是需要一些共同参数进行配合的)
如果网络不通畅,大概率的出现丢包和重传
三次握手进一步验证,通信的双方发送能力和接收能力是否都是正常 的。
这也是保证TCP可靠传输的机制,但他是一个前提条件。
断开连接也是双向远离的过程,主要是通过 FIN
这一个特殊位。
通信的双方各自向对方发送 FIN(结束报文),再各自返回ACK响应。
这里中间一般是不能合并到一起的,但是在特殊情况之下是可以合并
触发FIN
的情况:
1、手动调用 socket.close()
2、强制退出进程
当客户端触发FIN之后,服务器只要收到FIN,就会立即返回ACK(内核完成);当服务器的代码中运行到 socket.close()操作的时候,就会触发FIN。
这是两个不同的时机,中间带有一定的时间间隔
CLOSE_WAIT && TIME_WAIT:
在TCP中会向多线程一样有多种状态,但是我们重点关注 CLOSE_WAIT
and TIME_WAIT
这两个状态。
1、如果服务器那边没有调用close方法,此时就会看到 CLOSE_WAIT的状态(如上图的右边是服务器,左边是客服端)。
2、如果服务器那边看到了 lots of CLOSE_WAIT,就说明服务器出现了BUG,就是代码写的有问题。
1、谁主动断开连接,谁进入 TIME_WAIT 状态
建立连接,一定是客户端主动发起;
断开连接,可能是客服端,也可能是服务器主动发起断开
2、TIME_WAIT会脱离进程的存在,即使进程结束,TIME_WAIT状态可能仍然存在,存在的目的就是为了处理最后一个ACK丢包之后重传的问题。
所以最后在客服端回复完最后ACK的时候,别先释放,先等一段时间,确保FIN不被重传了,就可以真正释放连接了。
TIME_WAIT等待的时间,叫做 2MSL (MSL是网络上两点之间传输消耗最大的时间,此处的MSL的值是可以配置的)。
TCP的可靠性已经得到了保障,但是TCP在可靠性的基础上,提高效率。
滑动窗口是提高TCP传输效率的重要方法。
要知道客服端每次给服务器发送SYN
,都要等待服务器返回ACK
,等待时间就会让速度效率就会减慢,滑动窗口就是减少无效的等待次数,提高速度效率。
我们就通过一次性发一批SYN的操作,然后等一批ACK的操作,这样一份时间等待多组ACK提高了速度效率。
等待的过程中ACK也不会同时到达,是有先有后的,假如2001先应答,那么在12000的数据就被对方已经收到了,接下来A就可以立即发出500160000。
这里一次发送长度为 N 的数据,然后等一批ACK,那么这个N就成为“窗口的大小”
。
N越大,传输的速率也就越快,但是N是有界限的,不能无限大。
所谓“滑动”又是什么意思???
由图可以看出,当主句A接受到ACK的时候,那么窗口就会往后滑动,等待数据的范围也就会变化,这种显现称之为“滑动”。
虽然 滑动窗口 减少了等待次数,提升了效率,但是仍然无法跟UDP无确认应答的速度相比。
传输数据大多数都会有丢包的情况,既然滑动窗口是一批一批的操作,如果放中间数据丢包了咋整???
1、如果是ACK丢包,影响是不大的。
这张图可以看出,中间多次ACK丢包,但是我们TCP是有确认序号
概念的,当1001ACK丢了,但是后面6001这个ACK送到了,说明6001之前的数据也就顺利是被收到了,这个丢包关系就不大了。
2、如果是SYN丢包了,那么是会出发重传的机制的
由图可以看出,SYN发的1001~2000 这一段的数据丢包了,这个主机A一直连续发很多的数据,但是主句B一直想主句A索要 1001 这一段的数据,那么主句A收到3次同样的确认应答都是1001,那么主句A就意识到,1001这个包的数据丢失了,触发重传机制,主句B得到了缺的1001这个数据,就会把这个数据补上了,直接返回7001就行了,因为A一直发数据的,B都把这些数据记录在接受缓冲区中,当缺的补上了,就发现接受缓冲区里面7001之前的数据都齐,于是直接ACK7001.
这些都是用到了确认序号
和 超时重传
重要机制,
也是一种保证可靠性的机制
之前也说了,虽然滑动窗口的“窗口”越大,传输效率就越快,但是并不是无限放大的,就越好,如果说发送方发的太快,接受方顶不住,其余额外的数据也是大概率会丢包的,丢包就触发超时重传,我们要找到合适的发送速率和接受处理速率相匹配才行。
流量控制
就是根据接收方处理数据的能力,来制约发送方的滑动窗口
大小
所以说发送方的滑动窗口大小是变化,不固定的。
衡量接收方的处理速率,处理的速率取决于应用程序,调用socket api 的读取操作(read)数据的速度。
在更详细的的衡量处理速率,那么通过接收缓冲区中剩余空间的大小,来衡量。结社接收缓冲区一共是4k,当前使用3k还剩1k,此时接收方返回ACK的时候,告诉发送方,我们缓冲区还剩1k的空间了,你们悠着点,接下来,发送方就可以按照1k这样的窗口来发数据。
conclusion:通过接收缓冲区剩余的空间大小来衡量发送窗口的大小。
何为接收缓冲区:
每个主机上都会有用户态和内核态,发送数据的时候,都是内核态之间建立TCP连接(三次握手,四次挥手)发送数据,完了之后,都是用户态去取内核态的数据;接收缓冲区就在内核态中,这个接收缓冲区使用阻塞队列实现的,数据都放到了缓冲区中,代用 socket api来读取其中的数据,只要应用程序读了数据,此时接收缓冲区中对应的数据就可以被删除掉。
具体关于socket api的调用,网络编程可以看 网络初始&网络编程 这篇文章。
极端情况:
接收方的接收缓冲区满了,窗口大小为0,这时先让发送方暂停不要发送数据,接收方没有收到发送的数据,那么就不会返回ACK。但是当接收缓冲区已经腾出空间了,没有返回ACK,说明发送方就不能及时感知到。
怎么解决?
为了让发送方及时感知到,窗口已有剩余,发送方就会定时给接收方放送一个“探测报文”,这个探测报文不传输实际的数据,只是为了触发ACK,只是为了更新,接收方的窗口大小。
它也是可靠性的机制
拥塞控制是根据发送方到接收方这一系列通信链路
的处理速率来衡量的。
在主机A和主机B之间网络通信这个过程会经过很多的设备,各式各样的路由器,交换机…,这些设备是为了转发数据的,传输的数据一方面会受到中间节点的处理能力的影响,另一方面会受到主机B之间的处理能力影响。
由于整个链路中的每个节点的情况是非常复杂的,如果中间某个链路节点出现问题,就不能快速的转发了,我们只需要把整个链路看做一个整体即可,然后通过“实验”的方式,不断的尝试不同的窗口大小,保证可靠性的前提下,获取合适的窗口大小。
拥塞控制会设置出一个“拥塞窗口”这样的指标,通过拥塞窗口来影响到滑动窗口的窗口大小。
这个图反应了“拥塞窗口”变化规律:
1、刚开始传输的时候,慢开始;
2、指数规律增长,短时间内窗口大小加速提升;
3、指数增长到一定数值的时候(超过设定的國值),进入线性增长;
4、遇到了网络拥塞,丢包,立即让窗口大小回到最初很小的值,同时修改线性增长的國值,为刚才窗口大小的一半。
5、继续重复刚才的步骤。
这个图也描述了拥塞窗口是动态变化的,这样的动态变化是有意义的,这种变化可以更灵活的解决网络传输容易出现的一些突发情况。
考虑在可靠性的前提下,尽量把这个窗口大小给变大一些
当A给B传数据的时候,会把数据放入B的接收缓冲区中,B也不停的在接收缓冲区中取数据,B接收到A的SYN不要立即返回ACK,等一个500ms,B就会从接收缓冲区中取出一部分的数据,接收缓冲区就空出来了一定的空间,这时再次发出ACK,那么ACK的报头返回的接收缓冲区就会比立即发出的接收缓冲区更大,那么SYN传输的窗口大小也会更大。
窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况
下尽量提高传输效率
TCP的延时应答,能够让程序对于接收方的处理能力有一个更准确的评估。(关联了接收缓冲区剩余空间的大小,同时也关联了程序的处理速度)。
延迟的范围:
1、数量的限制:每隔N个包就应答一次;
2、时间限制:超过最大延迟时间就应答一次;(延时的最大时间是不会超过超时重传)
客服端和服务器之间的通信模型是“一问一答”,客户端发一个请求,服务器返回一个响应,但是HTTP也是基于TCP,客户端发送一个请求,服务器也会回一个ACK,然后在根据请求构造计算响应,再把响应返回给客户端。
这样一来服务器返回的ACK和响应就不在同一时间了,就不会合并成同一个数据报。
但是TCP中有延时应答的机制,B给A返回的ACK不会立即返回,而是等一会在返回,就导致发送时间可能和响应时间是同一时机,是同一时机就可以合并。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ttXyFNF4-1658057769371)(D:\常用文件夹\资料信息图片\Typora-image\Image\image-20220717174654088.png)](https://img-blog.csdnimg.cn/e74523ee0c3c4e95a6f0b0505b92e07c.png)
这种情况就是 捎带应答,也是延时应答的延伸。
有了捎带应答,四次挥手,可能就变成三次挥手了,
触发捎带应答不是100%触发,主要看写的代码。假设代码中的延时应答时间是200ms,在这200ms之内,代码就得执行到close。
不算是TCP的机制,是应用层都会有粘包问题;
粘包问题指多个TCP数据包到达的时候,如果不显示约定包和包之间的边间,容易产生混淆。
由图可看,接收缓存区里面有一串数据,但并不能区分是那一段跟那一段的数据,这种粘包粘的是应用层的数据报,只要有一个能区分数据报与报之间的边界就行了,如在应用层的数据 333 出现一个分号333;
那么我们就用这个 分号 来区分边界,这种也是应用层协议设定的。
在HTTP协议中(应用层协议)我们发送 GET 请求,一般GET请求是没有bady,空bady后面还有一个\n换行,那么这个换行就是区分边界的标志符;
在POST请求中,带bady,我们还是要找到POST的\n,这\n之间在找到 Content-Length这么长的数据,那么这么长的数据也就达到了请求边界。 可以看看 HTTP 协议 中的 GET和POST协议格式。
总结,解决粘包问题两种典型方案:
1、通过分隔符指定
2、通过长度来指定
建立好连接的双方,在通信工程中,遇到了一些突发状况,如何处理?
1、进程终止:A B 中的某个进程突然终止,但是操作系统会早做准备,操作系统会释放这个进程的相关资源(TCP这里依赖一个socket文件,操作系统会自动关闭这个socekt文件,类似于scocket.close())触发四次挥手。
2、机器重启或关机:这个还是交给操作系统先把所有应用程序强制终止,一终止进程就和前面进程终止触发四次挥手一样。
3、机器掉电/网线断开:
这种直接掉电的才是让操作系统猝不及防,特别是服务器会对机器本身制造伤害(主要是硬盘),因此服务器的机房,应该准备备用电源UPS,这样断电了,还可以用UPS撑一段时间,保证服务器的这些机器通过合法程序关机。
两个主机通信,如果是接收方掉电,那么就不会返回ACK,发送方就超时重传,重传一定次数,尝试重新连接,最后认为连接不可恢复,放弃连接,释送发送方这边自己保存的连接信息。
如果是发送方掉电,在TCP协议中连接的双方都会给彼此发送“探测报文”,“探测报文”不传递实际的数据,这是用来检查对方是否可以正常工作。
有个心跳包的机制,很多程序中都会用到心跳包的机制。
总结:
TCP可靠性:确认应答,超时重传,连接管理,流量控制,拥塞控制,心跳机制
TCP效率:滑动窗口,延时应答,捎带应答
编码注意事项:粘包问题
TCP的原则:优先保证可靠性,在可靠性的基础上,进一步提升效率;
关于TCP和UDP的优势:TCP可靠性;UDP效率更高
场景中,对可靠性要求不高的,同时对效率要求比较高的,优先考虑UDP。
场景中,需要可靠传输,必优先考虑TCP。
经典面试题:
如何基于UDP实现可靠传输??
看似问UDP,实则答TCP,把TCP中的可靠机制,搬给UDP即可。
铁汁们,觉得笔者写的不错的可以点个赞哟❤,收藏关注呗,你们支持就是我写博客最大的动力!!!!