BIO与NIO以及零拷贝(zero copy)

本篇博客主要讲述BIO、NIO的网络模型以及零拷贝

BIO(Blocking IO)阻塞式IO

  1. BIO网络模型代码
    server端:
public class BIOServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=new ServerSocket(8088);
        while(true){
            //阻塞,放弃CPU资源
            Socket clientSocket = serverSocket.accept();
            System.out.println("连接成功IP:"+clientSocket.getInetAddress());
            byte[] bs=new byte[1024];
            InputStream is = clientSocket.getInputStream();
            //阻塞,放弃CPU资源
            is.read(bs);
            System.out.println(new String(bs));
        }
    }
}

client端:

public class BIOClient {
    public static void main(String[] args) throws IOException {
        Socket socket=new Socket("127.0.0.1",8088);

        OutputStream outputStream = socket.getOutputStream();
        Scanner scanner=new Scanner(System.in);
        String str = scanner.next();
        outputStream.write(str.getBytes());
        outputStream.close();
    }
}

以上是BIO的网络模型,存在的问题:

  • 服务器端serverSocket.accept()方法发生阻塞。
  • 服务器端is.read(bs)方法发生阻塞。
  • 上面两个阻塞的地方导致服务器每次只能处理一个客户端连接,当一个客户端连接不发数据时,其他客户端则不能连接服务器。

测试结果:
BIO与NIO以及零拷贝(zero copy)_第1张图片
2.BIO网络模型代码(多线程模型)
为了解决连接操作、流读取操作都在主线程中阻塞导致的服务端同时只能处理一个客户端连接进行优化修改,优化思路:让accept()连接在主线程中阻塞,每当一个客户端连接成功,则启动一个子线程让每个连接的read()操作在子线程中阻塞。

服务器端代码:

public class BIOServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket=new ServerSocket(8088);
        while(true){
            //阻塞,放弃CPU资源
            Socket clientSocket = serverSocket.accept();
            System.out.println("连接成功IP:"+clientSocket.getInetAddress());
            //创建子线程处理新来的客户端连接
            new Thread(new Runnable() {
                @Override
                public void run() {
                    byte[] bs=new byte[1024];
                    InputStream is = null;
                    try {
                        is = clientSocket.getInputStream();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    try {
                        //阻塞,放弃CPU资源
                        is.read(bs);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    System.out.println(new String(bs));
                }
            }).start();
        }
    }
}

测试结果:
BIO与NIO以及零拷贝(zero copy)_第2张图片
BIO与NIO以及零拷贝(zero copy)_第3张图片
以上结果是使用多线程处理客户端请求,成功的解决了服务器端同时只能处理一个客户端的连接,分析以上模型存在的问题:

  • 线程是计算机中非常宝贵的资源,每当来一个客户端就创建一个子线程进行处理非常浪费线程资源(可以进行进一步优化使用线程池,但是线程池中线程也是有限的)
  • 每当客户端连接成功后不发送消息,占用该线程不做事(占着茅坑不拉屎)则会导致服务该连接的子线程一直被阻塞无法释放资源,白白浪费资源

为了解决以上所有的问题,在JDK1.4版本中诞生了NIO。

NIO(New IO)非阻塞式IO

NIO又可以成为Non-blocking IO
1.NIO的网络模型代码
Server端代码(客户端还可以用之前的):

    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        //把服务器端通道设置为非阻塞,这样在accept()操作就不会进行阻塞了
        serverSocketChannel.configureBlocking(false);
        while(true){
            for(SocketChannel client:socketList){
                if((readSign=client.read(byteBuffer))>0){
                    //切换到写模式
                    byteBuffer.flip();
                    byte[] bs=new byte[readSign];
                    byteBuffer.get(bs);
                    System.out.println(new String(bs));
                    byteBuffer.clear();
                }else if(readSign<0){
                    //如果读到的长度为-1证明断开连接了
                    System.out.println(client.getRemoteAddress()+"断开连接");
                    socketList.remove(client);
                }
            }
            if((acceptSign=serverSocketChannel.accept())!=null){
                //如果接收到客户端连接请求,则把客户端连接通道设置为非阻塞,
                // 这样在read()操作时就不会阻塞
                acceptSign.configureBlocking(false);
                System.out.println("连接成功IP:"+acceptSign.getRemoteAddress());
                socketList.add(acceptSign);
            }

        }
    }
}

