Socket、异步非阻塞 I/O 与 NGINX、Netty 模型

Socket、异步非阻塞 I/O 与 NGINX、Netty 模型

本文以 TCP 服务端和 Linux 为主要背景,串联监听 socket、连接 socket、I/O 多路复用,以及 NGINX 和 Netty 如何接受和处理连接。不同操作系统、传输实现和框架版本的细节可能有所不同。

1. TCP 监听 socket 与连接 socket

socket 是操作系统提供的网络通信抽象。应用通过文件描述符引用内核中的 socket;文件描述符是进程描述符表中的编号,并不是 socket 数据本身。

1.1 服务端如何建立连接

服务端通常先创建监听 socket:

c 复制代码
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
bind(listen_fd, ...);       // 绑定本地地址与端口
listen(listen_fd, 128);     // 进入监听状态

int conn_fd = accept(listen_fd, ...);  // 接受一个连接
recv(conn_fd, buffer, sizeof(buffer), 0);

客户端发起连接后,内核处理 TCP 握手,并将待接受的连接放入监听队列;应用调用 accept(),取出一个连接,并获得指向该连接 socket 的新描述符。监听 socket 仍可继续接受其他连接。

text 复制代码
客户端 connect
      ↓
内核处理 TCP 握手和等待接受队列
      ↓
服务端在监听 socket 上调用 accept
      ↓
获得连接 socket,后续使用它收发数据

TCP 的 accept() 返回连接并不表示应用数据已经到达。非阻塞模式下,即便收到了就绪通知,调用 accept() 或 recv() 也仍须处理暂时无可用结果等情况。

1.2 两种 socket 的职责

对比 监听 socket 连接 socket
作用 等待并接受新连接 与特定对端收发数据
常用操作 bind、listen、accept read/recv、write/send
可读事件 可以尝试接受连接 可以尝试读取数据或发现连接状态变化
数量关系 一个监听入口可以接受多个连接 通常每条 TCP 连接对应一个服务端连接 socket

监听 socket 不能像已连接的 TCP socket 一样直接与具体客户端收发业务数据;已连接的 socket 也不能作为监听 socket 去 accept 其他连接。客户端同样先调用 socket(),再通过 connect() 主动建立连接。两种角色的区别来自后续操作和状态。

连接一般通过源 IP、源端口、目标 IP、目标端口及协议区分。多个客户端可以同时连接同一个服务端端口,因为它们的连接标识不同。

2. fork、文件描述符与共享 socket

在 Linux 中,fork() 创建子进程。父子进程的普通内存拥有各自独立的地址空间,初始内容相同,通常由写时复制实现;共享内存映射等有不同语义。

子进程继承父进程打开的文件描述符副本。父子进程描述符表分离,但对应描述符可以引用同一个内核对象:

text 复制代码
父进程的 fd ──┐
              ├──→ 同一个内核监听 socket
子进程的 fd ──┘

因此,父进程先绑定监听端口,再 fork 出多个子进程时,子进程不是分别重新 bind 同一端口,而是继承该监听 socket 的引用,不会因此产生重复绑定冲突。各进程关闭各自的描述符,底层对象在不再有引用时才会释放。

fork 不会复制父进程的其他线程;子进程只包含调用 fork 的线程。父进程的待处理信号、进程关联文件记录锁和部分定时器等也不会原样继承。多线程进程 fork 后,子进程在 execve 前只能安全调用 async-signal-safe 函数,需注意继承的锁状态。

3. 阻塞、非阻塞、异步与 I/O 多路复用

3.1 阻塞与非阻塞

阻塞 描述调用线程是否等待:阻塞 read() 在无数据时可以使线程等待;非阻塞 read() 在暂时无数据时会返回,应用稍后再尝试。

异步描述操作如何完成和交付结果:程序发起操作后可以继续执行,结果之后通过完成通知、回调或 Future 等方式交付。异步 API 的具体底层实现可能因操作系统和运行时而不同。

二者是不同维度:非阻塞调用可以由程序自行反复检查;异步发起的任务也可以在调用 Future.get() 后让调用线程阻塞等待。CompletableFuture 表达的是任务的异步组合,本身不能证明底层网络 I/O 使用内核完成式异步 I/O。

