作者: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
, 接着把 SYN
和 ACK
标志位置为 1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD
状态。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 四次挥手的过程&状态变化?
双方都可以主动断开连接,断开连接后主机中的「资源」将被释放,四次挥手的过程如下图:
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 状态主要的危害有两种:
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 和 tcp_timestamps
如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。有一点需要注意的是,tcp_tw_reuse 功能只能用客户端(连接发起方),因为开启了该功能,在调用 connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。
net.ipv4.tcp_tw_reuse = 1
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
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日更新,敬请期待,仓库地址在文章顶部开头标注。