NIO快速入门

简介

在当今这个数据大爆炸时代,I/O 问题尤其突出,很容易成为一个性能瓶颈。正因如此,所以 Java 在 I/O 上也一直在做持续的优化,如从 1.4 开始引入了 NIO,提升了 I/O 的性能。

在了解NIO之前,关于IO的五种模型可以参考这篇文章:https://blog.csdn.net/qq_42191317/article/details/95937475

Java在JDK1.4引入的NIO就属于其中的多路复用IO。

NIO包括三个核心组件Selector、Channel和Buffer,现在分别介绍如下:

通道Channel

通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。

通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。

通道包括以下类型:

  • FileChannel:从文件中读写数据;
  • DatagramChannel:通过 UDP 读写网络中数据;
  • SocketChannel:通过 TCP 读写网络中数据;
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。

通道的使用如下:

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

/**
 * Channer 常用API
 */
public class Channer {


    public static void main(String[] args) throws IOException {

        //----------------   服务端    ----------------------

        //服务端通过服务端Socket创建Channel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //给服务端Channel绑定8088端口
        serverSocketChannel.bind(new InetSocketAddress(8088));

        //服务端监听客户端连接,建立SocketChannel连接
        SocketChannel socketChannel = serverSocketChannel.accept();

        //----------------   客户端    ----------------------------
        //客户端通道连接远程主机端口和IP
        SocketChannel socketChannel1 = SocketChannel.open(new InetSocketAddress("127.0.0.1",8088));

    }


}

缓冲区Buffer

发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。

缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

缓冲区包括以下类型:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

缓冲区的状态变量:

  • capacity:最大容量;
  • position:当前已经读写的字节数;
  • limit:还可以读写的字节数。
  • mark:标记

状态变量的改变过程举例:

① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。

 

② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。

 

③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

 

④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。

 

⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。

选择器Selector

NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。

NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。

通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。

应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。

Selector使用如下:

package nio;

import java.io.IOException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.util.Set;

/**
 * Selector的API使用
 */

public class SelectorDemo {


    public static void main(String[] args) throws IOException {


        //创建一个Selector
        Selector selector = Selector.open();

        //创建一个Channel用于演示注册Selector
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //将Channel注册到Selector上 注册可读事件   SocketChannel注册Selector同理
        serverSocketChannel.register(selector, SelectionKey.OP_READ);

        //Selector阻塞等待事件发生
        selector.select();

        //获得发生事件的Channel集合
        Set<SelectionKey> selectionKeys = selector.selectedKeys();

    }



}

在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

它们在 SelectionKey 中定义如下:

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

NIO编程步骤总结

  1. 创建Selector
  2. 创建ServerSocketChannel,并绑定监听端口
  3. 将Channel设置为非阻塞模式
  4. 将Channel注册到Selector上,监听连接事件
  5. 循环调用Selector的select方法,检测就绪情况
  6. 调用Selector的SelectedKeys方法获取就绪Channel集合
  7. 判断就绪事件种类,调用业务处理方法
  8. 根据业务需要决定是否再次注册监听事件,重复执行第三步操作

NIO编程实战 ---  实现多人聊天室系统

服务端实现

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * 多人聊天系统服务端
 */
public class NioServer {


    /**
     * 服务端启动
     */
    public void start() throws IOException {

        //创建Selector
        Selector selector = Selector.open();

        //创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //绑定端口
        serverSocketChannel.bind(new InetSocketAddress(8088));

        //将Channel设置非阻塞
        serverSocketChannel.configureBlocking(false);

        //将Channel注册到Selector上 监听连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务端通道已连接,服务端启动成功!");

        while (true){
            //轮询Selector  获得触发连接事件的channel数量
            int readyChannel = selector.select();

            //如果个数为0 则下一次循环  这是由于nio存在空轮询的bug
            if (readyChannel == 0)  continue;

            //获取可用的Channel集合    selectedKeys方法是获取所有触发事件的客户端Channel
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();

            while (iterator.hasNext()){
                //获取SelectionKey实例
                SelectionKey selectionKey = iterator.next();

                //移除处理过的key
                iterator.remove();

                //判断触发的事件 进行相应的处理
                if(selectionKey.isAcceptable()){
                    //接入事件
                    acceptHandler(serverSocketChannel,selector);
                }

                if (selectionKey.isReadable()){
                    //可读事件
                    readHandler(selectionKey,selector);
                }
            }
        }



    }


    //可读事件处理器
    private void readHandler(SelectionKey selectionKey,Selector selector) throws IOException {

        //获取客户端Channel
        SocketChannel channel = (SocketChannel)selectionKey.channel();

        //读取客户端发来的消息
            //创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuffer request = new StringBuffer();
            //循环读取客户端内容
        while (channel.read(buffer) > 0){
            //当前是读模式  现在切换写模式  将读取到的数据写入request中
            buffer.flip();

            //读取buffer中的内容
            request.append(Charset.forName("UTF-8").decode(buffer));
        }

        //将Channel再次注册打Selector上监听可读事件
        channel.register(selector,SelectionKey.OP_READ);

        //将客户端发来的消息广播给其他用户
        if(request.length() > 0){
            broadCast(selector,channel,request);
        }
    }

