NIO通信代码怎么写(句句注释手把手教学)

几年前用NIO写了个仿qq软件,最近回顾时有点懵逼,当时囫囵吞枣、不求甚解,现在只能连蒙带猜,所以说时学时记真的不算浪费时间。
废话不多说,这篇博客将直接从代码的角度来解释NIO通信,用一个server端和一个client端的代码把整个NIO通信流程过一遍。详读代码,不懂再看解释,才是最正确的学习姿势。

什么是NIO

以下两张图分别是传统阻塞IO通信模型和NIO通信模型。
阻塞 I/O 的编程模型较为简单明晰,但性能相对较差。每个客户端连接进入服务器后生成一个 Socket 对象,服务器必须生成一个 Thread 对象在线程中处理这个 Socket 对象上数据的读取,即每线程对象对应与一个连接对象。当连接数增多时,仅创建大量线程,就会导致服务器非常慢甚至内存溢出。


传统阻塞模型

而NIO的服务器端,并不需要对每个连接(ScoketChannel)生成一个线程处理。每个连接进入后,将连接上需要处理的事件注册在通道管理器 Selector 对象上,只需要一个线程监视,并处理对应的 I/O 事件即可。


NIO模型

阻塞 I/O 是主动地调用 read()方法读取数据,会一直阻塞。在 NIO 中,仅当数据到达时,处理线程被动地被触发相应的 I/O 事件。而在NIO 体系中,连接的建立、通道数据的到达(readable)、通道可写(writeable)都基于事件通知机制实现,为非阻塞。

NIOServer.java


/**
 * NIOServer.java
 */

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.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOServer {
    private Selector selector; //通道选择器对象
    
    public void initServer(int port) throws IOException { 
        //创建 ServerSocket 通道
         ServerSocketChannel server = ServerSocketChannel.open(); 
         //绑定到指定端口
         server.socket().bind(new InetSocketAddress(port)); 
         //非阻塞模型
         server.configureBlocking(false);
         selector = Selector.open(); //初始化通道管理器
         //在通道上注册 accept 事件
         server.register(selector, SelectionKey.OP_ACCEPT);
     }
    
    /**
      * 监听端口
      */
     public void ioAction() {
         try {
             
             while(true) { //轮询等待事件发生
                 //System.out.println("1.在此等待 I/O 事件发生 ...");
                 //select 可理解为监听系统事件发生,此处阻塞直到事件发生
                 selector.select();
                 //System.out.println("2.有 I/O 事件发生");
                 Iterator iter = selector.selectedKeys().iterator(); 
                 while (iter.hasNext()){ 
                     SelectionKey key = (SelectionKey) iter.next(); 
                     iter.remove(); //移除这个事件
                     //System.out.println("3.处理这个 I/O 进入的事件 ");;
                     //1.如果是一个连接,处理一个进入的连接事件,
                     if (key.isAcceptable()) {//是客户端的连接请求到达
                         //System.out.println("4.一个连接进入事件发生");
                         ServerSocketChannel server = (ServerSocketChannel) key.channel(); 
                         //得到一个 Socket 通道(与客户端连接的).
                         SocketChannel channel = server.accept(); 
                         //设置非阻塞模式 
                         channel.configureBlocking(false);
                         //发送一条欢迎消息
                         String outMsg="你好,欢迎测试 NIO\r\n";
                         ByteBuffer resBuffer=ByteBuffer.wrap(outMsg.getBytes());
                         channel.write(resBuffer);
                         //注册有数据到达时可读的事件
                         channel.register(selector, SelectionKey.OP_READ); 
                         //System.out.println("有连接进入: "+channel.socket().toString());
                     } 
                     //2.如果是可读取事件(一个字节到达)
                     else if (key.isReadable()){
                         //System.out.println("5.一个可读数据事件发生(客户发了数据来)");
                         processRead(key); 
                     }
                 }
             }
             
         } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
         }  
     }
     
     protected void processRead(SelectionKey key) throws IOException {
         SocketChannel channel = (SocketChannel)key.channel();  //通过当前key获得对应channel
         
         //创建读取的缓冲区
         ByteBuffer buffer = ByteBuffer.allocate(10);//分配空间
         int count = channel.read(buffer);
         byte[] data=buffer.array();
         String msg=new String(data).trim();
         msg = channel.socket().getRemoteSocketAddress()+" 说:"+msg;
         System.out.println("读到数据: "+msg);
         //回送给客户端
         ByteBuffer outBuffer=ByteBuffer.wrap(msg.getBytes());
         channel.write(outBuffer);//将消息回送给客户端
     }
     
    
    //主函数
     public static void main(String[] args) { 
         int port = 8888; 
         try { 
             NIOServer server = new NIOServer(); 
             
             server.initServer(port);
             System.out.println("Server on " + port); 
             server.ioAction(); 
         } catch (IOException e) { 
             e.printStackTrace(); 
         } 
     }
}

