JAVA基础知识-NIO

NIO简介

NIO全称java non-blocking IO 始于java 1.4,是一种同步非阻塞的IO模型,提供了新的JAVA IO操作非阻塞API,用意就是代替JAVA IO 和Java Networking相关的API。

JAVA NIO的非阻塞模式,是一个线程维护着一个选择器(Selector),客户端读/写操作而不是直接对接线程,而是先进入缓冲区,通道(channel)从缓冲区获取相应事件,准备好读/写事件后通道会通知选择器(Selector)来执行,只对准备好的通道进行数据的读写操作,如果当前通道没有读写事件,而是阻塞等待事件,那么它会去处理其它已经准备好读写的通道,这样从而达到了一个线程可以维护多个客户端读写事件

  • 三个核心组件

NIO的三个核心组件包含缓冲区(Buffer )、通道(Channel )、选择器(Selector )

1.Buffer 缓冲区

缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次获取,此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块。

相对直接对数组的操作,Buffer API更加容易操作和管理

1.1Buffer读写步骤

第一:将数据写入缓冲区

第二:调用buffer.flip(),转换为读取模式

第三:缓冲区读取数据

第四:调用buffer.clear()或者Buffer.compact()清除缓存区

1.2Buffer的工作原理

三个重要属性:

capacity容量:作为一个内存块,Buffer具有一定的固定大小,也称为“容量”

position位置:写入模式时代表写数据的位置。读取模式时代表读取数据的位置。

limit限制:写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。


代码示例1.2.1

package com.nipx.demo.aio.nio;

import java.nio.ByteBuffer;

public class Buffer_demo {

    public static void main(String[] args) {

        //构建一个byte字节缓冲区,容量是4

        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4);

        //默认写入模式,套看三个重要的指标

        System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s",

                byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

        //写入3个字节的数据

        byteBuffer.put((byte) 1);

        byteBuffer.put((byte) 2);

        byteBuffer.put((byte) 3);

        //再看看数据