    //将某个客户端发来的消息广播给其他用户
    private void broadCast(Selector selector, SocketChannel sourceChannel, StringBuffer request) {

        //获取所有已接入的客户端       keys方法是获取所有的客户端Channel  无论是否触发事件
        Set<SelectionKey> keys = selector.keys();

        //遍历所有客户端 向他们发送消息
        for (SelectionKey selectionKey : keys){
            //获得客户端Channel
            Channel targetChannel = (Channel)selectionKey.channel();
            //判断是否是客户端通道和发来消息的客户端通道   剔除服务端通道和发来消息的客户端
            if (targetChannel instanceof SocketChannel && targetChannel != sourceChannel){
                try {
                    //将消息发送到客户端
                    ((SocketChannel) targetChannel).write(Charset.forName("UTF-8").encode(CharBuffer.wrap(request)));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }

    }


    //接入事件处理器
    private void acceptHandler(ServerSocketChannel serverSocketChannel,Selector selector) throws IOException {

        //获取客户端Channel
        SocketChannel socketChannel = serverSocketChannel.accept();

        //将客户端Channel设置为非阻塞模式
        socketChannel.configureBlocking(false);

        //将客户端注册到selector上  监听可读事件
        socketChannel.register(selector,SelectionKey.OP_READ);

        //回复客户端消息
        socketChannel.write(Charset.forName("UTF-8").encode("欢迎您进入多人聊天室..."));

    }


    public static void main(String[] args) throws IOException {

        //启动服务端
        new NioServer().start();

    }

}

客户端实现

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Scanner;

/**
 * 多人聊天系统客户端
 */
public class NioClient {


    public void start(String name) throws IOException {

        //创建SocketChannel通道
        SocketChannel socketChannel = SocketChannel.open();

        //绑定IP和端口 连接服务器
        socketChannel.connect(new InetSocketAddress("127.0.0.1",8088));

        //接受服务器响应
        Selector selector = Selector.open();
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);

        //如果服务器没有响应  会一直阻塞在这里 如果服务器响应 则进入下面

        //新开线程 专门负责处理服务端响应数据
        new Thread(new NioClientHandler(selector)).start();

        //向服务端发送数据
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){

            String line = scanner.nextLine();
            if (line != null && line.length() > 0){
                socketChannel.write(Charset.forName("UTF-8").encode(name +" : "+line));
            }

        }


    }


    public static void main(String[] args) throws IOException {

        new NioClient().start("A");

    }

}
package nio;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * Nio客户端用来处理服务端响应的任务
 */
public class NioClientHandler implements Runnable {


    private Selector selector;

    public NioClientHandler(Selector selector){
        this.selector = selector;
    }

    @Override
    public void run() {
        try {
            for (;;) {
                int readyChannels = selector.select();

                if (readyChannels == 0) continue;

                /**
                 * 获取可用channel的集合
                 */
                Set<SelectionKey> selectionKeys = selector.selectedKeys();

                Iterator iterator = selectionKeys.iterator();

                while (iterator.hasNext()) {
                    /**
                     * selectionKey实例
                     */
                    SelectionKey selectionKey = (SelectionKey) iterator.next();

                    /**
                     * **移除Set中的当前selectionKey**
                     */
                    iterator.remove();

                    /**
                     * 7. 根据就绪状态,调用对应方法处理业务逻辑
                     */

                    /**
                     * 如果是 可读事件
                     */
                    if (selectionKey.isReadable()) {
                        readHandler(selectionKey, selector);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    /**
     * 可读事件处理器
     */
    private void readHandler(SelectionKey selectionKey, Selector selector)
            throws IOException {
        /**
         * 要从 selectionKey 中获取到已经就绪的channel
         */
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

        /**
         * 创建buffer
         */
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        /**
         * 循环读取服务器端响应信息
         */
        String response = "";
        while (socketChannel.read(byteBuffer) > 0) {
            /**
             * 切换buffer为读模式
             */
            byteBuffer.flip();

            /**
             * 读取buffer中的内容
             */
            response += Charset.forName("UTF-8").decode(byteBuffer);
        }

        /**
         * 将channel再次注册到selector上,监听他的可读事件
         */
        socketChannel.register(selector, SelectionKey.OP_READ);

        /**
         * 将服务器端响应信息打印到本地
         */
        if (response.length() > 0) {
            System.out.println(response);
        }
    }


}

源代码:

NIO编程存在的问题

  • API繁杂,编程麻烦,学习成本大
  • 存在半包、粘包问题等,可靠性需要自己补齐
  • 存在Selector空轮询,导致CPU100%的bug

由于NIO存在以上问题,因此高性能网络编程很多使用Netty框架,Netty是对NIO进行封装,并解决了很多NIO存在的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值