美文网首页
Java系列6 NIO

Java系列6 NIO

作者: 莫小归 | 来源:发表于2019-02-11 14:57 被阅读0次

参考:
https://www.jianshu.com/p/362b365e1bcc
https://www.jianshu.com/p/5d2c68b89f1d
https://www.jianshu.com/p/389e4571cd2c

一.NIO产生背景

  • 高并发场景的技术要求
    1)传统IO即使使用线程池技术,面临高并发时依然线程不足
    2)传统阻塞I/O模式,大量线程在等待数据时被挂起,CPU利用率低,系统吞吐量差
    3)线程较长阻塞时间在网络不稳定场景下,将降低系统可靠性
  • 传统IO特性
    1)IO是面向流的阻塞的
    2)传统IO模型中,一个连接对应一个线程
    3)传统IO面向流意味着连接每次从流中读取一个或多个字节,直至全部读取完毕,没有缓存在任何地方,同时不能前后移动流中数据
    4)传统IO的各种流是阻塞的,意味着当线程调用read或write方法时将被阻塞,不能执行其他操作
  • NIO特性
    1)NIO是面向块的非阻塞的
    2)NIO面向块意味着将把数据读取到一个稍后处理的缓冲区中,必要时可在缓冲区内前后移动,增加数据处理的灵活性
    3)NIO非阻塞模式使得线程从某个通道读取数据或者向某个通道写数据的过程中,遇到数据等待时不会挂起,可执行其他工作
    4)NIO通过将多个Channel以事件注册到一个Selector实现由一个线程处理多个请求

二.NIO核心实现

NIO核心API Channel,Buffer,Selector。数据总是从Chanel读取到Buffer,或从Buffer写入Channel

1.通道Channel
  • 可以同时进行读写(从缓冲区读数据,或写数据到缓冲区)
  • 可以异步读写数据
2.缓冲区Buffer
  • 本质是一个可以写入数据的内存块,可以再次读取
  • 读写数据一般遵循:
    1)数据到Buffer
    2)调用Buffer.flip()方法,将Buffer从写模式切换到读模式
    3)从Buffer读取数据
    4)调用Buffer.clear() 或 Buffer.compat() 方法,清空Buffer
  • Buffer常用标志:
    1)Buffer的大小/容量 - Capacity
    2)Buffer当前读写位置 - Position
    3)Buffer中信息末尾位置 - Limit
    Buffer读写模式下的Capacity、Position、Limit

3.Selector

  • 多个Channel以事件的方式注册到一个Selector,实现一个线程处理多个请求
    一个线程处理多个请求
  • 调用Selector的select()或selectNow()方法时只返回有数据读取的SelectableChannel实例


    返回有数据读取的Channel

三.NIO常用方法

1.Buffer类的flip、clear、compact方法

本质是设置控制Buffer状态的position、limit、capacity三个变量

  • flip方法
    使Buffer从读状态转为写状态:当前position设置为limit,并将position指向数据开始位置
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
 }
flip方法变读状态为写状态
  • clear方法
    重设缓冲区以重新接收字符
public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}
clear方法将缓冲区position清零
  • compact方法
    与clear方法类似,但只清空已读取的数据,还未读取的数据仍保留在缓冲区

2.Channel类

  • configureBlocking()register()
channel.configureBlocking(false);  //设置channel为非阻塞
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);  //注册channel

3.Selector类

  • SelectionKey
    注册到Selector上的实例
    1)register()方法注册的事件类型有4种
    Connect 某个Channel成功连接到另一服务器
    Accept 某个ServerSocketChannel准备好接收新连接
    Read 某个Channel有数据可读
    Write 某个Channel等待写数据
    2)对应于SelectionKey的常量
    SelectionKey.OP_CONNECT
    SelectionKey.OP_ACCEPT
    SelectionKey.OP_READ
    SelectionKey.OP_WRITE
    3)SelectionKey包含如下属性
    The interest set 感兴趣的事件的集合
    The ready set 已经准备就绪的操作的集合
    The Channel
    The Selector
    An attached object(optional) 将对象或信息attach到SelectionKey以便识别
  • select()
    返回int值表示有多少通道已就绪,包含重载方法
    1)int select():阻塞到至少有一个通道在注册的事件上就绪
    2)int select(long timeout):和select()一样,但设定阻塞时间上限timeout毫秒
    3)int selectNow():不会阻塞,不管什么通道就绪都立刻返回。如果自从前一次select后没有通道就绪,则返回0
  • selectedKeys()
    在调用select()获取就绪通道数后,可通过selectedKeys()方法返回就绪的Channel,之后可通过迭代SelectionKey获得就绪的Channel
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) { 
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
    // a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
    // a connection was established with a remote server.
} else if (key.isReadable()) {
    // a channel is ready for reading
} else if (key.isWritable()) {
    // a channel is ready for writing
}
keyIterator.remove();
}
  • 注意
    1)Selector对象并不会从自己的SelectedKey集合中自动移除SelectedKey实例,需要在处理完一个Channel时调用keyIterator.remove()方法手动移除,下一次Channel就绪时,Selector会再将它添加到SelectedKey集合中
    2)SelectionKey.channel()方法返回的channel需要转成具体要处理的类型,如ServerSocketChannel或SocketChannel等
  • wakeup()
  • open()和close()
    使用Selector前调用Selector.open()打开Selector
    使用Selector后调用Selector.close()关闭Selector并使注册到该Selector上的所有SelectionKey实例无效,但通道本身并不会关闭

