NIO 理论 与 编程

NIO 简 介

  • NIO 官方叫法叫 New I/O,原因在于它相比于之前的 I/O 类库是新增的。而由于老的 I/O 类库是阻塞 I/O,New I/O 类库的目标就是让 Java 支持非阻塞 I/O ,所以更多的人喜欢称之为非阻塞 I/O(Non-block I/O)。
  • 与 BIO 中 Socket 类和 ServerSocket 类相对应,NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。一般来说,低负载,低并发的应用程序可以选择同步阻塞 I/O以降低编程难度;对于高负载、高并发的网络应用,需要使用 NIO 的非阻塞模式进行开发。
  • 可以参考《传统 BIO 编程》、《TCP 理论详解》进行对比
  • NIO 库是在 Java JDK 1.4 中引入的,弥补了原来同步阻塞 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据,NIO 不用使用本机代码就可以利用低级优化,这是 BIO 所无法做到的。
  • 下面对 NIO 的主要概念进行简单介绍。

Buffer 缓冲区

  • 缓冲区(Buffer) 是一个对象,它包含一些要写入或者读出的数据。在 NIO 类库中加入 Buffer 对象,体现了新库与原 I/O 的重要区别。
  • 在原来的面向流的 I/O 中,可以将数据直接写入或者将数据直接读到 Stream 对象中,在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它直接读到缓冲区中,在写入数据时,写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。
  • 缓冲区实质是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其它种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit) 等信息。
  • 最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了 ByteBuffer,还有其它一些缓冲区,事实上每一种 Java 基本类型(Boolean 除外)都对应有一种缓冲区,具体如下:

ByteBuffer :字节缓冲区

CharBuffer :字符缓冲区

ShortBuffer :  短整形缓冲区

IntBuffer : 整形缓冲区

LongBuffer : 长整型缓冲区

FloutBuffer : 浮点型缓冲区

DoubleBuffer :双精度浮点型缓冲区

  • 每一个 XxxBuffer 类 都是 Buffer 接口的一个子实例,除了 ByteBuffer ,每一个 XxxBuffer 类都有完全一样的操作,只是它们处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它在具有一般缓冲区的操作之外还提供了一些特有的操作,以方便网络读写。

Channel 通道

  • Channel 是一个通道,它就像自来水管一样,网络数据通过 Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是 InputSrteam 或者 OutputStream 的子类),而通道可以用于读、写或者二者同时进行。
  • 因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API,特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

  • 自上向下,前三层主要是 Channel 接口,用于定义它的功能,后面是一些具体的功能类(抽象类)。
  • Channel 可以分为两大类:用于网络读写的 Selectable 和 用于文件操作的 FileChannel。

Selector 多路复用器

  • Selector 多路复用器是 Java NIO 编程的基础,提供了选择已经就绪的任务的能力,简单的讲,Selector 会不断的轮询注册在其上的 Channel,如果某个 Channel 上面发生读 或者 写事件,则表示这个 Channel 处于就绪状态,从而会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。
  • 一个多路复用器 Selector 可以同时轮询多个 Channel ,由于 JDK 使用了 epoll() 代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制,也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。

NIO 编 程

  • NIO 虽然优点繁多,但是在编程上确实比以前 BIO 要稍微复杂一些,这里以一个实际的例子进行说明 NIO 编程步骤。

NIO 服务端编码步骤

  • 在服务端源码正式编码之前,先了解 NIO 服务端编码的步骤。

  • 步骤一:打开 ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道,示例代码:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

  • 步骤二:绑定监听端口,设置连接为非阻塞模式,示例代码:

ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8080));
serverSocketChannel.configureBlocking(false);

  • 步骤三:创建 Reactor(反应器) 线程,创建多路复用器并启动线程,示例代码:

