Java 原生网络编程(BIO | NIO | Reactor 模式)

1、基本常识

Socket 是应用层与 TCP/IP 协议族通信的中间软件抽象层,是一组接口,使用了门面模式对应用层隐藏了传输层以下的实现细节。TCP 用主机的 IP 地址加上主机端口号作为 TCP 连接的端点,该端点叫做套接字 Socket。

比如三次握手,调用 Socket.connect() 就能完成,应用开发者无须关心如何具体实现三次握手。

长连接与短连接没有哪一个更好之说,只是结合具体业务使用哪一个更合适。

任何网络通信编程关注的三件事:

  1. 连接(客户端连接服务器,服务器接收客户端的连接)
  2. 读网络数据
  3. 写网络数据

常见的网络编程方式有三种:

  1. BIO:阻塞式 IO,当线程无法读取到数据或无法写入数据时,线程会进入阻塞状态
  2. NIO:非阻塞式 IO,也称 IO 多路复用,即一个线程为多个客户端执行读写操作。当一个客户端无法读写数据即将陷入阻塞状态之前,线程会切换到其他客户端的读写工作中,避免阻塞带来的效率低下问题
  3. AIO:异步 IO,Linux 的异步 IO 实际上是通过 NIO 实现的,而 Windows 才提供了真正的异步 IO,因此在 Linux 和 Java 这一侧关注的是 BIO 与 NIO

2、BIO

服务端通过 ServerSocket 获取到客户端的连接 Socket,为每个连接分配一个单独的线程,通过 IO 流进行同步阻塞式通信:

public class Server {// 别用 CachedThreadPool,与 new Thread() 没啥区别private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);public static void main(String[] args) {ServerSocket serverSocket = null;try {serverSocket = new ServerSocket();serverSocket.bind(new InetSocketAddress(10001));System.out.println("Start server...");while (true) {executorService.submit(new ServerTask(serverSocket.accept()));}} catch (IOException e) {e.printStackTrace();} finally {try {if (serverSocket != null) {serverSocket.close();}} catch (IOException e) {e.printStackTrace();}}}static class ServerTask implements Runnable {private Socket socket;public ServerTask(Socket socket) {this.socket = socket;}@Overridepublic void run() {try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {String userName = objectInputStream.readUTF();objectOutputStream.writeUTF("Hello," + userName);objectOutputStream.flush();} catch (IOException e) {e.printStackTrace();}}}
}

客户端使用 Socket 连接绑定服务器端口后与服务器通信:

public class Client {public static void main(String[] args) throws IOException {Socket socket = new Socket();socket.connect(new InetSocketAddress("127.0.0.1", 10001));try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream())) {objectOutputStream.writeUTF("James");objectOutputStream.flush();System.out.println(objectInputStream.readUTF());} finally {socket.close();}}
}

3、NIO

首先要清楚一点,在配置参数相同的情况下,单次网络通信,BIO 的效率是比 NIO 高的,但是由于 NIO 中一个服务端线程可以与多个客户端通信,所以 NIO 这个 IO 多路复用的机制,总体上比 BIO 效率更高。从成本角度考虑,NIO 节省成本,而 BIO 则是以成本换效率。

3.1 三大核心组件

NIO 的三大核心组件:

  • Selector:选择器,也称为轮询代理器或事件订阅器,可以在一个单独的线程中操作 Selector 选择不同的 Channel,从而实现在一个线程中管理多个通道。应用程序向 Selector 注册需要其关注的 Channel 以及 Channel 感兴趣的 IO 事件,Selector 内则保存已经注册的 Channel 的容器
  • Channels:通道,是应用程序与操作系统读写数据的渠道,通道中的数据总是先读到 Buffer 或从 Buffer 写入。Selector 注册的是 SelectableChannel,其子类 ServerSocketChannel 支持应用程序向操作系统注册 IO 多路复用的端口监听,同时支持 TCP 和 UDP;而另一个子类 SocketChannel 则是 TCP Socket 的监听通道
  • Buffer:本质上就是一个数组,其内存被包装成 Buffer 对象并提供了方便访问该内存的方法。仅与 Channel 做数据交换。

3.2 重要概念 SelectionKey

除此之外还有一个重要概念 SelectionKey,表示 SelectableChannel 在 Selector 中注册的标识。Channel 向 Selector 注册时,会创建 SelectionKey 建立 Channel 与 Selector 的联系,同时维护 Channel 事件。

