当前位置: 首页 > news >正文

NIO网络通信基础

文章目录

  • 概述
  • 一、Socket
  • 二、NIO三大组件与事件
  • 三、Reactor模式
  • 四、NIO通信案例
    • 4.1、服务端
    • 4.2、客户端


本文为个人学习笔记整理,仅供交流参考,非专业教学资料,内容请自行甄别

概述

  前篇中提到,BIO是阻塞的IO,阻塞体现在建立连接和通信时,并且线程模型是1:1的。即使使用线程池进行处理,也受限于最大线程数以及cpu上下文的切换。
  NIO则是非阻塞的IO,利用了Reactor反应器模式和多路复用机制。可以实现服务端一个线程应对多个客户端的连接和请求而不阻塞。

一、Socket

  在计算机网络中,TCP、UDP是运输层的协议,应用层需要依靠运输层提供服务。Socket是位于应用层和运输层之间的一个中间软件抽象层,屏蔽了运输层协议的一些实现细节,例如TCP的三次握手,四次挥手,滑动窗口算法等。
  从设计模式的角度来看,Socket属于典型的外观模式,用户无需关心底层的具体实现细节,通过Socket提供的API即可实现网络编程。
  主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,无论是客户端连接上服务端,还是服务端接收了客户端的连接,都会产生一个Socket 实例:
在这里插入图片描述
创建服务端
在这里插入图片描述
接收客户端的连接
在这里插入图片描述
启动客户端

二、NIO三大组件与事件

  NIO的三大组件:

  • Selector:选择器,客户端和服务端会将自己感兴趣的事件注册到选择器上,在有事件到达时,选择器通知订阅者去执行相应的操作。(Reactor模式),通过一个Selector去管理多个Channel,就称为NIO的多路复用机制
  • Channel:通道,是应用间传递数据的渠道,是双工通信,应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据,而且可以同时进行读写。
  • Buffer:缓冲区,BIO和NIO的区别其一就是,BIO是面向字节流的,而NIO是面向缓冲区的。数据是从通道读入缓冲区,从缓冲区写入到通道中。
    • 在写入时,应用程序将数据写入Buffer,再通过Channel将Buffer中的数据传递出去。
    • 在读取时,先将Channel中的数据读入Buffer,应用程序再从Buffer中读取数据。

  与Selector和Channel相关的,还有一个SelectionKey。SelectionKey是一个抽象类,维护了Selector和Channel的关系。SelectionKey定义了四个事件:
在这里插入图片描述

  • OP_READ:读事件,当缓冲区中有数据可读时,触发该事件,通常需要主动去关注,以接收到缓冲区有数据可读的通知,并进行处理。
  • OP_WRITE:写事件,当缓冲区有空闲时,就会触发该事件。一般情况下是无需关注该事件的,因为缓冲区不会被占满,关注该事件浪费CPU资源。
  • OP_CONNECT:连接事件,当SocketChannel.connect()请求连接成功后就绪,只允许客户端关注。
  • OP_ACCEPT:接收连接事件,当服务器接收到客户端的连接后就需,只允许服务端关注。

  对于服务端而言,通常是ServerSocketChannel创建后注册OP_ACCEPT事件,在接收到客户端的连接,产生SocketChannel后,关注OP_READ事件。
在这里插入图片描述
在这里插入图片描述
  对于客户端而言,通常是SocketChannel创建后,执行连接服务端的操作,因为通过SocketChannelconfigureBlocking方法,将通道设置成了非阻塞,所以调用connect()方法可能立即返回,而不会等待连接建立。连接过程在后台异步进行,线程可以继续执行其他任务,就要根据connect()方法的返回值,执行不同的操作:

  • 如果connect()方法返回true,代表连接建立成功,就关注OP_READ事件。
  • 如果connect()方法返回false,代表连接没有建立完成,继续关注OP_CONNECT事件。

在这里插入图片描述
  后续在进行事件处理时,如果关注了OP_CONNECT事件,就再次判断通道是否建立完成,如果建立完成,就关注OP_READ事件。
在这里插入图片描述

三、Reactor模式

  Reactor模式是NIO的底层实现机制,体现在NIO的Selector选择器上。
