Java基础——Java NIO详解(二)

一、简介


       在我的上一篇文章Java NIO详解(一)中介绍了关于标准输入输出NIO相关知识, 本篇将重点介绍基于网络编程NIO(异步IO)。


二、异步IO


       异步 I/O 是一种没有阻塞地读写数据的方法。通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样, write()调用将会阻塞直至数据能够写入,关于同步的IO请参考另一篇文章Java IO

       另一方面,异步 I/O 调用不但不会阻塞,相反,您可以注册对特定 I/O 事件诸如数据可读、新连接到来等等,而在发生这样感兴趣的事件时,系统将会告诉您。

       异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。


三、Selector


       在我的Java NIO详解(一)中已经详细介绍了Java NIO三个核心对象中的BufferChannel,现在我们就重点介绍一下第三个核心对象SelectorSelector是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了。


1、采用Selector模式的的好处

       有了Selector,我们就可以利用一个线程来处理所有的channels。线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。

       但是,需要记住,现代的操作系统和CPU在多任务方面表现的越来越好,所以多线程的开销随着时间的推移,变得越来越小了。实际上,如果一个CPU有多个内核,不使用多任务可能是在浪费CPU能力。不管怎么说,关于那种设计的讨论应该放在另一篇不同的文章中。在这里,只要知道使用Selector能够处理多个通道就足够了。

下面这幅图展示了一个线程处理3个 Channel的情况:



2、如何创建一个Selector

       异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。

Selector selector = Selector.open();
       然后,就需要注册Channel到Selector了。


3、如何注册Channel到Selector

       为了能让Channel和Selector配合使用,我们需要把Channel注册到Selector上。通过调用 channel.register()方法来实现注册:

channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
       注意,注册的Channel  必须设置成异步模式 才可以,,否则异步IO就无法工作,这就意味着我们不能把一个 FileChannel注册到Selector,因为FileChannel没有异步模式,但是网络编程中的 SocketChannel是可以的。
       需要注意register()方法的第二个参数,它是一个 “interest set”,意思是注册的Selector对Channel中的哪些时间感兴趣,事件类型有四种:

  • Connect
  • Accept
  • Read
  • Write

       通道触发了一个事件意思是该事件已经 Ready(就绪)。所以,某个Channel成功连接到另一个服务器称为Connect Ready。一个ServerSocketChannel准备好接收新连接称为 Accept Ready,一个有数据可读的通道可以说是 Read Ready,等待写数据的通道可以说是Write Ready

上面这四个事件对应到SelectionKey中的四个常量:

1. SelectionKey.OP_CONNECT
2. SelectionKey.OP_ACCEPT
3. SelectionKey.OP_READ
4. SelectionKey.OP_WRITE
如果你对多个事件感兴趣,可以通过or操作符来连接这些常量:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

四、关于SelectionKey


       请注意对register()的调用的返回值是一个SelectionKey。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。SelectionKey中包含如下属性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

Interest Set

       就像我们在前面讲到的把Channel注册到Selector来监听感兴趣的事件,interest set就是你要选择的感兴趣的事件的集合。你可以通过SelectionKey对象来读写interest set:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;   
       通过上面例子可以看到,我们可以通过用AND 和SelectionKey 中的常量做运算,从SelectionKey中找到我们感兴趣的事件。

Ready Set

       ready set 是通道已经准备就绪的操作的集合。在一次选Selection之后,你应该会首先访问这个ready set。Selection将在下一小节进行解释。可以这样访问ready集合:

int readySet = selectionKey.readyOps();

       可以用像检测interest集合那样的方法,来检测Channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();


Channel 和Selector

       我们可以通过SelectionKey获得Selector和注册的Channel:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector(); 

Attach 一个对象

       可以将一个对象或者更多信息 attach 到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:

selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
       还可以在用register()方法向Selector注册Channel的时候附加对象。如: 
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

