Netty入门和原理架构解析

Netty的基本信息

 

原生NIO的问题存在以下问题:

  • NIO的类库和API比较繁杂,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
  • 需要熟悉Java多线程,因为NIO涉及到Reactor模式,必须对度线程和网络编程熟悉才能编写出高质量的NIO程序
  • 开发工作量和难度比较大,比如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等网络问题
  • JDK NIO存在一些bug,比如臭名昭著的Epoll Bug,它会导致Selector的空轮训,导致导致CPU100%,知道jdk1.7该问题仍然存在,没有根本解决。

 

Netty的介绍

  1. Netty是由JBOSS提供的一个java开源框架,现为github上的独立项目,
  2. Netty是一个异步的,基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络I/O程序
  3. Netty是主要针对TCP协议下面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用
  4. Netty的本质是一个NIO框架,适用于服务器通讯相关的各种应用场景,使用Netty 可以快速和简单的开发出一个网络应用,避免使用复杂繁琐的JDK NIO类库。
  5. Netty对JDK自带的NIO的API进行了封装,解决了NIO中的问题,适用于各种传输类型的同意API阻塞和非阻塞Socket;基于灵活和可扩展的事件模型,高度可定制的线程模型-单线程,一个或多个线程池

 

Netty使用场景

1)互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty 作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。典型的应用有:阿里分布式服务框架 Dubbo 的 RPC 框架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。

2)游戏行业:无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。

非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过 Netty 进行高性能的通信。

3)大数据领域:经典的 Hadoop 的高性能通信和序列化组件 Avro 的 RPC 框架,默认采用 Netty 进行跨界点通信,它的 Netty Service 基于 Netty 框架二次封装实现。

Netty可以帮助快速简单的开发出一个网络应用,相当于简化和流程化NIO的开发,Netty是目前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域,游戏行业,通信行业获得广泛的应用。

 

Netty的官方介绍

地址:https://netty.io/

Netty is an asynchronous event-driven network application framework
for rapid development of maintainable high performance protocol servers & clients.

从上图可以看出:

核心:可扩展的事件模型、常用的通信API、支持零拷贝字节缓冲
协议支持:webSocket长连接,gzip压缩、大文件传送、Protobuf编解码、实时流传输协议RTSP、二进制协议、文本协议、HTTP等
传输服务:传统阻塞BIO socket传输、非阻塞NIO socket传输、非阻塞NIO socket传输
容器集成:Spring、JbossMC、OSGI、Guice

 

Netty的高性能设计

Netty 作为异步事件驱动的网络,高性能之处主要来自于其 I/O 模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据。常用的线程模型主要包括:

  • 传统BIO的线程模型

  • Reactor线程模型

传统阻塞BIO线程模型

传统BIO线程模型图

传统BIO线程模型特点和问题

  • 采用阻塞I/O模式获取输入的数据,即:连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。
  • 每个连接(请求)都要独立的线程完成数据 Read(输入),业务处理,数据 Write (返回),当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。

 

Reactor 线程模型

针对传统阻塞I/O服务模型的2个缺点,Reactor的解决方案:

  • 基于I/O复用模型,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接,当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
  • 基于线程池服用线程资源,不必在为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接任务

Reactor模型图

Reactor是反应器的意思,Reactor模式是通过一个或多个输出同时传递给服务处理器ServiceHandler(基于事件驱动),服务器端程序处理传入的多个请求并将它们同步分发给不同处理线程,因此Reactor模式也叫Dispatch模式,Reactor模式使用了I/O多路复用监听事件,收到事件后分发给某个线程(进程),这点就是网络服务高并发的关键


Reactor 线程模型中有 2 个关键组成:
1)Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。
2)Handlers:处理程序执行 I/O 事件要完成的实际事件,Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

Reactor线程模型的分类
根据Reactor的数量和处理资源池线程的数量不通,主要分为3种典型的实现

  • 单Reactor单线程
  • 单Reactor多线程
  • 主从Reactor多线程(Netty)

单Reactor单线程模式

select是前面I/O复用模型介绍的标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求,Reactor对象通过Select监控客户端请求事件,收到事件后通过Dispatch进行分发,如果是建立连接请求事件,则由Accept通过accept方法处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理,如果不是建立连接事件则Reactor会分发调用连接对应的Handler来响应,Handler会完成Read-业务处理-write的完整的业务流程,具体的流程图解如下:

单Reactor单线程模式下,Reactor通过select方法获取客户端就绪的事件,并dispatch给响应的Handler进行读取和响应,整个过程都在一个线程中。这样的缺点显而易见:当客户端连接过多时候,单线程就会造成阻塞和性能瓶颈。无法发挥多核CPU的性能,可靠性差,线程意外终止或者进入死循环则整个通信模块不可用,出现节点故障。而单线程的优点就是:模型简单,没有通信,线程切换、竞争等问题。

单Reactor多线程

  1. Reactor对象通过select监控客户端请求事件,收到事件后通过dispatch进行分发
  2. 如果是建立连接请求则由Acceptor通过accept方法处理连接请求,然后创建一个Handler对象处理完成连接后的各种事件
  3. 如果不是连接请求,则由Reactor分发调用连接对应的Handler来处理
  4. Handler只负责响应事件不做具体的业务处理,通过read读取数据后分发给后面的worker线程池,由worker线程池的某个线程处理业务等耗时的逻辑
  5. worker线程池会分配独立的线程处理真正的业务,并将结果返回给Handler,,Handler收到响应后,通过send方法返回给client

具体的业务流程图示如下:

单Reactor多线程模式下,改变了读写和业务处理的单线程瓶颈,将业务处理逻辑抽取出交给线程池处理,提升了整体的性能,可以发挥多核CPU的处理能力,但是对于Reactor来说,Reactor在单线程运行中处理所有的事件的监听和响应,在高并发场景容易造成性能瓶颈。同样,多线程处理业务处理过程中的数据共享和访问比较复杂,线程上下文之间的切换。

 

主从Reactor多线程

  1. 基于单Reactor的单线程的操作的瓶颈,主从Reactor将可以将Reactor的任务分散,MainReactor主线程通过select监听连接事件,收到事件后,通过Acceptor处理连接事件
  2. 当Acceptor处理连接事件后,MainReactor主线程将连接分配给SubReactor子线程,SubReactor子线程将连接加入到连接队列,同时创建Handler进行各种事件处理,
  3. 当有新的事件发生,SubReactor子线程就会调用对应的Handler进行处理,Handler通过read读取数据后分发给后面的worker线程池,worker线程池分配worker线程进行业务处理并返回结果
  4. Handler收到结果后,在通过send方法,将结果返回给client
  5. 注意:MainReactor主从模式,Reactor主线程可以对应多个Reactor子线程,即Reactor主线程可以关联多个子Reactor线程。

