NIO模型

一、概念

socke本身是阻塞的,非阻塞IO要求tsocket被设置为non-blocking。

  • 系统调用
    1)在内核缓冲区没有数据的情况下,会立即返回一个调用失败的信息。
    2)在内核缓冲区有数据的情况下,是阻塞的。直到内核缓冲区的数据全部复制到进程缓冲区,系统调用成功。
    在这里插入图片描述
  • NIO特点
    每次的IO调用,在内核数据未就绪的情况下,应用程序需要不停地进入IO调用,轮询查看数据是否就绪,如果没有就绪,继续轮询;如果就绪,才会返回。
  • 优点:在内核缓冲区没有数据的情况下,发起的系统调用不会阻塞,用户程序不会阻塞。
  • 缺点:需要不断地反复地发起IO调用,这种不断轮询,不断询问内核的方式,会占用CPU大量时间,资源利用率比较低;在内核缓冲区有数据的情况下,也是阻塞的。NIO模型在高并发场景下是不可用的。
  • Java中NIO提供了选择器(Selector)类似操作系统提供的 select/epoll,也叫做IO多路复用器。作用是检查一个channel(通道)的状态是否是可读、可写、可连接、可接收的状态,为了实现单线程管理多个channel,也就是管理多个网络请求。
  • Channel通道
    即可以读,又可以写,不直接和数据源打交道,主要和缓冲区Buffer进行交互java.nio.channels
    ServerSocketChannel 服务器端
    SocketChannel 客户端
  • Buffer缓冲区
    IO流中的数据通过缓冲区交给Channel

二、NIO编程

  • 服务器端
    1)实例化ServerSocketChannel
    2)绑定端口 通过ServerSocketChannel调用bind() 方法
    3)设置ServerSocketChannel 为非阻塞 configir…
    4)实例化Selector选择器
    5)将ServerSocketChannel注册到选择器上ServerSocketChannel.register()
    6)监听是否有新的事件 接收(连接)事件/读写事件/ Selector.select()
    7)获取已完成事件的集合,对于这个集合进行遍历
    8)如果发现是Accept事件,则进行accept调用,获取SocketChannel,注册到Selector上,关注read事件
    9)监听是否有read读事件
    10)通过SocketChananel通道来读取数据,其中通过buffer作为传输介质
    11)关闭资源
    SocketChannel
    Selector
    ServerSocketChannel
public class MyNIOServer {
    public static void main(String[] args) {
        ServerSocketChannel serverSocketChannel = null;
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
        ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
        try {
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.bind(new InetSocketAddress(9999));
            System.out.println("服务器端已经启动...");
            serverSocketChannel.configureBlocking(false);

            Selector selector = Selector.open();
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            //等待监听事件结果,这个select是一个阻塞的方法,直到有事件才会返回
            while(selector.select() > 0){
                //获取事件的集合
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                //遍历这个集合,选择事件
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    iterator.remove();

                    if(key.isAcceptable()){
                        //通过key获取通道channel
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        //接收连接
                        SocketChannel accept = channel.accept();
                        System.out.println("客户端:"+accept.getRemoteAddress()+"已连接...");
                        //设置accept为非阻塞
                        accept.configureBlocking(false);
                        //将accept通道注册到selector选择器上关注读事件
                        accept.register(selector, SelectionKey.OP_READ);
                    }

                    if(key.isReadable()){
                        //通过key获取通道channel
                        SocketChannel channel = (SocketChannel) key.channel();
                        //进行读取操作
                        int read = channel.read(readBuffer);
                        if(read == -1){
                            //通道关闭
                            channel.close();
                            //连接关闭
                            key.cancel();
                            continue;
                        }
                        //进行读写模式切换
                        readBuffer.flip();
                        //将数据从buffer读取
                        byte[] bytes = new byte[readBuffer.remaining()];
                        readBuffer.get(bytes);
                        System.out.println("客户端:"+channel.getRemoteAddress()+",发送消息:"+new String(bytes, 0, bytes.length));
                        readBuffer.clear();
                        //将channel通道注册到selector选择上关注写事件
                        channel.register(selector, SelectionKey.OP_WRITE);
                    }

                    if(key.isWritable()){
                        //通过key获取通道channel
                        SocketChannel channel = (SocketChannel) key.channel();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
                        String info = reader.readLine();
                        //将info写入channel
                        writeBuffer.put((info+"\n").getBytes());
                        writeBuffer.flip();
                        channel.write(writeBuffer);
                        writeBuffer.clear();
                        //将channel通道注册到selector选择上关注读事件
                        channel.register(selector, SelectionKey.OP_READ);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(serverSocketChannel != null){
                try {
                    serverSocketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 客户端
    1)实例化 SocketChannel
    2)设置 SocketChannel 为非阻塞
    3)实例化 Selector
    4)连接服务器connect,在这个方法中提供ip地址端口号,注意:这个方法不是一个阻塞方法,如果连接失败返回false,连接成功返回true。
    5)如果是false,则将SocketChannel注册到Selector选择器中,监听connect可连接事件
    6)监听selector中是否有可完成事件遍历可完成事件的集合,判断该事件是否是可连接事件
    7)connect方法返回true
    8)给服务器端发送消息,channel.write
    9)关闭资源,selector SocketChannel
