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

NIO 非阻塞网络编程相关的(Selector、SelectionKey、ServerScoketChannel 和 SocketChannel)关系梳理图
image.png
对上图的说明:

  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,完成业务处理。

可以参考下面的案例进行理解

二、案例

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

    服务器端

    1. public class NIOServer {
    2. public static void main(String[] args) throws IOException {
    3. //创建ServerSocketChannel -> ServerSocket
    4. ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    5. //得到一个 selector 对象
    6. Selector selector = Selector.open();
    7. //绑定一个端口6666, 在服务器端监听
    8. serverSocketChannel.socket().bind(new InetSocketAddress(6666));
    9. // 设置为非阻塞
    10. serverSocketChannel.configureBlocking(false);
    11. //把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
    12. serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    13. System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
    14. //循环等待客户端连接
    15. while (true) {
    16. //这里我们等待1秒,如果没有事件发生, 返回
    17. if (selector.select(1000) == 0) { // 没有事件发生
    18. System.out.println("服务器等待了1秒,无连接");
    19. continue;
    20. }
    21. //如果返回的>0, 就获取到相关的 selectionKey集合
    22. //1.如果返回的>0, 表示已经获取到关注的事件
    23. //2. selector.selectedKeys() 返回关注事件的集合
    24. // 通过 selectionKeys 反向获取通道
    25. Set<SelectionKey> selectionKeys = selector.selectedKeys();
    26. System.out.println("selectionKeys 数量 = " + selectionKeys.size());
    27. //遍历 Set<SelectionKey>, 使用迭代器遍历
    28. Iterator<SelectionKey> iterator = selectionKeys.iterator();
    29. while (iterator.hasNext()) {
    30. // 获取到 selectionKey
    31. SelectionKey key = iterator.next();
    32. // 根据 key 对应的通道发生的事件做相应的处理
    33. if (key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
    34. //该该客户端生成一个 SocketChannel
    35. SocketChannel socketChannel = serverSocketChannel.accept();
    36. System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
    37. //将 SocketChannel 设置为非阻塞
    38. socketChannel.configureBlocking(false);
    39. //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
    40. //关联一个Buffer
    41. socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
    42. // 下面这行代码小提示 selector.selectedKeys() 和 selector.keys() 不一样
    43. // selector.selectedKeys() 返回有事件的 SelectionKey
    44. // selector.keys() 返回所有 SelectionKey
    45. System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
    46. }
    47. if (key.isReadable()) { //发生 OP_READ
    48. SocketChannel channel = (SocketChannel) key.channel();
    49. // 原来是监听 read事件的,可以改为监听 write事件
    50. // key.interestOps(SelectionKey.OP_WRITE);
    51. // 获取到该 channel 关联的 buffer
    52. ByteBuffer buffer = (ByteBuffer) key.attachment();
    53. channel.read(buffer);
    54. System.out.println("form 客户端 :" + new String(buffer.array()));
    55. }
    56. //手动从集合中移动当前的selectionKey, 防止重复操作
    57. iterator.remove();
    58. }
    59. }
    60. }
    61. }

    socketChannel一定要设置成非阻塞,不然会报错
    image.png

    客户端

    1. public class NIOClient {
    2. public static void main(String[] args) throws IOException {
    3. // 得到一个网络通道
    4. SocketChannel socketChannel = SocketChannel.open();
    5. //设置非阻塞
    6. socketChannel.configureBlocking(false);
    7. //提供服务器端的ip 和 端口
    8. InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
    9. //连接服务器
    10. if(!socketChannel.connect(inetSocketAddress)){
    11. while (!socketChannel.finishConnect()){
    12. System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
    13. }
    14. }
    15. //...如果连接成功,就发送数据
    16. String str = "hello, supkingx";
    17. // Wraps a byte array into a buffer.
    18. ByteBuffer buffer = ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8));
    19. //发送数据,将 buffer 数据写入 channel
    20. socketChannel.write(buffer);
    21. System.in.read();
    22. }
    23. }

    三、SelectionKey

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

    • int OP_ACCEPT:有新的网络连接可以 accept,值为 16
    • int OP_CONNECT:代表连接已经建立,值为 8
    • int OP_READ:代表读操作,值为 1
    • int OP_WRITE:代表写操作,值为 4
  • 源码中对应
    1. public static final int OP_READ = 1 << 0;
    2. public static final int OP_WRITE = 1 << 2;
    3. public static final int OP_CONNECT = 1 << 3;
    4. public static final int OP_ACCEPT = 1 << 4;
  1. SelectionKey 相关方法

image.png
这里面的方法在 上面的 案例 中悉数用到了。

四、ServerSocketChannel

  1. ServerSocketChannel 在服务器端监听新的客户端 Socket 连接,负责监听,不负责实际的读写操作
  2. 相关方法如下

image.png
这里面的方法在 上面的 案例 中悉数用到了。

五、SocketChannel

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

image.png
这里面的方法在 上面的 案例 中悉数用到了。