长篇tcp 网络,汇集大小厂经典问题

2022-12-05 10:04:34 浏览数 (1)

作者:Bruce.D

github:https://github.com/doukoi-BDB

今日主题:

1、大小厂,面试中 tcp 中的问题;

2、偶尔来个故事、还是技术服务读友;

01

开场

根据公众号读友们的反馈,年底了。该分享分享一些大小厂核心面试【模块】点了,特意总结了周围一波朋友的【 tcp 网络】的面试点。因此本篇有点长,建议收藏慢慢看,你用的到,我也用的到。

除此之外、按照社群各位反馈,在补充几个微信支付的案例、类库,本周也会更新,更新后会特意发一篇文章内容更新公告。

02

常见tcp问题

分为3块进行讲解:tcp 的基础问题、tcp的连接问题、tcp的断开问题。 下面我的回答会相对简化一些,有需要的朋友,可以跟着问题去搜索更深入的细节。

模块一:tcp 的基础问题

1、 什么是 tcp ?

2、什么是tcp 连接?

3、tcp 头部格式?

4、tcp 最大链接数是多少?

5、udp 与 tcp 的区别?

6、udp与tcp的场景?

7、为什么tcp 头部没有【包长度】字段呢?

模块二:tcp 的连接建立问题

8、 tcp 三次握手过程&状态变化?

9、 linux系统中如何查看tcp状态?

10、为什么是3次握手?而不是其他次数?

11、tcp每次链接为什么初始化序列号都不一致呢?

12、初始化序列号 如何产生的?

模块三:tcp 的连接断开问题

13、握手中断,会发生什么?

14、tcp 四次挥手的过程&状态变化?

15、为何time_wait 等待时间是2msl?

16、为什么需要time_wait 这个状态?

17、time_wait 过多什么危害?

18、如何优化time_wait?

解答: 1、什么是 tcp ?

tcp 是面向连接、可靠的、基于字节流的传输层通信协议。

对于面向连接:一对一才能连接,不像udp 可以一个主机同事向多个主机发送消息。对于可靠的:无论网络链路种出现了怎么样的变化,tcp都可以保证一个报文一定能够达到接收端。对于字节流:用户消息通过tcp协议传输时,消息可能会被操作系统分成多个tcp报文,接收方如果不知道消息的边界,是无法读出有效用户信息的。

2、什么是 tcp 连接?

官方定义连接:上述可靠性和流量控制机制要求TCP初始化并维护每个数据流的特定状态信息。这些信息的组合,包括套接字、序列号和窗口大小,称为连接。

通俗解释连接:用于保证可靠性 流量控制维护某些状态信息的组合,包括(socket、序列号、窗口大小)俗称连接。

简单解释:socket:ip地址 端口号组成;序列号:用来解决乱序问题;窗口大小:用来做流量控制。

3、tcp头部格式?

序列号(32位)、确认应答号(32位),控制位(ack、rst、syn、fin)。序列号:上面也说了,解决网络包乱序问题;确认应答号:用来解决丢包问题(例如:发送者收到数据序列号后,发送端收到确认应答后,会认为前面传输都是正常);控制位:单独解释一波:(ack:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 );(rst:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接);(syn:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。);(fin:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。)

4、tcp最大连接接数是多少?

服务端通常固定在某个本地端口监听,等待客户端链接请求。因此客户端 ip 端口也是可变的,所以上公式:最大tcp 连接数 = 客户端ip数 * 客户端端口数 。

仅仅是理论上限:对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。实际影响:文件描述符限制、系统级、用户级、进程级。

5、udp与tcp的区别?

区别:1)连接:tcp是面向连接的传输层协议,传输数据前先要建立连接;udp是不需要连接,即刻传输数据。2)服务对象:tcp是一对一的两点服务,即一条连接只有两个端点。udp是支持一对一、一对多、多对多的交互通信。3)可靠性:tcp是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。udp是尽最大努力交付,不保证可靠交付数据。但是我们可以基于 UDP 传输协议实现一个可靠的传输协议,比如 QUIC 协议。4)流量控制:tcp有拥塞控制和流量控制机制,保证数据传输的安全性。udp:则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。5)首部开销:tcp首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。udp首部只有 8 个字节,并且是固定不变的,开销较小。6)传输方式:tcp是流式传输,没有边界,但保证顺序和可靠。udp:是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

6、udp与tcp的场景?

1)tcp场景:由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:FTP 文件传输;HTTP / HTTPS;2)udp场景:由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:包总量较少的通信,如 DNS 、SNMP 等;视频、音频等多媒体通信;广播通信

7、为什么tcp 头部没有【包长度】字段呢?

