Netty 核心技术介绍

原生NIO 存在的问题

1). NIO 的库类和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等

2). 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你需要对多线程和网络编程非常熟悉,才能编写出高质量 NIO 程序

3). 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等

4). JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会倒置 Selector 空轮训,最终倒置 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决

我额外有一篇文章是有单独介绍 NIO 的:https://blog.csdn.net/weixin_43582499/article/details/115456034

1.Netty 官网说明

官网:https://netty.io

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

翻译: Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能协议服务器和客户端。

Netty 官网放了一张图:
在这里插入图片描述

2.Netty 版本说明

1). netty 版本分为 netty3.x 和 netty4.x、netty5.x

2). 因为 Netty5 出现重大 bug,已经被官网废弃了,目前推荐使用的是 Netty4.x 的稳定版本

3). 目前在官网可下载的版本 netty3.x 、netty4.0.x 和 netty4.1.x

4).下面的讲解,我们依局 Netty4.1.x 版本

5). Netty 的下载地址:https://bintray.com/netty/downloads/netty/

3.线程模型基本介绍

1). 不同的线程模式,对程序的性能有很大影响,为了搞清楚 Netty 线程模式,我们来系统的讲解下各个线程模式,最后看看 Netty 线程模型有什么优越性

2). 目前存在的线程模型有:

  • 传统阻塞I/O服务模型
  • Reactor 模式

3). 根据 Reactor 的数量和处理资源池线程的数量不同,有3种典型的实现:

  • 单 Reactor 单线程
  • 单 Reactor 多线程
  • 主从 Reactor 多线程

4). Netty 线程模式 (Netty 主要基于主从 Reactor 多线程模型做了一定的改进,其中主从 Reactor 多线程模型有多个 Reactor)

4.主从 Reactor 多线程

因为:Netty 主要基于主从 Reactor 多线程模型做了一定的改进,然后我们本篇也是主要讲 Netty,那就只说说 主从 Reactor 多线程

4.1 工作原理示意图:

在这里插入图片描述

4.2 说明

1). Reator 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件

2). 当 Acceptor 处理连接事件后, MainReactor 将连接分配个 SubReactor

3). SubReactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理

4). 当有新事件发生时, SubReactor 就会调用对应的 handler 处理

5). Handler 通过 read 读取数据,分发给后面的 worker 线程处理

6). worker 线程池分配独立的 worker 线程进行业务处理,并返回结果

7). Handler 收到响应的结果后,在通过 send 将结果返回给 client

8). Reactor 主线程可以对应多个 Reactor 子线程,即 MainReactor 可以关联多个 SubReactor

5. Netty 模型

工作原理示意图(详细版):

在这里插入图片描述
图解说明:

1). Netty 抽象出两组线程池: BossGroup 专门负责接收客户端的连接,WorkGroup 专门负责网络的读写

2). BossGroup 和 WorkGroup 类型都是 NioEventLoopGroup

3). NioEventLoopGroup 相当于一个时间循环组,这个组中含有多个事件循环,每个事件循环都是 NioEventLoop

4). NioEventLoop 表示一个不断循环的执行处理任务的线程,每个NioEventLoop 都有一个selector,用于监听绑定在其上的 socket 的网络通讯

5). NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop

6). 每个 BossGroup的 NioEventLoop 循环执行的步骤有3步:

  • 轮询 accept 事件
  • 处理 accept 事件,与 client 建立连接,生成 NioSocketChannel,并将其注册到某个 worker的 NioEventLoop 上的 selector
  • 处理任务队列的任务,即 runAllTashs

7). 每个 WorkerGroup 的 NioEventLoop 循环执行的步骤:

  • 轮询 read、write 事件
  • 处理 i/o 事件,即 read、write 事件,在对应的 NioSocketChannel 处理
  • 处理任务队列的任务,即 runAllTasks

8). 每个 Worker NIOEventLoop 处理业务时,会使用 pipeline (管道),pipeline 中含有 channel ,即通过 pipeline 可以获取到相应通道,管道中维护了很多的处理器

5.1 Netty快速入门示例-TCP服务

示例要求:

1). Netty 服务器在6668 端口监听,客户端能发送消息给服务器 “Hello, 服务器~~”
2). 服务器可以恢复消息给客户端 “Hello ,客户端”

5.1.1 创建 Maven 项目,引入 Netty包(环境搭建)

  • Project Structure -> Modules -> Dependenies -> +号 -> Library
  • New Library -> From Maven ->
  • 输入:io.netty:netty-all
  • 选择 4.1.20.Final 版本

在这里插入图片描述

5.1.2 服务端代码:

