禅与计算机 禅与计算机
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

sharkchili

计算机禅修者
首页
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析Java泛型的魅力与机制
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • LinkedHashMap源码到面试题的全解析
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
    • Java8流式编程入门
    • 一文速通lambda与函数式编程
    • Java8函数式方法引用最佳实践
  • Java并发编程

    • Java并发编程基础小结
    • 深入理解Java中的final关键字
    • 浅谈Java并发安全发布技术
    • 浅谈Java并发编程中断的哲学
    • Java线程池知识点小结
    • 浅谈Java线程池中拒绝策略与流控的艺术
    • synchronized关键字使用指南
    • 深入源码解析synchronized关键字
    • 详解JUC包下的锁
    • 详解并发编程中的CAS原子类
    • LongAdder源码分析
    • AQS源码解析
    • 深入剖析Java并发编程中的死锁问题
    • Java并发容器总结
    • 详解Java并发编程volatile关键字
    • 并发编程ThreadLocal必知必会
    • CompletableFuture基础实践小结
    • CompletableFuture异步多任务最佳实践
    • 硬核详解FutureTask设计与实现
    • 线程池大小设置的底层逻辑与场景化方案
    • 来聊一个有趣的限流器RateLimiter
  • JVM相关

    • 从零开始掌握 JVM
    • JVM核心知识点小结
    • JVM指令集概览:基础与应用
    • JVM类加载器深度解析
    • JVM方法区深度解析
    • Java内存模型JMM详解
    • Java对象大小的精确计算方法
    • 逃逸分析在Java中的应用与优化
    • 从零开始理解JVM的JIT编译机制
    • G1垃圾回收器:原理详解与调优指南
    • JVM故障排查实战指南
    • JVM内存问题排错最佳实践
    • JVM内存溢出排查指南
    • 简明的Arthas使用教程
    • 简明的Arthas配置及基础运维教程
    • 基于Arthas Idea的JVM故障排查与指令生成
    • 基于arthas量化监控诊断java应用方法论与实践
    • 深入剖析arthas技术原理
  • 深入理解Spring框架

    • Spring 核心知识点全面解析
    • Spring核心功能IOC详解
    • Spring AOP 深度剖析与实践
    • Spring 三级缓存机制深度解析
    • 深入 Spring 源码,剖析设计模式的落地实践
    • 探索 Spring 事务的奥秘
    • 深入解析Spring Bean的生命周期管理
    • 解读 Spring Boot 核心知识点
    • Spring Boot 启动优化实战:1分钟到13秒的排查与优化之路
    • Spring Boot自动装配原理及实践
    • 一文快速上手Sharding-JDBC
    • sharding-jdbc如何实现分页查询
    • 基于DynamicDataSource整合分库分表框架Shardingsphere
  • 计算机组成原理

    • 计算机硬件知识小结
    • CPU核心知识点小结
    • 浅谈CPU流水线的艺术
    • 从Java程序员视角聊聊CPU缓存
    • CPU任务调度和伪共享问题小结
    • CPU MESI缓存一致性协议
    • CPU内存管理机制
    • 内存深度解析
    • 磁盘存储原理
    • 详解计算机启动步骤
    • CPU南北桥架构与发展史
    • CPU中断机制与硬件交互详解
  • 操作系统

    • 如何实现一个高性能服务器
    • Linux文件结构与文件权限
    • Linux常见压缩指令小结
    • Linux核心系统调用详解
    • Linux进程管理
    • Linux线程管理
    • 进程与线程深度解析
    • Linux进程间通信机制
    • 零拷贝技术原理与实践
    • CPU缓存一致性问题深度解析
    • IO任务与CPU调度艺术
  • 计算机网络

    • 网卡通信原理详解
    • 网卡数据包处理指南
    • 基于抓包详解TCP协议
  • 编码最佳实践

    • 浅谈现代软件工程TDD最佳实践
    • 浅谈TDD模式下并发程序设计与实现
    • 面向AI编程新范式Trae后端开发环境搭建与实践
    • 基于提示词工程的Redis签到功能开发实践
    • 基于Vibe Coding的Redis分页查询实现
    • 告别AI无效对话:资深工程师的提示词设计最佳实践
  • 实用技巧与配置

    • Mac常用快捷键与效率插件指南
    • Keynote技术科普短视频制作全攻略
  • 写作

    • 写好技术博客的5大核心原则:从认知科学到AI工具的全流程指南
  • 开发工具

    • IDEA配置详解与高效使用指南
  • Nodejs
  • 博客搭建
  • Redis

    • Redis核心知识小结
    • 解锁Redis发布订阅模式
    • 掌握Redis事务
    • Redis主从复制技术
    • Redis的哨兵模式详解
    • 深度剖析Redisson分布式锁
    • 详解redis单线程设计思路
    • 来聊聊Redis所实现的Reactor模型
    • Redis RDB持久化源码深度解析
    • 来聊聊redis的AOF写入
    • 来聊聊Redis持久化AOF管道通信的设计
    • 来聊聊redis集群数据迁移
    • Redis SDS动态字符串深度解析
    • 高效索引的秘密:redis跳表设计与实现
    • 聊聊redis中的字典设计与实现
  • MySQL

    • MySQL基础知识点小结
    • 解读MySQL 索引基础
    • MySQL 索引进阶指南
    • 解读MySQL Explain关键字
    • 探秘 MySQL 锁:原理与实践
    • 详解MySQL重做日志redolog
    • 详解undoLog在MySQL MVCC中的运用
    • MySQL二进制日志binlog核心知识点
    • MySQL高效插入数据的最佳实践
    • MySQL分页查询优化指南
    • MySQL流式查询的奥秘与应用解析
    • 来聊聊分库分表
    • 来聊聊大厂常用的分布式ID生成方案
  • ElasticSearch

    • 从Lucene到Elasticsearch:进化之路
    • ES 基础使用指南
    • ElasticSearch如何写入一篇文档
    • 深入剖析Elasticsearch文档读取原理
    • 聊聊ElasticSearch性能调优
    • Spring借助Easy-Es操作ES
  • Netty

    • 一文快速了解高性能网络通信框架Netty
    • Netty网络传输简记
    • 来聊聊Netty的ByteBuf
    • 来聊聊Netty消息发送的那些事
    • 解密Netty高性能之谜:NioEventLoop线程池阻塞分析
    • 详解Netty中的责任链Pipeline如何管理ChannelHandler
    • Netty Reactor模型常见知识点小结
    • Netty如何驾驭TCP流式传输?粘包拆包问题全解
    • Netty解码器源码解析
  • 消息队列

    • 一文快速入门消息队列
    • 消息队列RocketMQ入门指南
    • 基于RocketMQ实现分布式事务
    • RocketMQ容器化最佳实践
    • RocketMQ常见问题与深度解析
    • Kafka快速安装与使用指南
  • Nginx

    • Linux下的nginx安装
    • Nginx基础入门总结
    • Nginx核心指令小结
    • Nginx进程结构与核心模块初探
    • Nginx应用进阶HTTP核心模块配置
    • Nginx缓存及HTTPS配置小记
    • nginx高可用实践简记
    • Nginx性能优化
  • 微服务基础

    • 微服务基础知识小结
    • 分布式事务核心概念小结
    • OpenFeign核心知识小结
    • 微服务组件Gateway核心使用小结
    • 分布式事务Seata实践
    • 用 Docker Compose 完成 Seata 的整合部署
  • Nacos

    • Nacos服务注册原理全解析
    • Nacos服务订阅流程全解析
    • Nacos服务变更推送流程全解析
    • 深入解析SpringCloud负载均衡器Loadbalancer
    • Nacos源码环境搭建与调试指南
  • Seata

    • 深度剖析Seata源码
  • Docker部署

    • 一文快速掌握docker的理念和基本使用
    • 使用docker编排容器
    • 基于docker-compose部署微服务基本环境
    • 基于docker容器化部署微服务
    • Gateway全局异常处理及请求响应监控
    • Docker图形化界面工具Portainer最佳实践
  • Go基础

    • 一文带你速通Go语言基础语法
    • 一文快速掌握Go语言切片
    • 来聊聊go语言的hashMap
    • 一文速通go语言类型系统
    • 浅谈Go语言中的面向对象
    • go语言是如何实现协程的
    • 聊聊go语言中的GMP模型
    • 极简的go语言channel入门
    • 聊聊go语言基于epoll的网络并发实现
    • 写给Java开发的Go语言协程实践
  • mini-redis实战

    • 来聊聊我用go手写redis这件事
    • mini-redis如何解析处理客户端请求
    • 实现mini-redis字符串操作
    • 硬核复刻redis底层双向链表核心实现
    • 动手复刻redis之go语言下的字典的设计与落地
    • Go 语言下的 Redis 跳表设计与实现
    • Go 语言版 Redis 有序集合指令复刻探索
  • 项目编排

    • Spring脚手架创建简记
    • Spring脚手架集成分页插件
    • Spring脚手架集成校验框架
    • maven父子模块两种搭建方式简记
    • SpringBoot+Vue3前后端快速整合入门
    • 来聊聊Java项目分层规范
  • 场景设计

    • Java实现文件分片上传
    • 基于时间缓存优化浏览器轮询阻塞问题
    • 基于EasyExcel实现高效导出
    • 10亿数据高效插入MySQL最佳方案
    • 从开源框架中学习那些实用的位运算技巧
  • CI/CD

    • 基于NETAPP实现内网穿透
    • 基于Gitee实现Jenkins自动化部署SpringBoot项目
    • Jenkins离线安装部署教程简记
    • 基于Nexus搭建Maven私服基础入门
    • 基于内网的Jenkins整合gitlab综合方案简记
  • 监控方法论

    • SpringBoot集成Prometheus与Grafana监控
    • Java监控度量Micrometer全解析
    • 从 micrometer计量器角度快速上手promQL
    • 硬核安利一个监控告警开源项目Nightingale
  • Spring AI

    • Spring AI Alibaba深度实战:一文掌握智能体开发全流程
    • Spring AI Alibaba实战:JVM监控诊断Arthas Agent的工程化构建与最佳实践
  • 大模型评测

    • M2.7 真能打!我用两个真实场景测了测,结果有点意外
    • Qoder JetBrains插件评测:祖传代码重构与接口优化实战
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础

    • 聊一聊java一些核心知识点
    • 聊聊java面向对象核心知识点
    • 聊聊Java中的异常
    • 聊聊Java中的常用类String
    • 万字长文带你细聊Java注解本质
    • 来聊聊Java的反射机制
    • 深入解析 Java 泛型的魅力与机制
    • 来聊聊Java为什么只有值传递
    • 来聊聊大厂常问的SPI工作原理
    • 来聊聊session与token的区别
    • Java集合框架深度解析与面试指南
    • Java常用集合类HashMap深度解析
    • 一文带你速通HashMap底层核心数据结构红黑树
    • 深入HashMap底层理解阿里手册的遍历守则
    • LinkedHashMap源码到面试题的全解析
    • 空间预分配思想提升HashMap插入效率
    • 解析Java集合工具类:功能与实践
    • 深入解析CopyOnWriteArrayList的工作机制
    • Java基础IO总结
    • Java三大IO模型小结
    • Java BIO NIO AIO详解
    • Java进阶NIO之IO多路复用详解
      • 多路复用的NIO实现
      • Reactor模型
        • 传统IO模型
        • Reactor事件驱动模型(业务处理与IO分离)
        • 图解
        • 代码示例
        • Reactor事件驱动模型并发读写
        • 图解
      • Java对多路IO复用的支持
        • 核心概念介绍
        • 代码示例
        • 服务端代码
        • 客户端代码
      • 小结
      • Netty
        • 简介
        • 使用示例
        • 导入pom
        • 服务端代码
        • 客户端代码
      • 参考文献
    • 聊聊Java关于IO流中的设计模式
    • 为什么流不关闭会导致内存泄漏
    • 聊聊java零拷贝的几种实现
    • Java8流式编程入门
    • Java8流式编程详解
    • 来聊聊java8的数值流
    • 聊聊Java8中的函数式编程
    • 一文速通lambda与函数式编程
    • 基于lambda简化设计模式
    • Java8函数式方法引用最佳实践
    • 使用Java8并行流的注意事项
    • 详解java数值类型核心知识点
    • 将一维数组按指定长度转为二维数组
    • 33个非常实用的JavaScript一行代码
    • 多种数组去重性能对比
    • 防抖与节流函数
    • 比typeof运算符更准确的类型判断
    • new命令原理
    • ES6面向对象
    • ES5面向对象
    • 判断是否为移动端浏览器
    • JS随机打乱数组
    • JS获取和修改url参数
    • 三级目录

  • 并发编程

  • JVM相关

  • 深入理解Spring框架

  • Java核心技术
  • Java基础