Selector selector = Selector.open();
new Thread(new ServerSelector()).start();

  • 步骤四:将 ServerSocketChannel 注册到 Reactor(反应器) 线程 的多路复用器 Selector 上,监听 ACCEPT 事件,示例代码:

 SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

  • 步骤五:多路复用器在线程的 run 方法的无限循环体内轮询准备就绪的 Key,示例代码:

            int num = selector.select();
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey1 = iterator.next();
                //...处理 I/O 事件...
            }

  • 步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成 TCP 三次握手,建立物理链路,示例代码:

SocketChannel socketChannel = serverSocketChannel.accept();

  • 步骤七:设置客户端链路为非阻塞模式,示例代码:

socketChannel.configureBlocking(false);
socketChannel.socket().setReuseAddress(true);

..............

  • 步骤八:将新接入的客户端连接注册到 Reactor(反应器)线程的多路复用器上,监听读操作,读取客户端发送的网络消息,示例代码:

SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);

  • 异步读取客户端请求消息到缓冲区,示例代码:

int readNumber = socketChannel.read(byteBuffer);

  • 步骤十:对 ByteBuffer 进行编解码,如果有半包消息指针 rest,继续读取后续的报文,将解码成功的消息封装成 Task ,投递到业务线程池中,进行业务逻辑处理。
  • 步骤十一:将 POJO 对象 encode 成 ByteBuffer ,调用 SocketChannel 的异步 write 方法,将消息异步发送给客户端,示例代码:

socketChannel.write(byteBuffer);

 注意:如果发送区 TCP 缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入 TCP 缓冲区。

NIO 服务端源码分析

  • 需求:客户端往服务器发送消息,服务器收到消息后,进行回复,最后客户端退出,服务器继续监听。
  • 对于关键 API 都采用注释的形式直接写在了源码中。
package com.lct.nio;

/**
 * Created by Administrator on 2018/10/19 0019.
 * 时间服务器端
 */
public class TimerServer {
    public static void main(String[] args) {
        /**
         * ServerSelector 为 多路复用器类,一个单独的线程,负责轮询多路复用器 Selector
         * 可以处理多个客户端并发接入
         */
        ServerSelector serverSelector = new ServerSelector();
        new Thread(serverSelector).start();
    }
}
package com.lct.nio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * Created by Administrator on 2018/10/19 0019.
 * Timer 服务器多路复用器线程
 */
public class ServerSelector implements Runnable {

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private volatile boolean stop = false;

