谈谈NIO的优劣

BIO

传统socket编程,服务端代码如下

public static void main(String[] args) {
      try {
         ServerSocket ss = new ServerSocket(8888);
         System.out.println("启动服务器....");
         while(true){
         	 Socket s = ss.accept();   // 第一步阻塞
	         System.out.println("客户端:"+s.getInetAddress().getLocalHost()+"已连接到服务器");
	         BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));    
	         //读取客户端发送来的消息
	         String mess = br.readLine();  // 第二步阻塞
	         System.out.println("客户端:"+mess);
	      } catch (IOException e) {
	         e.printStackTrace();
	      }
       }
   }

在BIO中,如果不考虑多线程,会有两个地方阻塞:一个接受连接,一个接受数据。如果考虑多线程,资源消耗很大。

改进BIO

如何改进BIO,我们假设在BIO中,在阻塞的两个地方,都让它不阻塞(假设),同时将建立连接的socket存入集合,这样可以通过遍历来判断每个socket的消息发送情况,假设服务端代码如下

public static void main(String[] args) {
	  List<Socket> list = new ArrayList<>();
      try {
         ServerSocket ss = new ServerSocket(8888);
         System.out.println("启动服务器....");
         while(true){
         	 for(Socket s : list){
         	 	 s.setBlock(false);     // 第二步非阻塞
	         	 BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
		         //读取客户端发送来的消息
		         String mess = br.readLine();
		         System.out.println("客户端:"+mess);
         	 }
             ss.setBlock(false);  // 第一步非阻塞
	         Socket s = ss.accept();
	         if(s != null){
	         	list.add(s);
	         	System.out.println("客户端:"+s.getInetAddress().getLocalHost()+"已连接到服务器");
	         }	  
	      } catch (IOException e) {
	         e.printStackTrace();
	      }   
       }
   }

不断轮询Socket,看是否有数据,但是这种模式,是我们自己来控制遍历。如果连接特别多,而大多数仅仅开启了连接,并没有发送数据,会非常浪费cpu资源,但是这也是NIO的基础思想:解决了阻塞问题,统一管理socket,来获取是否有数据交互。

NIO

NIO采用的是单线程Reactor模型,核心就是Selector多路复用器,作用类似于改进BIO中的List,Selector机制有select,poll,epoll,在操作系统内核进行数据判别socket是否有数据,而不需要通过我们自己来控制遍历的socket结果,大大提高了效率。同时Selector可以注册事件,比如接收连接、读取数据等事件,针对不同事件进行单独处理。不管是什么事件,都会触发事件监听函数,通过函数结果来对建立的通道注册不同的事件。代码如下

public class NioServerWork implements Runnable {
 
	//多路复用器 Selector会对注册在其上面的channel进行;轮询,当某个channel发生读写操作时,
	//就会处于相应的就绪状态,通过SelectionKey的值急性IO 操作
	private Selector selector;//多路复用器
	private ServerSocketChannel channel;
	private volatile boolean stop;
	/**
	 * @param port
	 * 构造函数
	 */
	public NioServerWork(int port) {
		try {
			selector = Selector.open();//打开多路复用器
			channel = ServerSocketChannel.open();//打开socketchannel
			channel.configureBlocking(false);//配置通道为非阻塞的状态
			channel.socket().bind(new InetSocketAddress(port), 1024);//通道socket绑定地址和端口
			channel.register(selector, SelectionKey.OP_ACCEPT);//将通道channel在多路复用器selector上注册为接收操作
			System.out.println("NIO 服务启动 端口: "+ port);
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}		
	}
	public void stop(){
		this.stop=true;
	}
 