public class MyNIOClient {
    public static void main(String[] args) {
        SocketChannel socketChannel = null;

        try {
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            Selector selector = Selector.open();

            if(!socketChannel.connect(new InetSocketAddress("127.0.0.1", 9999))){
                socketChannel.register(selector, SelectionKey.OP_CONNECT);
                while(selector.select() > 0){
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
                        SelectionKey key = iterator.next();
                        iterator.remove();
                        if(key.isConnectable()){
                            SocketChannel channel = (SocketChannel) key.channel();
                            //完成连接
                            channel.finishConnect();
                            System.out.println("客户端已启动...");
                        }
                    }
                }
            }
            Scanner scanner = new Scanner(System.in);
            while(true){
                //连接成功,给服务器端发送消息
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                System.out.println("客户端要发送消息到服务器端:");
                String info = scanner.nextLine();
                if("".equals(info) || "exit".equals(info)){
                    break;
                }
                buffer.put(info.getBytes(StandardCharsets.UTF_8));
                buffer.flip();
                socketChannel.write(buffer);
                buffer.clear();

                //获取服务器端的响应消息
                int read = socketChannel.read(buffer);
                if(read == -1){
                    break;
                }
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                System.out.println("服务器端响应消息:"+new String(bytes, 0, bytes.length));
                buffer.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(socketChannel != null){
                try {
                    socketChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

多线程(Server端)

思路:主线程接收到客户端连接,将连接所对用的socketChannel交给子线程处理读写请求。
代码实现:

//子线程  实现Runnable接口
class NIOServerHandler implements Runnable{
    private SocketChannel socketChannel;
    //创建Selector实例
    private Selector selector;

    public NIOServerHandler(SocketChannel socketChannel){
        this.socketChannel = socketChannel;
        if(selector == null){
            try {
                selector = Selector.open();//创建IO复用器
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void run() {//关注socketChannel通道的读写就绪事件,进而处理
        try {
            socketChannel.configureBlocking(false);//设置为非阻塞
            socketChannel.register(selector, SelectionKey.OP_READ);//把要关注的读事件注册到selector上

            while(selector.select() > 0){//有 读/写 事件发生
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    iterator.remove();

                    if(key.isReadable()){//是可读事件
                        SocketChannel channel = (SocketChannel) key.channel();//获取key所对应的Channel
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        channel.read(buffer);
                        buffer.flip();//读写模式切换

                        byte[] bytes = new byte[buffer.remaining()];//buffer.remaining -> 可读大小
                        buffer.get(bytes);
                        String msg = new String(bytes, 0, bytes.length);
                        System.out.println(Thread.currentThread().getName()+", 客户端:"+channel.getRemoteAddress()+", 消息: "+msg);

                        buffer.clear();
                        buffer.put(("hello client\n").getBytes());
                        buffer.flip();
                        channel.write(buffer);
                        if("".equals(msg) || "exit".equals(msg)){
                            System.out.println(Thread.currentThread().getName()+"客户端:"+channel.getRemoteAddress()+"下线了");
                            key.cancel();
                            channel.close();
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
//主线程
public class MyMultiThreadNIOServer {
    public static void main(String[] args) {
        ServerSocketChannel serverSocketChannel = null;

        try {
            //创建ServerSocketChannel
            serverSocketChannel = ServerSocketChannel.open();
            //绑定端口
            serverSocketChannel.bind(new InetSocketAddress(9998));
            //将SerSocketChannel置为非阻塞
            serverSocketChannel.configureBlocking(false);
            //创建Selector选择器
            Selector selector = Selector.open();
            //将serverSocketChannel注册到选择器上,关注可接受事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            //使用固定数量的线程池
            ExecutorService pool = Executors.newFixedThreadPool(3);
            while(selector.select() > 0){
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//获取当前迭代器
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    iterator.remove();
                    if(key.isAcceptable()){//是可接受事件
                        System.out.println(Thread.currentThread().getName()+"关注可接受事件");
                        ServerSocketChannel serverSocketChannel1 = (ServerSocketChannel) key.channel();//获取通道
                        SocketChannel channel = serverSocketChannel1.accept();
                        System.out.println("客户端:"+channel.getRemoteAddress()+"已连接");
                        //将SocketChannel channel提交给子线程
                        pool.submit(new NIOServerHandler(channel));
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

NIO中重要组件

Channel

  1. 类似流,即可以从通道中读取数据,也可以写数据到通道中, 通道可以异步地读写,通道中的数据先要读取到一个Buffer中,或者从一个Buffer中写入。
    读数据:将数据从 channel读到 buffer
    写数据:从 buffer中写入到 channel通道中
  2. 常用实现类
    DatagramChannel 通过UDP的方式读写网络中的数据通道
    SocketChannel 通过TCP的方式读写网络中的数据,一般用于客户端
    ServerSocketChanneel 通过TCP的方式读写网络中的数据,一般用于服务器端
    FileChannel 用于读写操作文件的通道

Buffer

  1. 缓冲区,与NIO Channel交互,数据是从通道读取进入缓冲区,从缓冲区写入到通道中。
  2. 使用Buffer注意点:
    (1) 写数据到buffer
    (2) 调用buffer.flip
    (3) 从Buffer中读取数据
    (4) buffer.clear/buffer.compact
    当从buffer中读取数据,调用 clear方法清空buffer中的数据,或者调用 compact方法清空已经读过的数据。任何未读到数据会被移动缓冲区的起始位置,新写入的数据将放到缓冲区未读数据的后面。
  3. Buffer实现依赖3个指针 :position limit capacity
    position:取决于Buffer处于读模式还是写模式,写数据到Buffer中,position表示当前位置,初始的值为0;读数据时,从某个特定位置开始去读,需要将buffer从写模式切换为读模式,position会被重置为0。
    limit写模式下,表示最多能往里写多少数据读模式下,表示最多能读到的数据
    capacity
    作为buffer内存块,有一个固定的大小

示例:
ByteBuffer.allocate(100);
1、position = 0 limit = capacity = 100
2、buffer.put(“hello\n”) position=6 limit = capacity = 100
3、buffer.flip() position=0 limit = position capacity = 100
4.buffer.get() 3 position=3 limit = 6 capacity = 100

  1. Buffer 的方法
    ByteBuffer.allocate() ;在堆上分配空间
    ByteBuffer.allocateDirect() ;在堆外分配空间
    ByteBuffer.wrap(byte[] bytes); 通过 byte数据创建一个缓冲区
    flip()
    capacity()
    limit()
    position()

Selector

  1. 选择器,也叫做多路复用器,作用是检查一个或多个channel通道是否处于可读、可写、可连接(管理多个网络请求)
  2. 优势
    单线程管理多个网络连接,相比于多线程使用了更多的线程,效率反而更减少了线程上下文切换带来的资源耗费
  3. Selector的使用
    (1) Selector.open();
    (2) channel.register;(xxx, SelectionKey的四种事件)
    (3) selector.select() ;这个方法是一个阻塞方法,如果有事件就绪则返回
  • Selector维护三种类型的selectionKey集合
    selector.selectedKeys();已选择键的集合
    selector.keys();已注册键的集合
    selector.cancelKey();已取消键的集合
  • SelectionKey的四种事件
    一个通道可以注册多个事件 通过SelectionKey.OP_READ | SelectionKey.OP_WRITE

NIO和BIO的比较

  1. BIO的方式处理数据,NIO的方式处理数据,块I/O的效率比流I/O高很多。
  2. BIO阻塞的(线程等待数据,没有数据发送过来,继续等待),NIO非阻塞的(通道没有数据时不用等待)。
  3. BIO基于字节流字符流进行操作,而NIO基于Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
NIOBIO
面向缓冲区(Buffer)面向流(Stream)
非阻塞(Non Blocking IO)阻塞(Blocking IO)
选择器(Selectors)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值