Netty学习-2. Java NIO初探

498 阅读6分钟

前言

当当当当,我又来了,趁热打铁,这次肝下Java NiO的demo。毕竟技术精通的各位大佬都知道Netty也是基于Java NIO做的封装。如果要学习Netty,肯定要对Java NIO有一定的了解啦。

注意, 这节基本就是代码了,介意的同学就先说声抱歉啦。

1. 服务端

  1. NioServer
public class NioServer {

  public static void main(String[] args) {
    int port = 8080;

    NioServerHandler nioServerHandler = new NioServerHandler(port);
    new Thread(nioServerHandler, "server").start();
  }
}

这个是服务端的入口,这里可以说是啥正经的都没干啊,就是启动了一个NioServerHandler这个线程而已,还有把8080这个端口,传给了这个线程。

  1. NioServerHandler
public class NioServerHandler implements Runnable {

  private Selector selector;

  private ServerSocketChannel serverSocketChannel;

  private volatile boolean stop;

  private List<SocketChannel> socketChannelList = new ArrayList<>();

  public NioServerHandler(int port) {
    try {
      // 创建多路复用器
      selector = Selector.open();
      // 创建 ServerSocketChannel
      serverSocketChannel = ServerSocketChannel.open();
      // 设置为异步非阻塞模式
      serverSocketChannel.configureBlocking(false);
      // 绑定端口,并且设置最大连接数量为1024个
      serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
      // 注册感兴趣的事件,这里注册的建立请求事件
      serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void run() {
    while (!stop) {
      try {
        // 无论有没有事件,多路复用器1s都会被唤醒1次, 如果不设置时间,就会一直等到有就绪的事件
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        SelectionKey key = null;
        while (iterator.hasNext()) {
          key = iterator.next();
          // 删掉已经处理过的key
          iterator.remove();
          try {
            handlerInput(key);
          } catch (IOException e) {
            key.cancel();
            if (key.channel() != null) {
              key.channel().close();
            }
          }
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
    if (selector != null) {
      try {
        selector.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  public void handlerInput(SelectionKey selectionKey) throws IOException {
    if (selectionKey.isValid()) {
      // 处理接入的请求消息
      if (selectionKey.isAcceptable()) {
        ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
        SocketChannel socketChannel = ssc.accept();
        // 客户端的socket也需要设置成非阻塞的
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        socketChannelList.add(socketChannel);
      }
      if (selectionKey.isReadable()) {
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        int readBytes = socketChannel.read(readBuffer);
        if (readBytes > 0) {
          readBuffer.flip();
          byte[] bytes = new byte[readBuffer.remaining()];
          readBuffer.get(bytes);
          String body = new String(bytes, "utf-8");
          System.out.println("server received :" + body);
          socketChannelList.forEach(sc -> {
            try {
              doWrite(sc, body);
            } catch (IOException e) {
              e.printStackTrace();
            }
          });
        }
      }
    }
  }

  private void doWrite(SocketChannel socketChannel, String message) throws IOException {
    byte[] bytes = message.getBytes(StandardCharsets.UTF_8);
    ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
    writeBuffer.put(bytes);
    writeBuffer.flip();
    socketChannel.write(writeBuffer);
  }


  public void stop() {
    this.stop = true;
  }
}

这个就是服务端主要的代码了,这里可以看到NioServerHandler这个类里面做了很多的事。没关系,我们一步步来。

  1. 构造函数,看一个类的代码,构造函数肯定是必须要看的函数之一了。我们可以看到这个类的构造函数用了一个Selector.open()的函数,这个Selector就是java里面对I/O多路复用的一个抽象了,对应的就是前面一节的select/poll/epoll的系统调用。然后是ServerSocketChannel.open(),这个方法其实就是创建一个套接字了,可以类比于ServerSocket。再然后就是serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);, 这个代码是啥意思呢?其实就是告诉前面创建的多路复用器,如果有客户端连接事件,你就告诉我, 这里的SelectionKey.OP_ACCEPT表示的就是服务端收到客户端的连接事件,与之对应的则是SelectionKey.OP_CONNECT,表示的是客户端连接服务端成功的事件。
  2. run方法, 既然NioServerHandler实现了Runable接口,那么run方法肯定也是特别重要的方法,但是这里的run方法其实挺简单的,就是调用selector.select()方法,这个方法的含义就是阻塞当前线程,直到注册的事件发生。然后遍历需要处理的SelectionKey。(看这个类的源码的注释的第一行可以知道,SelectionKey表示SelectableChannelSelector上的注册关系)
  3. handlerInput方法,这个方法则主要是对selectionKey进行判断了,看看当前的selectionKey代表的是客户端的接入事件,还是数据可读的事件。针对客户端的接入事件,将客户端的对象也绑定注册到selector上,并绑定SelectionKey.OP_READ,这样当客户端的发送来的数据包准备好之后,又会被selector.select()方法筛选出来,从而走到数据可读事件中。针对数据可读事件,这里就是读取SocketChannel中的数据,并向客户端发送了数据。
  4. doWrite方法,这个方法就是单纯的向客户端发送数据,应该没啥要讲的了。

3. 客户端

  1. NioClient
public class NioClient {

  public static void main(String[] args) {
    new Thread(new NioClientHandler("localhost", 8080), "`server").start();
  }
}

这里是客户端启动的入口,和服务端的入口类似,就是把需要连接的地址和端口传给了NioClientHandler, 主要的业务逻辑都是在这个NioClientHandler中。 2. NioClientHandler

public class NioClientHandler implements Runnable {

  private String host;

  private int port;

  private Selector selector;

  private SocketChannel socketChannel;
  private volatile boolean stop;

  public NioClientHandler(String host, int port) {
    this.host = host;
    this.port = port;
    try {
      selector = Selector.open();
      socketChannel = SocketChannel.open();
      socketChannel.configureBlocking(false);

      if (socketChannel.connect(new InetSocketAddress(host, port))) {
        socketChannel.register(selector, SelectionKey.OP_READ);
        doWrite(socketChannel);
      } else {
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  @Override
  public void run() {
    while (!stop) {
      try {
        selector.select();
        Set<SelectionKey> selectionKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = selectionKeys.iterator();
        SelectionKey key = null;
        while (iterator.hasNext()) {
          key = iterator.next();
          iterator.remove();
          handleInput(key);
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  private void handleInput(SelectionKey key) throws IOException {
    if (key.isValid()) {
      SocketChannel sc = (SocketChannel) key.channel();
      if (key.isConnectable()) {
        if (sc.finishConnect()) {
          sc.register(selector, SelectionKey.OP_READ);
          doWrite(sc);
        } else {
          System.exit(1);
        }
      }
      if (key.isReadable()) {
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        int readBytes = sc.read(readBuffer);
        if (readBytes > 0) {
          readBuffer.flip();
          byte[] bytes = new byte[readBuffer.remaining()];
          readBuffer.get(bytes);
          String body = new String(bytes, "utf-8");
          System.out.println("receive :" + body);
        }
      }
    }
  }

  public void doWrite(SocketChannel sc) {
    new Thread(() -> {
      while (true) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要发送的消息");
        String message = scanner.next();
        byte[] req = message.getBytes(StandardCharsets.UTF_8);
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        try {
          sc.write(writeBuffer);
        } catch (IOException e) {
          e.printStackTrace();
        }
      }

    }).start();
  }


  public void stop() {
    this.stop = true;
  }
}

我们也和前面一样,一个方法一个看下吧

  1. 构造方法,这里和服务端类似,首先创建了一个Selector, 由于这里是客户端,所有后面创建的是SocketChannel, 并将SocketChannel设置为非阻塞。后面调用socketChannel.connect(new InetSocketAddress(host, port))连接服务端。看这个方法的注释有如下一段文字:
If this channel is in non-blocking mode then an invocation of this method initiates a non-
blocking connection operation. If the connection is established immediately, as can happen with 
a local connection, then this method returns true. Otherwise this method returns false and the 
connection operation must later be completed by invoking the finishConnect method.

这段话的意思是,如果这个chanel是在非阻塞的模式下,这个方法调用会发起一个非阻塞的连接操作。如果这个连接马上建立成功了,比如是个本地连接,这个方法就会返回true,但是如果不能马上成功就会发挥false,然后这个连接操作成功之后,会去调用finishConnect方法。
所以我们这里,其实if成立的情况只有在本地连接才会发生,大概率是走到else的逻辑的也就是将socketChannel绑定连接事件。if成立的时候,也就是连接成功了,这里客户端是可以发起读/写的操作的,我们这里是先绑定到SelectionKey.OP_READ事件的,也就是绑定了读事件。

  1. run方法,也和服务端类似,就是遍历可以处理的SelectionKey,交给我们的handleInput方法做处理。
  2. handleInput方法,这里也是需要判断事件类型,是连接服务端成功的事件,还是数据可读事件。如果是连接服务端成功了,则需要转换成绑定数据可读事件。如果是数据可读事件,则去读取数据。
  3. doWrite方法,这个方法就是起了个线程将控制台输入的消息,通过SocketChannel发送到客户端而已。

到这里,特别简单的Java NiO的代码demo就完成了,分别启动服务端和客户端后,在客户端的控制台输入文字并回车后,服务端就会打印出对应的字符了。
这里我们总结下,NIO开发的常见套路吧:

  1. 创建多路复用器Selector.open()
  2. 创建channelServerSocketChannel/SocketChannel,并设置成非阻塞。
  3. 绑定事件, 初始的时候,服务端需要绑定SelectionKey.OP_ACCEPT,而客户端需要绑定SelectionKey.OP_CONNECT
  4. 事件处理:
    1. 针对SelectionKey.OP_ACCEPT,在连接成功后,需要转成绑定SelectionKey.OP_READ事件
    2. 针对SelectionKey.OP_CONNECT,在连接成功后,需要转成绑定SelectionKey.OP_READ事件
    3. 针对SelectionKey.OP_READ事件, 表示数据已经准备好了,可以做读操作了。
    4. 针对SelectionKey.OP_WRITE事件, 这个一般使用场景为数据过大,channel的发送缓冲区不够,需要多次写入的情况。