五、通过Selector选择通道


       一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“Read Ready”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道:

  • int select(): 阻塞到至少有一个通道在你注册的事件上就绪
  • int select(long timeout):select()一样,除了最长会阻塞timeout毫秒(参数)
  • int selectNow(): 不会阻塞,不管什么通道就绪都立刻返回,此方法执行非阻塞的选择操作。如果自从前一次选择操作后,没有通道变成可选择的,则此方法直接返回零。

       select()方法返回的int值表示有多少通道已经就绪。亦即,自上次调用select()方法后有多少通道变成就绪状态。如果调用select()方法,因为有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道处于就绪状态。


selectedKeys()

       一旦调用了 select()方法,它就会返回一个数值,表示一个或多个通道已经就绪,然后你就可以通过调用 selector.selectedKeys()方法返回的SelectionKey集合来获得就绪的Channel。请看演示方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys();
       当你通过Selector注册一个Channel时, channel.register()方法会返回一个SelectionKey对象,这个对象就代表了你注册的Channel。这些对象可以通过 selectedKeys()方法获得。你可以通过迭代这些selected key来获得就绪的Channel,下面是演示代码:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) { 
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {// a connection was established with a remote server.
} else if (key.isReadable()) {// a channel is ready for reading
} else if (key.isWritable()) {// a channel is ready for writing
}
keyIterator.remove();
}
       这个循环遍历selected key的集合中的每个key,并对每个key做测试来判断哪个Channel已经就绪。

       请注意循环中最后的keyIterator.remove()方法。Selector对象并不会从自己的selected key集合中自动移除SelectionKey实例。我们需要在处理完一个Channel的时候自己去移除。当下一次Channel就绪的时候,Selector会再次把它添加到selected key集合中。

SelectionKey.channel()方法返回的Channel需要转换成你具体要处理的类型,比如是ServerSocketChannel或者SocketChannel等等。


WakeUp()和Close()

       某个线程调用select()方法后阻塞了,即使没有通道就绪,也有办法让其从select()方法返回。只要让其它线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。

       如果有其它线程调用了wakeup()方法,但当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立即“醒来(wake up)”

       当用完Selector后调应道掉用close()方法,它将关闭Selector并且使注册到该Selector上的所有SelectionKey实例无效。通道本身并不会关闭。


六、一个完整的例子


下面通过一个MultiPortEcho的例子来演示一下上面整个过程。

