概述
TCP是面试必考的知识点,它是庞大且复杂的,现将TCP相关知识总结为如下几个部分:
- TCP基础
- TCP建立连接
- TCP断开连接
- Socket编程
一. TCP基础
TCP头部格式
首先看看TCP首部的格式,这里只阐述与本文关联较大的字段: 序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包发送给接收端主机。每发送一次数据,就累加一次该数据字节数的大小。用来解决网络包乱序问题。
确认应答号:指下一次期望收到的数据的序列号,发送端收到这个确认应答之后可以认为在这个序号以前的数据都已经被正常接收。 用来解决不丢包的问题。
控制位:
- ACK:该位为
1 时,确认应答的字段变为有效,TCP规定除了最初建立连接时的SYN 包之外该位必须置为1 ; - RST:该位为
1 时,表示TCP连接中出现异常必须强制断开连接; - SYN:该位为
1 时,表示希望建立连接,并在其序列号的字段进行序列号初始值的设定; - FIN:该位为
1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN 位为1 的TCP段;
为什么需要TCP协议?TCP工作在哪一层?
IP 层是不可靠的,因为它:
- 不保证网络包的交付;
- 不保证网络包的按序交付;
- 不保证网络包中的数据完整性;
如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的TCP 协议来负责。
因为TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
什么是 TCP ?
TCP 是面向连接的、可靠的、基于字节流的传输层协议。
- 面向连接:一定是一对一才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息也就是一对多,TCP是无法做到的;
- 可靠的:无论网络链路中出现了怎样的链路变化,TCP 都可以保证报文一定能够到达接收端;
- 字节流:消息是没有边界的,所以无论消息有多大都可以进行时传输。并且消息是有序的,当前一个消息没有收到的时候,即使它先收到了后面的字节,那么也不能扔给应用层去处理,同时对重复的报文会自动丢弃。
什么是 TCP 连接?
TCP连接 简单来说就是:用于保证可靠性和流量控制所维护的某些状态信息,这些信息的组合,包括Socket、序列号、和窗口大小称为连接。
所以我们可以知道,建立一个TCP连接是需要客户端与服务器达成上述三个信息的共识。
- Socket:由IP地址和端口号组成;
- 序列号:用来解决乱序问题等;
- 窗口大小:用来做流量控制。
如何唯一确定一个 TCP 连接呢?
TCP 四元组可以唯一确定一个连接,四元组包括如下:
源地址和目的地址的字段(32位)是在 IP 头部中,作用是:通过 IP 协议发送报文给对方主机。
源端口和目的端口的字段(16位)是在 TCP 头部中,作用是:告诉 TCP 协议应该把报文发给哪个进程。
有一个IP的服务器监听了一个端口,它的TCP最大连接数是多少 ?
服务器通常固定在某个本地端口上监听,等待客户端的连接请求。
因此,客户端 IP 和端口号是可变的,其理论值计算公式如下:
最大 TCP 连接数 = 客户端 IP 数 ?? 客户端端口数
对 IPV4 ,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是说服务器单机的最大TCP连接数,约为 2 的 48 次方。
当然,服务器最大并发的TCP连接数远不能达到理论上限,有两方面原因:
- 首先主要是文件描述符限制,Socket 都是文件,所以首先要通过
ulimit 配置文件描述符的数目; - 另一个是内存限制,每个TCP连接都要占用一定内存,操作系统的内存是有限的。
UDP 和 TCP 有什么区别呢?应用场景分别是什么?
UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务;
UDP 协议是十分简单的,头部只有 8 个字节(64位) ,UDP头部格式如下:
- 目标和源端口:主要是告诉
UDP 协议应该把报文发送给哪个进程; - 包长度:该字段保存了
UDP 首部长度和数据长度之和; - 校验和:校验和是为了提供可靠的UDP首部和数据而设计;
TCP 和 UDP 的区别
1. 连接
- TCP 是面向连接的传输层协议,传输数据前先要建立连接;
- UDP 不需要连接,即刻传输数据。
2. 服务对象
- TCP 是一对一的两点服务,即一条连接只有两个端点;
- UDP 支持一对一、一对多、多对多的交互通信。
3. 可靠性
- TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达;
- UDP 是尽最大努力交付,不保证可靠交付数据。
4. 拥塞控制、流量控制
- TCP 有拥塞控制和流量控制机制,保证数据传输的安全性;
- UDP 则没有,即使网络非常拥堵了,也不会影响UDP的发送速率。
5. 首部开销
- TCP 首部长度较长,会有一定的开销。首部在没有使用
选项 字段时是20 个字节,如果使用了选项字段则会变长; - UDP 首部只有
8 个字节,并且是固定不变的,开销较小。
6. 传输方式
- TCP 是流式传输,没有边界,但保证顺序和可靠;
- UDP 是一个包一个包的发送,是有边界的,但可能丢包和乱序。
7. 分片方式
- TCP 的数据大小如果大于
MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装TCP数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片; - UDP 的数据大小如果大于
MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,在实现可靠传输的UDP时就需要重传所有的数据包,这样传输效率非常差,所以通常UDP的报文应该小于MTU。
TCP 和 UDP 的应用场景
由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:
由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:
- 数据包总量较少的通信,如
DNS 、SNMP 等; - 视频、音频等多媒体通信;
- 广播通信。
TCP/UDP 首部字段的区别
***1. 为什么UDP头没有首部长度字段,而TCP头有首部长度字段呢?***
原因是:TCP 头有一个可变长的选项 字段,而 UDP 头部长度则是不会变化的,所以就无需多一个字段去记录 UDP 的首部长度。
***2. 为什么UDP头有包长度字段,而TCP头则没有包长度字段呢?***
先说说 TCP 是如何计算负载数据长度:
TCP 数据长度 = IP 总长度 - IP 首部长度 - TCP 首部长度
其中IP 总长度 和IP首部长度 ,在 IP 首部中是已知的。而TCP 首部长度 ,则是在TCP首部中是已知的,所以就可以求得 TCP 数据的长度。
你此时可能会有一些疑惑:“UDP 也是基于IP层的,那么UDP的数据长度应该也可以通过上面这个公式计算,为何还需要包长度 字段呢?” 如果这么看,确实感觉UDP的包长度 是冗余的;
但实际上,这主要是为了网络设备硬件设计和处理方便,首部长度需要是 4 字节的整数倍。
二. TCP建立连接
TCP 三次握手和状态迁移
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。下面就来详细剖析一下这个过程:
- 一开始,客户端和服务器都处于
CLOSED 状态。先是服务器主动监听某个端口,处于 LISTEN 状态; - 第一次握手: 客户端会随机初始化序列号
client_isn ,将此序号置于 TCP 首部的序号字段中,同时把 SYN 标志位置为 1 ,表示为 SYN 报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN - SENT 状态;(下图为第一个 SYN 报文)
- 第二次握手:服务端收到客户端的
SYN 报文后,首先服务端也先随机初始化自己的序号 server - isn ,将此序号填入 TCP 首部的序号字段中,然后在 TCP 首部的确认应答号字段填入 client - isn + 1 ,接着将 SYN 和 ACK 标识位都置为 1 。最后把该报文发送给客户端,该报文也不包含应用层数据,之后服务端处于 SYN - RCVD 状态;(下图为第二个 SYN + ACK 报文)
- 第三次握手:客户端收到服务端报文后,还要向服务端回应最后一个应答报文。首先该应答报文 TCP 首部
ACK 标志位置为 1 ,然后确认应答号字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户端到服务器的数据,之后客户端处于 ESTABLISHED 状态;(下图为第三个 ACK 报文)
- 最后,当服务端收到客户端的应答报文后,也进入
ESTABLISHED 状态。
从上面的过程可以发现,第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常考的问题。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 中可以通过 netstat - napt 命令查看:
为什么是三次握手?而不是两次、四次?
相信大家一般都会回答:“因为三次握手才能保证双方具有接收和发送的能力”。这回答确实没问题,但太片面了并没有说出主要的原因。
在前面我们知道了什么是TCP 连接:
用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括<font color=#0000FF Socket、序列号和窗口大小称为连接。
所以,重要的是为什么三次握手才可以初始化Socket、序列号和窗口大小并建立 TCP 连接。
接下来以三个方面分析三次握手的原因:
- 阻止重复历史连接的初始化(主要原因);
- 同步双方的初始序列号;
- 避免资源浪费;
原因一:避免历史连接
简单来说,三次握手的主要原因是为了防止旧的重复连接初始化造成混乱。
网络环境是错综复杂的,往往并不是像我们期望的一样,先发送的数据包就先到达目标主机,可能会由于网络拥堵等乱七八糟的原因,会使得旧数据包先到达目标主机,那么这种情况下 TCP 三次握手是如何避免的呢?
如上图所示,客户端连续发送多次建立连接的 SYN 报文,在网络拥堵情况下:
- 一个旧SYN报文比最新的SYN报文更早地到达了服务端;
- 那么此时服务端就会回一个
SYN + ACK 报文给客户端; - 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送
RST 报文给服务端,表示中止这一次连接。
如果是两次握手建立连接,就不能判断当前连接是否为历史连接,,三次握手则可以在客户端 (发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否为历史连接:
- 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是
RST 报文,以此中止历史连接; - 如果不是历史连接,则第三次握手发送的就是
ACK 报文,通信双方就会成功建立连接。
所以,TCP 使用三次握手建立连接的最主要原因就是:防止历史连接初始化造成混乱。
原因二:同步双方初始序列号
TCP 协议的通信双方,都必须维护一个序列号,序列号是实现可靠传输的一个关键因素,它的作用是:
- 接收方可以去除重复的数据;
去重 - 接收方可以根据数据包的序列号按序接收;
按序接收 - 可以标识发送出去的数据包中,哪些是已经被对方收到的;
记录
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带初始序列号的 SYN 报文时,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已经被服务端成功接收,那当服务端发送初始序列号给客户端的时候,同样也要得到客户端的回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
下面是三次握手与四次握手的对比图:
如上图所示,四次握手其实也能够可靠的同步双方的初始序列号,但由于第二步和第三步可以优化成一步,所以就成了三次握手。
然而,两次握手只能够保证一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有两次握手,当客户端的 SYN 连接请求在网络中阻塞,如果客户端迟迟接收不到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器就不清楚客户端是否接收到了自己发送的建立连接的 ACK ,所以只能每收到一个 SYN 就先主动建立一个连接,这样会造成什么情况呢?
如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效连接,造成不必要的资源浪费。
也就是说,两次握手导致的问题是:在消息滞留情况下,服务器重复接收到冗余的连接请求 SYN 报文,而造成重复分配资源。
ps: 这里实际上又回到了第一种情况,当旧SYN 报文抵达服务器后,服务器会回给客户端一个ACK ,此时客户端根据上下文判断当前连接为历史连接,直接发送给服务器一个RST 表示中止当前连接。
相关问题解析
1. 为什么是三次握手?不是两次、四次?
TCP 建立连接时,通过三次握手能够防止历史连接的建立,减少双方不必要的资源开销,能帮助双方同步初始序列号。序列号能够保证数据包不重复、不丢失和按序传输。
不使用两次握手和三次握手的原因:
- 两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 四次握手:三次握手就已经建立可靠连接,所以不需要使用更多的通信次数。
2. 初始序列号ISN是如何产生的?为什么两端的ISN不同?
起始 ISN 是基于时钟的,每 4 ms + 1,转一圈要 4.55 个小时。
RFC1948 中提出了一个较好的初始化序列号 ISN 随机生成算法。
ISN = M + F(localhost, localport, remotehost, remoteport)
M 是一个计时器,这个计时器每隔 4 ms + 1;F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成的一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。
下面解释为什么客户端和服务端的初始序列号是不同的:
如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中。如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新连接接收了,则会产生数据错乱。
3. 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
我们先来认识下 MTU 和 MSS :
- MTU:一个网络包的最大长度,以太网中一般为
1500 字节; - MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度。
如果将 TCP 的整个报文(头部和数据)交给 IP 层进行分片,会有什么异常呢?
当 IP 层有一个超过 MTU 大小的数据(TCP 头 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,当对端接收后由目标主机的 IP 层来进行重新组装后,再交给上一层 TCP 层进行处理。
上面的过程看起来井然有序,但实际上是存在隐患的,如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。这是因为 IP 层本身没有超时重传机制,当 IP 分片丢失后并不会返回响应给对端。
因此,由 IP 层进行分片传输是效率很低的。所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 的时候就会先进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU,自然就不需要 IP 分片了。
经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率
4. 什么是SYN攻击?如何避免SYN攻击?
-
SYN 攻击防御 关于 SYN 攻击防御的方法有很多,这里只提一种常见的:Cookie 源认证 原理是:syn报文首先由DDOS防护系统来响应syn_ack。带上特定的sequence number (记为cookie)。真实的客户端会返回一个ack 并且Acknowledgment number 为cookie+1。 而伪造的客户端,将不会作出响应。这样我们就可以知道那些IP对应的客户端是真实的,将真实客户端IP加入白名单。下次访问直接通过,而其他伪造的syn报文就被拦截。 网上关于这方面的博文有很多,感兴趣的可以自行搜索。
三. TCP断开连接
TCP四次挥手和状态迁移
正所谓天下没有不散的宴席,对于 TCP 连接也是这样,TCP 断开连接正是通过四次挥手的方式。
双方都可以主动断开连接,断开连接后主机中的资源将被释放。
下面是四次挥手的全过程:
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态; - 服务端收到该报文后,就向客户端发送
ACK 应答报文,接着服务器进入 CLOSED_WAIT 状态; - 客户端收到服务端的
ACK 应答报文后,之后进入 FIN_WAIT2 状态; - 等待服务端处理完数据后,也向客户端发送
FIN 报文,之后服务端进入 LAST_ACK 状态; - 客户端收到服务端的
FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态 - 服务端收到了
ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭; - 客户端在经过
2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。
你可以看到,每个方向都需要一个FIN 和一个ACK ,因此通常被称为四次挥手。
这一点需要注意:主动关闭连接的一方,才有 TIME_WAIT状态。
相关问题解析
***1. 为什么挥手需要四次?***
回顾一下四次挥手双方发送 FIN 包的过程,就能理解为什么需要四次了;
- 当客户端主动关闭连接时,客户端向服务器发送
FIN 包,这仅仅表示客户端不再发送数据了但是还能接收数据; - 服务端收到客户端的
FIN 报文后,先回应一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端表示同意关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理 ,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手多了一次。
2. 关于TIME_WAIT状态
- 为什么 TIME_WAIT 等待的时间是 2MSL?
MSL 与 TTL
MSL 是最大报文生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃;
因为 TCP 报文是基于 IP 协议的,而 IP 头中有一个TTL 字段,是 IP 数据报可以经过的最大路由数,该报文每经过一个路由器此值就减 1 ,当此值为 0 时报文就会被丢弃,同时发送 ICMP 报文通知源主机。
MSL 和 TTL 的区别:MSL 的单位是时间,而 TTL 是经过路由跳数。所以 ** MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已经被自然消亡。**
为什么 TIME_WAIT 要等待 2MSL?
先给出答案再进行分析:
- 理由1:为了可靠地实现全双工连接的终止;
- 理由2:为了保证本连接持续的时间所产生的所有分组都从网络中消失。
首先看理由1,为了可靠地实现全双工连接的终止,假设客户端发送的最后一个 ACK 丢失,服务端将重传 FIN ,为了能够接收到这个超时重传的 FIN ,客户端需要 TIME_WAIT 状态;
那 TIME_WAIT 状态就必须是 2MSL 了吗?其实这个要看服务端 FIN 的超时重传时间 RTO , 如果 RTO 小于 MSL,那么 TIME_WAIT 状态 MSL 就够了,如果 RTO 大于 2MSL 那么 TIME_WAIT 状态 2MSL 也是不够的。所以只有 RTO 在 MSL 和 2MSL 之间的时候,TIME_WAIT 状态存在的理由1才是 TIME_WAIT 时间是 2MSL 的原因。其实在一般情况下,RTO 都是远远小于 MSL 的,但考虑到最糟糕的情况 RTO 就设置为了 2MSL。所以,TIME_WAIT 状态为 2MSL 可以保证最糟糕情况下也可以收到超时重传的 FIN 。
TIME_WAIT 的时间为 2MSL 的另外一个重要原因是理由2,为了保证本连接持续的时间所产生的所有分组都从网络中消失。也就是保证新建立一个 TCP 连接时,来自该连接老的重复分组都已经在网络中消失了;
那么 1 个 MSL 不够吗,为什么一定需要 2MSL 呢?假设客户端发送 ACK 刚刚过了 1 个MSL 时间,而服务端在收到这个 ACK 之前一瞬间刚好启动了超时重传 FIN,所以要等这个重传的 FIN 也消失,就是 2MSL 了。
2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME_WAIT 时间内,客户端又接收到了服务端超时重传的 FIN 报文,那么** 2MSL 时间将重新计时。**
在 Linux 系统? 2MSL 默认是 60 秒,那么?个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
其定义在 Linux 内核代码?的名称为 TCP_TIMEWAIT_LEN ;如果要修改 TIME_WAIT 的时间?度,只能修改 Linux 内核代码? TCP_TIMEWAIT_LEN 的值,并重新新编译 Linux 内核。
其实本题在上个问题的答案中已经间接回答了,这里再详细分析一下;
注意,主动发起关闭连接的一方,才会有 TIME_WAIT 状态。需要 TIME_WAIT 状态,主要是两个原因:
- 防止具有相同四元组的旧数据包被收到;
- 保证被动关闭连接的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
原因一:防止旧连接的数据包
经过 2MSL 的时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
原因二:保证连接正确关闭
假设 TIME_WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?
- 如果四次挥手中客户端发送的最后一个 ACK 报文在网络中丢失了,此时客户端的 TIME_WAIT 过短或没有就会直接进入 CLOSED 状态,那么服务端就会一直处在 LAST_ACK 状态;
- 当客户端发起建?连接的 SYN 请求报?后,服务端会发送 RST 报?给客户端,连接建?的过程就会被终?。
如果 TIME-WAIT 等待?够?的情况就会遇到两种情况:
- 服务端正常收到四次挥?的最后?个 ACK 报?,则服务端正常关闭连接;
- 服务端没有收到四次挥?的最后?个 ACK 报?时,则会重发 FIN 报?并等待新的 ACK 报
?。
所以客户端在 TIME-WAIT 状态等待 2MSL 时间后,就可以保证双?的连接都可以正常的关闭。
下面说一些本人的疑惑:
TCP 是如此经典以至于每次看一遍都会有新的理解,上面TIME_WAIT 为什么等待2MSL 有一个理由是:保证全双工连接的正常关闭。对于 MSL ,确实可以保证服务端最后一次重传的 FIN 自然消失,但是万一重传的 FIN 全部在网络中阻塞了服务端就不可能收到任何一个 ACK ,此时客户端由于经历了 2MSL 就进入了 CLOSED 状态,然而服务端由于收不到最后一次 ACK 就会一直处于 LAST_ACK 状态,这时 2MSL 也无法保证服务端正常关闭了。
这里有一种解释(感觉并不严谨),MSL 时间内报文可以绕地球传递几圈,因此若这个时间内都没有收到 FIN 可以假定没有 FIN ,也就是可以认为服务端成功接收到了 ACK 而没有重传 FIN 。
但就算如此,在面临一些极端情况即使是 nMSL 可能也无法保证连接正常关闭,所以说上面的讨论仅仅是在绝大多数正常情况下 才可以成立的。
如果服务器有处于 TIME_WAIT 状态的 TCP ,则说明是由服务器方主动发起的断开请求。
过多的 TIME_WAIT 状态主要的危害有两种:
- 第一是内存资源占用;----
针对服务端 - 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口。----
针对客户端
第二个危害是会造成严重后果的,要知道端口资源也是有限的,一般可以开启的端口为 32768 ~ 61000 ,也可以通过 net.ipv4.ip_local_port_range 进行设置。如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
客户端受端?资源限制:
客户端TIME_WAIT过多,就会导致端?资源被占?,因为端?就65536个,被占满就会导致?法创建新的连接。
服务端受系统资源限制:
由于?个四元组表示 TCP 连接,理论上服务端可以建?很多连接,服务端确实只监听?个端? 但是会把连接扔给处理线程,所以理论上监听的端?可以继续监听。但是线程池处理不了那么多?直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接。
这?给出优化 TIME-WAIT 的?个?式,都是有利有弊:
- 打开
net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项; ipv4.tcp_max_tw_buckets ;- 程序中使?
SO_LINGER ,应?强制使? RST 关闭。
方式一:net.ipv4.tcp_tw_reuse 和 net.ipv4.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 头部的「选项」?,?于记录 TCP 发送?的当前时间戳和从对端接收到的最新时间戳。
由于引?了时间戳,我们在前?提到的 2MSL 问题就不复存在了,因为重复的数据包会因为时间戳过期被?然丢弃。
方式二:ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接?旦超过这个值时,系统就会将后?的 TIME_WAIT 连接状态重置。
这个?法过于暴?,?且治标不治本,带来的问题远?解决的问题多,不推荐使?。
方式三:程序中使用 SO_LINGER
我们可以通过设置 socket 选项,来设置调? close 关闭连接?为:
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger))
如果 l_onoff 为? 0,且 l_linger 值为 0,那么调? close 后,会?该发送?个 RST 标志给对端,该 TCP 连接将跳过四次挥?,也就跳过了 TIME_WAIT 状态,直接关闭。
但这为跨越 TIME_WAIT 状态提供了?个可能,不过是?个?常危险的?为,不值得提倡
3. 如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP 有一个保活机制。这个机制原理是这样的:
定义一个时间段,在这个时间段内如果没有任何连接相关的活动,TCP 保活机制会开始起作用,每隔一个时间间隔发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
net.ipv4.tcp_keepalive_time = 7200 net.ipv4.tcp_keepalive_intvl = 75 net.ipv4.tcp_keepalive_probes = 9
tcp_keepalive_time=7200 :表示保活时间是 7200 秒(2?时),也就 2 ?时内如果没有任何连接相关的活动,则会启动保活机制;tcp_keepalive_intvl=75 :表示每次检测间隔 75 秒;tcp_keepalive_probes=9 :表示检测 9 次?响应,认为对?是不可达的,从?中断本次的连接。
也就是说在 LInux 系统中,最少需要经过 7200 + (75 * 9) = 7875s 才可以发现一个死亡连接。这个时间是有点?的,我们也可以根据实际的需求,对以上的保活相关的参数进?设置。
如果开启了 TCP 保活,需要考虑以下几种情况:
- 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端后,对端会正常响应,这样 TCP 保活时间会被重置,等待下一个保活时间的到来;
- 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
- 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海没有响应,连续发送几次直到达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
四. Socket编程
基于TCP的客户端/服务端Socket编程
相关问题解析
1. listen函数中参数backlog的意义?
2. accept发?在三次握?的哪?步?
3. 客户端调?close,连接断开的流程是什么?
|