专栏首页Liusy01拔刀吧!BIO,NIO

拔刀吧!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的使用。

本文分享自微信公众号 - Liusy01(Liusy_01),作者:Liusy01

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-20

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【设计模式-状态模式】

    【导读】人在不同的状态下会做出不同的行为,比如愤怒的时候会做一些出格的事,高兴的时候会分享快乐,这种就是状态模式。

    Liusy
  • 【设计模式-单例模式】

    今天来说一下同样属于创建型模式的单例模式,相信这个模式普遍都清楚,因为平时在编码的时候都会进行相应的使用,我这边就当做日志记录一下。免得以后忘了还得去搜,我发现...

    Liusy
  • Netty之TCP粘包/拆包

    TCP会根据缓冲区的实际大小情况进行包的拆分和合并,所谓粘包,就是将多个小的包封装成一个大的包进行发送。拆包,即是将一个超过缓冲区可用大小的包拆分成多个包进行发...

    Liusy
  • PHP汉字转拼音

    基于 CC-CEDICT 词典的中文转拼音工具,更准确的支持多音字的汉字转拼音解决方案。

    php007
  • Presto安装完成之后需要做的

    Presto因其优秀的查询速度被我们所熟知,它本身基于MPP架构,可以快速的对Hive数据进行查询,同时支持扩展Connector,目前对Mysql、Mongo...

    叁金
  • 细说ASP.NET Core与OWIN的关系

      最近这段时间除了工作,所有的时间都是在移植我以前实现的一个Owin框架,相当移植到到Core的话肯定会有很多坑,这个大家都懂,以后几篇文章可能会围绕这个说下...

    yoyofx
  • MySQL数据库优化小结

    第三范式-表的其他普通数据不依赖其他普通数据,就是依赖的数据记得给索引。要用其他属性做查询条件记得用索引

    ydymz
  • CVPR 2020 | 腾讯和南京大学提出:轻量级行为识别模型TEA

    南京大学MCG group/腾讯PCG 提出可以进行用于时序建模的轻量级行为识别模型TEA

    Amusi
  • JDK13 GA发布:5大特性解读

    350: Dynamic CDS Archives351: ZGC: Uncommit Unused Memory353: Reimplement the Le...

    zhisheng
  • 00后:移动互联网崛起新势力

    00后赶上了互联网爆发的时代,成为网络原住民。已形成了一批不可忽视的用户群体。 ? ? ? ? ? ? ? ? ? ? ? ? ?

    腾讯大数据

扫码关注云+社区

领取腾讯云代金券