服务器端把ServerSocketChannel通道设置为非阻塞模式,在进行accept()接收客户端连接的时候就不会进行阻塞了,再把接收到的客户端连接的SocketChannel设置为非阻塞并放到一个队列中保存,在进行read()读取客户端消息的时候就不会进行阻塞了。再不断循环该队列判断是否有客户端发送消息,若读到的消息长度为-1则说明该客户端断开连接了,从队列中删除。这样就可以实现服务端一个线程同时处理多个客户端连接。

结果如下:BIO与NIO以及零拷贝(zero copy)_第4张图片
BIO与NIO以及零拷贝(zero copy)_第5张图片
以上服务端模型虽然解决了使用单线程来同时处理所有的客户端连接,但是仔细分析会发现其中存在一个问题:不断循环已经连接的客户端列表,挨个查看是否有读请求,假如List中存在1W个客户端连接,每次循环只有一个客户端发了消息,会有9999个无用循环,针对这个问题NIO还提供了一个核心组件Selector选择器,使用如下:

public class NIOServer2 {
    static ByteBuffer bs=ByteBuffer.allocate(1024);
    static int len=-1;
    public static void main(String[] args) throws IOException {
        ServerSocketChannel server=ServerSocketChannel.open();
        server.bind(new InetSocketAddress(8080));
        //设置为非阻塞模式
        server.configureBlocking(false);
        //创建一个选择器
        Selector selector=Selector.open();
        //将接收客户端连接的通道注册到选择其中
        server.register(selector, SelectionKey.OP_ACCEPT);
        //轮询访问监听器上是否有已经就绪时间,该操作会阻塞
        while(selector.select()>0){
            //给所有就绪时间创建一个迭代器,迭代这些事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while(iterator.hasNext()){
                SelectionKey next = iterator.next();
                //判断就绪事件,是什么事件
                if (next.isAcceptable()){
                    //如果是连接就绪,获取连接的客户端
                    SocketChannel socketChannel = server.accept();
                    //将获取到连接的客户端设置为非阻塞状态
                    //并且注册到选择器中,监听该连接的读请求
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("连接成功IP:"+socketChannel.getRemoteAddress());
                }else if(next.isReadable()){
                    //如果是读就绪,就处理该客户端的消息
                    SocketChannel socketChannel = (SocketChannel) next.channel();
                    if ((len = socketChannel.read(bs))>0){
                        //把缓冲区设置为读模式
                        bs.flip();
                        byte[] b=new byte[len];
                        bs.get(b);
                        System.out.println("接收到IP:"+socketChannel.getRemoteAddress()+"发送的:"+new String(b));
                        bs.clear();
                    }else{
                        //如果收到的消息长度为-1,则客户端断开连接,
                        //服务器端需要关闭该通道,否则该通道会一直发送-1进入,一直处理该事件
                        socketChannel.close();
                    }
                }
                //删除处理完的事件
                iterator.remove();
            }
        }

    }
}

使用Selector选择器,把所有的Channel以及对这些Channel感兴趣的事件注册到选择器中,Selector.select()是一个阻塞方法,用该方法判断注册的通道中是否有感兴趣的事件需要处理,,这就称作I/O多路复用
注意:根据测试当客户端断开连接后,服务器端对应的SocketChannel会不断发送读就绪事件,而且读到的长度为-1,所以当客户端断开连接后服务器端需要把对应该客户端的Channel进行关闭。关闭后也就从Selector中注销了。
结果:
BIO与NIO以及零拷贝(zero copy)_第6张图片
以上就是NIO的网络模型,不过没有使用到零拷贝

Linux相关知识

文件描述符

对于Java来说万物皆对象,对于Linux来说万物皆文件,使用一个叫做file descriptor后面简称fd,比如下面几种文件文件描述符:

  • 标准输入:0
  • 标准输出:1
  • 标准错误输出:2
  • Socket也有对应的fd, Java中创建Socket最终会调用到Linux内核函数去创建,如图:BIO与NIO以及零拷贝(zero copy)_第7张图片

用户空间/内核空间

Linux系统对自身进行了划分,一部分核心软件独立于普通应用程序,运行在较高的特权级别上,它们驻留在被保护的内存空间上,拥有访问硬件设备的所有权限,Linux将此称为内核空间。
相对地,应用程序则是在“用户空间”中运行。运行在用户空间的应用程序只能看到允许它们使用的部分系统资源,并且不能使用某些特定的系统功能,也不能直接访问内核空间和硬件设备,以及其他一些具体的使用限制。
将用户空间和内核空间置于这种非对称访问机制下有很好的安全性,能有效抵御恶意用户的窥探,也能防止质量低劣的用户程序的侵害,从而使系统运行得更稳定可靠。 --百度百科

