netty组件详解-上

netty服务端示例:

java 复制代码
private void doStart() throws InterruptedException {
        System.out.println("netty服务已启动");
        // 线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 创建服务器端引导类
            ServerBootstrap server = new ServerBootstrap();
            // 初始化服务器配置
            server.group(group) // 配置处理客户端的连接线程组
                    .channel(NioServerSocketChannel.class) // 指定channel为 NioServerSocketChannel
                    .localAddress(port) // 配置服务端口号
                    .childHandler(new ChannelInitializer<SocketChannel>() { // 指定客户端通信的处理类,添加到pipline中,进行初始化
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            socketChannel.pipeline().addLast(new EchoServerHandler());
                        }
                    });
            // 绑定端口,sync()会阻塞到完成
            ChannelFuture sync = server.bind().sync();
            // 阻塞当前线程,直到服务器的ServerChannel被关闭
            sync.channel().closeFuture().sync();
        }finally {
            // 关闭资源
            group.shutdownGracefully().sync();
        }
    }

netty各组件解析;

  1. EventLoop 与 EventGroup
    EventLoop : 单线程+任务队列
    EventGroup: 多个EventLoop

    思考问题: netty底层每个channel中的事件都是由同一个EventLoop来处理的,而EventLoop是单线程的,这样无需考虑为了并发冲突而加锁的问题,提升了性能。并发高效的本质,不是关注如何科学安全的加锁,而是想尽办法避免加锁,来提升性能。
  2. channel接口
    每个channel都会被注册到一个EventLoop上,以下是Channel抽象出的方法

    每个channel都有自己的生命周期,channel在生命周期的不同节点会回调不同的处理函数:
    (1)当channel被注册到EventLoop中时,会调用isRegistered()方法,来确认是否注册成功
    (2)每个channel在处理事件时,都有一个对应的pipeline,这个pipeline中以责任链模式来处理事件
  3. channelPipeline &&channelHandler
    channelPipeline 的实现是一个双向链表,链表的每个节点对应一个channelHandler,每个channel在处理事件时会调用channelPipeline中的各个channelHandler
    channelHandler也有自己的生命周期,在添加到channelPipeline 或被移除出channelPipeline 时,会调用相应的生命周期方法。
  4. channelPipeline的入站事件和出站事件
    netty如何在同一个channelPipeline中区分出出站事件链路和入站事件链路?

  5. ChanelHandlerContext 上下文
    表示ChannelPipeline和ChannelHandler关联
    ChannelPipeline 是双向链表
    ChanelHandlerContext 维护了双向链表的pre和 next 指针
    具体实现:

    ChanelHandlerContext 的作用不仅仅只是维护了指针信息,而且还需要控制channelPipeline中每个ChannelHandler处理的方向和数据流动,比如像下面这些:

    ChanelHandlerContext 中的写方法区别:

    pipline中有一系列链式的处理逻辑:

    ctx.write(in) / ctx.writeAndFlush(in):
    在某个入站事件中的handler中直接找到pipline中最近的出站事件节点,在出站事件中输出数据.
    ctx.pipeline().write(in) /ctx.channel().write(in)
    在当前入站事件handler结束后,继续按照pipline中handler的顺序依次处理后,在输出数据。
    对比上面两种做法:
    可根据业务需求进行优化,不经过pipeline直接返回的效率更快。
  6. channelHandler的适配器
    channelHandler根据功能,设计了几种适配器,其中包括ChannelOutboundHandlerAdapter出站事件适配器
    问题:为什么ChannelOutboundHandlerAdapter中包含一个read()事件方法
    netty将read()动作打包成一个读事件放到了pipeLine中
  7. channelHandler的并发共享机制:
    根据netty的设计,每个socketchannel都是由一个EventLoop线程处理的,每个channel中包含一个pipeline,而pipeline中的每个handler在使用的时候都是重新new 的一个实例,由于每个socket都是独立的线程隔离的,因此每个socket是线程安全的。
    但有些业务场景需要各个socket之间共享通信,比如要统计服务器接收到的报文总数。此时就要维护一个共享变量 total,每来一个新的
    socket都要去进行 total+1的操作,此时就会产生并发安全问题。
    netty如何解决这个问题呢,我们可以定义一个共享的hander,这个hander的定义成一个全局共享的,每个socketchannel的pipeline都添加这个handler,通过这个并发共享的handler来实现socket进程间的通信,代码如下:
    (1)首先定义一个共享的handler,其内部实现统计报文的业务逻辑:
java 复制代码
// 这个注解的含义就是声明这个handler是共享的
// 如果不声明这个注解的话,在添加到pipline中时会报错
@ChannelHandler.Sharable
public class MessageCountHandler extends ChannelDuplexHandler {
    private static final Logger LOG = LoggerFactory.getLogger(MessageCountHandler.class);
	
    private AtomicLong inCount = new AtomicLong(0);
    private AtomicLong outCount = new AtomicLong(0);

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        LOG.info("收到报文总数:"+inCount.incrementAndGet());
        super.channelRead(ctx, msg);
    }

    @Override
    public void flush(ChannelHandlerContext ctx) throws Exception {
        LOG.info("发出报文总数:"+outCount.incrementAndGet());
        super.flush(ctx);
    }
}

(2)服务器端实现

java 复制代码
public void start() throws InterruptedException {
		// 将统计报文的handler定义成共享变量
        final MessageCountHandler messageCountHandler = new MessageCountHandler();
        /*线程组*/
        EventLoopGroup boss  = new NioEventLoopGroup();
        EventLoopGroup work  = new NioEventLoopGroup();
        try {
            /*服务端启动类*/
            ServerBootstrap b = new ServerBootstrap();
            b.group(boss,work)
            .channel(NioServerSocketChannel.class)/*指定使用NIO的通信模式*/
            .localAddress(new InetSocketAddress(port))/*指定监听端口*/ 
            .childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
                    ch.pipeline().addLast(messageCountHandler); // 添加一个共享的hander到pipeline中
                    ch.pipeline().addLast(new EchoServerMCHandler());
                }
            });
            ChannelFuture f = b.bind().sync();/*异步绑定到服务器,sync()会阻塞到完成*/
            LOG.info("服务器启动完成");
            f.channel().closeFuture().sync();/*阻塞当前线程,直到服务器的ServerChannel被关闭*/
        } finally {
            boss.shutdownGracefully().sync();
            work.shutdownGracefully().sync();
        }
    }
  1. netty内存泄露与资源释放注意事项
    netty底层的实现机制是java的nio,因此其通信机制是面向缓冲区的,在nio中,当channel中有事件发生时,比如读事件时,会将数据读取到一块直接内存中,当我们处理完这部分数据的时候,应该将这块内存资源释放掉,以防止内存泄露。那么这部分netty是怎么做的呢?
    (1)netty中数据的处理是在pipeline中,由每个handler进行处理,那么netty在定义pipeline时,默认在链表的头尾帮我们各自实现了一个handler用于资源分配与释放的。
    源码:

    从这个ch.pipeline()方法点进去后,找到对应实现


我们可以发现,pipeline的创建是基于DefaultChannelPipeline.class这个类

我们找到DefaultChannelPipeline.class这个类,可以看到这head和tail在pipeline初始化的时候就被添加进去了

我们找到这个方法看看这个headContext,可以看到他继承自AbstractChannelHandlerContext,并实现了入站和出站事件处理方法

可以看到这是一个内部类,可以看到,他是实现了资源释放及异常处理的方法

正常情况下,如果事件在pipeline中正常传递的情况下,我们无需手动去管理资源,但是有一种情况,需要手动释放资源

上面的情况就是,当事件读取发生异常,或因为某些业务需求,不能将该事件向pipeline中传递时,需要自己实现资源释放逻辑

此外,大部分的业务逻辑是在入站事件中资源在某个handler中读取异常时终止传递,否则就正常传递,针对这种业务,netty还单独实现了一个handler来实现异常时自动释放资源,即 SimpleChannelInboundHandler:

实现SimpleChannelInboundHandler后,在发生异常时我们无需手动去释放资源,看源码:

因此,到这里,关于资源释放的问题,我们可以有三种做法:

(1)无论何时都保证让业务在pipeline中正常传递,依靠DefaultPipeLine中的head和tail来保证资源的释放

(2)在代码逻辑中手动释放资源 如: ctx.fireChannelRead(msg);

(3)继承SimpleChannelInboundHandler这个类,重写channelRead0(ChannelHandlerContext ctx, ByteBuf msg)方法

  1. 同时处理入站和出站事件
    netty中根据业务模型为我们提供了 ChannelInboundHandlerAdapter 和 ChannelOutboundHandler分别处理入站和出站事件,但是有时,我们需要同时处理入站和出站事件,这里netty为我们提供了 ChannelDuplexHandler 这个实现,我们看源码:
相关推荐
livemetee7 天前
netty单线程并发量评估对比tomcat
java·tomcat·netty
冷环渊13 天前
Finish技术生态计划: FinishRpc
java·后端·nacos·rpc·netty
你熬夜了吗?20 天前
spring中使用netty-socketio部署到服务器(SSL、nginx转发)
服务器·websocket·spring·netty·ssl
异常君20 天前
Netty Reactor 线程模型详解:构建高性能网络应用的关键
java·后端·netty
次元20 天前
初识Netty的奇经八脉
netty
南客先生20 天前
马架构的Netty、MQTT、CoAP面试之旅
java·mqtt·面试·netty·coap
异常君23 天前
一文吃透 Netty 处理粘包拆包的核心原理与实践
java·后端·netty
猫吻鱼24 天前
【Netty4核心原理】【全系列文章目录】
netty
用户905558421480525 天前
AdaptiveRecvByteBuAllocator 源码分析
netty
菜菜的后端私房菜1 个月前
深入剖析 Netty 中的 NioEventLoopGroup:架构与实现
java·后端·netty