欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

网络IO模型之BIO与NIO

程序员文章站 2022-06-14 11:18:19
...

BIO模型(Blocking-IO)

BIO最大的特点是单线程无法处理并发请求除非每连接每线程。每个请求服务器的客户端,对于服务器来讲,都要开辟一个线程去处理该客户端的IO事件。在操作系统中线程是个开销不小的资源,如果客户端只是与服务器建立了连接,并没有产生IO事件,那么对于服务器来讲,为这个客户端开辟这个线程岂不是在浪费资源?虽然可以使用线程池对服务端进行优化,但是治标不治本图示:
网络IO模型之BIO与NIO

在JAVA中JDK提供了BIO的相关实现-ServerSocket示例代码如下:

public class BIOServer {
    private static ExecutorService threadpool;
	// 初始化固定大小的线程池
    static {
        threadpool = Executors.newFixedThreadPool(10);
    }
	// 服务器接受客户端连接后,打包成一个任务提交给线程池进行处理
    private static class innerTask implements Runnable {
        private Socket socket;

        public innerTask(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    InputStream inputStream = socket.getInputStream();
                    int read = 0;
                    if (inputStream.available() != 0) {
                        byte[] bytes = new byte[1024];
                        // read()阻塞方法
                        inputStream.read(bytes);
                        String s = new String(bytes, Charset.forName("utf-8"));
                        System.out.println("服务器收到消息:" + s);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket();
        // 绑定端口启动服务端监听状态
        serverSocket.bind(new InetSocketAddress("localhost", 9999));
        System.out.println("BIO服务器启动监听端口9999...");
        while (true) {
        // 阻塞方法accept
        System.out.println("BIO服务器阻塞等待客户端连接...");
        Socket socket = serverSocket.accept();
        System.out.println("BIO服务器收到来自客户端[" + socket.getRemoteSocketAddress()+ "]的连接请求...");
        threadpool.execute(new innerTask(socket));
        }
    }
}

先简单看一下这个程序,main方法这里向OS申请创建了一个socket,设置为LISTEN状态,监听9999端口。由于accept()是阻塞方法,因此main线程在执行到accept()的时候进入阻塞状态,直到有客户端与服务器建立连接。此时accept()解除阻塞状态,返回一个socket,将这个socket封装成一个任务抛给线程池去等待并处理客户端的IO事件。main线程继续死循环再次accept()等待其他客户端的连接,如此循环…对于线程池中的线程来讲,死循环读取socket中的InputStream,看客户端有没有发送数据,如果读到了就输出在控制台。
这就是BIO的IO模型,accept()和read()都是阻塞方法。

NIO模型(NoneBlocking-IO/NEW IO)

非阻塞IO或者说是新的IO模型,由于BIO的单线程是无法处理并发的,所以出现了NIO,NIO可以实现服务端单线程,同时处理多个客户端的连接以及IO事件。当然大部分NIO的应用场景都是一个线程只负责客户端的连接事件在Netty里叫BossGroup,然后把这些连接交给多个线程(WorkerGroup)去处理IO事件。

在NIO的模型中有三个非常重要的组件:

  • Selector 选择器,也叫多路复用器。用来管理Channel上的事件(连接事件,可读事件,可写事件)在WinOS中是通过轮训的机制去查看Channle上有没有事件发生,如果有就把Channel放到一个Set集合中,解除select方法的阻塞状态,根据SelectionKey执行对应的事件。在Linux中使用的是epoll
  • Channel 通道,与流不同,流要么是输入流要么是输出流,Channel是双攻的通道,可读可写
  • ByteBuffer 字节缓冲区,主要与Channel中的IO数据进行交互

在JAVA中JDK提供了相关NIO的实现-ServerSocketChannel示例代码:

public class NIOServer {
    public static void main(String[] args) throws IOException {
    	// 向OS申请一个通道作为服务端
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 监听端口8888
        serverSocketChannel.bind(new InetSocketAddress("localhost", 8888));
        System.out.println("NIO服务器启动绑定端口8888....");
        // 设置通道的非阻塞
        serverSocketChannel.configureBlocking(false);
        // 多路复用器
        Selector selector = Selector.open();
        // 通道注册到多路复用器上,设置感兴趣的事件为连接事件
        serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
        while (true) {
            // select是阻塞的,当多路复用器上轮训到事件的时候解阻塞
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 接受到连接事件
                    System.out.println("NIO服务器收到客户端["+socketChannel.getRemoteAddress()+"]的连接...");
                    // 设置非阻塞
                    socketChannel.configureBlocking(false);
                    // 接收到连接事件后向多路复用器注册该通道目前感兴趣的事件为读事件
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                if (key.isReadable()) {
                    // 接受到读事件
                    // 创建ByteBuffer缓冲区用来接收IO数据
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    socketChannel.read(byteBuffer);
                    byteBuffer.flip();
                    System.out.println("NIO服务器收到消息: " + new String(byteBuffer.array(), Charset.forName("utf-8")));
                }
                iterator.remove();
            }
        }
    }
}

这个程序是由单线程既来接受客户端的连接请求,又处理IO事件。

  1. 首先还是先向OS申请一个服务通道ServerSocketChannel,设置为LISTEN状态,监听的端口是8888,设置此Channel为非阻塞。
  2. 获取多路复用器Selector,并把此通道注册到Selector上绑定目前的事件是连接事件。如果Selector监听到了连接事件,请你告诉我
  3. Selector调用select()进行阻塞,直到Selector轮训到Channel上的事件,解除阻塞
  4. 根据SelectionKey去处理不同的事件

这里在贴一下客户端的代码:

public class BIOClient {
    public static void main(String[] args) throws IOException, InterruptedException {
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", 8888));
        TimeUnit.SECONDS.sleep(5);
        OutputStream os = socket.getOutputStream();
        InputStream is = System.in;
        byte[] bytes = new byte[1024];
        while (true) {
            if (is.available() != 0) {
                is.read(bytes);
                os.write(bytes);
                os.flush();
            }
        }
    }
}