之前做的一个项目,先开始用的是BIO(即阻塞式IO),然后因为一些性能问题,然后用NIO(即非阻塞式IO)替换了BIO。
我们先说说BIO有什么缺点为什么要使用NIO:
以java中TCP为例来讲解:
我们知道,在客户端java调用connect方法会阻塞,调用read的时候也会阻塞也就是读不到就一直阻塞在那里,而服务器端呢,调用accept()方法会阻塞,调用read方法也会阻塞这样的话,会对性能造成很大的影响。
接下来我们讲解NIO的原理,还是以TCP为例,现在很多框架中都在用NIO像是mina框架,以及java中RMI调用等等都会用到NIO,如果堆NIO原理不清楚的话,就很容易造成性能,上的问题:
1.NIO中主要的概念:
第一个概念就是Channel,即通道,在BIO中是通过输入输出流来交互的,但是这些流是单向的,不能双向流通,那么在NIO中用Channel代替了流,作为通信,那么Channel是双向的,也就是数据流动的方式是双向的,也就服务器或者是客户端,谁想读取数据,就从channel中读取,谁想写入数据就向channel中写入数据。现在把流替换成管道就能实现NIO了吗,当然不可以,所以设计到了,第二个概念。
第二个主要概念就是Selector,NIO中就是通过这个概念来解决了阻塞的问题,在讲这个概念的时候我们得先,知道NIO的一点工作方式,NIO之所以是非阻塞的是因为,NIO是事件驱动的,而不是BIO中,通过阻塞实现的,这么说有点不清楚,那么我们举例说明,先讲什么是事件驱动,例如你没话费了,而且手机坏了不能接收短信,同时你也不能上网,那么你必须跑去营业厅查看,是不是欠费了,而如果营业厅没开门,你就回家去干其他的事。而阻塞是什么呢,就是如果营业厅不开门,你一直等在营业厅门口,知道开门查完你你才回家去,当然在营业厅的时候你不能干任何事情。那么我的NIO就是需要你不断的去看营业厅是否开门了,就是多跑几趟。这时问题出现了,那如果你回家了,这时候营业厅门开了,但是你没去,那岂不是就错过了。而NIO对于这个问题的解决方法是这样的,如果你和营业厅的人很熟,那么可能有人会把要是放在一个容器中说,你要是想查话费就去这个容器中取钥匙吧,假设这个容器是个盒子,那么你如果错过了的话,你就可以去盒子中取钥匙然后开门查看。那么Selector其实就相当于这个容器,而事件就相当于是钥匙,每次如果说想查看没有数据需要读取,那么就遍历这个盒子,如果盒子中有代表读事件的钥匙,那么就去管道当中读取,因为在通信过程中有好多事件,像是读(read),写(write),连接(connect),接收连接(accept),那么就相当于有四种钥匙,看到不同的时间去执行,不同的操作。那么这个时候新的问题又来了,假如你的手机是移动的,而且放钥匙的盒子在一个公共的地方,也就是说,移动的人能够往这个盒子中放钥匙,联通的人也能往这个盒子中放钥匙,那么你假如拿钥匙的时候,拿了联通的钥匙,然后你还去开联通的门,这时候你是不是就进错房间了,房间就相当于是channel,selector是公共的,你拿到的别的连接的时间,那么你岂不是就进错channel了,另一中情况是你不知道,拿着该时间,往哪个通道里写数据或者是读数据,这时候第三个重要的概念,就出现了。
第三个主要概念就是register注册,就是将selector注册给通道,也就是每一个通道一个selector,当然某些时候selector是可以被channel公用的,但是建议一个channel注册一个selector,这样的话在用你的channel的时候,就获取不到别的channel的事件了,也就是移动和联通每个都有一个盒子,你去不同地方拿钥匙,就会去到不同的房间。但是这样还是会有问题,你如果只是想去查手机话费,但是移动的盒子里放着两把,钥匙一把是移动营业厅的前门钥匙,一把是移动营业厅的后门钥匙,但是你指向从前门进,不想从后门进,也就是说,你对后门那把钥匙不感兴趣,我们人当然是不去拿他就好了,但是在程序中没这么智能,也就是在程序中,必须得看看这个钥匙是不是前门的,但是就是看这个动作在程序中也是耗时的,这时候为了解决这个问题,又出现第四个概念。
第四个主要概念,也可以说是一种规范,不是强制的,就是设置selector中的时间类型,如果说selector中只对读感兴趣,那么其他类型的时间一旦到达的话,会被selector忽视也就是不会,把时间放入selector,也就是你告诉营业厅的人,不要把后门钥匙放在盒子里。
2.工作原理:
经过以上的四个概念的阐述,我们队工作原理已经很清楚了,就是一个通信节点,先建立一个存储事件的selector,然后你还得注册感兴趣的时间,不然任何事件不会被放入,selector中,然后就是获取管道,因为管道总是需要通信双方有一方建立,建立通道后,然后给出你要连接的IP和端口,然后将IP和端口绑定在管道上,那么这个管道,会根据IP和port寻找你要通信的目标机,那么对方肯定会在一个已有的管道上,监听这个请求,如果监听到,那么就相当于管道,就对接好了,然后往管道上注册selector,管道双方的节点,都需要注册自己的selector,这样就可以通信了。一个节点,去查询selector,查询到事件后,然后响应时间,或是往通道里写数据,或是从通道里读数据,这样就可以通信了。基本原理是这样,但是在具体实施的时候还有一些细节问题需要搞懂,还有就是TCP连接的话,在客户端与服务器端是有点区别的,因为管道分为普通管道和服务器管道,但是两者其实没大区别,就是服务器管道主要是用来监听连接的,主要对accept事件,感兴趣,而普通管道主要是对读写事件感兴趣。为了帮助大家更好的理解,下面是我写个服务器代码和客户端代码,大家可以参考,基本就是一个客户端连接,那么在服务器端就建立一个线程进行处理。
服务器代码:
package com.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Channel;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NIOServer {
//服务端用于监听的selector
private Selector selector;
public void initServer(int port) throws IOException{
//获取一个ServerSocket通道
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//设置为非阻塞的
serverSocketChannel.configureBlocking(false);
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(port));
//获取一个监听器
this.selector=Selector.open();
//把channal于selector绑定
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
}
static class TCPservice implements Runnable{
private Selector serviceSelector;
private SocketChannel serviceChannel;
public TCPservice(SocketChannel serviceChannel){
this.serviceChannel=serviceChannel;
}
private void init() throws IOException{
serviceSelector=Selector.open();
//注册读事件,绑定
serviceChannel.register(serviceSelector, SelectionKey.OP_READ);
}
@Override
public void run() {
try {
init();
//轮询读事件
int count=10;
while(count>0){
count--;
//先写一条数据
serviceChannel.write(ByteBuffer.wrap(new String("Hello client").getBytes()));
serviceSelector.select();
Iterator<SelectionKey> it=serviceSelector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey key=it.next();
it.remove();
if(key.isReadable()){
ByteBuffer buffer = ByteBuffer.allocate(10);
serviceChannel.read(buffer);
String msg=new String(buffer.array());
System.out.println("服务器收到信息: "+msg);
serviceChannel.write(ByteBuffer.wrap("ServerAck".getBytes()));
}
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//监听连接
public void listen() throws IOException{
System.out.println("服务端启动成功!");
while(true){
//轮询连接事件
selector.select();
//遍历连接事件
Iterator<SelectionKey> it=selector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey key=it.next();
//以防重复处理
it.remove();
if(key.isAcceptable()){
//获取服务器通道
ServerSocketChannel serverSocketChannel=(ServerSocketChannel)key.channel();
//获取和客户端连接的通道
SocketChannel channel=serverSocketChannel.accept();
//设置成非阻塞
channel.configureBlocking(false);
//启动一个服务线程
System.out.println("开启一个服务线程");
Runnable service=new TCPservice(channel);
new Thread(service).start();
}
}
}
}
public static void main(String[] args) throws IOException {
NIOServer nioServer=new NIOServer();
nioServer.initServer(9768);
nioServer.listen();
}
}
客户端代码:
package com.test;
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;
public class NIOClient {
private Selector connectSelector;
public void init(String ip,int port) throws IOException{
//获取SocketChannel
SocketChannel socketChannel=SocketChannel.open();
//设置为非阻塞
socketChannel.configureBlocking(false);
//获取连接选择器
connectSelector=Selector.open();
//连接
socketChannel.connect(new InetSocketAddress(ip, port));
//注册连接事件
socketChannel.register(connectSelector, SelectionKey.OP_CONNECT);
}
public void listen() throws IOException{
while(true){
connectSelector.select();
Iterator<SelectionKey> it=connectSelector.selectedKeys().iterator();
while(it.hasNext()){
SelectionKey key=it.next();
it.remove();
if(key.isConnectable()){
SocketChannel channal=(SocketChannel)key.channel();
if(channal.isConnectionPending()){
channal.finishConnect();
}
channal.configureBlocking(false);
channal.write(ByteBuffer.wrap("Hello Server".getBytes()));
channal.register(connectSelector, SelectionKey.OP_READ);
}else if(key.isReadable()){
// 服务器可读取消息:得到事件发生的Socket通道
SocketChannel channel = (SocketChannel) key.channel();
// 创建读取的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
channel.read(buffer);
byte[] data = buffer.array();
String msg = new String(data).trim();
System.out.println("客户端收到信息: "+msg);
ByteBuffer outBuffer = ByteBuffer.wrap("client yes".getBytes());
channel.write(outBuffer);// 将消息回送给客户端
}
}
}
}
public static void main(String[] args) throws IOException {
NIOClient nioClient=new NIOClient();
nioClient.init("127.0.0.1", 9768);
nioClient.listen();
}
}