Netty 主要基于主从 Reactors 多线程模型(如上图)做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor:

1)MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor;

2)SubReactor 负责相应通道的 IO 读写请求;

3)非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。

引用 Doug Lee 大神的 Reactor 介绍——Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:

主从Reactor的优缺点

优点:父线程和子线程的数据交互简单职责明确,父线程只需要接受新连接,子线程完成后续的业务处理;父线程与子线程的数据交互简单,Reactor主线程主需要把新连接传给子线程,子线程无需返回数据
缺点:编程复杂度高
结合实例:这种模型在许多项目中广泛使用,包括Nginx主从Reactor多进程模型,Memcached主从多线程,Netty主从多线程模型的支持

Reactor线程模型的总结

单Reactor单线程:前台接待员和服务员是同一个人,全程为顾客服务
单Reactor多线程:1个前台接待员,多个服务生,接待员只负责接待
主从Reactor多线程:多个前台接待员,多个服务生

Reactor线程模型的优点

  • 模式响应快,不比为单个同步事件所阻塞,虽然Reactor本身依然是同步的
  • 可以最大的程度避免复杂的多线程同步问题,避免了多线程/进程切换开销
  • 扩展性好,可以方便的通过增加Reactor实例个数来充分利用CPU资源
  • 复用性好,Reactor模型本身与具体的事件处理逻辑无关,具有很高的复用性

 

Netty的线程模型和工作原理

Netty的线程模型不是一成不变的,它实际取决于用户的启动参数配置,通过设置不同的启动参数(主要是通过调整线程池的线程个数,是否共享线程池等方式),Netty可以支持Reactor单线程模型、多线程模型和主从Reactor多线程模型,如下:

Netty中引入了NioEventLoopGroup,服务端在启动的时候,创建两个NioEventLoopGroup,实际上是两个独立的Reactor线程池,基本的过程如下:

  • 初始化创建 2 个 NioEventLoopGroup,其中 boosGroup 用于 Accetpt 连接建立事件并分发请求(接受客户端TCP请求),workerGroup 用于处理 I/O 读写事件和业务逻辑(包括执行系统Task,定时任务Task)。

  • 基于 ServerBootstrap(服务端启动引导类),配置 EventLoopGroup、Channel 类型,连接参数、配置入站、出站事件 handler。

  • 绑定端口,开始工作

Netty用于接受客户端请求的线程池职责:

(1)接受客户端TCP连接,初始化Channel参数

(2)将链路状态变更事件通知给ChannelPipeline

Netty用于接受I/O操作的Reactor的线程池职责:

(1)异步读取通信对端的数据报,发送读事件到ChannelPipeline

(2)异步发送消息到通信对端,调用ChannelPipeline的消息发送接口

(3)执行系统调用Task

(4)执行定时任务Task,例如链路空闲状态监测定时任务

为了尽可能的提升性能,Netty在很多地方采用无锁化的设计,在I/O线程内部进行串行化操作,workerGroup中的线程串行化的处理I/O操作和业务处理,通过调整NIO线程池的线程数量,这种无锁化的串行线程设计相比一个队列多个工作线程的模型性能更优。Netty的NioEventLoop读写消息之后,直接调用ChannelPipelline的fireChannelRead(Object msg),只要用户不主动切换线程,一直都是由NioEventLoop调用用户的Handler,期间不进行线程切换,串行化处理避免多线程操作的锁竞争。

Netty的工作架构图如下:

Netty抽象出两组线程池: BoosGroup 和WorkerGroup ,它们的类型都是NioEventLoopGroup,NioEventLoopGroup相当于一个事件循环组,组内包含多个事件循环(即多个线程),每个事件循环就是NioEventLoop(包含多个NioEventLoop)。

NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个selector,一个taskQueue,其中selector用于监听绑定在其上的socket网络通道,taskQueue是用来处理异步任务的任务队列,NioEventLoop内部采用串行化设计,从消息的读取->解码->编码->发送,始终由I/O线程NioEventLoop负责。

每个NioEventLoop的selector上可以注册监听多个NioChannel,每个NioChannel只会绑定在唯一的NioEventLoop上,每个NioEventLoop都绑定有一个自己的ChannelPipeline。

每个 Boss Group中的 NioEventLoop 循环执行的任务包含 3 步:

  • 轮询 Accept 事件。

  • 处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上。

  • 处理任务队列中的任务。包括系统Task和定时任务

系统Task

通过调用NioEventLoop的execute(Runnable task)方法实现,Netty有很多系统Task,创建它们的主要原因是:当I/O线程和用户线程同事操作网络资源时,为了防止并发操作导致的锁竞争,将用户线程的操作封装成Task放到消息队列中,由I/O线程负责执行,这样就实现了局部无锁化。

定时任务

通过调用NioEventLoop的schedule(Runnable command, long delay, TimeUnit unit)方法实现

每个 Worker NioEventLoop 循环执行的任务包含 3 步:

  • 轮询 Read、Write 事件

  • 处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理。

  • 处理任务队列中的任务,runAllTasks,系统任务和定时任务,同上。

 

Netty的开发入门

开发前引入Netty的maven依赖,目前主流的是Netty 4.xx的版本

<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.1.16.Final</version>
</dependency>

Netty入门的服务端开发

