Java-第十七部分-NIO和Netty-线程模式、Netty模型和Netty-TCP服务

311 阅读9分钟

NIO和Netty全文

线程模式

  • 目前的线程模式,传统阻塞IO服务模型Reactor(反应器模式)模式

传统阻塞IO服务模型,采用阻塞IO模型获取输入的数据,每个连接需要独立的线程完成数据的输入、业务处理和返回;当并发数很大时,需要创建大量的线程,占用大量系统资源;连接创建后,如果当前线程暂时没有数据可读,阻塞在read操作上,造成线程资源浪费

  • Reactor的三种典型实现,单Reactor单线程单Reactor多线程主从Reactor多线程
  1. 单Reactor单线程,前台接待员和服务员是同一个人,全程服务顾客
  2. 单Reactor多线程,1个前台接待员,多个服务员,接待员只负责接待
  3. 主从Reactor多线程,多个前台接待员,多个服务员
  • Netty线程模式基于主从Reactor多线程模型

Reactor

  • 反应器模式、分发者模式(Dispatcher)、通知者模式(notifier)
  • 基本原理
  1. 基于IO服用模型,多个连接共用一个阻塞对象Selector,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接,当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
  2. 基于线程池复用线程资源,不必为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接业务 image.png
  • 设计理念
  1. Reactor,通过一个或多个输入同时传递给服务处理器的模型,基于事件驱动
  2. 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,分发者
  3. 使用了多路IO复用监听事件,收到事件后,分发给某个线程(进程),网络服务高并发处理的关键 IMG_3A4A631FF29F-1.jpeg
  • 核心组成部分
  1. Reactor,在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件作出反应,类比电话接线员,通过接线员进行转接
  2. Handlers,处理程序执行IO事件要完成的实际事件,真正处理业务的部分;Reactor通过调度适当的处理程序来响应IO事件,处理程序执行非阻塞操作
  • 特点
  1. 响应快,不必为单个同步事件所阻塞,但是本身依然是同步的,单个阻塞,但是可以调用其他的线程处理其他业务
  2. 最大程度避免复杂的多线程及同步问题,避免了多线程/进程的切换开销
  3. 扩展性好,增加Reactor实例,来利用CPU资源
  4. 复用性好,与具体事件处理逻辑无关

单Reactor单线程

  • reactor和handler在一个线程里
  • 群聊系统
  1. accept,接受链接
  2. readData,接受发送的信息,进行群发 IMG_1F91AC1E32BC-1.jpeg
  • 处理过程
  1. Reactor对象通过Select监控客户端请求,收到事件后,通过DIspatch进行分发
  2. 如果建立连接请求事件,则由Acceptor通过Accept处理连接请求,创建Handler对象处理连接完成后的业务
  3. 如果不是建立连接事件,则Reactor分发调用连接对应的Handler来响应
  • 优缺点
  1. 模型简单,没有多线程、进程通信、竞争的问题
  2. 性能不足,只有一个线程,无法完全发挥多核CPU性能,Handler在处理某个连接的业务时,整个进程无法处理其他连接事件
  3. 线程意外终止,或进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
  • 使用场景,适用于客户端的数量有限,业务处理非常迅速,如Redis在业务处理的时间复杂度O1的情况

单Reactor多线程

  • Reactor 通过select监听客户端请求事件,收到事件后,通过dispatch进行分发
  • 如果是建立连接,则有Acceptor通过accept处理连接请求,创建Handler对应该连接,完成连接之后的业务
  • 如果不是连接请求,由Reactor分发调用连接对应的Handler来处理
  • Handler负责响应事件,不做具体的业务处理,通过read读取数据后,分发给Worker线程池的某个线程处理某个业务
  • Worker线程池分配独立的线程,完成真正的业务,同时把结果返回给Handler
  • Handler收到响应后,通过send将结果返回给Client IMG_8CA589540089-1.jpeg
  • 优点,可以充分利用多核CPU处理能力
  • 缺点,多线程数据共享和访问比较复杂;Reactor处理所有的事件的监听和响应,在单线程运行,高并发场景中出现性能瓶颈

主从Reactor多线程

  • Reactor主线程MainReactor对象通过select监听连接事件,收到事件后,通过Acceptor处理连接事件
  • Acceptor处理连接事件后,MainReactor将连接分配给SubReactor
  • SubReactor将连接加入到连接队列进行监听,并创建Handler进行事件处理
  • 当有新事件发生时,SubReactor调用对应的Handler进行处理
  • Handler通过read读取数据,分配给Worker线程池分配的线程进行业务处理,并返回处理
  • Handler收到响应的结果后,通过send将结果返回给Client
  • Reactor主线程可以对应多个Reactor子线程,即MainReactor可以关联多个SubReactor IMG_873824C3145A-1.jpeg
  • Doug Lea,Scalable IO in Java IMG_6FA20C6FEEAE-1.jpeg
  • 优点,父线程与子线程交互简单,职责明确,父线程只需要接受新连接,子线程完成后续的业务处理
  • 缺点,编程复杂读高
  • 实际应用,Nginx、Memcache、Netty

Netty模型

简单版

  • BossGroup维护Selector,只关心Accept
  • 当接收到Accept事件,获取对应的SocketChannel,封装成NIOSocketChannel,并注册到Worker线程的Selector(事件循环)
  • Wroker线程监听到Selector中注册的通道,发生感兴趣的事件,进行处理,分配给Handler,已经加入到通道中 image.png