可以简单理解为:

  • 用户空间(User space):提供给各个进程的主要空间
  • 内核空间(kernel space):程序调度、内存分配、连接硬件
    如果位于用户空间的程序想使用位于内核态的资源则需要从用户空间切换到内核空间,完成操作之后再从内核空间切换到用户空间。

Linux的I/O模型

BIO与NIO以及零拷贝(zero copy)_第8张图片

  • 阻塞式I/O: BIO与NIO以及零拷贝(zero copy)_第9张图片
  • 非阻塞式I/O:
    BIO与NIO以及零拷贝(zero copy)_第10张图片
  • 多路复用I/O:
    BIO与NIO以及零拷贝(zero copy)_第11张图片
    当调用了Selector.select()方法就会阻塞,发生系统调用Kernel会去查看所有注册到Selector选择器中的并感兴趣的事件,当有事件发生处理该事件。

BIO与Linux系统调用

Linux云服务器端代码:
BIO与NIO以及零拷贝(zero copy)_第12张图片
通过使用strace -ff -o ./log/NIO java BIOServer命令来跟踪这段代码的系统调用,将会阻塞,如图:

  • 跟踪系统调用后,程序会阻塞等待客户端连接
  • 在这里插入图片描述
  • 在log目录中会生成一些文件,这些文件是启动程序所创建的每个线程所对应的文件,并在这些文件中查找那个文件时主线程所对应的文件BIO与NIO以及零拷贝(zero copy)_第13张图片
  • 在通过刚才的查找结果去/proc/1164/目录下的fd目录中BIO与NIO以及零拷贝(zero copy)_第14张图片
  • 现在使用一个客户端连接以上几张图片变化如下:在这里插入图片描述
  • BIO与NIO以及零拷贝(zero copy)_第15张图片
  • BIO与NIO以及零拷贝(zero copy)_第16张图片
  • 再看一下那个对应着主线程中系统调用输出的文件,通过信息查找发现:BIO与NIO以及零拷贝(zero copy)_第17张图片
  • 真是吐了,不想写这块了,写写零拷贝把,感兴趣留言交流吧。

零拷贝

维基上是这么描述零拷贝的:零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输一个文件时以减少CPU周期和内存带宽。

零拷贝的优点:

  • 避免不必要的CPU拷贝,可以解放出来CPU去执行其他任务
  • 可以减少用户空间和内核空间状态的上下文切换

传统IO

BIO与NIO以及零拷贝(zero copy)_第18张图片
一次读写请求进行4次用户空间和内核空间上下文的切换,以及2次DMA拷贝和2次CPU拷贝,很容易发现这两次CPU拷贝不是必要的

基于sendFile的零拷贝

BIO与NIO以及零拷贝(zero copy)_第19张图片
可以看到一次sendFile系统调用需要2次用户空间和内核空间状态转换,2次DMA拷贝和1次CPU拷贝,这样做有一个限制==因为数据没有拷贝到用户空间,所以用户无法对数据进行修改,==可以看一下Linux内核提供的sendFile提供的方法:BIO与NIO以及零拷贝(zero copy)_第20张图片

带有DMA收集拷贝功能的sendfile实现的I/O

从Linux 2.4版本开始,操作系统底层提供了带有scatter/gather的DMA来从内核空间缓冲区中将数据读取到协议引擎中。这样一来待传输的数据可以分散在存储的不同位置上,而不需要在连续存储中存放。那么从文件中读出的数据就根本不需要被拷贝到socket缓冲区中去,只是需要将缓冲区描述符添加到socket缓冲区中去,DMA收集操作会根据缓冲区描述符中的信息将内核空间中的数据直接拷贝到协议引擎中。
BIO与NIO以及零拷贝(zero copy)_第21张图片
总的来说,带有DMA收集拷贝功能的sendfile实现的I/O只使用了2次用户空间与内核空间的上下文切换,以及2次数据的拷贝,而且这2次的数据拷贝都是非CPU拷贝。这样一来我们就实现了最理想的零拷贝I/O传输了,不需要任何一次的CPU拷贝,以及最少的上下文切换。

通过mmap实现的零拷贝I/O

mmap(内存映射)是一个比sendfile昂贵但优于传统I/O的方法。
BIO与NIO以及零拷贝(zero copy)_第22张图片
总的来说,通过mmap实现的零拷贝I/O进行了4次用户空间与内核空间的上下文切换,以及3次数据拷贝。其中3次数据拷贝中包括了2次DMA拷贝和1次CPU拷贝。
零拷贝参考地址:https://www.jianshu.com/p/e76e3580e356

你可能感兴趣的:(java,BIO,NIO,零拷贝)