深入浅出BIO和NIO(基于java)

一、什么BIO?
1.简单实现BIO

客户端:

public class BioClient {
    public static void main(String[] args) {
        Socket socket = null;
        BufferedReader in = null;
        BufferedWriter out = null;
        try {
            socket = new Socket("localhost", 8080);
            //发送消息到服务端
            out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            //通过\n告诉接收端消息发送结束
            out.write("我是客户端发送了一个消息\n");
            out.flush();
            //接收来自服务端的消息
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println("来自服务端的消息:" + in.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                in.close();
                out.close();
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端:

public class BioServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        BufferedReader in = null;
        BufferedWriter out = null;
        try {
            //监听8080端口
            serverSocket = new ServerSocket(8080);
            //阻塞等待客户端连接(连接阻塞)
            Socket socket = serverSocket.accept();
            //通过InputStream()接收数据
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println("接收到客户端的消息 : " + in.readLine());
            //写回去,通过OutputStream()发送数据
            out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            //通过\n告诉接收端消息发送结束
            out.write("服务端回复的消息\n");
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                out.close();
                in.close();
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

先启动服务端,在启动客户端
在这里插入图片描述
在这里插入图片描述
可以运行成功。
图解:
在这里插入图片描述

2.BIO有什么问题?怎么优化?

1).我们在服务端给客户端返回消息之前加一段阻塞代码来模拟服务端阻塞

   			in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println("接收到客户端的消息 : " + in.readLine());
            //写回去,通过OutputStream()发送数据
            Thread.sleep(100000); 
            out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            //通过\n告诉接收端消息发送结束
            out.write("服务端回复的消息\n");

我们客户端复制一份(BioClient1),我们启动服务端。启动BioClient,在启动一个客户端BioClient1。
在这里插入图片描述
我们发现服务端只接收到了BioClient发来的消息,BioClient1没有做处理。这个时候我们发现服务端一下只能处理一个请求,我们称为迭代服务器

2).我们在客户端(BioClient)给服务端发消息之前加一段阻塞代码来模拟客户端阻塞

  			out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            Thread.sleep(100000);
            //通过\n告诉接收端消息发送结束
            out.write("我是客户端发送了一个消息\n");

启动服务端。启动BioClient,在启动一个客户端BioClient1
在这里插入图片描述
服务端依然阻塞了,BioClient1的请求也没回应。为什么呢?
因为服务端在读取客户端发送过来的数据时发生了IO阻塞。因为客户端阻塞了,服务端读取不到数据。就阻塞到了下面的代码处,而且其他的客户端发送请求,得不到处理
在这里插入图片描述
3)优化:IO阻塞怎么让它不影响其他的客户端呢?
我们可以让IO的读写交给异步线程,优化我们的服务端如下:

public class BioNewServer {
    static ExecutorService executorService = newFixedThreadPool(20);
    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        //监听8080端口
        try {
            serverSocket = new ServerSocket(8080);
            while (true){
                Socket socket = serverSocket.accept();
                //异步
                executorService.execute(new SocketThread(socket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
public class SocketThread implements Runnable {
    private Socket socket;
    public SocketThread(Socket socket) {
        this.socket = socket;
    }
    @Override
    public void run() {
        BufferedReader in = null;
        BufferedWriter out = null;
        try {
            //通过InputStream()接收数据
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            System.out.println("接收到客户端的消息 : " + in.readLine());
            //写回去,通过OutputStream()发送数据
            out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            //通过\n告诉接收端消息发送结束
            out.write("服务端回复的消息\n");
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                out.close();
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

我们依然让客户端BioClient阻塞,启动服务端BioNewServer,在启动BioClient,BioClient1
在这里插入图片描述
我们发现BioClient阻塞了,服务端依然可以处理客户端BioClient1的请求。

图解:
在这里插入图片描述

3.BIO是阻塞IO,到底阻塞在哪?

1)我们先了解下TCP的通信过程
首先,对于 TCP 通信来说,每个 TCP Socket 的内核中都有一个发送缓冲区和一个接收缓冲区,TCP 的全双工的工作模式就是依赖于这两个独立的 Buffer 和该 Buffer 的填充状态。接收缓冲区把数据缓存到内核,若应用进程一直没有调用 Socket 的 read 方法进行读取,那么该数据会一直被缓存在接收缓冲区内。不管进程是否读取Socket,对端发来的数据都会经过内核接收并缓存到Socket 的内核接收缓冲区。 read 所要做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的 Buffer 里。进程调用Socket 的 send 发送数据的时候,**一般情况下是将数据从应用层用户的 Buffer 里复制到Socket 的内核发送缓冲区,**然后 send 就会在上层返回。换句话说,send 返回时,数据不一定会被发送到对端。

在这里插入图片描述
面我们提到,Socket 的接收缓冲区被 TCP 用来缓存网络上收到的数据,一直保存到应用进程读走为止。如果应用进程一直没有读取,那么 Buffer 满了以后,出现的情况是:通知对端TCP 协议中的窗口关闭,保证 TCP 接收缓冲区不会移除,保证了 TCP 是可靠传输的。如果对方无视窗口大小发出了超过窗口大小的数据,那么接收方会把这些数据丢弃。
BIO
前面其实已经简单讲过了阻塞 IO 的原理,我想在这里重申一下什么是阻塞 IO 呢? 就是当客户端的数据从网卡缓冲区复制到内核缓冲区之前,服务端会一直阻塞。以socket 接口为例, 进程空间中调用recvfrom,进程从调用 recvfrom 开始到它返回的整段时间内都是被阻塞的,因此被成为阻塞IO 模型
在这里插入图片描述
NIO
提前对比下NIO的模型,如果我们希望这台服务器能够处理更多的连接,怎么去优化呢?
我们第一时间想到的应该是如何保证这个阻塞变成非阻塞吧。所以就引入了非阻塞IO 模型, 非阻塞 IO 模型的原理很简单,就是进程空间调用 recvfrom,如果这个时候内核缓冲区没有数据的话,就直接返回一个 EWOULDBLOCK 错误,然后应用程序通过不断轮询来检查这个状态状态,看内核是不是有数据过 来。
在这里插入图片描述

二、什么NIO
1.NIO简介

JAVA NIO有两种解释:一种叫非阻塞IO(Non-blocking I/O),另一种也叫新的IO(New I/O),其实是同一个概念。它是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

NIO是一种基于通道和缓冲区的I/O方式,它可以使用Native函数库直接分配堆外内存(区别于JVM的运行时数据区),然后通过一个存储在java堆里面的DirectByteBuffer对象作为这块内存的直接引用进行操作。这样能在一些场景显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。

2.NIO中的重要对象

NIO主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。传统IO是基于字节流和字符流进行操作(基于流),而NIO基于Channel和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个线程可以监听多个数据通道。

Buffer
Buffer(缓冲区)是一个用于存储特定基本类型数据的容器。除了boolean外,其余每种基本类型都有一个对应的buffer类。Buffer类的子类有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer 。

Channel
Channel(通道)表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接。Channel接口的常用实现类有FileChannel(对应文件IO)、DatagramChannel(对应UDP)、SocketChannel和ServerSocketChannel(对应TCP的客户端和服务器端)。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。

Selector
Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。即用选择器,借助单一线程,就可对数量庞大的活动I/O通道实施监控和维护。

selector 必须是非阻塞的
selector事件类型:读事件(OP_READ),写时间(OP_WRITE),连接事件(OP_CONNECT),接收事件(OP_ACCEPT)

3.NIO的简单实现

client:

public class NewIoClient {

    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.connect(new InetSocketAddress("localhost", 8080));
        // 打开选择器
        Selector selector = Selector.open();
        // 监听服务端接受的连接事件(当服务端accpet的时候,会触发这个事件)
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        while (true) {
            // 监听事件,没有事件,未阻塞状态
            selector.select();
            // 可能有多个事件,客户端一般只有一个
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();

                // 连接事件
                if (selectionKey.isConnectable()) {
                    handleConnect(selector, selectionKey);
                }
                // 读事件
                if (selectionKey.isReadable()) {
                    handleRead(selector, selectionKey);
                }
                // 处理完删除,防止重复处理
                iterator.remove();
            }

        }
    }
    private static void handleConnect(Selector selector, SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        if (socketChannel.isConnectionPending()) {
            socketChannel.finishConnect();
        }
        // 完成连接,后我们就可以向服务端发送数据
        socketChannel.write(ByteBuffer.wrap("我是客户端".getBytes()));
        // 同时注册读事件,这样服务端发送数据过来,selector可以监听到
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private static void handleRead(Selector selector, SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.out.println(new String(byteBuffer.array()));
    }
}

server:

public class NewIoServer {

    public static void main(String[] args) throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // nio 默认也为阻塞,设置成非阻塞
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        // 监听客户端发来的连接事件,(当客户端 socketChannel.connect(new InetSocketAddress("localhost", 8080))会触发)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            // 监听事件,没有事件,未阻塞状态
            selector.select();
            // 可能有多个事件
            Set<SelectionKey> keys = selector.selectedKeys();

            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 连接事件,(当客户端 socketChannel.connect(new InetSocketAddress("localhost", 8080))会触发)
                if ( selectionKey.isAcceptable()) {
                    handleAccept(selector,selectionKey);
                }
                // 读事件
                if (selectionKey.isReadable()) {
                    handleRead(selector, selectionKey);
                }
                // 处理完删除,防止重复处理
                iterator.remove();
            }

        }

    }

    private static void handleAccept(Selector selector, SelectionKey key) throws IOException {
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(false);
        socketChannel.write(ByteBuffer.wrap("接收到了客户端请求".getBytes()));
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private static void handleRead(Selector selector, SelectionKey key) throws IOException {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.out.println(new String(byteBuffer.array()));
    }
}

多路复用模型:
在这里插入图片描述
优化点:
省去Java轮询操作系统这件事情,直接让操作系统去处理。即将上述核心代码中的for循环给省去了,在NIO中直接变成了native方法,交给操作系统函数去做轮询并查找到以及通知对应的socket去做后续操作。
NIO的read、accept方法等都交给操作系统去进行处理不再由程序员写Java代码处理,通过内核级别的线程去做进行提速。

三、什么是零拷贝?

在这里插入图片描述
比如客户端要将本地的磁盘中的数据,通过网络发送到服务端server.需要如下步骤:
1.将磁盘中的数据读取系统的缓冲区
2.将系统中缓冲的数据拷贝到用户的缓冲区
3.然后再从用户的缓冲区拷贝到缓冲的数据

我们在上面IO中有提到,我们在调用in.read(),实际上就是.将系统中缓冲的数据拷贝到用户的缓冲区。**out.wirte()**就是用户的缓冲区拷贝到缓冲的数据,这个时候并不一定讲将数据发送到服务端了。

零拷贝,并不是没有拷贝,上面1的过程我们无法避免,但是2.3步骤是可以避免的。零拷贝就是将磁盘中的数据读取系统的缓冲区,然后直接将数据交给网卡发送。

tip:
kafak 消息队列就是用的零拷贝。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值