拔刀吧!BIO,NIO

时间:2022-07-24
本文章向大家介绍拔刀吧!BIO,NIO,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

也许你或多或少的会在平时接触到IO,也许你平时最经常接触到的就是文件IO流读写,也可能听过这两种IO的区别,所以今儿咱来聊一下这个东西。

还有一种IO叫AIO,但这里不做记录。

先来看一下它们三的区别:

BIO:同步阻塞IO,客户端请求服务端,在服务端处理完成返回之前客户端一直会处于阻塞的状态。类似于你去外面吃饭需要排队,排队中你不干任何东西,直到叫号叫到你。

NIO:同步非阻塞IO,客户端请求服务端,在服务端处理过程中,客户端可以去干其他的东西,也可以隔一段时间去询问服务端,是否已处理完成。类似于排队叫号,你拿到号之后可以去干其他事情,比如逛逛街啥的,逛街回来询问一下是否叫到你,如果没有,你再去买个冰淇淋。

AIO:异步非阻塞IO,客户端请求服务端,服务端处理过程中客户端去干其他活,处理结束后通知客户端。类似于点外卖,外卖点了之后去改bug,等外卖员打电话给你。

下面就来看一下BIO和NIO的使用方法。

一、传统的BIO编程

在传统同步阻塞模型开发中,ServerSocket负责绑定服务端IP和监听端口,Socket负责发起连接请求,连接成功后,双方通过输入和输出流进行同步阻塞式通信。

采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。

该模型缺乏扩展性,如果客户端并发访问增加,服务端就需要起与客户端数量一致的线程,线程数量大的时候,系统性能就会下降,最终会导致服务端宕机。

接下来看一下其编程案例:

server端:

public class BIOServer {
    private static final Log logger = LogFactory.getLog(BIOServer.class);
    public static void main(String[] args) {
        new BIOServer().start();
    }

    private void start() {
        try {
            ServerSocket serverSocket = new ServerSocket();
            //绑定本地8888端口
            serverSocket.bind(new InetSocketAddress(8888));

            while (true) {
                //接收客户端连接
                Socket accept = serverSocket.accept();
                InputStream is = accept.getInputStream();
                OutputStream os = accept.getOutputStream();

                byte[] bytes = new byte[1024*1024];
                int len = 0;
                //读取客户端请求消息
                while ((len = is.read(bytes)) != -1) {
                    String msg = new String(bytes, 0, len);
                    logger.info("读取客户端的消息:" + msg);
                    bytes = new byte[1024];
                    String rspMsg = "服务端于【{0}】接收到客户端信息:【{1}】";
                    String format = MessageFormat.format(rspMsg,
                            new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(new Date()),
                            msg);
                    //给客户端回写消息
                    os.write(format.getBytes());
                    os.flush();
                }


            }

        }catch (Exception e) {
            logger.error("ServerSocket服务端启动失败",e);
        } finally {

        }
    }
}

客户端:

public class BIOClient {
    private static final Log logger = LogFactory.getLog(BIOClient.class);
    public static void main(String[] args) {
        new BIOClient().start();
    }
    private void start() {
        Socket socket = new Socket();
        try {
            //发起连接
            socket.connect(new InetSocketAddress("localhost", 8888));
            //等待连接成功
            while (!socket.isConnected()) {
            }
            
            OutputStream os =socket.getOutputStream();
            InputStream is = socket.getInputStream();

            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                //监听输入,将内容发送给服务端
                String msg = scanner.nextLine();
                os.write(msg.getBytes());
                //接收到服务端返回信息
                byte[] bytes = new byte[1024 * 1024];
                while (is.available()<=0) {
                    int len = is.read(bytes);
                    String msgResp = new String(bytes, 0, len);
                    logger.info("服务端返回消息:" + msgResp);
                    if (is.available() == 0) {
                        break;
                    }
                }
            }
        } catch (Exception e) {
            logger.error("客户端启动失败", e);
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

}

先启动服务端,之后再启动客户端,运行结果如下:

客户端:

服务端:

因为传统的BIO在每当有一个客户端连接时,服务端就会创建一个线程去处理新的客户端链路,还有一个是伪异步,就是服务端用线程池去处理客户端的连接。

由于伪异步底层采用的依然是同步阻塞模型,在线程池被耗时线程塞满之后,依然无法接入新的客户端连接,所以其只是一个优化,并不能完全解决阻塞的问题。

二、同步非阻塞IO:NIO

与BIO的SocketServer、Socket对应的是,NIO提供了ServerSocketChannel和SocketChannel,这两种套接字支持阻塞和非阻塞,需要使用者进行配置阻塞or非阻塞。

NIO有几个重要的关键点:

(1)缓冲区Buffer:在NIO中,所有数据的读写都是需要通过缓冲区处理。缓冲区实际上是一个数组,一般是ByteBuffer,但其又不仅仅是一个数组,其提供了对数据的结构化访问以及维护读写位置等信息。基本上每一种java基本类型都有对应的一种缓冲区:

缓冲区最重要的属性有三个,分别是position,limit和capacity。

position:下一个要被写入或读取的元素索引,初始化为0

limit:指定还有多少数据需要取出或者还有多少空间可以放入数据,初始化值与容量一致。

capacity:缓冲区容量。

其中0<=position<=limit<=capacity,当调用flip方法的时候,会将position值赋值给limit,position值赋值为0。调用clear方法会重新初始化这三个值。

(2)通道Channel:用于数据的读写,其与流的不同之处就在于通道是双向的,可用于读、写或读写同时进行,而流只能一个方向流动。channel是全双工,而流是单工的。channel可以分为两类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。

(3)多路复用器Selector:提供选择已经就绪的任务的能力。Selector会不断轮询注册在其上的Channel,如果某个Channel发生读写事件,这个Channel就处于就绪状态。一个多路复用器可以轮询多个Channel,JDK使用epoll()代替传统select实现,所以没有连接限制。

下面看一下NIO的编码示例:

服务端:

public class NIOServer {