3.2 select、poll、epoll 与就绪通知

I/O 多路复用让一个线程等待多个 socket 的事件。以 Linux epoll 为例:

text 复制代码
注册 socket 及关注事件
        ↓
epoll_wait 等待一个或多个事件就绪
        ↓
收到"可读/可写/可接受连接"等就绪通知
        ↓
应用调用 read、write 或 accept,处理返回值

epoll 是就绪通知 :它提示应用可以尝试 I/O,并不意味着已经替应用完成读取。epoll_wait() 本身可以阻塞;它让一个线程等待多个连接,而不是让每个连接各占一个等待线程。通常套接字也设置为非阻塞,以便事件状态变化或并发竞争时不会意外长时间阻塞。

3.3 完成通知式异步 I/O

与就绪通知相比,完成通知式接口让应用先提交具体 I/O 请求,在请求完成或失败后获取结果:

text 复制代码
提交读取请求和缓冲区 → 执行读取 → 完成通知 → 处理结果
  • Windows 的 Overlapped I/O 可配合 IOCP,向完成端口投递完成事件。
  • Linux 的 io_uring 使用提交队列和完成队列;具体操作的内核执行方式依赖操作类型和环境。
  • Java AsynchronousSocketChannel.read 可通过 CompletionHandler 或 Future 交付结果;底层实现取决于平台,不应仅凭 API 名称断定其内核实现。

完成通知也需要线程或事件循环消费结果;它不代表系统从头到尾完全不使用线程。一条 TCP 消息也可能需要多次读取和组装。

4. NGINX 的 I/O 模型

4.1 Master 与 Worker

NGINX 主要采用多进程、事件驱动模型:

text 复制代码
master:读取配置、管理 worker,通常创建和绑定监听 socket
    ├── worker 1:接受连接、处理连接 I/O
    ├── worker 2:接受连接、处理连接 I/O
    └── worker 3:接受连接、处理连接 I/O

默认共享监听 socket 的模式下,多个 worker 持有引用同一个底层监听 socket 的文件描述符。新连接由一个 worker 成功 accept();该 worker 获得连接 socket 的描述符,之后通常由它的事件循环处理这条连接的读取和写入。master 通常不接受客户端连接或处理请求数据。

text 复制代码
新连接 → 共享监听 socket → worker 2 accept
                              ↓
                        worker 2 管理连接 socket
                              ↓
                         可读事件 → recv → 处理

一个 worker 的事件循环可以管理多个连接。若多个 worker 关注同一个监听 socket,具体唤醒和接受行为取决于配置和平台:accept_mutex 可协调接受连接,Linux 的 EPOLLEXCLUSIVE 可减少不必要的唤醒。

4.2 reuseport

配置 listen ... reuseport 时,NGINX 为 worker 创建独立的监听 socket,这些 socket 使用相应的端口复用机制绑定同一地址和端口,由内核分配新连接。连接被某个 worker 接受后,通常仍由该 worker 继续处理。

NGINX 并非完全不使用线程:其主要网络请求处理是 worker 进程内的事件循环,某些工作也可以使用线程池。多进程提供一定的故障和内存隔离,但共享内存等场景仍需同步;它并不必然比多线程模型更快。

5. Netty 的 I/O 模型

5.1 Boss 与 Worker

典型 Netty 服务端在同一个 JVM 内使用两个 EventLoopGroup:

java 复制代码
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
         .channel(NioServerSocketChannel.class)
         .childHandler(/* 初始化客户端 Channel 的 Pipeline */)
         .bind(8080);
  • Boss Group 中的 EventLoop 负责服务端 Channel 的事件,通常包括接受新连接。
  • Worker Group 中的 EventLoop 负责被接受的客户端 Channel 的 I/O 事件。
  • 这两个变量名是惯例;也可以配置同一个 Group 同时承担两种职责。

使用 Java NIO 时,监听 socket 封装在 NioServerSocketChannel 中。通常单次 bind 得到一个服务端 Channel,并将它注册到 Boss Group 的一个 EventLoop;Boss Group 有多个线程并不意味着同一个监听 Channel 会自动注册到全部 Boss EventLoop。

5.2 接受连接与注册到 Worker

以 Netty 4.1 NIO 模型为例:

text 复制代码
监听 Channel 的接受事件就绪
        ↓