四.NIO实践

1.从文件读取数据

  • 读数据步骤
    1)从FileInputStream获取Channel
    2)创建Buffer
    3)从Channel读取数据到Buffer
FileInputStream fin = new FileInputStream("ReadTest.txt");
FileChanel fc = fim.getChannel();  //获取通道
ByteBuffer buffer = ByteBuffer.allocate(1024);  //创建缓冲区
fc.read(buffer);  //从通道读入数据到缓冲区
  • 写数据步骤
    1)从FileOutputStream获取Channel
    2)创建Buffer,并将数据放入Buffer
    3)把Buffer中数据写入Channel
FileOutputStream fout = new FileOutputStream("WriteTest.txt");
FileChannel fc = fout.getChannel;  //获取通道
ByteBuffer buffer = ByteBuffer.allocate(1024);  //创建缓冲区
for(int i = 0 ; i < message.length ; i++) {
  buffer.put( message[i] );
}    //将数据放入缓冲区
buffer.flip();  //切换缓冲区为写模式
fc.write(buffer);  //将缓冲区内容写入通道
  • 读写结合例程
/**
 * 用java NIO api拷贝文件
 * @param src
 * @param dst
 * @throws IOException
 */
public static void copyFileUseNIO(String src,String dst) throws IOException{
    //声明源文件和目标文件
            FileInputStream fi=new FileInputStream(new File(src));
            FileOutputStream fo=new FileOutputStream(new File(dst));
            //获得传输通道channel
            FileChannel inChannel=fi.getChannel();
            FileChannel outChannel=fo.getChannel();
            //获得容器buffer
            ByteBuffer buffer=ByteBuffer.allocate(1024);
            while(true){
                //判断是否读完文件
                int eof =inChannel.read(buffer);
                if(eof==-1){
                    break;  
                }
                //重设一下buffer的position=0,limit=position
                buffer.flip();
                //开始写
                outChannel.write(buffer);
                //写完要重置buffer,重设position=0,limit=capacity
                buffer.clear();
            }
            inChannel.close();
            outChannel.close();
            fi.close();
            fo.close();
}     

2.网络Socket使用NIO

  • 步骤

  • 例程

  public void client() throws Exception{
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 3000));
        socketChannel.configureBlocking(false);
        
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){
            String str = scanner.next();
            buffer.put(str.getBytes());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();
        }
public void server() throws Exception {
        // 获取通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        // 设置为非阻塞
        serverChannel.configureBlocking(false);
        // 绑定端口
        serverChannel.bind(new InetSocketAddress(3000));

        // 创建连接器
        Selector selector = Selector.open();
        // 将连接器注册到channel,并设置监听事件(接受事件)
        // SelectionKey.OP_CONNECT:链接状态
        // SelectionKey.OP_READ:读状态
        // SelectionKey.OP_WRITE:写状态
        // SelectionKey.OP_ACCEPT:接受状态,当接受准备就绪,开始进行下一步操作
        // 通过 | 进行链接可以监听多个状态
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 轮寻获取选择器上已经准备就绪的状态
        while (selector.select() > 0) {
            // 获取所有的监听Key
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    // 若获取状态就绪,就获取客户端的链接
                    SocketChannel clientChannel = serverChannel.accept();
                    // 将客户端的链接设置为非阻塞状态
                    clientChannel.configureBlocking(false);
                    // 给该通道注册到选择器上,并设置状态为读就绪
                    clientChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len = 0;
                    while ((len = channel.read(buffer)) > 0) {
                        buffer.flip();
                        System.out.println(new String(buffer.array(), 0, len));
                        buffer.clear();
                    }
                }
                // 取消处理完了的选择建
                iterator.remove();
            }
        }
    }

愿将腰下剑,直为斩楼兰

相关文章

网友评论

      本文标题:Java系列6 NIO

      本文链接:https://www.haomeiwen.com/subject/ezzfeqtx.html