NIO编程总结

news/2024/11/8 6:52:28/

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

NIO的三个核心组件介绍

1、selector

selector是NIO的选择器,NIO的同步功能就是靠selector来实现的,多个channel可以注册到同一个selector上,然后用一个线程来管理一个selector选择器,就实现了通过一个线程来管理多个通道实现客户端同步请求的效果。我们可以将Server端的监听连接请求的事件和处理请求的事件放在一个线程中,但是在事件应用中,我们通常会把它们放在两个线程中:一个线程专门负责监听客户端的连接请求(OP_ACCEPT),而且是以阻塞方式执行的;另外一个线程专门负责处理请求(OP_READ、OP_WRITE等),这个专门处理请求的线程才会真正采用NIO的方式。

2、channel

NIO通道是一个对象,可以通过它(从客户端写入缓冲区)读取和写入数据(从缓冲区通过通道发送给客户端),可以理解为是对原I/O包中流的模拟,通道和流的区别在于,通道是双向的,通道可用于读、写或者同时读写,而流只能是一个方向,inputstream是读,outputstream是写。

3、buffer

NIO缓冲区就是一个数组,有byteBuffer、charBuffer、floatBuffer、LongBuffer等。数据总是从通道读取到缓冲区,或者从缓冲区写入到通道。

Channel

什么是Channel

这个概念绝对是一级概念,Channel是一个管道,用于连接字节缓冲区和另一端的实体, 这个字节缓冲区就是ByteBuffer, 另一端的实体可以是一个File 或者是 Socket ;

基于IO的网络编程, 数据的交互借助于InputStream或者是OutputStream, 而Channel可以理解成对Stream的又一层封装;在这种编程模型中 服务端想和客户端进行交互,就需要从服务端自己的ServerSocketChannel中获取前来连接的客户端的SocketChannel,并把他注册关联上感性趣的事件到自己的Selector选择器上, 这样一旦客户端把Buffer中的数据推送进channel, 服务端就可以感知,进而处理。

常用的Chanenl

  • 文件通道: FileChannel
  • 套接字通道
    • 服务端: ServerSocketChannel
    • 客户端: SocketChannel
  • 数据包通道: DataGramSocket

Channel 与 Stream(流)

Channel是NIO编程模型中一大组件,它类似IO中的Stream,但是两者也有本质的区别;

为什么说是类似呢? 看下面的两段代码, 需求是磁盘上的文件进行读写

在IO编程中,我们第一步可能要像下面这样获取输入流,按字节把磁盘上的数据读取到程序中,再进行下一步操作

 FileInputStream fileInputStream = new FileInputStream("123.txt");

在NIO编程中,目标是需要先获取通道,再基于Channel进行读写

FileInputStream fileInputStream = new FileInputStream("123.txt");
FileChannel channel = fileInputStream.channel();

对用户来说,在IO / NIO 中这两种都直接关联这磁盘上的数据文件,数据的读写首先都是获取Stream和Channel,所以说他们相似;

但是: 对于Stream来说,所有的Stream都是单向的,对我们的程序来说,Stream要么只能是从里面获取数据的输入流,要么是往里面输入数据的输出流,因为InputStream和outputStream都是抽象类,在java中是不支持多继承的, 而通道不同,他是双向的,对一个通道可读可写

怎么理解 Channel可以是双向的?#
如上图,凡是同时实现了readable,writeable接口的类,都双向的通道. 下面是典型的例子

SocketChannel
在NIO网络编程中,服务端可以通过ServerSocketChannel获取客户端的SocketChannel
这个SocketChannel可以read() 客户端的消息存入Buffer, 往客户端 write()buffer里的内容
socketChannel1.read(byteBuffer);
socketChannel1.write(byteBuffer);

对于一个channel,我们既能从中获取数据,也能往外read数据

Selector

Selector选择器NIO的第三个组件,三者的关系图如上所示

解释:一个线程专门管理Selector,Selector再去管理各个Channel通道。

什么是selector? 作用是什么?

selector是选择器的意思, 和它直接关联的组件是Channel, 没错,它的作用就是不断的轮询绑定在他身上的所有channel. 一旦有通道发生了它感兴趣的事件,接着处理此事件

