带你了解面试高频TCP协议——详解
Posted Y—X
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你了解面试高频TCP协议——详解相关的知识,希望对你有一定的参考价值。
文章目录
1.TCP协议
TCP的全称是Transmission Control Protocol,传输控制协议,是一种面向连接的协议;
它规范了网络上的所有通信设备,尤其是一个主机与另一个主机之间的数据往来格式以及传送方式。
TCP报头
-
16位源端口号:发送端的端口号
-
16位目的端口号:接收端的端口号
-
保留(6位):保留该字段为保留,未使用,
值都为0
-
6位标志位:
- URG: 紧急指针是否有效
- ACK: 确认号是否有效
- PSH: 督促接收端应用程序立刻从TCP缓冲区把数据读走
- RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
-
16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.
-
16位紧急指针: 标识哪部分数据是紧急数据(
只有当URG标志位为1时紧急指针才有效
) -
4位首部长度:
4位首部长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60(
15即1111
),但是四位首部长度不可以是0,根据上图划分的区域最小位20字节,也就是选项可以为0。报头和数据
的有效分离:直接先读取20字节,再根据首部长度算出报头总长度,然后将报头全部读完,得到的就是数据的起始位置。
32位序号和32位确认序号
首先TCP是基于确认应答机制
的,在互联网通信中,没有绝对的可靠性。当每次发多条数据时,我们不能保证第一个数据会第一个到达,所以才有了序号。
为什么需要两套序号? TCP是全双工的,在数据通信的时候,扮演的角色有可能有不同,因此需要两套序号。
2. 确认应答机制
TCP在确保可靠性时,只要收到了对方的应答,就认为之前的数据对方已经收到。反之,则数据丢失的可能性很大。
TCP将每个字节的数据都进行了编号. 即为序列号。每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。
2. 超时重传机制
超时重传是指在重发数据之前,等待确认应答到来的那个特定时间间隔。如果超过了这个时间仍未收到确认应答,发送端将进行数据重发。
重传的俩种情况:
- 因网络拥堵等原因发送数据丢包
- 确认应答
ACK丢失
-
主机B会收到很多重复数据. 那么TCP协议需要识别出那些包是重复的包, 并且把重复的丢弃掉。
这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。那么,我们怎么确定超时的时间?
3.连接管理机制
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接。
连接本身是有成本的:空间加时间。
TCP面向链接:3次握手成功,意味着客户端和服务器都要维护连接。
3.1 为什么要3次握手?
一次两次不行,比如通过给服务器发送大量的半连接请求,服务器都要维护这些连接,进而耗费CPU和内存资源。造成synflood(SYN洪水攻击)
。
三次握手,是对通信信道的验证
,让客户端和服务器都验证了接受和发送能力正常,用最小成本验证全双工。
第一次和第二次握手丢失,不用担心,因为双方都不会确立连接,第三次握手丢失,客户端认为握手成功。四次握手,如果最后一次握手失败,服务器认为握手成功。
不管谁发最后一次,谁就要有大量的无用连接,而浪费资源。为了让服务器不要出现连接建立误判的情况,减少服务器的资源浪费
,要让客户端背锅,握最后一次。服务器是一对多的,防止太多无用的连接占用服务器资源。所有奇数次的握手次数都是可以的,但出于成本的考虑,三次最好。
3.2 半连接队列和全连接队列
Linux内核协议栈为一个tcp连接管理使用两个队列:
半链接队列
(用来保存处于SYN_SENT和SYN_RECV状态
的请求)全连接队列
(accpetd队列)(用来保存处于established状态
,但是应用层没有调用accept取走的请求
)
3.3TCP的三次握手是否都可以携带数据?
第一次和第二次是不可以携带数据的,但是第三次是可以携带数据的。
tcp必须建立完连接,才能通信。第三次握手,此时客户端已经处于ESTABLISHED状态。对于客户端来说,他已经建立起连接了,并且已经知道服务器的接收和发送能力是正常的。所以也就可以携带数据了。
3.4 为什么要四次挥手?
TCP是全双工的,断开连接的时候,客户端和服务端都需要断开连接,即双方都需要向对方发送断开连接的请求,并且从对方接受确认应答。主动关闭方发送FIN
请求不代表完全断开连接,只能表示主动关闭方不再发送数据了。而接收方可能还要发送数据。
1.CLOSE_WAIT
如果服务端存在大量的CLOSE_WAIT说明服务端的上层没有调用close,即客户端发送FIN并且收到服务端的ACK应答之后,服务端由于没有调用close,因此并不会向客户端发送FIN,即在内部会存在大量的CLOSE_WAIT状态。
`因此当我们检查服务器发现存在大量的CLOSE_WAIT状态时,就需要检查上层代码,是否调用了close关闭服务端的连接`
2.TIME_WAIT
首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL指的是是数据包在网络中的最大生存时间。
为什么TIME_WAIT的时间是2MSL`?
MSL是TCP报文最大的生存时间,因此TIME_WAIT持续存在2MSL,就能保证传输的俩个方向上的数据都能到达或丢失(否则服务器重启,可能会收到来自上一个进程的迟到的数据,这种数据可能是错误的)
存在的意义:
1.尽量保证最后一个ACK被对方收到,进而,尽快的释放服务器的资源
最后一次ACK是有可能丢掉的,由于TCP有超时重传机制,丢掉之后,服务端会继续向客户端发送FIN,如果最后ACK始终丢掉,由于超时重传机制,服务端会认为对端主机出现异常,强制关闭连接
2.等待历史数据从网络消散
比如,客户端给服务器发送:你好(FIN)! 但是FIN先到达的服务端,此时服务端会进行应答,开始进行断开连接流程,而TIME_WAIT状态会等待2MSL的时间(MSL数据单向最大传送时间)。由于TIME_WAIT的存在,就可以接收到遗留的信息。
3.CLOSED
只有当双方都变成CLOSED状态,才代表双方已经达成共识,这时,双方才会真正意义上的释放链接对应的资源,不再能够进行通信。
4.滑动窗口
对每一个发送的数据段, 都要给一个ACK确认应答. 收到ACK后再发送下一个数据段。这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时
间重叠在一起了。
如果出现了丢包, 那么该如何进行重传呢?
情况1:数据包已经到达,ACK被丢了。
情况二: 数据包丢了.
这种机制被称为 “高速重发控制”(也叫 “快重传”)。
为什么有了快重传还需要超时重传?
这两种重传是互相补充的,比如滑动窗口内只有两组数据,那么最多接收到两个ACK,因此不会触发快重传,此时就需要超时重传了。
5.流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位(左移一位相当于乘以2);
6.拥塞控制
虽然TCP有了滑动窗口, 能够高效可靠的发送大量的数据. 但是如果在刚开始阶段就发送大量的数据, 而网络状态正好比较拥堵,极有可能会导致整个网络的瘫痪。
TCP引入 慢启动
机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据。
- 此处引入一个概念程为拥塞窗口
- 发送开始的时候, 定义拥塞窗口大小为1;
- 每次收到一个ACK应答, 拥塞窗口加1;
- 每次发送数据包的时候, 将
拥塞窗口和接收缓冲区剩余的窗口大小做比较, 取较小的值作为实际发送的窗口
;
上图这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快。
1.为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。
2.此处引入一个叫做慢启动的阈值(ssthresh)
3.当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。(拥塞避免
)
- 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
- 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
7.延迟应答
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。
1.假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
2.但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
3.在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
4.如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
8.捎带应答
在延迟应答的基础上, 我们发现, 很多情况下, 客户端服务器在应用层也是 “一发一收” 的. 意味着客户端给服务器说了 “吃饭了吗”, 服务器也会给客户端回一个 “吃了”;
那么这个时候ACK就可以搭顺风车, 和服务器回应的 “吃了” 一起回给客户端。
9.面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲一个 接收缓冲区;
1.调用write时, 数据会先写入发送缓冲区中;
2.如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
3.如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
4.接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
5.然后应用程序可以调用read从接收缓冲区拿数据;
6.另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工;
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次
read一个字节, 重复100次。
10.粘包问题
1.首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包;
2.在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段;
3.站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中;
4.站在应用层的角度, 看到的只是一串连续的字节数据;
5.那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包;
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界;
1.对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可; 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置; 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔 符不和正文冲突即可)
;
TCP小结
为什么TCP这么复杂? 因为既要保证可靠性, 同时又要保证效率。
可靠性:
- 校验和
- 序列号(按序到达)
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
效率:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
TCP和UDP的对比:
我们说了TCP是可靠连接, 那么是不是TCP一定就优于UDP呢? TCP和UDP之间的优点和缺点, 不能简单, 绝对的进行比较。
TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
UDP用于对高速传输和实时性要求较高的通信领域, 例如, 现在的直播, 视频传输等. 另外UDP可以用于广 播。
归根结底, TCP和UDP都是程序员的工具, 什么时候, 具体怎么用, 还是要根据具体的情况去判定。
以上是关于带你了解面试高频TCP协议——详解的主要内容,如果未能解决你的问题,请参考以下文章