public class NettyServer {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        //创建 BossGroup 和 WorkGroup
        /**
         * 说明:
         * 1.创建两个线程组 bossGroup 和 workGroup
         * 2.bossGroup 只是处理连接请求,真正和客户端业务处理,会交给 workGroup 完成
         * 3.两个都是无限循环
         */
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
    
    

            //创建服务器端的启动对象,配置参数
            ServerBootstrap bootstrap = new ServerBootstrap();

            //使用链式编程来进行设置
            bootstrap.group(bossGroup, workGroup)//设置两个线程组
                    .channel(NioServerSocketChannel.class)//使用 NioServerSocketChannel 作为服务器的通道实现
                    .option(ChannelOption.SO_BACKLOG, 128)//设置线程队列得到连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true)//设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() {
    
     //创建一个通道测试对象(匿名对象)
                        //给 pipeline 设置处理器
                        @Override
                        protected void initChannel(SocketChannel ch) {
    
    
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    });//给我们的 workGroup 的 EventLoop 对应的管道设置处理器


            System.out.println("Server is reading....");

            //绑定一个端口并且同步,生成一个 ChannelFuture 对象
            //启动服务器(并绑定端口)
            ChannelFuture cf = bootstrap.bind(6668).sync();

            //对关闭通道进行监听
            cf.channel().closeFuture().sync();

        } finally {
    
    
            //优雅地关闭
            bossGroup.shutdownGracefully();
        }
    }
}
/**
 * 1.我们自定义一个 handler 需要继承 netty 规定好的某个 HandlerAdapter
 * 2.这时我们自定义一个 Handler,才能称为称为一个 handler
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
    
    

    /**
     * 1.ChannelHandlerContext ctx:上下文对象,含有 管道 pipeline,通道 channel ,地址
     * 2.Object msg:就是客户端发送的数据 默认为 Object
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    

        System.out.println("server ctx" + ctx);
       //讲 msg 转成一个 ByteBuf
       //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 {
    
    
        //writeAndFlush 是 write + flush
        //将数据写到缓存,并刷新
        //一般来讲,我们会对发送的数据进行编码
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello,客户端  ", CharsetUtil.UTF_8));
    }


    //处理异常,一般需要关闭通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
        ctx.close();
    }
}

5.1.3 客户端代码:

public class NettyClient {
    
    
    public static void main(String[] args) throws InterruptedException {
    
    
        //客户端需要一个事件循环组
        EventLoopGroup eventExecutors = new NioEventLoopGroup();

        try {
    
    

            //创建客户端启动对象
            //注意客户端使用的不是 ServerBootstrap,而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();

            //设置相关参数
            bootstrap.group(eventExecutors)//设置线程组
                    .channel(NioSocketChannel.class)//设置客户端通道的实现类(反射)、
                    .handler(new ChannelInitializer<SocketChannel>() {
    
    
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
    
    
                            ch.pipeline().addLast(new NettyClientHandler());  //加入自己的处理器

                        }
                    });

            System.out.println("客户端 is ok");

            //启动客户端去连接服务端
            //关于 ChannelFuture 要分析,涉及到 netty 的异步模型
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();

            //关闭通道进行监听
            channelFuture.channel().closeFuture().sync();

        } finally {
    
    
            eventExecutors.shutdownGracefully();
        }
    }
}
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
    
    


    //当通道就绪时,就会触发该方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        System.out.println("client" + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello Server: miao~~", CharsetUtil.UTF_8));
    }


    //当通道有读取事件时,会触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        //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 exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
        cause.printStackTrace();
        ctx.close();
    }
}

5.1.4 运行结果:

在这里插入图片描述
在这里插入图片描述
NioEventLoopGroup默认的线程数 = 电脑CPU 的核数 * 2

底层源码:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

DEFAULT_EVENT_LOOP_THREADS 这个变量放在了一个静态代码块里面做初始化。
而 NettyRuntime.availableProcessors() 就能拿到你电脑的 CPU 的核数

6. Netty 模型再说明

1). Netty 抽象出两组线程池、BossGroup 专门负责接收客户端连接,WorkGroup 专门负责网络读写操作

2). NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道

3). NioEventLoop 内部采用串行化设计,从消息的读取 -> 解码 -> 处理 -> 编码 -> 发送,始终由 IO 线程 NioEventLoop 负责

  • NioEventLoopGroup 下包含多个 NioEventLoop
  • 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
  • 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
  • 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
  • 每个 NioChannel 都绑定有一个自己的 ChannelPipeline

7. 异步模型

1). 异步的概念和同步相对。当一个异步过程调用发生后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者

2). Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture

3). 调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果

7.1 Future-Listener 机制

1). 当 Future 对象刚刚创建时,处于非王城状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作

2). 常见有如下的操作:

  • 通过 isDone 方法来判断当前操作是否完成
  • 通过 isSuccess 方法来判断已完成的当前操作是否成功
  • 通过 getCause 方法来获取已完成的当前操作失败的原因
  • 通过 isCancelled 方法来判断已完成的当前操作是否被取消
  • 通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则通知指定的监听器

代码举例:

            //启动服务器(并绑定端口)
            ChannelFuture cf = bootstrap.bind(6668).sync();

            //给 cf 注册监听器,监控我们关心的事件
            cf.addListener(new ChannelFutureListener() {
    
    
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
    
    
                    if (cf.isSuccess()) {
    
    
                        System.out.println("监听端口 6668 成功");
                    } else {
    
    
                        System.out.println("监听端口 6668 失败");
                    }
                }
            });

7.2 Netty 搭建 Http 服务

要求:Netty 服务器在 8888 端口监听,浏览器发出请求 “http://localhost:8888”。
服务器可以回复消息给客户端 “Hello ! 我是服务器”,并对特定请求资源进行过滤

Server端:

public class TestServer {
    
    

    public static void main(String[] args) {
    
    
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();

        try {
    
    
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class)
                    .childHandler(new TestServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(8006).sync();

            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
    
    

        }
    }
}

ChannelInitializer:

public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
    
    
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
    
    
        //向管道加入处理器

        //得到管道
        ChannelPipeline pipeline = ch.pipeline();

        //加入一个 netty 提供的 httpServerCodec codec => [coder - decoder]
        /**
         * HttpServerCodec 说明
         * 1.HttpServerCodec 是 netty 提供处理 http 的编码-解码器
         *
         */
        pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
        //2.增加一个自定义的 Handler
        pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());

    }
}

