文档章节

Java NIO用法详解

爱宝贝丶
 爱宝贝丶
发布于 05/13 09:57
字数 2692
阅读 597
收藏 25

       对于Java NIO,其主要由三个组件组成:Channel、Selector和Buffer。关于这三个组件的作用主要如下:

  • Channel是客户端连接的一个抽象,当每个客户端连接到服务器时,服务器都会为其生成一个Channel对象;
  • Selector则是Java NIO实现高性能的关键,其本质上使用了IO多路复用的原理,通过一个线程不断的监听多个Channel连接来实现多所有这些Channel事件进行处理,这样的优点在于只需要一个线程就可以处理大量的客户端连接,当有客户端事件到达时,再将其分发出去交由其它线程处理;
  • Buffer从字面上讲是一个缓存,本质上其是一个字节数组,通过Buffer,可以从Channel上读取数据,然后交由下层的处理器进行处理。这里的Buffer的优点在于其封装了一套非常简单的用于读取和写入数据Api。

       关于Channel和Selector的整体结构,可以通过下图进行的理解,这也是IO多路复用的原理图:

image.png

       可以看到,对于每个Channel对象,其只要注册到Selector上,那么Selector上监听的线程就会监听这个Channel的事件,当任何一个Channel有对应的事件到达时,Selector就会将该事件分发到下层的应用进行处理。

      本文首先会对Channel,Selector和Buffer的主要Api进行讲解,然后会结合一个服务器与客户端的例子来具体讲解它们三者的使用方式。

1. 核心Api

1.1 Channel

       对于Channel,其主要的api如下:

// 服务器端:
// 用于创建一个供服务器使用的ServerSocketChannel实例
ServerSocketChannel.open();
// 绑定一个服务器端口,从而提供对外的服务
ServerSocketChannel.bind();
// 获取一个客户端的Channel连接
ServerSocketChannel.accept();

// 客户端:
// 用于创建一个供客户端使用的SocketChannel实例
SocketChannel.open();
// 连接参数中指定地址和端口对应的服务器
SocketChannel.connect();

// ServerSocketChannel和SocketChannel两者兼备的方法
// 用于指定服务器处理请求的方式是阻塞的还是非阻塞的,对于Java NIO都是以非阻塞的方式进行处理的
Channel.configureBlocking();
// 将当前channel注册到一个Selector上,该方法会返回注册之后得到的SelectionKey对象。
// 这里在注册Channel的时候可以选择Selector将关注该Channel的哪些事件,可选的有如下几种:
// SelectionKey.OP_CONNECT:监听Channel建立连接事件
// SelectionKey.OP_READ:监听Channel的可读取事件,也即客户端已经发送数据过来,此时可以读取
// SelectionKey.OP_WRITE:监听Channel的可写事件,即当前可以写入数据到Channel中
Channel.register(Selector, int);

1.2 Selector

       对于Selector,其主要的api如下:

// 创建一个Selector实例
Selector.open();
// 监听所有注册的Channel,一直阻塞知道有任何一个客户端Channel有相应的事件到达,
// 需要注意的是,这里的select()方法返回的是当前接收到是事件数目,而不是具体的事件,
// 具体的事件要通过selectedKeys()方法获取
Selector.select();
// 获取当前所有有事件到达的客户端Channel对应的SelectionKey实例
Selector.selectedKeys();

1.3 SelectionKey

// 判断当前收到的Channel的事件是否为OP_CONNECT事件
SelectionKey.isConnectable();
// 判断当前收到的Channel的事件是否为OP_READ事件
SelectionKey.isReadable();
// 判断当前收到的Channel的事件是否为OP_WRITE事件
SelectionKey.isWritable();
// 返回当前SelectionKey中所封装的Channel对象
SelectionKey.channel();

       从上面的API中可以看出,这里关于Channel处理的大致流程是,首先由SocketChannel或者ServerSocketChannel调用open()方法创建一个Channel对象;然后调用Channel.register()方法将当前Channel注册到Selector中;接着通过Selector.select()方法监听所有注册的Channel的连接,如果有任何一个有事件到达,此时这些事件会封装到当前客户端Channel对应的SelectionKey中,最后通过SelectionKey判断具体是什么类型的事件,然后对这些事件进行处理。

2. 用法示例

2.1 服务器

       服务器端使用的是ServerSocketChannel,这里主要是通过监听客户端Channel,获取数据进行打印,然后返回一段数据给客户端。如下是具体的示例:

public class Server {

  public static void main(String[] args) throws IOException {
    new Server().start();
  }

  private void start() throws IOException {
    // 创建一个服务器ServerSocketChannel对象
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    // 将当前服务器绑定到8080端口
    serverSocketChannel.bind(new InetSocketAddress(8080));
    // 设置当前channel为非阻塞的模式
    serverSocketChannel.configureBlocking(false);

    // 创建一个Selector对象
    Selector selector = Selector.open();
    // 将服务器ServerSocketChannel注册到Selector上,并且监听与客户端建立连接的事件
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    while (true) {
      // 监听ServerSocketChannel上的事件,每秒钟循环一次,
      // 这里select()方法返回的是当前监听得到的事件数目,为0表示当前没有任何事件到达
      if (selector.select(1000) == 0) {
        System.out.println("has no message...");
        continue;
      }

      // 走到这里说明当前有监听的事件到达,获取所有监听的Channel所对应的SelectionKey对象,
      // 这里需要注意的是,前面我们已经将ServerSocketChannel注册到Selector中了,
      // 因而对于ServerSocketChannel,其监听得到的则是SelectionKey.OP_CONNECT事件。
      // 但是下面的代码中,我们也会将与客户端建立的连接Channel注册到Selector中,
      // 因而这里Selector中也会存在接收到的SelectionKey.OP_READ和OP_WRITE事件。
      Set<SelectionKey> selectionKeys = selector.selectedKeys();
      // 对监听到的事件进行遍历
      Iterator<SelectionKey> iterator = selectionKeys.iterator();
      while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        // 这里需要注意的是,Selector在为每个有事件到达的Channel建立SelectionKey对象
        // 之后,其并不会将其移除,如果我们不进行移除,那么下次循环时该事件还会再被处理一次,
        // 因而这里要调用remove()方法移除该SelectionKey
        iterator.remove();

        // 如果是有新的客户端Channel连接建立,则处理该事件
        if (key.isAcceptable()) {
          accept(key, selector);
        }

        // 如果客户端连接中有可读取的数据,则处理该事件
        if (key.isReadable()) {
          read(key);
        }

        // 如果可往客户端连接中写入数据,则处理该事件
        if (key.isValid() && key.isWritable()) {
          write(key);
        }
      }
    }
  }

  private void accept(SelectionKey key, Selector selector) throws IOException {
    // 这里由于只有ServerSocketChannel才会有客户端连接建立事件,因而这里可以直接将
    // Channel强转为ServerSocketChannel对象
    ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
    // 获取客户端的连接
    SocketChannel socketChannel = serverChannel.accept();
    socketChannel.configureBlocking(false);
    // 将客户端连接Channel注册到Selector中,并且监听该Channel的OP_READ事件,
    // 也即等待客户端发送数据到服务器端
    socketChannel.register(selector, SelectionKey.OP_READ);
  }

  private void read(SelectionKey key) throws IOException {
    // 这里只有客户端才会发送数据到服务器,因而可将其强转为SocketChannel对象
    SocketChannel clientChannel = (SocketChannel) key.channel();
    ByteBuffer buffer = ByteBuffer.wrap(new byte[1024]);
    // 从客户端Channel中读取数据,这里read()方法返回读取到的数据长度,
    // 如果为-1,则表示客户端断开连接了
    int len = clientChannel.read(buffer);
    if (len == -1) {
      clientChannel.close();
      return;
    }

    // 处理客户端数据
    System.out.println("**********server: read message**********");
    System.out.println(new String(buffer.array(), 0, len));
    // 由于已经读取了客户端数据,因而这里将对该Channel感兴趣的事件修改为
    // SelectionKey.OP_READ	和OP_WRITE,用于服务器往该Channel中写入数据
    key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
  }

  private void write(SelectionKey key) throws IOException {
    String message = "message from server";
    ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
    // 由于上面为客户端Channel设置了可供写入数据的事件,因而这里可以往客户端Channel写入数据
    SocketChannel clientChannel = (SocketChannel) key.channel();

    if (clientChannel.isOpen()) {
      System.out.println("**********server: write message**********");
      System.out.println(message);
      // 往客户端Channel写入数据
      clientChannel.write(buffer);
    }

    // 写入完成后,监听客户端会继续发送的数据
    if (!buffer.hasRemaining()) {
      key.interestOps(SelectionKey.OP_READ);
    }

    buffer.compact();
  }
}