SelectionKey 有四种类型:

  1. OP_READ:操作系统读缓冲区可读,并非所有时刻都有数据可读,因此需要注册该操作
  2. OP_WRITE:操作系统写缓冲区有空闲空间,一般情况下都有空闲空间,因此没必要注册该类型,否则浪费 CPU;但如果是写密集型的任务,比如下载文件,缓冲区可能会满,此时就需要注册该操作类型,并在写完后取消注册
  3. OP_CONNECT:只给客户端使用,在 SocketChannel.connect() 连接成功后就绪
  4. OP_ACCEPT:只给服务器使用,在接收到客户端连接请求时就绪

这四种类型也再次阐明了网络编程关注的三件事:连接(客户端连接服务器,服务器接收客户端的连接)、读、写网络数据。

不同的 Channel 允许注册的事件类型不同:

  • 服务器 ServerSocketChannel:仅 OP_ACCEPT
  • 服务器 SocketChannel:OP_READ、OP_WRITE
  • 客户端 SocketChannel:OP_READ、OP_WRITE 和 OP_CONNECT

3.3 Buffer

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存(其实就是数组),这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。

Buffer 位于 Channel 和应用程序之间。应用程序对外写数据时,是写到 Buffer,由 Channel 将 Buffer 中的数据读出并发送出去;读也是类似的,数据是先从 Channel 读到 Buffer 后,应用程序再读 Buffer 中的数据。

Buffer 有三个重要属性:

  1. capacity:内存容量,只能写 capacity 个 byte、long、char 类型数据,Buffer 满了之后需要通过读数据或清除数据将其清空后,才能继续写数据
  2. position:表示操作数据的位置,写模式下,每写完一个数据会向下移动一个单位,最大为 capacity - 1;读模式下,每读完一个数据会向前移动到下一个可读的位置。读写模式切换时,position 会被重置为 0
  3. limit:写模式下表示最多能向 Buffer 中写多少数据,此时 limit 等于 capacity;读模式下表示最多能读到多少数据,切换到读模式时,limit 会被置为写模式下的 position,即可读取此前所有写入的数据

Buffer 既可以读也可以写,需要通过 flip() 从写模式切换到读模式,而当读完数据后,可以通过 clear() 或 compact() 清理缓冲区并切换成写模式,其中前者会清空整个缓冲区,而后者则只清除已经读取过的数据。

完整的通信结构如下:

请添加图片描述

大致步骤:

  1. 服务端 ServerSocketChannel 向 Selector 注册 OP_ACCEPT
  2. 客户端连接服务器,Selector 会通知 ServerSocketChannel 连接事件,此时 ServerSocketChannel 可以产生一个 SocketChannel 与客户端进行通信,并注册 OP_READ
  3. 客户端发送数据,Selector 会通知服务端的 SocketChannel 读取数据,这些数据会被写入 Buffer,服务器的应用程序可以从 Buffer 中读取这些数据
  4. 当服务器的应用程序发送应答消息给客户端时,是向 Buffer 中写入数据,SocketChannel 会从 Buffer 中读取这些数据并发送出去

BIO 时,假如分三次向对端写 100 个字节,那么就要进行三次系统调用。而使用 NIO,可以将 100 个字节写入 Buffer,从 Buffer 读取数据再进行一次系统调用就可以发送数据了。由于系统调用会消耗大量系统资源,所以 NIO 是提升了性能的。类似的,BIO 在读取数据时,不论从系统读取到多少数据都要经过一次系统调用交给应用程序,而 NIO 可以将从操作系统读取的数据先存入 Buffer 中,然后从 Buffer 通过一次系统调用传输给应用程序。

3.4 NIO 编程实践

基础使用代码见 GitHub 上相关章节,注意事项见课程文档。这里主要说一下在读写数据时为什么一般不注册写事件 OP_WRITE。