public class SimpleNettyServer {
    public static void main(String[] args) throws InterruptedException {
        /**
         * 创建两个线程组:BossGroup和WorkerGroup,两个都是无限循环
         * NioEventLoopGroup含有自己的selector和taskQueue
         * BossGroup和WorkerGroup 含有子线程(NioEventLoop)的个数可以在创建NioEventLoopGroup可以指定线程数,
         * 不传递默认为0,此时系统初始化是系统CPU核心线程数
         */
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();//处理连接请求
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();//处理I/O 读写事件和业务逻辑
        try {
            //ServerBootstrap对象是Netty用于启动NIO服务端的辅助启动类,设置启动参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)//设置两个线程组
                    .channel(NioServerSocketChannel.class)//设置服务器的通道类型:NioServerSocketChannel
                    .option(ChannelOption.SO_BACKLOG, 1024)//设置NioServerSocketChannel的TCP参数,线程队列等待连接的个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true)//设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() {//绑定I/O事件处理类
                        //给pipeline设置处理器
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyServerHandler());
                        }
                    });//给我们workGroup的eventLoop的对应的管道设置处理器
            ChannelFuture channelFuture = bootstrap.bind(9911).sync();//绑定监听端口并调用同步阻塞方法等待绑定操作完成
            channelFuture.channel().closeFuture().sync();//等待服务器链路关闭之后main函数才退出
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    //自定义一个Handler继续Netty规定的某个Handler适配器
   static class NettyServerHandler extends ChannelInboundHandlerAdapter{
       /**
        * ChannelHandlerContext 上下文对象,含有管道pipeline,通道channel,其中管道种含有通道的引用,通道中含有管道的应用
        * pipeline的本质就是一个双向链表
        * Object msg :就是客户端发送的数据,默认Object
        *
        * channelRead: 是读取通道,在客户端发送消息后通过该方法接受通道种内容
        */
       @Override
       public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
           System.out.println("Server ctx: " +ctx);
           //将msg转成一个ByteBuf,Netty提供的,不是Nio种的ByteBuffer
           ByteBuf buf = (ByteBuf)msg;
           System.out.println("客户端发送消息是:"+buf.toString(CharsetUtil.UTF_8));
           System.out.println("客户端地址:" +ctx.channel().remoteAddress());
       }

       /**
        * 服务端读取完成后执行,响应给客户端
        */
       @Override
       public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
           ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端", CharsetUtil.UTF_8));
       }

       /**
        * 遇到异常时关闭上下文
        */
       @Override
       public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
           ctx.close();
       }
   }
}

Netty入门的客户端开发

public class SimpleNettyClient {

    public static void main(String[] args) throws InterruptedException {
        //客户端需要一个事件循环组
        NioEventLoopGroup loopGroup = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap();//客户端使用BootStrap
            bootstrap.group(loopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new NettyClientHandler());
                        }
                    });
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 9911);//连接服务端
            channelFuture.channel().closeFuture().sync();
        }finally {
            loopGroup.shutdownGracefully();
        }

    }
    static class NettyClientHandler extends ChannelInboundHandlerAdapter{
        /**
         * 当通道就绪就会触发该方法,发送消息给服务器端(写入通道)
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("Server ctx: " +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("服务器回复的消息是:"+buf.toString(CharsetUtil.UTF_8));
            System.out.println("服务器地址:" +ctx.channel().remoteAddress());
        }

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

分别启动服务端和客户端执行结果如下:

客户端发送消息是:Hello, server
客户端地址:/127.0.0.1:54995

服务器回复的消息是:hello,客户端
服务器地址:/127.0.0.1:9911

Netty的任务队列

Netty的NioEventLoop 循环执行的第三步就是处理任务队列中所有的task,Netty在每一个事件循序中维护了一个任务队列,可以将业务处理过程中一些耗时的处理放到任务队列中异步执行,从而提交整个过程的效率,如之前的读取客户端消息前一些耗时的操作:

static class NettyServerHandler extends ChannelInboundHandlerAdapter{
      
       @Override
       public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        

           Thread.sleep(10000);
           ctx.writeAndFlush(Unpooled.copiedBuffer("hello,我的耗时任务完成了", CharsetUtil.UTF_8));
           System.out.println("服务端任务完成..");
           ByteBuf buf = (ByteBuf)msg;
           System.out.println("客户端发送消息是:"+buf.toString(CharsetUtil.UTF_8));
           System.out.println("客户端地址:" +ctx.channel().remoteAddress());
       }

可以通过以下3个方法实现:

①用户程序自定义的普通任务

②用户自定义定时任务

③非当前 Reactor 线程调用 Channel 的各种方法

代码如下:

 bootstrap.group(bossGroup, workerGroup)//设置两个线程组
                    .channel(NioServerSocketChannel.class)//设置服务器的通道类型:NioServerSocketChannel
                    .option(ChannelOption.SO_BACKLOG, 1024)//设置NioServerSocketChannel的TCP参数,线程队列等待连接的个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true)//设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() {//绑定I/O事件处理类
                        //给pipeline设置处理器
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            /**
                             * 方法3:可以使用集合管理SocketChannel,在推送消息时可以将业务加入到各个Channel对应的NioEventLoop的taskQueue
                             * 或者ScheduleTaskQueue,每个客户端对应的socketChannel都是不一样的
                             * socketChannel.eventLoop().execute(new Runnable{ todo });
                             */
                            System.out.println("客户的channel的hashCode" +socketChannel.hashCode());
                            socketChannel.pipeline().addLast(new NettyServerHandler());
                        }
                    });//给我们workGroup的eventLoop的对应的管道设置处理器
            ChannelFuture channelFuture = bootstrap.bind(9911).sync();//绑定监听端口并调用同步阻塞方法等待绑定操作完成
            channelFuture.channel().closeFuture().sync();//等待服务器链路关闭之后main函数才退出
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
    //自定义一个Handler继续Netty规定的某个Handler适配器
   static class NettyServerHandler extends ChannelInboundHandlerAdapter{
       /**
        * ChannelHandlerContext 上下文对象,含有管道pipeline,通道channel,其中管道种含有通道的引用,通道中含有管道的应用
        * pipeline的本质就是一个双向链表
        * Object msg :就是客户端发送的数据,默认Object
        *
        * channelRead: 是读取通道,在客户端发送消息后通过该方法接受通道种内容
        */
       @Override
       public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
           //这里有个耗时的业务处理,可以利用taskQueue实现异步执行,提交到该channel对应的NioEventLoop的taskQueue
           //方法1:用户程序自定义普通任务,把任务提交到taskQueue
           ctx.channel().eventLoop().execute(new Runnable() {
               @Override
               public void run() {
                   try {
                       Thread.sleep(5000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   ctx.writeAndFlush(Unpooled.copiedBuffer("hello,我的耗时任务1完成了", CharsetUtil.UTF_8));
               }
           });
           ctx.channel().eventLoop().execute(new Runnable() {
               @Override
               public void run() {
                   try {
                       Thread.sleep(5000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   ctx.writeAndFlush(Unpooled.copiedBuffer("hello,我的耗时任务2完成了", CharsetUtil.UTF_8));
               }
           });
            //方法2:用户程序自定义定时任务,把任务提交到scheduleTaskQueue
           ctx.channel().eventLoop().schedule(new Runnable() {
               @Override
               public void run() {
                   try {
                       Thread.sleep(5000);
                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   }
                   ctx.writeAndFlush(Unpooled.copiedBuffer("hello,我的耗时任务3完成了", CharsetUtil.UTF_8));
               }
           },5, TimeUnit.SECONDS);
            //耗时操作
            //Thread.sleep(10000);
            //ctx.writeAndFlush(Unpooled.copiedBuffer("hello,我的耗时任务完成了", CharsetUtil.UTF_8));
           System.out.println("服务端任务完成..");
           ByteBuf buf = (ByteBuf)msg;
           System.out.println("客户端发送消息是:"+buf.toString(CharsetUtil.UTF_8));
           System.out.println("客户端地址:" +ctx.channel().remoteAddress());
       }

其中ChannelOption参数如下:

ChannelOption.SO_BACKLOG :表示对应TCP/IP协议listen函数中的backlog参数,用来初始化服务器可连接队列的大小,服务器端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接放在队列中等待处理,backlog参数指定了队列的大小。

ChannelOption.SO_KEEPALIVE:表示活动连接的数量

 

Netty的异步模型

在Netty中,所有的I/O操作都是异步的,这意味着任何的I/O调用都会立即返回,而不像传统的BIO那样同步等待操作完成。,异步的的结果获取就是通过ChannelFuture这个对象去获取,ChannelFuture有两种状态:uncompleted和completed,当开始一个I/O操作时,创建一个ChannelFuture,此时处于uncompleted状态(非失败、非成功、非取消状态),一旦I/O操作完成,ChannelFuture将会被设置成completed,此时他的结果可能是操作成功、操作失败、操作被取消

ChannelFuture是一个接口,继承Future,主要方法除了Future中的方法还有如下的方法

public interface ChannelFuture extends Future<Void> {
    //获取通道
    Channel channel();
    //添加监听器
    ChannelFuture addListener(GenericFutureListener<? extends Future<? super Void>> var1);
    //添加多个监听器
    ChannelFuture addListeners(GenericFutureListener<? extends Future<? super Void>>... var1);
    //删除监听器
    ChannelFuture removeListener(GenericFutureListener<? extends Future<? super Void>> var1);
    //删除多个监听器
    ChannelFuture removeListeners(GenericFutureListener<? extends Future<? super Void>>... var1);

    ChannelFuture sync() throws InterruptedException;

    ChannelFuture syncUninterruptibly();

    ChannelFuture await() throws InterruptedException;

    ChannelFuture awaitUninterruptibly();

    boolean isVoid();
}

当I/O操作完成后,I/O线程会回调ChannelFuture中的GenericFutureListener的operationComplete方法并把ChannelFuture对象当作方法的入参,一般推荐使用GenericFutureListener的回调方法而不是ChannelFuture的get()方法去获取结果,因为:当进行异步I/O操作的时,完成的时间无法预测,如果不设置超时时间,会导致调用线程长时间被阻塞,甚至挂死,如果设置超时时间,时间又无法精确预测,显然利用异步通知机制回调监听器是最佳解决方案,性能最优。

在客户端绑定端口时可以添加监听器,具体如下,当绑定端口操作执行成功后,就会回调监听器中的方法:

            ChannelFuture channelFuture = bootstrap.bind(9911).sync();//绑定监听端口并调用同步阻塞方法等待绑定操作完成
            channelFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture channelFuture) throws Exception {
                    if(channelFuture.isSuccess()){
                        System.out.println("绑定端口成功");
                    }else {
                        System.out.println("绑定端口失败");
                    }
                }
            });

注意不要再ChannelHandler中调用ChannelFuture的await()方法,因为在发起I/O操作后,由I/O线程负责异步通知发起I/O操作的用户线程,如果I/O线程和用户线程是同一个线程,就会导致I/O线程等待自己通知操作完成,这就导致死锁。

此外,Future的相关方法都是读相关的方法并没有写操作相关的接口方法,Netty中引入Promise来解决可写的Future,用于设置I/O操作的结果

 

Netty的核心组件

 

引导类类:Bootstrap和ServerBootstrap

Bootstrap是引导的意思,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中的Bootstrap是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类

引导类类常见API

//应用于客户端,设置一个EventLoopGroup
public B group(EventLoopGroup group)
//应用于服务端,设置两个EventLoopGroup
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) 
//设置客户端和服务端的通道实现
public B channel(Class<? extends C> channelClass) 
//设置客户端和服务端的通道SocketChannel的参数设置
public <T> B option(ChannelOption<T> option, T value) 
//用来给接收到的通道添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) 
//设置业务处理类,即自定义的handler
public ServerBootstrap childHandler(ChannelHandler childHandler)
//给bossGroup添加handler
public B handler(ChannelHandler handler) 
//用于服务端,设置占用的端口号
public ChannelFuture bind(int inetPort)
//用户客户端,用来连接服务器
public ChannelFuture connect(String inetHost, int inetPort) 

