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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

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