selector维护了什么?

无论是服务端的Selector 还是客户端的Selector 它都维护了三个Set集合 , 里面封装的是 SelectionKey, 他是channel注册进Selector的产物,一般是使用它反向获取channel

  1. key set
  • 他是一个全集,每当channel通过register方法注册进选择器时,于此同时也会把包含自己信息的key添加到这个全集中来 注册的信息就会以SelectionKey的封装形式保存在这个集合中, 选择器每次轮询的channel,就是这里面的channel
  1. selected key
  • 感兴趣的key的集合, 举个例子, 通道1注册进选择器时,告诉选择器,我可能会给你发信息,你得盯着我,读我给你的信息, 于是选择器对通道1感性趣的事件是 read, 那么在选择器轮询channel时, 一旦通道1出现了write操作,就会被选择器感知,开始read
  • 每次遍历selected key时我们会执行这行代码:Set<SelectionKey> selectionKeys = selector.selectedKeys(); 它的意思是,我们取出了 选择器的感性事件的set集合,只要程序还在运行,只要选择器一旦被open(),除非我们手动的close() 否则选择器对象就不会被释放,所以它的感兴趣的set集合是不会被自动会收到,于是我们就得收到的把处理过的感兴趣的事件对应的SelectionKey移除出这个set集合,不然下一次轮询时,这个事件还会再一次被处理,并且无限制的处理下去
  • key有且仅有两种方式从 selected-key-set 中剔除 1. 通过Set的remove()方法, 2.通过迭代器的remove()方法
  1. cannelled key
  • 取消的key的集合,代表原来感兴趣的事件,现在不感兴趣了. 下一次轮询,进行select() 本集合中的SelectionKey会从key set中移除, 意味着它所关联的channel将会被选择器丢弃掉,不再进行监听
  • 关闭channel 或者是调用了cancel()方法都会将key添加到cannelled key 集合中
  • 使用场景: 一般会在客户端主动断开连接的时候使用它.

selector的select()方法

select(long); // 设置超时时间selectNow(); // 立即返回,不阻塞select(); 阻塞轮询

select()过程的细节:

  • 第一步, cannelled-key中的每一个元素会从全集key set中剔除,表示这些可以关联的通道不会被注册

  • 第二步

    操作系统帮我们轮询每一个通道是否有选择器感性趣的事情发生

    • 对于一条准备就绪的channel(发生事件通道),他至少会发生下面两件事之一:
      • 它的key会被添加进selected-key-set中,来标识它将被选中,进而处理
      • 如果它的key,已经存在于这个集合中了,下一步就是它的 read-operation将被更新
  • 第三步: 如果在轮询时发现了有任何key被放置在了cannelled-key-set中,重复第一步,不再注册它关联的通道

romove key 和 cannel key 的区别

前者是把key从selected key set集合,也就是被选中的集合中剔除出去,表示当前的事件已经处理完了

后者是表示,把key从全集中剔除出去, 表示想要废弃这个key关联的channel

selector的创建

他是根据不同操作系统提供的不同的Provider使用provide()创建出来的

NIO编程模型

如上图, 在NIO网络编程模式中,不再是传统的多线程编程模型,当有新的客户端的连接到来,不再重新开辟新的线程去跑本次连接,而是统一,一条线程处理所有的连接, 而一次连接本质上就是一个Channel, NIO网络编程模型是基于事件驱动型的; 即,有了提前约定好的事件发生,接着处理事件,没有事件发生,选择器就一直轮询 。

下面我们同通过一个简单聊天室实例来了解上面说的上图的流程

客户端

我们需要先定义一些变量
port = 9999                  //服务端端口
private Charset charset = Charset.forName("UTF-8");  //编码格式//打开服务器套接字通道 	 
ServerSocketChannel serverChannel = ServerSocketChannel.open();//创建Selector实例 选择器  Selector.open()
Selector selector = Selector.open();// 设置为非阻塞的方式
server.configureBlocking(false);
//绑定想要监听的端口   			 
server.bind(new InetSocketAddress(port));
// 注册到选择器上,设置为监听状态 
server.register(selector,SelectionKey.OP_ACCEPT);//=================现在就正在监听port端口===================while(true){	//轮询if (selector.select() == 0){  // 阻塞式等待 channel上有事件发生continue;}// 可以通过这个方法,知道可用通道的集合Set selectedKeys = selector.selectedKeys();Iterator keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey sk = (SelectionKey) keyIterator.next();keyIterator.remove();   		//处理一次事件就要把当前的key剔除!!!dealWithSelectionKey(server, sk);  //自定义方法 处理数据}
}

