带你了解面试高频TCP协议——详解

Posted Y—X

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了带你了解面试高频TCP协议——详解相关的知识,希望对你有一定的参考价值。

文章目录

1.TCP协议

TCP的全称是Transmission Control Protocol,传输控制协议,是一种面向连接的协议;

它规范了网络上的所有通信设备,尤其是一个主机与另一个主机之间的数据往来格式以及传送方式。

TCP报头

  1. 16位源端口号:发送端的端口号

  2. 16位目的端口号:接收端的端口号

  3. 保留(6位):保留该字段为保留,未使用,值都为0

  4. 6位标志位:

    • URG: 紧急指针是否有效
    • ACK: 确认号是否有效
    • PSH: 督促接收端应用程序立刻从TCP缓冲区把数据读走
    • RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
    • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
    • FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段
  5. 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.

  6. 16位紧急指针: 标识哪部分数据是紧急数据(只有当URG标志位为1时紧急指针才有效

  7. 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. 超时重传机制

超时重传是指在重发数据之前,等待确认应答到来的那个特定时间间隔。如果超过了这个时间仍未收到确认应答,发送端将进行数据重发。

重传的俩种情况:

  1. 因网络拥堵等原因发送数据丢包
  2. 确认应答ACK丢失
  • 主机B会收到很多重复数据. 那么TCP协议需要识别出那些包是重复的包, 并且把重复的丢弃掉。
    这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果。

    那么,我们怎么确定超时的时间?

3.连接管理机制

在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接。

连接本身是有成本的:空间加时间。
TCP面向链接:3次握手成功,意味着客户端和服务器都要维护连接。

3.1 为什么要3次握手?

一次两次不行,比如通过给服务器发送大量的半连接请求,服务器都要维护这些连接,进而耗费CPU和内存资源。造成synflood(SYN洪水攻击)

三次握手,是对通信信道的验证,让客户端和服务器都验证了接受和发送能力正常,用最小成本验证全双工
第一次和第二次握手丢失,不用担心,因为双方都不会确立连接,第三次握手丢失,客户端认为握手成功。四次握手,如果最后一次握手失败,服务器认为握手成功。

不管谁发最后一次,谁就要有大量的无用连接,而浪费资源。为了让服务器不要出现连接建立误判的情况,减少服务器的资源浪费,要让客户端背锅,握最后一次。服务器是一对多的,防止太多无用的连接占用服务器资源。所有奇数次的握手次数都是可以的,但出于成本的考虑,三次最好。

3.2 半连接队列和全连接队列

Linux内核协议栈为一个tcp连接管理使用两个队列:

  1. 半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求)
  2. 全连接队列(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协议——详解的主要内容,如果未能解决你的问题,请参考以下文章

带你了解面试高频TCP协议——详解

TCP/UDP面试题高频词汇详解

一篇文章带你详解 HTTP 协议之报文首部及字段详解(中)

计算机网络高频面试题解析(含书籍推荐)

TCP协议三次握手与四次挥手详解(上)

TCP协议三次握手与四次挥手详解(上)