ChannelInboundHandlerAdapter:

/**
 * 1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter 的子类
 * 2. HttpObject 客户端和服务器 端互相通讯的数据被封装成 HttpObject
 */
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    
    

    //channelRead0 读取客户端的数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
    
    

        //判断 msg 是不是 httpRequest 请求
        if (msg instanceof HttpRequest) {
    
    

            /**
             * 两个浏览器请求,可以发现打印出来的 hashcode 是不相同的
             * 这是因为一个 浏览器就对应一个相应的 pipeline 和 handler
             */
            System.out.println("pipeline hashcode" + ctx.pipeline().hashCode()
                    + "TestHttpServerHandler" + this.hashCode());

            System.out.println(" msg 的类型=" + msg.getClass());
            System.out.println("客户端的地址=" + ctx.channel().remoteAddress());

            //因为 http 发了两次请求,为了过滤掉 /favicon.ico
            HttpRequest request = (HttpRequest) msg;
            URI uri = new URI(request.uri());
            if ("/favicon.ico".equals(uri.getPath())) {
    
    
                System.out.println("请求了 favicon.ico 资源,不做相应");
                return;
            }

            //回复信息给浏览器[满足 http 协议]
            ByteBuf content = Unpooled.copiedBuffer("Hello,我是服务器", CharsetUtil.UTF_8);

            //构造一个 http 的相应,即 httpResponse
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);

            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=utf-8");

            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());

            //将构建好的 response 返回
            ctx.writeAndFlush(response);

        }
    }
}

注意的地方:

1). 个别浏览器可能不支持端口数太小,所以端口设置尽量往大了设置,8000以上吧

2). 服务端发送的 “Hello,我是服务器” 这句话可能发生乱码,所以记得加上:charset=utf-8

3). 我们用两个浏览器去访问:http://localhost:8006,会发现打印日志
在这里插入图片描述
pipeline 和 handler 的 hashcode 值不相同,
证明: 一个浏览器的连接就对应一个相应的 pipeline 和 handler

8. Netty 重要组件

8.1 Bootstrap、ServerBootstrap

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

2). 常见的方法:

  • public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) ,该方法用于服务器端,用来设置两个 EventLoop
  • public ServerBootstrap group(EventLoopGroup group):用于客户端,用来设置一个 EventLoop
  • public B channel(Class<? extends C> channelClass):用来设置一个服务器端的通道实现
  • public B option(ChannelOption option, T value):给 ServerChannel 添加配置,设置线程队列得到连接个数
  • public ServerBootstrap childOption(ChannelOption childOption, T value):用来给接收到的用到添加配置,设置保持活动连接状态
  • public ServerBootstrap childHandler(ChannelHandler childHandler):设置业务处理类(自定义的 handler)
  • public ChannelFuture bind(int inetPort):用于服务器端,设置占用的端口号
  • public ChannelFuture connect(String inetHost, int inetPort):用于客户端,用来连接服务器端