Netty的通道Channel

Java中的Channel是JDK的NIO类库的重要组成,就是java.nio.SocketChanneljava.nio.ServerSocketChannel,用于非阻塞的I/O操作,而Netty中的io.netty.channel/Channel是Netty网络操作抽象类

功能包括:网络的读写、客户端发起连接、链路关闭、获取通信双方的网络地址等,它也包含了Netty框架相关的一些功能,如:获取该Channel的EventLoop、获取缓冲分配器ByteBufAllocator和pipeline等。Channel提供了异步的网络I/O操作(建立连接,读写,绑定端口),调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以监听I/O操作成功失败或者取消的回调。此外不同的协议不同的阻塞类型的连接都有不同的Channel类型与之对应,常用的Channel类型:

  • NioSocketChannel:异步的客户端TCP socket连接
  • NioServerSocketChannel:异步的服务器端TCP socket连接
  • NioDatagramChannel:异步的UDP socket连接
  • NioSctpChannel:异步的客户端Sctp 连接
  • NioSctpServerChannel:异步的服务器端Sctp连接

Channel是Netty抽象出来的网络I/O读写相关的接口,为什么不直接使用JDK NIO原生的Channel呢?主要原因如下:

  • JDK的SocketChannel和ServerSocketChannel没有统一的Channel接口供业务开发者使用。
  • JDK的SocketChannel和ServerSocketChannel主要职责是网络I/O操作,它们是SPI类接口,由具体的虚拟机厂家提供,通过继承SPI功能类扩展难度大
  • Netty的Channel需要能够和Netty整体架构融合,例如I/O模型,基于ChannelPipeline的定制模型以及元数据描述配置化的TCP参数等,JDK的SocketChannel和ServerSocketChannel都没有提供

Selector

Netty的Selector对象实现I/O多路复用,通过Selector一个线程可以监听多个连接的Channel事件,当向一个Selector中注册Channel后Selector内部的机制就可以自动不断地轮训(select())这些注册的Channel是否有已就绪的I/O事件(如可读可写、网络连接完成等),这样程序就可以很简单的使用一条线程高效的管理多个Channel。