sharkchili
2023-05-22
目录

Java进阶NIO之IO多路复用详解

# 多路复用的NIO实现

  1. poll:性能较高,也是基于Reactor模型,仅Linux系统支持,Linux下kernels 2.6内核版本之前使用poll来支持Java的NIO框架的。
  2. epoll:性能高,仅仅Linux下支持,Linux的kernels 2.6之后的内核版本都是基于epoll支持Java的NIO框架,但Linux没有windows系统的IOCP技术,所以也都是用epoll来模拟异步IO技术。
  3. select (重点):性能较高,Linux和Windows系统都支持,Linux的kernels 2.6之前默认都是使用select模型,Windows的同步IO也都是既有select模型。
  4. kqueue:性能高,基于Proactor,目前的Java版本不支持。

# Reactor模型

# 传统IO模型

如下图每个客户端连接到达时,服务端都会分配一个线程进行各种处理,所以这种模型线程数和用户访问量呈线性关系。这种模型在访问量不大的情况下还是可以扛住的,但是在高并发场景下,会有以下几个问题:

  1. 由于操作系统可以创建的线程数是有限制的。感兴趣的读者可以使用这条命令查看Linux可以创建的最大线程数:cat /proc/sys/kernel/threads-max。
  2. 无论用户是何种的请求我们都会专门分配一个线程让他处理数据接受、解码、业务处理、发送数据等操作,在网络质量不好的情况下,这就会大量线程阻塞,进而导致服务器效率降低,从而降低了服务器的吞吐量。

