NIO 编程及代码实现

本文详细介绍Java NIO编程的基本概念和技术要点,包括缓冲区(Buffer)、通道(Channel)、多路复用器(Selector)等核心组件的使用,并提供了一个完整的NIO服务端和客户端的实现案例。
摘要由CSDN通过智能技术生成

NIO 编程

    JDK 1.4中的java.nio.*包中引入新的Java I/O库,其目的是提高速度。实际上,“旧”的I/O包已经使用NIO重新实现过,即使我们不显式的使用NIO编程,也能从中受益。速度的提高在文件I/O和网络I/O中都可能会发生,但本文只讨论后者。

    2.1、简介

    NIO我们一般认为是New I/O(也是官方的叫法),因为它是相对于老的I/O类库新增的(其实在JDK 1.4中就已经被引入了,但这个名词还会继续用很久,即使它们在现在看来已经是“旧”的了,所以也提示我们在命名时,需要好好考虑),做了很大的改变。但民间跟多人称之为Non-block I/O,即非阻塞I/O,因为这样叫,更能体现它的特点。而下文中的NIO,不是指整个新的I/O库,而是非阻塞I/O。

    NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。

    新增的着两种通道都支持阻塞和非阻塞两种模式。

    阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。

    对于低负载、低并发的应用程序,可以使用同步阻塞I/O来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用NIO的非阻塞模式来开发。

    下面会先对基础知识进行介绍。

    2.2、缓冲区 Buffer

    Buffer是一个对象,包含一些要写入或者读出的数据。

    在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。

    缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。

    具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。

    2.3、通道 Channel

    我们对数据的读取和写入要通过Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。

    底层的操作系统的通道一般都是全双工的,所以全双工的Channel比流能更好的映射底层操作系统的API。

    Channel主要分两大类:

  •     SelectableChannel:用户网络读写
  •     FileChannel:用于文件操作

    后面代码会涉及的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。

    2.4、多路复用器 Selector

    Selector是Java  NIO 编程的基础。

    Selector提供选择已经就绪的任务的能力:Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

    一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

    2.5、NIO服务端

    代码比传统的Socket编程看起来要复杂不少。

    直接贴代码吧,以注释的形式给出代码说明。

    NIO创建的Server源码:

[java] view plain copy
 print?
  1. package com.anxpp.io.calculator.nio;  
  2. public class Server {  
  3.     private static int DEFAULT_PORT = 12345;  
  4.     private static ServerHandle serverHandle;  
  5.     public static void start(){  
  6.         start(DEFAULT_PORT);  
  7.     }  
  8.     public static synchronized void start(int port){  
  9.         if(serverHandle!=null)  
  10.             serverHandle.stop();  
  11.         serverHandle = new ServerHandle(port);  
  12.         new Thread(serverHandle,"Server").start();  
  13.     }  
  14.     public static void main(String[] args){  
  15.         start();  
  16.     }  
  17. }  

    ServerHandle:

[java] view plain copy
 print?
  1. package com.anxpp.io.calculator.nio;  
  2. import java.io.IOException;  
  3. import java.net.InetSocketAddress;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.channels.SelectionKey;  
  6. import java.nio.channels.Selector;  
  7. import java.nio.channels.ServerSocketChannel;  
  8. import java.nio.channels.SocketChannel;  
  9. import java.util.Iterator;  
  10. import java.util.Set;  
  11.   
  12. import com.anxpp.io.utils.Calculator;  
  13. /** 
  14.  * NIO服务端 
  15.  * @author yangtao__anxpp.com 
  16.  * @version 1.0 
  17.  */  
  18. public class ServerHandle implements Runnable{  
  19.     private Selector selector;  
  20.     private ServerSocketChannel serverChannel;  
  21.     private volatile boolean started;  
  22.     /** 
  23.      * 构造方法 
  24.      * @param port 指定要监听的端口号 
  25.      */  
  26.     public ServerHandle(int port) {  
  27.         try{  
  28.             //创建选择器  
  29.             selector = Selector.open();  
  30.             //打开监听通道  
  31.             serverChannel = ServerSocketChannel.open();  
  32.             //如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式  
  33.             serverChannel.configureBlocking(false);//开启非阻塞模式  
  34.             //绑定端口 backlog设为1024  
  35.             serverChannel.socket().bind(new InetSocketAddress(port),1024);  
  36.             //监听客户端连接请求  
  37.             serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
  38.             //标记服务器已开启  
  39.             started = true;  
  40.             System.out.println("服务器已启动,端口号:" + port);  
  41.         }catch(IOException e){  
  42.             e.printStackTrace();  
  43.             System.exit(1);  
  44.         }  
  45.     }  
  46.     public void stop(){  
  47.         started = false;  
  48.     }  
  49.     @Override  
  50.     public void run() {  
  51.         //循环遍历selector  
  52.         while(started){  
  53.             try{  
  54.                 //无论是否有读写事件发生,selector每隔1s被唤醒一次  
  55.                 selector.select(1000);  
  56.                 //阻塞,只有当至少一个注册的事件发生的时候才会继续.  
  57. //              selector.select();  
  58.                 Set<SelectionKey> keys = selector.selectedKeys();  
  59.                 Iterator<SelectionKey> it = keys.iterator();  
  60.                 SelectionKey key = null;  
  61.                 while(it.hasNext()){  
  62.                     key = it.next();  
  63.                     it.remove();  
  64.                     try{  
  65.                         handleInput(key);  
  66.                     }catch(Exception e){  
  67.                         if(key != null){  
  68.                             key.cancel();  
  69.                             if(key.channel() != null){  
  70.                                 key.channel().close();  
  71.                             }  
  72.                         }  
  73.                     }  
  74.                 }  
  75.             }catch(Throwable t){  
  76.                 t.printStackTrace();  
  77.             }  
  78.         }  
  79.         //selector关闭后会自动释放里面管理的资源  
  80.         if(selector != null)  
  81.             try{  
  82.                 selector.close();  
  83.             }catch (Exception e) {  
  84.                 e.printStackTrace();  
  85.             }  
  86.     }  
  87.     private void handleInput(SelectionKey key) throws IOException{  
  88.         if(key.isValid()){  
  89.             //处理新接入的请求消息  
  90.             if(key.isAcceptable()){  
  91.                 ServerSocketChannel ssc = (ServerSocketChannel) key.channel();  
  92.                 //通过ServerSocketChannel的accept创建SocketChannel实例  
  93.                 //完成该操作意味着完成TCP三次握手,TCP物理链路正式建立  
  94.                 SocketChannel sc = ssc.accept();  
  95.                 //设置为非阻塞的  
  96.                 sc.configureBlocking(false);  
  97.                 //注册为读  
  98.                 sc.register(selector, SelectionKey.OP_READ);  
  99.             }  
  100.             //读消息  
  101.             if(key.isReadable()){  
  102.                 SocketChannel sc = (SocketChannel) key.channel();  
  103.                 //创建ByteBuffer,并开辟一个1M的缓冲区  
  104.                 ByteBuffer buffer = ByteBuffer.allocate(1024);  
  105.                 //读取请求码流,返回读取到的字节数  
  106.                 int readBytes = sc.read(buffer);  
  107.                 //读取到字节,对字节进行编解码  
  108.                 if(readBytes>0){  
  109.                     //将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作  
  110.                     buffer.flip();  
  111.                     //根据缓冲区可读字节数创建字节数组  
  112.                     byte[] bytes = new byte[buffer.remaining()];  
  113.                     //将缓冲区可读字节数组复制到新建的数组中  
  114.                     buffer.get(bytes);  
  115.                     String expression = new String(bytes,"UTF-8");  
  116.                     System.out.println("服务器收到消息:" + expression);  
  117.                     //处理数据  
  118.                     String result = null;  
  119.                     try{  
  120.                         result = Calculator.cal(expression).toString();  
  121.                     }catch(Exception e){  
  122.                         result = "计算错误:" + e.getMessage();  
  123.                     }  
  124.                     //发送应答消息  
  125.                     doWrite(sc,result);  
  126.                 }  
  127.                 //没有读取到字节 忽略  
  128. //              else if(readBytes==0);  
  129.                 //链路已经关闭,释放资源  
  130.                 else if(readBytes<0){  
  131.                     key.cancel();  
  132.                     sc.close();  
  133.                 }  
  134.             }  
  135.         }  
  136.     }  
  137.     //异步发送应答消息  
  138.     private void doWrite(SocketChannel channel,String response) throws IOException{  
  139.         //将消息编码为字节数组  
  140.         byte[] bytes = response.getBytes();  
  141.         //根据数组容量创建ByteBuffer  
  142.         ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);  
  143.         //将字节数组复制到缓冲区  
  144.         writeBuffer.put(bytes);  
  145.         //flip操作  
  146.         writeBuffer.flip();  
  147.         //发送缓冲区的字节数组  
  148.         channel.write(writeBuffer);  
  149.         //****此处不含处理“写半包”的代码  
  150.     }  
  151. }  

    可以看到,创建NIO服务端的主要步骤如下:

  1.     打开ServerSocketChannel,监听客户端连接
  2.     绑定监听端口,设置连接为非阻塞模式
  3.     创建Reactor线程,创建多路复用器并启动线程
  4.     将ServerSocketChannel注册到Reactor线程中的Selector上,监听ACCEPT事件
  5.     Selector轮询准备就绪的key
  6.     Selector监听到新的客户端接入,处理新的接入请求,完成TCP三次握手,简历物理链路
  7.     设置客户端链路为非阻塞模式
  8.     将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
  9.     异步读取客户端消息到缓冲区
  10.     对Buffer编解码,处理半包消息,将解码成功的消息封装成Task
  11.     将应答消息编码为Buffer,调用SocketChannel的write将消息异步发送给客户端

    因为应答消息的发送,SocketChannel也是异步非阻塞的,所以不能保证一次能吧需要发送的数据发送完,此时就会出现写半包的问题。我们需要注册写操作,不断轮询Selector将没有发送完的消息发送完毕,然后通过Buffer的hasRemain()方法判断消息是否发送完成。

    2.6、NIO客户端

    还是直接上代码吧,过程也不需要太多解释了,跟服务端代码有点类似。

    Client:

[java] view plain copy
 print?
  1. package com.anxpp.io.calculator.nio;  
  2. public class Client {  
  3.     private static String DEFAULT_HOST = "127.0.0.1";  
  4.     private static int DEFAULT_PORT = 12345;  
  5.     private static ClientHandle clientHandle;  
  6.     public static void start(){  
  7.         start(DEFAULT_HOST,DEFAULT_PORT);  
  8.     }  
  9.     public static synchronized void start(String ip,int port){  
  10.         if(clientHandle!=null)  
  11.             clientHandle.stop();  
  12.         clientHandle = new ClientHandle(ip,port);  
  13.         new Thread(clientHandle,"Server").start();  
  14.     }  
  15.     //向服务器发送消息  
  16.     public static boolean sendMsg(String msg) throws Exception{  
  17.         if(msg.equals("q")) return false;  
  18.         clientHandle.sendMsg(msg);  
  19.         return true;  
  20.     }  
  21.     public static void main(String[] args){  
  22.         start();  
  23.     }  
  24. }  

    ClientHandle:

[java] view plain copy
 print?
  1. package com.anxpp.io.calculator.nio;  
  2. import java.io.IOException;  
  3. import java.net.InetSocketAddress;  
  4. import java.nio.ByteBuffer;  
  5. import java.nio.channels.SelectionKey;  
  6. import java.nio.channels.Selector;  
  7. import java.nio.channels.SocketChannel;  
  8. import java.util.Iterator;  
  9. import java.util.Set;  
  10. /** 
  11.  * NIO客户端 
  12.  * @author yangtao__anxpp.com 
  13.  * @version 1.0 
  14.  */  
  15. public class ClientHandle implements Runnable{  
  16.     private String host;  
  17.     private int port;  
  18.     private Selector selector;  
  19.     private SocketChannel socketChannel;  
  20.     private volatile boolean started;  
  21.   
  22.     public ClientHandle(String ip,int port) {  
  23.         this.host = ip;  
  24.         this.port = port;  
  25.         try{  
  26.             //创建选择器  
  27.             selector = Selector.open();  
  28.             //打开监听通道  
  29.             socketChannel = SocketChannel.open();  
  30.             //如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式  
  31.             socketChannel.configureBlocking(false);//开启非阻塞模式  
  32.             started = true;  
  33.         }catch(IOException e){  
  34.             e.printStackTrace();  
  35.             System.exit(1);  
  36.         }  
  37.     }  
  38.     public void stop(){  
  39.         started = false;  
  40.     }  
  41.     @Override  
  42.     public void run() {  
  43.         try{  
  44.             doConnect();  
  45.         }catch(IOException e){  
  46.             e.printStackTrace();  
  47.             System.exit(1);  
  48.         }  
  49.         //循环遍历selector  
  50.         while(started){  
  51.             try{  
  52.                 //无论是否有读写事件发生,selector每隔1s被唤醒一次  
  53.                 selector.select(1000);  
  54.                 //阻塞,只有当至少一个注册的事件发生的时候才会继续.  
  55. //              selector.select();  
  56.                 Set<SelectionKey> keys = selector.selectedKeys();  
  57.                 Iterator<SelectionKey> it = keys.iterator();  
  58.                 SelectionKey key = null;  
  59.                 while(it.hasNext()){  
  60.                     key = it.next();  
  61.                     it.remove();  
  62.                     try{  
  63.                         handleInput(key);  
  64.                     }catch(Exception e){  
  65.                         if(key != null){  
  66.                             key.cancel();  
  67.                             if(key.channel() != null){  
  68.                                 key.channel().close();  
  69.                             }  
  70.                         }  
  71.                     }  
  72.                 }  
  73.             }catch(Exception e){  
  74.                 e.printStackTrace();  
  75.                 System.exit(1);  
  76.             }  
  77.         }  
  78.         //selector关闭后会自动释放里面管理的资源  
  79.         if(selector != null)  
  80.             try{  
  81.                 selector.close();  
  82.             }catch (Exception e) {  
  83.                 e.printStackTrace();  
  84.             }  
  85.     }  
  86.     private void handleInput(SelectionKey key) throws IOException{  
  87.         if(key.isValid()){  
  88.             SocketChannel sc = (SocketChannel) key.channel();  
  89.             if(key.isConnectable()){  
  90.                 if(sc.finishConnect());  
  91.                 else System.exit(1);  
  92.             }  
  93.             //读消息  
  94.             if(key.isReadable()){  
  95.                 //创建ByteBuffer,并开辟一个1M的缓冲区  
  96.                 ByteBuffer buffer = ByteBuffer.allocate(1024);  
  97.                 //读取请求码流,返回读取到的字节数  
  98.                 int readBytes = sc.read(buffer);  
  99.                 //读取到字节,对字节进行编解码  
  100.                 if(readBytes>0){  
  101.                     //将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作  
  102.                     buffer.flip();  
  103.                     //根据缓冲区可读字节数创建字节数组  
  104.                     byte[] bytes = new byte[buffer.remaining()];  
  105.                     //将缓冲区可读字节数组复制到新建的数组中  
  106.                     buffer.get(bytes);  
  107.                     String result = new String(bytes,"UTF-8");  
  108.                     System.out.println("客户端收到消息:" + result);  
  109.                 }  
  110.                 //没有读取到字节 忽略  
  111. //              else if(readBytes==0);  
  112.                 //链路已经关闭,释放资源  
  113.                 else if(readBytes<0){  
  114.                     key.cancel();  
  115.                     sc.close();  
  116.                 }  
  117.             }  
  118.         }  
  119.     }  
  120.     //异步发送消息  
  121.     private void doWrite(SocketChannel channel,String request) throws IOException{  
  122.         //将消息编码为字节数组  
  123.         byte[] bytes = request.getBytes();  
  124.         //根据数组容量创建ByteBuffer  
  125.         ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);  
  126.         //将字节数组复制到缓冲区  
  127.         writeBuffer.put(bytes);  
  128.         //flip操作  
  129.         writeBuffer.flip();  
  130.         //发送缓冲区的字节数组  
  131.         channel.write(writeBuffer);  
  132.         //****此处不含处理“写半包”的代码  
  133.     }  
  134.     private void doConnect() throws IOException{  
  135.         if(socketChannel.connect(new InetSocketAddress(host,port)));  
  136.         else socketChannel.register(selector, SelectionKey.OP_CONNECT);  
  137.     }  
  138.     public void sendMsg(String msg) throws Exception{  
  139.         socketChannel.register(selector, SelectionKey.OP_READ);  
  140.         doWrite(socketChannel, msg);  
  141.     }  
  142. }  

    2.7、演示结果

    首先运行服务器,顺便也运行一个客户端:

[java] view plain copy
 print?
  1. package com.anxpp.io.calculator.nio;  
  2. import java.util.Scanner;  
  3. /** 
  4.  * 测试方法 
  5.  * @author yangtao__anxpp.com 
  6.  * @version 1.0 
  7.  */  
  8. public class Test {  
  9.     //测试主方法  
  10.     @SuppressWarnings("resource")  
  11.     public static void main(String[] args) throws Exception{  
  12.         //运行服务器  
  13.         Server.start();  
  14.         //避免客户端先于服务器启动前执行代码  
  15.         Thread.sleep(100);  
  16.         //运行客户端   
  17.         Client.start();  
  18.         while(Client.sendMsg(new Scanner(System.in).nextLine()));  
  19.     }  
  20. }  

    我们也可以单独运行客户端,效果都是一样的。

    一次测试的结果:

 
  1. 服务器已启动,端口号:12345
  2. 1+2+3+4+5+6
  3. 服务器收到消息:1+2+3+4+5+6
  4. 客户端收到消息:21
  5. 1*2/3-4+5*6/7-8
  6. 服务器收到消息:1*2/3-4+5*6/7-8
  7. 客户端收到消息:-7.0476190476190474

    运行多个客户端,都是没有问题的。

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值