进阶版

  • 主从Reactor image.png IMG_25CFED705D93-1.jpeg

详细版

  • Netty抽象出两组线程池,BossGroup专门负责接受客户端的连接,WorkerGroup专门负责网络的读写
  • BossGroupWorkerGroup类型为NioEventLoopGroup,相当于事件循环组,这个组中还有多个时间循环,每一个事件循环为NioEventLoop
  • NioEventLoop,表示一个不断循环的执行处理任务的线程,每个都有一个Selector,用于监听绑定在其上的Socket的网络通讯
  • NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由IO线程NioEventLoop负责
  • NioEventLoopGroup含有多个线程,即含有多个NioEventLoop
  1. 每个NioEventLoop中包含一个Selector,一个taskQueue
  2. 每个NioEventLoopSelector可以注册监听多个NioChannel
  3. 每个NioChannel只会绑定在唯一的NioEventLoop
  4. 每个NioChannel都绑定一个自己的ChannelPipeline
  • 每个Boss NioEventLoop执行步骤
  1. 轮询accept事件
  2. 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个Worker NioEventLoop上的Selector
  3. 处理任务队列的其他任务,即runAllTasks
  • 每个Work NioEventLoop执行步骤
  1. 轮询read/write事件
  2. 处理IO事件,即read/write事件,在对应的NioSocketChannel上进行处理
  3. 处理任务队列的其他任务,即runAllTasks
  • 每个Work NioEventLoop在处理业务时,会使用pipeline 管道,其中包含了Channel,通过pipeline可以获取到对应的通道,管道中维护了很多的处理器,通过处理器进行业务处理 IMG_BB841DB05AFC-1.jpeg

Netty-TCP服务

案例

Client

public class Client {
    public static void main(String[] args) throws InterruptedException {
        //一个事件循环组即可
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            //客户端使用Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group) //设置线程组
                    .channel(NioSocketChannel.class) //设置通道实现类
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new ClientHandler()); //加入自定义处理器
                        }
                    });
            System.out.println("client is ready");
            //启动客户端连接
            ChannelFuture cf = bootstrap.connect("localhost", 8888).sync();
            //监听关闭通道
            cf.channel().closeFuture().sync();
        } finally {
            group.shutdownGracefully();
        }
    }
}

ClientHandler

//inboud 入栈
public class ClientHandler extends ChannelInboundHandlerAdapter {
    //当通道就绪时触发
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("client: " + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, server", CharsetUtil.UTF_8));
    }
    //当通道有读取事件时,触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf  = (ByteBuf) msg;
        System.out.println("msg from server: " + buf.toString(CharsetUtil.UTF_8));
        System.out.println("server address: "+ ctx.channel().remoteAddress());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

Server

public class Server {
    public static void main(String[] args) throws InterruptedException {
        //创建BossGroup WorkerGroup,都是无限循环
        //处理连接请求
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //处理业务
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //创建服务器端的启动对象,配置启动参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            //链式编程
            bootstrap.group(bossGroup, workerGroup) //设置两个线程组
                    .channel(NioServerSocketChannel.class) //设置BossGroup的通道类型
                    .option(ChannelOption.SO_BACKLOG, 128) //设置线程队列等待连接的个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() { //创建通道初始化对象
                        @Override
                        //给pipeline设置处理其
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //返回channel对应的pipeline,并增加处理器
                            ch.pipeline().addLast(new ServerHandler());
                        }
                    }); //设置workerGroup中的EventLoop对应的管道的处理器
            System.out.println("server is ready");
            //绑定一个端口,并且同步执行,启动服务器
            ChannelFuture cf = bootstrap.bind(8888).sync();
            //对关闭通道进行监听,异步监听
            cf.channel().closeFuture().sync();
        } finally {
            //关闭
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
}

ServerHandler

//自定义handler 需要继承netty规定好的HandlerAdapter,自定义的Handler才能成为一个Hanlder
public class ServerHandler extends ChannelInboundHandlerAdapter {
    //读取客户端发送的消息
    //ChannelHandlerContext 上下文对象,包含 管道、通道、地址
    //Object msg 客户端发送的数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("server ctx: " + ctx);
        //将msg转换成ByteBuf,netty提供的
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("client msg: " + buf.toString(CharsetUtil.UTF_8));
        System.out.println("client address: " + ctx.channel().remoteAddress());
    }
    //数据读取完毕后执行
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //write + flush 将数据写入到缓存,并刷新
        //发送的数据需要编码
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, client", CharsetUtil.UTF_8));
    }
    //处理异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //关闭通道
        cause.printStackTrace();
        ctx.close();
    }
}

分析

  • BossGroupWorkerGroup包含的NioEventLoop子线程个数,默认为系统CPU核数*2,由构造函数追入 image.png image.png image.png
  • WorkerGroup根据子线程数循环分配,加入当前子线程数为8,并且有9个客户端访问,前八个客户端依次对应一个子线程,第九个重新分配给第一个
  • 手动设置线程数
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
  • EventLoop包含的信息,每个子线程都有自己selector,进行事件循环 image.png
  • ctx的信息,涉及到出站入站的操作,利用管道实现流式操作 image.png
  • pipeline实现上为双向链表 image.png