Boss 执行 accept,获得客户端连接
        ↓
创建对应的 Netty child Channel
        ↓
从 Worker Group 选择 EventLoop,提交注册任务
        ↓
Worker 在自己的 Selector 上注册连接 Channel
        ↓
Worker 监听并处理该连接的 I/O 事件

Boss 和 Worker 通常是同一进程中的线程,这里的交接是事件处理职责的交接,不是跨进程传递 socket,也不会创建第二条 TCP 连接。一个 Channel 通常绑定一个 EventLoop;一个 EventLoop 可管理多个 Channel。

注册时,Netty NIO 实现先将 Java Channel 注册到 Selector,随后在开始读取等阶段设置关注事件。连接的默认 Pipeline 回调通常由所属 EventLoop 执行;若 Handler 指定了其他执行器,则按其配置执行。

5.3 连接关闭由谁处理

注册完成后,通常由连接所属的 Worker EventLoop 处理其读写和生命周期事件。例如,对端正常关闭发送方向时,内核通知连接可读,Worker 读取并发现流结束;Netty 根据配置处理关闭,常规完整关闭时触发 channelInactive。启用半关闭时,输入方向关闭不一定意味着整个连接立即关闭。

网络异常断开也未必立即可见,可能要等读写错误、TCP 保活或应用心跳超时才能发现。

默认 EventLoop 同时处理其管理的多个连接;在 Handler 中执行耗时计算或阻塞调用会影响其他连接,应按场景交给合适的业务执行器,并考虑顺序、线程安全和背压。

5.4 NGINX 与 Netty 对比

项目 NGINX 常见模型 Netty 常见模型
运行单位 master、worker 是不同进程 Boss、Worker 通常是同一 JVM 内的线程组
接受连接 worker 执行 accept Boss EventLoop 执行 accept
处理连接 I/O 通常由接受连接的 worker 继续处理 child Channel 注册给 Worker EventLoop
管理职责 master 管理配置、worker 生命周期等 Boss 主要负责服务端 Channel 事件,不等同于 master
共通点 事件循环管理多个非阻塞连接 事件循环管理多个非阻塞连接

6. 同一应用中的 Web、Netty 与 Dubbo 端口

一个进程可以监听多个不同端口。例如,Web 容器监听 8080,应用内嵌 Netty 服务监听 80,前提是地址与端口可用且有绑定权限。端口号不会自动决定协议;Netty 要处理 HTTP,仍需配置相应的编解码与处理逻辑。应用关闭时也应关闭 Channel 和 EventLoopGroup。

Dubbo 服务提供者如果使用 dubbo 协议且传输实现采用 Netty,那么配置的 Dubbo 协议端口通常就是 Netty 服务端绑定的端口。实际监听、发布到注册中心的地址、协议和传输实现仍应结合版本、部署及配置核对;注册中心本身的端口是另一回事。

参考资料

相关推荐
沫璃染墨1 小时前
从零入门计算机网络系列(十一):HTTP协议进阶——从重定向原理到GET/POST与Fiddler抓包
linux·服务器·开发语言·网络·c++·tcp/ip·html
蓝祁子yoky2 小时前
Qt学习日记 Day13:网络编程(一)-TCP基础与Socket实战
网络·qt·学习
箓维2 小时前
TCP Socket的进阶实现
网络·笔记·tcp/ip
小羊没烦恼!3 小时前
关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
java·服务器·网络·学习·负载均衡
小Ti客栈3 小时前
Netty 零拷贝原理与实战
运维·服务器·网络
国科安芯3 小时前
商业光学遥感载荷焦平面供电链的低噪声稳压器研究综述——以国科安芯ASP7A84AS型3A低压差线性稳压器为器件级参照
大数据·网络·嵌入式硬件·平面·卫星·商业航天·抗辐射
静以修身zzz3 小时前
计算云服务
运维·网络·云计算
星恒随风3 小时前
Linux网络编程:应用层协议究竟是什么?从网络计算器到TCP Socket抽象基类完整实现
linux·网络·笔记·tcp/ip
万联WANFLOW4 小时前
异地组网速度忽快忽慢到底怎么回事?直连与中继的判断方法和处理步骤
运维·网络·网络协议·p2p