可以公开的情报:

SelectionKey

SelectionKey,选择键,在每次通道注册到选择器上时都会创建一个SelectionKey储存在该选择器上,该SelectionKey保存了注册的通道channel注册的选择器通道事件类型操作符等信息。

监听的Channel通道触发了一个事件意思是该事件已经就绪。

这四种事件用SelectionKey的四个 final变量 来表示:

SelectionKey.OP_CONNECT 连接就绪
SelectionKey.OP_ACCEPT 接收就绪
SelectionKey.OP_READ 读就绪
SelectionKey.OP_WRITE 写就绪

拿到了有事件的 SelectionKey

dealWithSelectionKey(ServerSocketChannel server, SelectionKey sk){//先判断一下哪些事件已经就绪if (sk.isAcceptable()) {SocketChannel sc = server.accept();	//非阻塞获取 套接字通道  连接成功// 设置非阻塞模式sc.configureBlocking(false);sc.register(selector, SelectionKey.OP_READ);// 注册选择器,并设置为读取模式,以后这个就用通道// 将此对应的channel设置为准备接受其他客户端请求sk.interestOps(SelectionKey.OP_ACCEPT);System.out.println("Server is listening from client :" + sc.getRemoteAddress());//远程地址sc.write(charset.encode("Please input your name.")); //给链接的客户端发消息}if (sk.isReadable()) {// 返回该SelectionKey对应的 Channel 客户端,其中有数据需要读取SocketChannel sc = (SocketChannel) sk.channel();// 获取当前 是哪个客户端发起的信息ByteBuffer byteBuffer = ByteBuffer.allocate(1024);StringBuilder content = new StringBuilder();try {while (true) {// todo todo todo  很重要的一点!!!  read方法是非阻塞的, 很可能还有没读取到数据就返回了int read = socketChannel.read(byteBuffer);if (read <= 0) {content.append(charset.decode(byteBuffer));//设置缓冲区编码格式break;}}} catch (IOException io) { //如果发生错误,比如客户端chanel了链接。sk.cancel();if (sk.channel() != null) {sk.channel().close();  //关闭该套接字通道}}//=============== 广播数据到所有的SocketChannel中==============for (SelectionKey key : selector.keys()) {Channel targetchannel = key.channel();//  targetchannel != sc 不回发给发送此内容的客户端if (targetchannel instanceof SocketChannel && targetchannel != sc) {SocketChannel dest = (SocketChannel) targetchannel;dest.write(charset.encode(content));}}}
}

客户端

对于客户端,如果它想往服务端发送键盘录入的内容时,获取键盘录入对象是免不了的事, 但是这对象会阻塞,于是客户端不得不开启一条新的线程运行读取服务器回传的数据,让自己能随时读取回传的数据,还能输入消息不被阻塞。这样我们的客户端就比较完善了。

先来准备一些变量
port = 9999; //服务端端口
Charset charset = Charset.forName("UTF-8"); //编码格式
String name = "";   //聊天昵称Selector selector = Selector.open();
//连接远程主机的IP和端口
SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", port));
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
======================== init完成 ===========================
//开辟一个新线程来读取从服务器端的数据
new Thread(new ClientThread()).start();//在主线程中 从键盘读取数据输入到服务器端
Scanner scan = new Scanner(System.in);
while (scan.hasNextLine()) {String line = scan.nextLine();//不允许发空消息if ("".equals(line)) {continue;}if ("".equals(name)) {name = line;line = name + USER_CONTENT_SPILIT;} else {line = name + USER_CONTENT_SPILIT + line;}//sc既能写也能读,这边是写sc.write(charset.encode(line));
}

读取从服务器端回传的数据 线程