2.2 客户端

       客户端使用SocketChannel连接服务器,并且会往服务器中写入数据,然后等待服务器返回数据并且打印出来。如下是客户端代码:

public class Client {

  public static void main(String[] args) throws IOException {
    new Client().start();
  }

  private void start() throws IOException {
    // 创建一个客户端SocketChannel对象
    SocketChannel channel = SocketChannel.open();
    // 设置客户端Channel为非阻塞模式
    channel.configureBlocking(false);
    
    // 创建一个供给客户端使用的Selector对象
    Selector selector = Selector.open();
    // 注册客户端Channel到Selector中,这里客户端Channel首先监听的是OP_CONNECT事件,
    // 因为其首先必须与服务器建立连接,然后才能发送和读取数据
    channel.register(selector, SelectionKey.OP_CONNECT);
    // 调用客户端Channel.connect()方法连接服务器,需要注意的是,该方法的调用必须放在
    // 上述Channel.register()方法之后,否则在注册之前客户端就已经注册完成,
    // 此时Selector就无法收到SelectionKey.OP_CONNECT事件了
    channel.connect(new InetSocketAddress("127.0.0.1", 8080));
    while (true) {
      // 监听客户端Channel的事件,这里会一直等待,直到有监听的事件到达。
      // 对于客户端,首先监听到的应该是SelectionKey.OP_CONNECT事件,
      // 然后在后续代码中才会将SelectionKey.OP_READ和WRITE事件注册
      // 到Selector中
      selector.select();
      Set<SelectionKey> selectionKeys = selector.selectedKeys();
      Iterator<SelectionKey> iterator = selectionKeys.iterator();
      while (iterator.hasNext()) {
        SelectionKey key = iterator.next();
        iterator.remove();
          
        // 监听到客户端Channel的SelectionKey.OP_CONNECT事件,并且处理该事件
        if (key.isConnectable()) {
          connect(key, selector);
        }

        // 监听到客户端Channel的SelectionKey.OP_WRITE事件,并且处理该事件
        if (key.isWritable()) {
          write(key, selector);
        }

        // 监听到客户端Channel的SelectionKey.OP_READ事件,并且处理该事件
        if (key.isReadable()) {
          read(key);
        }
      }
    }
  }
  
  private void connect(SelectionKey key, Selector selector) throws IOException {
    // 由于是客户端Channel,因而可以直接强转为SocketChannel对象
    SocketChannel channel = (SocketChannel) key.channel();
    channel.finishConnect();
    // 连接建立完成后就监听该Channel的WRITE事件,以供客户端写入数据发送到服务器
    channel.register(selector, SelectionKey.OP_WRITE);
  }
    
  private void write(SelectionKey key, Selector selector) throws IOException {
    SocketChannel channel = (SocketChannel) key.channel();
    String message = "message from client";
    System.out.println("**********client: write message**********");
    System.out.println(message);
    // 客户端写入数据到服务器Channel中
    channel.write(ByteBuffer.wrap(message.getBytes()));
    // 数据写入完成后,客户端Channel监听OP_READ事件,以等待服务器发送数据过来
    channel.register(selector, SelectionKey.OP_READ);
  }

  private void read(SelectionKey key) throws IOException {
    System.out.println("**********client: read message**********");
    SocketChannel channel = (SocketChannel) key.channel();
    ByteBuffer buffer = ByteBuffer.wrap(new byte[1024]);
    // 接收到客户端Channel的SelectionKey.OP_READ事件,说明服务器发送数据过来了,
    // 此时可以从Channel中读取数据,并且进行相应的处理
    int len = channel.read(buffer);
    if (len == -1) {
      channel.close();
      return;
    }

    System.out.println(new String(buffer.array(), 0, len));
  }
}