从main函数开始,首先指定服务器开的端口(port),并对其进行初始化(initServer)。
initServer:创建一个ServerSocketChannel,绑定到port端口,初始化通道管理器Selector,并在创建好的ServerSocketChannel上注册accept事件(同时创建一个选择键key)。
目前,服务器上拥有了一个服务器通道,一个selector和一个在selector上注册的accept事件。
紧接着开启消息循环(ioAction)。
ioAction:在轮询等待事件发生的过程中,selector阻塞等待某个通道传来的特定类型的消息(当然此时通道只有ServerSocket通道和其上的accept事件)。当客户端A请求连接时,从selector中得到所有已选择键集,然后循环处理每一个键key。通过key可以获得其通道的状态,并对对应状态进行相应的操作。例如,如果通道已准备好接受新的套接字连接(key.isAcceptable()),那么通过该键获取其通道(这里就是初始化服务器时创建的ServerSocket通道),使用accpet方法创建与客户端A的SocketChannel,然后在这个客户端A通道上注册read事件。紧接着,当客户端A通道上有可读取事件到达时(key.isReadable()),从当前键值即可获取客户端A的channel,再针对该channel做读取写入处理等,即使以后出现客户端BCDEF,也不会存在搞不清channel的问题,因为key告诉了我们他是谁的key。

NIOClient.java


/**
 * NIOClient.java
 */

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 selector; //通道选择器对象
     
     public void initConn(String serverIp,int port) throws IOException { 
         //服务器地址对象
         InetSocketAddress address=new InetSocketAddress(serverIp,8888);
         //创建与服务器连接的 Socket 通道对象
         SocketChannel sc=SocketChannel.open();
         //非阻塞模型
         sc.configureBlocking(false); 
         selector = Selector.open(); //可开始监听连接进入
         //在通道上注册连接成功事件
         sc.register(selector, SelectionKey.OP_CONNECT); ;
         sc.connect(address);
     }
     
     /**
      * 监听端口
      */
     public void listen() {
         try {
             
             while(true) {
                 //System.out.println("在此等待 I/O 事件发生 ...");
                 selector.select();//select 可理解为监听系统事件发生,此处阻塞直到事件发生
                 //System.out.println("有 I/O 事件发生");
                 Iterator iter = selector.selectedKeys().iterator();
                 while(iter.hasNext()) {
                     SelectionKey key = (SelectionKey)iter.next();
                     iter.remove(); //移除这个事件
                     //System.out.println("处理这个io事件");
                     
                     if(key.isConnectable()) {
                         //System.out.println("isConnectable");
                         SocketChannel channel = (SocketChannel)key.channel();  //通过当前key获得对应channel
                         //如果连接正在进行中则完成这个连接
                         if(channel.isConnectionPending()) {
                             channel.finishConnect();   
                         }
                         
                         channel.configureBlocking(false);  //设置使用非阻塞模式
                         
                         //发送一条欢迎消息
                         String outMsg = "你好,我是NIOClient\r\n";
                         ByteBuffer buffer = ByteBuffer.wrap(outMsg.getBytes());
                         channel.write(buffer);
                         
                         //在通道上注册连接成功事件
                         channel.register(selector, SelectionKey.OP_READ);
                         
                         //System.out.println("连接服务器成功:"+channel.socket().getRemoteSocketAddress());
                         
                     }else if(key.isReadable()) {
                         //System.out.println("isreadable");
                         processRead(key);
                     }
                 }
             }
             
         } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
         }  
     }
     
     protected void processRead(SelectionKey key) throws IOException {
         SocketChannel channel = (SocketChannel)key.channel();  //通过当前key获得对应channel
         
         //创建读取的缓冲区
         ByteBuffer buffer = ByteBuffer.allocate(10);//分配空间
         int count = channel.read(buffer);
         byte[] data=buffer.array();
         String msg=new String(data).trim();
         System.out.println("读到数据: "+msg);
         //回送给服务器
         //ByteBuffer outBuffer=ByteBuffer.wrap(msg.getBytes());
         //channel.write(outBuffer);//将消息回送给对方
     }
     
    
    //主函数
     public static void main(String[] args) { 
         int port = 8888; 
         try { 
             NIOClient client = new NIOClient(); 
             
             client.initConn("localhost",port);
             System.out.println("listening on " + port); 
             client.listen(); 
         } catch (IOException e) { 
             e.printStackTrace(); 
         } 
     }
}

客户端与服务器端的代码非常相似,这里我不在赘述,相信以上代码加注释,再配合某度和jdk文档已足以让你搞懂NIO通信的流程了。

结果图

server
client1
client2
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • # Java NIO # Java NIO属于非阻塞IO,这是与传统IO最本质的区别。传统IO包括socket和文...
    Teddy_b阅读 626评论 0 0
  • 概述 NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区),Selector。 传统IO基于...
    时之令阅读 3,726评论 0 8
  • NIO 新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、...
    脆皮鸡大虾阅读 388评论 0 0
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    JackChen1024阅读 7,583评论 1 143
  • 熟练掌握 BIO,NIO,AIO 的基本概念以及一些常见问题是你准备面试的过程中不可或缺的一部分,另外这些知识点也...
    小王学java阅读 2,077评论 0 0