private class ClientThread implements Runnable {@Overridepublic void run() {try {while (true) {int readyChannels = selector.select();if (readyChannels == 0) {continue;}//可以通过这个方法,知道可用通道的集合Set selectedKeys = selector.selectedKeys();Iterator keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey sk = (SelectionKey) keyIterator.next();keyIterator.remove();dealWithSelectionKey(sk);}}} catch (IOException io) {io.getMessage();}}private void dealWithSelectionKey(SelectionKey sk) throws IOException {if (sk.isReadable()) {//使用 NIO 读取 Channel中的数据,这个和全局变量sc是一样的,因为只注册了一个SocketChannel//sc既能写也能读,这边是读SocketChannel sc = (SocketChannel) sk.channel();ByteBuffer buff = ByteBuffer.allocate(1024);String content = "";while (sc.read(buff) > 0) {buff.flip();content += charset.decode(buff);}//若系统发送通知名字已经存在,则需要换个昵称if (USER_EXIST.equals(content)) {name = "";}System.out.println(content);sk.interestOps(SelectionKey.OP_READ);}}
}

参考:Nio编程模型总结


http://www.ppmy.cn/news/99832.html

相关文章

JAVA之数组2

添加元素 从后往前进行迭代&#xff0c;最后在末尾插入元素 tip&#xff1a;为避免数字在覆盖过程中丢失&#xff0c;故从后往前覆盖 删除元素 从前往后迭代&#xff0c;最后将末尾赋值为0 tip: 以覆盖的数arr【i】为基准&#xff0c;构造循环 共同处Tip: 范围均为【index1&…

图【数据结构】

目录 一、图的定义和基本术语 二、图的类型定义 三、图的存储结构 1、数组&#xff08;邻接矩阵&#xff09;表示法 二、邻接表&#xff08;链式&#xff09;表示法 三、图的邻接表的存储表示 四、十字链表与邻接多重链表 &#xff08;1&#xff09;十字链表 &#xff…

如何在华为OD机试中获得满分?Java实现【IPv4地址转换成整数】一文详解!

✅创作者:陈书予 🎉个人主页:陈书予的个人主页 🍁陈书予的个人社区,欢迎你的加入: 陈书予的社区 🌟专栏地址: Java华为OD机试真题(2022&2023) 文章目录 1. 题目描述2. 输入描述3. 输出描述4. Java算法源码5. 测试6.解题思路1. 题目描述 存在一种虚拟 IPv4<

Redis数据库简介

1.Redis数据库介绍 Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库&#xff0c;并提供多种语言的API。 2.Redis数据库特性 Redis支持数据的持久化&#xff0c;可以将内存中的数据保存在磁盘中&#xff0c;重启的时候可以再次加…

基于51单片机的项目作品汇总

篇记录下自己做的项目作品&#xff0c;作品有实物也有仿真&#xff0c;以实物居多&#xff0c;主要是以单片机为主&#xff0c;单片机有HC32,STM32,STC,51等&#xff0c;本人从事单片机行业5年&#xff0c;拥有丰富的经验。也涉及QT&#xff0c;LVGL&#xff0c;嵌入式&#xf…

哈夫曼树(Huffman)【数据结构】

目录 ​编辑 一、基本概念 二、哈夫曼树的构造算法 三、哈夫曼编码 假如<60分的同学占5%&#xff0c;60到70分的占15%…… 这里的百分数就是权。 此时&#xff0c;效率最高&#xff08;判断次数最少&#xff09;的树就是哈夫曼树。 一、基本概念 权&#xff08;we…

基于Android的视频分享平台的设计与实现

基于Android的视频分享平台的设计与实现 摘 要 短视频平台是以特定群众为目标的差异化群体定位工具。其利用自身的便捷性可以实现视频的随时拍摄和随时上传&#xff0c;可以产生亚文化圈的萌芽。这种开放便利的特性在吸引了广大用户的同时&#xff0c;也在一定程度上解决了由…

Pycharm中更新pip过程中遇见的问题

问题一&#xff1a; pip._vendor.urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host‘files.pythonhosted.org’, port443): Read timed out. You are using pip version 10.0.1, however version 23.1.2 is available. You should consider upgrading via the …