原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。

8、tcp 三次握手过程&状态变化?

  • 一开始,客户端和服务端都处于 CLOSE 状态。先是服务端主动监听某个端口,处于 LISTEN 状态
  • 客户端会随机初始化序号(client_isn),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1 ,表示 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT 状态。
  • 服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn 1, 接着把 SYNACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
  • 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
  • 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。

从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。

一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。

9、linux系统中如何查看tcp状态?

TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。

10、为什么是3次握手?而不是其他次数?

笼统回答:“因为三次握手才能保证双方具有接收和发送的能力。”

精细回答:我们知道了什么是 TCP 连接:用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小并建立 TCP 连接。接下来,以三个方面分析三次握手的原因:

  • 三次握手才可以阻止重复历史连接的初始化(主要原因)首要原因是为了防止旧的重复连接初始化造成混乱。
  • 三次握手才可以同步双方的初始序列号
  • 三次握手才可以避免资源浪费

11、tcp每次链接为什么初始化序列号都不一致呢?

主要原因有两个方面:1)为了防止历史报文被下一个相同四元组的连接接收(主要方面);2)为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收;

12、初始化序列号 如何产生的?

起始 ISN 是基于时钟的,每 4 微秒 1,转一圈要 4.55 个小时。

RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M F(localhost, localport, remotehost, remoteport)。

  • M 是一个计时器,这个计时器每隔 4 微秒加 1。
  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

13、握手中断,会发生什么?

客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。

当客户端超时重传 3 次 SYN 报文后,由于 tcp_syn_retries 为 3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。

14、tcp 四次挥手的过程&状态变化?

双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下图:

  • 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

15、为何time_wait 等待时间是2msl?

网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

16、为什么需要time_wait 这个状态?

主动发起关闭连接的一方,才会有 TIME-WAIT 状态。

需要 TIME-WAIT 状态,主要是两个原因:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收;
  • 保证「被动关闭连接」的一方,能被正确的关闭;

17、time_wait 过多什么危害?

过多的 TIME-WAIT 状态主要的危害有两种:

  • 第一是占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等;
  • 第二是占用端口资源,端口资源也是有限的,一般可以开启的端口为 32768~61000,也可以通过 net.ipv4.ip_local_port_range参数指定范围。

客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。

如果客户端(主动发起关闭连接方)的 TIME_WAIT 状态过多,占满了所有端口资源,那么就无法对「目的 IP 目的 PORT」都一样的服务端发起连接了,但是被使用的端口,还是可以继续对另外一个服务端发起连接的。具体可以看我这篇文章:客户端的端口可以重复使用吗?(opens new window)

因此,客户端(发起连接方)都是和「目的 IP 目的 PORT 」都一样的服务端建立连接的话,当客户端的 TIME_WAIT 状态连接过多的话,就会受端口资源限制,如果占满了所有端口资源,那么就无法再跟「目的 IP 目的 PORT」都一样的服务端建立连接了。

不过,即使是在这种场景下,只要连接的是不同的服务端,端口是可以重复使用的,所以客户端还是可以向其他服务端发起连接的,这是因为内核在定位一个连接的时候,是通过四元组(源IP、源端口、目的IP、目的端口)信息来定位的,并不会因为客户端的端口一样,而导致连接冲突。

如果服务端(主动发起关闭连接方)的 TIME_WAIT 状态过多,并不会导致端口资源受限,因为服务端只监听一个端口,而且由于一个四元组唯一确定一个 TCP 连接,因此理论上服务端可以建立很多连接,但是 TCP 连接过多,会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。

18、如何优化time_wait?

这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
  • net.ipv4.tcp_max_tw_buckets
  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

方式一:net.ipv4.tcp_tw_reuse 和 tcp_timestamps

如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。有一点需要注意的是,tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。

代码语言:javascript复制
net.ipv4.tcp_tw_reuse = 1

使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即

代码语言:javascript复制
net.ipv4.tcp_timestamps=1(默认即为 1)

这个时间戳的字段是在 TCP 头部的「选项」里,它由一共 8 个字节表示时间戳,其中第一个 4 字节字段用来保存发送该数据包的时间,第二个 4 字节字段用来保存最近一次接收对方发送到达数据的时间。

由于引入了时间戳,我们在前面提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃。

方式二:net.ipv4.tcp_max_tw_buckets

这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将后面的 TIME_WAIT 连接状态重置,这个方法比较暴力。

方式三:程序中使用 SO_LINGER

我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。

结尾:

《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。

如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT。

github 仓库代码11月9~10日更新,敬请期待,仓库地址在文章顶部开头标注。

0 人点赞