2.3 运行结果

服务器:

has no message...
has no message...
has no message...
has no message...
**********server: read message**********
message from client
**********server: write message**********
message from server

客户端:

**********client: write message**********
message from client
**********client: read message**********
message from server

3. 小结

       本文首先讲解了Java NIO中三大组件的作用,然后讲解了各个组件主要的方法及其注意事项,最后通过一个客户端和服务器实例详细讲解了Java NIO是如何使用的

© 著作权归作者所有

爱宝贝丶

爱宝贝丶

粉丝 324
博文 130
码字总数 430897
作品 0
武汉
程序员
私信 提问
少啰嗦!一分钟带你读懂Java的NIO和经典IO的区别

本文引用了“架构师社区”公众号的《史上讲的最好的Java NIO与IO的区别与应用》一文部分内容,感谢原作者的技术分享。 1、引言 很多初涉网络编程的程序员,在研究Java NIO(即异步IO)和经典...

JackJiang2011
06/25
0
0
Java NIO系列教程(十六) Java NIO Files

原文地址 译者:章筱虎 java NIO Files类(java.nio.file.Files) 提供了操作文件的相关方法。本篇文章将会覆盖大多数常用的方法。Files类包含了很多方法,如果你需要的功能在文中没有提及,需...

章 筱虎
2018/01/23
0
0
晒晒你看过的哪些Java相关的书。

@红薯 要我们晒Jar包 , 我倒想看看大家都看过什么Java编程相关的书籍,我先来:如下 1、iText in Action 主要内容: a、自动化静态和动态XFA表单 b、如何从XML生成动态PDF或数据库 c、如何添...

刘学炜
2012/09/27
342
4
Jenkins 教程(一)实现自动化打包及邮件通知

个人不喜欢装腔作势一堆专业术语放上去,让大多数人看不懂来提升逼格(所谓的专家),所以我简单的介绍jenkins是干啥的。本文使用jenkins,就是让它把git仓库里的东西取出来,然后在jenkins容器...

FantJ
2018/05/26
0
0
Java程序员从笨鸟到菜鸟全部博客目录【2012年十一月七日更新】

本文来自:曹胜欢博客专栏。转载请注明出处:http://blog.csdn.net/csh624366188 大学上了一年半,接触java也一年半了,虽然中间也有其他东西的学习,但是还是以java为主路线,想想这一年半,...

长平狐
2012/11/12
209
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周日乱弹 —— 别问,问就是没空

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @tom_tdhzz :#今日歌曲推荐# 分享容祖儿/彭羚的单曲《心淡》: 《心淡》- 容祖儿/彭羚 手机党少年们想听歌,请使劲儿戳(这里) @wqp0010 :周...

小小编辑
今天
167
4
golang微服务框架go-micro 入门笔记2.1 micro工具之micro api

micro api micro 功能非常强大,本文将详细阐述micro api 命令行的功能 重要的事情说3次 本文全部代码https://idea.techidea8.com/open/idea.shtml?id=6 本文全部代码https://idea.techidea8....

非正式解决方案
今天
5
0
Spring Context 你真的懂了吗

今天介绍一下大家常见的一个单词 context 应该怎么去理解,正确的理解它有助于我们学习 spring 以及计算机系统中的其他知识。 1. context 是什么 我们经常在编程中见到 context 这个单词,当...

Java知其所以然
昨天
5
0
Spring Boot + Mybatis-Plus 集成与使用(二)

前言: 本章节介绍MyBatis-Puls的CRUD使用。在开始之前,先简单讲解下上章节关于Spring Boot是如何自动配置MyBatis-Plus。 一、自动配置 当Spring Boot应用从主方法main()启动后,首先加载S...

伴学编程
昨天
8
0
用最通俗的方法讲spring [一] ──── AOP

@[TOC](用最通俗的方法讲spring [一] ──── AOP) 写这个系列的目的(可以跳过不看) 自己写这个系列的目的,是因为自己是个比较笨的人,我曾一度怀疑自己的智商不适合干编程这个行业.因为在我...

小贼贼子
昨天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部