bio与nio的几种实现方式

本文详细介绍了Java中BIO(阻塞IO)、BIO-Plus(多线程改进)、NIO(非阻塞IO)以及NIO-Epoll(高效IO)的原理与实现。通过示例代码展示了各自在处理并发连接时的优缺点,尤其是BIO在高并发下性能瓶颈以及NIO-Epoll如何通过选择器优化连接处理,有效解决c10k问题。
摘要由CSDN通过智能技术生成

在socket 通讯层面,BIO就是阻塞io,也就是说,在socket等待连接事件或者读写事件的发生的过程中,当前线程会一直处于阻塞状态,不能做其他事情;

1、bio

public static void main(String[] args) throws IOException {
 //bio阻塞io,主线程只能处理一个连接,当前连接没有处理完,是不能接收新链接的,这对于并发来讲,是非常不友好的
    ServerSocket serverSocket = new ServerSocket(9000);
    while(true) {
        //此处会阻塞,等待客户端连接
        Socket socket = serverSocket.accept();
        System.out.println("客户端已连接");
        byte[] bytes = new byte[128];
        //此处会阻塞,等待客户端发送数据
        int len = socket.getInputStream().read(bytes);
        System.out.println("客户端发送数据:" + new String(bytes, 0, len));
    }
​
}

2、bio-plus

对于上种情形,可以简单地做出改进,也就是利用多线程把建立连接和处理读写事件分开,这样,二者之间就不会互相影响

public static void main(String[] args) throws IOException {
​
    //加强版bio,利用异步线程去做收发数据,主线程只负责建立连接,这样,即时已连接客户端不发送数据,主线程也不会一直阻塞,可以继续循环接收新的连接;
    // 但是,这样在并发高的情况下,对内存是巨大的消耗;于是,nio就应运而生;
    ServerSocket serverSocket = new ServerSocket(9000);
    while(true) {
        Socket socket = serverSocket.accept();
        System.out.println("客户端已连接");
        new Thread(() -> {
            byte[] bytes = new byte[128];
            int len = 0;
            try {
                len = socket.getInputStream().read(bytes);
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("客户端发送数据:" + new String(bytes, 0, len));
        }).start();
    }
​
}

bio-plus版虽然将建立连接和处理读写事件分开了,但是在高并发的情况下仍然“不堪一击”,并且,这种方式虽然看似是解决了bio的阻塞问题,但是属于治标不治本,它的解决方式只不过是利用空间(开辟新线程)去换取时间而已,实质上还是阻塞IO;

3.Nio-Select

于是,nio非阻塞式IO出现了,如上所示,将通道对象serverSocketChannel设置为非阻塞即可,并且引入了selector选择器(可以认为就是socketList),将建立好的连接对象放到socketList中,每次循环就会将所有的socket连接遍历一遍,处理读写事件。这样,单个线程也可以处理多个连接和事件。

public class NioSelect {
    private static List<SocketChannel> socketList=new ArrayList();
    public static void main(String[] args) throws IOException {
        //nio演示,接收客户端连接和接收数据都不会阻塞;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(9000));
        //设置serverSocketChannel为非阻塞
        serverSocketChannel.configureBlocking(false);
        System.out.println("服务启动");
        while(true) {
            SocketChannel socketChannel = serverSocketChannel.accept();
            if(socketChannel!=null){
                System.out.println("客户端已连接");
                //设置socketChannel为非阻塞
                socketChannel.configureBlocking(false);
                //将客户端连接放到集合中
                socketList.add(socketChannel);
            }
            //遍历所有的socket连接,获取数据;但是这样会有空轮训的问题,就是没有发送数据的连接也会被遍历,不合理
            while(socketList.size()>0){
                for(int i=0;i<socketList.size();i++){
                    ByteBuffer allocate = ByteBuffer.allocate(128);
                    int read = socketList.get(i).read(allocate);
                    if(read>0){
                        System.out.println("客户端发送消息:"+new String(allocate.array()));
                    }else{
                        socketList.remove(i);
                    }
                }
            }
        }
​
​
    }
}

但是,问题仍然存在。比如此时socketList维护了100个连接,只有一个连接向服务端发送了数据,但是,按照代码却需要将着100个连接全部遍历一遍,这显然是不合理的,并且,socketList中可以存放的连接数量也是有限的,无法解决c10k问题。(所谓c10k问题,指的是:服务器如何支持10k个并发连接)

4.Nio-Epoll

public class NioEpoll {
    private static List<SocketChannel> socketList=new ArrayList();
    public static void main(String[] args) throws IOException {
        //nio演示,接收客户端连接和接收数据都不会阻塞;
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(9000));
        //设置serverSocketChannel为非阻塞
        serverSocketChannel.configureBlocking(false);
        //获取selector选择器
        Selector selector = Selector.open();
        //将serverSocketChannel注册到选择器,并且声明需要选择器监听的是accept事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务启动");
        while(true) {
            //选择器开始执行监听,无监听事件发生会一直阻塞,有监听事件发生会跳出阻塞,继续往下执行
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                //判断发生的是什么事件
                //是接收连接事件,就获取新的客户连接,及事件的注册
                if(selectionKey.isAcceptable()){
                    ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
                    SocketChannel socketChannel = channel.accept();
                    System.out.println("客户端连接成功");
                    socketChannel.configureBlocking(false);
                    //监听的是数据读事件
                    socketChannel.register(selector,SelectionKey.OP_READ);
                    //如果是发生数据读事件,则接收数据,并打印
                }else if(selectionKey.isReadable()){
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer allocate = ByteBuffer.allocate(128);
                    int read = channel.read(allocate);
                    if(read>0){
                        System.out.println("客户端发送数据:"+new String(allocate.array(),0,read,"GBK"));
                    }else{
                        System.out.println("客户端已断开连接");
                        channel.close();
                    }
                }
                //将处理过的事件从列表中移除,防止重复处理
                iterator.remove();
​
            }
        }
    }
}

这个版本的nio看起来就又进步了一节,这里用epoll选择器代替了上面的select选择器,epoll选择器内部可以分成两部分,一部分是注册到其上的对象列表,另一个部分是有事件发生的列表,于是,可以想到的就是,相比于select遍历所有,epoll只需要遍历有事件发生的对象即可。

其实,在select和epoll之间还有一个poll选择器,只不过poll与select相比,仅仅是存放的连接对象数量变多了,其他的并没有什么区别。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值