开始
最近在学习Netty,其核心便是基于Java的NIO编程封装而来,这篇文章对NIO编程的原理进行介绍,并提供一个NIO编程的完整实践案例。
什么是Netty?
Netty的特点总结如下:
- Netty是由JBOSS提供的一个Java开源框架,现在为Github上的独立开发项目。
 - Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。
 - Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。
 - Netty的本质是一个NIO框架,用于服务器通信。
 
Java网络编程三兄弟:BIO、NIO、NIO2(AIO)
显然NIO是Netty的灵魂,由于NIO属于Java三大I/O模型之一,要学习它不如把这三者合在一起先做个了解。
| I/O模型 | 类型 | 描述 | 
|---|
| Java BIO | 同步阻塞(传统阻塞型) | 服务器实现模式为一个连接绑定一个线程即客户端有一个连接,当有客户端请求时,就需要启动一个线程进行处理。如果这个连接不做任何事情则造不必要的线程开销。 | 
| Java NIO | 同步非阻塞 | 服务器实现模式为一个线程处理多个请求,即客户端发送的请求会注册到多路复用器上,多路复用器可以轮询存在的I/O请求进行处理。 | 
| Java AIO | 异步非阻塞 | AIO引入异步通道的概念,采用Proactor模式,简化了程序的编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 | 
NIO核心原理
NIO由三大核心部分组成:
Channel:通道。Buffer:缓冲区。Selector:选择器。
原理说明:
- 每个
Channel对应一个Buffer。 Selector对应一个线程,一个线程对应多个Channel。- 多个
Channel可以注册到一个Selector。 - 程序切换Channel由事件决定。
 Selector会根据不同事件,在各个Channel切换。- 数据的读取或者写入通过
Buffer,是双向的,但需要flip()切换读/写模式。 Channel是双向的,可以返回底层操作系统的情况(比如Linux底层是双向的)。
NIO模式示意图如下:
![NIO示例图]()
关于Channel
基本介绍:
BIO中的stream是单向的,例如FileInputStream 对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。Channel在NIO中是一 个接口:public interface Channel extends Closeable{}。- 常用的
Channel类有: FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel。ServerSocketChannel类似ServerSocket,SocketChannel类似Socket FileChannel 用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。
关于Buffer
Java的基本数据类型(除了boolean)均有一个Buffer类与之对应,最常用的自然是ByteBuffer类,该类的主要方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
   | public abstract class ByteBuffer{                  public static ByteBuffer llocateDirect(int capacty)         public static ByteBuffer llocate(int capacty)         public static ByteBuffer wrap(byte[] array)                  public static ByteBuffer wrap(byte[] array,int offset, int length)                  public abstract byte get( )         public abstract byte get (int index);         public abstract ByteBuffer put (byte b):         public abstract ByteBuffer put (int index, byte b); }
  | 
关于Selector
Selector类是一个抽象类,常用的方法如下:
1 2 3 4 5
   | public abstract class Selector implements Closeable {         public static Selector open();         public int select(long timeout);         public Set<SelectionKey> selectedKeys(); }
  | 
NIO编程流程
- 当客户端需要连接时,通过
ServerSocketChannel得到SocketChannel对象。 - 将该
SocketChannel对象注册到Selector上,使用register(Selector sel)方法,在一个selector上可以注册多个SocketChannel对象。(将监听服务通道和监听到的对象通道均注册到Selector) - 注册后返回一个
SelectionKey,会和该Selector关联(集合)。 Selector进行监听select方法,返回有事件发生的通道个数。- 进一步得到各个
SelectionKey。 - 通过使用
SelectionKey的channel()方法反向获取SocketChannel。 - 通过得到的
channel完成业务处理。 
NIO代码实践—简单的多用户聊天服务场景
服务端:NIOServer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
   | package com.tosang.nio;
  import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set;
 
 
 
 
  public class NIOServer {     public static void main(String[] args) throws IOException {                  ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
                   Selector selector = Selector.open();
                   serverSocketChannel.socket().bind(new InetSocketAddress(6666));
                   serverSocketChannel.configureBlocking(false);
                   serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
                   while (true){                          if(selector.select(1000) == 0){                                  System.out.println("服务器等待了1秒,无连接");                 continue;             }                                                    Set<SelectionKey> selectionKeys = selector.selectedKeys();                          Iterator<SelectionKey> keyIterator = selectionKeys.iterator();             while (keyIterator.hasNext()){                                  SelectionKey key = keyIterator.next();                                  if(key.isAcceptable()){                                                                                                         SocketChannel socketChannel = serverSocketChannel.accept();                     System.out.println("客户端连接成功,生成了一个通道 "+                             socketChannel.hashCode());                                          socketChannel.configureBlocking(false);                                                               socketChannel.register(selector,SelectionKey.OP_READ,                             ByteBuffer.allocate(1024));                 }                 if(key.isReadable()){                                                                                    SocketChannel channel = (SocketChannel) key.channel();                                          ByteBuffer buffer = (ByteBuffer)key.attachment();                     channel.read(buffer);                     System.out.println("From 客户端:"+new String(buffer.array()));                 }                                  keyIterator.remove();             }         }     } }
   | 
客户端:NIOClient
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
   | package com.tosang.nio;
  import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel;
 
 
 
 
  public class NIOClient {     public static void main(String[] args) throws Exception{                  SocketChannel socketChannel = SocketChannel.open();                  socketChannel.configureBlocking(false);                  InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);                                    if(!socketChannel.connect(inetSocketAddress)){             while (!socketChannel.finishConnect()){                 System.out.println("因为连接需要时间,客户端不会阻塞,可以继续工作");
              }         }                  String str = "hello ,小yi";                           ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
                   socketChannel.write(buffer);                  System.in.read();
      } }
   | 
启动NIOServer和多个NIOClient,可以看到打印的hashcode值不相等,说明一个Selector同时管理着多个连接。