zoukankan      html  css  js  c++  java
  • Java IO 学习(四)BIO/NIO

    本文会尝试介绍Java中BIO与NIO的范例与原理

    使用的模型非常简单:服务器--客户端模型,服务器会将客户端发送的字符串原样发回来。也就是所谓的echo server。

    BIO

    也就是所谓的Socket通信,直接上代码了

    public class BioServer {
        public void go(int port) {
            try (ServerSocket server = new ServerSocket(port)) {
                while (true) {
                    final Socket socket = server.accept();//接受客户端的连接请求
                    new Thread(new Runnable() {//创建线程处理这条连接
                        @Override
                        public void run() {
                            try (BufferedReader in = new BufferedReader(
                                    new InputStreamReader(socket.getInputStream()));
                                    PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
                                String line;
                                while (true) {
                                    if ((line = in.readLine()) == null) {
                                        break;
                                    }
    
                                    out.println(line);
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            BioServer bioServer = new BioServer();
            bioServer.go(9090);
        }
    }

    逻辑不算复杂,先创建一个ServerSocket对象,然后调用accept方法

    accept方法会一直阻塞到有客户端发起连接请求为止,accept的返回值为一个Socket对象,在echo server的场景里,这个Socket对象对应于一个TCP连接,我们可以从这个连接上读取/写入数据。

    我们需要新建一个线程用于处理这个连接,在这个线程里,我们会尝试从Socket对象里读取数据并作出相应的处理。

    但是为什么要把处理线程的逻辑写在一个while循环里并且还要新建一个线程来处理呢?

    这是为了并发处理多个客户端连接,如果依然在主线程里操作这个新建的Socket对象,那么在操作的过程中,服务器就无法响应其他客户端的连接请求了(在当前连接断开之前无法再次执行accept方法)

    这就是一个线程对应一个连接的模式了。但是大家都知道,线程是一种很昂贵的资源,如果与客户端的连接并不活跃(聊天室场景),为每个连接创建一个线程是非常浪费的。

    我在服务器上的测试结果是:最大并发连接数只有1400出头,然后程序就开始抛异常了“java.lang.OutOfMemoryError: unable to create new native thread”。

    虽然认真调节JVM和系统参数肯定能有更好的结果,但是并不能在本质上解决BIO的效率问题。

    NIO

    利用IO多路复用实现的同步非阻塞IO,为用单台服务器处理百万级别的长连接提供了可能。

    先上代码:

    public class NioServer {
        public void go(int port) {
    
            try (Selector selector = Selector.open();
                    ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
    
                serverChannel.configureBlocking(false);//配置为非阻塞模式
    
                serverChannel.socket().bind(new InetSocketAddress(port));
                serverChannel.register(selector, SelectionKey.OP_ACCEPT);//selector监听channel上的accept事件
    
                while (true) {
                    selector.select();//阻塞查看是否有事件被触发
    
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {//如果有事件被触发,则遍历所有事件
                        SelectionKey selectionKey = iterator.next();
                        iterator.remove();//Java采用的是水平触发,如果不移除已经处理的事件,下次select会将此事件立即返回
    
                        if (selectionKey.isValid()) {//如果事件依然有效
                            if (selectionKey.isAcceptable()) {//如果是accept事件,说明有新客户端连接进来了,创建一条新的socket连接
                                ServerSocketChannel serverSocketChannel =
                                        (ServerSocketChannel) selectionKey.channel();
                                SocketChannel socketChannel = serverSocketChannel.accept();
                                socketChannel.configureBlocking(false);//将新的socket连接也配置为非阻塞
                                socketChannel.register(selector,
                                        SelectionKey.OP_READ);//将这个socket上的可读事件也加入到selector的监听事件列表中
                            }
    
                            if (selectionKey.isReadable()) {//如果是read事件,说明有客户端发送过来数据,需要将这些数据回写给客户端
                                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                                ByteBuffer byteBuffer = ByteBuffer.allocate(256);
    
                                int readBytes = socketChannel.read(byteBuffer);//读取数据
                                if (readBytes > 0) {
                                    byteBuffer.flip();
                                    socketChannel.write(byteBuffer);//回写
                                }
                            }
                        }
                    }
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            NioServer nioServer = new NioServer();
            nioServer.go(9090);
        }
    }

    这个代码就比BIO的版本长很多了,理解起来也不容易。我试着逐步阐释一下

    1. 声明一个ServerSocketChannel(注册了一个普通的fd)

    2. 声明一个Selector(相当于注册一个epoll里的epfd)

    3. 将ServerSocketChannel绑定到Selector,只监听accept事件(相当于调用epoll_ctl方法,把epfd与ServerSocketChannel对应的fd关联起来)

    4. 调用Selector的select方法(相当于调用epoll_wait方法,等待所有监听的fd上是否有是事件发生)

    5. select方法返回,遍历Selector.selectedKeys()方法的返回值(相当于epoll_wait方法返回时,遍历方法中的events参数)

    6. 如果触发的事件是accept类型,创建新的SocketChannel,并将这个SocketChannel也注册到Selector上,关联read事件(相当于新建一个fd,然后再调用epoll_ctl方法将新的fd与epfd关联起来)

    7. 如果触发的事件是read类型,则从相关的SocketChannel中读取数据并回写(相当于调用recvfrom方法从fd中读取数据,然后写回到fd中)

    可以看出,Java的NIO代码可以完全的对应于epoll的执行逻辑。

    在两台物理机上测试了一下,客户端同时发起5w个长连接(单线程处理的极限了,如果想要处理更多连接需要注册多个epfd分担压力)。

    每个连接以一秒钟的间隔向服务器发送长度大约为20byte的字符串。

    连接全部建立成功,每个请求的平均返回时间小于1ms(偶尔会有2-15ms的波动,由服务器进程的gc操作引起)。服务器的cpu占用是100%(单线程,跑满了一个核)

    也就是说,如果配置得当,在单台服务器上处理百万级别的长连接应该是没有问题的。

    总结

    Java中NIO的效率远高于BIO。

    其原因是NIO利用了Linux系统提供的IO多路复用机制(epoll),让在一个线程中监听多个连接上的事件成为可能。

    与BIO的一个连接对应一个线程的模型相比,NIO减少了新建线程的操作,也减少了线程切换所带来的开销。让单台服务器处理百万级别的长连接成为可能。

    当然,直接使用NIO原生API编程难度较高,幸好市面上已经有许多对NIO做了再次封装的网络库可供使用,这些库不仅降低了学习成本,还修复了NIO中存在的一些bug(比方说JDK1.6以前的Selector.select()方法就存在bug,有可能Selector.select()方法返回,但是selectedKeys中没有事件,这会导致无限循环,直接占满一个core)

    例如著名的Netty与Mina

    后续我还会写系列文章来介绍Netty的使用与原理。

  • 相关阅读:
    ext2 / ext3 结构分析
    怎么解决TortoiseSVN文件夹图标不显示?
    CVS Update后,p u 各代表什么意思? 颜色代表什么意思?
    Oracle Purge和drop的区别
    oracle怎样删除回收站里面的表
    oracle 查询所有表 和视图表
    PLSQL 数据中去掉 字段有空格 回车 换行
    plsql update 字段值 前面增加 字符
    function 通过商品编号 获取商品名称
    远程连接后 Xshell 怎么显示桌面 命令
  • 原文地址:https://www.cnblogs.com/stevenczp/p/7498950.html
Copyright © 2011-2022 走看看