ChannelHandler

ChannelHandler类似于Servlet的Filter的过滤器,负责对I/O事件或者I/O操作进行拦截处理,可以选择性的拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递,处理完后将其转发到其ChannelPipeline(业务处理链)的下一个处理程序,基于ChannelHandler接口,用户可以方便的进行业务逻辑定制,如打印日志,统一封装异常消息,性能统计和消息编解码等。

ChannelHandler支持注解,目前支持两种注解

@Sharable:可分享的,表示多个ChannelPipeline共用一个ChannelHandler

@Skip:跳过,被Skip注解的方法不会被调用,直接被忽略。

Channel的继承图

ChannelHandlerAdapter

Netty提供了ChannelHandlerAdapter类实现ChannelHandler接口,它的所有接口实现都是事件透传,如果用户的ChannelHandler关心某个事件,只需要覆盖ChannelHandlerAdapter对应的方法即可,对于不关心的只需要继承父类的方法即可,而不需要实现ChannelHandler的所有方法而造成代码冗余和臃肿。

ChannelInboundHandler和ChannelOutboundHandler

ChannelHandler充当了处理入站和出站数据的应用程序处理的容器,如:实现ChannelInboundHandler接口(或ChannelInboundHnadlerAdapter)就可以接受入站事件和数据,这些数据会被业务逻辑处理,当要给客户端发送响应时,也可以从ChannelInboundHandler冲刷数据,业务逻辑通常写在一个或者多个ChannelInboundHandler中,ChannelOutboundHandler同理。ChannelInboundHandlerAdapter和ChannelOutboundHandler都是继承ChannelHandlerAdapter类并实现各自的InBoundHandler接口,其中ChannelInboundHandler用于处理入站I/O事件,ChannelOutboundHandler用于处理出站 I/O操作。常用的API如下:

public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
    public ChannelInboundHandlerAdapter() {
    }
    //通道注册事件
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelRegistered();
    }
    
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelUnregistered();
    }
    //通道就绪事件
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelActive();
    }

    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelInactive();
    }
    //通道读取数据事件
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ctx.fireChannelRead(msg);
    }
    //通道读取数据完毕事件
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelReadComplete();
    }

    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
        ctx.fireChannelWritabilityChanged();
    }
    //通道捕获异常事件
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.fireExceptionCaught(cause);
    }
}

以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么称之为这些事件为出站,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler,并被这些Handler处理,反之,如果事件是从服务端到客户端的就称之为入站。

ChanelPipeline

Netty的ChannelPipeline和ChannelHandler机制类似于Servelt和Filter,这类拦截器实际上是责任链模式的一种变形,主要为了方便事件的拦截和用户业务逻辑的定制

ChannelPipeline是ChannelHandler的容器,它负责ChannelHandler的管理和事件拦截与调度,包括inbound或者outbound的事件和操作,相当于一个贯穿Netty的链。在Netty中每个Channel都有且仅有一个ChannelPipeline与之对应, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表。这个链表的头是 HeadContext,链表的尾是 TailContext, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。如下图:

  • 一个Channel包含一个ChannelPipeline(ChannelPipeline也持有Channel的引用,可以相互获取),而ChannelPipeline中又维护了一个ChannelHandlerContext组成的双向链表,每个ChannelHandlerContxt中又关联一个ChannelHandler。
  • ChannelHandlerContext其实是代表了ChannelHandler和ChannelPipeline之间的关联,每当有ChannelHandler添加到ChannelPipeline中时,都会创建一个ChannelHandlerContext。
  • 而一个 ChannelHandler 可以从属于多个 ChannelPipeline,可以绑定到多个 ChannelHandlerContext 实例。对于多个ChannelPipeline 中共享同一个 ChannelHandler,对应的 ChannelHandler 必须要使@Sharable 注解标注;否则将会触发异常
  • ChannelHandler通过 EventLoop(I/O 线程)来处理传递给它的事件的。

ChannelPipeline的事件处理

  • 底层的SocketChannel read()方法读取ByteBuf,触发ChannelRead()事件,由I/O线程NioEventLoop调用ChannelPipeline的FileChannelRead(Object msg)方法,将消息ByteBuf传输到ChannelPipeline中。
  • 消息到达ChannelPipeline后,一次被HeadHandler、ChannelHandler1、ChannelHandler1..TailHandler拦截和处理,在整个过程中,任何ChannelHandler都可以中断当前流程结束消息传递
  • 调用ChannelHandlerContext的write()方法发送消息,消息依次从从TailHandler...ChannelHandler2、ChannelHandler1到HeadChannelHandler,最终消息被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息,例如当编码失败时,就需要中断流程,构造异常的Future返回

ChannelPipeline的inbound事件

inbound事件通常由I/O线程触发(从链表head往后传递到最后一个入站的handler),例如TCP链路建立事件、链路关闭事件、读取操作完成事件、异常通知事件等,事件在pipeline中得到传播和处理,它是事件处理的总入口。

pipeline中以fireXxx命名的方法都是I/O线程流向用户业务Handler的inbound事件,处理的步骤如下

(1)调用HeadHandler对应的fireXxx方法

(2)执行事件相关的逻辑操作

以fireChannelActive方法为例,调用head.fireChannelActive()方法后,判断当前的Channel配置是否自动读取,如果为true则调用Channel的read方法

ChannelPipeline的outbound事件

outbound事件通常由用户发起的网络I/O操作(从链表tail往前传递到最前一个出站的handler),例如用户发起连接操作、绑定操作、消息发送等操作。入站事件和出站事件的handler互不干扰。对于客户端程序,如果事件运动方向是从服务端到客户端则称inbound事件,反之为outbound。服务端一样的道理

//触发inbound事件
public interface ChannelInboundInvoker {
    //Channel注册事件
    ChannelInboundInvoker fireChannelRegistered();

    ChannelInboundInvoker fireChannelUnregistered();
    //TCP链路建立成功,Channel激活事件, 触发inbound事件
    ChannelInboundInvoker fireChannelActive();
    //TCP连接关闭,链路不可用通知事件, 触发inbound事件
    ChannelInboundInvoker fireChannelInactive();
    //异常通知事件, 触发inbound事件
    ChannelInboundInvoker fireExceptionCaught(Throwable var1);
    //用户自定义事件 触发inbound事件
    ChannelInboundInvoker fireUserEventTriggered(Object var1);
    //读事件 触发inbound事件
    ChannelInboundInvoker fireChannelRead(Object var1);
    //读操作事件完成, 触发inbound事件
    ChannelInboundInvoker fireChannelReadComplete();
    //Channel可读写状态变化通知事件, 触发inbound事件
    ChannelInboundInvoker fireChannelWritabilityChanged();
}
//触发outbound事件
public interface ChannelOutboundInvoker {

