RocketMQ源码系列(4) — 基于Netty的网络服务器

专栏:RocketMQ 源码系列

Netty 服务端

NettyRemotingServer

RocketMQ 是基于 Netty 实现的网络通信,NameServer 与 Broker 通信中,NameServer 网络服务器主要由 NettyRemotingServer 实现。

1、成员属性

NettyRemotingServer 继承自网络抽象类 NettyRemotingAbstract,实现了 RemotingServer 接口,这两个在上篇文章已经分析过了,NettyRemotingAbstract 的核心原理就是发送请求和处理响应,发送请求时支持同步执行、异步执行、Oneway执行三种方式。建议先看下上篇文章中 NettyRemotingAbstract 的实现原理,有助于理解服务端实现。

NettyRemotingServer 的成员属性很好理解,主要就是 Netty 服务器相关的组件:

  • ServerBootstrap:Netty 服务器启动类
  • EventLoopGroup:工作线程组,有 bossGroup 和 workerGroup
  • 处理器:HandshakeHandler、NettyEncoder、NettyConnectManageHandler、NettyServerHandler 等
public class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer {
    
    // Netty网络服务器(服务端启动类)
    private final ServerBootstrap serverBootstrap;
    // 负责和客户端建立网络连接的线程组
    private final EventLoopGroup eventLoopGroupBoss;
    // 负责处理网络IO请求读取和处理的线程组
    private final EventLoopGroup eventLoopGroupSelector;
    // Netty Server 配置
    private final NettyServerConfig nettyServerConfig;
    // 业务公共线程池
    private final ExecutorService publicExecutor;
    // 通道事件监听器
    private final ChannelEventListener channelEventListener;
    // 定时任务
    private final Timer timer = new Timer("ServerHouseKeepingService", true);
    // 默认执行器线程组
    private DefaultEventExecutorGroup defaultEventExecutorGroup;

    // TCP 握手处理器
    private HandshakeHandler handshakeHandler;
    // 编码器
    private NettyEncoder encoder;
    // 连接管理器
    private NettyConnectManageHandler connectionManageHandler;
    // Netty 服务端处理器
    private NettyServerHandler serverHandler;
}

2、初始化

NettyRemotingServer 构造方法需要NettyServerConfigChannelEventListener两个组件,NettyServerConfig 就是 Netty 服务端相关配置,这个前面的文章已经有提到过。ChannelEventListener 是网络连接事件处理器,前面分析 NamesrvController 初始化时也分析过了。

NettyRemotingServer 初始化的流程主要如下:

  • 创建 Netty 服务器 ServerBootstrap
  • 创建固定线程数的公共执行器,默认的线程数为 4
  • 根据不同平台,使用不同的 EventLoopGroup
  • 最后加载 SSL 配置
public NettyRemotingServer(final NettyServerConfig nettyServerConfig, final ChannelEventListener channelEventListener) {
    // OneWay 和异步请求 的请求信号量限制,默认为 256、64
    super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue());

    // Netty 配置
    this.nettyServerConfig = nettyServerConfig;
    // Netty Server 启动类
    this.serverBootstrap = new ServerBootstrap();
    // 通道事件监听器
    this.channelEventListener = channelEventListener;

    // Netty 公共线程池线程数,默认为 0
    int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads();
    if (publicThreadNums <= 0) {
        publicThreadNums = 4;
    }
    // 公共线程池,用于执行回调
    this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory() {...});

    // Linux 平台下使用 epoll
    if (useEpoll()) {
        // boss group:负责和客户端建立网络连接的线程组,只有一个线程
        this.eventLoopGroupBoss = new EpollEventLoopGroup(1, new ThreadFactory() {...});
        // worker group:负责处理网络IO请求读取和处理的线程组,默认是3个线程
        this.eventLoopGroupSelector = new EpollEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {...});
    } else {
        // boss group
        this.eventLoopGroupBoss = new NioEventLoopGroup(1, new ThreadFactory() {...});
        // worker group
        this.eventLoopGroupSelector = new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {...});
    }

    // 加载 SSL 配置
    loadSslContext();
}

3、公共线程池

NettyRemotingServer 构造方法中创建了默认 4 个线程的公共执行器,这个线程池主要用在如下两个地方。

  • 注册处理器时,如果没有传入线程池,则使用公共线程池
@Override
public void registerProcessor(int requestCode, NettyRequestProcessor processor, ExecutorService executor) {
    ExecutorService executorThis = executor;
    // 线程池为 null,则默认用公共线程池
    if (null == executor) {
        executorThis = this.publicExecutor;
    }

    Pair<NettyRequestProcessor, ExecutorService> pair = new Pair<NettyRequestProcessor, ExecutorService>(processor, executorThis);
    this.processorTable.put(requestCode, pair);
}
  • NettyRemotingAbstract 中所需的回调线程池也是用的这个公共线程池
@Override
public ExecutorService getCallbackExecutor() {
    return this.publicExecutor;
}

公共线程池的线程数通过 serverCallbackExecutorThreads 配置,如果需要调整可修改这个配置。

4、EventLoopGroup

在初始化 EventLoopGroup 时,如果当前系统是 Linux 平台,且支持 epoll,则创建 EpollEventLoopGroup,否则创建 NioEventLoopGroup。epoll 是支持IO多路复用的系统调用,是实现高性能网络服务器的关键。

private boolean useEpoll() {
    return RemotingUtil.isLinuxPlatform()
            && nettyServerConfig.isUseEpollNativeSelector()
            && Epoll.isAvailable();
}

需要注意的是,在 NettyServerConfig 中 useEpollNativeSelector 默认为 false,在 Linux 平台上建议修改为 true。

private boolean useEpollNativeSelector = false;

EventLoopGroup 需要创建两个:

  • eventLoopGroupBoss:负责和客户端建立网络连接的线程组,固定只有一个线程
  • eventLoopGroupSelector:负责处理网络IO请求读取和处理的线程组,默认是3个线程

eventLoopGroupSelector 是 workerGroup,默认3个线程,通过 serverSelectorThreads 配置。

private int serverSelectorThreads = 3;

需要注意的是,真正的业务处理并不是在 eventLoopGroupSelector 中进行,eventLoopGroupSelector 实际上只负责网络I/O及序列化。真正的业务处理在 NettyRemotingAbstract 已经分析过,业务处理会封装成一个 Runnable,提交到处理器所绑定的线程池中异步处理。而这个线程池的线程数由 serverWorkerThreads 配置,默认为 8。所以如果要调整业务线程池线程数应该修改这个配置值。

private int serverWorkerThreads = 8;

