思考
在进入正文之前,我们先看看这些面试问的一些问题!
TCP和UDP区别?TCP报文是什么?TCP的三次握手?两次行不行?TCP的四次挥手?TIME_WAIT知道吗?
TCP为什么是安全的?TCP为什么会有黏包问题?
TCP特性
我们都知道TCP/UDP都是传输层的协议!这个一定需要知道,意思是它只负责传输,类似货车运东西。
基于TCP的应用层协议有那些呢?常用的基于TCP的应用层协议有 SMTP:(简单的邮件传输协议)HTTP ,Telnet,以及FTP(文件传输协议)都是基于TCP的。
基于UDP呢?SNMP:(简单的网络管理协议)TFTP:简单的文件传送协议,以及DNS:域名解析协议。
TCP是面向字节流的 ,具有可靠性,面向连接的这三个特性。我们从这三个特性来展开讨论
面向字节流
为什么是说TC平时面向字节流的呢
按照图示所描述的那样,我们在传输层的上面可能有很多的程序,它们都需要发起网络传输的服务,就拿经常使用的微信来说,我们平时发的都是文本,怎么到TCP是字节流了呢?
我们看看TCP的报文结构,在计算机的网络里面,报文就代表一次传输的数据。
我们看到在传输层,它是不知道这个数据要送往那个ip 的,但他会知道是目标端口和源端口。
不加上其他的数据TCP的一个报文头部数据就占了,20个字节,所以这也是TCP的一个缺点,报头占空间大。
可变选项
可变选项是TCP代表的内容可以有多种,比如扩大窗口
为什么说TCP是面向字节流呢?
TCP不会把应用层传递的内容一下子都都发送出去,因为网络是不稳定的,如果一旦发送失败就会造成大量的数据浪费。所以TCP会把应用层传递的内容,变成字节流,给他分段并且进行编号,分批发送。
- TCP会把应用层发送的数据,放到自己的发送缓冲区,同时给这些字节标上序号。
- TCP从发送缓冲区取出适当大小的字节,组成一个TCP报文发送出去
- 接收方也会有一个数据接受缓冲区,在合适的时候进行组包传给给接收方。
这样的方式意味着不会一下子发送很大的数据,但是会引入一个新问题,就是接收方并不知道这些字节流代表什么?会出现上一个请求的数据发送的数据和下一个请求发送的数据都是一起发送过来的出现了粘连的情况。
可靠性
根据上面的TCP报文结构我们知道了,TCP会对每一个发送的包进行编号,这个编号在接收方在接受到一个数据包之后,会向发送方确认数据包,也就是我们上面看到数据包的 ACK标志位会被设置为1。
但是会出现一种问题,如果我发送的这个包接收方并没有收到或者它发送的ACK的这个数据包我也没有收到,出现了数据包丢失的情况。TCP为了保证数据的完整性它会采用超时重传机制,会在发送一个编号的包之后进行计时,如果在超时,便会重写发送这个数据包。所以接收方可能会收到两个相同序号的数据包,这时候需要根据序号来去重。
如果在发送方发送一个字段就开始等待ACK确认,这样的效率太低了,所以TCP会一下子发送一连串的数据包。但是也不能无限制的发送,我们知道接收方是有一个缓冲区的,他可能会受到内存,网络以及其他因素的限制,不可能随时都接受大量的数据。这时候TCP就引入滑动窗口的机制
- 发送方需要根据接收方的缓存区大小,设置发送窗口大小。
- 当窗口里面的数据都被确认回复之后,窗口会进行滑动发送下一段的数据。
在TCP的报文里面我们可以看到有一个窗口字段,这个就是用来控制发送方窗口的,当接收方处理不过来这么多数据的时候,它就会缩小窗口来进行流量控制。
但是新的问题产生了!由于发送一连串的数据包,接收方如果对每一个都进行确认又会导致效率变低。TCP会进行累计确认,比如发送方发送了1,2,3,4,四个数据包,接收方只用回复ACK 4这个序号的数据包就可以了,代表前面传递1,2,3,4已经都被接收了。
快速重传机制
但这样发送连续窗口的时候,我们发生丢包的时候按照超时重传的机制会造成大量的数据冗余问题。TCP可以采用超时重传的机制来提高传输效率比如发送方 ,发送了 1,2,3,4,5,6,这么长串的数据包,报文段1成功接收并被确认ACK 2,接收端的期待序号为2!结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下:
但是为什么是三次ACK报文呢?
首先要明白一点,即使发送端是按序发送,由于TCP包是封装在IP包内,IP包在传输时乱序,意味着TCP包到达接收端也是乱序的,乱序的话也会造成接收端发送冗余ACK。这时候我们就要区分是乱序造成还是因为丢包引起的。
例如:我们发送 N-1 N N+1 N+2 一连串的报文
从以上罗列的情况可以看出,
在没丢失的情况下,有40%的可能出现3次冗余ACK
在乱序的情况下必定是2次冗余ACK
在丢失的情况下,必定出现3次冗余ACK
基于这样的概率,选定3次冗余ACK作为阈值也算是合理的。在实际抓包中,大多数的快速重传都会在大于3次冗余ACK后发生。
选择确认机制(SACK)
超时重传还是会有冗余的ACK报文出现,我们还可以在报文里面的选择的字段,添加需要重传的序号,减少冗余的确认报文。
网络拥塞
在网络世界里面传递数据是不稳定的,收到各种因素的影响。我们知道TCP发送方会根据接收方的缓存区的大小来调整自己的发送窗口。但是它是怎么确定这个大小的呢?
如果所示(这里的Y轴代表窗口大小,X轴代表传输的轮次)
TCP在网络拥塞的时候会遵循 四个特性:慢开始、快恢复、快重传、拥塞避免。
- 在最开始的时候会把窗口设置为一个极小值,然后每传递一轮如果没有发生超时重传,就会把窗口变为原来的两倍。
- 在达到某个阈值(这阈值是根据当前网络情况决定的)的时候开始进入网络拥塞,每轮把窗口大小提高1,慢慢试探。
- 当发生超时的时候,表示可能发生了网络的拥塞,这时候会重新回到慢启动(这里被称为快恢复)
- 如果出现了快速重传的现象,标明当前的网络情况不好,会将阈值变为当前的一半。
- 如果出现了丢包情况,会尽快重传丢失的包(快重传)
面向连接
TCP的连接是指的是:通信双方之间的记录,并不是真的去进行连接了 ,根据上面的可靠性我们可以知道,TCP是一个双工的通信,发送发可以给接收方发送报文,接收方也可以给发送方返回报文。它们都维护了一个缓存区,记录了双方的发送信息。
为了记录信息,TCP会经历握手的环节。
服务器A向服务器B发送数据,则它会先经历握手。
- SYN_SEND, 服务器A会发送一个SYN的报文包,它的假设它的序号为X,自己进入SYN_SEND状态
- SYN_RECED。服务器B接受到服务器A发送的SYN报文之后,会立即回复ACK报文,发送序号是x+1,期望接受序号是Y。
*ESTABLISHED: 当服务器A接受到ACK报文之后,会向服务器B也发送一个ACK报文,发送序号是y。服务器B接受到ACK报文后也会进入 ESTABLISHED,它们就可以发送数据进行通信了。
断开连接,同样的TCP在断开连接的时候也会经历一些流程,也就是我们说的四次挥手。
还是上面的例子,服务器A要和服务器B进行断开连接的请求
- FIN_WAIT-1: 服务器A发送一个FIN报文给服务器B,代表自己已经没有数据发送了,自身进入FIN_WAIT-1状态。
- CLOSE_WAIT:服务器B在接受到服务器A发送的FIN字段之后,自身进入CLOSE_WAIT,并且会回复服务器A一个ACK字段。
- FIN_WAIT-2:服务器A接受到回复的ACK报文之后,自身会进入FIN_WAIT-2状态。(这种阶段在实际中很少见)
- TIME_WAIT:服务器B没有数据发送了,会向服务器A发送FIN字段。这时候服务器A会进入TIME_WAIT状态。(如果服务器B同时发送FIN ACK标志位都为1的报文,会立即进入这个状态,不会进入FIN_WAIT-2)
LAST_ACK:服务器B发送FIN字段之后,自身会进入LAST_ACK状态。
CLOSE:当服务器A等待2MSL单位时间之后,会向服务器B发送ACK字段,正式的关闭连接。这个2MSL在官方文档里面是4分钟,Linux很早就改成60s了。
这里我们可以解答开始的问题了,为什么要三次握手?
答:因为TCP是双工的,建立连接的双方都可以向对方传递数据,如果只用两次握手只能保证单方的发送。比如A,B两次握手,B不向A发送ACK报文,B并不能保证它的数据一定能发送到A。
为什么要四次挥手?
答:因为在对方发送FIN的时候只能代表对方没有数据传递了,但是接收方还有数据要向对方传递,所以在发送数据完了之后,再回复FIN字段,代表数据传输完毕可以关闭了。
为什么要由TIME_WAIT阶段:第一个原因是为了确保最后发送的ACK报文能传递成功,如果失败,我们在这个时间里面可以进行超时重传。第二个原因是为了保证后序因为超时重传的数据包都失效,不干扰下一次连接。
为什么会有黏包问题?
TCP是面向字节流,会把应用层传递的数据变成数据流,TCP不懂这些数据流的意义,他只知道从应用层拿到数据流,切割成一份份报文,然后发送给目标对象。而如果应用层传输下来的是两个数据包,那么极有可能出现这种情况:
- 应用层需要向目标进程发送两份数据,一份音频,一份文本
- TCP只知道接收到一个流,并把流拆分成4段进行发送
- 中间第二个报文的数据就出现两个文件的数据混在一起,这就是粘包
- 目标进程应用层在接收到数据之后,需要把这些数据拆分成正确的两个文件,就是拆包
粘包与拆包都是应用层需要解决的问题,可以在每个文件的最后附加上一些特殊的字节,如换行符;或者控制每个报文只包含一个文件的数据,不足的用0补充。
UDP
我们都知道TCP还有一个好兄弟,它就是UDP,UDP只实现了传输层的最小功能,他会给应用层传递的报文添加一个头部就交给下一层了,并没有做分包这样的控制。
头部
它的头部很简单,只包含以下字段:
- 源端口、目标端口:端口号用来区分主机的不同进程
- 校验码:用于校验数据包在传输的过程中没有出现错误,例如某个1变成了0
- 长度:报文的长度
优缺点
由于头部简单,也没有任何的控制措施,UDP具有以下缺点:
- 无法保证消息完整、正确到达,UDP是一个不可靠的传输协议;
- 缺少拥塞控制容易互相竞争资源导致网络系统瘫痪
但是由于没有这些机制的限制,UDP很放飞自我,它也有一些优点:
- 效率更快;不需要建立连接以及拥塞控制
- 连接更多的客户;没有连接状态,不需要为每个客户创建缓存等
- 分组首部字节少,开销小;TCP首部固定首部是20字节,而UDP只有8字节;更小的首部意味着更大比例的数据部分
- 在一些需要高效率允许可限度误差的场景下可以使用。如直播场景,并不需要保证每个数据包都完整到达,* 允许一定的丢包率,这个时候TCP的可靠特性反而成为了累赘;精简的UDP更高的效率是更加适合的选择
- 可以进行广播;UDP并不是面向连接的,所以可以同时对多个进程进行发送报文
UDP的适用场景
UDP适用于对传输模型需要应用层高度自定义、允许出现丢包、需要高效率的场景、需要广播;例如
- 视屏直播
- DNS
- RIP路由选择协议