	@Override
	public void run() {//线程的Runnable程序
		System.out.println("NIO 服务  run()");
		while(!stop){
			try {
				selector.select(1000);//最大阻塞时间1s
				//获取多路复用器的事件值SelectionKey,并存放在迭代器中
				Set<SelectionKey> selectedKeys = selector.selectedKeys();
				Iterator<SelectionKey> iterator = selectedKeys.iterator();
				SelectionKey key =null;
				//System.out.println("NIO 服务  try");
				while(iterator.hasNext()){
					System.out.println("NIO 服务  iterator.hasNext()");
					key = iterator.next();
					iterator.remove();//获取后冲迭代器中删除此值
					try {
						handleinput(key);//根据SelectionKey的值进行相应的读写操作				
					} catch (Exception e) {
						if(key!=null){
							key.cancel();
							if(key.channel()!=null)
								key.channel().close();							
						}
					}									
				}							
			} catch (IOException e) {
				System.out.println("NIO 服务  run  catch IOException");
				e.printStackTrace();
				System.exit(1);
			}
		}		
	}
 
	/**
	 * @param key
	 * @throws IOException
	 * 根据SelectionKey的值进行相应的读写操作
	 */
	private void handleinput(SelectionKey key) throws IOException {
		System.out.println("NIO 服务  handleinput");
		if(key.isValid()){//判断所传的SelectionKey值是否可用
			if(key.isAcceptable()){//在构造函数中注册的key值为OP_ACCEPT,,在判断是否为接收操作
				ServerSocketChannel  ssc = (ServerSocketChannel)key.channel();//获取key值所对应的channel
				SocketChannel sc = ssc.accept();//设置为接收非阻塞通道
				sc.configureBlocking(false);
				sc.register(selector, SelectionKey.OP_READ);//并把这个通道注册为OP_READ			
			}
			if(key.isReadable()){//判断所传的SelectionKey值是否为OP_READ,通过上面的注册后,经过轮询后就会是此操作
				SocketChannel sc = (SocketChannel)key.channel();//获取key对应的channel
				ByteBuffer readbuf = ByteBuffer.allocate(1024);
				int readbytes = sc.read(readbuf);//从channel中读取byte数据并存放readbuf
				if(readbytes > 0){
					readbuf.flip();//检测时候为完整的内容,若不是则返回完整的
					byte[] bytes = new byte[readbuf.remaining()];
					readbuf.get(bytes);
					String string = new String(bytes, "UTF-8");//把读取的数据转换成string
					System.out.println("服务器接受到命令 :"+ string); 
					//"查询时间"就是读取的命令,此字符串要与客户端发送的一致,才能获取当前时间,否则就是bad order
					String currenttime = "查询时间".equalsIgnoreCase(string) ? new java.util.Date(System.currentTimeMillis()).toString() : "bad order";
					dowrite(sc,currenttime);//获取到当前时间后,就需要把当前时间的字符串发送出去
				}else if (readbytes < 0){
					key.cancel();
					sc.close();					
				}else{}				
			}			
		}		
	}
	/**
	 * @param sc
	 * @param currenttime
	 * @throws IOException
	 * 服务器的业务操作,将当前时间写到通道内
	 */
	private void dowrite(SocketChannel sc, String currenttime) throws IOException {
		System.out.println("服务器 dowrite  currenttime"+  currenttime);
	if(currenttime !=null && currenttime.trim().length()>0){
		byte[] bytes = currenttime.getBytes();//将当前时间序列化
		ByteBuffer writebuf = ByteBuffer.allocate(bytes.length);
		writebuf.put(bytes);//将序列化的内容写入分配的内存
		writebuf.flip();
		sc.write(writebuf);	//将此内容写入通道		
	}
 }

NIO的优点上面已经说了,缺点也很明显,作为单线程Reactor模型,虽然不阻塞,但是在处理事件上,也只有一条线程来处理,如果A事件的处理事件很长,会影响B事件的处理

Netty

基于NIO,采用的是主从Reactor多线程模型。对于NIO来说,主从就表示针对建立连接和其他事件分别用两个Selector来处理,同时在从Selector使用多线程来处理事件

参考

https://mp.weixin.qq.com/s/VdyXDBevE48Wtr95ug_aKw

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值