8.2 Future、ChannelFuture

1). Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会儿等它执行完成或者直接注册一个监听器,具体的实现就是通过 Future 和 ChannelFutures,它们可以注册一个监听,当执行成功或失败时监听会自动出发注册的监听事件

8.3 Selector

1). Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 channel 事件

2). 当向一个 Selector 中注册 channel 后,Selector 内部的机制就可以自动不断地查询(Select)这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel

8.4 ChannnelHandler 及其实现类

1). ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到期 ChannelPipeline(业务处理链)中的下一个处理程序

2). ChannelHandler 本身没有提供很多方法,因为这个接口有许多的方法需要被实现,方便使用期间,可以继承它的子类
在这里插入图片描述
ChannelInboundHandlerAdapter、SimpleChannelInboundHandler 是最常用的两个子类

3). 我们经常需要自定义一个handler 类去继承 ChannelInboundHandlerAdapter,然后通过重写相应的方法实现业务逻辑,

public class ChannelInboundHandlerAdapter extends ChannelHandlerAdapter implements ChannelInboundHandler {
    
    

    /**
     * Calls {@link ChannelHandlerContext#fireChannelRegistered()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Override
    public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
    
    
        ctx.fireChannelRegistered();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelUnregistered()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Override
    public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
    
    
        ctx.fireChannelUnregistered();
    }

    //通道就绪事件
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
    
    
        ctx.fireChannelActive();
    }

    //
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
    
    
        ctx.fireChannelInactive();
    }

    //通道读取数据事件
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    
    
        ctx.fireChannelRead(msg);
    }

    //数据读取完毕事件
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    
    
        ctx.fireChannelReadComplete();
    }

    /**
     * Calls {@link ChannelHandlerContext#fireUserEventTriggered(Object)} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
    
    
        ctx.fireUserEventTriggered(evt);
    }

    /**
     * Calls {@link ChannelHandlerContext#fireChannelWritabilityChanged()} to forward
     * to the next {@link ChannelInboundHandler} in the {@link ChannelPipeline}.
     *
     * Sub-classes may override this method to change behavior.
     */
    @Override
    public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
    
    
        ctx.fireChannelWritabilityChanged();
    }

    //通道发生异常事件
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
            throws Exception {
    
    
        ctx.fireExceptionCaught(cause);
    }
}

8.5 Pipeline 和 ChannelPipeline

ChannelPipeline 是一个重点:

1). ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链。

2). 在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:
在这里插入图片描述

  • 一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler
  • 入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler ,出站事件会从链表 tail 往前传递到最前一个出站的 handler ,两种类型的 handler,两种类型的 handler 互补干扰

3). 常用的方法:

  • ChannelPipeline addFirst(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的第一个位置
  • ChannelPipeline addLast(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的最后一个位置

8.6 ChannelHandlerContext

1). 保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象

2). 即 ChannelHandlerContext 中包含了一个 具体的事件处理器 ChannelHandler,同时 ChannelHandlerContext 中也绑定了对应 pipeline 和 Channel 的信息,方便对 ChannelHandler 进行调用

在这里插入图片描述

8.7 EventLoopGroup 和其实现类 NioEventLoopGroup

1). EventLoopGroup 是一组 EventLoop 的抽象,Netty 为了更好地利用多核 CPU 资源,一般会有多个 EventLoop 同时工作,每个 EventLoop 维护这一个 Selector 实例

2). EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop 来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BossEventLoopGroupWorkerEventLoopGroup

8.8 Unpooled 类

1). Netty 提供一个专门用来操作缓冲区(即Netty的数据容器)的工具类

2). 通过给定的数据和字符串编码返回一个 ByteBuf 对象
(类似于 NIO 中的 ByteBuffer 但是有区别)

  //创建一个ByteBuf
 ByteBuf byteBuf = Unpooled.copiedBuffer("Hello,Hello", Charset.forName("utf-8"));

3). 代码示例:

public class NettyByteBuf01 {
    
    

    public static void main(String[] args) {
    
    

        ByteBuf byteBuf = Unpooled.buffer(10);

        for (int i = 0; i < 10; i++) {
    
    
            byteBuf.writeByte(i);
        }

        for (int i = 0; i < byteBuf.capacity(); i++) {
    
    
            byteBuf.readByte();
        }
    }
}

