Java NIO编程-netty学习之旅

(源码之后放Gitee上)

1. IO模型

1.1 IO模型基本说明

IO模型类别概念优点缺点
BIO同步并阻塞,服务器实现方式为一个请求就开辟一个线程处理,除非使用线程池,但终归是一对一的线程和请求编程简单,使用🐟连接数少且架构固定的项目连接空闲时,开销时是不必要的
NIO同步非阻塞,服务器实现方式为一个线程去处理多个io请求(连接),客户端发送的请求都会注册到io多路复用器上,多路复用器轮询到连接通道上有io请求时就进行处理连接数多,并且短的架构,比如聊天服务器,弹幕系统编程复杂
AIO异步,启用了proactor模式,有效请求才去启动线程,由操作系统完成后再去通知服务端程序处理请求连接数多且时间长的服务

1.2 bio工作机制

模型:
在这里插入图片描述
工作流程:
在这里插入图片描述
问题分析:

  • 每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write 。

  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。

  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费

代码实例

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

        //思路
        //1. 创建一个线程池
        //2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了");


        while (true) {

            System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
            System.out.println("wating for connecting");

            //监听,等待客户端连接, 若无连接是阻塞状态
            final Socket accept = serverSocket.accept();
            System.out.println("connect to one client");
            newCachedThreadPool.execute(() -> {
                // 可以和客户端通讯
                handle(accept);
            });
        }
    }

    public static void handle(Socket socket) {
        System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
        byte[] bytes = new byte[1024];
        try {
            //通过socket 获取输入流
            InputStream inputStream = socket.getInputStream();
            //循环的读取客户端发送的数据
            while (true) {
                System.out.println(Thread.currentThread().getName() + "---wating for reading");
                //阻塞式读取
                int read = inputStream.read(bytes);
                // -1代表读到文件尽头了
                if (read != -1) {
                    System.out.println(new String(bytes, 0, read));
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            System.out.println("---closing connection");
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
terminal连接成功后,不断输入字符
在这里插入图片描述
在这里插入图片描述

1.3nio工作机制

1.3.1基本概念

  • Java NIO 全称 java non-blocking IO,是同步非阻塞的
  • NIO 有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
    ,其基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
  • Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情

简单理解可以类比配送外卖,假设有个智障配置系统,骑手只能挨家去查看有无外卖需要配送,并且不断在固定的商家之间循环配送,但是由于时间关系,假设有一家外卖还没做出来,骑手不会等待会继续查看下一家的外卖准备情况,有就拿走配送,无就下一家继续查看。

1.3.2 和bio的比较

io模式处理方式是否阻塞
bio基于字符流和字节流阻塞
nio基于块的,有三大组件,Channel(通道)和 Buffer(缓冲区)和Selector(选择器)非阻塞

1.3.3 核心原理

selector会根据不同的事件在各个通道上进行切换。
在这里插入图片描述

1.3.3.1Buffer

Buffer:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),即使Channel 是提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
简单的例子可以更好认识buffer,注释部分可以逐个去掉试下效果

public class BasicBuffer {
    public static void main(String[] args) {
        // 创建一个Buffer, 大小为 5, 即可以存放5个int
        IntBuffer intBuffer = IntBuffer.allocate(5);
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i * 2);
        }
        // 从缓冲区读取数据
        // 写时需要切换模式
        intBuffer.flip();
        //intBuffer.position(1);
        //intBuffer.limit(3);
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

可以点击进入java.nio.Buffer源码部分,查看主要的继承关系和主要的成员变量。其中mark是反转读写的用于标记的位置。
在这里插入图片描述

    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;

1.3.3.2Channel

NIO的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲:

首先入手两个例子

    public static void main(String[] args) {
        File file = new File("D:\\Java\\channel.txt");
        try {
            //创建一个输出流->channel
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            //通过 fileOutputStream 获取 对应的 FileChannel
            FileChannel channel = fileOutputStream.getChannel();
            String s = "真的会有人选择阿福吗";
            //创建一个缓冲区 ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //将 str 放入 byteBuffer
            byteBuffer.put(s.getBytes());
            //对byteBuffer 进行flip,准备进行写操作
            byteBuffer.flip();
            //将byteBuffer 数据写入到 fileChannel
            channel.write(byteBuffer);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

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

        File file = new File("D:\\Java\\channel.txt");
        //创建文件的输入流
        FileInputStream fileInputStream = new FileInputStream(file);
        //通过fileInputStream 获取对应的FileChannel -> 实际类型  FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();
        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
        //将 通道的数据读入到Buffer
        fileChannel.read(byteBuffer);
        //将byteBuffer 的 字节数据 转成String
        System.out.println(new String(byteBuffer.array()));
        fileInputStream.close();
    }

进入Channel类查看继承关系,发现其下有几个主要实现类。
其中FileChannel 用于文件的数据读写,DatagramChannel 用于 UDP 的数据读写,ServerSocketChannel 和 SocketChannel 用于 TCP 的数据读写。
在这里插入图片描述

1.3.3.3Selector

Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
实现照样是例子先行。体验到底怎么个非阻塞法。

//服务端
public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //通过channel得到ServerSocket,然后绑定(监听)一个端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        //得到一个Selecor对象
        Selector selector = Selector.open();
        //把 serverSocketChannel 注册到selector,设置关心事件为 OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            //select能返回各个通道处发生事件的数量
            if (selector.select(1000) == 0) {
                System.out.println("sys has been waiting for 1s");
                continue;
            }
            //如果返回的>0, 就获取到相关的 selectionKey集合
            //1.如果返回的>0, 表示已经获取到关注的事件
            //2. selector.selectedKeys() 返回关注事件的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //判断key 对应的通道发生的事件类型,做对应的处理
                if (key.isAcceptable()) {//代表OP_ACCEPT, 有新的客户端连接
                    //为该客户端的连接,新建一个socketChannel,相当于bio中的socket一般
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);
                    //并将该通道注册到selector上,设置该通道的关注事件为OP_READ
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if (key.isReadable()) {//代表OP_READ,客户端有数据传送过来
                    //通过key 反向获取到对应channel
                    SocketChannel channel = (SocketChannel) key.channel();
                    //获取到该channel关联的buffer
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    int read = channel.read(buffer);
                    System.out.println(new String(buffer.array()));
                }
                //手动从集合中移动当前的selectionKey, 防止重复操作
                iterator.remove();
            }
        }

    }
public static void main(String[] args) throws IOException {
        //得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
        //连接服务器
        if (!socketChannel.connect(address)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("client can do other thing...");
            }
        }
        String s = "hah shssssssssssssssssssssssssssssssssssssssssss";
        ByteBuffer byteBuffer = ByteBuffer.wrap(s.getBytes());
        socketChannel.write(byteBuffer);
        System.in.read();
    }

进入Selector抽象类,可以查看几个主要的方法。
在这里插入图片描述
工作流程:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值