    //绑定本地地址事件
    ChannelFuture bind(SocketAddress var1, ChannelPromise var2);
    //连接服务端事件
    ChannelFuture connect(SocketAddress var1, SocketAddress var2, ChannelPromise var3);
    //断开连接
    ChannelFuture disconnect(ChannelPromise var1);
    //关闭当前Channel
    ChannelFuture close(ChannelPromise var1);
    //读事件
    ChannelOutboundInvoker read();
    //发送事件
    ChannelFuture write(Object var1, ChannelPromise var2);
    //刷新事件
    ChannelOutboundInvoker flush();
}

ChannelHandlerContext 接口

ChannelHandlerContext保存了所有上下文信息,同时关联一个事件处理器ChannelHandler对象,同时ChannelHandlerContext中也绑定对应的pipeline和Channel信息,方法对ChannelHandler进行调用。

public interface ChannelHandlerContext extends AttributeMap, ChannelInboundInvoker, ChannelOutboundInvoker {
    //获取通道
    Channel channel();

    EventExecutor executor();

    String name();

    ChannelHandler handler();

    boolean isRemoved();

    ChannelHandlerContext fireChannelRegistered();

    ChannelHandlerContext fireChannelUnregistered();

    ChannelHandlerContext fireChannelActive();

    ChannelHandlerContext fireChannelInactive();

    ChannelHandlerContext fireExceptionCaught(Throwable var1);

    ChannelHandlerContext fireUserEventTriggered(Object var1);

    ChannelHandlerContext fireChannelRead(Object var1);

    ChannelHandlerContext fireChannelReadComplete();

    ChannelHandlerContext fireChannelWritabilityChanged();

    ChannelHandlerContext read();
    //刷新
    ChannelHandlerContext flush();

    ChannelPipeline pipeline();

    ByteBufAllocator alloc();

}

ChannelPipeline的使用和特性

事实上,用户不需要自己创建pipeline,因为使用ServerBootstrap或Bootstrap启动服务端或者客户端时候,Netty会为每个Channel连接创建一个独立的pipeline,对于开发者来说只需要将自定义的拦截器加入到pipeline中即可,主要有以下两种方式:

socketChannel.pipeline().addLast(new NettyServerHandler());
socketChannel.pipeline().addFirst(new new NettyServerHandler2())

ChannelPipeline是线程安全的,这意味着N个业务线程可以并发的操作ChannlePipeline而不存在多线程并发问题,ChannelPipeline支持运行期动态修改,因此存在两种潜在的多线程并发访问场景

  • I/O线程和用户业务线程的并发访问
  • 用户多个线程之间的并发访问

为了保证ChannelPipeline的线程安全性,Netty中直接使用了synchronized关键字,保证同步块内所有操作的原子性,但是ChannelHandler却不是线程安全的,这意味着尽管ChannelPipeline是线程安全的,用户仍然需要保证ChannelHandler的线程安全。

NioEventLoop

EventLoopGroup是一组EventLoop的抽象,Netty为了提高性能利用多核CPU资源,一般会有多个EventLoop同时工作,作为Netty框架的Reactor线程,NioEventLoop需要处理网络I/O读写事件,因此它必须聚合一个多路复用器对象,每个NioEventLoop都维护一个Selector实例,通常一个服务端口即一个ServerSocketChannel对应一个Selector和EventLoop线程,BossEventLoop负责接受客户端的连接并将SocketChannel交给WorkEventLoopGroup来进行I/O处理

BossEventLoopGroup通常是由一个单线程的EventLoop,EventLoop维护上一个注册了SeverSocketChannel的Selector实例,BossEventLoop不断轮询Selector将连接时间分离出来,通常是OP_ACCEPT事件,然后将接收到的SocketChannel交给SocketChannel交给WorkerEventLoopGroup,WorkerEventLoopGroup会由next选择其中一个EventLoopGroup来将这个SocketChannel注册到其维护的Selector并对其后续的I/O事件进行处理。

ByteBuf

在NIO中的ByteBuffer的局限性

对于NIO编程来说,我们主要使用的是ByteBuffer,从功能性来说,ByteBuffer完全满足NIO编程的需要,但是也存在一定的局限性,主要如下:

  • ByteBuffer的长度固定,一旦分配围城,他的容量不能动态的扩展和收缩,当需要编码的POJO对象大于ByteBuffer的容量时,会发生索引越界异常
  • ByteBuffer只有一个标志位置的指针position,读写的时候需要手动调用flip()方法(每次操作position和capacity之间的数据,如果不进行flip()操作,那么读取的则是错误的,当执行flip()方法后,他的limit被设置成position,position被设置成0,capacity保持不变,这样才能读取正确的内容。)
  • ByteBuffer的API功能有限,不支持一些高级实用的特性

ByteBuf的介绍

ByteBuf也是一个Byte数组的缓冲区,基本功能和JDK的ByteBuf一致,ByteBuf中通过两个位置指针来协助缓冲区的读写,读操作使用的是readerIndex,写操作使用的是writeIndex。readerIndex和writeIndex的初始值都是0,随着数据的写入,writeIndex不断增加。同样读取数据会使readerIndex增加,但是不会超过writeIndex。在读取之后,0-readerIndex就被视为discard,调用discardReadBytes方法,可以释放这部分空间,类似于ByteBuffer的compact方法。readerIndex和wirterIndex之间的数据是可读取的,等价于position和limit之间的数据。如下图

readerIndex到writerIndex之间的空间是可读的字节缓冲区,从writerIndex到capacity之间为可写的字节缓冲区,0到readerIndex之间是已经读取的缓冲区,调用discardReadBytes操作来重用这部分空间,以节约内存,防止ByteBuf动态扩张。

ByteBuf的顺序读写

ByteBuf的write操作类似ByteBuffer的put操作,包括不同类型的write操作,如writeByte()、writeInt()...每次操作成功后writerIndex会增加不通的数值(取决于不同类型的字节数,如byte会+1,int类型会+4),同样read操作类似于ByteBuffer的get操作,包括不同类型的read操作,如readByte()、readInt()...每次操作成功后readerIndex会增加不通的数值(取决于不同类型的字节数,如byte会+1,int类型会+4)

