【SpringBoot】计算机网络

【Springboot】计算机网络

  • [【一】计算机网络基础模型(OSI 七层 / TCP/IP 四层)](#【一】计算机网络基础模型(OSI 七层 / TCP/IP 四层))
    • [【1】TCP/IP 四层模型](#【1】TCP/IP 四层模型)
    • 【2】关键概念拆解
    • [【3】TCP 三次握手(建立连接)](#【3】TCP 三次握手(建立连接))
    • [【4】TCP 四次挥手(断开连接)](#【4】TCP 四次挥手(断开连接))
    • [【5】HTTP 协议(应用层,基于 TCP)](#【5】HTTP 协议(应用层,基于 TCP))
    • [【6】DNS(Domain Name System,域名系统)](#【6】DNS(Domain Name System,域名系统))
    • 【7】Socket(套接字)
      • [(1)Socket 分类](#(1)Socket 分类)
      • [(2)TCP Socket 通信完整流程(服务端 + 客户端)](#(2)TCP Socket 通信完整流程(服务端 + 客户端))
    • 【8】JSON序列化
  • 【二】SpringBoot各个组件的计算机网络案例
  • [【三】网络模型 BIO / NIO](#【三】网络模型 BIO / NIO)
    • 【1】网络模型介绍
    • 【2】核心概念
    • [【3】Java 4 种 IO 模型](#【3】Java 4 种 IO 模型)
      • [(1)BIO 同步阻塞 IO](#(1)BIO 同步阻塞 IO)
      • [(2)NIO 同步非阻塞 IO(Java NIO,New IO,JDK1.4 引入)](#(2)NIO 同步非阻塞 IO(Java NIO,New IO,JDK1.4 引入))
        • [NIO 三大核心组件](#NIO 三大核心组件)
        • [NIO 核心原理](#NIO 核心原理)
        • [对比 BIO](#对比 BIO)
        • [NIO 问题(原生 JDK NIO 痛点)](#NIO 问题(原生 JDK NIO 痛点))
        • [NIO 真实生产案例](#NIO 真实生产案例)
        • [NIO 两种线程模型(Reactor 反应器模式 Netty)](#NIO 两种线程模型(Reactor 反应器模式 Netty))
      • [(3)AIO(NIO.2 异步非阻塞 IO)](#(3)AIO(NIO.2 异步非阻塞 IO))
    • [【4】BIO/NIO/AIO 对比表](#【4】BIO/NIO/AIO 对比表)
  • 【四】生产环境常见网络故障
  • 【五】总结

计算机网络核心目标:不同主机上的进程之间数据通信。SpringBoot 绝大部分微服务能力都建立在计算机网络之上。

【一】计算机网络基础模型(OSI 七层 / TCP/IP 四层)

【1】TCP/IP 四层模型

OSI 七层是理论模型;Linux/Java 实际走 TCP/IP 四层。

【2】关键概念拆解

(1)IP 地址:定位一台机器;一台机器可以多个 IP。

(2)端口 port:定位机器上的进程。IP + 端口 = Socket。

一台服务器可以同时跑 Nacos (8848)、SpringBoot (20119)、Redis (6379)、MySQL (3306),依靠端口区分。

(3)Socket 套接字:操作系统提供给应用程序的网络 API;Java 网络编程本质就是调用操作系统 Socket。

(4)TCP(面向连接、可靠):三次握手建立连接、四次挥手断开;确认应答、超时重传、拥塞控制;不会丢数据。HTTP、Dubbo、MySQL、Nacos 底层都是 TCP。

(5)UDP(无连接,不可靠):不建立连接,只管发送;速度快,允许丢包;DNS 查询、流媒体。

SpringBoot 绝大多数业务使用TCP。

【3】TCP 三次握手(建立连接)

  1. 客户端发送 SYN 包
  2. 服务端回复 SYN+ACK
  3. 客户端回复 ACK,连接建立,开始传输数据。

【4】TCP 四次挥手(断开连接)

双向关闭,一方发起 FIN,另一方 ACK,等应用处理完再 FIN,对方 ACK。

⚠️生产经常遇到:TIME_WAIT、CLOSE_WAIT,都是 TCP 连接状态,大量堆积会导致服务无法建立新连接。

【5】HTTP 协议(应用层,基于 TCP)

HTTP 就是运行在 TCP 之上的文本协议。

  1. 先建立 TCP 连接,再发送 HTTP 请求报文;服务端返回 HTTP 响应报文;TCP 可以复用(HTTP1.1 keep‑alive、HTTP2 多路复用)。
  2. HTTPS = HTTP + TLS 加密;TCP 握手之后再 TLS 握手,数据全部密文传输

【6】DNS(Domain Name System,域名系统)

域名解析,把域名转为 IP,底层默认 UDP。

作用:把域名(www.baidu.com)翻译成 IP 地址。

计算机网络底层通信靠 IP 地址,人记不住一串数字 IP,DNS 充当互联网的 "电话本"。

没有 DNS:访问网站要输入 180.101.49.11;

有 DNS:直接输入域名 www.baidu.com

【7】Socket(套接字)

Java 中 java.net.Socket(客户端 TCP),ServerSocket(服务端监听端口)。

SpringBoot 内置 Tomcat 底层就是ServerSocket。Socket 是操作系统给应用程序提供的网络编程 API,是应用层和传输层 (TCP/UDP) 之间的接口。

通俗理解:操作系统内核实现了 TCP/IP 协议栈,应用程序不能直接操作网卡;Socket 就是程序调用操作系统网络能力的入口。

Socket = IP 地址 + 端口号,用来唯一标识网络上某一台机器上的一个进程。

所有跨机器网络通信,应用层协议(HTTP、MySQL 协议、Redis RESP 协议)都是把业务字节数据,交给 Socket,通过 TCP 传输。Socket 只是搬运字节,不关心字节代表什么业务含义。HTTP、MySQL 协议是在 Socket 之上定义的业务规则。

通俗比喻

Socket 就像电话线:

(1)服务端开启 Socket,相当于电话局装好一台座机,绑定号码(IP + 端口)等待别人打进来。

(2)客户端 Socket 就是你的手机,拨号 connect,三次握手就是接通电话。

(3)两边通过电话线 (Socket) 传递声音字节;声音内容可以是 HTTP、Redis 命令、SQL 语句(应用层协议)。

(4)close 就是挂断电话,四次挥手。

(5)电话线 (Socket) 只负责传输数据流,不关心你说什么内容。

(1)Socket 分类

主要两大类:TCP Socket(流式 Socket)、UDP Socket(数据报 Socket)

① TCP Socket(SOCK_STREAM)

基于 TCP 协议,面向连接、可靠。

通信前要三次握手建立连接;通信结束四次挥手断开。

数据是字节流,保证顺序,不丢包。

Tomcat、Nacos、MySQL、Redis、Feign 调用全部使用 TCP Socket。

② UDP Socket(SOCK_DGRAM)

基于 UDP 协议,无连接。

不需要握手,直接发数据包;不保证到达,不保证顺序。

DNS 默认就是 UDP Socket。

我们开发业务几乎都是 TCP Socket。

(2)TCP Socket 通信完整流程(服务端 + 客户端)

服务端(例如 Tomcat,监听 20119 端口)

(1)创建 ServerSocket(服务端套接字)

(2)bind 绑定 IP + 端口:0.0.0.0:20119,告诉操作系统:我要监听这个端口。

(3)listen:开启监听,操作系统内核维护连接队列。

(4)accept():阻塞等待客户端 TCP 连接到来。

有客户端连接过来,操作系统完成三次握手,返回一个新的 Socket 对象,专门用来和这个客户端通信。

⚠️注意:ServerSocket 只负责接收连接;真正收发数据用 accept 返回出来的 Socket。

(5)使用这个 Socket 做 read 读、write 写,和客户端交互。

(6)close 关闭 Socket,触发四次挥手断开 TCP 连接。

客户端(浏览器 / Feign / Redis 客户端)

(1)创建客户端Socket。

(2)connect:向服务端IP:port发起连接,操作系统执行 TCP 三次握手。

(3)连接成功后,read/write 读写字节数据。

(4)close 关闭套接字。

数据流动:Java 应用程序 → Socket API → OS 内核 TCP 缓冲区 → 网卡 → 网络。

bash 复制代码
服务端:ServerSocket.bind(20119) → listen → accept()阻塞等待
                                              ↑
客户端:Socket.connect(服务IP:20119) →【TCP三次握手建立连接】
                                              ↓
服务端拿到通信Socket <--------> 客户端Socket 互相读写字节流

【8】JSON序列化

序列化:把 Java 对象(内存中的对象) → JSON 字符串。(对象转字符串,用于网络传输、保存)

反序列化:JSON 字符串 → Java 对象。(字符串转回内存对象)

⚠️反序列化要求实体类有无参构造函数,否则报错。

【二】SpringBoot各个组件的计算机网络案例

SpringBoot 所有网络能力最终都是 Java → JDK Socket API → Linux 操作系统内核 → TCP/IP 协议栈。

【1】SpringBoot Web 服务(内置 Tomcat,对外提供 HTTP 接口)

(1)功能

写@RestController,对外暴露接口,server.port=20119,其他服务 /postman 调用。

(2)用到网络

TCP、HTTP 协议

(3)底层原理

(1)SpringBoot 启动,初始化内置 Tomcat;Tomcat 内部创建ServerSocket,绑定本机0.0.0.0:20119端口,操作系统内核监听 TCP 连接。

0.0.0.0:监听本机所有网卡 IP;如果写127.0.0.1,只能本机访问,外部机器无法连接。

(2)外部客户端(postman、另一个微服务)发起 HTTP 请求:

bash 复制代码
客户端 DNS 解析域名 → 获取服务端 IP
客户端操作系统发起TCP 三次握手和 Tomcat 建立 TCP 连接
在 TCP 通道内发送 HTTP Request 报文
Tomcat 接收报文,解析 HTTP,分发到对应@Controller方法执行业务代码
业务返回结果,封装 HTTP Response 报文,通过已经建立的 TCP 连接回传给客户端
HTTP1.1 默认 keep‑alive,TCP 连接会复用一段时间,不会立刻四次挥手断开。
java 复制代码
@RestController
public class TestController {
    @GetMapping("/hello")
    public String hello(){
        return "damp‑ind‑api";
    }
}

访问 http://172.17.0.XX:20119/hello

抓包可以看到:先 TCP 三次握手,然后 HTTP GET 报文,响应 200。

生产问题:端口被占用(Address already in use),就是 ServerSocket 绑定端口失败。防火墙 / 安全组没有开放 20119,外部 TCP 连接无法到达服务器。

【2】Nacos 配置中心 & Nacos 服务注册发现

(1)功能 1:Nacos 配置中心,拉取远程 yml 配置

spring.cloud.nacos.config.server‑addr:172.17.0.82:8848,项目启动读取 Nacos 上的damp‑ind‑api.yml、global‑xxx.yml。

(2)用到网络

HTTP/TCP,长轮询(Long Polling)

(3)底层原理

(1)SpringBoot 启动,bootstrap 上下文初始化 NacosConfigClient;底层是 HTTP 客户端,和 Nacos 服务端 8848 端口建立 TCP 连接,发送 HTTP 请求拉取配置。

(2)动态刷新(refresh:true)核心:长轮询 Long Polling

客户端发送 HTTP 请求到 Nacos,服务端不会立刻返回;如果配置没有变更,请求 hold 住挂起 30s;一旦配置发生修改立刻返回响应;超时就重新发起 http 请求。

不是 Websocket,是 HTTP 长轮询,基于 TCP。

(4)常见网络故障

nacos 地址写错、防火墙阻断 8848 端口;TCP 连接失败,报连接超时connect timed out。

namespace、账号密码错误,HTTP 返回 403。

(5)功能 2:Nacos 服务注册与发现

(1)服务启动向 Nacos 注册自己 IP:port;

(2)Feign 调用其他服务,从 Nacos 获取实例列表,选择实例发起调用。

底层:HTTP/TCP,心跳机制。

服务向 Nacos 定时发送心跳 HTTP 请求,告诉 Nacos 自己还活着;超过时间没有心跳,Nacos 剔除实例。

yml 复制代码
spring:
  cloud:
    nacos:
      discovery:
        server‑addr: 172.17.0.82:8848

【3】OpenFeign / RestTemplate 微服务之间调用(服务间 RPC)

(1)功能

damp‑ind‑api 调用另一个微服务接口

(2)用到网络

HTTP 协议,TCP

(3)底层

Feign 底层封装 HTTP 客户端(OkHttp / Apache HttpClient)。

(4)原理

(1)Feign 接口调用,内部拼接目标服务 URL;

(2)通过 Nacos 发现拿到目标服务实例 IP+port;

(3)Java 底层创建 Socket,TCP 三次握手和目标服务建立连接;

(4)在 TCP 连接发送 HTTP POST/GET 请求报文;

(5)等待服务端返回 HTTP 响应,解析结果,得到返回对象。

java 复制代码
@FeignClient(name = "damp‑other‑api")
public interface OtherApi{
    @GetMapping("/user/get")
    UserDTO getUser();
}

(5)生产网络问题:

ConnectException: Connection refused:目标服务没启动,端口没监听,TCP 连接被拒绝。

SocketTimeoutException:TCP 连接成功,但是业务处理慢,HTTP 读取响应超时。

区分两个超时:

ConnectTimeout:TCP 握手超时,连不上机器 / 端口;

ReadTimeout:TCP 连接建立成功,等待业务响应超时。

【4】Dubbo RPC

Dubbo 默认协议 dubbo 协议,自定义二进制 TCP 协议,不是 HTTP。

Consumer 和 Provider 之间直接 TCP 长连接;

二进制序列化数据在 TCP 报文传输;相比 HTTP 开销更小。

同样基于 Socket (TCP),只是应用层不是 HTTP,自定义协议。

【5】SpringBoot 连接数据库 MySQL

spring.datasource.url=jdbc:mysql://172.17.0.10:3306/db

用到网络:MySQL 自定义协议,TCP

底层原理:

(1)JDBC 驱动com.mysql.cj.jdbc.Driver内部创建 Socket,和 MySQL 服务器 3306 端口建立 TCP 连接。执行 SQL,把 SQL 命令封装 MySQL 协议报文,通过 TCP 发给 MySQL;数据库执行,返回结果集。

(2)连接池 HikariCP 维护一组 TCP 长连接,避免每次执行 SQL 都三次握手。

(3)故障:CommunicationsException,TCP 连接失败,网络不通、防火墙、mysql 拒绝客户端 ip。

【6】Redis(SpringDataRedis)

连接 Redis spring.redis.host=172.17.0.20 port=6379

底层:RESP 协议,TCP。

Jedis/Lettuce 创建 Socket,TCP 连接 Redis 服务端,发送 Redis 命令。Lettuce 支持 TCP 长连接。

Redis 单节点可以做到10 万 + QPS,普通 MySQL 几千 QPS。Redis 快不是单纯因为放在内存,是「内存 + IO 模型 + 数据结构 + 网络模型 + 设计取舍」综合结果。

内存只是必要条件,如果用多线程加锁做内存数据库,大量锁竞争性能会大幅下降。内存 + 单线程无锁 + epoll 多路复用三者结合

(1)redis高并发的核心原因

(1)全部数据放内存,避免磁盘 IO(最核心)

(2)单线程模型(主线程处理命令,无锁竞争)

(3)IO 多路复用(epoll)网络模型,高性能处理大量客户端连接

(4)高效的底层数据结构,不是简单 HashMap

(5)C 语言实现,贴近操作系统,开销小

(6)协议简单:RESP 序列化协议,解析开销极低

(7)优化:管道 pipeline、IO 线程、零拷贝等系统调用

⚠️注意:Redis 主线程单线程,并不是说整个 Redis 进程只有 1 个线程;持久化、删除大 key、IO 读写会有后台子线程。执行命令的核心主线程是单线程。

(2)数据全部存在内存,避开磁盘慢速 IO

磁盘(SSD)读写速度是微秒~毫秒级别;内存是纳秒级别,相差上千倍。

(1)MySQL:主要数据在磁盘,查询需要磁盘 IO;即使有 buffer‑pool 缓存,依然有磁盘回写、页管理开销。

(2)Redis:所有读写命令操作都在内存中完成,不需要访问磁盘。

RDB/AOF 持久化是为了宕机不丢数据,持久化是后台子进程做,不阻塞主线程执行命令。

只有做持久化的时候才碰磁盘;业务读写完全不走磁盘。

⚠️误区:"只要放内存就一定快"。Java 的 HashMap 也在内存,但是如果 10000 个并发线程读写 HashMap,需要大量 synchronized 锁,大量锁竞争、上下文切换,性能会掉下来。内存只是基础,单线程无锁才是高性能关键点。

(3)核心:主线程单线程模型,避免锁竞争

Redis 执行所有命令(set/get/hset 等)都在同一个主线程。

(1)同一时刻,只会执行一条客户端命令,天然不需要加锁,没有多线程锁竞争、没有线程上下文切换开销。

(2)多个客户端命令排队,顺序执行。

(3)多客户端并发过来,命令放到队列,主线程逐个处理。

✅优势:完全规避并发锁、竞态条件,代码简单,性能极高。

❌局限:CPU 多核也只能用一个 CPU 核心;如果有一条慢命令(keys *、flushdb、大 key),主线程被阻塞,所有客户端全部卡顿。

Redis6 之后引入多线程,只是 IO 多线程(读写网络数据),执行命令依旧是单线程,命令执行不做多线程。

(4)网络层:IO 多路复用 epoll(NIO)处理上万连接

Redis 使用 Linux epoll IO 多路复用模型,和 Java NIO/Netty 原理一样。

(1)少量线程,管理成千上万 TCP 客户端连接。

(2)不会为每一个客户端创建一个线程(BIO 模式)。

如果是 BIO 模型,1 万个客户端就要 1 万个线程,内存暴涨,操作系统大量上下文切换,性能直接垮掉。

工作流程:

(1)epoll 监听所有客户端 Socket;

(2)哪个客户端 Socket 有请求数据到达,内核通知 Redis;

(3)Redis 主线程只处理就绪的 Socket,读取命令,执行内存操作,返回结果。

对应 Java 的 NIO Selector;Lettuce 客户端就是 Netty NIO 和 Redis 通信。

(5)底层数据结构做了极致优化

Redis 对外提供 5 种常用结构:String、List、Hash、Set、ZSet。

底层并不是简单的 Java HashMap、ArrayList,会根据元素数量自动切换底层存储结构,兼顾内存占用和查询速度。

例如 Hash 结构,当 field 很少的时候,使用 ziplist 紧凑连续存储,内存占用极小;field 变多自动转 hashtable 哈希表。

(6)RESP 简单协议,解析开销小

Redis 使用 RESP 协议,是二进制安全的简单文本协议。

协议格式简单,服务端解析逻辑简单,不需要复杂 JSON 序列化反序列化,CPU 消耗低。

对比 HTTP:HTTP 报文头复杂,解析成本更高。

【7】C 语言实现,性能损耗低

C 直接操作内存,没有 JVM 虚拟机、GC 垃圾回收开销。

内存分配使用 jemalloc 内存分配器,减少内存碎片。

Java 程序要经过 JVM,GC 停顿、对象头、对象内存开销;Redis 没有这部分损耗。

【8】其他高性能机制

Pipeline 管道:客户端可以一次性发送多条命令,不用等每条命令返回,减少网络 RTT 来回次数。

零拷贝:RDB 持久化使用 sendfile 系统调用,数据直接内核缓冲区输出到网卡,不需要拷贝到用户态。

异步持久化:RDB 使用 fork 子进程,AOF 刷盘后台线程,持久化磁盘 IO 不阻塞主线程。

【7】Sentinel 流量控制

Sentinel 客户端和 dashboard 通信:HTTP/TCP,上报监控指标。

【8】WebSocket

应用层协议,基于 HTTP 握手,之后切换 TCP 双向长连接。适合推送消息。

流程:先发送 HTTP 请求完成握手;之后不再是 HTTP 报文,直接双向传输自定义消息,TCP 通道一直保持。

【9】HTTPS / SSL

SpringBoot 配置 ssl 证书,server.ssl.key-store

TCP 三次握手完成后,进行 TLS 握手,后续所有 HTTP 数据全部加密传输。防止抓包窃取数据。

【10】文件上传下载、调用第三方 http 接口(RestTemplate/WebClient)

比如调用第三方 http 接口,发送 http 请求,底层都是 Socket+TCP。

WebClient 响应式编程底层 Netty,依然是 TCP Socket。

【11】Netty(SpringCloud Gateway 网关底层)

Gateway 底层 Netty 框架;Netty 对 JDK 原生 Socket 做 NIO 非阻塞封装。

BIO:一个连接一个线程;NIO 非阻塞,少量线程处理大量 TCP 连接,网关高并发依靠 NIO。

【三】网络模型 BIO / NIO

(1)BIO 阻塞 IO:JDK 原生ServerSocket;每一个 TCP 连接占用一个 Java 线程。老版本 Tomcat BIO 模式。

(2)NIO(非阻塞 IO):Netty、SpringCloud Gateway、新版本 Tomcat NIO 模式。

现在 SpringBoot2 + 内置 Tomcat 默认 NIO;Gateway 强制 Netty NIO。

NIO 底层操作系统提供 epoll (Linux) 多路复用,可以用少量线程处理成千上万 TCP 连接,微服务网关高并发基础。

【1】网络模型介绍

(1)IO:Input/Output,数据读写;网络 IO 就是从 Socket 套接字读写网络数据。

(2)BIO、NIO、AIO 是 Java 处理网络 Socket 的三种 IO 模型,本质是:线程和连接之间的协作方式。

(3)Netty、Tomcat、SpringCloud Gateway、Nacos 客户端底层全部是 IO 模型。

操作系统底层:

(1)内核缓冲区:网卡收到数据,先存到操作系统内核缓冲区。

(2)用户缓冲区:Java 应用程序缓冲区。

(3)IO 操作 = 将数据在内核缓冲区 ↔ 用户缓冲区拷贝。

【2】核心概念

(1)阻塞(block):线程调用方法,如果数据没准备好,线程挂起休眠,不占用 CPU,等待数据就绪后才返回。

(2)非阻塞(non‑block):调用方法立刻返回;数据没准备好返回标识,不会把线程挂起,程序可以继续干别的事。

(3)同步:应用程序自己负责去拿数据拷贝;

(4)异步:操作系统内核把数据拷贝完成之后通知应用程序。

Java 4 种 IO 模型:

(1)BIO 同步阻塞 IO(Blocking IO)

(2)NIO 同步非阻塞 IO(Non‑Blocking IO,Java NIO,也叫 New IO)

(3)NIO.2 / AIO 异步非阻塞 IO(Asynchronous IO)

(4)Netty(基于 Java NIO 封装,Reactor 反应器模式,生产最常用)

【3】Java 4 种 IO 模型

(1)BIO 同步阻塞 IO

一个客户端连接,就要占用一个独立线程。

(1)底层原理

1-服务端:ServerSocket 监听端口,调用accept(),阻塞等待客户端 TCP 连接过来;没有客户端连接,线程卡死在accept(),休眠。

2-一旦客户端建立 TCP 连接,就新建一个新线程专门处理这个 Socket。

3-线程调用 socket.read()读取网络数据,如果客户端还没发数据,线程阻塞休眠,一直等待数据到达。

4-数据读取完毕执行业务逻辑;输出 write 同样会阻塞。

5-客户端断开,线程销毁。

(2)BIO 流程图

1-Main 线程:ServerSocket.accept() → 阻塞等待连接

2-来了连接 → new Thread (),把 Socket 交给子线程

3-子线程执行 read(),没有数据就阻塞挂起。

java 复制代码
// BIO服务端
public class BioServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(8888);
        while(true){
            // accept() 阻塞,没有客户端连接,线程停在这里不动
            Socket socket = serverSocket.accept();
            System.out.println("客户端连接进来");
            // 每来一个连接,新建一个线程
            new Thread(()->{
                try(InputStream is = socket.getInputStream()){
                    byte[] buf = new byte[1024];
                    // read()阻塞:客户端没发送数据,线程在这里休眠等待
                    int len;
                    while((len = is.read(buf)) != -1){
                        System.out.println(new String(buf,0,len));
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

(3)BIO 的优缺点

✅优点:代码简单,逻辑直白。

❌缺点:

连接多,线程爆炸:1000 个客户端连接,就要 1000 个 Java 线程。Java 线程占栈内存,几千个线程就会 OOM。

大部分线程阻塞在 read (),客户端空闲不发数据,线程也被占用,资源浪费。

只适合连接数很少的场景。

(4)现实使用场景

Tomcat 早期版本 BIO 模式(Tomcat7 可以配置 BIO,SpringBoot2 已经移除 BIO)。

传统老项目,少量客户端。

生产环境不会用 BIO 做高并发服务。

(2)NIO 同步非阻塞 IO(Java NIO,New IO,JDK1.4 引入)

一个或者少量线程,可以处理成千上万个 TCP 连接。

核心组件:Selector选择器、Channel通道、Buffer缓冲区。

NIO 三大核心组件

(1)Channel(通道):相当于 BIO 的 Socket;双向可读可写。SocketChannel、ServerSocketChannel。可以设置为非阻塞模式。

(2)Buffer(缓冲区):数据读写必须经过 Buffer;相当于字节数组。ByteBuffer。

(3)Selector(选择器,多路复用器)

最核心!一个 Selector 可以注册成千上万个 Channel。向操作系统内核询问:哪些 Channel 已经就绪(有数据可读 / 可以写 / 新连接到来)。

Linux 底层对应系统调用 epoll;Windows 是 iocp;mac 是 kqueue。

同步非阻塞:线程不会阻塞在 Channel 上,线程阻塞在Selector.select()。

NIO 核心原理

(1)ServerSocketChannel开启非阻塞,注册到 Selector,监听接收连接事件 OP_ACCEPT。

(2)主线程调用selector.select();这个方法阻塞,等待内核返回就绪事件。

(3)当有新客户端 TCP 连接:Selector 返回就绪事件,拿到SocketChannel,设置非阻塞,注册到 Selector,监听读事件 OP_READ。

(4)select 返回就绪读事件:代表该 Socket 内核缓冲区已经有数据,线程去读取数据。

(5)如果 Socket 没有数据,channel.read(buffer)立刻返回 0,不会阻塞线程,线程可以去处理其他就绪 Channel。

💡关键点:

  • Channel 设置非阻塞:read () 没有数据直接返回,线程不会挂起。
  • Selector 帮程序批量找出已经就绪的连接;不用每一个连接分配一个线程。
    成千上万个连接,只需要 1 个(或少量)线程循环轮询就绪事件。
    Java NIO 简单示例

Java NIO 简单示例

java 复制代码
public class NioServer {
    public static void main(String[] args) throws IOException {
        // 1. 开启通道,设置非阻塞
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8888));
        serverChannel.configureBlocking(false); // 设置非阻塞!NIO关键

        // 2. 创建多路复用器Selector
        Selector selector = Selector.open();
        // 将服务端通道注册到selector,监听接收连接事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        while(true){
            // select()阻塞:等待内核告知哪些channel就绪
            int readyCount = selector.select();
            if(readyCount ==0 ) continue;

            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iter = keys.iterator();
            while (iter.hasNext()){
                SelectionKey key = iter.next();
                iter.remove();

                if(key.isAcceptable()){
                    //事件1:新客户端TCP连接到来
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    SocketChannel clientChannel = ssc.accept();
                    clientChannel.configureBlocking(false); //客户端通道也设置非阻塞
                    //注册这个客户端通道,监听读事件
                    clientChannel.register(selector,SelectionKey.OP_READ);
                    System.out.println("客户端接入");
                }else if(key.isReadable()){
                    //事件2:socket有数据可读
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = clientChannel.read(buffer); //非阻塞,没有数据立刻返回0
                    if(len <= 0){
                        clientChannel.close();
                        continue;
                    }
                    buffer.flip();
                    byte[] data = new byte[len];
                    buffer.get(data);
                    System.out.println("收到数据:"+new String(data));
                }
            }
        }
    }
}
对比 BIO

BIO:1000 连接 →1000 线程。

NIO:10000 连接,只需要 1 个线程跑 Selector 循环。

NIO 问题(原生 JDK NIO 痛点)

(1)空轮询 bug:JDK 的 Selector 在 Linux 下会出现select()立刻返回 0,无限循环消耗 CPU。Netty 已经修复。

(2)API 繁琐,代码复杂,需要手动处理 Buffer、事件、异常、断开连接。

(3)单线程如果执行业务耗时阻塞,整个整个 IO 循环卡住,所有连接都得不到处理。

解决方案:Netty(Reactor 模式,IO 线程只处理网络,业务丢给业务线程池)。

NIO 真实生产案例

(1)SpringBoot2 内置 Tomcat 默认 NIO 模式。Tomcat NIOEndpoint,底层 Java NIO。

(2)SpringCloud Gateway:底层 Netty,Netty 封装 Java NIO。

(3)Nacos 客户端、Dubbo、RocketMQ 全部基于 Netty(NIO)。

(4)Redis 客户端 Lettuce(NIO Netty);Jedis 是 BIO。

✔BIO (Jedis):每一个 Redis 连接占用一个线程;

✔NIO (Lettuce):少量线程处理大量 Redis 连接。

NIO 两种线程模型(Reactor 反应器模式 Netty)

(1)单 Reactor 单线程(上面示例)

1 个线程负责 Selector 轮询事件 + 读写数据 + 执行业务。

❌业务代码耗时会阻塞整个网络,不适合生产。

(2)单 Reactor 多线程

IO 线程只做网络读写;业务交给线程池执行。

(3)主从 Reactor 多线程(Netty 默认生产模型,最常用)

MainReactor(boss 线程组):只负责接收 TCP 连接,把 Socket 交给 SubReactor。

SubReactor(worker 线程组 N 个线程):每个 SubReactor 有自己 Selector;处理 Socket 读写事件。

业务线程池:耗时业务逻辑交给业务线程池,不要阻塞 IO 线程。

SpringCloud Gateway 就是这套模型。

(3)AIO(NIO.2 异步非阻塞 IO)

JDK7 引入 AIO。真正异步。

(1)BIO、NIO 都属于同步 IO,需要应用线程自己去把数据从内核缓冲区拷贝到应用。

(2)AIO:内核完成数据拷贝之后,主动回调通知 Java 程序。

API:AsynchronousServerSocketChannel。

java 复制代码
// AIO伪代码,注册回调,不需要线程循环select
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel,Object>(){
    @Override
    public void completed(AsynchronousSocketChannel ch, Object attachment) {
        //内核完成连接,回调这个方法
    }
});

✅真正异步。

❌Linux 系统对 AIO 支持很差;Windows 下 AIO 性能好;生产几乎没有人用 AIO,Netty 也不推荐 AIO。

【4】BIO/NIO/AIO 对比表

餐馆例子:

(1)BIO:每来一桌客人,分配一个服务员全程专门伺候。客人不说话,服务员就站在那里干等。客人多服务员就要很多。

(2)NIO (Selector 多路复用):1 个服务员,不停轮询所有桌子;看哪一桌客人说话了,就过去服务;客人没说话,立刻切换下一桌。少量服务员处理很多桌。

(3)Netty 主从 Reactor:专门一个接待员 (boss) 接待客人入座;多个服务员 (worker) 轮询服务各桌;炒菜业务交给后厨线程池。

【四】生产环境常见网络故障

(1)Connection refused:TCP 三次握手失败,目标机器端口没有进程监听。

(2)connect timed out:TCP 握手超时;防火墙、路由不通,网络无法到达目标 IP。

(3)Socket read timeout:TCP 连接成功,读响应超时,服务处理慢。

(4)Too many open files:TCP 连接太多,操作系统文件句柄耗尽。Socket 在操作系统中本质是文件。

(5)大量TIME_WAIT:短连接频繁创建销毁,四次挥手后状态停留,耗尽本地端口。

(6)CLOSE_WAIT:程序没有正确关闭 Socket,对方关闭 TCP,我方没关闭,连接堆积内存泄漏。

【五】总结

(1)SpringBoot 所有跨机器通信,底层最终落到操作系统Socket 套接字 + TCP 协议;应用层再封装 HTTP、MySQL、Dubbo、Redis 等上层协议。

(2)TCP 负责可靠传输字节流;应用层协议规定字节代表什么含义(是 HTTP 请求?SQL?Redis 命令?)。

(3)项目的整套微服务链路:

SpringBoot (damp‑ind‑api) <TCP‑HTTP> Nacos;Feign <TCP‑HTTP> 其他微服务;Hikari MySQL;Redis 客户端Redis。全部依赖计算机网络。

相关推荐
心运软件1 小时前
SpringBoot+ Vue校园社团管理平台的完整架构设计
vue.js·后端
Jodie同志2 小时前
第16~23天:持久化、HITL、流式、MCP与安全
前端·后端·agent
Jodie同志2 小时前
第1~15天:原生Agent、RAG与LangGraph基础(完整代码实操)
前端·后端·agent
Zane19942 小时前
单例线程安全、生产者消费者、死锁:并发面试三连问串讲
java·后端
用户69371750013842 小时前
#DeepSeek+Pi‑Agent 王炸组合跑赢 Claude‑Code!
前端·人工智能·后端
Zane19942 小时前
类变量与实例变量:一个共享列表引发的线上事故
后端·python
Scene2162 小时前
AgentScope 2.0:2. 快速上手 从零构建生产级智能体
后端
前端一课2 小时前
用 TRAE Work 把项目踩坑经验沉淀成「团队可复用工程规范」,新人再也不重复掉坑
前端·后端
神奇小汤圆2 小时前
一文吃透 Spring 框架:原理、实践与面试全解析
后端
站大爷IP2 小时前
Python 的切片把我坑惨了,原来 `[:]` 是浅拷贝,而 `copy.deepcopy` 才是我的救命稻草
后端