上图可如,此处报文长度是两个字节范围是0-65535即0-64k。无法表示一个较大的数据包,如果需要穿一个比较大的数据有两个方法:
下策:在应用层针对大的数据包进行分包,然后再通过多个UDP数据包分别发送,这个时候接收方再根据收到的几个包重新进行拼接成完整的数据。问题就是太玛法写起来非常复杂,要考虑到很多情况,比如丢包或者包的顺序错了等等。
上策:改成TCP,TCP没有长度限制。
校验和
用来验证网络传输这个数据是否是正确的。网络上传递的数据本质是光信号和电信号,但是如果有一却外界干扰磁场之类的就可能会导致一些传递信息发生改变,校验和就能帮助我们发现数据中的错误。比如使用01的个数来作为校验和,校验和正确不能保证数据100%就是正确的,但是校验和不正确数据一定就是有问题的。虽然能如此校验和仍然广泛使用,因为成本低效果好。当然校验和也不一定单纯就是使用“个数”。还可以使用数据内容参与运算,如果是基于数据内容得到的校验和识别错误的概率是很高的。
描述网络数据的单位:
传输层的数据通常叫做段“segment”
网络层数据通常叫做包/报“packet”
数据链路层通常叫做帧“frame”
目的
保证可靠传输的核心机制(可靠性:发送方发出数据后,能够知道对方有没有收到)
方法
对消息进行编号,在接收方收到消息之后,针对消息编号给发送方返回一个应答报文(ACK,acknowlegdege),表示自己已经收到了。
32位序号:消息的编号,TCP的针对消息的序号还有说法,并不是按照“消息条数”来进行编号的,而是按照字节来编号的。
32位确认序号:表示当前这个应答报文是针对哪个消息进行的确认应答。
定义
相当于对确认应答进行了补充,确认应答是网络一切正常的时候,通过ACK通知发送方我收到了,如果出现了丢包的情况,超时重传机制就要起到效果了。
例子:
当我发出消息后,一直没有ACK可能会出现的情况,1是确实发丢了对方没有收到,我这里肯定也没有办法收到ACK,2是对方的ACK丢了,虽然对方收到了我的消息但是我收不到ACK。
解决方法:
对于发送方来说,发送方无法区分是哪种原因大致的没有收到ACK,但是最坏情况下是对方没有收到那我就重新再发一次。这里的重发也不是立即就重发,而是等一会。等到超出等待时间就进行重传。正常情况下,连续发丢两次的概率是比较低的。当然重传的数据也不一定会成功,如果是网络遭受到了严重伤害可能没那么容易恢复,重传也成功不了,在这种情况下也不会一直进行重传,重传的时间间隔也会逐渐变大。
问题:
如果是对方ACK丢了,此处触发了超时重传就会导致接收方收到了重复的消息。
解决方法:
TCP内部就会有一个去重操作,接收方收到的数据会先放到操作系统内核的“接收缓冲区”中,接收缓冲区可以视为是一个内存空间,也可以视为是一个阻塞队列。
收到新的数据,TCP就会根据序号,检查数据是不是在接收缓冲区中已经存在了。如果不存在就放进去,如果存在就直接丢弃。保证应用程序调用socket api拿到的这个数据一定是不重复的。应用程序是感知不到超时重传的过程的。
目的
也是TCP保证可靠性的一个机制
1)如何建立连接
三次握手,客户端和服务器之间,通过三次交互完成了建立连接的过程,“握手”是一个形象的比喻。
客户端是主动发起连接请求的一方,客户端先发送一个SYN同步报文段给服务器。这个过程就像表白双向奔赴的过程,双方都像对方表白这才算是正式确立关系。
如上图所示,如果ACK这一位为1,表示这个报文就是一个“确认报文段”。如果SYN这一位为1,表示当前报文就是“同步报文段”主机A和主机B之间要建立连接。
中间的这两次“握手”是一定会合二为一的,每次要传输的数据都要经过一系列的封装和分用才能完成传输,两次封装肯定不如一次封装来的高效。
TCP的状态
TCP的状态有很多,不需要去研究。
LISTEN:表示服务器启动成功,端口绑定成功,随时可以有客户端来建立连接。(手机开机,信号良好,随时可以接听电话)
ESTABLISHED:表示客户端已经连接成功,随时可以进行通信了。(有人给我打电话,我接听了,接下来可以说话了)
三次握手的作用,与可靠性的关系
1.相当于是“投石问路”,检查当前网络是否满足可靠传输的基本条件。如果网络本身比较差,强行进行TCP传输就会涉及大量丢包。
2.让双方能够协商一些必要的信息。
三次握手对于TCP可靠传输来说是非常必要的,尤其是这个“投石问路”的过程。
描述三次握手的过程,为啥握手是三次的?两次?四次?(经典面试题)
客户端向服务端发送一个同步报文段SYN,服务器向客户发送一个同时包含SYN和ACK的报文段,客户端再向服务器发送一个确认报文段ACK。
四次握手可以,当时分开传输降低效率不如合在一起好。
两次不行,意味着缺少最后一次ACK,此时客户端这边关于发送接受能力正常情报是完整的,但是服务器这边是残缺的,服务器不知道自己的发送能力是否ok,也不知道客户端这边的接收能力是否ok。所以此时服务器对于当下是否能满足可靠传输是没底的。三次交互就是给服务器吃了一个定心丸。
2)如何断开连接
四次挥手,建立好连接后,操作系统内核中就会存储一定的数据结构来保存连接的相关信息,保存的信息通常就是常说的“五元组”:源IP,源端口,目的IP,目的端口,TCP。
既然是保存了信息就占用系统的资源,有一天断开连接了就需要释放对应的空间。
三次握手,中间两次能合并。四次挥手,中间两次有可能合并不了,有时候能合并。
不能合并的原因:
B发送ACK和B发送的FIN的时机不同。四次挥手中,B发送给A的ACK是内核负责的。B发送给A的FIN的用户代码负责的(B的代码里调用了socket.close()放大,触发了FIN。此时B收到FIN,立即就由内核返回ACK,但是执行到用户代码中close才会触发。如果这两个时间差比较小,这是可能合并的。
三次握手中,B发送的ACK和SYN同一时机就能合并,即此处的B给A发送的ACK和SYN都是操作系统内核进行的。
CLOSE_WAIT:
四次挥手回了两次后出现的状态,这个状态就是在等待代码调用socket.close方法,来进行后续的挥手过程。正常情况下,一个服务器上不应该存在大量的CLOSE_WAIT,如果存在说并大概率是代码bug,close没有被执行。
TIME_WAIT:
谁主动发起FIN谁就进入TIME_WAIT.作用在于就是给最后一次ACK提供重传机会。表面上看A发送完ACK之后就没有A的啥事情了,按理说A就应该销毁连接释放资源,但是并没有直接释放而是进入TIME_WAIT等待一段时间再释放连接。等这一会是怕最后一个ACK丢包,如果最后一个ACK丢包了就意味着B过一会就会重传FIN。
TIME_WAIT应该持续多久?
设定时间是2*MSL,MSL表示网络上任意两点之间传输需要的最大时间,这个时间也可以是系统上可以配置的参数,一个典型的设置是60s(经验值)。
目的
在保证可靠性的前提下提高传输效率。
滑动窗口
本质就是在“批量发送数据”,以此发送一波数据,然后一起等一波ACK。
窗口大小
一次批量发送数据量N为窗口大小。
滑动
并不用把N组数据的ACK都等到了才继续往下发送,而是收到一个ACK就继续往下发送一个。
当这个窗口大小越大就可以认为就是传输速度越快,窗口大了同一份时间内等待ACK就更多,总的等待ACK的时间就少了。
在这个图上所患的情况是6组ACK丢了3组。在发送4001之前,发现收到一个2001此时没有收到1001,2001表示的意思就是:2001之前的数据都已经确认收到了,ACK确认序号的特定含义就保证了后一条的ACK久能涵盖前面的ACK。
由于1001-2000这个数据丢了,所以B就反复索要1001这个数据,即使A给B已经往后发了这个时候仍然在索要1001,当索要若干次后A就明白了就出发了重传。
当A重传了1001-2000之后,B的接收缓冲区就把缺口给补上了,后续的2001-7000这些数据都已经穿输了就不需要重传,接下来B就向A索要7001的数据。这里的重传只是需要把丢了的那一块数据给重传了即可,其他已经到达了的数据就不要再重传了,整体效率还是比较高的。
目的
滑动窗口的延伸,为了保证可靠性
问题:在滑动窗口中,窗口越大传输速率就越高,不光要考虑发送方还要考虑接收方,当发送速率过快接收方根本处理不过来,接收方就会把新收到的包给丢了于是还得重传。流量控制的关键就是能够衡量接收方的处理速度。此处就直接使用接收方接收黄崇取得剩余空间大小,来衡量当前的处理能力。
这样的数据传输过程可以理解成“生产者消费者模型”,A就是生产者,B的应用程序就是消费者,B的接收缓冲区就是交易场所。接收缓冲区肯定有一个总大小,随着A发送数据,数据缓冲区就会逐渐放入一些数据,剩余空间就会逐渐缩小。如果剩余空间较大,就认为B的处理能力是比较强的,就可以让A发的快点。反之则相反。
接收方如何告知发送方剩余空间大小?
通过ACK报文告知。如下图:
A确实是不发送了,但是又不是完全不发送,A会定期发送一个探测报文,探测报文不传输实际的数据,只是为了触发ACK来知道当前窗口的大小是多少。
目的
也是滑动窗口的延伸,限制滑动窗口发送的速率。拥塞控制衡量的是,发送方到接收方着整个链路之间拥堵情况。
A能够发多快不光取决于B的处理能力,也取决于中间链路的处理能力。A和B之间有多少个中间节点是未知的,处理方案就是通过实验逐渐调整发送速度来找到一个比较合适的值(范围)。
处理方案:A刚开始的时候以一个比较小的窗口开始来发送数据,如果数据流畅到达就逐渐加大窗口大小。当出现了丢包(丢包意味着通信链路出现了拥堵),再减小窗口。通过反复的增大/减小过程居间就摸到了一个合适的范围。拥塞窗口就在这个范围不断变化达到动态平衡。
拥塞窗口的用途
通过拥塞窗口的大小来制约滑动窗口的大小,最终滑动窗口的大小=min(拥塞窗口,流量量控制窗口)
1)最开始的时候,去的初始窗口大小非常小。可能合适的只是一个更大的值,通过上述这个之数规律增长就可以更快的接近合适值。
2)指数增长到一定程度就会进入线性增长,线性增长也是增长。
3)增长到一定程度就会出现丢包,一旦丢包此时发送方立即就让窗口变小(回归初始的窗口大小),继续重复刚才的指数增长+线性增站的过程。直接让窗口回归初始值主要的目的,网络情况是复杂的不稳定的,如果出现丢包刚把速度降下来一点不能解决问题,讲的太慢就会出现持续性的丢包,就对网络通信质量带来了很大的影响。一下让窗口变得很小就是期望这次传输一定成功。
ssthresh这个阈值决定了什么时候从指数->线性,这个阈值也不是一直不变的,每次出现丢包预支就更新为当前出现丢包的窗口大小的一半。
理想的窗口大小:预知和丢包窗口之间。
目的
相当于是流量控制的延伸,流量控制是踩下了刹车,使发送方发的不要太快,延时应答就是在这个基础上能够尽量的在让窗口更大一些。原理如下图
注水的同时也在出水,每次注入一波谁就会询问当前水池里剩余空间有多少。采取的策略就是不立即回答,而是稍微晚一点回答,意味着在这个延伸时间里更够处理更多的消息。这个操作就是在有限情况下,又尽可能的提高一点传输速度。
目的
延时应答的延伸
ACK是内核响应的(立即执行),应用程序返回的是执行相关代码,这俩是不同实际出发的应该不能合并。因为延时应答的存在导致ACK不一定是立即返回的,如果当前的延时应答和应用程序代码中返回的响应时机重合了,就可以把这俩数据合二为一。
客户端和服务器之间的通信有以下几种模型:
1.一问一答:客户端发一个请求,服务器返回一个对应的响应。用浏览器上网,打开网页就是这种模型。
2.多问一答:上传文件
3.一问多答:下载文件
4.多问多答:直播,串流
产生的问题:粘包问题,不仅仅TCP存在粘包问题,其他面向字节流的机制也会存在比如读文件。TCP粘包指的是粘的是应用层数据报。在TCP接收缓冲区中,若干个应用层数据报混在一起分不清谁是谁。
问题描述:
1)这些数据包到达B之后就会进行分用,分用意味着就把TCP数据进行解析了,取出其中的应用层数据放到接收缓冲区中,以备应用程序来取。
2)B的应用程序就需要通过read方法来从接收缓冲区中取数据,因为TCP是面向字节流的,B取的时候就是取的若干个字节,从哪里取取到哪里,是一个完整的应用层数据报就无法知晓了。如果没有额外的限制就很难进行区分了,归根到底是没有明确包之间的边界。
解决方法:
在应用层协议里加入包之间的边界(粘包问题,说是在TCP这里讲的,实际上是一个应用层的问题),例如:约定每个包以;结尾。这个时候只要能够按照;来进行切分就可以区分出当前从哪里是一个完整的应用层数据了。
1.进程终止
TCP连接,是通过socket来建立的,socket本质上是进程打开的一个文件,文件其实就存在于进程的PCB里面有一个文件描述符表,每一次打开一个文件都会在文件描述符表里增加一项,关闭一个文件就会删除一项。如果直接杀死进程,PCB也就没了,里面的文件描述符表也就没了,此处的文件相当于自动关闭了。这个过程其实和手动调用socket.close()一样,都会触发4次挥手。
2.机器关机
正常流程的关机会让操作系统,杀死进程然后关机,同样会触发四次挥手。
3.机器掉电/网线断开
台式机直接拔电源,操作系统不会有任何反应时间,更不会有任何处理措施。
如果B断电意味着A发送的数据不再会有ACK,A进入超时重传逻辑,重传几次之后,A认为这个链接已经出现严重故障了。尝试重新建立连接,重连失败之后就会放弃连接。(A主动释放曾经和B相关的连接信息)
如果A断电,B就不知道当前A是挂了还是A再休息一会再继续,B就会是不是得给A发送一个小的探测报文(不带有实际的数据只是为了触发ACK),通过探测报文(心跳包)发现A不再返回ACK,因此B就会认为A出现了问题。
1)啥时候使用TCP?对可靠性有一定的要求(日常的开发大多数都是基于TCP)
2)啥时候使用UDP?对可靠性要求不高,对于效率要求更高(典型的例子,机房内部的主机之间的通信,分布式系统中)
其实是在考TCP,本质上就是在应用层基于UDP复刻TCP的机制。UDP可靠性传输需要考虑的几个点,