public class MultiPortEcho {private int ports[];private ByteBuffer echoBuffer = ByteBuffer.allocate(1024);public MultiPortEcho(int ports[]) throws IOException {this.ports = ports;go();}private void go() throws IOException {// 1. 创建一个selector,select是NIO中的核心对象// 它用来监听各种感兴趣的IO事件Selector selector = Selector.open();// 为每个端口打开一个监听, 并把这些监听注册到selector中for (int i = 0; i < ports.length; ++i) {//2. 打开一个ServerSocketChannel//其实我们没监听一个端口就需要一个channelServerSocketChannel ssc = ServerSocketChannel.open();ssc.configureBlocking(false);//设置为非阻塞ServerSocket ss = ssc.socket();InetSocketAddress address = new InetSocketAddress(ports[i]);ss.bind(address);//监听一个端口//3. 注册到selector//register的第一个参数永远都是selector//第二个参数是我们要监听的事件//OP_ACCEPT是新建立连接的事件//也是适用于ServerSocketChannel的唯一事件类型SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT);System.out.println("Going to listen on " + ports[i]);}//4. 开始循环,我们已经注册了一些IO兴趣事件while (true) {//这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时// select() 方法将返回所发生的事件的数量。int num = selector.select();//返回发生了事件的 SelectionKey 对象的一个 集合Set selectedKeys = selector.selectedKeys();//我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件//对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。Iterator it = selectedKeys.iterator();while (it.hasNext()) {SelectionKey key = (SelectionKey) it.next();//5. 监听新连接。程序执行到这里,我们仅注册了 ServerSocketChannel//并且仅注册它们“接收”事件。为确认这一点//我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {//6. 接收了一个新连接。因为我们知道这个服务器套接字上有一个传入连接在等待//所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞ServerSocketChannel ssc = (ServerSocketChannel) key.channel();SocketChannel sc = ssc.accept();sc.configureBlocking(false);// 7. 讲新连接注册到selector。将新连接的 SocketChannel 配置为非阻塞的//而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector上SelectionKey newKey = sc.register(selector,SelectionKey.OP_READ);it.remove();System.out.println("Got connection from " + sc);} else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {// Read the dataSocketChannel sc = (SocketChannel) key.channel();// Echo dataint bytesEchoed = 0;while (true) {echoBuffer.clear();int r = sc.read(echoBuffer);if (r <= 0) {break;}echoBuffer.flip();sc.write(echoBuffer);bytesEchoed += r;}System.out.println("Echoed " + bytesEchoed + " from " + sc);it.remove();}}// System.out.println( "going to clear" );// selectedKeys.clear();// System.out.println( "cleared" );}}static public void main(String args2[]) throws Exception {String args[]={"9001","9002","9003"};if (args.length <= 0) {System.err.println("Usage: java MultiPortEcho port [port port ...]");System.exit(1);}int ports[] = new int[args.length];for (int i = 0; i < args.length; ++i) {ports[i] = Integer.parseInt(args[i]);}new MultiPortEcho(ports);}}















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

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

相关文章

Java基础——Java NIO详解(一)

一、基本概念 1、I/0简介 I/O即输入输出&#xff0c;是计算机与外界世界的一个借口。IO操作的实际主题是操作系统。在java编程中&#xff0c;一般使用流的方式来处理IO&#xff0c;所有的IO都被视作是单个字节的移动&#xff0c;通过stream对象一次移动一个字节。流IO负责把对象…

MAC上Git安装与GitHub基本使用

参考链接 MAC上Git安装与GitHub基本使用

Java基础——深入理解Java线程池

简介 我们使用线程的时候就去创建一个线程&#xff0c;这样实现起来非常简便&#xff0c;但是就会有一个问题&#xff1a; 如果并发的线程数量很多&#xff0c;并且每个线程都是执行一个时间很短的任务就结束了&#xff0c;这样频繁创建线程就会大大降低系统的效率&#xff0c;…

密码机项目安装软件时候出现的问题以及对应的解决办法

Could NOT find Boost (missing: locale) (found version "1.65.1") 使用命令 apt-get install libboost-locale-dev 进行安装 解决普通用户cmake版本11&#xff0c;而root用户版本15&#xff0c;clion对于版本兼容的问题 修改clion里面的toolchain&#xff0c;将其…

Java基础——线程及并发机制

前言 在Java中&#xff0c;线程是一个很关键的名词&#xff0c;也是很高频使用的一种资源。那么它的概念是什么呢&#xff0c;是如何定义的&#xff0c;用法又有哪些呢&#xff1f;为何说Android里只有一个主线程呢&#xff0c;什么是工作线程呢。线程又存在并发&#xff0c;并…

密码机 密钥管理项目安装配置 从零开始

安装gcc 更新sudo apt-get update下载gcc sudo apt-get install gcc参考链接 不推荐 安装g 下载g sudo apt-get install g 安装make sudo apt -get install make参考链接 安装cmake 下载地址参考链接 安装ssh sudo apt-get install ssh 安装git和配置 sudo apt-get inst…

Androud 如何有效减少重复代码

前言 重复的代码一直都是可维护性的大敌&#xff0c;重构的重要任务之一也就是要去除掉重复的代码&#xff0c;有效的减少重复代码&#xff0c;可以大大提高软件的扩展性。 在Android开发中&#xff0c;很容易产生重复的代码。因为Android是组件&#xff0c;模板式开发&#xf…

解决在sample文件夹里面写代码,在测试的时候因为virtual原因,make编译报错

代码的结构 错误显示 解决办法 添加一句话&#xff0c;具体的cpp依据情况而定set_source_files_properties(${PROJECT_SOURCE_DIR}/src/sample_storage_test.cpp COMPILE_FLAGS "-Wno-unused-parameter")

Android SharedPreferences总结及优化

一、SharedPreferences简介 Android 中的 SharedPreferences&#xff08;后续简称SP&#xff09;是轻量级的数据存储方式&#xff0c;能够保存简单的数据类型&#xff0c;比如 String、int、boolean 值等。应用场合主要是数据比较少的配置信息。其内部是以 XML 结构保存在 /dat…

Java基础——深入理解ReentrantLock

一、简介在Java中通常实现锁有两种方式&#xff0c;一种是synchronized关键字&#xff0c;另一种是Lock。二者其实并没有什么必然联系&#xff0c;但是各有各的特点&#xff0c;在使用中可以进行取舍的使用。二、ReentrantLock与synchronized的比较相同点&#xff1a; &#xf…

使用开源的openssl的md5头文件,实现对于文件的md5代码

需要安装openssl的库 sudo apt-get install opensslsudo apt-get install libssl-dev参考链接 代码 #include "openssl/md5.h" #include <iostream> #include <fstream> #include <iomanip>//#define MAX_DATA_BUFF 1024; //#define MD5_LENGTH…

Android 多进程开发

前言正常情况下&#xff0c;一个apk启动后只会运行在一个进程中&#xff0c;其进程名为AndroidManifest.xml文件中指定的应用包名&#xff0c;所有的基本组件都会在这个进程中运行。但是如果需要将某些组件&#xff08;如Service、Activity等&#xff09;运行在单独的进程中&am…

clion中链接openssl库

错误显示 前提条件 apt-get install opensslapt-get install openssl-dev 解决办法 在CMakeLists.txt文件中加入如下命令link_libraries(crypto) 参考链接 无法将openssl库链接到CLion C 程序c - 无法将openssl库链接到CLion C程序

Java中String、StringBuffer、StringBuilder三者的区别

一、简介String、StringBuffer、StringBuilder三个类之间的区别主要是在两个方面&#xff1a;运行速度和线程安全。二、区别1、运行速度&#xff0c;或者说是执行速度在这方面运行速度快慢为&#xff1a;StringBuilder > StringBuffer > String StringString为字符串常量…

Ubuntu环境下,使用clion编译器,使用开源opensll的对称AES算法对于文件进行加密,C++代码

前提准备条件 需要安装openssl需要安装openssl-dev需要配置CMakeLists.txt文件集体内容可以参考我提供的相关参考链接 AES_file.h #include <openssl/aes.h> #include <iostream> #include <fstream> #include <cstring>#define RELEASE_ARRAY(P) if…

Java提高篇 —— Java关键字之static的四种用法

一、前言 在java的关键字中&#xff0c;static和final是两个我们必须掌握的关键字。不同于其他关键字&#xff0c;他们都有多种用法&#xff0c;而且在一定环境下使用&#xff0c;可以提高程序的运行性能&#xff0c;优化程序的结构。下面我们先来了解一下static关键字及其用法…

C++ 使用move来删除用户指定的文件

代码 #include <iostream>bool remove_file(std::string path){if (remove(path.c_str())0){std::cout << "success!" << std::endl;}else{std::cout << "False!" << std::endl;} } int main() {std::string path "/…

Java提高篇 —— Java关键字之final的几种用法

一、前言 在java的关键字中&#xff0c;static和final是两个我们必须掌握的关键字。不同于其他关键字&#xff0c;他们都有多种用法&#xff0c;而且在一定环境下使用&#xff0c;可以提高程序的运行性能&#xff0c;优化程序的结构。下面我们来了解一下final关键字及其用法。 …

使用C++的方式实现AES算法

aes_file.h #include <iostream> #include <fstream> #include <bitset> #include <string> using namespace std; typedef bitset<8> byte; typedef bitset<32> word;const int Nr 10; // AES-128需要 10 轮加密 const int Nk 4; /…

Java提高篇 —— Java三大特性之封装

一、封装 封装从字面上来理解就是包装的意思&#xff0c;专业点就是信息隐藏&#xff0c;是指利用抽象数据类型将数据和基于数据的操作封装在一起&#xff0c;使其构成一个不可分割的独立实体&#xff0c;数据被保护在抽象数据类型的内部&#xff0c;尽可能地隐藏内部的细节&am…