    private static final Log logger = LogFactory.getLog(NIOServer.class);

    private int port;
    private InetSocketAddress address;

    private Selector selector;
    ServerSocketChannel server;

    NIOServer(int port) throws Exception {
        this.port =  port;
        address = new InetSocketAddress(this.port);
        //打开ServerSocketChannel
        server = ServerSocketChannel.open();
        //绑定端口
        server.bind(address);
        //设置为非阻塞
        server.configureBlocking(false);
        //创建多路复用器
        selector = Selector.open();
        //将ServerSocketChannel注册到多路复用器上,注册类型是监听客户端连接
        server.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器启动,监听端口为:"+port);
    }

    public void listen() throws IOException {
        while (true) {
            //轮询是否有已就绪的channel
            int wait = this.selector.select();
            if (wait == 0) {
                continue;
            }
            //获取已就绪的channel
            Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                //处理channel读写
                process(key);
                //将处理完的移除,一次只能处理一个请求,如果想要对一个channel处理多次请求,请重新注册到selector上
                iterator.remove();
            }

        }
    }

    private void process(SelectionKey key) throws IOException {
        //如果是ByteBuffer.allocateDirect(1024),则会直接分配在堆外内存里
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        //接收客户端连接
        if (key.isAcceptable()) {
            //获取客户端连接
            SocketChannel channel = ((ServerSocketChannel) key.channel()).accept();
            //设置为非阻塞
            channel.configureBlocking(false);
            //将channel注册到多路复用器上,监听读请求
            channel.register(selector,SelectionKey.OP_READ);
        } else if (key.isReadable()) { //监听channel的读请求
            SocketChannel client = (SocketChannel) key.channel();
            //将客户端内容读取到buffer中
            int read = client.read(buffer);
            if (read>0) {
                //buffer写转成读,其实就是控制position,limit的值,将position置为0,limit变成原先position的值
                buffer.flip();
                String result = new String(buffer.array(), 0, read);
                logger.info("接收到客户端内容为:"+result);
                //将channel注册为写事件
                client.register(selector,SelectionKey.OP_WRITE);
            }
            buffer.clear();
            buffer.flip();
        } else if (key.isWritable()) {
            //写
            SocketChannel client = (SocketChannel) key.channel();
            client.write(ByteBuffer.wrap("hello world".getBytes()));
        }
    }


    public static void main(String[] args) throws Exception {
        new NIOServer(8888).listen();
    }

}

客户端:

public class NIOClient {

    private static final Log logger = LogFactory.getLog(NIOClient.class);

    int port;
    InetSocketAddress socketAddress;
    Selector selector;

    NIOClient(int port) throws IOException, InterruptedException {
       this.port = port;
       socketAddress = new InetSocketAddress("localhost",port);
       //打开SocketChannel
       SocketChannel channel = SocketChannel.open();
       //配置为非阻塞
       channel.configureBlocking(false);
       //发起连接
       channel.connect(socketAddress);
       //打开多路复用器
       selector = Selector.open();
       //将SocketChanne注册到多路复用器上,监听连接请求
       channel.register(selector,SelectionKey.OP_CONNECT);



        boolean isOver = false;

        while(!isOver){
            //轮询是否有已就绪的channel
            int select = selector.select();
            if (select==0) {
                continue;
            }
            //获取已就绪的channel
            Iterator ite = selector.selectedKeys().iterator();
            while(ite.hasNext()){
                SelectionKey key = (SelectionKey) ite.next();
                ite.remove();

                SocketChannel sc = (SocketChannel) key.channel();
                //是否已连接
                if(key.isConnectable()){
                    if(channel.finishConnect()){
                        //只有当连接成功后才能注册OP_READ事件
                        sc.register(selector,SelectionKey.OP_WRITE);
                    } else{
                        System.exit(1);
                    }
                }else if(key.isReadable()){
                    //读取服务端返回的消息
                    ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                    int read = channel.read(byteBuffer);
                    if (read>0) {
                        byteBuffer.flip();
                        logger.info("接收到服务端信息为:" + new String(byteBuffer.array()));
                        isOver = true;
                    } else {
                        key.cancel();
                        sc.close();
                    }
                } else if (key.isWritable()) {
                    //写
                    sc.write(ByteBuffer.wrap("我是客户端".getBytes()));
                    sc.register(selector,SelectionKey.OP_READ);
                }
            }
        }

        selector.close();


    }

    public static void main(String[] args) throws IOException, InterruptedException {
        new NIOClient(8888);
    }

}

运行结果:

服务端:

客户端:

读取上一段代码,你只会发现,JavaNIO的编码很复杂。相比于JavaNIO,Netty的编码就简单的多,所以之后主要也会记一下Netty的使用。