在这里插入图片描述

# Reactor事件驱动模型(业务处理与IO分离)

# 图解

如下图,相较于传统IO模型来说,他将处理用户请求和网络请求拆分,而且该模型还是非阻塞IO模型,所以当网络事件需要处理时,程序还可以处理其他任务,处理效率有显著提高。

在这里插入图片描述

# 代码示例

首先是Reactor ,可以看到改代码就是服务端的入口,他做的事情就是初始化感兴趣的通道,一旦监听到客户端的请求就分发给Accept代码

public class Reactor implements Runnable {


    private static Logger logger = LoggerFactory.getLogger(Reactor.class);

    private final Selector selector;
    private final ServerSocketChannel serverSocketChannel;


    public Reactor(int port) throws IOException {
        //创建一个自己感兴趣的频道 并注册到selector
        serverSocketChannel = ServerSocketChannel.open();
        //设置为非阻塞模式
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(port));

        selector = Selector.open();
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        selectionKey.attach(new Acceptor(serverSocketChannel));

    }

    @Override
    public void run() {
        while (!Thread.interrupted()) {
            try {
                selector.select();
            } catch (IOException e) {
                logger.error("服务端使用一个线程不断等待客户端的连接到达失败,失败原因{}", e.getMessage(), e);
            }

            // 客户端请求到达,使用迭代器遍历处理
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                logger.info("监听到客户端连接事件后将其分发给Acceptor");
                dispatch(iterator.next());
                //因为我们注册的通道是accept,这里已经调用dispatch了,所以需要将其删除掉,避免迭代器二次使用
                iterator.remove();
            }

            try {
                int count = selector.selectNow();
                logger.info("此方法执行非阻塞 选择操作。如果自上一次 ,选择操作以来没有通道成为可选择的,则此方法立即返回零。,返回结果[{}]", count);
            } catch (IOException e) {
                logger.error("非阻塞选择操作失败,失败原因[{}]", e.getMessage(), e);
            }
        }
    }

    private void dispatch(SelectionKey selectionKey) {
        //将客户端的请求交给accept处理
        Runnable accept = (Runnable) selectionKey.attachment();
        accept.run();
        ;
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

Acceptor 代码如下,可以看到他做的就是将网络读写任务分发到线程池中

public class Acceptor implements Runnable {

    private static Logger logger = LoggerFactory.getLogger(Acceptor.class);

    private final ServerSocketChannel serverSocketChannel;

    private static final ExecutorService threadPool = Executors.newFixedThreadPool(20);

    public Acceptor(ServerSocketChannel serverSocketChannel) {
        this.serverSocketChannel = serverSocketChannel;
    }


    @Override
    public void run() {
        try {
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel != null) {
                //将任务分发到线程池中处理
                try {
                    threadPool.execute(new Handler(socketChannel));
                } catch (Exception e) {
                    logger.error("任务分发失败,失败原因:[{}]", e.getMessage(), e);
                }
            }

        } catch (IOException e) {
            logger.error("Acceptor分发任务到线程池失败,失败原因:[{}]", e.getMessage(), e);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

最后就是Handler 代码

public class Handler implements Runnable {

    private static Logger logger = LoggerFactory.getLogger(Handler.class);

    private volatile static Selector selector;
    private final SocketChannel serverSocketChannel;
    private SelectionKey selectionKey;

    private volatile ByteBuffer input = ByteBuffer.allocate(1024);
    private volatile ByteBuffer output = ByteBuffer.allocate(1024);


    public Handler(SocketChannel channel) throws Exception {
        selector = Selector.open();
        serverSocketChannel = channel;
        channel.configureBlocking(false);
        selectionKey = channel.register(selector, SelectionKey.OP_READ);


    }


    @Override
    public void run() {
        while (selector.isOpen() && serverSocketChannel.isOpen()) {
            try {
                Set<SelectionKey> keys = select();
                Iterator<SelectionKey> iterator = keys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if (key.isReadable()) {
                        read(key);
                    } else if (key.isWritable()) {
                        write(key);
                    }

                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void write(SelectionKey key) throws IOException {
        output.flip();
        if (serverSocketChannel != null) {
            serverSocketChannel.write(output);
            selectionKey.channel();
            serverSocketChannel.close();
            output.clear();
        }

    }

    private void read(SelectionKey key) throws IOException {
        serverSocketChannel.read(input);
        if (input.position() == 0) {
            return;
        }

        input.flip();
        process();
        input.clear();
//        完成读取后就监听写入事件
        key.interestOps(SelectionKey.OP_WRITE);

    }

    private void process() throws UnsupportedEncodingException {
        byte[] bytes = new byte[input.remaining()];
        input.get(bytes);
        String str = new String(bytes, CharsetUtil.UTF_8);
        logger.info("读取到的数据为[{}]", str);
        output.put("receive u msg".getBytes());
    }


    // 这里处理的主要目的是处理Jdk的一个bug,该bug会导致Selector被意外触发,但是实际上没有任何事件到达,
    // 此时的处理方式是新建一个Selector,然后重新将当前Channel注册到该Selector上
    private Set<SelectionKey> select() throws IOException {
        selector.select();
        Set<SelectionKey> keys = selector.selectedKeys();

        while (keys.isEmpty()) {
            int interestOps = selectionKey.interestOps();
            selector = Selector.open();
            selectionKey = serverSocketChannel.register(selector, interestOps);
            return select();
        }

        return keys;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

# Reactor事件驱动模型并发读写

# 图解

上述仅仅实现的网络处理和网络读写业务处理的拆分,但在高并发场景下,如果将网络读写和业务处理耦合在一起的情况下,很可能因为网络质量问题导致线程阻塞,进而导致服务器吞吐量降低,所以我们对Reactor进行一番改造,如下图,可以看到我们将Reactor拆分成来个,一个处理网络请求,另一个负责网络读写和业务计算,但是网络请求我们也会专门开个线程池进行处理,处理完成的分发的业务处理的线程池中,这样性能就一下子提高了。

在这里插入图片描述

# Java对多路IO复用的支持

# 核心概念介绍

多路IO复用工作机制整体如下图所示,接下来我们就来简单介绍一下,这里面几个核心概念:

  1. channel:与操作系统内核交互、传递数据的通道,一个通道通常有一个专属的文件描述符,常见的通道有ServerSocketChannel、ScoketChannel、DatagramChannel,其中应用程序只有通过ServerSocketChannel向选择器注册时才能实现多路IO复用的事件监听(这种监听支持TCP、UDP),而ScoketChannel则是TCP套接字监听通道,DatagramChannel则是UDP报文监听通道。
  2. buffer: Java的NIO框架为每种类型的读写都设置的缓存buffer,buffer缓冲区有3个比较重要的概念,position在读状态下时代表读取到的位置,在写状态代表写的起始位置。limit在读状态代表数据最多可以写到位置,在写状态代表还可以写capacity-limit个位置。capacity则代表buffer容量。建议想详细了解缓冲概念的读者参考这篇文章Java NIO 之 Buffer(缓冲区) (opens new window)
  3. Selector(重要):Java NIO中比较重要的概念,你可以将其理解为轮询代理器、channel容器管理器,有了selector,应用程序不再通过阻塞或者非阻塞模式询问操作系统是否有事件发生,取而待之的是selector去代理轮询。

在这里插入图片描述

# 代码示例

我们现在不妨就编写一个支持多路IO复用的Java NIO框架

# 服务端代码

代码如下所示,为该应用程序注册感兴趣的通道后,即可启动选择器不断轮询,如果有读请求进来则对读数据进行处理,我们会将这个频道的hash值算出来去concurrentHashMap 查看这个用户之前是否有发送消息,若有则且用户本次发送了over,我们则将信息拼接起来输出。若没有用户本次发送的文本没有over,我们则将本次的消息拼接起来存到concurrentHashMap 中。

public class SocketServer1 {


    /**
     * 日志
     */
    private static Logger logger = LoggerFactory.getLogger(SocketServer1.class);


    private static ConcurrentHashMap<Integer, StringBuffer> concurrentHashMap = new ConcurrentHashMap<>();

    public static void main(String[] args) throws Exception {
        // 创建服务端的ServerSocketChannel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式,即可read、write没有结果返回一个标志即直接结束了
        serverChannel.configureBlocking(false);
        // 获得与此通道相关的服务套接字
        ServerSocket serverSocket = serverChannel.socket();
        serverSocket.setReuseAddress(true);
        serverSocket.bind(new InetSocketAddress(83));

        // 获得选择器
        Selector selector = Selector.open();
        //将我们上面感兴趣的频道注册到selector中,注意:服务端的通道只能注册SelectionKey.OP_ACCEPT事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        try {
            while (true) {
                //如果条件成立,说明本次询问selector,并没有获取到任何准备好的、感兴趣的事件
                //java程序对多路复用IO的支持也包括了阻塞模式 和非阻塞模式两种。
                if (selector.select(100) == 0) {
                    logger.info("100ms毫秒没有没有频道事件,可以再次处理一些业务逻辑");
                    continue;
                }
                //这里就是本次询问操作系统,所获取到的“所关心的事件”的事件类型(每一个通道都是独立的)
                Iterator<SelectionKey> selecionKeys = selector.selectedKeys().iterator();

                while (selecionKeys.hasNext()) {
                    SelectionKey readyKey = selecionKeys.next();
                    //这个已经处理的readyKey一定要移除。如果不移除,就会一直存在在selector.selectedKeys集合中
                    //待到下一次selector.select() > 0时,这个readyKey又会被处理一次
                    selecionKeys.remove();

                    SelectableChannel selectableChannel = readyKey.channel();
                    if (readyKey.isValid() && readyKey.isAcceptable()) {
                        logger.info("======accept =======");
                        /*
                         * 当server socket channel通道已经准备好,就可以从server socket channel中获取socketchannel了
                         * 拿到socket channel后,要做的事情就是马上到selector注册这个socket channel感兴趣的事情。
                         * 否则无法监听到这个socket channel到达的数据
                         * */
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectableChannel;
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        registerSocketChannel(socketChannel, selector);

                    } else if (readyKey.isValid() && readyKey.isConnectable()) {
                        logger.info("======socket channel 建立连接=======");
                    } else if (readyKey.isValid() && readyKey.isReadable()) {
                        logger.info("======socket channel 数据准备完成,可以进行数据读取");
                        readSocketChannel(readyKey);
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        } finally {
            serverSocket.close();
        }
    }

    /**
     * 在server socket channel接收到/准备好 一个新的 TCP连接后。
     * 就会向程序返回一个新的socketChannel。<br>
     * 但是这个新的socket channel并没有在selector“选择器/代理器”中注册,
     * 所以程序还没法通过selector通知这个socket channel的事件。
     * 于是我们拿到新的socket channel后,要做的第一个事情就是到selector“选择器/代理器”中注册这个
     * socket channel感兴趣的事件
     *
     * @param socketChannel 新的socket channel
     * @param selector      selector“选择器/代理器”
     * @throws Exception
     */
    private static void registerSocketChannel(SocketChannel socketChannel, Selector selector) throws Exception {
        socketChannel.configureBlocking(false);
        //socket通道可以且只可以注册三种事件SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT
        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(2048));
    }

    /**
     * 这个方法用于读取从客户端传来的信息。
     * 并且观察从客户端过来的socket channel在经过多次传输后,是否完成传输。
     * 如果传输完成,则返回一个true的标记。
     *
     * @throws Exception
     */
    private static void readSocketChannel(SelectionKey readyKey) throws Exception {
        logger.info("开始阅读客户端传入的数据**************************");
        SocketChannel clientSocketChannel = (SocketChannel) readyKey.channel();
        //获取客户端使用的端口
        InetSocketAddress sourceSocketAddress = (InetSocketAddress) clientSocketChannel.getRemoteAddress();
        Integer resoucePort = sourceSocketAddress.getPort();


        ByteBuffer contextBytes = (ByteBuffer) readyKey.attachment();
        StringBuffer stringBuffer = new StringBuffer();
        int realLen = -1;
        try {
//            若有读取到数据则皆有缓存将数据到String
            while ((realLen = clientSocketChannel.read(contextBytes)) != -1) {
//                将limit到position的位置,position调整到其实位置,为读数据做好准备
                contextBytes.flip();
                int position = contextBytes.position();
                int length = contextBytes.limit();
                byte[] bytes = new byte[length];
                contextBytes.get(bytes, position, length);

                String messageEncode = new String(bytes, 0, length, "UTF-8");
                stringBuffer.append(messageEncode);

                //position变为0,limit回去,即切换为写模式
                contextBytes.clear();
            }

        } catch (Exception e) {
            //这里抛出了异常,一般就是客户端因为某种原因终止了。所以关闭channel就行了
            logger.error("读取客户端数据异常,异常原因:[{}]", e.getMessage(), e);
            clientSocketChannel.close();
            return;
        }


        //如果收到了“over”关键字,才会清空buffer,并回发数据;
        //否则不清空缓存,还要还原buffer的“写状态”
        if (URLDecoder.decode(stringBuffer.toString(), "UTF-8").indexOf("over") != -1) {
            Integer hashCode = clientSocketChannel.hashCode();
            String message = null;
            if (concurrentHashMap.containsKey(hashCode)) {
                StringBuffer str = concurrentHashMap.get(hashCode);
                str.append(stringBuffer);
                message = str.toString();
            }else{
                message = stringBuffer.toString();
            }

            logger.info("端口:" + resoucePort + "客户端发来的信息======message : " + message);

            clientSocketChannel.close();
            concurrentHashMap.remove(hashCode);
        } else {
            logger.info("端口:" + resoucePort + "客户端信息还未接受完,继续接受======message : " + URLDecoder.decode(stringBuffer.toString(), "UTF-8"));
            Integer hashCode = clientSocketChannel.hashCode();
            String message = null;
            if (concurrentHashMap.containsKey(hashCode)) {
                StringBuffer str = concurrentHashMap.get(hashCode);
                str.append(stringBuffer);
                concurrentHashMap.put(hashCode, str);
            }
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161

# 客户端代码

public class Client {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1", 83);
        OutputStream out = socket.getOutputStream();
        String s = "hello world over";
        out.write(s.getBytes());
        out.close();
    }
}
1
2
3
4
5
6
7
8
9

# 小结

  1. Java NIO多路复用不再使用多线程进行IO处理,当然在业务处理中,我们仍然可以通过线程池进行处理。
  2. 同一个端口可以处理TCP或者UDP,例如你的应用程序向选择器注册ServerSocketChannel。
  3. Java NIO是操作系统级别的优化,能够同时接口多个客户端IO事件,同时具有阻塞式同步IO和非阻塞式同步IO的所有特点。当然多路复用IO还是属于操作系统级别的同步IO。

# Netty

# 简介

Netty是一款高性能、异步事件驱动的强大的NIO框架,业界主流的RPC框架、zookeeper等都是基于其构建的。其特点为:



1. api简单,开发门槛低 功能强大
2. 内置了多种编码、解码功能 
3. 与其它业界主流的NIO框架对比,netty的综合性能最优 社区活跃,使用广泛,经历过很多商业应用项目的考验 
4. 定制能力强,可以对框架进行灵活的扩展 
1
2
3
4
5
6

# 使用示例

# 导入pom

        <dependency>
            <groupId>org.jboss.netty</groupId>
            <artifactId>netty</artifactId>
            <version>3.2.5.Final</version>
        </dependency>
1
2
3
4
5

# 服务端代码

入口

package com.guide.io.netty.code;

import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.InetSocketAddress;
import java.util.concurrent.Executors;

public class NettyServer {




    private static Logger logger = LoggerFactory.getLogger(NettyServer.class);


    public void bind(int port) throws Exception {

        ServerBootstrap serverBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),
                Executors.newCachedThreadPool()));

        serverBootstrap.setPipelineFactory(() -> {
            ChannelPipeline channelPipeline = Channels.pipeline();
            channelPipeline.addLast(MessageHandler.class.getName(), new MessageHandler());
            return channelPipeline;
        });

        serverBootstrap.bind(new InetSocketAddress(port));

    }


    public static void main(String[] args) {
        try {
            new NettyServer().bind(83);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

消息处理器

package com.guide.io.netty.code;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * 收到客户端请求的消息处理器
 *
 */
public class MessageHandler extends SimpleChannelHandler {

    private static Logger logger = LoggerFactory.getLogger(MessageHandler.class);

    public void messageReceived(ChannelHandlerContext channelHandlerContext, MessageEvent messageEvent) throws Exception {
        ChannelBuffer message = (ChannelBuffer) messageEvent.getMessage();
        byte[] bytes = message.readBytes(message.readableBytes()).array();
        logger.info("服务端收到消息:[{}]", new String(bytes, "UTF-8"));

        byte[] body = "服务端已收到".getBytes();
        byte[] header = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(body.length).array();
        Channels.write(channelHandlerContext.getChannel(), ChannelBuffers.wrappedBuffer(header, body));


    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 客户端代码

package com.guide.io.netty.code;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.SocketChannel;

public class NettyClient {


    private static Logger logger = LoggerFactory.getLogger(NettyClient.class);

    private final ByteBuffer readHeader = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);
    private final ByteBuffer writeHeader = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);

    private SocketChannel socketChannel;


    public void sendMessage(byte[] body) throws Exception {
        this.socketChannel = SocketChannel.open();
        this.socketChannel.socket().setSoTimeout(60000);
        this.socketChannel.connect(new InetSocketAddress(83));

        writeWithHeader(this.socketChannel, body);

        readHeader.clear();

        read(this.socketChannel, readHeader);
        int bodyLen = readHeader.getInt(0);
        ByteBuffer bodyBuf = ByteBuffer.allocate(bodyLen).order(ByteOrder.BIG_ENDIAN);
        read(this.socketChannel, bodyBuf);

        logger.info("客户端收到的响应内容:[{}]", new String(bodyBuf.array(), "UTF-8"));
    }


    private void writeWithHeader(SocketChannel channel, byte[] body) throws IOException {
        writeHeader.clear();
        writeHeader.putInt(body.length);
        writeHeader.flip();
        channel.write(ByteBuffer.wrap(body));
    }


    private void read(SocketChannel channel, ByteBuffer buffer) throws IOException {
        while (buffer.hasRemaining()) {
            int len = channel.read(buffer);
            if (len == -1) {
                throw new IOException("end of stream when reading header");
            }
        }
    }

    public static void main(String[] args) throws Exception {
        String body = "客户发的测试请求!";
        new NettyClient().sendMessage(body.getBytes());
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63

# 参考文献

Java NIO - IO多路复用详解 (opens new window)

NIO-SocketChannel.configureBlocking(false)作用 (opens new window)

超详细Netty入门,看这篇就够了! (opens new window)

编辑 (opens new window)
上次更新: 2026/03/26, 01:05:31
Java BIO NIO AIO详解
聊聊Java关于IO流中的设计模式

← Java BIO NIO AIO详解 聊聊Java关于IO流中的设计模式→

最近更新
01
基于EasyExcel实现高效导出
03-25
02
从开源框架中学习那些实用的位运算技巧
03-25
03
浅谈分布式架构设计思想和常见优化手段
03-25
更多文章>
Theme by Vdoing | Copyright © 2025-2026 Evan Xu | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×
×