    /**
     * 初始化多路复用器
     */
    public ServerSelector() {
        try {
            /**
             * 创建多路复用器 selector
             * 创建 NIO 服务端管道
             * configureBlocking(false):表示设置 serverSocketChannel 为异步非阻塞模式
             */
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            /**socket 返回的是 java.net.ServerSocket 对象
             * bind(SocketAddress endpoint),默认 backlog 为 50
             * bind(SocketAddress endpoint, int backlog)
             * backlog:请求传入连接队列的最大长度。
             */
            SocketAddress socketAddress = new InetSocketAddress("192.168.1.20", 8080);
            serverSocketChannel.socket().bind(socketAddress, 1000);
            /**
             * 将 ServerSocketChannel 注册到多路复用器 selector 上
             * 监听 SelectionKey.OP_ACCEPT 操作位(事件)
             * SelectionKey.OP_ACCEPT :接收连接就绪事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
             */
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println(Thread.currentThread().getName() + ":服务器准备就绪..........");
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

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

    @Override
    public void run() {
        while (!stop) {
            try {
                /**
                 * 循环遍历多路复用器 selector
                 * select(long timeout):设置休眠时间为 1 秒,即无论是否有读写等事件发生,selector 每隔 1s 被唤醒一次,然后向后执行
                 * select():重载方法,会一直阻塞,知道多路复用器检测到有通道事件
                 */
                System.out.println(Thread.currentThread().getName() + ":进入 run 方法..........");
                selector.select();
                /**
                 * select():重载方法,当有就绪状态的 Channel 时,selector 将返回该 Channel 的 SelectorKey 集合
                 * 通过对就绪状态的 Channel 集合进行迭代,可以进行网络的异步读写操作。
                 */
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    handleInput(key);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        /**
         * 多路复用器关闭后,所有注册在上面的 Channel 和 Pipe 等资源都会被自动去注册关闭,所以不需要重复释放资源
         */
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 处理新的客户端接入
     *
     * @param key
     */
    public void handleInput(SelectionKey key) {
        try {
            if (key.isValid()) {
                /**处理客户端的连接请求消息
                 * 根据 SelectionKey 的操作位进行判断即可获知网络事件的类型
                 * */
                if (key.isAcceptable()) {
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                    /**
                     * accept 方法接收客户端的连接请求并创建 SocketChannel 实例
                     * 此时Tcp 3次握手完成,连接正式完成
                     */
                    SocketChannel sc = ssc.accept();
                    /**
                     * 同理设置 SocketChannel 为异步非阻塞模式
                     * 并注册到多路复用器上,以后此 SocketChannel 如果有读写操作,则 selector 就会将它提取出来
                     *  SelectionKey.OP_READ:读就绪事件,表示通道中如果有可读的数据,可以执行读操作了
                     */
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                }

                /**处理客户端的发送的数据消息,即 SelectionKey 为读就绪事件时,则开始读取数据
                 * 根据 SelectionKey 的操作位进行判断即可获知网络事件的类型
                 * 客户端关闭连接时,服务器也会触发 读事件,通过 read 方法返回 -1 进行判断,当 read 返回 -1 时,服务器也要关闭此连接管道
                 * */
                if (key.isReadable()) {
                    /**
                     * 同样从 SelectionKey 获取 管道 SocketChannel
                     */
                    SocketChannel sc = (SocketChannel) key.channel();

                    /**
                     * allocate(int capacity):capacity 表示容量,即字节缓冲数组的大小,设置大小为 1024 KB,即 1 M
                     * SocketChannel 的 read(ByteBuffer dst) 方法读取请求码流,将客户端发送来的数据读取到字节缓存数组中
                     * 因为客户端连接的时候,已经将 SocketChannel 设置为异步非阻塞模式,因此 SocketChannel 的 read 方法是非阻塞的
                     */
                    ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                    /**
                     * read方法 的返回值表示读取的字节数,有如下三种情况:
                     * 1)返回值大于0:读到了字节,对自己进行编解码
                     * 2)返回值等于0:没有读取到字节,属于正常情况,忽略
                     * 3)返回值为 -1:表示链路已经关闭,需要关闭 SocketChannel,释放资源
                     */
                    int readBytes = 0;
                    /**
                     * 为了防止 客户端非正常关闭,read 必须捕获异常,然后服务器也要关闭连接通道
                     * 否则如果客户端强迫关闭后,服务器会一直触发 读事件,进入死循环,所以必须服务器也进行关闭
                     */
                    try {
                        readBytes = sc.read(readBuffer);
                    } catch (Exception e) {
                        e.printStackTrace();
                        key.cancel();
                        sc.close();
                    }
                    if (readBytes > 0) {
                        /**
                         * 当读取到码流后,先进行解码,首先对 ByteBuffer 进行 flip(翻转)操作,limit = position;position = 0;mark = -1;
                         * 反转此缓冲区。首先将限制(limit)设置为当前位置(position),然后将位置(position)设置为 0。如果已定义了标记,则丢弃该标记。
                         * 用于后续对缓冲区的读取操作
                         */
                        readBuffer.flip();
                        /**
                         * 根据缓冲区可读的字节个数创建字节数组
                         * remaining():返回当前位置(position)与限制(limit)之间的元素数。
                         */
                        byte[] bytes = new byte[readBuffer.remaining()];
                        /**
                         * ByteBuffer 的 get(byte[] dst) 方法将缓冲区可读的字节数组复制到新创建的字节数组中
                         * 接着 new String 将字节数组转为字符串,同时指定编码
                         */
                        readBuffer.get(bytes);
                        String body = new String(bytes, "UTF8");
                        System.out.println(Thread.currentThread().getName() + ":收到客户端消息:" + body);
                        doWrite(sc, "收到你的消息 \"" + body + "\"");
                    } else if (readBytes < 0) {
                        /**如果读取的数据为 -1,则表示客户端已经关闭连接,此时服务器也要关闭此连接管道
                         * */
                        key.cancel();
                        sc.close();
                    } else {
                        /**读到0字节,忽略*/
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();

        }
    }

    /**
     * 写数据 —————— 发送消息
     *
     * @param channel
     * @param response
     */
    public void doWrite(SocketChannel channel, String response) {
        if (response != null && response.trim().length() > 0) {
            try {
                /**
                 * 先将待发送的数组转为字节数组,同时指定编码
                 * 根据字节数组大小构建 ByteBuffer,调用 put(byte[] src) 方法将字节数组复制到缓冲区中
                 * 然后对缓冲区进行 flip(翻转)操作,最后调用 SocketChannel 的 write(ByteBuffer src) 方法将缓冲区中的字节数组发送出去
                 */
                byte[] bytes = response.getBytes("UTF-8");
                ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
                writeBuffer.put(bytes);
                writeBuffer.flip();
                channel.write(writeBuffer);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 注意事项:因为 SocketChannel 是异步非阻塞的,它并不保证一次能把需要发送的字节数组发送完,此时会出现“写半包”问题,需要注册写操作,不断轮询 Selector 将没有发送完的 ByteBuffer 发送完毕,然后通过 ByteBuffer 的 hasRemain() 方法判断消息是否发送完成。此例为入门演示教程,暂时未处理“写半包”场景。

NIO 客户端编码步骤

  • 步骤一:打开SocketChannel ,绑定客户端本地地址(可选,系统默认会随机分配可用的本地地址),实例代码:

SocketChannel socketChannel = SocketChannel.open();

  • 步骤二:设置 SocketChannel 为非阻塞模式,同时设置客户端连接的 TCP 参数,示例代码:

socketChannel.configureBlocking(false);
Socket socket = socketChannel.socket();
socket.setReuseAddress(true);
socket.setReceiveBufferSize(1024);
socket.setSendBufferSize(1024);    

  • 步骤三:异步连接客户端,示例代码:

boolean connected = socketChannel.connect(new InetSocketAddress("192.168.1.20", 8080));

  • 步骤四:判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中,如果当前没有连接成功(异步连接,返回 false,说明客户端已经发送 sync 包,服务端没有返回 ack 包,物理链路没有成立),实例代码:

if (connected) {
    socketChannel.register(selector, SelectionKey.OP_READ);
} else {
     socketChannel.register(selector,SelectionKey.OP_CONNECT);
}

  • 步骤五:如果没有当时没有连接成功,则向 Reactor(反应器)线程的多路复用器注册 OP_CONNECT 状态位,监听服务器 TCP 的 ack 应答,实例代码:

 socketChannel.register(selector,SelectionKey.OP_CONNECT);

  • 步骤六:创建 Reactor 线程,创建多路复用器并启动线程,实例代码:

Selector selector = Selector.open();

new Thread(new ReactorTask()).start();

  • 步骤七:多路复用器在 run 方法中无限循环体内轮询准备就绪的 Key,实例代码:

int num = selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
 while (iterator.hasNext()) {
       SelectionKey selectionKey1 = iterator.next();
        //...处理 I/O 事件...
}

  • 步骤八:接收 connect 事件进行处理,实例代码:

if(key.isConnectable()){

.......

}

  • 步骤九:如果连接成功,则注册读事件到多路复用器,实例代码:

if(channel.finishConnect()){

registerRead();

}

  • 步骤十:注册读事件到多路复用器,实例代码:

socketChannel.register(selector, SelectionKey.OP_READ);

  • 步骤十一:异步读取服务端请求消息到缓冲区,实例代码:

int readNumber = channel.read(receivedBuffer);

  • 步骤十二:对 ByteBuffer 进行编解码,如果有半包消息接收缓冲区 Reset,继续读取后续的报文,将解码成功的消息封装成 Task ,投递到业务线程池中,进行业务逻辑处理。
  • 步骤十三:将待发送的消息 encode 成 ButeBuffer ,调用 SocketChannel 的异步 write 方法将消息异步发送给服务器,实例代码:

socketChannnel.write(byteBuffer);

NIO 客户端源码分析

  • 关键 API 已经以注释的形式写在了源码中。
package com.lct.nio;

/**
 * Created by Administrator on 2018/10/20 0020.
 * 时间客户端
 */
public class TimerClient {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            TimerHandle timerHandle = new TimerHandle();
            new Thread(timerHandle).start();
        }
    }
}
package com.lct.nio;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * Created by Administrator on 2018/10/20 0020.
 * 客户端处理器
 */
public class TimerHandle implements Runnable {
    private String host;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;
    private volatile boolean stop = false;

    /**
     * 初始化多路复用器以及 SocketChannel对象
     * 设置 SocketChannel 为异步非阻塞模式,同理也可以设置客户端连接的 TCP 参数
     */
    public TimerHandle() {
        host = "192.168.1.20";
        port = 8080;
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        doConnect();
        /** 多路复用器轮询准备就绪的 Channel*/
        while (!stop) {
            try {
                /**
                 * select():这是一个阻塞的方法,当有准备就绪的 Channel 时才会继续向后运行
                 * select(long timeout):重载的这个可以设置超时时间,单位毫秒,如 select(1000),表示每隔1秒,无论有没有
                 * 准备就绪的 Channel ,都会向后运行
                 */
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> it = selectionKeys.iterator();
                SelectionKey key = null;
                while (it.hasNext()) {
                    key = it.next();
                    it.remove();
                    handleInput(key);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        /**
         * 多路复用器关闭后,所以注册在上面的 Channel 和 Pipe 等资源都会自动去注册并关闭,所以不需要重复释放资源
         * 建议先关闭多路复用器上的 连接管道,最后再关闭 多路复用器
         * 因为当直接关闭 selector 时,会导致其上面的连接管道强迫关闭,从而在服务器触发读事件时,read 的时候抛出异常,说客户端强迫关闭了一个连接
         * 此时只需要先正常关闭连接管道 SocketChannel 即可解决,此时服务器同样会触发 读事件,但是 read 的时候返回 -1
         */
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 处理多路复用器检测到的每一个 SelectionKey
     *
     * @param key
     */
    public void handleInput(SelectionKey key) {
        /** 保证 SelectionKey 必须有效*/
        if (key.isValid()) {
            try {
                SocketChannel sc = (SocketChannel) key.channel();
                /**
                 * 连接就绪事件,表示客户与服务器的连接已经建立就绪
                 */
                if (key.isConnectable()) {
                    /**完成套接字通道的连接过程
                     *当且仅当已连接此通道的套接字时才返回 true
                     * */
                    if (sc.finishConnect()) {
                        sc.register(selector, SelectionKey.OP_READ);
                        doWrite(sc);
                    } else {
                        System.out.println("服务器连接失败...............");
                    }
                }

                /**
                 * 读就绪事件,表示通道中已经有了可读的数据
                 */
                if (key.isReadable()) {
                    /**创建接收消息的字节缓冲数组,大小为 1024 KB
                     * SocketChannel 的 read 方法进行异步读取操作
                     * read 方法返回读取的字节数,如果返回-1,则表示对方已经关闭了连接管道*/
                    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("收到服务器回复的数据:" + body);

                        /**
                         * 业务结束后,客户端主动关闭连接管道 SocketChannel ,然后设置 stop 标识 为 true,
                         * 接着下一次循环时就会退出,最后关闭多路复用器
                         */
                        socketChannel.close();
                        this.stop = true;
                    } else if (readBytes < 0) {
                        /**关闭链路
                         * 当服务器主动关闭连接通道时,客户端触发读事件,read 读取时返回 -1
                         * 此时客户端也应该关闭连接通道
                         * */
                        key.channel();
                        sc.close();
                    } else {
                        //读到 0 字节,忽略
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 连接服务器
     */
    public void doConnect() {
        try {
            /**
             * connect(SocketAddress remote):连接此通道的套接字
             * 如果此通道处于非阻塞模式,则调用此方法会发起一个非阻塞连接操作。
             * 如果立即建立连接(使用本地连接时就是如此),则此方法返回 true。否则此方法返回 false,并且必须在以后通过调用 finishConnect 方法来完成该连接操作。
             */
            if (socketChannel.connect(new InetSocketAddress(host, port))) {
                socketChannel.register(selector, SelectionKey.OP_READ);
                doWrite(socketChannel);
            } else {
                /**
                 * 如果 connect 直接连接成功,则注册 读 事件;如果没有直接连接成功,则说明服务器没有返回 TCP 握手应答消息,但这并不代表连接失败。
                 * 此时注册 连接就绪事件,当服务器返回 TCP syn-ack 消息后,Selector 就能轮询到这个 SocketChannel 处于就绪状态。
                 */
                socketChannel.register(selector, SelectionKey.OP_CONNECT);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 往服务器写数据
     *
     * @param socketChannel
     */
    public void doWrite(SocketChannel sc) {
        try {
            /**将待发送的消息转为字节数组,同时指定编码
             * 接着创建字节缓冲数组
             * put 方法将字节数组复制到缓冲数组中
             * flip 翻转缓冲数组
             * SocketChannel write 进行异步发送数据*/
            byte[] req = ("Hello ,My Name is Coco " + Thread.currentThread().getName()).getBytes("UTF-8");
            ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
            writeBuffer.put(req);
            writeBuffer.flip();
            sc.write(writeBuffer);

            /** 如果缓冲区的消息全部发送完成,则打印消息*/
            if (!writeBuffer.hasRemaining()) {
                System.out.println("客户端往服务器发送消息成功..........");
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

结果分析

  • 先启动服务器,再启动客户端,输出如下:

服务器控制台输出如下:

main:服务器准备就绪..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-0
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-1
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-2
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-3
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-4
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........

客户端控制台输出如下:

客户端往服务器发送消息成功..........
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-0"
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-1"
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-2"
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-3"
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-4"

  • 通过源码对比分析,NIO 编程难度确实比《传统 BIO 编程》大很多,上面的例子作为入门例子,并没有考虑“半包读”、“半包写”,后面的文章会逐步深入。
  • NIO 编程有点总结:

1)客户端发起的连接操作是异步的,可以通过在多路复用器注册 OP_CONNECT 等待后续结果,不再像 BIO 客户端一样被同步阻塞。

2)BIO 的读写操作都是阻塞的,NIO 的 SocketChannel 的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样 I/O 通信线程就可以处理其它连接,不需要同步等待这个连接可用。

3)线程模型的优化,由于JDK 的 Selector 在 Linux 等主流操作系统上通过 epoll 实现,它没有连接句柄数的限制(只受限制于操作系统的最大句柄数或者单个进程的句柄限制),这意味着一个 Selector 线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端增加而线性下降。因此非常适合做高性能、高负载的网络服务器。

4)Jdk 1.7 升级了 NIO 类库,升级后的 NIO 类库被称为 NIO 2.0,。Java 正式提供了异步文件 I/O 操作,同时提供了与 UNIX 网络编程事件驱动 I/O 对应的 AIO。

猜你喜欢

转载自blog.csdn.net/wangmx1993328/article/details/83182210