从bio到nio到netty实现原理浅析

Java资料站

共 20513字,需浏览 42分钟

 · 2021-05-23

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

今天记录一下这一块的演变历史~

首先是我们熟悉的bio,利用原生socket进行操作。
BIO

public class SocketServer {
    public static void main(String[] args) {
        try {
            ServerSocket server = new ServerSocket(8888);
            System.out.println("服务器已经启动!");
            // 接收客户端发送的信息
            Socket socket = server.accept();

            InputStream is = socket.getInputStream();
            BufferedReader br = new BufferedReader(new InputStreamReader(is));

            String info = null;
            while ((info = br.readLine()) != null) {
                System.out.println(info);
            }

            // 向客户端写入信息
            OutputStream os = socket.getOutputStream();
            String str = "欢迎登陆到server服务器!";
            os.write(str.getBytes());

            // 关闭文件流
            os.close();
            br.close();
            is.close();
            socket.close();
            server.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

上面是一个最简单的socket单线程服务器实例没,accept()方法阻塞等待请求,请求处理完之后结束程序。大致过程如下所示。

我们可以进一步优化代码,accept()接收请求之后,不在当前线程内部进行io处理,另外开辟新的线程进行io操作

public class Bio {
    public static void main(String[] args) {
            ServerSocket server = new ServerSocket(8888);
            System.out.println("服务器已经启动!");
            // 接收客户端发送的信息
            while(true){
                Socket socket = null;
                try {
                    socket = server.accept();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                new Thread(() ->{
                    try {
                        InputStream is = socket.getInputStream();
                        BufferedReader br = new BufferedReader(new InputStreamReader(is));

                        String info = null;
                        while ((info = br.readLine()) != null) {
                            System.out.println(info);
                        }
                        // 向客户端写入信息
                        OutputStream os = socket.getOutputStream();
                        String str = "欢迎登陆到server服务器!";
                        os.write(str.getBytes());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
    }
}

上面这段代码有了部分优化,主线程进行循环accept(),负责请求接收,工作线程采用新线程或者线程池的方式处理。这种方式的好处是,将io工作委派给新的线程,可以较为及时接收新的请求。缺点也很明显,耗费大量的线程资源,即使使用线程池来进行管理,当大量请求到来,线程池的队列爆满,程序就会崩溃。

NIO
NIO出现解决的bio的缺点问题,我们先看一下网上找滴一个nio的简单例子。

public class Nio {
    // 本地字符集
    private static final String LocalCharSetName = "UTF-8";

    // 本地服务器监听的端口
    private static final int Listenning_Port = 8888;

    // 缓冲区大小
    private static final int Buffer_Size = 1024;

    // 超时时间,单位毫秒
    private static final int TimeOut = 3000;

    public static void main(String[] args) throws IOException {
        // 创建一个在本地端口进行监听的服务Socket信道.并设置为非阻塞方式
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.socket().bind(new InetSocketAddress(Listenning_Port));
        serverChannel.configureBlocking(false);
        // 创建一个选择器并将serverChannel注册到它上面
        Selector selector = Selector.open();
        //设置为客户端请求连接时,默认客户端已经连接上
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            // 轮询监听key,select是阻塞的,accept()也是阻塞的
            if (selector.select(TimeOut) == 0) {
                System.out.println(".");
                continue;
            }
            // 有客户端请求,被轮询监听到
            Iterator<SelectionKey> keyIter = selector.selectedKeys().iterator();
            while (keyIter.hasNext()) {
                SelectionKey key = keyIter.next();
                if (key.isAcceptable()) {
                    SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                    clientChannel.configureBlocking(false);
                    //意思是在通过Selector监听Channel时对读事件感兴趣
                    clientChannel.register(selector, SelectionKey.OP_READ,
                            ByteBuffer.allocate(Buffer_Size));
                }
                else if (key.isReadable()) {
                  
                    SocketChannel clientChannel = (SocketChannel) key.channel();
                    // 接下来是java缓冲区io操作,避免io堵塞
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    buffer.clear();
                    long bytesRead = clientChannel.read(buffer);
                    if (bytesRead == -1) {
                        // 没有读取到内容的情况
                        clientChannel.close();
                    } else {
                        // 将缓冲区准备为数据传出状态
                        buffer.flip();
                        // 将获得字节字符串(使用Charset进行解码)
                        String receivedString = Charset
                                .forName(LocalCharSetName).newDecoder().decode(buffer).toString();
                        System.out.println("接收到信息:" + receivedString);
                        String sendString = "你好,客户端. 已经收到你的信息" + receivedString;
                        buffer = ByteBuffer.wrap(sendString.getBytes(LocalCharSetName));

                        clientChannel.write(buffer);
                        key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                    }
                }

                keyIter.remove();
            }
        }

    }
}


基本操作绑定监听端口,然后就是开启一个selector轮询监听客户端请求。注意,这里当客户端请求上来了,是不会马上和客户端建立连接的(此时客户端发送的请求已经告知selector并注册key,并且等待),用一个key来做记录,在轮询处理中,发现服务器通道有连接key,如果是可接受key事件,ServerSocketChannel.accept(),建立连接(这里就是基于事件通知机制,不再用阻塞accept()等待,而是基于事件通知,再进行accept()),建立连接通道后,通道注册到selector并且关注可读事件,客户端发送数据可以写入到接收端的TCP缓冲区。等到写完之后,客户端通道出现可读事件,isReadable()为true,进行读取TCP缓冲,写入到应用缓冲,使用的是java的缓冲io,程序读取信息到buffer和写返回结果到buffer都是在main线程,都是一个线程,并没有new 新的线程处理io。在把信息写入缓冲区后,才会新建线程进行io操作。大体流程如下。

nio相对bio有啥好的?总结一下。
是基于nio类似swing那种事件驱动机制来连接客户端的。也就是说,bio时,客户端来连接了就是直接连接怼到端口,然后阻塞后续请求,等待上一个连接处理完。nio呢,当有客户端来连接时,不好意思,服务器并不给你直接怼到端口创建连接,而是注册一个key到selector说我是可处理的,然后selector轮询发现,这里有个key是客户端要来的意思(类似于swing的事件源),于是处理客户端的这个请求,设置为读操作,然后建立连接进行io操作,io是利用缓冲区做桥梁,后续再进行new Thread io操作,不耽误selector下一次轮询操作,这种io方式效率很高,在这里不详细说。
这种方式客户端请求来一个记录一个,类似批量操作请求,不用为没一个请求启用新线程进行处理,在一个main里面统统搞定,同时采用非阻塞I/O的通信方式,不要求阻塞等待I/O 操作完成即可返回,从而减少了管理I/O 连接导致的系统开销,大幅度提高了系统性能。
总的来说就是事件驱动模型和非阻塞io的结合使用。

更新:

  1. Nio是面向缓存区操作,在read或者wirte的时候可以不阻塞,例如上面的例子,建立了通道,分配了缓冲区,然后进行read。这个read数据到缓冲区的过程由操作系统控制,相对java程序是异步的。如果java程序read的时候客户端发送的数据还没有完全写入到缓冲区,那么read得到的只是部分数据,这个时候read不阻塞,线程可以处理别的事情,稍后继续读取缓冲区中的数据。

  2. Nio同时还使用到虚拟内存技术,从网卡读取的数据由DMA处理写入到虚拟内存,java用户空间内存映射到虚拟内存地址,相当于可以直接操作读取虚拟内存的数据,这样的好处是在操作系统内存不足的情况也可以读取大文件、数据。同时java用户程序直接操作虚拟内存,也减少了一次数据拷贝【网卡->内核空间->用户空间】,直接操作虚拟内存,相当于零拷贝。如果给通道分配缓冲区时,使用直接内存,还可以更快的读取数据,java程序相当于直接操作内核空间内存,零拷贝且读取快

Netty
终于说到netty了,为什么有netty,首要原因就是:nio使用起来太麻烦
netty保留的nio的特性,进行了封装优化

直接上nettydemo

public class Netty {

    public void start(int port) throws Exception
    {
        ServerBootstrap strap = new ServerBootstrap();
        //主线程
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //从线程
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            strap.group(bossGroup, workerGroup).
                    //主线程监听通道
                    channel(NioServerSocketChannel.class).
                    option(ChannelOption.SO_BACKLOG, 1024).
                    //定义从线程的handler链,责任链模式
                    childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });
            ChannelFuture future=strap.bind(port).sync();
            future.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

    public static void main(String[] args) throws Exception {
        System.out.println("start server");
        new Netty().start(8000);
    }
}

上面是一个netty的server,相比起nio的server简洁了非常多,netty使用Reactor模型,代码中的bossGroup 就是一个线程池,Reactor中扮演接收请求,并且分派任务到工作线程的角色。workerGroup 就是我们的苦力干活的。workerGroup 中的线程调用我们定义好的handler、链进行各种业务处理,bossGroup负责请求接收。处理过程大致如下。

netty怼nio进行了封装,使得我们使用起来极为方便,同时netty里面的selector采用了线程池的方式进行监听客户端请求,就是好多个NioEventLoop,原则上,一个端口就只是用一个NioEventLoop线程来处理客户端请求,线程池的话有什么用我还不知道(可以监听多个端口,在多个服务共用netty时可以起用处?)。。。设置多个boss会不会还是资源浪费?workerGroup中,一个客户端过来的channel就绑定在一个NioEventLoop上,单线程,按顺序执行handler链,channel还绑定了一个selector来处理channel的各种key。


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

本文链接:

https://blog.csdn.net/qq_20597727/article/details/80789272






粉丝福利:Java从入门到入土学习路线图

👇👇👇

👆长按上方微信二维码 2 秒


感谢点赞支持下哈 

浏览 26
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报