BIO到NIO到Netty实现原理

目录

BIO

简单代码实现

代码优化实现

NIO

代码实现

nio相对bio

Netty

1. Netty简介

2. Netty线程模型

3. Reator模式的实现

1.第一种实现模型

2.Reactor多线程模型

3.Reactor主从模型

4.Netty模型

4.Buffer

5.简单代码实现

6.总结


 

BIO

简单代码实现

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

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

1. Netty简介

Netty是一个高性能、异步事件驱动的NIO框架,基于JAVA NIO提供的API实现。它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。 作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。

2. Netty线程模型

在JAVA NIO方面Selector给Reactor模式提供了基础,Netty结合Selector和Reactor模式设计了高效的线程模型。先来看下Reactor模式:首先Reactor模式首先是事件驱动的,有一个或者多个并发输入源,有一个Server Handler和多个Request Handlers,这个Service Handler会同步的将输入的请求多路复用的分发给相应的Request Handler。过程如下:

从结构上有点类似生产者和消费者模型,即一个或多个生产者将事件放入一个Queue中,而一个或者多个消费者主动的从这个队列中poll事件来处理;而Reactor模式则没有Queue来做缓冲,每当一个事件输入到Service Handler之后,该Service Handler会主动根据不同的Evnent类型将其分发给对应的Request Handler来处理。

 

3. Reator模式的实现

1.第一种实现模型

这是最简单的Reactor单线程模型,由于Reactor模式使用的是异步非阻塞IO,所有的IO操作都不会被阻塞,理论上一个线程可以独立处理所有的IO操作。这时Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分发请求到处理链中。

对于一些小容量应用场景,可以使用到单线程模型。但对于高负载,大并发的应用却不合适,主要原因如下:

  1. 当一个NIO线程同时处理成百上千的链路,性能上无法支撑,即使NIO线程的CPU负荷达到100%,也无法完全处理消息
  2. 当NIO线程负载过重后,处理速度会变慢,会导致大量客户端连接超时,超时之后往往会重发,更加重了NIO线程的负载。
  3. 可靠性低,一个线程意外死循环,会导致整个通信系统不可用

为了解决这些问题,出现了Reactor多线程模型。

 

2.Reactor多线程模型

 

相比上一种模式,该模型在处理链部分采用了多线程(线程池)。

在绝大多数场景下,该模型都能满足性能需求。但是,在一些特殊的应用场景下,如服务器会对客户端的握手消息进行安全认证。这类场景下,单独的一个Acceptor线程可能会存在性能不足的问题。为了解决这些问题,产生了第三种Reactor线程模型。

 

3.Reactor主从模型

该模型相比第二种模型,是将Reactor分成两部分,mainReactor负责监听server socket,accept新连接;并将建立的socket分派给subReactor。subReactor负责多路分离已连接的socket,读写网络数据,对业务处理功能,其扔给worker线程池完成。通常,subReactor个数上可与CPU个数等同。

4.Netty模型

Reactor的三种模型,那么Netty是哪一种呢?其实Netty的线程模型是Reactor模型的变种,那就是去掉线程池的第三种形式的变种,这也是Netty NIO的默认模式。Netty中Reactor模式的参与者主要有下面一些组件:

  1. Selector
  2. EventLoopGroup/EventLoop     Reactor模式中的分发(Dispatcher)的角色
  3. ChannelPipeline    请求处理器

Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程EventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。这也解释了为什么Netty线程模型去掉了Reactor主从模型中线程池。

 

4.Buffer

Netty提供的经过扩展的Buffer相对NIO中的有个许多优势,作为数据存取非常重要的一块,我们来看看Netty中的Buffer有什么特点。

1.ByteBuf读写指针

  • 在ByteBuffer中,读写指针都是position,而在ByteBuf中,读写指针分别为readerIndex和writerIndex,直观看上去ByteBuffer仅用了一个指针就实现了两个指针的功能,节省了变量,但是当对于ByteBuffer的读写状态切换的时候必须要调用flip方法,而当下一次写之前,必须要将Buffe中的内容读完,再调用clear方法。每次读之前调用flip,写之前调用clear,这样无疑给开发带来了繁琐的步骤,而且内容没有读完是不能写的,这样非常不灵活。相比之下我们看看ByteBuf,读的时候仅仅依赖readerIndex指针,写的时候仅仅依赖writerIndex指针,不需每次读写之前调用对应的方法,而且没有必须一次读完的限制。

2.零拷贝

  • Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
  • Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
  • Netty的文件传输采用了transferTo方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write方式导致的内存拷贝问题。

3.引用计数与池化技术

  • 在Netty中,每个被申请的Buffer对于Netty来说都可能是很宝贵的资源,因此为了获得对于内存的申请与回收更多的控制权,Netty自己根据引用计数法去实现了内存的管理。Netty对于Buffer的使用都是基于直接内存(DirectBuffer)实现的,大大提高I/O操作的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操作效率高之外还有一个天生的缺点,即对于DirectBuffer的申请相比HeapBuffer效率更低,因此Netty结合引用计数实现了PolledBuffer,即池化的用法,当引用计数等于0的时候,Netty将Buffer回收致池中,在下一次申请Buffer的没某个时刻会被复用。

5.简单代码实现

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

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线程来处理客户端请求,workerGroup中,一个客户端过来的channel就绑定在一个NioEventLoop上,单线程,按顺序执行handler链,channel还绑定了一个selector来处理channel的各种key。

 

6.总结

Netty其实本质上就是Reactor模式的实现,Selector作为多路复用器,EventLoop作为转发器,Pipeline作为事件处理器。但是和一般的Reactor不同的是,Netty使用串行化实现,并在Pipeline中使用了责任链模式。

Netty中的buffer相对有NIO中的buffer又做了一些优化,大大提高了性能。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值