说明:

  1. 创建对象,该对象包含一个数组 arr,是一个 byte[10]
  2. 在 netty 的 buffer 中,不需要 flip 进行反转,因为底层维护了 readerIndex 和 writerIndex‘’
  3. 通过 readerIndex 和 writerIndex 和 capacity 讲 buffer 分成3个区域
  • 0–readerIndex 已经读取的区域
  • readerIndex–writerIndex :可读的区域
  • writerIndex–capacity: 可写的区域

byteBuf.getByte(i) 和 byteBuf.readByte() 两个方法都能从 ByteBuf 里面拿取数据,
区别就是:调用 readByte()方法才会改变 readerIndex 的值

9. Netty 网络编程引用–群聊系统

要求:

  1. 要求写一个Netty 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
  2. 实现多人群聊
  3. 服务器端:可以检测用户上线,离线,并实现消息转发功能
  4. 客户端:通过 channel 可以物阻塞发送消息给其他所有用户,同时也可以接收其它用户发送的消息

在这里插入图片描述
Server端

public class GroupChatServer {
    
    

    private int port; //监听端口
    
    public GroupChatServer(int port) {
    
    
        this.port = port;
    }

    //编写run方法,处理客户端的请求
    public void run() throws Exception {
    
    

        //创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop

        try {
    
    
            ServerBootstrap b = new ServerBootstrap();

            b.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 ch) throws Exception {
    
    

                            //获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //向pipeline加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            //向pipeline加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自己的业务处理handler
                            pipeline.addLast(new GroupChatServerHandler());

                        }
                    });
            System.out.println("netty 服务器启动");
            ChannelFuture channelFuture = b.bind(port).sync();

            //监听关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
    
    
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

    }
    public static void main(String[] args) throws Exception {
    
    
        new GroupChatServer(7000).run();
    }
}

Server-Handler

public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {
    
    

    //定义一个 channel 组,管理着所有的 channel
    //GlobalEventExecutor.INSTANCE 是全局的执行器,是一个单例
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);

    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    //handlerAdded 标识连接一旦成功,这个方法第一个被执行
    //将当前 channel 加入到 channelGroup
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
    
    
        Channel channel = ctx.channel();
        //将该客户加入聊天的信息推送给其它在线的客户端
        /**
         * writeAndFlush 方法会将 channelGroup 中所有的 channel 遍历,发送消息
         * 我们自己就不需要遍历了
         */
        channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + "加入聊天" + sdf.format(new Date()) + "\n");
        channelGroup.add(channel);
    }
    
    //断开连接,将**客户离开信息推送给当前在线的客户
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
    
    

        Channel channel = ctx.channel();
        channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + "离开了\n");
        System.out.println("channelGroup size" + channelGroup.size());
    }

    //表示 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
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
    
    

        Channel channel = ctx.channel();
        //这里我们遍历 channelGroup ,根据不同的情况,回送不同的消息
        channelGroup.forEach(ch -> {
    
    
            if (channel != ch) {
    
      //不是当前 channel,就转发消息
                ch.writeAndFlush("[客户]" + channel.remoteAddress() + "发送了消息" + msg + "\n");
            } else {
    
    //回显自己发送的消息给自己
                ch.writeAndFlush("[自己]发送了消息" + msg + "\n");
            }
        });
    }

    //发送了异常
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    
    
        //关闭通道
        System.out.println(cause.getMessage());
        ctx.close();
    }
}

Client端

public class GroupChatClient {
    
    

    //属性
    private final String host;

    private final int port;

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

    public void run() throws InterruptedException {
    
    
        EventLoopGroup group = new NioEventLoopGroup();

        try {
    
    
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
    
    
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
    
    

                            //得到 pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //加入相关的 handler
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());

                            //加入自定义的 handler
                            pipeline.addLast(new GroupChatClientHandler());
                        }
                    });

            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            //得到 channel
            Channel channel = channelFuture.channel();
            System.out.println("========" + channel.localAddress() + "---------");

            //客户端需要输入信息,创建一个扫描器
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
    
    
                String msg = scanner.nextLine();
                //通过 channel 发送到服务器端
                channel.writeAndFlush(msg + "\r\n");

            }
        } finally {
    
    
            group.shutdownGracefully();
        }
    }

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

Client-Handler

public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {
    
    
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
    
    
        System.out.println(msg.trim());
    }
}

测试效果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
说一个当时我的错误
向 Pipeline 指定编码解码器的时候,注意这两种编码解码器类型是不同的:
在这里插入图片描述

(内容很多。未写完,每天都会继续写!!!)
(内容很多。未写完,每天都会继续写!!!)

猜你喜欢

转载自blog.csdn.net/weixin_43582499/article/details/115757962
今日推荐