5、服务器创建和启动

NamesrvController 的初始化方法中,创建了网络服务器 NettyRemotingServer。从这段程序可以看出,创建 NettyRemotingServer 时注册了默认的请求处理器,并绑定了默认的线程池,这个线程池就是上面提到的真正的业务处理线程池,默认线程数为 8

public boolean initialize() {
    // 创建 Netty 远程通信服务器,就是初始化 ServerBootstrap
    this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);

    // 业务处理线程池,默认线程数 8 个
    this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
    
    // 注册默认处理器和线程池
    this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.remotingExecutor);

    return true;
}

NamesrvController 初始化完成后,在启动方法中,调用了 start() 方法来启动服务器 NettyRemotingServer。所以服务器的启动还要看 start() 方法。

public void start() throws Exception {
    // 启动 NettyServer
    this.remotingServer.start();

    //...
}

服务器启动

1、核心流程

NettyRemotingServer 服务器启动的流程如下:

  • 创建默认的处理器线程池DefaultEventExecutorGroup,这个主要用于执行自定义的处理器的业务逻辑
  • 初始化处理器,就是成员属性中的 HandshakeHandler、NettyEncoder、NettyConnectManageHandler、NettyServerHandler 几个处理器。
  • 接着就是最核心的配置ServerBootstrap,添加一系列的处理器,然后绑定(bind)监听的端口(9876),至此服务器启动成功。
  • 接着启动NettyEventExecutor,它是 NettyRemotingAbstract 中定义的,主要的作用就是处理 Netty 事件
  • 最后启动一个定时任务,每隔1秒扫描一次 responseTable,关于 responseTable 在上篇文章中已经介绍过,它就是在发送请求时,会封装 ResponseFuture 放进这个响应表中。
@Override
public void start() {
    // 默认业务执行器线程组
    this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
            // 业务线程组,默认8个线程
            nettyServerConfig.getServerWorkerThreads(),
            new ThreadFactory() {...});

    // 初始化处理器
    prepareSharableHandlers();

    ServerBootstrap childHandler =
            // 设置 bossGroup 和 workerGroup 线程组
            this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
                    // 设置网络通信通道,负责监听指定的端口
                    .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                    // SO_BACKLOG 设置全连接队列大小,默认 1024
                    .option(ChannelOption.SO_BACKLOG, nettyServerConfig.getServerSocketBacklog())
                    // SO_REUSEADDR 让端口释放后立即就可以被再次使用
                    .option(ChannelOption.SO_REUSEADDR, true)
                    // 不保持网络连接
                    .option(ChannelOption.SO_KEEPALIVE, false)
                    //禁用Nagle算法,Nagle算法的作用是减少小包的数量
                    .childOption(ChannelOption.TCP_NODELAY, true)
                    // 绑定本地监听的端口,配置的为 9876
                    .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
                    // 添加处理器
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline()
                                    // 添加握手处理器
                                    .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
                                    // 添加其它处理器
                                    .addLast(defaultEventExecutorGroup,
                                            encoder, // 编码器,RemotingCommand 转为 ByteBuf
                                            new NettyDecoder(), // 解码器,ByteBuf 转为 RemotingCommand
                                            // 空闲状态处理器,默认空闲时间为 120s
                                            new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
                                            connectionManageHandler, // 连接管理器
                                            serverHandler // 服务端处理器
                                    );
                        }
                    });
    if (nettyServerConfig.getServerSocketSndBufSize() > 0) {
        // SO_SNDBUF,TCP发送缓冲区的容量上限,默认为 默认64k
        childHandler.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize());
    }
    if (nettyServerConfig.getServerSocketRcvBufSize() > 0) {
        // SO_RCVBUF:TCP接受缓冲区的容量上限,默认为 64k
        childHandler.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize());
    }
    if (nettyServerConfig.getWriteBufferLowWaterMark() > 0 && nettyServerConfig.getWriteBufferHighWaterMark() > 0) {
                nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark());
        // WRITE_BUFFER_WATER_MARK,配置高低水位线
        childHandler.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(
                nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark()));
    }
    if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
        // 设置Buffer分配器,采用池化的模式
        childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
    }

    // 绑定要监听的端口,sync() 同步等待完成
    ChannelFuture sync = this.serverBootstrap.bind().sync();

    // 启动 Netty 事件执行器,开始消费 Netty 事件
    if (this.channelEventListener != null) {
        this.nettyEventExecutor.start();
    }

    // 每隔 1 秒扫描一次 超时过期的请求
    this.timer.scheduleAtFixedRate(new TimerTask() {
        @Override
        public void run() {
            NettyRemotingServer.this.scanResponseTable();
        }
    }, 1000 * 3, 1000);
}

2、ServerBootstrap

ServerBootstrap 的配置其实就是标准的 Netty 服务器的配置,看着是不是很熟悉。

网络连接的一些配置如下:

  • SO_BACKLOG:全连接队列大小,通过serverSocketBacklog配置,默认 1024
  • SO_REUSEADDR:端口释放后立即就可以被再次使用,默认 true
  • SO_KEEPALIVE:是否保持网络连接,默认 false
  • TCP_NODELAY:禁用 Nagle 算法,默认 true
  • SO_SNDBUF:TCP发送缓冲区的容量上限,通过serverSocketSndBufSize配置,默认 0,一般系统默认 64k
  • SO_RCVBUF:TCP接受缓冲区的容量上限,通过serverSocketRcvBufSize配置,默认 0,一般系统默认 64k
  • WRITE_BUFFER_WATER_MARK:配置内核缓冲区高低水位线,通过writeBufferHighWaterMark配置,默认 0
  • ALLOCATOR:设置Buffer分配器,采用池化的模式,默认开启

ServerBootstrap 依次添加了如下几个处理器:

  • 1、HandshakeHandler:SSL/TLS握手处理器
  • 2、NettyEncoder:编码器
  • 3、NettyDecoder:解码器
  • 4、IdleStateHandler:空闲状态处理器
  • 5、NettyConnectManageHandler:网络连接管理器
  • 6、NettyServerHandler:服务端处理器

3、扫描超时的请求

定时任务每隔1秒调用一次 scanResponseTable,从代码可以了解到,其主要就是扫描 responseTable 响应表,判断如果请求超时了,就将其从响应表移除,并释放占用的资源(信号量),并且对于超时的请求,会直接执行回调。

其实我们就是要注意对于内存表中的数据,一定要有过期机制,比如这里采用的定时任务扫描过期的数据,然后从内存表中移除,避免内存表越来越大而发生OOM的情况。