public static void main(String[] args){
    ByteBuf buffer = Unpooled.buffer(5);
      for (int i=0; i< 11; i ++){
           //buffer.setByte(0,1);//根据索引设置
           buffer.writeByte(i);
        }

    for(int i=0; i< 12; i ++){
        System.out.println(buffer.getByte(i)); //按照指定索引读取,索引不存在的默认为0
        System.out.println(buffer.readByte()); //根据readerIndex顺序读取,索引不存在IndexOutOfBoundsException
        }

    ByteBuf byteBuf = Unpooled.copiedBuffer("hello,world,合肥", CharsetUtil.UTF_8);
        if(byteBuf.hasArray()){//true
            byte[] array = byteBuf.array();//返回byte 数组
            System.out.println(new String(array, Charset.forName("UTF-8")));
            System.out.println("byteBuf" + byteBuf);
            System.out.println(byteBuf.arrayOffset());
            System.out.println(byteBuf.readerIndex());
            System.out.println(byteBuf.writerIndex());
            System.out.println(byteBuf.readableBytes());//可读的字节数,如果通过buffer.readByte()读取后就会减少

            for (int i=0; i<array.length;i++){
                System.out.println((char)byteBuf.getByte(i));
            }
            System.out.println(byteBuf.getCharSequence(0,5,CharsetUtil.UTF_8));

        }
    }
}

Netty权威指南的入门程序

Netty实现NIO服务端开发

public class NettyServer {

    public void bind(int port){
        //NioEventLoopGroup是一个线程组,包含一组NIO线程,用于网络事件处理,实际上就是Reactor线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();//用于服务端接受客户端的连接
        EventLoopGroup workerGroup = new NioEventLoopGroup();//用于SocketChannel的网络读写
        try{
            //ServerBootstrap对象是Netty用于启动NIO服务端的辅助启动类
            ServerBootstrap bs = new ServerBootstrap();
            bs.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)//设置创建的channel
                    .option(ChannelOption.SO_BACKLOG, 1024)//设置NioServerSocketChannel的TCP参数
                    .childHandler(new ChildChannelHandler());//绑定I/O事件处理类ChildChannelHandler
            ChannelFuture sync = bs.bind(port).sync();//绑定监听端口并调用同步阻塞方法等待绑定操作完成
            sync.channel().closeFuture().sync();//等待服务器链路关闭之后main函数才退出
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放连接池资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    //事件处理类ChildChannelHandler,类似Reactor中的Handler
    class ChildChannelHandler extends ChannelInitializer<SocketChannel>{
        @Override
        protected void initChannel(SocketChannel socketChannel) throws Exception {
            socketChannel.pipeline().addLast(new TimeServerHandler());
        }
    }

    class TimeServerHandler extends ChannelHandlerAdapter{
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf)msg;
            byte[] req = new byte[buf.readableBytes()];
            buf.readBytes(req);
            String body = new String(req, "UTF-8");
            System.out.println("Time Server receive order:" + body);
            String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new Date(System.currentTimeMillis()).toString(): "BAD ORDER";
            ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
            ctx.writeAndFlush(resp);
        }

        @Override
        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
            //Netty把write方法并不直接将消息写入到SocketChannel中,调用write方法只是把待发送的消息放到缓冲数组中,
            // 调用flush方法才将消息全部写道SocketChanel
            ctx.flush();
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            //释放相关句柄等资源
            ctx.close();
        }
    }

    public static void main(String[] args) {
        new NettyServer().bind(9988);
    }
}

Netty实现NIO客户端开发

public class NetttClient {

    public void connect(String host, int port){
        //配置客户端NIO线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try{
            //创建客户端辅助启动类Bootstrap
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY,Boolean.TRUE)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        //创建NioSocketChannel成功之后,进行初始化
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new TimeServerHandler());
                        }
                    });
            //发起异步连接操作
            ChannelFuture sync = bootstrap.connect(host, port).sync();
            //等待客户端链路关闭
            sync.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //释放NIO线程组资源
            group.shutdownGracefully();
        }
    }

    class TimeServerHandler extends ChannelHandlerAdapter {
        private final ByteBuf firstMessage;
        public TimeServerHandler() {
            byte[] req = "QUERY TIME ORDER".getBytes();
            firstMessage = Unpooled.buffer(req.length);
            firstMessage.writeBytes(req);
        }
        //当客户端和服务端TCP链路建立成功之后,Netty的NI线程会调用channelActive方法,发送查询指定给服务端
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            ctx.writeAndFlush(firstMessage);
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            ByteBuf buf = (ByteBuf) msg;
            byte[] bytes = new byte[buf.readableBytes()];
            buf.readBytes(bytes);
            String body = new String(bytes, "UTF-8");
            System.out.println("now is :" + body);
        }

        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            log.info("Unexpected exception frm downstream:" + cause.getMessage());
            ctx.close();
        }
    }

    public static void main(String[] args){
        new NetttClient().connect("127.0.0.1",9988);

    }
}

分别启动服务端和客户端执行结果如下:

Time Server receive order:QUERY TIME ORDER

now is :Sat May 30 18:30:18 CST 2020

可以看出相比于传统的NIO程序,使用Netty开发代码更加简洁,开发难度更低,扩展性也更好。在上面的Netty入门的代码中并没有考虑读半包等问题。当系统压力突增或者发送大报文之后,就会存在粘包/拆包的问题,可能导致解码错位甚至错误,导致程序不能正常工作。

Netty实现群聊系统

群聊系统服务端功能和代码:

  • pipeline添加字符串相关的编解码类(群聊的交互是使用字符串)
  • 定义Channel组,管理所有的Channel,在连接建立时把Chanel初始化到Channel组中,并做出提示,连接断开时从组中移除
  • 读取消息时候,遍历Channel组,向所有的通道写入消息,供其他用户(客户端)获取
public class NettyChatServer {

    private int port;

    public NettyChatServer(int port) {
        this.port = port;
    }

