前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Java网络编程之NIO

Java网络编程之NIO

作者头像
心平气和
发布2021-03-16 16:31:31
发布2021-03-16 16:31:31
26600
代码可运行
举报
运行总次数:0
代码可运行

一、什么是NIO

有人称之为New I/O,因为它是相对于之前的I/O库是新的,不过在NIO之前是BIO,即阻塞I/O,所以NIO的目标是让Java支持非阻塞的I/O,所以有人也称之为非阻塞I/O。

NIO是在JDK1.4中引入的,它提供了更高效的I/O操作方式。

二、NIO的基本概念

1、缓冲区(Buffer)

它代表要写入/读取的数据,为什么要引入Buffer呢,是为了异步化读写数据的流程从而系统的吞吐能力,在BIO中系统将数据直接写入或者从Stream中读取,而在NIO中写入数据时先写到缓冲区中,再将数据从缓存区中发往内核进行实际的发送,不再直接写入到Stream中,这样将应用的发送和实际物理的发送隔离开来,从而实现程序的异步化,从而提高系统的吞吐量,当然这样也会增加系统的复杂性。

最常用的缓冲区是ByteBuffer,它提供了基于byte的缓冲操作,还有其它一些缓存,如CharBuffer、ShortBuffer等,除了Boolean之外每一种Java基本类型都有对应的缓冲区。

2、通道(Channel)

通过通道可以读取、写入数据,就像水管一样,网络数据的读取和写入都是通过Channel来完成的。通道是双向的,即可以同时双向操作,且同时支持读写操作。

说起来可能有些抽象,其实在其它系统中也有相应的概念,请看我以前的文章从RabbitMQ Channel设计看连接复用 ,可以理解Channel表示一次客户端和服务端收发数据的一个过程,它和连接还不一样,连接代表的物理的联系,对应一个TCP连接,而通道是个逻辑的概念,可以在一个连接发生多次。

实际打交道比较多的是ServerSocketChannel和SocketChannel,前者代表服务端,后者表示客户端。

3、多路复用器(Selector)

这个是NIO编程的基础,多路复用器可以理解为对通道的管理,因为实际数据的收发都是在通道上完成的,实际的情况是需要同时处理多个通道,如果全由应用去维护是非常麻烦的,多路复用器就是做这个事情的,我们把通道注册进去,然后注册需要感兴趣的事件,多路复用器就在相应事件发生的时候回调我们。

三、NIO编程实践

接下来就写一个echo的服务器熟悉下整个流程,这个服务器很简单,客户端发送什么它就返回什么。

先梳理下服务端的编写过程:

1、打开ServerSocketChannel;

2、绑定并监听地址;

3、创建多路复用器Selector;

4、将ServerSocketChannel注册到多路复用器Selector,并监听Accept事件;

5、当有新连接的时候Accept之后再注册到Selector里,并监听Read事件;

完整代码如下:

代码语言:javascript
代码运行次数:0
复制
public class EchoServer implements Runnable{
  private Selector selector;
  private ServerSocketChannel servChannel;
  private static int port = 8080;

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {

    EchoServer echoServer = new EchoServer();
    new Thread(echoServer, "NIO-EchoServer-001").start();
    }

    public EchoServer() {
    try {
      selector = Selector.open();
      servChannel = ServerSocketChannel.open();
      servChannel.configureBlocking(false);
      servChannel.socket().bind(new InetSocketAddress(port), 1024);
      servChannel.register(selector, SelectionKey.OP_ACCEPT);
    } catch (IOException e) {
      e.printStackTrace();
      System.exit(1);
    }
  }

  /**
   * 主函数
   */
  @Override
  public void run () {

    try {
      while (true) {
        try {
          selector.select(1000);
          Set<SelectionKey> selectedKeys = selector.selectedKeys();
          Iterator<SelectionKey> it = selectedKeys.iterator();
          SelectionKey key = null;
          while (it.hasNext()) {
            key = it.next();
            it.remove();
            try {
              handleInput(key);
            } catch (Exception e) {
              if (key != null) {
                key.cancel();
                if (key.channel() != null) {
                  key.channel().close();
                }
              }
            }
          }
        } catch (Throwable t) {
          t.printStackTrace();
        }
      }
    }finally {
      //关闭多路复用器,自动关闭其上的Channel
      if (selector != null) {
        try {
          selector.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

  }

  /**
   * 处理请求
   * @param key
   * @throws IOException
   */
  private void handleInput(SelectionKey key) throws IOException {
    if (!key.isValid()) {
      return;
    }

    if (key.isAcceptable()) {
      ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
      SocketChannel sc = ssc.accept();
      sc.configureBlocking(false);
      //将新进来的连接加入到多路复用器,并监听读事件
      sc.register(selector, SelectionKey.OP_READ);
    }

    //读取消息,这个是前面Accept后的Socket
    if (key.isReadable()) {
      // Read the data
      SocketChannel sc = (SocketChannel) key.channel();
      //这里先写死1024字节,实际场景按需分配
      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");
        doWrite(sc, body);
      } else if (readBytes < 0) {
        key.cancel();
        sc.close();
      } else {
        // 读到0字节,忽略
      }
    }

  }

  /**
   * 写入数据
   * @param channel
   * @param response
   * @throws IOException
   */
  private void doWrite(SocketChannel channel, String response) throws IOException {
    if (response != null && response.trim().length() > 0) {
      byte[] bytes = response.getBytes();
      ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
      writeBuffer.put(bytes);
      writeBuffer.flip();
      channel.write(writeBuffer);
    }
  }
}

相对来说客户端就简单些,步骤如下:

1、打开SocketChannel;

2、设置SocketChannel为非阻塞模式;

3、调用SocketChannel的connect方法连接到服务器,连接成功后将其绑定到Selector上,并监听Read事件;

因流程和上面无太多区别,这里就不贴代码了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员升级之路 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档