public void scanResponseTable() {
    final List<ResponseFuture> rfList = new LinkedList<ResponseFuture>();
    Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
    while (it.hasNext()) {
        Entry<Integer, ResponseFuture> next = it.next();
        ResponseFuture rep = next.getValue();

        if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= System.currentTimeMillis()) {
            rep.release();
            it.remove();
            rfList.add(rep);
        }
    }

    // 超时的请求直接执行回调
    for (ResponseFuture rf : rfList) {
        executeInvokeCallback(rf);
    }
}

SSL/TLS 认证处理器

服务端 ChannelPipeline 添加的第一个处理器是 HandshakeHandler,见名知意,它是在网络连接建立握手时的处理器,它会在握手时判断是否要SSL/TLS加密传输,如果要加密传输,则添加SslHandler

1、HandshakeHandler

HandshakeHandler 是一个通道输入处理器,它的主要功能是判断客户端是否要建立 SSL/TLS 连接,如果是的,则向管道(ChannelPipeline)中添加 SslHandlerFileRegionEncoder 两个处理器。客户端如果要开启 SSL/TLS 加密功能,也需要添加 SslHandler 处理器,它会在发送数据时对数据加密,服务端接收到数据时则对数据解密。

@ChannelHandler.Sharable
class HandshakeHandler extends SimpleChannelInboundHandler<ByteBuf> {
    // 模式:DISABLED(禁用)、PERMISSIVE(可选的)、ENFORCING(强制加密)
    private final TlsMode tlsMode;
    // 建立 SSL 握手标识
    private static final byte HANDSHAKE_MAGIC_CODE = 0x16;

    HandshakeHandler(TlsMode tlsMode) {
        this.tlsMode = tlsMode;
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        // 标记初始位置
        msg.markReaderIndex();
        // 读取第一个字节
        byte b = msg.getByte(0);
        // SSL/TLS  握手
        if (b == HANDSHAKE_MAGIC_CODE) {
            switch (tlsMode) {
                case DISABLED:
                    ctx.close(); // 服务端禁用SSL,客户端开启SSL,关闭通道
                    break;
                case PERMISSIVE:
                case ENFORCING:
                    // 如果需要加密,添加 SslHandler、FileRegionEncoder 两个处理器
                    ctx.pipeline()
                            .addAfter(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, TLS_HANDLER_NAME, sslContext.newHandler(ctx.channel().alloc()))
                            .addAfter(defaultEventExecutorGroup, TLS_HANDLER_NAME, FILE_REGION_ENCODER_NAME, new FileRegionEncoder());
            }
        } else if (tlsMode == TlsMode.ENFORCING) {
            ctx.close(); // 强制SSL校验,客户端没有SSL认证,则直接关闭通道
        }

        // 复位索引
        msg.resetReaderIndex();
        // 移除当前处理器,避免循环处理
        ctx.pipeline().remove(this);
        // 触发后续的处理器继续操作
        ctx.fireChannelRead(msg.retain());
    }
}

2、TLS 配置

NettyRemotingServer 构造方法中,最后一步调用 loadSslContext() 来加载 SslContext,从这段代码可以知道 TLS 的配置属性在 TlsSystemConfig 中。

public void loadSslContext() {
    TlsMode tlsMode = TlsSystemConfig.tlsMode;

    if (tlsMode != TlsMode.DISABLED) {
        try {
            sslContext = TlsHelper.buildSslContext(false);
        } catch (Exception e) {...}
    }
}

经过分析源码可以得知,TLS 的配置文件默认路径为 /etc/rocketmq/tls.properties,也可以通过系统参数 tls.config.file 指定配置文件的路径。在 TlsSystemConfig 中可以看到有如下的一些配置参数。

public class TlsSystemConfig {
    // TLS 模式,有 disabled、permissive、enforcing 三种,默认为 permissive
    public static final String TLS_SERVER_MODE = "tls.server.mode";
    public static final String TLS_ENABLE = "tls.enable";
    // TLS 配置文件路径,默认为 /etc/rocketmq/tls.properties
    public static final String TLS_CONFIG_FILE = "tls.config.file";
    // 测试模式,默认 true
    public static final String TLS_TEST_MODE_ENABLE = "tls.test.mode.enable";

    // Server 端是否对 Client 进行认证,默认 none
    public static final String TLS_SERVER_NEED_CLIENT_AUTH = "tls.server.need.client.auth";
    // Server 端私钥路径
    public static final String TLS_SERVER_KEYPATH = "tls.server.keyPath";
    // Server 端私钥密码
    public static final String TLS_SERVER_KEYPASSWORD = "tls.server.keyPassword";
    // Server 端证书路径
    public static final String TLS_SERVER_CERTPATH = "tls.server.certPath";
    // 是否严格认证客户端证书,默认 false
    public static final String TLS_SERVER_AUTHCLIENT = "tls.server.authClient";
    // 信任 Client 端证书的证书
    public static final String TLS_SERVER_TRUSTCERTPATH = "tls.server.trustCertPath";

    // Client 端私钥路径
    public static final String TLS_CLIENT_KEYPATH = "tls.client.keyPath";
    // Client 端密码
    public static final String TLS_CLIENT_KEYPASSWORD = "tls.client.keyPassword";
    // Client 端证书路径
    public static final String TLS_CLIENT_CERTPATH = "tls.client.certPath";
    // 是否认证服务端的证书,默认 false
    public static final String TLS_CLIENT_AUTHSERVER = "tls.client.authServer";
    // 信任证书路径
    public static final String TLS_CLIENT_TRUSTCERTPATH = "tls.client.trustCertPath";
}

网络连接管理处理器

1、NettyConnectManageHandler

NettyConnectManageHandler 的主要功能是,在各种事件发生时,如果连接激活、关闭、空闲、异常等事件发生时,去发布一个 Netty 事件。

@ChannelHandler.Sharable
class NettyConnectManageHandler extends ChannelDuplexHandler {
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        // 发布连接事件
        if (NettyRemotingServer.this.channelEventListener != null) {
            NettyRemotingServer.this.putNettyEvent(new NettyEvent(NettyEventType.CONNECT, remoteAddress, ctx.channel()));
        }
    }
    // ...
}

public enum NettyEventType {
    CONNECT,  // 建立连接
    CLOSE, // 连接关闭
    IDLE, // 连接空闲
    EXCEPTION // 出现异常
}

2、事件执行器

发布事件都是在调用父类 NettyRemotingAbstractputNettyEvent 方法,父类封装了一个执行器 NettyEventExecutor 来处理事件。

public void putNettyEvent(final NettyEvent event) {
    this.nettyEventExecutor.putNettyEvent(event);
}

NettyEventExecutor 继承自 ServiceThread,ServiceThread 之前已经介绍过了,它会绑定一个线程在后台运行任务,并提供了优雅的线程终止方式。

NettyEventExecutor 内部使用了一个阻塞队列来存放事件,然后不断从阻塞队列消费事件,根据事件类型触发对应的操作。

class NettyEventExecutor extends ServiceThread {
    // 事件队列
    private final LinkedBlockingQueue<NettyEvent> eventQueue = new LinkedBlockingQueue<NettyEvent>();
    // 最大大小为 10000
    private final int maxSize = 10000;

    // 放入 Netty 事件
    public void putNettyEvent(final NettyEvent event) {
        int currentSize = this.eventQueue.size();
        if (currentSize <= maxSize) {
            this.eventQueue.add(event);
        } else {
            // 超出队列大小直接丢弃...
            log.warn("event queue size [{}] over the limit [{}], so drop this event {}", currentSize, maxSize, event.toString());
        }
    }

    @Override
    public void run() {
        // 通道监听事件
        final ChannelEventListener listener = NettyRemotingAbstract.this.getChannelEventListener();
        // 循环运行
        while (!this.isStopped()) {
            // 消费队列中的事件
            NettyEvent event = this.eventQueue.poll(3000, TimeUnit.MILLISECONDS);
            if (event != null && listener != null) {
                switch (event.getType()) {
                    case IDLE:
                        listener.onChannelIdle(event.getRemoteAddr(), event.getChannel());
                        break;
                    case CLOSE:
                        listener.onChannelClose(event.getRemoteAddr(), event.getChannel());
                        break;
                    case CONNECT:
                        listener.onChannelConnect(event.getRemoteAddr(), event.getChannel());
                        break;
                    case EXCEPTION:
                        listener.onChannelException(event.getRemoteAddr(), event.getChannel());
                        break;
                    default:
                        break;
                }
            }
        }
    }
}

NettyEventExecutor 内部使用了阻塞队列 LinkedBlockingQueue 来存放事件,控制的最大容量为10000,可以看到,在putNettyEvent添加事件时,如果阻塞队列容量超过 10000 时,会直接丢弃这个事件,这种方式还是要考虑对业务没有太大的影响才可以丢弃。

需要注意的是,它这里的判断方式是非原子性的,可能会有并发问题,高并发时,有可能阻塞队列的容量会超过 10000,不过从这里的场景来看,这种误差是可以接受的。如果要保证并发的安全性,可以直接使用阻塞队列来控制容量,首先给队列设置大小,然后用 offer() 方法即可。

// 最大大小为 10000
private final int maxSize = 10000;
// 事件队列
private final LinkedBlockingQueue<NettyEvent> eventQueue = new LinkedBlockingQueue<NettyEvent>(maxSize);

// 放入 Netty 事件
public void putNettyEvent(final NettyEvent event) {
    try {
        eventQueue.offer(event, 1, TimeUnit.SECONDS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

服务端处理器

ServerBootstrap 添加的最后一个处理器是 NettyServerHandler,这就是最后的业务分发处理器了,可以看到它其实就是在调用父类的 processMessageReceived 方法,来处理器请求和响应。

@ChannelHandler.Sharable
class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand> {

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
        // 处理器消息的接收
        processMessageReceived(ctx, msg);
    }
}

通信协议

ServerBootstrap 添加了多个处理器,可以看到,第一个 HandshakeHandler 处理器接收的入参是 ByteBuf,最后一个 NettyServerHandler 接收的入参是 RemotingCommand。ByteBuf 就是从网络传输进来的字节数据,RemotingCommand 可以认为是 RocketMQ 自定义的通信协议。

class HandshakeHandler extends SimpleChannelInboundHandler<ByteBuf>

class NettyServerHandler extends SimpleChannelInboundHandler<RemotingCommand>

在读取数据的时候,要将 ByteBuf 转成 RemotingCommand,在发送数据的时候,要将 RemotingCommand 转成 ByteBuf,这就会涉及到数据的序列化以及编码和解码,对应就是 NettyEncoderNettyDecoder。下面就来看下RocketMQ是如何自定义通信协议,然后实现序列化以及编解码的。

定义协议

RemotingCommand 就是 RocketMQ 自定义的通信协议,它定义了如下一些信息:

  • code:请求编码,RocketMQ 中每个API都有一个编码,服务端接收到这个请求之后根据编码来分发。这个东西就类似于 HTTP 请求的 URL 路径。

  • language:客户端的编程语言,默认是 JAVA,其它的比如 CPP、DOTNET、PYTHON、GO 等等

  • version:发送端 RocketMQ 本身的版本号,这个版本号主要用于不同 Client、Broker、NameServer 版本之间的一些兼容处理。

  • opaque:每个请求都有一个ID,它是用一个全局的 AtomicInteger 自增实现的

  • flag:请求类型,有 Request、Response、Oneway 三种类型

  • customHeader:自定义请求头,需要自定义一个VO实现 CommandCustomHeader 接口,然后创建这个对象来封装数据。注意它是 transient 标识的,不会被序列化。

  • extFields:在序列化时,会反射解析 customHeader 对象中的字段和数据,然后放入 extFields 表中

  • serializeTypeCurrentRPC:序列化的类型,有 JSONROCKETMQ 两种;通过这个字段,Server 端就可以知道 Client 端用的什么方式来序列化,然后就用相同的方式来反序列化。

  • body:要发送的消息主体,注意它是byte数组,需要先将对象序列化成字节数组后再设置进来。它也是 transient 标识的,不会被序列化。

RocketMQ 将消息分为请求头和请求体两部分,请求头就是 RemotingCommand 中可序列化的字段属性,请求体就是 body 字节数组,在后面的分析中就可以看到,RocketMQ 是分开处理这两部分数据的。

public class RemotingCommand {
    private static AtomicInteger requestId = new AtomicInteger(0);

    // 请求编码
    private int code;
    // 编程语言
    private LanguageCode language = LanguageCode.JAVA;
    // 版本号
    private int version = 0;
    // 请求ID
    private int opaque = requestId.getAndIncrement();
    // 标识
    private int flag = 0;
    // 备注
    private String remark;
    // 自定义header
    private transient CommandCustomHeader customHeader;
    // 扩展字段
    private HashMap<String, String> extFields;
    // 序列化类型
    private SerializeType serializeTypeCurrentRPC = serializeTypeConfigInThisServer;
    // 请求的消息体序列化成字节
    private transient byte[] body;
}

编码器与解码器

1、编码器

NettyEncoder 的顶层接口是 ChannelOutboundHandler,这是输出时的一个处理器,NettyEncoder 就是负责在发送请求时对 RemotingCommand 进行编码,将其转成 ByteBuf。

它的逻辑很简单,调用 RemotingCommand 的 fastEncodeHeader 方法编码,将请求头 RemotingCommand 转成字节然后写入 ByteBuf 中。之后再将请求体 body 再写入 ByteBuf 中,注意它这里是将 RemotingCommand 和 body 分开写入的。

@ChannelHandler.Sharable
public class NettyEncoder extends MessageToByteEncoder<RemotingCommand> {
    @Override
    public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out) throws Exception {
        try {
            // 对 RemotingCommand 编码,将数据写入 ByteBuf 中(不包含body)
            remotingCommand.fastEncodeHeader(out);
            // 若果有 body,则写入 body
            byte[] body = remotingCommand.getBody();
            if (body != null) {
                out.writeBytes(body);
            }
        } catch (Exception e) {
            // 关闭通道
            RemotingUtil.closeChannel(ctx.channel());
        }
    }
}

2、解码器

NettyDecoder 的顶层接口是 ChannelInboundHandler,这是输入时的一个处理器,NettyDecoder 负责在接收请求时对 ByteBuf 解码,将其转成 RemotingCommand。

NettyDecoder 继承自 LengthFieldBasedFrameDecoder,也就是说它是基于固定长度的一个解码器。可以看到默认长度为 16777216,也就是 16M,就是说单次请求最大不能超过 16M 的数据,否则就会报错,当然可以通过参数 com.rocketmq.remoting.frameMaxLength 修改这个限制。

从构造方法可以看出,消息的前4个字节用来表示消息的总长度,所以在编码的时候,前 4 个字节一定会写入消息的总长度,然后它的第五个参数(initialBytesToStrip)表示要去掉前4个字节,所以在 decode 中拿到的 ByteBuf 是不包含这个总长度的。

然后解码的核心逻辑,其实就是调用 RemotingCommand.decode(ByteBuf) 方法来解码,将 ByteBuf 转成了 RemotingCommand。

public class NettyDecoder extends LengthFieldBasedFrameDecoder {
    private static final int FRAME_MAX_LENGTH =
        Integer.parseInt(System.getProperty("com.rocketmq.remoting.frameMaxLength", "16777216"));

    public NettyDecoder() {
        // 从0开始,用4个字节来表示长度,解码时删除前4个字节
        // LengthFieldBasedFrameDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, 
        //  int lengthAdjustment, int initialBytesToStrip)
        super(FRAME_MAX_LENGTH, 0, 4, 0, 4);
    }

    @Override
    public Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf frame = null;
        try {
            frame = (ByteBuf) super.decode(ctx, in);
            if (null == frame) {
                return null;
            }
            // ByteBuf 解码为 RemotingCommand
            return RemotingCommand.decode(frame);
        } catch (Exception e) {
            RemotingUtil.closeChannel(ctx.channel());
        } finally {
            if (null != frame) {
                frame.release(); // 释放资源
            }
        }
        return null;
    }
}

编码与解码

从上面的分析可以看出,编码的逻辑主要是 RemotingCommand#fastEncodeHeader 方法,解码的逻辑主要是 RemotingCommand#decode 方法,下面就来分析下这两个方法。

1、序列化方式

序列化的方式有 JSON 序列化以及 RocketMQ 自定义的 ROCKETMQ 序列化两种,可以看到默认是 JSON 序列化,序列化方式可以通过环境变量 ROCKETMQ_SERIALIZE_TYPE 指定,或者通过配置 rocketmq.serialize.type 来指定。

JSON 序列化的体积比较大,一般我们可以修改配置指定为 ROCKETMQ 序列化。

public class RemotingCommand {
    // 序列化类型 JSON
    private static SerializeType serializeTypeConfigInThisServer = SerializeType.JSON;

    static {
        // 从配置文件或环境变量中读取序列化类型配置
        final String protocol = System.getProperty("rocketmq.serialize.type", System.getenv("ROCKETMQ_SERIALIZE_TYPE"));
        if (!isBlank(protocol)) {
            serializeTypeConfigInThisServer = SerializeType.valueOf(protocol);
        }
    }

    // 序列化类型
    private SerializeType serializeTypeCurrentRPC = serializeTypeConfigInThisServer;
}

2、消息编码

NettyEncoder 中调用了 RemotingCommand 的成员方法 fastEncodeHeader 来对数据编码,它的核心逻辑就是将 RemotingCommand 转成字节,写入 ByteBuf 中。

前面说过,消息的前4个字节是用来标识消息的总长度的,所以在写数据的时候,要计算每个写入数据的字节长度的总和。因为 body 和 customHeader 是 transient 标识的,不会被序列化,所以计算长度的时候要把 body 的长度算进去,它这里就包括请求头 RemotingCommand 和请求体 body 的长度之和。

需要注意它前4个字节是保存的消息字节总长度,接着的4个字节存储了请求头的长度和序列化的方式,所以在解码反序列化的时候,它一定会读这个4个字节,看请求头的长度是多少,这样才能和请求头分开。并从这四个字节读取序列化的方式,然后用相同的方式来反序列化。

public void fastEncodeHeader(ByteBuf out) {
    // 消息主体的字节长度
    int bodySize = this.body != null ? this.body.length : 0;
    // 初始位置
    int beginIndex = out.writerIndex();
    // 先占用8个字节
    out.writeLong(0);
    int headerSize;
    // ROCKETMQ 序列化
    if (SerializeType.ROCKETMQ == serializeTypeCurrentRPC) {
        if (customHeader != null && !(customHeader instanceof FastCodesHeader)) {
            // customHeader 转到 extFields
            this.makeCustomHeaderToNet();
        }
        // ROCKETMQ 序列化
        headerSize = RocketMQSerializable.rocketMQProtocolEncode(this, out);
    } 
    // JSON 序列化
    else {
        // customHeader 转到 extFields
        this.makeCustomHeaderToNet();
        // JSON 序列化
        byte[] header = RemotingSerializable.encode(this);
        headerSize = header.length;
        // 直接写入字节
        out.writeBytes(header);
    }
    // 前4个字节写入消息的长度
    out.setInt(beginIndex, 4 + headerSize + bodySize);
    // 4-8字节写入序列化的类型和请求头长度
    out.setInt(beginIndex + 4, markProtocolType(headerSize, serializeTypeCurrentRPC));
}

3、自定义请求头转换

不管是哪种序列化方式,如果传入了自定义请求头 CommandCustomHeader,就会将这个对象中的字段和值解析出来,放入 extFields 扩展字段表中。可以看到它就是通过反射的方式,获取对象中的所有字段,然后通过反射的方式获取字段值。

public void makeCustomHeaderToNet() {
    if (this.customHeader != null) {
        // 获取自定义请求头类中的字段
        Field[] fields = getClazzFields(customHeader.getClass());
        if (null == this.extFields) {
            this.extFields = new HashMap<String, String>();
        }

        for (Field field : fields) {
            // 非静态字段
            if (!Modifier.isStatic(field.getModifiers())) {
                String name = field.getName();
                if (!name.startsWith("this")) {
                    field.setAccessible(true);
                    // 反射读值
                    Object value = field.get(this.customHeader);
                    // 添加到 extFields
                    if (value != null) {
                        this.extFields.put(name, value.toString());
                    }
                }
            }
        }
    }
}

4、消息解码

NettyDecoder 中调用了 RemotingCommand 的静态方法 decode(ByteBuf byteBuffer) 来对传入的数据解码,就是将 ByteBuf 转成 RemotingCommand。

它首先读取数据的总长度,然后读取前四个字节(注意最开始的前四个字节存储的数据总长度,已经去掉),然后从这个整数中去读取请求头的长度和序列化的方式,接着就是对请求头反序列化得到 RemotingCommand。最后,总长度减去请求头的长度,以及前4个字节,剩下的就是请求体 body 了。

public static RemotingCommand decode(final ByteBuf byteBuffer) throws RemotingCommandException {
    // 数据总长度
    int length = byteBuffer.readableBytes();
    // 读取第4-8字节(前4个字节标识总长度,已被去掉)
    int oriHeaderLen = byteBuffer.readInt();
    // 请求头的长度
    int headerLength = getHeaderLength(oriHeaderLen);
    // 序列化类型
    SerializeType serializeType = getProtocolType(oriHeaderLen);
    // 解码得到 RemotingCommand
    RemotingCommand cmd = headerDecode(byteBuffer, headerLength, serializeType);

    // 剩下的就是 body 部分
    int bodyLength = length - 4 - headerLength;
    byte[] bodyData = null;
    if (bodyLength > 0) {
        bodyData = new byte[bodyLength];
        byteBuffer.readBytes(bodyData);
    }
    cmd.body = bodyData;

    return cmd;
}

在对请求头解码时,其实就是从 ByteBuf 读取字节数据,转成 RemotingCommand。根据序列化类型的不同,用对应的方式来反序列化。

private static RemotingCommand headerDecode(ByteBuf byteBuffer, int len, SerializeType type) throws RemotingCommandException {
    switch (type) {
        case JSON:
            byte[] headerData = new byte[len];
            // 读取指定长度的字节到字节数组中
            byteBuffer.readBytes(headerData);
            // JSON 反序列化
            RemotingCommand resultJson = RemotingSerializable.decode(headerData, RemotingCommand.class);
            resultJson.setSerializeTypeCurrentRPC(type);
            return resultJson;
        case ROCKETMQ:
            // ROCKETMQ 反序列化
            RemotingCommand resultRMQ = RocketMQSerializable.rocketMQProtocolDecode(byteBuffer, len);
            resultRMQ.setSerializeTypeCurrentRPC(type);
            return resultRMQ;
        default:
            break;
    }
    return null;
}

一个整数存储两个状态

编码时,是将请求头与请求体分开的,这就需要记录请求头或请求体的长度,这样才能分割这两部分的字节。可以看到,编码时 RocketMQ 用一个 int 型整数就存储了请求头的长度和序列化类型两个状态,这样就能节省内存空间,我们来看下它是如何实现的。

1、编码过程

要知道一个 int 有4个字节,一个字节有8个bit位,它这里其实就是用第1个字节来存储序列化的类型,后3个字节用来存储请求头的长度。

public static int markProtocolType(int source, SerializeType type) {
    return (type.getCode() << 24) | (source & 0x00FFFFFF);
}

SerializeType 有两种类型 JSON(0)、ROCKETMQ(1),也就是将 0 或 1 左移(<<)24 位。比如是 ROCKETMQ 类型,那么这个 int 的二进制就是这样:

0 0 0 0 0 0 0 1 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0  (1 << 24)

接着将请求头长度与 0x00FFFFFF 按位与(&),0x00FFFFFF 的二进制后三个字节全是 1,其实就是将整数转为二进制。假设请求头的长度为 555,其二进制就是 1000101011,按位与的结果如下:

  0 0 0 0 0 0 0 0 | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1  (0x00FFFFFF)
& 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 1 0 | 0 0 1 0 1 0 1 1  (555)
= 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 1 0 | 0 0 1 0 1 0 1 1  (555)

最后再按位或(|),将两部分合为一个整数,这样就可以用一个整数存储两个状态了:

  0 0 0 0 0 0 0 1 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0  (1 << 24)
| 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 1 0 | 0 0 1 0 1 0 1 1  (555)
= 0 0 0 0 0 0 0 1 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 1 0 | 0 0 1 0 1 0 1 1  (16777771)

2、获取长度

解码时会先将前面那个整数读取出来,然后从这个整数中读取请求头的长度。

public static int getHeaderLength(int length) {
    return length & 0x00FFFFFF;
}

因为请求头长度是存储在后3个字节,所以和 0x00FFFFFF 按位与(&)就能得到后3个字节的值:

  0 0 0 0 0 0 0 1 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 1 0 | 0 0 1 0 1 0 1 1  (16777771)
& 0 0 0 0 0 0 0 0 | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1 | 1 1 1 1 1 1 1 1  (0x00FFFFFF)
= 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 1 0 | 0 0 1 0 1 0 1 1  (555)

3、获取序列化类型

同样的,从那个整数中读取序列化的类型。

public static SerializeType getProtocolType(int source) {
    return SerializeType.valueOf((byte) ((source >> 24) & 0xFF));
}

序列化类型存储在第一个字节,所以只需将其右移(>>)24位就可以得到第一个字节的值:

  0 0 0 0 0 0 0 1 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 1 0 | 0 0 1 0 1 0 1 1  (16777771)
  0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 1  (16777771 >> 24)
& 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 1 1 1 1 1 1 1 1  (0xFF)
= 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 0 | 0 0 0 0 0 0 0 1  (1)

5、数据长度问题

由于它是用后三个字节来存储请求头的长度,所以请求头的最大长度为 16777215(0x00FFFFFF),需要注意控制请求头的大小不能超过这个阈值。

NettyDecoder 中解析数据设置的最大长度为 16777216,而这个长度是包括请求头和请求体两部分的长度,所以如果请求头和请求体都比较长,注意修改这个限制。

JSON 序列化

1、JSON 序列化

如果是 JSON 序列化,会调用 RemotingSerializable 的 encode 方法来序列化 RemotingCommand

byte[] header = RemotingSerializable.encode(this);

进去可以看到,它其实就是用 fastjson 包来序列化对象成字符串,然后返回字符串的字节数组。

public static byte[] encode(final Object obj) {
    final String json = toJson(obj, false);
    if (json != null) {
        return json.getBytes(CHARSET_UTF8);
    }
    return null;
}

public static String toJson(final Object obj, boolean prettyFormat) {
    return JSON.toJSONString(obj, prettyFormat);
}

2、JSON 反序列化

对应的,反序列化时,它是从 ByteBuf 读取请求头长度的字节数据,然后调用 RemotingSerializabledecode 方法来反序列化得到 RemotingCommand。

byte[] headerData = new byte[len];
// 读取指定长度的字节到字节数组中
byteBuffer.readBytes(headerData);
// JSON 反序列化
RemotingCommand resultJson = RemotingSerializable.decode(headerData, RemotingCommand.class);

进去可以看到,它就是将字节数组转为字符串,然后用 fastjson 反序列化成对象。

public static <T> T decode(final byte[] data, Class<T> classOfT) {
    final String json = new String(data, CHARSET_UTF8);
    return fromJson(json, classOfT);
}

public static <T> T fromJson(String json, Class<T> classOfT) {
    return JSON.parseObject(json, classOfT);
}

ROCKETMQ 序列化

1、ROCKETMQ 序列化

如果是 ROCKETMQ 序列化,会调用 RocketMQSerializablerocketMQProtocolEncode 方法来序列化 RemotingCommand

headerSize = RocketMQSerializable.rocketMQProtocolEncode(this, out);

进去可以看到,它其实就是将 RemotingCommand 中的属性挨个写入 ByteBuf 中,那么在读取的时候必定也是按写入的顺序读取。

public static int rocketMQProtocolEncode(RemotingCommand cmd, ByteBuf out) {
    // 记录初始位置
    int beginIndex = out.writerIndex();
    // int code(~32767)
    out.writeShort(cmd.getCode());
    // LanguageCode language
    out.writeByte(cmd.getLanguage().getCode());
    // int version(~32767)
    out.writeShort(cmd.getVersion());
    // int opaque
    out.writeInt(cmd.getOpaque());
    // int flag
    out.writeInt(cmd.getFlag());
    // String remark
    String remark = cmd.getRemark();
    if (remark != null && !remark.isEmpty()) {
        writeStr(out, false, remark);
    } else {
        out.writeInt(0); // 数据长度为0
    }

    int mapLenIndex = out.writerIndex();
    out.writeInt(0);
    if (cmd.readCustomHeader() instanceof FastCodesHeader) {
        ((FastCodesHeader) cmd.readCustomHeader()).encode(out);
    }
    HashMap<String, String> map = cmd.getExtFields();
    if (map != null && !map.isEmpty()) {
        map.forEach((k, v) -> {
            if (k != null && v != null) {
                writeStr(out, true, k);
                writeStr(out, false, v);
            }
        });
    }
    // 写入 map 的长度
    out.setInt(mapLenIndex, out.writerIndex() - mapLenIndex - 4);
    // 返回写入的总长度
    return out.writerIndex() - beginIndex;
}

写入数据时,要记录数据的总长度,它这里在写完后,用结束时的 writerIndex 和起始的 writerIndex 相减得到写入的数据总长度。

2、ROCKETMQ 反序列化

ROCKETMQ 反序列化,会调用 RocketMQSerializablerocketMQProtocolDecode 方法来反序列化 ByteBuf 得到 RemotingCommand。

RemotingCommand resultRMQ = RocketMQSerializable.rocketMQProtocolDecode(byteBuffer, len);

进去可以看到,其实就是序列化时的逆向过程,再挨个按顺序将数据读取出来设置到 RemotingCommand 中。

public static RemotingCommand rocketMQProtocolDecode(final ByteBuf headerBuffer, int headerLen) throws RemotingCommandException {
    RemotingCommand cmd = new RemotingCommand();
    // int code(~32767)
    cmd.setCode(headerBuffer.readShort());
    // LanguageCode language
    cmd.setLanguage(LanguageCode.valueOf(headerBuffer.readByte()));
    // int version(~32767)
    cmd.setVersion(headerBuffer.readShort());
    // int opaque
    cmd.setOpaque(headerBuffer.readInt());
    // int flag
    cmd.setFlag(headerBuffer.readInt());
    // String remark
    cmd.setRemark(readStr(headerBuffer, false, headerLen));

    // HashMap<String, String> extFields
    // 读取扩展Map
    int extFieldsLength = headerBuffer.readInt();
    if (extFieldsLength > 0) {
        if (extFieldsLength > headerLen) {
            throw new RemotingCommandException("RocketMQ protocol decoding failed, extFields length: " + extFieldsLength + ", but header length: " + headerLen);
        }
        cmd.setExtFields(mapDeserialize(headerBuffer, extFieldsLength));
    }
    return cmd;
}

3、变长数据处理

对于 int、short、byte 等类型的长度是固定的,所以读取的时候就读取对应长度的数据即可。但如果是字符串、或是 Map 等对象类型,它的长度就不是固定的了。

例如写入字符串时,可以看到,它会固定用一个 int(4字节) 来标识字符串的长度,如果字符串为空,则长度为 0。这样在读取数据时,就会先读取这个长度,再读取后面指定长度的字节,这样就能读取变长数据了。

if (remark != null && !remark.isEmpty()) {
    writeStr(out, false, remark);
} else {
    out.writeInt(0); // 数据长度为0
}

public static void writeStr(ByteBuf buf, boolean useShortLength, String str) {
    // 起始位置
    int lenIndex = buf.writerIndex();
    if (useShortLength) { // 用 short 来记录长度
        buf.writeShort(0);
    } else {
        buf.writeInt(0); // 用 int 来记录长度
    }
    // 写入字符串
    int len = buf.writeCharSequence(str, StandardCharsets.UTF_8);
    // 写入数据长度
    if (useShortLength) {
        buf.setShort(lenIndex, len);
    } else {
        buf.setInt(lenIndex, len);
    }
}

可以看到,读取的时候就是先读取数据长度,再往后读取指定长度的字节。

private static String readStr(ByteBuf buf, boolean useShortLength, int limit) throws RemotingCommandException {
    // 读取长度
    int len = useShortLength ? buf.readShort() : buf.readInt();
    if (len == 0) {
        return null;
    }
    if (len > limit) {
        throw new RemotingCommandException("string length exceed limit:" + limit);
    }
    // 读取指定长度的数据
    CharSequence cs = buf.readCharSequence(len, StandardCharsets.UTF_8);
    return cs == null ? null : cs.toString();
}

消息体序列化

消息主体 body 在设置的时候就是字节数组,那他是怎么序列化的呢。

public void setBody(byte[] body) {
    this.body = body;
}

我们以获取路由数据为例来看看,可以看到它是调用 TopicRouteData 对象的 encode 方法来编码成字节数组。

public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException {
    // 创建相应
    final RemotingCommand response = RemotingCommand.createResponseCommand(null);
    // 获取路由数据
    TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
    // TopicRouteData 编码成字节数组
    byte[] content = topicRouteData.encode();
    // 设置请求体
    response.setBody(content);
    return response;
}

可以看到 TopicRouteData 是继承自 RemotingSerializable 类,这不就是前面提供 JSON 序列化的类吗。

public class TopicRouteData extends RemotingSerializable {}

而所调用的 encode() 方法实际就是调用的 RemotingSerializable 的 encode 方法,它就是将当前对象序列化为 json 字符串,然后返回字符串的字节数组。

public byte[] encode() {
    // 当前对象转为 json 字符串
    final String json = this.toJson();
    if (json != null) {
        return json.getBytes(CHARSET_UTF8);
    }
    return null;
}

而在获取到数据后,就是在调用 RemotingSerializable 的 decode 方法来反序列化成指定的对象。

byte[] body = response.getBody();
if (body != null) {
    return TopicRouteData.decode(body, TopicRouteData.class);
}
public static <T> T decode(final byte[] data, Class<T> classOfT) {
    final String json = new String(data, CHARSET_UTF8);
    return fromJson(json, classOfT);
}

public static <T> T fromJson(String json, Class<T> classOfT) {
    return JSON.parseObject(json, classOfT);
}

从上面的分析可以看出,如果要传输 body 数据,需自定义数据对象继承自 RemotingSerializable,然后调用 encode 和 decode 方法来序列化和反序列化对象。可以得知,请求体 body 部分的序列化是基于 JSON 的序列化方式,请求头是支持 JSON 和 ROCKETMQ 两种序列化方式的。

请求处理器

请求编码

RocketMQ 所有的API都有一个唯一的数字编码与之对应,客户端与服务端就是通过这个编码建立调用关系。所有的编码都维护在 RequestCode 中,所以当我们想了解一个API调用时,比如获取路由数据、发送消息等,就可以通过这个类对应的编码来查找客户端的调用位置和服务端的处理位置。

public class RequestCode {

    public static final int SEND_MESSAGE = 10;

    public static final int PULL_MESSAGE = 11;

    public static final int QUERY_MESSAGE = 12;
    public static final int QUERY_BROKER_OFFSET = 13;
    public static final int QUERY_CONSUMER_OFFSET = 14;
    public static final int UPDATE_CONSUMER_OFFSET = 15;
    public static final int UPDATE_AND_CREATE_TOPIC = 17;
    
    //.........
}

默认处理器

NettyRemotingServer 注册了默认处理器 DefaultRequestProcessor,它就是 NameServer 所有请求的处理中心。可以看到它就是根据不同的请求编码,调用不同的方法来处理客户端请求。

从这里可以看出,NameServer 主要包含 KV配置、NameServer配置、Broker注册/下线、Topic路由管理 四大类API。

public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException {
    switch (request.getCode()) {
        // KV 配置API
        case RequestCode.PUT_KV_CONFIG:
            return this.putKVConfig(ctx, request);
        case RequestCode.GET_KV_CONFIG:
            return this.getKVConfig(ctx, request);
        case RequestCode.DELETE_KV_CONFIG:
            return this.deleteKVConfig(ctx, request);
        case RequestCode.QUERY_DATA_VERSION:
            return queryBrokerTopicConfig(ctx, request);

        // Broker 注册/下线
        case RequestCode.REGISTER_BROKER:
            Version brokerVersion = MQVersion.value2Version(request.getVersion());
            if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
                return this.registerBrokerWithFilterServer(ctx, request);
            } else {
                return this.registerBroker(ctx, request);
            }
        case RequestCode.UNREGISTER_BROKER:
            return this.unregisterBroker(ctx, request);

        // Topic 相关API
        case RequestCode.GET_ROUTEINFO_BY_TOPIC:
            return this.getRouteInfoByTopic(ctx, request);
        case RequestCode.GET_BROKER_CLUSTER_INFO:
            return this.getBrokerClusterInfo(ctx, request);
        case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
            return this.wipeWritePermOfBroker(ctx, request);
        case RequestCode.ADD_WRITE_PERM_OF_BROKER:
            return this.addWritePermOfBroker(ctx, request);
        case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
            return getAllTopicListFromNameserver(ctx, request);
        case RequestCode.DELETE_TOPIC_IN_NAMESRV:
            return deleteTopicInNamesrv(ctx, request);
        case RequestCode.GET_KVLIST_BY_NAMESPACE:
            return this.getKVListByNamespace(ctx, request);
        case RequestCode.GET_TOPICS_BY_CLUSTER:
            return this.getTopicsByCluster(ctx, request);
        case RequestCode.GET_SYSTEM_TOPIC_LIST_FROM_NS:
            return this.getSystemTopicListFromNs(ctx, request);
        case RequestCode.GET_UNIT_TOPIC_LIST:
            return this.getUnitTopicList(ctx, request);
        case RequestCode.GET_HAS_UNIT_SUB_TOPIC_LIST:
            return this.getHasUnitSubTopicList(ctx, request);
        case RequestCode.GET_HAS_UNIT_SUB_UNUNIT_TOPIC_LIST:
            return this.getHasUnitSubUnUnitTopicList(ctx, request);
         // NameServer 配置API
        case RequestCode.UPDATE_NAMESRV_CONFIG:
            return this.updateConfig(ctx, request);
        case RequestCode.GET_NAMESRV_CONFIG:
            return this.getConfig(ctx, request);
        default:
            break;
    }
    return null;
}

网络服务器架构

最后,用一张图来总结下NameServer基于 Netty 的网络服务器设计。

image.png

猜你喜欢

转载自juejin.im/post/7115707196142256158