一般情况下,服务器在写数据时,是不注册 OP_WRITE 直接通过 SocketChannel.write() 写的:

	private void handleInput(SelectionKey key) throws IOException {// 由于 SelectionKey 是可以取消的,因此使用前需要先判断是否可用if (key.isValid()) {if (key.isAcceptable()) {// 只有 ServerSocketChannel 才关注 OP_ACCEPTServerSocketChannel ssc = (ServerSocketChannel) key.channel();// 获取和客户端通信的 SocketSocketChannel sc = ssc.accept();System.out.println("有客户端连接");sc.configureBlocking(false);sc.register(selector, SelectionKey.OP_READ);}// 读数据if (key.isReadable()) {SocketChannel sc = (SocketChannel) key.channel();// 如果要读取的数据多于 1024 字节,那么读事件会被触发多次直到读完ByteBuffer buffer = ByteBuffer.allocate(1024);int readBytes = sc.read(buffer);if (readBytes > 0) {// 因为 Channel 写入了 Buffer,因此读的时候需要进行模式切换buffer.flip();// 读取数据做业务处理byte[] bytes = new byte[readBytes];buffer.get(bytes);String message = new String(bytes, "UTF-8");System.out.println("服务器收到消息: " + message);String result = Const.response(message);// 发送应答消息doWrite(sc, result);} else if (readBytes < 0) {// 小于 0 说明链路已经关闭,释放资源key.cancel();sc.close();}}}}private void doWrite(SocketChannel sc, String result) throws IOException {byte[] bytes = result.getBytes();ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);// 将字节数组复制到 writeBufferwriteBuffer.put(bytes);// 切换到读模式writeBuffer.flip();sc.write(writeBuffer);}

假如想在 OP_WRITE 下向客户端写数据,就要修改为如下这样:

	private void handleInput(SelectionKey key) throws IOException {// 由于 SelectionKey 是可以取消的,因此使用前需要先判断是否可用if (key.isValid()) {...// 添加写数据逻辑if (key.isWritable()) {System.out.println("writable...");SocketChannel sc = (SocketChannel) key.channel();ByteBuffer attachment = (ByteBuffer) key.attachment();if (attachment.hasRemaining()) {System.out.println("write :" + sc.write(attachment) + " byte");} else {// 写完数据后要取消对写事件的注册,否则系统会一直通知写事件key.interestOps(SelectionKey.OP_READ);}}}}private void doWrite(SocketChannel sc, String result) throws IOException {byte[] bytes = result.getBytes();ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);// 将字节数组复制到 writeBufferwriteBuffer.put(bytes);// 切换到读模式writeBuffer.flip();
//        sc.write(writeBuffer);// register() 注册哪一个事件就只关注该事件,因此这里在注册写事件时不要忘了读,同时将 writeBuffer// 作为附件也一并注册sc.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, writeBuffer);}

之所以在写完之后要取消对写事件的关注,主要是因为读写事件的触发机制是不一样的。当客户端向服务器发送数据时,服务端有数据可读,就会触发 OP_READ 事件。

而 OP_WRITE 则不同,当通信双方 Socket 连接成功后,操作系统会为每个 Socket 创建两个操作系统级别的缓存(注意并不是应用程序中用到的 Buffer,应用程序是感知不到这个缓存的,这个缓存在操作系统内核中),一个输出缓存,一个输入缓存。当输入缓存中有对端发来的数据时,就会触发 OP_READ 事件,而输出缓存中,只要有空闲空间,就会一直不停的触发 OP_WRITE。

通常都是要写的数据非常多,数据量大于缓冲区需要多次写的时候,才注册 OP_WRITE。

3.5 Reactor 模式

Reactor 翻译为“反应器”,可以延伸为“倒置”、“控制逆转”,即事件处理程序不调用反应器,而是向反应器注册一个事件处理器,当事件到来时调用事件处理程序做出反应。这种控制逆转又称为“好莱坞法则”。

NIO 的 Selector 就扮演着 Reactor 的角色。Reactor 模式又可以分为三种流程:

  1. 单线程 Reactor 模式:服务器全程只使用一个线程,即 IO 操作(accept、read、write)与业务操作(decode、compute、encode)都在一个线程上处理。这样有一个问题增大 IO 响应的时间。示意图如下:
    请添加图片描述
  2. 单线程 Reactor,工作者线程池:添加工作者线程池,将非 IO 操作从 Reactor 线程中移出交给工作者线程池执行。这种模式在处理大并发、大数据量的业务时是不合适的。因为面对成百上千的 IO 操作,一个线程的处理能力始终是有限的。再比如读取 10M 的数据,在读取时其他 IO 操作是无法进行的。示意图如下:
    请添加图片描述
  3. 多 Reactor 线程模式:针对第二种模式的缺点,再引入一个 Reactor 线程池。Reactor 线程池中的每一 Reactor 线程都会有自己的 Selector、线程和分发的事件循环逻辑。mainReactor 可以只有一个,但 subReactor 一般会有多个。mainReactor 线程主要负责接收客户端的连接请求,然后将接收到的 SocketChannel 传递给 subReactor,由 subReactor 来完成和客户端的通信。示意图如下:
    请添加图片描述

Reactor 模式看似与观察者模式很像,二者的主要区别是观察者模式与单个事件源关联,而反应器模式则与多个事件源关联。当一个主体发生改变时,所有依属体都得到通知。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/83447.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

OpenCV透视变换

概念 OpenCV 透视变换是将图像从一个视平面投影到另一个视平面的过程&#xff0c;也叫投影映射 &#xff0c;属于空间立体三维变换。它基于透视原理&#xff0c;通过 33 的变换矩阵作用于图像像素坐标来实现映射转换 &#xff0c;能模拟人眼或相机镜头观看三维空间物体时的透视…

STM32F103定时器1每毫秒中断一次

定时器溢出中断&#xff0c;在程序设计中经常用到。在使用TIM1和TIM8溢出中断时&#xff0c;需要注意“TIM_TimeBaseStructure.TIM_RepetitionCounter0;”&#xff0c;它表示溢出一次&#xff0c;并可以设置中断标志位。 TIM1_Interrupt_Initializtion(1000,72); //当arr1…

数据库——SQL约束窗口函数介绍

4.SQL约束介绍 &#xff08;1&#xff09;主键约束 A、基本内容 基本内容 p r i m a r y primary primary k e y key key约束唯一表示数据库中的每条记录主键必须包含唯一的值&#xff08;UNIQUE&#xff09;主键不能包含NULL值&#xff08;NOT NULL&#xff09;每个表都应…

【typenum】 8 常量文件(consts.rs)

一、源码 这段代码通过类型级编程&#xff08;type-level programming&#xff09;在编译期实现数值计算。以下是常量定义部分&#xff1a; // THIS IS GENERATED CODE #![allow(missing_docs)] use crate::int::{NInt, PInt}; /** Type aliases for many constants.This fil…

第8讲、Multi-Head Attention 的核心机制与实现细节

&#x1f914; 为什么要有 Multi-Head Attention&#xff1f; 单个 Attention 机制虽然可以捕捉句子中不同词之间的关系&#xff0c;但它只能关注一种角度或模式。 Multi-Head 的作用是&#xff1a; 多个头 多个视角同时观察序列的不同关系。 例如&#xff1a; 一个头可能专…

百度智能云千帆携手联想,共创MCP生态宇宙

5月7日&#xff0c;2025联想创新科技大会&#xff08;Tech World&#xff09;在上海世博中心举行&#xff0c;本届大会以“让AI成为创新生产力”为主题。会上&#xff0c;联想集团董事长兼CEO杨元庆展示了包括覆盖全场景的超级智能体矩阵&#xff0c;包括个人超级智能体、企业超…

【OpenCV】帧差法、级联分类器、透视变换

一、帧差法&#xff08;移动目标识别&#xff09;&#xff1a; 好处&#xff1a;开销小&#xff0c;不怎么消耗CPU的算力&#xff0c;对硬件要求不高&#xff0c;但只适合固定摄像头 1、优点 计算效率高&#xff0c;硬件要求 响应速度快&#xff0c;实时性强 直接利用连续帧…

数据库迁移的艺术:团队协作中的冲突预防与解决之道

title: 数据库迁移的艺术:团队协作中的冲突预防与解决之道 date: 2025/05/17 00:13:50 updated: 2025/05/17 00:13:50 author: cmdragon excerpt: 在团队协作中,数据库迁移脚本冲突是常见问题。通过Alembic工具,可以有效地管理和解决这些冲突。冲突预防的四原则包括功能分…

Linux常用命令43——bunzip2解压缩bz2文件

在使用Linux或macOS日常开发中&#xff0c;熟悉一些基本的命令有助于提高工作效率&#xff0c;bunzip2可解压缩.bz2格式的压缩文件。bunzip2实际上是bzip2的符号连接&#xff0c;执行bunzip2与bzip2 -d的效果相同。本篇学习记录bunzip2命令的基本使用。 首先查看帮助文档&#…

盲盒:拆开未知的惊喜,收藏生活的仪式感

一、什么是盲盒&#xff1f;—— 一场关于“未知”的浪漫冒险 盲盒&#xff0c;是一种充满神秘感的消费体验&#xff1a; &#x1f381; 盒中藏惊喜——每个盲盒外观相同&#xff0c;但内含随机商品&#xff0c;可能是普通款、稀有款&#xff0c;甚至是“隐藏款”&#xff1b;…

Android 中使用通知(Kotlin 版)

1. 前置条件 Android Studio&#xff1a;确保使用最新版本&#xff08;2023.3.1&#xff09;目标 API&#xff1a;最低 API 21&#xff0c;兼容 Android 8.0&#xff08;渠道&#xff09;和 13&#xff08;权限&#xff09;依赖库&#xff1a;使用 WorkManager 和 Notificatio…

使用大模型预测急性结石性疾病技术方案

目录 1. 数据预处理与特征工程伪代码 - 数据清洗与特征处理数据预处理流程图2. 大模型构建与训练伪代码 - 模型训练模型训练流程图3. 术前预测系统伪代码 - 术前风险评估术前预测流程图4. 术中实时调整系统伪代码 - 术中风险预警术中调整流程图5. 术后护理系统伪代码 - 并发症预…

每日Prompt:生成自拍照

提示词 帮我生成一张图片&#xff1a;图片风格为「人像摄影」&#xff0c;请你画一张及其平凡无奇的iPhone对镜自拍照&#xff0c;主角是穿着JK风格cos服的可爱女孩&#xff0c;在自己精心布置的可按风格的房间内的落地镜前用后置摄像头随手一拍的快照。照片开启了闪光灯&…

动态规划-64.最小路径和-力扣(LetCode)

一、题目解析 从左上角到右下角使得数字总和最小且只能向下或向右移动 二、算法原理 1.状态表示 我们需要求到达[i,j]位置时数字总和的最小值&#xff0c;所以dp[i][j]表示&#xff1a;到达[i,j]位置时&#xff0c;路径数字总和的最小值。 2.状态转移方程 到达[i,j]之前要先…

LeetCode LCR 010 和为 K 的子数组 (Java)

两种解法详解&#xff1a;暴力枚举与前缀和哈希表寻找和为k的子数组 在解决数组中和为k的连续子数组个数的问题时&#xff0c;我们可以采用不同的方法。本文将详细解析两种常见的解法&#xff1a;暴力枚举法和前缀和结合哈希表的方法&#xff0c;分析它们的思路、优缺点及适用…

OpenVLA (2) 机器人环境和环境数据

文章目录 [TOC](文章目录) 前言1 BridgeData V21.1 概述1.2 硬件环境 2 数据集2.1 场景与结构2.2 数据结构2.2.1 images02.2.2 obs_dict.pkl2.2.3 policy_out.pkl 3 close question3.1 英伟达环境3.2 LIBERO 环境更适合仿真3.3 4090 运行问题 前言 按照笔者之前的行业经验, 数…

深度学习(第3章——亚像素卷积和可形变卷积)

前言&#xff1a; 本章介绍了计算机识别超分领域和目标检测领域中常常使用的两种卷积变体&#xff0c;亚像素卷积&#xff08;Subpixel Convolution&#xff09;和可形变卷积&#xff08;Deformable Convolution&#xff09;&#xff0c;并给出对应pytorch的使用。 亚像素卷积…

大模型在腰椎间盘突出症预测与治疗方案制定中的应用研究

目录 一、引言 1.1 研究背景 1.2 研究目的与意义 二、腰椎间盘突出症概述 2.1 定义与病因 2.2 症状与诊断方法 2.3 治疗方法概述 三、大模型技术原理与应用基础 3.1 大模型的基本原理 3.2 大模型在医疗领域的应用现状 3.3 用于腰椎间盘突出症预测的可行性分析 四、…

Vue3学习(组合式API——ref模版引用与defineExpose编译宏函数)

目录 一、ref模版引用。 &#xff08;1&#xff09;基本介绍。 &#xff08;2&#xff09;核心基本步骤。(以获取DOM、组件为例) &#xff08;3&#xff09;案例&#xff1a;获取dom对象演示。 <1>需求&#xff1a;点击按钮&#xff0c;让输入框聚焦。 &#xff08;4&…

公链开发及其配套设施:钱包与区块链浏览器

公链开发及其配套设施&#xff1a;钱包与区块链浏览器的技术架构与生态实践 ——2025年区块链基础设施建设的核心逻辑与创新突破 一、公链开发&#xff1a;构建去中心化世界的基石 1. 技术架构设计的三重挑战 公链作为开放的区块链网络&#xff0c;需在性能、安全性与去中心…