Java NIO学习二
Posted 想作会飞的鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java NIO学习二相关的知识,希望对你有一定的参考价值。
一、NIO的异步方式
异步 I/O 是一种 没有阻塞地 读写数据的方法。通常,在代码进行 read() 调用时,代码会阻塞直至有可供读取的数据。同样,write() 调用将会阻塞直至数据能够写入。另一方面,异步 I/O 调用不会阻塞。相反您将注册对特定 I/O 事件的兴趣,包括可读的数据的到达、新的套接字连接,等等,而在发生这样的事件时,系统将会告诉您。
异步 I/O 的一个优势在于,它允许您同时根据大量的输入和输出执行 I/O。同步程序常常要求助于轮询,或者创建许许多多的线程以处理大量的连接。使用异步 I/O,您可以监听任何数量的通道上的事件,不用轮询,也不用额外的线程。
NIO实现异步IO的方法是通过Selectors。
二、Selectors
异步 I/O 中的核心对象名为 Selector。Selector 就是您注册对各种 I/O 事件的兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。选择器提供选择执行已经就绪的任务的能力,这使得多元I/O成为可能,就绪选择和多元执行使得单线程能够有效率的同时管理多个I/O通道(channels),简单言之就是selector充当一个监视者,您需要将之前创建的一个或多个可选择的通道注册到选择器对象中。一个表示通道和选择器的键将会被返回。选择键会记住您关心的通道。它们也会追踪对应的通道是否已经就绪当您调用一个选择器对象的 select( )方法时,相关的键会被更新,用来检查所有被注册到该选择器的通道。您可以获取一个键的集合,从而找到当时已经就绪的通道。通过遍历这些键,您可以选择出每个从上次您调用 select( )开始直到现在,已经就绪的通道。
1、新建Selector
我们需要做的第一件事就是创建一个 Selector:
Selector selector = Selector.open();
对于网络NIO,为了接收连接,我们需要一个 ServerSocketChannel。事实上,我们要监听的每一个端口都需要有一个 ServerSocketChannel 。对于每一个端口,我们打开一个 ServerSocketChannel,如下所示:
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking( false );
ServerSocket ss = ssc.socket();
InetSocketAddress address = new InetSocketAddress( ports[i] );
ss.bind( address );
第一行创建一个新的 ServerSocketChannel ,最后三行将它绑定到给定的端口。第二行将 ServerSocketChannel 设置为 非阻塞的 。我们必须对每一个要使用的套接字通道调用这个方法,否则异步 I/O 就不能工作。
2、注册Channel
我们将对不同的通道对象调用 register() 方法,以便注册我们对这些对象中发生的 I/O 事件的兴趣。register() 的第一个参数总是这个 Selector。
下一步是将新打开的 ServerSocketChannels 注册到 Selector上。为此我们使用 ServerSocketChannel.register() 方法,如下所示
SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );
register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 accept 事件,也就是在新的连接建立时所发生的事件。这是适用于 ServerSocketChannel 的唯一事件类型。
需要注意register()方法的第二个参数,它是一个“interest set”,意思是注册的Selector对Channel中的哪些时间感兴趣,事件类型有四种:Connect、Accept、Read和Write。通道触发了一个事件意思是该事件已经 Ready(就绪)。所以,某个Channel成功连接到另一个服务器称为 Connect Ready。一个ServerSocketChannel准备好接收新连接称为 Accept Ready,一个有数据可读的通道可以说是 Read Ready,等待写数据的通道可以说是Write Ready。上面这四个事件对应到SelectionKey中的四个常量:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果你对多个事件感兴趣,可以通过or操作符来连接这些常量:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
请注意对 register() 的调用的返回值为SelectionKey类型。 SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。
3、SelectionKey
SelectionKey表示SelectableChannel 在 Selector 中的注册的标记/句柄。register()方法返回一个SelectinKey对象,这个对象包含一些你感兴趣的属性。通过调用某个SelectionKey的cancel()方法,关闭其通道,或者通过关闭其选择器来取消该Key之前,它一直保持有效。取消某个Key之后不会立即从Selector中移除它,相反,会将该Key添加到Selector的已取消key set,以便在下一次进行选择操作的时候移除它。
- interest集合
感兴趣的事件集合,可以通过SelectionKey读写interest集合,
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = (interestSet & Selection.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectioKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
- ready集合 —— 是通道已经准备就绪的操作的集合,在一个选择后,你会是首先访问这个ready set
int readySet = selectionKey.readyOps();
可以向检测interet集合那样的方法,来检测channel中什么事件或操作已经就绪,也可以使用一下四个方法,
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
- 从SelectionKey获取对应的Channel和Selector:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
- 附加的对象 —— 可以将一个对象或者更多的信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加与通道一起使用的Buffer,或是包含聚集数据的某个对象。
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
4、通过Selector选择就绪的通道
一旦向Selector注册了一个或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(连接,接受,读或写)已经准备就绪的那些通道。换句话说,如果你对”读就绪“的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
select() —— 阻塞到至少有一个通道在你注册的事件上就绪了
select(long timeout) —— 和select()一样,除了最长会阻塞timeout毫秒
selectNow() —— 不会阻塞,不管什么通道就绪都立刻返回;此方法执行非阻塞的选择操作,如果自从上一次选择操作后,没有通道变成可选择的,则此方法直接返回0
select()方法返回的Int值表示多少通道就绪。一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用selector的selectorKeys()方法,访问”已选择键集“中的就绪通道,
Set selectedKeys = selector.selectedKeys();
可以遍历这个已选择的集合来访问就绪的通道
int num = selector.select();
Set selectedKeys = selector.selectedKeys();
Iterator 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 eatablished 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();
首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,select() 方法将返回所发生的事件的数量num。
接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个 集合 。
我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。
在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的remove() 方法来删除处理过的 SelectionKey。
某个线程调用select()方法后阻塞了,即使没有通道已经就绪,也有办法让其从select()方法返回。只要让其他线程在第一个线程调用select()方法的那个对象上调用Selector.wakeup()方法即可。阻塞在select()方法上的线程会立马返回。
三、通过NIO实现Socket通信
1、服务端
package com.kang;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO服务端
*
*/
public class Nioserver
// 通道管理器
private Selector selector;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
*
* @param port
* 绑定的端口号
* @throws IOException
*/
public void initServer(int port) throws IOException
// 获得一个ServerSocket通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置通道为非阻塞
serverChannel.configureBlocking(false);
// 将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
// 获得一个通道管理器
this.selector = Selector.open();
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
// 当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
* @throws InterruptedException
*/
public void listen() throws IOException, InterruptedException
System.out.println("服务端启动成功!");
// 轮询访问selector
while (true)
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext())
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 客户端请求连接事件
if (key.isAcceptable())
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 获得和客户端连接的通道
SocketChannel channel = server.accept();
// 设置成非阻塞
channel.configureBlocking(false);
// 在这里可以给客户端发送信息Hello World!
channel.write(ByteBuffer.wrap(new String("Hello World!").getBytes()));
// 在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。注册读就绪事件
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
else if (key.isReadable())
Thread.sleep(4000);
read(key);
/**
* 处理读取客户端发来的信息 的事件
*
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);//读取数据到缓存区
byte[] data = buffer.array();
String msg = new String(data).trim();//还原信息
System.out.println("服务端收到信息:" + msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
channel.write(outBuffer);// 将消息回送给客户端
/**
* 启动服务端测试
*
* @throws IOException
* @throws InterruptedException
*/
public static void main(String[] args) throws IOException, InterruptedException
NIOServer server = new NIOServer();
server.initServer(8000);
server.listen();
2、客户端
package com.kang;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* NIO客户端
*
*/
public class NIOClient
// 通道管理器
private Selector selector;
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
*
* @param ip
* 连接的服务器的ip
* @param port
* 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip, int port) throws IOException
// 获得一个Socket通道
SocketChannel channel = SocketChannel.open();
// 设置通道为非阻塞
channel.configureBlocking(false);
// 获得一个通道管理器
this.selector = Selector.open();
// 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
// 用channel.finishConnect();才能完成连接
channel.connect(new InetSocketAddress(ip, port));
// 将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
channel.register(selector, SelectionKey.OP_CONNECT);
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
*
* @throws IOException
* @throws InterruptedException
*/
public void listen() throws IOException, InterruptedException
// 轮询访问selector
while (true)
selector.select();
// 获得selector中选中的项的迭代器
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext())
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 连接事件发生
if (key.isConnectable())
SocketChannel channel = (SocketChannel) key.channel();
// 如果正在连接,则完成连接
if (channel.isConnectionPending())
channel.finishConnect();
// 设置成非阻塞
channel.configureBlocking(false);
// 在这里可以给服务端发送信息
channel.write(ByteBuffer.wrap(new String("OK").getBytes()));
// 在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
channel.register(this.selector, SelectionKey.OP_READ);
// 获得了可读的事件
else if (key.isReadable())
Thread.sleep(2000);
read(key);
/**
* 处理读取服务端发来的信息 的事件
*
* @param key
* @throws IOException
*/
public void read(SelectionKey key) throws IOException
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("客户端收到信息:" + msg);
ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
channel.write(outBuffer);// 将消息回送给服务端
/**
* 启动客户端测试
*
* @throws IOException
* @throws InterruptedException
*/
public static void main(String[] args) throws IOException, InterruptedException
NIOClient client = new NIOClient();
client.initClient("localhost", 8000);
client.listen();
上面完成了服务端和客户端的信息交互,服务端把接收到的数据重新传给客户端;客户端也是如此,这样就形成了两者消息的交替打印。运行结果如下:
服务端:
客户端:
以上是关于Java NIO学习二的主要内容,如果未能解决你的问题,请参考以下文章