    public void run() throws InterruptedException {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try{
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG,128)
                    .childOption(ChannelOption.SO_KEEPALIVE,true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //向pipeline加入String的解码器和编码器
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast(new NettyChatHandler());
                        }
                    });
            System.out.println("Netty服务器启动");
            ChannelFuture future = bootstrap.bind(port).sync();
            future.channel().closeFuture().sync();
        }finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }

    static class NettyChatHandler extends SimpleChannelInboundHandler<String>{
        //定义一个Channel组,管理所有的channel,GlobalEventExecutor.INSTANCE 是全局事件执行器,是个单列
        private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

        /**
         * 表示连接建立,一旦连接建立,第一个执行该方法:把channel加入到channelGroup中
         */
        @Override
        public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
            Channel channel = ctx.channel();
            //将该客户加入聊天的信息推送给其他在线的客户端
            /**
             * 该方法会将channelGroup中所有的channel遍历并发送以下消息
             */
            channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + "加入聊天");
            channelGroup.add(channel);
        }

        /**
         * channel处于活动状态触发此方法
         */
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(ctx.channel().remoteAddress() + "上线了");
        }
        /**
         * channel处于非活动状态触发此方法
         */
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println(ctx.channel().remoteAddress() + "下线了");
        }
        /**
         * 表示连接断开,一旦断开建立,执行该方法
         */
        @Override
        public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
            Channel channel = ctx.channel();
            //将该客户离开聊天的信息推送给其他在线的客户端
            /**
             * 该方法会将channelGroup中所有的channel遍历并发送以下消息
             */
            channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + "离开聊天");
            //channelGroup.remove(channel); 此方法自动执行
            System.out.println("channelGroup size = " + channelGroup.size());
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
            Channel channel = ctx.channel();
            //遍历channelGroup
            channelGroup.forEach(ch -> {
                if(channel != ch){ //不是当前通道
                    ch.writeAndFlush("[客户]" + channel.remoteAddress() + "发送消息" + msg);
                }else{
                    ch.writeAndFlush("我说:" + msg);
                }
            });
        }

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

    public static void main(String[] args) throws InterruptedException {
        new NettyChatServer(8888).run();
    }
}

群聊系统客户端功能和代码:

客户端连接服务端并且通过Scanner输入内容并发送给服务端,服务端收到消息后发送给Channel组中所有Channel

public class NettyChatClient {
    private final String host;
    private final int port;

    public NettyChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws InterruptedException {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        try{
            bootstrap.group(eventLoopGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //向pipeline加入String的解码器和编码器
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast(new NettyClientHandler());
                        }
                    });
            ChannelFuture future = bootstrap.connect(host, port).sync();
            Channel channel = future.channel();
            System.out.println("--------" + channel.localAddress() + "-----");
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()){
                String msg = scanner.nextLine();
                channel.writeAndFlush(msg);
            }
            future.channel().closeFuture().sync();
        }finally {
            eventLoopGroup.shutdownGracefully();
        }
    }

    static class NettyClientHandler extends SimpleChannelInboundHandler<String> {

        @Override
        protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
            System.out.println(msg+"---" + LocalDateTime.now());
        }
    }


    public static void main(String[] args) throws InterruptedException {
        new NettyChatClient("127.0.0.1", 8888).run();
    }
}

分别启动服务端和客户端,在服务端可以看到客户端的上线和发送的内容:

/127.0.0.1:56453上线了
10:54:23.387 [nioEventLoopGroup-3-2] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 32768
10:54:23.388 [nioEventLoopGroup-3-2] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
10:54:23.388 [nioEventLoopGroup-3-2] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
10:54:23.388 [nioEventLoopGroup-3-2] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
/127.0.0.1:56499上线了
10:54:23.410 [nioEventLoopGroup-3-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.bytebuf.checkAccessible: true
10:54:23.412 [nioEventLoopGroup-3-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@1702e467
/127.0.0.1:56576上线了

客户端56453的相关信息

--------/127.0.0.1:56453-----
10:54:23.421 [nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 32768
10:54:23.421 [nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
10:54:23.421 [nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
10:54:23.421 [nioEventLoopGroup-2-1] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
10:54:23.429 [nioEventLoopGroup-2-1] DEBUG io.netty.buffer.AbstractByteBuf - -Dio.netty.buffer.bytebuf.checkAccessible: true
10:54:23.431 [nioEventLoopGroup-2-1] DEBUG io.netty.util.ResourceLeakDetectorFactory - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@2fc40f5e
[客户端]/127.0.0.1:56499加入聊天---2020-08-18T10:54:23.460
[客户端]/127.0.0.1:56576加入聊天---2020-08-18T10:55:22.814
[客户]/127.0.0.1:56576发送消息你好---2020-08-18T10:55:32.006

Netty中的心跳机制

IdleStateHandler 是netty提供的处理空闲状态的处理器

public IdleStateHandler(
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {
        this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
    }
  • readerIdleTime:表示多长时间没有(Server端)读,就会发送心跳检测包检测是否还是连接状态
  • writerIdleTime:表示多长时间没有(Server端)写,就会发送心跳检测包检测是否还是连接状态
  • allIdleTime:表示多长时间没有(Server端)读写,就会发送心跳检测包检测是否还是连接状态

当channel没有读、写或者读写操作时候 ,会触发一个IdleStateEvent事件, 当IdleStateEvent事件触发后,就会传递给管道的下一个handler去处理,通过调用下个handler的userEventTriggered方法 , 在该方法中处理IdleStateEvent(读空闲,写空闲,读写空闲),具体使用如下:

public class MyServer {

    public static void main(String[] args) {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(1);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        try{
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            /**
                             * IdleStateHandler 是netty提供的处理空闲状态的处理器
                             * IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit)
                             * readerIdleTime:表示多长时间没有(Server端)读,就会发送心跳检测包检测是否还是连接状态
                             * writerIdleTime:表示多长时间没有(Server端)写,就会发送心跳检测包检测是否还是连接状态
                             * allIdleTime:表示多长时间没有(Server端)读写,就会发送心跳检测包检测是否还是连接状态
                             * 会触发一个IdleStateEvent事件,当channel没有读、写或者读写操作时候
                             *  当IdleStateEvent事件触发后,就会传递给管道的下一个handler去处理,通过调用下个handler的userEventTriggered方法
                             *  在该方法中处理IdleStateEvent(读空闲,写空闲,读写空闲)
                             */
                            pipeline.addLast(new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
                            //一个对空闲检测进一步处理的handler
                            pipeline.addLast(new MyServerHandler());
                        }
                    });
            ChannelFuture future = bootstrap.bind(9888).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
    static class MyServerHandler extends ChannelInboundHandlerAdapter{
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if(evt instanceof IdleStateEvent){
                IdleStateEvent event = (IdleStateEvent)evt;
                String eventType =null;
                switch (event.state()){
                    case READER_IDLE:
                        eventType = "读空闲";
                        break;
                    case WRITER_IDLE:
                        eventType = "写空闲";
                        break;
                    case ALL_IDLE:
                        eventType = "读写空闲";
                        break;
                }
                System.out.println(ctx.channel().remoteAddress() + "客户端,超时时间发生" + eventType);

            }
        }
    }
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值