您的位置 首页 golang

一文带你搞定TCP面试所有细节_TCP详谈

Linux服务器 开发相关视频解析:

1. TCP简介

传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。

1.1 TCP报头

1、源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去

2、6位标志位:

  • URG: 紧急指针是否有效
  • ACK: 确认号是否有效
  • PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走
  • RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段
  • SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段
  • FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段

3、16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.

4、16位紧急指针: 标识哪部分数据是紧急数据

对于TCP报头主要讲解几个重要部分:

4位首部长度,首部,和选项之间的关系:

  • 4位TCP报头长度: 表示该TCP头部有多少个32位bit(有多少个4字节); 所以TCP头部最大长度是15 * 4 = 60(15即1111)。但是四位首部长度不可以是0,即最小首部长度是20,如果长度比20大,则将多出来的长度填到选项中,以保证报头和有效数据的分离。所以他们之间的关系就是:选项是对超过首部长度的补充,而根据四位首部长度可以计算出报头总长度大小。(0<=选项<=40)。

32位序号和32位确认序号分别是干嘛的?

  • 首先TCP是基于确认应答机制的,即主机A给主机B发送了一则消息,当收到主机B的确认信息以后,才保证主机A发的信息主机B收到了,这里就可以提出可靠性的概念,可靠性又可以分为相对可靠性和绝对可靠性,网络传输数据是无法保证绝对可靠的,因为总有一条最新的消息没有被确认。
  • 32位序号即主机A对主机B发送消息时由于是面向字节流的,会将原始报文进行分割,则分割的时候就需要对报文进行编上序号,以保证在网络中传输时将顺序打乱时主机B可以按照序号读到正确的报文顺序,(按序到达)。32位确认序号则是主机B收到来自主机A的100号消息时,则要给主机A返回应答消息101,代表前100已经收到,下次发送从101号开始发。即确认应答机制。保证双方有序正常通信。

2. 确认应答机制

TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端将数据发出之后会等待对短的确认应答。如果有确认应答,说明数据已经成功到达对端,反之,则数据丢失的可能性很大

TCP将每个字节的数据都进行了编号. 即为序列号。

每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发。

【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL, Redis ,fastdfs, MongoDB ,ZK, 流媒体 CDN ,P2P,K8S, Docker ,TCP/IP,协程,DPDK, ffmpeg 等)

3. 超时重传机制

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

情况一:丢包

  • 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
  • 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发。

情况二:ACK丢失

  • 主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.这时候我们可以利用前面提到的序列号, 就可以很容易做到去重的效果.

4. 连接管理机制

连接管理机制主要是讲三次握手和四次挥手,在之前的博文中已经有详细的讲解。

这里主要讲面试高频问题:

三次握手:

一次,两次,四次或者更多次握手行不行?

  • 一次两次不行,比如给服务器发送大量请求,会被攻击(洪水攻击)。三次握手,是对通信信道的验证,让客户端和服务器都验证了接受和发送能力正常,用最小成本验证全双工。第一次和第二次握手丢失,不用担心,因为双方都不会确立连接,最担心第三次握手丢失,客户端认为握手成功。四次握手,如果最后一次握手失败,最担心,服务器认为握手成功, 不管几次握手,都担心最后一次。最担心最后一次握手丢失,应该让客户端背锅,免得服务器是一对多的,有大量的无用连接,而浪费资源。让服务器不要出现连接建立误判的情况,减少服务器的资源浪费。肯定不会用偶数次,更多奇数次是可以的,但是要最小成本。

TCP的三次握手是否都可以携带数据?

  • 带SYN的是不可以携带数据的第一次和第二次是不可以携带数据的,但是第三次是可以携带数据的。
  • 假如第一次握手可以携带数据的话,那对于服务器太危险,如果恶意攻击服务器,每次都在第一次握手中的SYN报文中放入大量数据。而且频繁重复发 SYN 报文,服务器会花费很多的时间和内存空间去接收这些报文。
  • 第三次握手,此时客户端已经处于ESTABLISHED状态。对于客户端来说,他已经建立起连接了,并且已经知道服务器的接收和发送能力是正常的。所以也就可以携带数据了。

TCP三次握手失败,服务端会如何处理?

握手失败的原因有两种:

  • 第一种是服务端没有收到SYN,则什么都不做。
  • 第二种是服务端回复了SYN+ACK后,长时间没有收到ACK响应。server端发送了SYN+ACK报文后就会启动一个定时器,等待 Client 返回的ACK报文。如果第三次握手失败的话 client server 返回了ACK报文,server并不能收到这个ACK报文。那么server端就会启动超时重传机制,超过规定时间后重新发送SYN+ACK,重传次数根据/proc/sys/net/ipv4/tcp_synack_retries来指定,默认是5次。如果重传指定次数到了后,仍然未收到ACK应答,那么一段时间后,server自动关闭这个连接。但是client认为这个连接已经建立,如果client端向server写数据,server端将以RST包响应。

什么是半连接队列?

  • 服务器第一次收到客户端的SYN之后,就会处于SYN_RECD状态,此时双方还没有完全建立连接。服务器会把这种状态下的请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,就是已经完成三次握手,建立起来连接的就会放在全连接队列中,如果队列满了就有可能出现丢包现象。

四次挥手:

当客户端想要断开连接时,并不是彻底和服务器断开了,是指应用层不会发数据了。客户端有四次状态,服务器有三次状态。