        System.out.println(String.format("写入字节后:capacity容量:%s, position位置:%s, limit限制:%s",

                byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

        //转换为读取模式(如果不调用flip方法,也是可以读取数据的,但是position的位置是错误的)

        System.out.println("++++++++++++++++++++++开始读取++++++++++++++++++++++++");

        byteBuffer.flip();

        byte a = byteBuffer.get();

        System.out.println("获取第一个字节:" + a);

        byte b = byteBuffer.get();

        System.out.println("获取第二个字节:" + b);

        System.out.println(String.format("读取2个字节后:capacity容量:%s, position位置:%s, limit限制:%s",

                byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

        //此时是读取模式,继续写入3个字节,limit=2,position=2,继续写入只能覆盖

        //compact方法仅清除已阅读的数据,转为写入模式

        byteBuffer.compact();//这句不写的话,会报数组下标越界的错误

        ByteBuffer t1 = byteBuffer.put((byte) 3);

        System.out.println("写入数据:" + t1);

        ByteBuffer t2 = byteBuffer.put((byte) 4);

        System.out.println("写入数据:" + t2);

        ByteBuffer t3 = byteBuffer.put((byte) 5);

        System.out.println("写入数据:" + t3);

        System.out.println(String.format("最后结果:capacity容量:%s, position位置:%s, limit限制:%s",

                byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit()));

    }

}

```

运行结果:

3、ByteBuffer内存模型

ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(head堆)两种实现,堆外内存获取的方式为ByteBuffer byteBuffer = ByteBuffer.allocateDirect(noBytes);

这样做的好处就是:

a、进行网络IO或者文件IO时比heapBuffer少一次拷贝,

file/socket ----OS memory ----jvm heap; GC会移动对象内存,在写file或socket的过程中,jvm的实现中,会先把数据赋值到堆外,再进行写入。

b、GC范围之外,降低GC压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator

2.Channel 通道

在BIO中,我们对数据的读写时通过OutputStream和InputStream来操作,但是在NIO中,我们只需要一个channel。

Channel的API涵盖了UDP/TCP网络和文件IO

FileChannel

DatagramChannel

SocketChannel     【主要】

SercerSocketChannel    【主要】

和标准的IO Stream的区别是:

在一个通道内进行读写

Stream是单向的

可以非阻塞读取和写入通道

通道始终读取或写入缓冲区

2.1SocketChannel  介绍

SocketChannel  用于建立TCP网络连接,类似于java.net.Socket。有两种创建方式:

客户端主动发起和服务器的连接

服务端获取的新连接

writer写:在write()尚未写入任何内容就可能反回了,需要在循环中调用

read读:read()方法可能直接返回而根本不读取任何数据,根据返回的int来判断读取了多少字节。

2.2ServerSocketChannel介绍

ServerSocketChannel可以监听新建的TCP连接通道,类似于ServerSocket。

serverSocketChannel.accept():如果该通道处于非阻塞模式,那么如果没有挂起的线程,它回直接返回一个null,所以必须检查返回的SocketChannel是否为null。

代码示例2.2.1

服务器端:

package com.nipx.demo.aio.nio;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

public class NIOServer {

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

        //创建网络服务端

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //默认是阻塞,设置为非阻塞

        serverSocketChannel.configureBlocking(false);

        //绑定端口

        serverSocketChannel.socket().bind(new InetSocketAddress(8080));

        System.out.println("启动成功");

        while (true) {

            //获取tcp连接通道

            SocketChannel socketChannel = serverSocketChannel.accept();

            if (socketChannel != null) {

                System.out.println("接收到新数据:" + socketChannel.getRemoteAddress());

                //设置为非阻塞

                socketChannel.configureBlocking(false);

                ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                

                /**这里存在的问题,当一个连接建立之后,如果没有数据,这里一直会处于循环状态,

                再来的连接无法及时获取连接,只能处于等待状态,当上一个连接的数据处理完之后,

                下一个连接才能处理数据*/

                while (socketChannel.isOpen() && socketChannel.read(byteBuffer) != -1) {

                    if (byteBuffer.position() > 0) {

                        break;

                    }

                }

                if (byteBuffer.position() == 0) {

                    continue;

                }

                byteBuffer.flip();

                byte[] content = new byte[byteBuffer.limit()];

                byteBuffer.get(content);

                System.out.println(new String(content));

                System.out.println("收到数据,来自:" + socketChannel.getRemoteAddress());

                String res = "HTTP/1.1 200 OK\r\n" +

                        "Content-Length: 11\r\n\r\n" +

                        "Hello World";

                ByteBuffer response = ByteBuffer.wrap(res.getBytes());

                while (response.hasRemaining()) {

                    socketChannel.write(response);

                }

            }

        }

    }

}

```

针对上边的问题,我们做了以下改进1:

代码示例2.2.2

package com.nipx.demo.aio.nio;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.util.ArrayList;

import java.util.Iterator;

import java.util.List;

public class NIOServer1 {

    //已经建立连接的集合

    private static ArrayList<SocketChannel> list = new ArrayList<>();

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

        //创建网络服务端

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //默认是阻塞,设置为非阻塞

        serverSocketChannel.configureBlocking(false);

        //绑定端口

        serverSocketChannel.socket().bind(new InetSocketAddress(8080));

        System.out.println("启动成功");

        while (true) {

            //获取tcp连接通道

            SocketChannel socketChannel = serverSocketChannel.accept();

            if (socketChannel != null) {

                System.out.println("接收到新数据:" + socketChannel.getRemoteAddress());

                //设置为非阻塞

                socketChannel.configureBlocking(false);

                list.add(socketChannel);

            } else {

                //如果没有连接了,则处理已经有的连接

                Iterator<SocketChannel> iterator = list.iterator();

                while (iterator.hasNext()) {

                    SocketChannel ch = iterator.next();

                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                    if (ch.read(byteBuffer) == 0) {

                        continue;

                    }

                    while (ch.isOpen() && ch.read(byteBuffer) != -1) {

                        if (byteBuffer.position() > 0) {

                            break;

                        }

                    }

                    if (byteBuffer.position() == 0) {

                        continue;

                    }

                    byteBuffer.flip();

                    byte[] content = new byte[byteBuffer.limit()];

                    byteBuffer.get(content);

                    System.out.println(new String(content));

                    System.out.println("收到数据,来自:" + ch.getRemoteAddress());

                    String res = "HTTP/1.1 200 OK\r\n" +

                            "Content-Length: 11\r\n\r\n" +

                            "Hello World";

                    ByteBuffer response = ByteBuffer.wrap(res.getBytes());

                    while (response.hasRemaining()) {

                        ch.write(response);

                    }

                    iterator.remove();

                }

            }

        }

    }

}

```

客户端:

代码示例2.2.3

package com.nipx.demo.aio.nio;

import com.fasterxml.jackson.databind.util.ByteBufferBackedInputStream;

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SocketChannel;

import java.util.Scanner;

public class NIOClient {

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

        SocketChannel socketChannel = SocketChannel.open();

        socketChannel.configureBlocking(false);

        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));

        while (!socketChannel.finishConnect()) {

            Thread.yield();

        }

        Scanner scanner = new Scanner(System.in);

        System.out.println("请输入:");

        String msg = scanner.nextLine();

        ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());

        while (byteBuffer.hasRemaining()) {

            socketChannel.write(byteBuffer);

        }

        //读取响应

        System.out.println("收到服务器相应:");

        ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

        while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {

            if (requestBuffer.position() > 0) {

                break;

            }

        }

        requestBuffer.flip();

        byte[] content = new byte[requestBuffer.limit()];

        requestBuffer.get(content);

        System.out.println(new String(content));

        scanner.close();

        socketChannel.close();

    }

}

```

3.Selector 选择器

selector是一个Java NIO的组件,可以检查一个或者多个NIO通道,并确定哪些通道已经准备好进行读取或者写入。实现单个线程可以管理多个通道,从而管理多个网络连接。

一个线程使用Selector监听多个channel的不同事件,也叫做:事件驱动机制

3.1四种事件

四个事件分别对应SelectionKey四个常量。

1、Connect 连接(SelectionKey.OP_CONNECT)

2、Accept准备就绪(OP_ACCEPT)

3、Read读取(OP_READ)

4、Writer写入(OP_WRITER)

使用selector来代替上面的轮询方式

代码示例3.1.1

package com.nipx.demo.aio.nio;

import java.net.InetSocketAddress;

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;

/**

 * 使用Selector选择器来实现非阻塞

 */

public class NIOServer2 {

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

        //1、创建网络服务端

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //默认是阻塞,设置为非阻塞

        serverSocketChannel.configureBlocking(false);

        //2、创建一个Selector选择器,并且将channel注册上去

        Selector selector = Selector.open();

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

        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

        //3、绑定端口

        serverSocketChannel.socket().bind(new InetSocketAddress(8080));

        System.out.println("启动成功");

        while (true) {

            //不再轮询通道,改用轮询事件的方式,selector方法有阻塞的效果,直到有事件通知才会有返回

            selector.select();

            //获取事件

            Set<SelectionKey> selectionKeys = selector.selectedKeys();

            //遍历查询结果

            Iterator<SelectionKey> iter = selectionKeys.iterator();

            while (iter.hasNext()) {

                //被封装的查询结果

                SelectionKey key = iter.next();

                iter.remove();

                //关注read 和 accept两个事件

                if (key.isAcceptable()) {

                    ServerSocketChannel server = (ServerSocketChannel) key.attachment();

                    //将拿到的客户端连接通道,,并且注册到selector上面

                    SocketChannel clientScocketChannel = server.accept();

                    clientScocketChannel.configureBlocking(false);

                    clientScocketChannel.register(selector, SelectionKey.OP_READ, clientScocketChannel);

                    System.out.println("接收到新数据:" + clientScocketChannel.getRemoteAddress());

                }

                if (key.isReadable()) {

                    SocketChannel socketChannel = (SocketChannel) key.attachment();

                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

                    while (socketChannel.isOpen() && socketChannel.read(byteBuffer) != -1) {

                        if (byteBuffer.position() > 0) {

                            break;

                        }

                    }

                    if (byteBuffer.position() == 0) {

                        continue;

                    }

                    byteBuffer.flip();

                    byte[] content = new byte[byteBuffer.limit()];

                    byteBuffer.get(content);

                    System.out.println(new String(content));

                    System.out.println("收到数据,来自:" + socketChannel.getRemoteAddress());

                    String res = "HTTP/1.1 200 OK\r\n" +

                            "Content-Length: 11\r\n\r\n" +

                            "Hello World";

                    ByteBuffer response = ByteBuffer.wrap(res.getBytes());

                    while (response.hasRemaining()) {

                        socketChannel.write(response);

                    }

                    key.cancel();

                }

            }

            selector.selectNow();

        }

    }

}

```

三.NIO和BIO的区别

Reactor--NIO与多线程结合的改进方案

主要的思想就是:

将处理网络连接交给一部分线程去做,将进行IO的读写交给另一部分线程去做。

代码示例1.1

package com.nipx.demo.aio.nio;

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.*;

import java.util.Iterator;

import java.util.Random;

import java.util.Set;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

import java.util.concurrent.FutureTask;

import java.util.concurrent.LinkedBlockingQueue;

import java.util.concurrent.atomic.AtomicInteger;

/**

 * NIO  selector  多路复用reactor线程模型

 */

public class NIOServer3 {

    //创建一个处理业务操作的线程池

    private static ExecutorService workPool = Executors.newCachedThreadPool();

    //封装了select.select()等事件轮询的代码

    abstract class ReactorThread extends Thread {

        Selector selector;

        LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();

        //selector监听到事件后 调用

        public abstract void handler(SelectableChannel channel) throws Exception;

        private ReactorThread() throws IOException {

            selector = Selector.open();

        }

        volatile boolean running = false;

        @Override

        public void run() {

            //轮询selector事件

            while (running) {

                Runnable task;

                while ((task = taskQueue.poll()) != null) {

                    task.run();

                }

                try {

                    selector.select(1000);

                    //直接获取查询结果

                    Set<SelectionKey> selected = selector.selectedKeys();

                    //遍历查询结果

                    Iterator<SelectionKey> iter = selected.iterator();

                    while (iter.hasNext()) {

                        //被封装的查询结果

                        SelectionKey key = iter.next();

                        iter.remove();

                        int readyOps = key.readyOps();

                        //关注read 和 accept

                        if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {

                            try {

                                SelectableChannel channel = (SelectableChannel) key.attachment();

                                channel.configureBlocking(false);

                                handler(channel);

                                if (!channel.isOpen()) {

                                    key.cancel(); // 如果关闭了,就取消这个KEY的订阅

                                }

                            } catch (Exception ex) {

                                key.cancel(); // 如果有异常,就取消这个KEY的订阅

                            }

                        }

                    }

                    selector.selectNow();

                } catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }

        private SelectionKey register(SelectableChannel channel) throws Exception {

            // 为什么register要以任务提交的形式,让reactor线程去处理?

            // 因为线程在执行channel注册到selector的过程中,会和调用selector.select()方法的线程争用同一把锁

            // 而select()方法实在eventLoop中通过while循环调用的,争抢的可能性很高,为了让register能更快的执行,就放到同一个线程来处理

            FutureTask<SelectionKey> futureTask = new FutureTask<>(() -> channel.register(selector, 0, channel));

            taskQueue.add(futureTask);

            return futureTask.get();

        }

        private void doStart() {

            if (!running) {

                running = true;

                start();

            }

        }

    }

    private ServerSocketChannel serverSocketChannel;

    // 1、创建多个线程 - accept处理reactor线程 (accept线程)

    private ReactorThread[] mainReactorThreads = new ReactorThread[1];

    // 2、创建多个线程 - io处理reactor线程  (I/O线程)

    private ReactorThread[] subReactorThreads = new ReactorThread[8];

    /**

     * 初始化线程组

     */

    private void newGroup() throws IOException {

        // 创建IO线程,负责处理客户端连接以后socketChannel的IO读写

        for (int i = 0; i < subReactorThreads.length; i++) {

            subReactorThreads[i] = new ReactorThread() {

                @Override

                public void handler(SelectableChannel channel) throws IOException {

                    // work线程只负责处理IO处理,不处理accept事件

                    SocketChannel ch = (SocketChannel) channel;

                    ByteBuffer requestBuffer = ByteBuffer.allocate(1024);

                    while (ch.isOpen() && ch.read(requestBuffer) != -1) {

                        // 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)

                        if (requestBuffer.position() > 0) break;

                    }

                    if (requestBuffer.position() == 0) return; // 如果没数据了, 则不继续后面的处理

                    requestBuffer.flip();

                    byte[] content = new byte[requestBuffer.limit()];

                    requestBuffer.get(content);

                    System.out.println(new String(content));

                    System.out.println(Thread.currentThread().getName() + "收到数据,来自:" + ch.getRemoteAddress());

                    // TODO 业务操作 数据库、接口...

                    workPool.submit(() -> {

                    });

                    // 响应结果 200

                    String response = "HTTP/1.1 200 OK\r\n" +

                            "Content-Length: 11\r\n\r\n" +

                            "Hello World";

                    ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());

                    while (buffer.hasRemaining()) {

                        ch.write(buffer);

                    }

                }

            };

        }

        // 创建mainReactor线程, 只负责处理serverSocketChannel

        for (int i = 0; i < mainReactorThreads.length; i++) {

            mainReactorThreads[i] = new ReactorThread() {

                AtomicInteger incr = new AtomicInteger(0);

                @Override

                public void handler(SelectableChannel channel) throws Exception {

                    // 只做请求分发,不做具体的数据读取

                    ServerSocketChannel ch = (ServerSocketChannel) channel;

                    SocketChannel socketChannel = ch.accept();

                    socketChannel.configureBlocking(false);

                    // 收到连接建立的通知之后,分发给I/O线程继续去读取数据

                    int index = incr.getAndIncrement() % subReactorThreads.length;

                    ReactorThread workEventLoop = subReactorThreads[index];

                    workEventLoop.doStart();

                    SelectionKey selectionKey = workEventLoop.register(socketChannel);

                    selectionKey.interestOps(SelectionKey.OP_READ);

                    System.out.println(Thread.currentThread().getName() + "收到新连接 : " + socketChannel.getRemoteAddress());

                }

            };

        }

    }

    /**

     * 初始化channel,并且绑定一个eventLoop线程

     *

     * @throws IOException IO异常

     */

    private void initAndRegister() throws Exception {

        // 1、 创建ServerSocketChannel

        serverSocketChannel = ServerSocketChannel.open();

        serverSocketChannel.configureBlocking(false);

        // 2、 将serverSocketChannel注册到selector

        int index = new Random().nextInt(mainReactorThreads.length);

        mainReactorThreads[index].doStart();

        SelectionKey selectionKey = mainReactorThreads[index].register(serverSocketChannel);

        selectionKey.interestOps(SelectionKey.OP_ACCEPT);

    }

    /**

     * 绑定端口

     *

     * @throws IOException IO异常

     */

    private void bind() throws IOException {

        //  1、 正式绑定端口,对外服务

        serverSocketChannel.bind(new InetSocketAddress(8080));

        System.out.println("启动完成,端口8080");

    }

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

        NIOServer3 nioServerV3 = new NIOServer3();

        nioServerV3.newGroup(); // 1、 创建main和sub两组线程

        nioServerV3.initAndRegister(); // 2、 创建serverSocketChannel,注册到mainReactor线程上的selector上

        nioServerV3.bind(); // 3、 为serverSocketChannel绑定端口

    }

}

```

由此可见创建连接的线程和处理数据的线程是不同的。

四.NIO应用场景

NIO适用于以下场景:

1.处理大量的并发连接

2.处理大文件传输

3.处理网络IO

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值