您的位置:首页 > 其它

nio原理与实例

2011-01-05 15:39 549 查看
Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()也是傻傻的等,这会影响我们程序继续做其他事情,那么改进做法就是开设线程,让线程去等待,但是这样做也是相当耗费资源的。
Java NIO非堵塞技术实际是采取Reactor模式,或者说是Observer模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。
Java NIO出现不只是一个技术性能的提高,你会发现网络上到处在介绍它,因为它具有里程碑意义,从JDK1.4开始,Java开始提高性能相关的功能,从而使得Java在底层或者并行分布式计算等操作上已经可以和C或Perl等语言并驾齐驱。
如果你至今还是在怀疑Java的性能,说明你的思想和观念已经完全落伍了,Java一两年就应该用新的名词来定义。从JDK1.5开始又要提供关于线程、并发等新性能的支持,Java应用在游戏等适时领域方面的机会已经成熟,Java在稳定自己中间件地位后,开始蚕食传统C的领域。
本文主要简单介绍NIO的基本原理,在下一篇文章中,将结合Reactor模式和著名线程大师Doug Lea的一篇文章深入讨论。
NIO主要原理和适用。
NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。
Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。

import java.io.IOException;
import java.net.InetAddress;
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.nio.charset.Charset;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;
public class Server{
private final Selector selector;
private final ServerSocketChannel serverSocketChannel;

public Server(int port) throws IOException{
// 创建选择器
selector = Selector.open();
// 打开监听信道
serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress adress = new InetSocketAddress(InetAddress.getLocalHost(),port);
//与本地端口绑定
serverSocketChannel.socket().bind(adress);
// 设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
// 注册选择器.并在注册过程中指出该信道可以进行Accept操作
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
}
public void start() {
System.out.println("the server is started......");
while (true) {
try {
int nKeys = selector.select();
if (nKeys > 0){
// selectedKeys()中包含了每个准备好某一I/O操作的信道的SelectionKey
Set<SelectionKey> scSet = selector.selectedKeys();
Iterator<SelectionKey> iter = scSet.iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey) iter.next();
iter.remove();
dispatch(key);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

public void dispatch(SelectionKey key) {
// 有客户端连接请求时
//if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
if (key.isAcceptable()) {
try {
System.out.println("Key is acceptable");
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel socket = (SocketChannel) ssc.accept();
socket.configureBlocking(false);
socket.register(selector, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
// 从客户端读取数据
//else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
else if (key.isReadable()) {
System.out.println("the key is readable");
// new Thread(new ReadeHandler(key)).start();
try {
SocketChannel socket = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socket.read(buffer);
if (bytesRead > 0) {
buffer.flip();
// 将字节转化为为UTF-16的字符串
String receivedString = Charset.forName("UTF-16").newDecoder().decode(buffer).toString();
// 控制台打印出来
System.out.println("接收到来自"
+ socket.socket()
.getRemoteSocketAddress()
+ "的信息:" + receivedString);
// 准备发送的文本
String sendString = "你好,客户端. @"
+ new Date().toString() + ",已经收到你的信息:"
+ receivedString;
buffer = ByteBuffer.wrap(sendString
.getBytes("UTF-16"));
socket.write(buffer);
// 设置为下一次读取或是写入做准备
key.interestOps(SelectionKey.OP_READ);
}
} catch (IOException e) {
//客户端断开连接,所以从Selector中取消注册
key.cancel();
if(key.channel() != null)
try {
key.channel().close();
System.out.println("the client socket is closed!");
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
// 客户端可写时
else if (key.isWritable()) {
System.out.println("tHe key is writable");
//new Thread(new WriteHandler(key)).start();
//do something
}
}

public static void main(String[] args) throws IOException {
Server server = new Server(9911);
server.start();
}
}

这是一个守候在端口9011的noblock server例子,如果我们编制一个客户端程序,就可以对它进行互动操作,或者使用telnet 主机名 9911 可以链接上。

import java.io.IOException;
import java.net.InetAddress;
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.nio.charset.Charset;
public class Client {
// 信道选择器
private Selector selector;
// 与服务器通信的信道
SocketChannel socketChannel;

public Client(int port)throws IOException{
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(),port));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
// 启动读取线程
new Thread(new ClientReadThread()).start();
}
//发送字符串到服务器
public void sendMsg(String message) throws IOException{
ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes("UTF-16"));
socketChannel.write(writeBuffer);
}

public static void main(String[] args) throws IOException{
Client client = new Client(9911);
client.sendMsg("你好!Nio!");
}

class ClientReadThread implements Runnable{
public void run() {
try {
while (selector.select() > 0) {
// 遍历每个有可用IO操作Channel对应的SelectionKey
for (SelectionKey sk : selector.selectedKeys()) {
// 如果该SelectionKey对应的Channel中有可读的数据
if (sk.isReadable()) {
// 使用NIO读取Channel中的数据
SocketChannel sc = (SocketChannel) sk.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
buffer.flip();
// 将字节转化为为UTF-16的字符串
String receivedString = Charset.forName("UTF-16").newDecoder().decode(buffer).toString();
// 控制台打印出来
System.out.println("接收到来自服务器" + sc.socket().getRemoteSocketAddress() + "的信息:" + receivedString);
// 为下一次读取作准备
sk.interestOps(SelectionKey.OP_READ);
}
// 删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}


注意的是:

在客户端主动关闭连接之后,按理说服务端在调用Selector的select方法时候应该是阻塞的,但是我的测试代码中却仍然能够返回,而且返回的SelectionKey的isReadable方法返回的仍然是key,导致死循环。

原因是 当客户端主动切断连接时,FD_READ仍然起作用,也就是说,状态仍然是有东西可读,不过读出来的字节是0,所以需要判断客户端是否已经断开:

链接断开后,虽然该channel的ready operation是OP_READ,但是此时channel.read(buffer)返回-1,此时可以增加一个判断
if (socketChannel.read(buffer) != -1) {
buffer.flip();
System.out.println(Charset.defaultCharset().decode(buffer));
} else {
System.out.println("client socket is closed");
socketChannel.close();
}


但是这种方法在我的测试代码中没有起作用,最终的解决方法是使用异常捕捉:

catch (IOException e) {
//客户端断开连接,所以从Selector中取消注册
key.cancel();
if(key.channel() != null)
try {
key.channel().close();
System.out.println("the client socket is closed!");
} catch (IOException e1) {
e1.printStackTrace();
}
}


另外,对于写通知,socket空闲时,即为可写;有数据来时,可读。空闲状态下,所有的通道都是可写的,如果你给每个通道注册了写事件,那么非常容易造成死循环。所以一般不使用写通知。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: