netty 主要组件+黏包半包+rpc框架+源码透析

本文深入剖析Netty的主要组件,包括EventLoop、Channel、Handler和ByteBuf,并探讨黏包半包问题及其解决方案。通过实例代码展示了Netty的执行流程和事件模型,同时介绍了如何利用定长解码器、行解码器和LTV解码器处理黏包半包。此外,还讨论了Netty中Future和Promise的异步处理机制,以及ByteBuf的内存管理和优势。
摘要由CSDN通过智能技术生成

本文是对netty主要的一些知识点做一个总结,因为netty的底层还是Nio,所以建议有学过Nio的伙伴们再看本文,如果没有Nio基础的小伙伴可以点击链接去看这一篇文件了解一下,这篇文章对Nio也做了比较详细的讲解。
本文主要知识来源: 哔哩哔哩_netty视频 , 下面开始步入正题。

入门

netty底层采用多路复用技术,是异步的。底层也是采用的NIO。

入门代码

要使用netty,第一步当然是导入坐标

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

然后创建服务器端的代码

package com.hs.nettyPrimary;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;

/**
 1. netty入门,服务器端代码
 2. @author hs
 3. @date 2021/07/16
 */
public class HelloServer {
   
    public static void main(String[] args) {
   
        // 服务器端的启动器  负责组装netty的组件 并协调它们工作
        new ServerBootstrap()
             // 添加一个EventLoop组 一个Selector+Thread = EventLoop  loop循环 event事件 也就是循环处理事件  group组
                .group(new NioEventLoopGroup())
                // 选择服务器的ServerSocketChannel实现
                .channel(NioServerSocketChannel.class)
         // child就相当于之前处理read事件的worker  childHandler()方法的作用就是决定了child能干什么事情
         // 方法的参数 ChannelInitializer 就代表跟客户端进行读写的通道 它其实也是一个handler 它的作用的加载其他的handler
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
   
                    @Override
                    protected void initChannel(NioSocketChannel nioSocketChannel) throws Exception {
   
                // 添加具体的handler   StringDecoder 解码 因为传输都是用的ByteBuffer 这里是将ByteBuffer转换为String
                        nioSocketChannel.pipeline().addLast(new StringDecoder());
                        // 自定义handler 处理一些自定义的事情
                        nioSocketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
   
                            @Override
                           public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
   
                                System.out.println("客户端接收的参数" + msg);
                            }
                        });
                    }
                })
                // 端口
                .bind(8080);
    }
}

服务端的大致流程就是:

  1. 创建一个ServerBootstrap() 服务器的启动器
  2. 添加NioEventloopGroup组
  3. 选择ServerSocketChannel的实现
  4. 添加child的处理器,这里仅仅是添加,执行会在客户端连接服务器后执行
  5. 绑定端口

客户端的代码和服务器端很相似

package com.hs.nettyPrimary;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

/**
 * @author hs
 * @date 2021/07/17
 */
public class HelloClient {
   
    public static void main(String[] args) throws InterruptedException {
   
        // 这里也是先创建一个启动器,这是客户端的启动器,注意导包不要导错了
        new Bootstrap()
                // 这是添加EventLoop的作用主要是体现在服务器端,因为服务器要开多个线程充当boss和worker
                .group(new NioEventLoopGroup())
                // 选择客户端SocketChannel的实现
                .channel(NioSocketChannel.class)
                // 添加处理器,下面的方法会在客户端与服务器端连接成功后调用,会调用initChannel()方法
                .handler(new ChannelInitializer<NioSocketChannel>() {
   
                    @Override
                    protected void initChannel(NioSocketChannel socketChannel) throws Exception {
   
                        // 服务器那边要接收数据,接收的是字节 就需要使用decoder来编码
                        // 而客户端这边是发送的字符串,要编码成字节发送,所以这里是使用encoder
                        socketChannel.pipeline().addLast(new StringEncoder());
                    }
                })
                // 连接服务器
                .connect("localhost",8080)
            	// 阻塞方法,直到客户端与服务器端连接建立后才会往下执行
                .sync()
            	// 代表的客户端与服务器之间数据传输的SocketChannel,netty对它做了封装 
                .channel()
                // 发送数据
                .writeAndFlush("hello,netty!");
    }
}

这时候服务器与客户端都启动后,客户端就会在连接服务器成功后往服务器发送一个"hello,netty!" 的消息,首先会经过客户端的Handler将字符串编码为字节数组进行传输,服务器端刚开始接受的也是字节数组,服务器端的Handler会先将字节数组解码为字符串,然后再由下一个Handler进行处理,也就是我们自定义的输出语句。具体的执行流程如下。


执行流程

  • 添加Handel处理器时,仅仅只是添加,还没有执行,需要等到客户端与服务器端连接成功后才会调用initChannel()初始化处理器,等到读写事件发生了就执行相应的处理器

  • 事件发生都会先到EventLoop这里,然后在由Handel处理器来进行实际的处理

  • 收发数据都会经过处理器

在这里插入图片描述

主要组件的理解

  • 服务器端msg:可以理解为流动的数据,服务器这边首先接收的是ByteBuf,然后在经过多个pipeline的处理加工,会变成其他类型的对象

  • channel:数据的通道

  • handler理解为处理数据的工序,工序有多个,合在一起就是pipeline(流水线)。pipeline负责发布事件,传播给每个handler,handler再对自己感兴趣的事情进行处理(在自定义handler中重写了相应的事件处理方法)

    handler分为两类:Inbound和Outbound 也就是入站和出站

  • EventLoop:处理数据的工人,因为EventLoop有一个线程,真正做事的也就是这个线程

    • 工人可以管理多个channel的io操作,并且一个工人如果选择了一个channel就要负责到底(绑定) ,这里主要也只是绑定io操作
    • 工人即可以执行io操作,也可以进行任务处理,每位工人有任务队列,队列里可以堆放多个channel待处理的任务,任务可以分为普通任务和定时任务
    • 工人按照pipeline顺序,依次按照handler的规则(代码)处理数据,对于非io操作的工序可以为指定不同的工人

总之,meg是数据,channel的传输数据的通道,handler是处理数据的工序,多个工序合在一起就是pipeline,EventLoop是处理数据的工人。



主要组件

EventLoop

EventLoop本质上就是一个Selector+一个单线程执行器。里面的run方法处理channel上源源不断的io事件

EventLoop它继承两个类

  • 第一个是继承netty自己的OrderedEventExecutor接口
  • 第二个是继承java.util.concurrent.ScheduledExecutorService接口

我们一般是不会直接用EventLoop 一般是使用EventLoopGroup。

因为我们肯定是需要多个EventLoop,也就相当于是需要多个线程来处理各种事件,而EventLoop本质又是一个单线程执行器。

channel一般会调用EventLoopGroup中的register()方法来绑定其中一个EventLoop,之后这个channel的io事件都又该EventLoop来处理。

创建EventLoopGroup

public static void main(String[] args) {
   
    // 创建EventLoopGroup,它是一个接口,常用的使用就是NioEventLoopGroup
    // 它可以处理io事件,普通任务,定时任务
    // 还有另一个实现 DefaultEventLoop 它可以处理普通任务,定时任务。适用于一些没有io事件发生的场景
    EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
}

一般情况我们都没有往构造方法里面写参数,可以写一个int表示要创建几个EventLoop也就是创建几个线程。那默认会创建几个嘞?

在源码中的第一层,这里默认写了0,然后再一直点下去。

public NioEventLoopGroup() {
   
    this(0);
}

然后到了父类MultithreadEventLoopGroup类

public abstract class MultithreadEventLoopGroup ... {
   
    // 这里最后是创建了cpu核数的两倍 NettyRuntime.availableProcessors()就是cpu核数
    private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

    // 如果为0就采用一个默认的静态常量的值DEFAULT_EVENT_LOOP_THREADS
    protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
   
        super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
    }
}

可以发现,如果我们指定了EventLoop的数量那就以我们指定的为准,如果没有指定那就是采用的cou核数*2


执行普通任务和定时任务

package com.hs.nettyPrimary;

import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @author hs
 * @date 2021/07/17
 */
@Slf4j
public class EventLoopGroupTest {
   
    public static void main(String[] args) {
   
        // 创建EventLoopGroup,它是一个接口,常用的使用就是NioEventLoopGroup
        // 它可以处理io事件,普通任务,定时任务
        // 还有另一个实现 DefaultEventLoop 它可以处理普通任务,定时任务。适用于一些没有io事件发生的场景
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();

        // 获取下一个EventLoop对象
        eventLoopGroup.next();

        // 执行普通任务,因为它还有线程池的方法,就可以使用submit(Runnable run)方法来执行一个普通任务
        // 执行普通任务的好处是可以执行一个异步处理
        eventLoopGroup.next().execute(() -> {
   
            log.debug("执行普通任务");
        });

        // 执行定时任务 scheduleAtFixedRate(任务对象,初始延迟时间 , 间隔时间 , 时间单位)
        // 下面的含义是,刚开始等待1秒执行,然后接下来每隔两秒执行一次
        eventLoopGroup.next().scheduleAtFixedRate(() -> {
   
            log.debug("执行定时任务");
        } , 1 , 2 , TimeUnit.SECONDS);

    }
}

处理io事件

服务器的代码

@Slf4j
public class EventLoopGroupServerTest2 {
   
    public static void main(String[] args) {
   
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
   
                    @Override
                    protected void initChannel(NioSocketChannel socketChannel) throws Exception {
   
                        // 这里就没有定义处理ByteBuf和字符串转换的Handler,仅仅定义一个,现在自己进行转换
                        socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
   
                            @Override
                           public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
   
                                ByteBuf byteBuf = (ByteBuf) msg;
                                log.debug("接收客户端的消息为:" + byteBuf.toString(Charsets.UTF_8));
                            }
                        });
                    }
                })
                .bind(8080);
    }
}

客户端的代码基本上没什么变化
在这里插入图片描述
但是服务器启动后,客户端也启动后,使用debug发送的数据,服务器竟然接收不到,这是因为netty采用的是多线程,而这里的断点主线程和发送数据的线程都停了,这里应该将下面的单选按钮选为Thread,就表示只是停主线程,但是发送数据的线程还是不会停止
在这里插入图片描述

EventLoopGroup

我们现在的代码是如下

new ServerBootstrap()
        .group(new NioEventLoopGroup())
        .channel(NioServerSocketChannel.class)
    	.childHandler(...)
    	.bind(8080);

这里的group()方法里面只是传了一个参数,就相当于这个组里面有处理accept事件的EventLoop 也有处理read事件的EventLoop。我们可以划分的更细,这个方法里面可以传两个EventLoopGroup对象

public static void main(String[] args) {
   
    new ServerBootstrap()
            // 前面负责ServerSocketChannel的accept事件,后面的负责SocketChannel的read事件
            .group(new NioEventLoopGroup() , new NioEventLoopGroup())
        	.channel(NioServerSocketChannel.class)
    		.childHandler(...)
    		.bind(8080);
}

这里就想,group()方法里面的第一个EventLoopGroup要不要指定数量,其实是不用指定的。因为这里只有一个ServerSocketChannel,只会占用一个EventLoop。

进一步细分,我们往pipeline中添加了多个Handler,但如果其中一个Handler耗时较长,进而影响到了其他的Handler执行,我们可以进一步细分,在创建一个NioEventLoopGroup,让这个新的EventLoopGroup专门去处理耗时较长的操作。

@Slf4j
public class EventLoopGroupServerTest2 {
   
    public static void main(String[] args) {
   
        // 创建一个专门处理耗时较长Handler 的EventLoopGroup 
        // 因为不是处理io操作,这里可以使用另一个实现 DefaultEventLoopGroup
        EventLoopGroup eventLoopGroup = new DefaultEventLoopGroup();
        
        new ServerBootstrap()
                // 前面负责ServerSocketChannel的accept事件,后面的负责SocketChannel的read事件
                .group(new NioEventLoopGroup() , new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(。。。)
                .bind(8080);
    }
}

现在的问题就是,EventLoopGroup创建好了后,如何跟这个耗时较长的handler联系起来。其实我们在调用nioSocketChannel.pipeline().addLast() addlast()方法可以接收多个参数

@Slf4j
public class EventLoopGroupServerTest2 {
   
    public static void main(String
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值