在这里插入图片描述
图片来源:图灵学院

  Reactor模式的思想,简单来说,就是用户向选择器进行注册,告诉选择器自己所关心的事件,当对应的事件发生时,再由选择器去通知用户,然后用户执行自己的操作。所以选择器是Reactor模式的核心组件。
  Reactor模式主要由两个核心部分组成:

  1. Reactor:负责监听和分发I/O事件(如连接事件、读写事件)。它运行在一个独立线程中,通过选择器(Selector)实现I/O多路复用,不断轮询注册的事件源。当事件就绪时,Reactor将其分发给对应的事件处理器。
  2. 处理资源池:通常是一个线程池,负责执行事件处理逻辑(如读取数据、执行业务逻辑、发送响应)。这避免了耗时操作阻塞Reactor线程。

  假设一个Web服务器使用Reactor模式:
3. 主线程(Reactor线程)注册SocketChannel的READ事件,并调用selector.select()阻塞。
4. 同时,一个工作线程(来自处理资源池)可以处理之前接收到的HTTP请求(如解析数据、访问缓存)。
5. 当新数据到达时,Reactor线程唤醒,派发READ事件给处理器,然后工作线程接管处理。

  Reactor模式也有三种实现:

  1. 单线程的reactor:连接,读取,发送数据,处理业务,都在一个线程中执行。
  2. 多线程工作reactor:连接,读取,发送数据,由一个线程完成,业务工作由线程池去处理。
  3. 多线程主从reactor:连接由一个线程完成,读取,发送数据,由另一个线程完成,业务工作由线程池去处理。

四、NIO通信案例

4.1、服务端

public class NioServerDemo {public static void main(String[] args) throws IOException {//netty服务端 开启ssc//everSocketChannel:对连接建立的事件感兴趣,是对SeverSocket的包装。ServerSocketChannel ssc = ServerSocketChannel.open();//开启选择器Selector selector = Selector.open();//设置为非阻塞ssc.configureBlocking(false);//绑定端口ssc.socket().bind(new InetSocketAddress(8080));//Selector登记SSC对连接事件感兴趣。//SelectionKey是SSC和SC向Selector注册的相关的事件的代号。标记了不同的事件。读,写,连接,接受连接事件。ssc.register(selector, SelectionKey.OP_ACCEPT);while (true) {//获取当前有哪些事件selector.select(1000);Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();//防止事件重复被处理iterator.remove();//进行处理handle(key, selector);}}}private static void handle(SelectionKey key, Selector selector) throws IOException {if (key.isValid()) {/*处理新加入客户端的请求假设此时有新连接到达Selector*/if (key.isAcceptable()) {//拿到服务端的ssc//Selector将新连接分配给SSC。ServerSocketChannel ssc = (ServerSocketChannel) key.channel();//接受请求,得到sc//SSC接收完连接后,和客户端进行三次握手,产生一个Socket,然后将Socket包装成SCSocketChannel sc = ssc.accept();System.out.println("==========建立连接=========");//同样设置为非阻塞sc.configureBlocking(false);//Selector登记SC对读事件感兴趣//关注读事件sc.register(selector, SelectionKey.OP_READ);}/*处理读事件假设此时客户端发送了数据到Selector,通知SC有数据过来了。*/ else if (key.isReadable()) {//sc:socketchannel 处理读事件//由SC和客户端进行实际的网络读写。SocketChannel sc = (SocketChannel) key.channel();//创建缓冲区//并非直接将数据交付给应用层,而是经过buffer缓冲区。发送数据也是要经过buffer。//buffer本质上是内存中的一块区域,需要读写模式切换ByteBuffer byteBuffer = ByteBuffer.allocate(1024);//从通道读消息,写入缓冲区int bytes = sc.read(byteBuffer);if (bytes > 0) {//切换模式byteBuffer.flip();byte[] bs = new byte[byteBuffer.remaining()];byteBuffer.get(bs);String message = new String(bs, StandardCharsets.UTF_8);System.out.println("服务器收到消息:" + message);//发送应答消息到客户端doWrite(sc, message);} else if (bytes < 0) {/*取消特定的注册关系*/key.cancel();/*关闭通道*/sc.close();}}}}/*发送应答消息*/private static void doWrite(SocketChannel sc, String response) throws IOException {byte[] bytes = response.getBytes();ByteBuffer buffer = ByteBuffer.allocate(bytes.length);//将服务端的响应存入buffer中buffer.put(bytes);//切换模式buffer.flip();//写入客户端(从buffer中读,写入对方 )sc.write(buffer);}}

4.2、客户端

public class NioClientDemo {private static SocketChannel sc;public static void main(String[] args) throws IOException {new Thread(()->{try {client();} catch (IOException e) {throw new RuntimeException(e);}}).start();Scanner scanner = new Scanner(System.in);String next = scanner.next();doWrite(sc,next);}private static void client() throws IOException {//创建选择器Selector selector = Selector.open();//创建客户端通道sc = SocketChannel.open();//设置通道为非阻塞sc.configureBlocking(false);//非阻塞的连接//如果sc.connect返回true,代表连接已经建立完成if (sc.connect(new InetSocketAddress("127.0.0.1", 8080))) {//可以关注读取事件sc.register(selector, SelectionKey.OP_READ);}//否则连接还没有建立完成else {//继续关注建立连接事件sc.register(selector, SelectionKey.OP_CONNECT);}//轮询选择器while (true) {selector.select(1000);Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {SelectionKey sk = it.next();it.remove();if (sk.isValid()) {//获得关心当前事件的channelSocketChannel socketChannel = (SocketChannel) sk.channel();//连接事件if (sk.isConnectable()) {//查看通道连接是否建立完成if (sc.finishConnect()) {//建立完成,注册读事件socketChannel.register(selector,SelectionKey.OP_READ);} else{System.exit(1);}}else if (sk.isReadable()){//创建ByteBuffer,并开辟一个1M的缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);//将channel中的数据读取到buffer中int readBytes = sc.read(buffer);//读取到字节,对字节进行编解码if(readBytes>0){//将缓冲区当前的limit设置为position,position=0,// 用于后续对缓冲区的读取操作buffer.flip();//根据缓冲区可读字节数创建字节数组byte[] bytes = new byte[buffer.remaining()];//将缓冲区可读字节数组复制到新建的数组中buffer.get(bytes);String result = new String(bytes,"UTF-8");System.out.println("客户端收到消息:" + result);}//链路已经关闭,释放资源else if(readBytes<0){sk.cancel();sc.close();}}}}}}private static void doWrite(SocketChannel channel,String request)throws IOException {//将消息编码为字节数组byte[] bytes = request.getBytes();//根据数组容量创建ByteBufferByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);//将字节数组复制到缓冲区writeBuffer.put(bytes);//flip操作writeBuffer.flip();//发送缓冲区的字节数组/*关心事件和读写网络并不冲突*/channel.write(writeBuffer);}
}

http://www.lryc.cn/news/592897.html

相关文章:

  • 堆的实现,堆排序,咕咕咕
  • (5)颜色的灰度,亮度,对比度,透明度,都啥意思
  • ES v.s Milvus v.s PG
  • makefile -- part 1
  • Windows 安装WSL +Docker 部署通义千问大模型(同步解决Ubuntu启动命令闪退)
  • 白话深度学习:一副PPT入门CNN,ResNet和Transformer
  • ESP32-IDF LVGL UI 设计工具的使用
  • vs openssl编译提示无法打开文件“libssl.lib”或“libcrypto.lib”
  • 046_局部内部类与匿名内部类
  • NQA_路由自动切换实验(H3C)
  • 小记_想写啥写啥_实现行间的Latex公式_VScode始终折叠大纲
  • [Raspberry Pi]如何將無頭虛擬顯示器服務(headless display)建置在樹莓派的Ubuntu桌面作業系統中?
  • 学校同步时钟系统让时间精准统一
  • 美客多跨境电商平台怎么开店?美客多入驻门槛有哪些?
  • OOA(面向对象分析)深度解析:业务建模的核心方法论
  • 零售快销行业中线下巡店AI是如何颠覆传统计算机视觉识别的详细解决方案
  • ABAP ANALYZE_ACT_FIELDCAT 错误
  • 控制鼠标和键盘
  • C++ 程序设计考量表
  • 7.18 Java基础 |
  • 全国高等院校计算机基础教育研究会2025学术年会在西宁成功举办 ——高原论道启新程,数智融合育英才
  • 【PTA数据结构 | C语言版】斜堆的合并操作
  • Flutter 多语言(国际化)入门教程
  • 智能交通4G专网解决方案,引领智慧出行新时代
  • LatentSync: 一键自动生成对嘴型的视频
  • PyCharm 高效入门指南(核心模块详解二)
  • 微服务架构详解
  • Flutter 应用如何设计通知服务
  • Webpack 项目构建优化详解
  • Linux驱动学习day24(UART子系统)