• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

NIO - IO多路复用

武飞扬头像
Heloise_yangyuchang
帮助3

NIO - IO多路复用

一、概述

一个通俗例子读懂BIO、NIO、AIO

  • 小明去餐厅,就这样在那里排队,等了一小时,然后才开始吃饭。(同步阻塞-BIO)
  • 小红也去餐厅,她一看要等挺久的,于是去逛会商场,每次逛一下,就跑回来看看,是不是轮到她了。于是最后她既购了物,又吃了饭。(同步非阻塞-NIO)
  • 小华一样,去餐厅,由于他是高级会员,所以店长说,你去商场随便逛会吧,等下有位置,我立马打电话给你。于是小华不用干巴巴坐着等,也不用每过一会儿就跑回来看有没有等到,最后也吃了饭(异步非阻塞-AIO)

如果这个餐厅同时有100位客人到店,当然到店后第一件要做的事情就是点菜。但是问题来了,餐厅老板为了节约人力成本目前只有一位大堂服务员拿着唯一的一本菜单等待客人进行服务。

  • 那么最笨(但是最简单)的方法是(方法A),无论有多少客人等待点餐,服务员都把仅有的一份菜单递给其中一位客人,然后站在客人身旁等待这个客人完成点菜过程。在记录客人点菜内容后,把点菜记录交给后堂厨师。然后是第二位客人。。。。然后是第三位客人。很明显因为随后的80位客人,再等待超时后就会离店(还会给差评)。
  • 于是还有一种办法(方法B),老板马上新雇佣99名服务员,同时印制99本新的菜单。每一名服务员手持一本菜单负责一位客人(关键不只在于服务员,还在于菜单。因为没有菜单客人也无法点菜)。在客人点完菜后,记录点菜内容交给后堂厨师(当然为了更高效,后堂厨师最好也有100名)。这样每一位客人享受的就是VIP服务咯,当然客人不会走,但是人力成本可是一个大头哦(亏死你)。
  • 另外一种办法(方法C),就是改进点菜的方式,当客人到店后,自己申请一本菜单。想好自己要点的才后,就呼叫服务员。服务员站在自己身边后记录客人的菜单内容。将菜单递给厨师的过程也要进行改进,并不是每一份菜单记录好以后,都要交给后堂厨师。服务员可以记录号多份菜单后,同时交给厨师就行了。那么这种方式,对于老板来说人力成本是最低的;对于客人来说,虽然不再享受VIP服务并且要进行一定的等待,但是这些都是可接受的;对于服务员来说,基本上她的时间都没有浪费,基本上被老板压杆了最后一滴油水。

客人: 客户端请求
点餐内容: 客户端发送的实际数据
老板: 操作系统
人力成本: 系统资源
菜单: 文件状态描述符。
操作系统对于一个进程能够同时持有的文件状态描述符的个数是有限制的。
服务员: 操作系统内核用于IO操作的线程(内核线程)
厨师: 应用程序线程(当然厨房就是应用程序进程咯)
餐单传递方式: 包括了阻塞式和非阻塞式两种。
方法A: 同步IO
方法B: 使用线程进行处理的 同步IO
方法C: 多路复用IO 典型的多路复用IO实现

二、多路复用IO

目前主流到的多路复用IO实现主要包括三种: select、poll、epoll。一些重要特性的比较:
学新通

1、传统IO模型:

学新通

  • 每个客户端连接到达之后,服务端会分配一个线程给该客户端,该线程会处理包括读取数据,解码,业务计算,编码,以及发送数据整个过程;
  • 同一时刻,服务端的吞吐量与服务器所提供的线程数量是呈线性关系的。

这种设计模式在客户端连接不多,并发量不大的情况下是可以运行得很好的,但是在海量并发的情况下,这种模式就不行了。这种模式主要存在的问题有如下几点:

  • 服务器的并发量对服务端能够创建的线程数有很大的依赖关系,但是服务器线程却是不能无限增长的;
  • 服务端每个线程不仅要进行IO读写操作,而且还需要进行业务计算;
  • 服务端在获取客户端连接,读取数据,以及写入数据的过程都是阻塞类型的,在网络状况不好的情况下,这将极大的降低服务器每个线程的利用率,从而降低服务器吞吐量。

2、Reactor事件驱动模型(单线程Reactor)

学新通
在Reactor模型中,主要有四个角色:客户端连接,Reactor,Acceptor和Handler。这里Acceptor会不断地接收客户端的连接,然后将接收到的连接交由Reactor进行分发,最后有具体的Handler进行处理。改进后的Reactor模型相对于传统的IO模型主要有如下优点:

  • 如果仅仅还是只使用一个线程池来处理客户端连接的网络读写,以及业务计算,那么Reactor模型与传统IO模型在效率上并没有什么提升。但是Reactor模型是以事件进行驱动的,其能够将接收客户端连接、网络读和网络写,以及业务计算进行拆分,从而极大的提升处理效率;
  • Reactor模型是异步非阻塞模型,工作线程在没有网络事件时可以处理其他的任务,而不用像传统IO那样必须阻塞等待。