为什么握手是三次,而挥手时需要四次呢?

  • 其实在TCP握手的时候,接收端将SYN包和ACK确认包合并到一个包中发送的,所以减少了一次包的发送。对于四次挥手,由于TCP是全双工通信,主动关闭方发送FIN请求不代表完全断开连接,只能表示主动关闭方不再发送数据了。而接收方可能还要发送数据,就不能立即关闭服务器端到客户端的数据通道,所以就不能将服务端的FIN包和对客户端的ACK包合并发送,只能先确认ACK,等服务器无需发送数据时在发送FIN包,所以四次挥手时需要四次数据包的交互。

CLOSE_WAIT状态详谈:

  • 出现CLOSE_WAIT,说明Server端没有发起close()操作,这基本上是用户server端程序的问题了;通常情况下,Server都是等待Client访问,如果Client退出请求关闭连接,server端自觉 close ()对应的连接,当服务器接受到来自客户端FIN的断开请求时,服务器进入 CLOSE _WAIT状态,并发送ACK,直到服务器想要断开连接时,发送FIN和ACK断开请求,并转变为LAST_ACK状态,如果用netstat查看有大量的CLOSE_WAIT状态说明服务器代码有BUG。
  • 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket , 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题

TIME_WAIT状态详谈:

  • 首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。 MSL 指的是是数据包在网络中的最大生存时间。
  • 确保最后一个确认报文能够到达。进而尽快释放服务器的资源。如果没能到达,服务端就会会重发FIN请求释放连接。等待一段时间没有收到重发就说明服务的已经CLOSE了。如果有重发,则客户端再发送一次ACK信号。
  • 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
  • 如果没有TIME_WAIT的话,假设连接1已经断开,然而其被动方最后重发的那个FIN(或者FIN之前发送的任何TCP分段)还在网络上,然而连接2重用了连接1的所有的5元素(源IP,目的IP,TCP,源端口,目的端口),刚刚将建立好连接,连接1迟到的FIN到达了,这个FIN将以比较低但是确实可能的概率终止掉连接2

解决TIME_WAIT状态引起的bind失败的方法:

在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的

  • 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
  • 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.
  • 由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,源端口, 目的ip, 目的端口, 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了, 就会出现问题.

使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符:

5. 滑动窗口

如果出现了 丢包 , 那么该如何进行重传呢?

分两种情况讨论:

1、数据包已经收到, 确认应答ACK丢了:

2、数据包丢失:

6. 流量控制

那么接收端如何把窗口大小告诉发送端呢?

  • TCP首部中, 有一个16位窗口大小字段, 就存放了窗口大小的信息;16位数字最大表示65536, 那么TCP窗口最大就是65536字节么?
  • 实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是窗口字段的值左移 M 位(左移一位相当于乘以2).

7. 拥塞控制

如果网络上的延时突然增加,那么TCP对这个事作出的应对只有重传数据,但是重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“ 网络风暴 ”,TCP这个协议就会拖垮整个网络。所以TCP不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络造成更大的伤害。对此TCP的设计理念是:TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了。

少量丢包, 仅仅是触发超时重传; 大量丢包, 认为网络拥塞;

当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;

拥塞控制, 归根结底是 TCP协议 想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。

8. 延迟应答

我们知道TCP中,有确认应答机制以保证数据的可靠传输。但是是不是接受方接受到数据就立即返回ACK应答呢?如果是这样,这时候的缓冲区中接收区的数据还没能够处理,缓存区的剩余大小就是窗口大小。但是如果我们延迟一会,等待缓存区中数据被处理,那么剩余的缓存区就会大些——这就是延时应答。

ps:假设接收端缓存区大小为1M,一次接收到了500K的数据,现在缓存区中剩余大小为500。但如果我们延时一段时间,等待接受方处理了该缓存区中的数据,处理以后接受窗口变大,那么我们的剩余大小就为1M了(即:窗口大小)

窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。

等待的时间

  • 每个操作系统中设置的等待时间是不一样的。(200ms)

是不是所有的包都可以延时应答?

  • 数量限制:每隔两个包就应答一次
  • 时间限制:超过最大延时时间就应答一次(200ms)

具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;

9. 捎带应答

捎带应答是指在同一个TCP包中即发送数据又发送确认应答的一种机制。由此,网络的利用率会提高,计算机的负荷也会减轻。不过,确认应答必须等到应用处理完数据并将作为回执的数据返回为止,才能进行捎带应答。

10. 面向字节流

11.粘包问题

  • 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
  • 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.
  • 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
  • 站在应用层的角度, 看到的只是一串连续的字节数据.
  • 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.

那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.

  • 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的 Request 结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
  • 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);

思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?

  • 对于 UDP , 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
  • 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现”半个”的情况

12. TCP异常情况

  • 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
  • 机器重启: 和进程终止的情况相同.
  • 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.

另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如 QQ , 在QQ断线之后, 也会定期尝试重新连接。

13. 基于TCP应用层协议

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP

也包括自己写TCP程序时自定义的应用层协议。

14. TCP小结

为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.

可靠性:

  1. 校验和
  2. 序列号(按序到达)
  3. 确认应答
  4. 超时重发
  5. 连接管理
  6. 流量控制
  7. 拥塞控制

提高性能:

  1. 滑动窗口
  2. 快速重传
  3. 延迟应答
  4. 捎带应答

其他:

  1. 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等。

文章来源:智云一二三科技

文章标题:一文带你搞定TCP面试所有细节_TCP详谈

文章地址:https://www.zhihuclub.com/100613.shtml

关于作者: 智云科技

热门文章

网站地图