一、NIO 非阻塞网络编程原理分析图

**NIO** 非阻塞网络编程相关的(**Selector****SelectionKey****ServerScoketChannel****SocketChannel**)关系梳理图

NIO 非阻塞网络编程(五) - 图1

对上图的说明:

  1. 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
  2. Selector 进行监听 select 方法,返回有事件发生的通道的个数。
  3. socketChannel 注册到 Selector 上,register(Selector sel, int ops),一个 Selector 上可以注册多个 SocketChannel
  4. 注册后返回一个 SelectionKey,会和该 Selector 关联(集合)。
  5. 进一步得到各个 SelectionKey(有事件发生)。
  6. 在通过 SelectionKey 反向获取 SocketChannel,方法 channel()
  7. 可以通过得到的 channel,完成业务处理。

在这其中有3个类要先解释一下理论,后会经过代码进行一一实践
SelectionKey ServerSocketChannel SocketChannel

二、SelectionKey

  1. **

**SelectionKey**,表示**Selector**和网络通道的注册关系,共四种:

  1. - `int OP_ACCEPT`:有新的网络连接可以 `accept`,值为 `16`
  2. - `int OP_CONNECT`:代表连接已经建立,值为 `8`
  3. - `int OP_READ`:代表读操作,值为 `1`
  4. - `int OP_WRITE`:代表写操作,值为 `4`
  1. **SelectionKey** 相关方法

NIO 非阻塞网络编程(五) - 图2

三、ServerSocketChannel

  1. **ServerSocketChannel** 在服务器端监听新的客户端 **Socket** 连接
  2. 相关方法如下

NIO 非阻塞网络编程(五) - 图3

四、SocketChannel

  1. **SocketChannel**,网络 **IO** 通道,具体负责进行读写操作。**NIO** 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。
  2. 相关方法如下

NIO 非阻塞网络编程(五) - 图4

五、NIO 非阻塞网络编程案例

案例要求:

  1. 编写一个 NIO 入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)
  2. 目的:理解 NIO 非阻塞网络编程机制

代码:

  1. 服务端 ```java import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.*; import java.util.Iterator; import java.util.Set;

/**

  • @author : [王振宇]
  • @version : [v1.0]
  • @className : NIOServer
  • @description : [服务端]
  • @createTime : [2021/8/2 14:18]
  • @updateUser : [王振宇]
  • @updateTime : [2021/8/2 14:18]
  • @updateRemark : [描述说明本次修改内容] */ public class NIOServer { public static void main(String[] args) throws Exception {

     //1.创建ServerSocketChannel  ===> 类似于ServerSocket
     ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
     //2.创建一个Selector选择器
     Selector selector = Selector.open();
     //3.绑定端口
     InetSocketAddress inetSocketAddress = new InetSocketAddress(9999);
     serverSocketChannel.socket().bind(inetSocketAddress);
     //4.设置为非阻塞
     serverSocketChannel.configureBlocking(false);
     //5.把serverSocketChannel注册到selector里, 关心的是SelectionKey.OP_ACCEPT 链接事件
     serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
     System.out.println("selector中的selector数量为:"+selector.keys().size());
     //6.循环等待客户端链接
     while (true){
         //这里我们等待1秒,
         if (selector.select(1000)==0){ //没有事件发生
             //System.out.println("服务器等待了1秒,无连接");
             continue;
         }
         //如果返回>0,就获取相关的selectionKeys集合
         //分析:
         //     1.如果返回>0,表示已经又关注事件发生了。
         //     2.通过selector.selectedKeys(),返回一下发生关注事件的集合
         //       然后通过selectionKeys反向获取通道
         Set<SelectionKey> selectionKeys = selector.selectedKeys();
         //7.编列集合,使用迭代器
         Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
         while (keyIterator.hasNext()){
             //获取到SelectionKey
             SelectionKey key = keyIterator.next();
             //这时候我们还得看看发生了什么事件
             if (key.isAcceptable()){ //如果是OP_ACCEPT事件就代表有客户端(新的网络)链接服务器了
                 //8.有链接就生成了SocketChannel了
                 //这里注意,我们在BIO的编程中,accept()是一个阻塞的方法,
                 // 这个我们不用担心,BIO中阻塞因为不知道下面还有没有客户端请求了,只能傻傻的等,
                 // 我们这边已经通过迭代便利就已经知道要链接了,链接行为都发生了,就不会阻塞了
                 SocketChannel socketChannel = serverSocketChannel.accept();
                 //9.把SocketChannel也放在selector选择器中 ,关注事件为OP_READ(表示通道里面已经有数据了,可以读了),再关联一个buffer缓存区
                 //注意设置socketChannel为非阻塞方式
                 socketChannel.configureBlocking(false);
                 System.out.println("有客户端链接了"+socketChannel.hashCode());
                 socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                 System.out.println("selector中的selector数量为:"+selector.keys().size());//因为服务器ServerSocketChannel已经占了一位了,这里客户端和服务器每链接一次就会加1
                 //2  3  4
             }
             if (key.isReadable()){ //如果是OP_READ事件(表示通道里面已经有数据了,可以读了)
                 //10.根据key反向获取相对应的通道  =>这里SelectableChannel强转一下
                 SocketChannel socketChannel = (SocketChannel)key.channel();
                 //11.根据key再获取当前通道关联的buffer,上文socketChannel注册的时候就已经关联进去buffer了 ==>这里Object强转一下
                 ByteBuffer buffer = (ByteBuffer) key.attachment();
                 //12.读入buffer
                 socketChannel.read(buffer);
                 System.out.println("from 客户端数据为:"+new String(buffer.array()));
                 //清空buffer
                 buffer.clear();
             }
             //13.特别注意,当一次遍历完后,要即使把SelectionKey从集合中删除出去,防止多线程重复操作
             keyIterator.remove();
         }
    
     }
    

    } } ```

  1. 客户端 ```java import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel;

/**

  • @author : [王振宇]
  • @version : [v1.0]
  • @className : NIOClient
  • @description : [客户端]
  • @createTime : [2021/8/2 15:38]
  • @updateUser : [王振宇]
  • @updateTime : [2021/8/2 15:38]
  • @updateRemark : [描述说明本次修改内容] */ 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",9999);
     //连接服务器
     if (!socketChannel.connect(inetSocketAddress)){
         while (!socketChannel.finishConnect()){
             System.out.println("因为连接需要时间,客户端不会阻塞,可以其他工作");
         }
     }
     //如果链接成功,,就发生数据
     String str = "这是一条客户端发生的数据";
     //创建buffer
     ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
     socketChannel.write(buffer);
     System.in.read();
    
    } } ```
  1. 运行结果

image.png