3、Reactor模型----业务处理与IO分离(多线程Reactor)

在上面的Reactor模型中,由于网络读写和业务操作都在同一个线程中,在高并发情况下,这里的系统瓶颈主要在两方面:

  • 高频率的网络读写事件处理;
  • 大量的业务操作处理;

基于上述两个问题,这里在单线程Reactor模型的基础上提出了使用线程池的方式处理业务操作的模型。如下是该模型的示意图:
学新通
在多线程进行业务操作的模型下,该模式主要具有如下特点:

  • 使用一个线程进行客户端连接的接收以及网络读写事件的处理;
  • 在接收到客户端连接之后,将该连接交由线程池进行数据的编解码以及业务计算。

这种模式相较于前面的模式性能有了很大的提升,主要在于在进行网络读写的同时,也进行了业务计算,从而大大提升了系统的吞吐量。但是这种模式也有其不足,主要在于:

  • 网络读写是一个比较消耗CPU的操作,在高并发的情况下,将会有大量的客户端数据需要进行网络读写,此时一个线程将不足以处理这么多请求

4、Reactor模型----并发读写(多线程主从Reactor)

对于使用线程池处理业务操作的模型,由于网络读写在高并发情况下会成为系统的一个瓶颈,因而针对该模型这里提出了一种改进后的模型,即使用线程池进行网络读写,而仅仅只使用一个线程专门接收客户端连接。如下是该模型的示意图:
学新通

改进后的Reactor模型将Reactor拆分为了mainReactor和subReactor。

  1. mainReactor主要进行客户端连接的处理,
  2. 处理完成之后将该连接交由subReactor以处理客户端的网络读写。
  3. subReactor则是使用一个线程池来支撑的,其读写能力将会随着线程数的增多而大大增加。
  4. 对于业务操作,这里也是使用一个线程池,而每个业务请求都只需要进行编解码和业务计算。

通过这种方式,服务器的性能将会大大提升,在可见情况下,其基本上可以支持百万连接。

三、Netty

目前主流的基于NIO的开源框架(Netty) ,优点:

  • api简单,开发门槛低 功能强大,内置了多种编码、解码功能
  • 与其它业界主流的NIO框架对比,netty的综合性能最优
  • 社区活跃,使用广泛,经历过很多商业应用项目的考验 定制能力强,可以对框架进行灵活的扩展
 
public class NettyServer {

    private static int HEADER_LENGTH = 4;

    public void bind(int port) throws Exception {

        ServerBootstrap b = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(),
                                                                                  Executors.newCachedThreadPool()));

        // 构造对应的pipeline
        b.setPipelineFactory(new ChannelPipelineFactory() {

            public ChannelPipeline getPipeline() throws Exception {
                ChannelPipeline pipelines = Channels.pipeline();
                pipelines.addLast(MessageHandler.class.getName(), new MessageHandler());
                return pipelines;
            }
        });
        // 监听端口号
        b.bind(new InetSocketAddress(port));
    }

    // 处理消息
    static class MessageHandler extends SimpleChannelHandler {

        public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
            // 接收客户端请求
            ChannelBuffer buffer = (ChannelBuffer) e.getMessage();
            String message = new String(buffer.readBytes(buffer.readableBytes()).array(), "UTF-8");
            System.out.println("<服务端>收到内容="   message);

            // 给客户端发送回执
            byte[] body = "服务端已收到".getBytes();
            byte[] header = ByteBuffer.allocate(HEADER_LENGTH).order(ByteOrder.BIG_ENDIAN).putInt(body.length).array();
            Channels.write(ctx.getChannel(), ChannelBuffers.wrappedBuffer(header, body));
            System.out.println("<服务端>发送回执,time="   System.currentTimeMillis());

        }
    }

    public static void main(String[] args) {
        try {
            new NettyServer().bind(8080);
        } catch (Exception e) {
            e.printStackTrace();
        }
        ;
    }
}
学新通

1、NioEventLoopGroup 是用来处理I/O操作的线程池,Netty对 EventLoopGroup 接口针对不同的传输协议提供了不同的实现,需要实例化两个NioEventLoopGroup,通常第一个称为“boss”,用来accept客户端连接,另一个称为“worker”,处理客户端数据的读写操作。

2、ServerBootstrap 是启动服务的辅助类,有关socket的参数可以通过ServerBootstrap进行设置。

3、这里指定NioServerSocketChannel类初始化channel用来接受客户端请求。

4、通常会为新SocketChannel通过添加一些handler,来设置ChannelPipeline。ChannelInitializer 是一个特殊的handler,其中initChannel方法可以为SocketChannel 的pipeline添加指定handler。

5、通过绑定端口8080,就可以对外提供服务了。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfhhgej
系列文章
更多 icon
同类精品
更多 icon
继续加载