一、语雀-Java并发面试题
1、进程与线程
1.1 线程特点
1.2 类变量、成员变量和局部变量
Java中共有三种变量,分别是类变量、实例变量和局部变量。他们分别存放在JVM的方法区(元空间)、堆内存和栈内存中。
2、线程有几种状态,状态之间的流转是怎样的?
| 维度 | 等待(Wait) | 超时等待(Timed Wait) |
|---|---|---|
| 目的 | 等待条件满足 | 等待条件满足或超时后恢复 |
| 时间限制 | 无时间限制(可能永久阻塞) | 强制设定超时时间 |
| 锁释放 | 自动释放锁(在同步块中) | 取决于方法(如 Thread.sleep() 不释放锁) |
| 唤醒机制 | 依赖 notify()/notifyAll() | 自动恢复(超时后)或手动唤醒 |
| 适用场景 | 条件驱动的协作(如生产者-消费者) | 有时间要求的任务(如定时任务、连接超时) |
3、什么是守护线程,和普通线程有什么区别?
3、JDK21 中的虚拟线程是怎么回事?
3.1 虚拟线程
4、创建线程有几种方式?
5、sleep和wait区别
6、什么是线程池,如何实现的?
6.1 Executors
7、线程数设定成多少更合适?
8、什么是ThreadLocal,如何实现的?
ThreadLocal 变量是为每个线程提供了独立的副本,因此不同线程之间只能访问它们自己的副本。
1. ThreadLocal 的正确使用场景
ThreadLocal 适用于以下场景,而非线程间通信:
-
线程本地上下文:
- 如存储用户身份信息(在 Web 请求处理中,每个线程处理独立请求)。
- 日志记录(在多线程环境中,记录每个线程的专属日志信息)。
-
避免同步开销:
- 在单线程内多次访问某个变量(如循环中的临时缓存),无需同步。
-
框架内部使用:
- Spring 的
RequestContextHolder使用ThreadLocal存储 HTTP 请求上下文。
- Spring 的
2. ThreadLocal 的潜在风险
- 内存泄漏:
在线程池中,线程会被复用。如果线程 A 使用了ThreadLocal并未释放,线程 B 可能会读取到线程 A 的旧数据(脏数据)。
解决方案:在任务结束时调用ThreadLocal.remove()清理数据。
9、线程之间如何通信
线程间通信是多线程编程的核心问题之一,Java 提供了多种机制来实现线程间的协作。以下是常见的通信方式及其原理、示例和适用场景:
9.1 共享内存(Shared Memory)
原理:
线程共享同一块内存区域(如堆中的对象),通过修改共享变量的值来传递信息。但需要同步机制(如 synchronized)避免竞态条件。
示例:
public class SharedCounter {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// 线程A调用increment()
// 线程B调用getCount()
关键点:
• 原子性:对共享变量的操作必须是原子的(如 synchronized 或 Lock)。
• 可见性:修改共享变量后需确保其他线程能看到最新值(volatile 或 Thread.sleep() 触发缓存刷新)。
9.2 wait/notify 机制
原理:
基于 Object 类的 wait()、notify() 和 notifyAll() 方法,必须在同步块中调用。线程通过等待/唤醒机制协调执行。
适用场景:
• 条件驱动的协作(如队列空/满时的等待)。
• 缺点:需手动处理循环条件(如 while 而非 if),否则可能遗漏唤醒。
9.3 BlockingQueue
原理:
Java 提供的阻塞队列(如 ArrayBlockingQueue、LinkedBlockingQueue)内置了线程安全机制,支持生产者放入元素和消费者取出元素的自动阻塞/唤醒。
优势:
• 简化代码逻辑,避免手动调用 wait()/notify()。
• 天然支持线程安全,无需额外同步。
9.4 Lock 接口
原理:
通过 ReentrantLock 或 ReentrantReadWriteLock 显式控制锁的获取和释放,结合 Condition 实现更灵活的等待/通知机制。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;
public class LockBasedCounter {
private int count = 0;
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try {
count++;
condition.signalAll(); // 唤醒等待的线程
} finally {
lock.unlock();
}
}
public int getCount() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
condition.await(); // 等待条件满足
}
return count;
} finally {
lock.unlock();
}
}
}
优势:
• 支持更细粒度的锁(如读写锁)。
• 可避免 wait()/notify() 的某些限制(如无法中断等待)。
9.5 volatile 关键字
原理:
• 可见性:确保变量修改对其他线程立即可见(直接写入主内存,绕过 CPU 缓存)。
• 非原子性:仅保证可见性,不保证复合操作(如 i++)的原子性。
• 适用场景:状态标志(如 boolean running = false 控制线程终止)。
public class VolatileFlag {
private volatile boolean running = true;
public void run() {
while (running) {
// 业务逻辑
}
}
public void stop() {
running = false; // 其他线程立即看到变化
}
}
注意事项:
• 不能替代锁:若需原子操作(如计数器),需配合 synchronized 或 AtomicInteger。
9.6 CAS(Compare-And-Swap)
原理:
通过 java.util.concurrent.atomic 包中的原子类(如 AtomicInteger、AtomicBoolean)实现无锁编程。CAS 是一种乐观锁机制,通过比较并交换值来保证原子性。
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子性自增
}
public int getCount() {
return count.get();
}
}
优势:
• 高性能,避免锁的开销。
• 适用于计数器、状态标志等简单原子操作。
9.7 通信方式对比表
| 机制 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 共享内存 + 同步 | 简单直接 | 易死锁,需手动处理等待条件 | 基础同步需求 |
| wait/notify | 非常适合条件驱动模型 | 代码复杂,需严格遵循同步规则 | 生产者-消费者、资源池管理 |
| BlockingQueue | 线程安全,简化代码 | 性能略低于手动实现 | 高并发生产者-消费者系统 |
| Lock + Condition | 灵活控制锁,支持读写锁 | 代码复杂度高 | 需细粒度锁的场景 |
| volatile | 轻量级,仅保证可见性 | 不支持复合操作 | 状态标志、终止信号 |
| CAS | 无锁,高性能 | 仅适用于简单原子操作 | 计数器、序列生成器 |
ThreadLocal 变量是为每个线程提供了独立的副本,因此不同线程之间只能访问它们自己的副本。
• 共享内存是基础,但必须结合同步机制。
• wait/notify 和 BlockingQueue 适合典型的生产者-消费者模型。
• Lock + Condition 提供更灵活的线程协作能力。
• volatile 和 CAS 适用于特定场景的轻量级同步需求。
10、什么是Java内存模型(JMM)?
二、小林-计网应用层面试题
1、如果我把数据存储到 localStorage,和Cookie有什么区别?
- 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合;
- 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据;
- 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除;
- 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些;
2、什么数据应该存在到cookie,什么数据存放到 Localstorage
Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据。
3、JWT 令牌和传统方式有什么区别?
- 无状态性:JWT是无状态的令牌,不需要在服务器端存储会话信息。相反,JWT令牌中包含了所有必要的信息,如用户身份、权限等。这使得JWT在分布式系统中更加适用,可以方便地进行扩展和跨域访问。
- 安全性:JWT使用密钥对令牌进行签名,确保令牌的完整性和真实性。只有持有正确密钥的服务器才能对令牌进行验证和解析。这种方式比传统的基于会话和Cookie的验证更加安全,有效防止了CSRF(跨站请求伪造)等攻击。
- 跨域支持:JWT令牌可以在不同域之间传递,适用于跨域访问的场景。通过在请求的头部或参数中携带JWT令牌,可以实现无需Cookie的跨域身份验证。
4、JWT 令牌都有哪些字段?
JWT令牌由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。其中,头部和载荷均为JSON格式,使用Base64编码进行序列化,而签名部分是对头部、载荷和密钥进行签名后的结果。
5、JWT 令牌为什么能解决集群部署,什么是集群部署?
在传统的基于会话和Cookie的身份验证方式中,会话信息通常存储在服务器的内存或数据库中。但在集群部署中,不同服务器之间没有共享的会话信息,这会导致用户在不同服务器之间切换时需要重新登录,或者需要引入额外的共享机制(如Redis),增加了复杂性和性能开销。
而JWT令牌通过在令牌中包含所有必要的身份验证和会话信息,使得服务器无需存储会话信息,从而解决了集群部署中的身份验证和会话管理问题。当用户进行登录认证后,服务器将生成一个JWT令牌并返回给客户端。客户端在后续的请求中携带该令牌,服务器可以通过对令牌进行验证和解析来获取用户身份和权限信息,而无需访问共享的会话存储。
由于JWT令牌是自包含的,服务器可以独立地对令牌进行验证,而不需要依赖其他服务器或共享存储。这使得集群中的每个服务器都可以独立处理请求,提高了系统的可伸缩性和容错性。
6、jwt的缺点是什么?
JWT 一旦派发出去,在失效之前都是有效的,没办法即使撤销JWT。
要解决这个问题的话,得在业务层增加判断逻辑,比如增加**黑名单机制。**使用内存数据库比如 Redis 维护一个黑名单,如果想让某个 JWT 失效的话就直接将这个 JWT 加入到 黑名单 即可。然后,每次使用 JWT 进行请求的话都会先判断这个 JWT 是否存在于黑名单中。
7、JWT 令牌如果泄露了,怎么解决,JWT是怎么做的?
- 及时失效令牌:当检测到JWT令牌泄露或存在风险时,可以立即将令牌标记为失效状态。服务器在接收到带有失效标记的令牌时,会拒绝对其进行任何操作,从而保护用户的身份和数据安全。
- 刷新令牌:JWT令牌通常具有一定的有效期,过期后需要重新获取新的令牌。当检测到令牌泄露时,可以主动刷新令牌,即重新生成一个新的令牌,并将旧令牌标记为失效状态。这样,即使泄露的令牌被恶意使用,也会很快失效,减少了被攻击者滥用的风险。
- 使用黑名单:服务器可以维护一个令牌的黑名单,将泄露的令牌添加到黑名单中。在接收到令牌时,先检查令牌是否在黑名单中,如果在则拒绝操作。这种方法需要服务器维护黑名单的状态,对性能有一定的影响,但可以有效地保护泄露的令牌不被滥用。
8、前端是如何存储JWT的?
JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案。互联网服务离不开用户认证。
一般流程如下:
- 用户向服务器发送用户名和密码。
- 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
- 服务器向用户返回一个 session_id,写入用户的 Cookie。
- 用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。
- 服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。
这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。
举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?
一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。
客户端收到服务器返回的 JWT,可以储存在 Local Storage 里面,也可以储存在Cookie里面,还可以存储在Session Storage里面。下面将说明存在上述各个地方的优劣势:
Local Storage(本地存储)
- 优点:Local Storage 提供了较大的存储空间(一般为5MB),且不会随着HTTP请求一起发送到服务器,因此不会出现在HTTP缓存或日志中。
- 缺点:存在XSS(跨站脚本攻击)的风险,恶意脚本可以通过JavaScript访问到存储在Local Storage中的JWT,从而盗取用户凭证。
Session Storage(会话存储)
- 优点:与Local Storage类似,但仅限于当前浏览器窗口或标签页,当窗口关闭后数据会被清除,这在一定程度上减少了数据泄露的风险。
- 缺点:用户体验可能受影响,因为刷新页面或在新标签页打开相同应用时需要重新认证。
Cookie
- 优点:可以设置HttpOnly标志来防止通过JavaScript访问,减少XSS攻击的风险;可以利用Secure标志确保仅通过HTTPS发送,增加安全性。
- 缺点:大小限制较小(通常4KB),并且每次HTTP请求都会携带Cookie,可能影响性能;设置不当可能会受到CSRF(跨站请求伪造)攻击。
9、为什么有HTTP协议了?还要用RPC?
- RPC 本质上不算是协议,而是一种调用方式,而像 gRPC 和 Thrift 这样的具体实现,才是协议,它们是实现了 RPC 调用的协议。目的是希望程序员能像调用本地方法那样去调用远端的服务方法。同时 RPC 有很多种实现方式,不一定非得基于 TCP 协议。
- 从发展历史来说,HTTP 主要用于 B/S 架构,而 RPC 更多用于 C/S 架构。但现在其实已经没分那么清了,B/S 和 C/S 在慢慢融合。很多软件同时支持多端,所以对外一般用 HTTP 协议,而内部集群的微服务之间则采用 RPC 协议进行通讯。
- RPC 其实比 HTTP 出现的要早,且比目前主流的 HTTP/1.1 性能要更好,所以大部分公司内部都还在使用 RPC。
- HTTP/2.0在 HTTP/1.1的基础上做了优化,性能可能比很多 RPC 协议都要好,但由于是这几年才出来的,所以也不太可能取代掉 RPC。
10、HTTP长连接与WebSocket有什么区别?
- 全双工和半双工:TCP 协议本身是全双工的,但我们最常用的 HTTP/1.1,虽然是基于 TCP 的协议,但它是半双工的,对于大部分需要服务器主动推送数据到客户端的场景,都不太友好,因此我们需要使用支持全双工的 WebSocket 协议。
- 应用场景区别:在 HTTP/1.1 里,只要客户端不问,服务端就不答。基于这样的特点,对于登录页面这样的简单场景,可以使用定时轮询或者长轮询的方式实现服务器推送(comet)的效果。对于客户端和服务端之间需要频繁交互的复杂场景,比如网页游戏,都可以考虑使用 WebSocket 协议。
11、Nginx有哪些负载均衡算法?
Nginx支持的负载均衡算法包括:
- 轮询:按照顺序依次将请求分配给后端服务器。这种算法最简单,但是也无法处理某个节点变慢或者客户端操作有连续性的情况。
- IP哈希:根据客户端IP地址的哈希值来确定分配请求的后端服务器。适用于需要保持同一客户端的请求始终发送到同一台后端服务器的场景,如会话保持。
- URL哈希:按访问的URL的哈希结果来分配请求,使每个URL定向到一台后端服务器,可以进一步提高后端缓存服务器的效率。
- 最短响应时间:按照后端服务器的响应时间来分配请求,响应时间短的优先分配。适用于后端服务器性能不均的场景,能够将请求发送到响应时间快的服务器,实现负载均衡。
- 加权轮询:按照权重分配请求给后端服务器,权重越高的服务器获得更多的请求。适用于后端服务器性能不同的场景,可以根据服务器权重分配请求,提高高性能服务器的利用率。
12、Nginx位于七层网络结构中的哪一层?
应用层,nginx 是七层负载均衡。
三、小林-计网传输层面试题
1、说一下tcp的头部
标注颜色的表示与本文关联比较大的字段,其他字段不做详细阐述。
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
2、TCP三次握手过程说一下?
TCP 是面向连接的协议,所以使用 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 状态。
- 客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次「确认应答号」字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED 状态。
- 服务端收到客户端的应答报文后,也进入 ESTABLISHED 状态。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
3、tcp为什么需要三次握手建立连接?
三次握手的原因:
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
原因一:避免历史连接
我们来看看 RFC 793 指出的 TCP 连接使用三次握手的首要原因:
The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.
简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。
我们考虑一个场景,客户端先发送了 SYN(seq = 90)报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100)报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。
看看三次握手是如何阻止历史连接的:
客户端连续发送多次 SYN(都是同一个四元组)建立连接的报文,在网络拥堵情况下:
- 一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端,那么此时服务端就会回一个 SYN + ACK 报文给客户端,此报文中的确认号是 91(90+1)。
- 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文。
- 服务端收到 RST 报文后,就会释放连接。
- 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。
上述中的「旧 SYN 报文」称为历史连接,TCP 使用三次握手建立连接的最主要原因就是防止「历史连接」初始化了连接。
如果是两次握手连接,就无法阻止历史连接,那为什么 TCP 两次握手为什么无法阻止历史连接呢?
我先直接说结论,主要是因为在两次握手的情况下,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接,造成资源浪费。
你想想,在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时可以给对方发送数据,但是客户端此时还没有进入 ESTABLISHED 状态,假设这次是历史连接,客户端判断到此次连接为历史连接,那么就会回 RST 报文来断开连接,而服务端在第一次握手的时候就进入 ESTABLISHED 状态,所以它可以发送数据的,但是它并不知道这个是历史连接,它只有在收到 RST 报文后,才会断开连接。
可以看到,如果采用两次握手建立 TCP 连接的场景下,服务端在向客户端发送数据前,并没有阻止掉历史连接,导致服务端建立了一个历史连接,又白白发送了数据,妥妥地浪费了服务端的资源。
因此,要解决这种现象,最好就是在服务端发送数据前,也就是建立连接之前,要阻止掉历史连接,这样就不会造成资源浪费,而要实现这个功能,就需要三次握手。
所以,TCP 使用三次握手建立连接的最主要原因是防止「历史连接」初始化了连接。
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的(通过 ACK 报文中的序列号知道);
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手与三次握手
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有「两次握手」,当客户端发生的 SYN 报文在网络中阻塞,客户端没有接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK 报文,所以服务端每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端发送的 SYN 报文在网络中阻塞了,重复发送多次 SYN 报文,那么服务端在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
即两次握手会造成消息滞留情况下,服务端重复接受无用的连接请求 SYN 报文,而造成重复分配资源。
4、TCP 三次握手,客户端第三次发送的确认包丢失了发生什么?
客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。
因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文。
举个例子,假设 tcp_synack_retries 参数值为 2,那么当第三次握手一直丢失时,发生的过程如下图:
具体过程:
- 当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
5、服务端发送第二个报文后连接的状态进入什么状态
syn_rcvd 状态
6、三次握手和 accept 是什么关系? accept 做了哪些事情?
tcp 完成三次握手后,连接会被保存到内核的全连接队列,调用 accpet 就是从把连接取出来给用户程序使用。
7、客户端发送的第一个 SYN 报文,服务器没有收到怎么办?
当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。
在这之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且重传的 SYN 报文的序列号都是一样的。
不同版本的操作系统可能超时时间不同,有的 1 秒的,也有 3 秒的,这个超时时间是写死在内核里的,如果想要更改则需要重新编译内核,比较麻烦。
当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?
在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries内核参数控制,这个参数是可以自定义的,默认值一般是 5。
# cat /proc/sys/net/ipv4/tcp_syn_retries
5
通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。
当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。
所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右。
举个例子,假设 tcp_syn_retries 参数值为 3,那么当客户端的 SYN 报文一直在网络中丢失时,会发生下图的过程:
具体过程:
- 当客户端超时重传 3 次 SYN 报文后,由于 tcp_syn_retries 为 3,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
8、服务器收到第一个 SYN 报文,回复的 SYN + ACK 报文丢失了怎么办?
当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。
第二次握手的 SYN-ACK 报文其实有两个目的 :
- 第二次握手里的 ACK, 是对第一次握手的确认报文;
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。
那么,如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。
# cat /proc/sys/net/ipv4/tcp_synack_retries
5
因此,当第二次握手丢失了,客户端和服务端都会重传:
- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
- 服务端会重传 SYN-ACK 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。
举个例子,假设 tcp_syn_retries 参数值为 1,tcp_synack_retries 参数值为 2,那么当第二次握手一直丢失时,发生的过程如下图:
具体过程:
- 当客户端超时重传 1 次 SYN 报文后,由于 tcp_syn_retries 为 1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
- 当服务端超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。