java NIO原理和应用

2022-11-30 17:03:02 浏览数 (1)

之前做的一个项目,先开始用的是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事件,感兴趣,而普通管道主要是对读写事件感兴趣。为了帮助大家更好的理解,下面是我写个服务器代码和客户端代码,大家可以参考,基本就是一个客户端连接,那么在服务器端就建立一个线程进行处理。

服务器代码:

代码语言:javascript复制
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();
	}
}

客户端代码:

代码语言:javascript复制
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();
	}

}

0 人点赞