RocketMQ通信层原理讲解以及源码解析

1,001 阅读36分钟

RocketMQ消息消息队列集群主要包括NameServer、Broker(Master/Slave)、Producer、Consumer4个角色,基本通信流程如下:

(1)Broker启动后需要完成一次将自己注册至NameServer的操作;随后每隔30s定时向NameServer上报Topic路由信息

(2)消息生产者Producer作为客户端发送消息时候,需要根据消息的Topic从本地缓存的 TopicPublistInfoTable 获取路由信息。如果没有则更新路由会从NameServer上重新拉取,同时Producer默认每隔30s向NameServer拉取一次路由信息

(3)消息生产者Producer根据2)中获取的路由信息选择一个队列(MessageQueue)进行消息发送;Broker作为消息的接受者接受消息并落盘存储

(4)消息消费者Consumer根据2)中获取的路由信息,并再完成客户端的负载均衡后,选择其中的某一个或者某几个消息队列来拉取消息并进行消费。

从上面1)~3)中可以看出在消息生产者,Broker与NameServer之间都会发生通信(这里只说了MQ的部分通信),因此如何设计一个良好的网络通信模块在MQ中至关重要,它将决定RocketMQ集群整体的消息传输能力与最终的性能。

rocketmq-remoting 模块是 RocketMQ 消息队列中负责网络通信的模块,它几乎被其他所有需要网络通信的模块(诸如rocketmq-client、rocket-broker、rocket-namesrv)所依赖和引用。为了实现客户端与服务器之间高效的数据请求与接受,RocketMQ消息队列自定义了通信协议并在Netty的基础之上扩展了通信模块。

为什么要自定义通信协议呢? 答案是肯定的,如果使用现有的http协议,但是其头部信息对于常规应用来说都是无用的,考虑到通信性能等因素,自定义应用层协议是最好的选择。并且Netty本身提供了通用的编码器以及解码器,用于解决TCP传输的 “粘包”、“半包问题”,例如 LengthFieldBasedFrameDecoder

Remoting通信类结构

根据上图的类结构,我们大致可以知道,通信分为客户端和服务端。

客户端使用的通信类:NettyRemotingClient

服务端使用的通信类:NettyRemotingServer

那么谁是客户端?谁又是服务端呢?

站着不同的角度来看是不一样的,

不过可以肯定的是 生产者、消费者是属于客户端,NameServer是属于服务端,而对于Broker来说,站在生产者和消费者的角度来说,Broker属于服务端,而站在NameServer的角度来看,Broker属于客户端。

协议设计与编解码

在Client和Server之间完成一次消息发送的时候,需要对发送的消息格式进行一个协议约定,因此就要必要自定义RocketMQ的消息协议。同时,为了高效地在网络中传输消息和对收到的消息读取,就需要对消息进行编解码。

请大家回想一下使用Netty进行传输数据,是不是需要需要配置多个ChannelHandler?而其中针对于数据编解码的ChannelHandler更是基础,是必须要配置的。

那么我们先来介绍一下RocketMQ定义的NettyDecoder。也就是Netty的解码器,因为我们知道网络数据传输都是二进制字节流,而解码器就是需要将二进制字节流解码为我们应用认识的数据格式。

下图为RocketMQ的数据格式

为大家简单介绍一下上图,其实画的图简单明了,但是还是为大家解释一下。

我们可以看到,首先会根据帧的长度获取到帧的数据,其实就是ByteBuf,然后将ByteBuf解码为RemotingCommand,因此,需要记住的是,在RocketMQ中,RemotingCommand这个类在消息传输过程中对所有数据内容的封装。

整体的RemotingCommand对象的数据格式可以分为 Header + Body。

为了方便表示,用不同的颜色代表不同的数据类型,其中需要关注的是int类型的 headerLength + SerializeType,其中低24位代表headerLength,也就是头的长度,高8位代表序列化类型,也就是需要根据什么样的序列化类型将headerData进行序列化。

RocketMQ默认提供两种 分别是 JSON、RocketMQ。默认使用JSON序列化。

为什么上图没有bodySize?其实bodySize = 总长度 - 4 - headerSize就可以计算出来。

还有一个字段需要关注的是 exFieldsData,是一个Map属性。既然是Map,那么肯定是KV结构,需要知道key的长度和value的长度,因此数据输出顺序为 keySize、key、valueSize、value。

下面表格是为大家介绍头部中每个字段的含义

Header字段类型Request说明Response说明
codeint请求操作码,应答方根据不同的请求码进行不同的业务处理应答响应码。0表示成功,非0则表示各种错误
languageLanguageCode请求方实现的语言例如:JAVA、C++等应答方实现的语言例如:JAVA、C++等
versionint请求方程序的版本应答方程序的版本
opaqueint相当于requestId,请求唯一id,在同一连接上的不同请求标识码,与响应消息中的相对应应答不做修改直接获取请求方的opaque返回
flagint区分是普通RPC还是onewayRPC的标志oneway为单向请求,不关心结果和返回值区分是普通RPC还是onywayRPC的标志
remarkString传输自定义文本信息传输自定义文本信息
exFieldsHashMap<String,String>请求自定义扩展信息响应自定义扩展信息

可见传输内容主要分为以下4部分:

(1)消息长度:总长度,四个字节存储,占用一个int类型;

(2)序列化类型&消息头长度:同样占用一个int类型,第一个字节表示序列化类型,后面三个字节表示消息头长度;

(3)消息头数据:经过序列化后的消息头数据

(4)消息主体数据:消息主题的二进制字节数据内容

消息的通信方式和流程

在RocketMQ消息队列中支持通信的方式主要有同步(sync)、异步(async)、单向(oneway)三种。其中“单向”通信模式相对简单,因为不关注返回结果,并且也不会阻塞等待。一般用在发送心跳包场景下。这里,主要介绍RocketMQ的异步通信流程。异步和同步的区别想必大家应该知道,同步发送端会等待结果,而异步则是不阻塞,传入回调函数等待回调。

可以看到,上面比较核心的四个类以及异步流程的交互,从NettyRemotingClient开始,之后向服务器发送消息,NettyRemotingServer进行处理然后返回结果对象,NettyRemotingClient根据唯一请求id“opaque”获取对应的ResponseFutrue对象,ResponseFutrue对象内存保存了回调函数,执行其回调函数。

上面是异步通信的大致流程,整体细节我们会在生产者章节进行更加全面和深入的介绍,整个生产者发送消息的流程解析。

下个章节会介绍通信层面的一些核心类,以及整个网络交互流程的原理以及源码解析

NettyRemotingAbstract

根据上图的整个通信类结构可以看到,NettyRemotingAbstract抽象类是真正实现 处理请求命令、处理响应命令、发送同步请求、发送异步请求、发送单向请求的类。因此我们先来解析该类的一些核心属性和构造方法

可以看到,构造器中创建了两个用于控制并发的新用量,一个是用于控制异步请求并发数量的,默认最大异步并发数量为65535。另外一个则是控制单向请求并发数量的,也是默认65535

还有两个比较关键的映射表。

  • responseTbale:缓存所有正在进行处理的请求,为什么要缓存?以生产者向NameServer同步获取路由信息为例,ResponseFuture内部实现了可以让生产者线程进行阻塞等待NameServer结果的逻辑,如果不缓存的话,NameServer响应的时候无法获取到生产者线程进行唤醒或者无法执行指定的异步回调函数。并且会定时扫描此表中过期的请求,防止长时间请求未响应导致线程永久阻塞。
  • procesorTable:请求处理器映射表,根据协议码code获取不同的请求处理器进行不同的业务逻辑处理

processMessageRecevied()方法解析

假设我们此时处于服务器端,当客户端发送消息过来以后,首先会进行解码操作,之后RocketMQ通过配置的NettyServerHandler读取到消息以后调用processMessageRecevied()方法

此方法逻辑如下:

总结:根据flag字段判断是请求命令还是响应命令

什么时候会执行到 处理请求命令的逻辑 里面?

我们以Broker为例:当Broker启动后会向NameServer注册自身相关的信息,此时NameServer就需要执行 处理请求命令逻辑,当处理完成以后,向Broker返回响应。 此时的Broker也会走此逻辑,会走处理响应命令逻辑。

    // 参数一:netty层面 channelHandler上下文
    // 参数二:rocketMQ通信层交互对象
    public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
        final RemotingCommand cmd = msg;
        if (cmd != null) {
            // 根据 命令类型 判断是 请求命令还是响应命令
            // 具体判断逻辑其实是请求头中的flag属性的
            // flag字段低1位 为0 代表是请求命令
            // flag字段低1位 为1 代表是响应命令
            switch (cmd.getType()) {
                case REQUEST_COMMAND:
                    // 处理请求命令逻辑
                    processRequestCommand(ctx, cmd);
                    break;
                case RESPONSE_COMMAND:
                    // 处理响应命令逻辑
                    processResponseCommand(ctx, cmd);
                    break;
                default:
                    break;
            }
        }
    }

processRequestCommand()方法解析

用于处理请求命令的,整个大致流程如下:

  1. 根据协议码code获取对应的请求处理器,如果未获取到则使用缺省的请求处理器
  2. 判断请求处理器是否拒绝此请求
  3. 将请求数据封装为Runable对象提交到线程池
  4. 线程池执行请求处理逻辑
  5. 尝试执行rpc钩子before方法(RocketMQ提供给程序员扩展的)
  6. 使用在1)过程中得到的请求处理器进行执行真正的处理请求方法(以Broker向NameServer注册自身信息为例,NameServer使用缺省的请求处理器进行执行)
  7. 请求命令处理完成后尝试执行rpc钩子after方法(RocketMQ提供给程序员扩展的)
  8. 判断此次请求是否是单向请求
  9. 如果不是单向请求则向对端返回响应结果
   // 参数一:netty层面的 管道上下文对象
    // 参数二:通信层交互对象
    public void processRequestCommand(final ChannelHandlerContext ctx, final RemotingCommand cmd) {
        // 根据 code值 获取对应的 处理器 ExecutorService用于执行处理器逻辑的线程池
        final Pair<NettyRequestProcessor, ExecutorService> matched = this.processorTable.get(cmd.getCode());
        // 如果matched为null,使用默认请求处理器
        final Pair<NettyRequestProcessor, ExecutorService> pair = null == matched ? this.defaultRequestProcessor : matched;
        // 获取 唯一请求id
        final int opaque = cmd.getOpaque();

        if (pair != null) {
            // 创建runnable对象
            Runnable run = new Runnable() {
                /**
                 * 执行此方法的时候 说明 已经被包装为了RequestTask对象,并且提交到了线程池
                 */
                @Override
                public void run() {
                    try {
                        // 获取对端地址
                        String remoteAddr = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
                        // 执行rpc钩子方法 before
                        doBeforeRpcHooks(remoteAddr, cmd);
                         // 定义callback对象 此方法不会立即执行,而是被 请求处理器执行完毕后,进行回调
                        final RemotingResponseCallback callback = new RemotingResponseCallback() {
                            @Override
                            public void callback(RemotingCommand response) {
                                // 执行到这里,说明已经处理完毕 可能会存在返回值 response为返回值

                                // 执行rpc钩子方法 after
                                doAfterRpcHooks(remoteAddr, cmd, response);

                                // 条件成立:说明不是单向请求
                                if (!cmd.isOnewayRPC()) {
                                    if (response != null) {
                                        // 设置请求唯一id
                                        response.setOpaque(opaque);
                                        // 标记为响应类型数据 其实就是flag字段的低0位的值是1
                                        response.markResponseType();
                                        try {
                                            // 写入对端
                                            ctx.writeAndFlush(response);
                                        } catch (Throwable e) {
                                            log.error("process request over, but response failed", e);
                                            log.error(cmd.toString());
                                            log.error(response.toString());
                                        }
                                    } else {
                                    }
                                }
                            }
                        };
                        // 条件成立 说明code对应的处理器 是 AsyncNettyRequestProcessor相关的 代表是异步请求
                        // 以默认请求处理器为例,DefaultRequestProcessor 是 继承了AsyncNettyRequestProcessor
                        if (pair.getObject1() instanceof AsyncNettyRequestProcessor) {
                            // 获取对应的处理器
                            AsyncNettyRequestProcessor processor = (AsyncNettyRequestProcessor)pair.getObject1();
                            // 执行异步处理请求 其实内部仍然是调用了processRequest()方法
                            // 没区别其实
                            // DefaultRequestProcessor.asyncProcessRequest()和DefaultRequestProcessor。processRequest()没区别
                            // 但是其他处理器可能是存在区别的
                            processor.asyncProcessRequest(ctx, cmd, callback);
                        } else {
                            // 获取对应的处理器
                            NettyRequestProcessor processor = pair.getObject1();
                            // 处理同步请求
                            RemotingCommand response = processor.processRequest(ctx, cmd);
                            // 处理完成后调用回调方法
                            callback.callback(response);
                        }
                    } catch (Throwable e) {
                        log.error("process request exception", e);
                        log.error(cmd.toString());

                        // 条件成立 说明不是单向请求
                        // 单向请求是指 对端发送数据之后,不需要响应对象
                        if (!cmd.isOnewayRPC()) {
                            final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_ERROR,
                                RemotingHelper.exceptionSimpleDesc(e));
                            response.setOpaque(opaque);
                            ctx.writeAndFlush(response);
                        }
                    }
                }
            };

            // 条件成立 说明此code对应的处理器拒绝所有请求
            if (pair.getObject1().rejectRequest()) {
                // 创建 响应对象 响应码类型为 系统忙碌 SYSTEM_BUSY 2
                final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                    "[REJECTREQUEST]system busy, start flow control for a while");
                // 设置请求带来的唯一id
                response.setOpaque(opaque);
                // 写回对端
                ctx.writeAndFlush(response);
                return;
            }

            try {
                // 包装成RequestTask对象
                // 参数一:runnable对象
                // 参数二:通道关联的channel
                // 参数三:cmd对象 通信对象
                final RequestTask requestTask = new RequestTask(run, ctx.channel(), cmd);
                // pair内部存在两个对象 object1是处理器 object2是执行此处理器逻辑的线程池
                // 向线程池提交requestTask任务
                pair.getObject2().submit(requestTask);
            } catch (RejectedExecutionException e) {
                if ((System.currentTimeMillis() % 10000) == 0) {
                    log.warn(RemotingHelper.parseChannelRemoteAddr(ctx.channel())
                        + ", too many requests and system thread pool busy, RejectedExecutionException "
                        + pair.getObject2().toString()
                        + " request code: " + cmd.getCode());
                }

                if (!cmd.isOnewayRPC()) {
                    final RemotingCommand response = RemotingCommand.createResponseCommand(RemotingSysResponseCode.SYSTEM_BUSY,
                        "[OVERLOAD]system busy, start flow control for a while");
                    response.setOpaque(opaque);
                    ctx.writeAndFlush(response);
                }
            }
        } else {
            String error = " request type " + cmd.getCode() + " not supported";
            final RemotingCommand response =
                RemotingCommand.createResponseCommand(RemotingSysResponseCode.REQUEST_CODE_NOT_SUPPORTED, error);
            response.setOpaque(opaque);
            ctx.writeAndFlush(response);
            log.error(RemotingHelper.parseChannelRemoteAddr(ctx.channel()) + error);
        }
    }

我们以Broker向NameServer发送心跳包信息为例,会根据协议码code获取到缺省的请求处理器。

我们以缺省的请求处理器为切入点来看上述的 处理请求命令 的后续逻辑。

然后会进入到 processRequest() 逻辑

根据协议码去执行不同的业务逻辑,以Broker向NameServer为例,code码为 REGISTER_BROKER,因此会走 registerBrokerWithFilterServer()逻辑,到这里,就非常接近我们在解析NameServer源码时 讲到的 路由注册的代码逻辑了。

    @Override
    public RemotingCommand processRequest(ChannelHandlerContext ctx,
        RemotingCommand request) throws RemotingCommandException {

        if (ctx != null) {
            log.debug("receive request, {} {} {}",
                request.getCode(),
                RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
                request);
        }


        // 根据请求code码 执行不同的业务逻辑
        switch (request.getCode()) {
            case RequestCode.REGISTER_BROKER:
                // 获取broker的版本号
                Version brokerVersion = MQVersion.value2Version(request.getVersion());
                // 条件成立,说明mq版本大于3.0.11 我们是4.9.3 因此关注此逻辑
                if (brokerVersion.ordinal() >= MQVersion.Version.V3_0_11.ordinal()) {
                    // 参数一:ctx 上下文对象
                    // 参数二:请求对象
                    return this.registerBrokerWithFilterServer(ctx, request);
                } else {
                    return this.registerBroker(ctx, request);
                }
            case RequestCode.UNREGISTER_BROKER:
                return this.unregisterBroker(ctx, request);
            // 根据topic获取路由信息
            // 以生产者发送消息为例 当生产者本地缓存映射表中不存在指定的topic时
            // 会向namesrv获取topic路由信息 code码为GET_ROUTEINFO_BY_TOPIC
            case RequestCode.GET_ROUTEINFO_BY_TOPIC:
                // 参数一:ctx 上下文对象
                // 参数二:请求信息
                return this.getRouteInfoByTopic(ctx, request);
            // 获取broker集群信息
            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);
            case RequestCode.UPDATE_NAMESRV_CONFIG:
                return this.updateConfig(ctx, request);
            case RequestCode.GET_NAMESRV_CONFIG:
                return this.getConfig(ctx, request);
            default:
                break;
        }
        return null;
    }

registerBrokerWithFilterServer()逻辑总结

  • 将本次路由注册请求所需要的信息从 extFields 表中 提取出来,为了方便操作,会将提取出来的信息封装为一个类,类名称为 RegisterBrokerRequestHeader
  • 将消息体使用crc32算法进行数据校验
  • 将消息体byte数组编码为 RegisterBrokerBody类,主要存放topic相关信息
  • 调用RouteInfoManager.registerBroker()操作进行真正的路由注册
  • 获取路由注册结果,返回响应对象
    // 参数一:ctx 上下文对象
    // 参数二:请求对象
    public RemotingCommand registerBrokerWithFilterServer(ChannelHandlerContext ctx, RemotingCommand request)
        throws RemotingCommandException {
        // 创建 SYSTEM_ERROR 响应命令对象
        // RemotingCommand.customHeader = 反射创建RegisterBrokerResponseHeader的对象
        final RemotingCommand response = RemotingCommand.createResponseCommand(RegisterBrokerResponseHeader.class);
        // 获取自定义header RegisterBrokerResponseHeader
        final RegisterBrokerResponseHeader responseHeader = (RegisterBrokerResponseHeader) response.readCustomHeader();
        // 从request中提取RegisterBrokerRequestHeader信息 此时正常情况下requestHeader中的属性是有值的
        // 比如 brokerName、brokerAddr、clusterName、haServerAddr、brokerId
        final RegisterBrokerRequestHeader requestHeader =
            (RegisterBrokerRequestHeader) request.decodeCommandCustomHeader(RegisterBrokerRequestHeader.class);

        // 用于数据校验 crc32 循环冗余校验
        if (!checksum(ctx, request, requestHeader)) {
            response.setCode(ResponseCode.SYSTEM_ERROR);
            response.setRemark("crc32 not match");
            return response;
        }

        // 生成注册broker需要的body数据 主要存放topic相关信息
        RegisterBrokerBody registerBrokerBody = new RegisterBrokerBody();

        if (request.getBody() != null) {
            try {
                // 提取body数据到registerBrokerBody中
                registerBrokerBody = RegisterBrokerBody.decode(request.getBody(), requestHeader.isCompressed());
            } catch (Exception e) {
                throw new RemotingCommandException("Failed to decode RegisterBrokerBody", e);
            }
        } else {
            registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setCounter(new AtomicLong(0));
            registerBrokerBody.getTopicConfigSerializeWrapper().getDataVersion().setTimestamp(0);
        }

        // 通过 routeInfoManager 注册broker 关键方法
        // 参数一:集群名称
        // 参数二:broker地址
        // 参数三:broker名称
        // 参数四:brokerID
        // 参数五:ha服务地址
        // 参数六:TopicConfigSerializeWrapper 存放topic相关信息
        // 参数七:类模式消息过滤相关
        RegisterBrokerResult result = this.namesrvController.getRouteInfoManager().registerBroker(
            requestHeader.getClusterName(),
            requestHeader.getBrokerAddr(),
            requestHeader.getBrokerName(),
            requestHeader.getBrokerId(),
            requestHeader.getHaServerAddr(),
            registerBrokerBody.getTopicConfigSerializeWrapper(),
            registerBrokerBody.getFilterServerList(),
            ctx.channel());

        // 设置结果对象
        responseHeader.setHaServerAddr(result.getHaServerAddr());
        responseHeader.setMasterAddr(result.getMasterAddr());
        
        byte[] jsonValue = this.namesrvController.getKvConfigManager().getKVListByNamespace(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG);
        response.setBody(jsonValue);

        // 返回响应
        response.setCode(ResponseCode.SUCCESS);
        response.setRemark(null);
        return response;
    }

而RouteInfoManager内部的逻辑我们在NameServer章节已经详细介绍过了,遗忘的同学们可以再去回顾一下其逻辑,这样就不再向大家介绍了。

在为大家贴一下 从 extFiles 属性中提取所需信息的代码

    public CommandCustomHeader decodeCommandCustomHeader(
        Class<? extends CommandCustomHeader> classHeader) throws RemotingCommandException {
        CommandCustomHeader objectHeader;
        try {
            // 反射创建classHeader对象
            objectHeader = classHeader.newInstance();
        } catch (InstantiationException e) {
            return null;
        } catch (IllegalAccessException e) {
            return null;
        }

        // 注册broker需要的相关信息 都被放入了此 扩展属性map中
        if (this.extFields != null) {

            // 获取classHeader的所有属性
            Field[] fields = getClazzFields(classHeader);
            for (Field field : fields) {
                // 条件成立:说明属性不是静态修饰的
                if (!Modifier.isStatic(field.getModifiers())) {
                    // 获取属性名
                    String fieldName = field.getName();
                    // 条件成立:属性名称不是以this开头
                    if (!fieldName.startsWith("this")) {
                        try {
                            // 获取对应的value值
                            String value = this.extFields.get(fieldName);
                            if (null == value) {
                                // 条件成立:说明 field字段上标注了CFNotNull注解,不能为空,
                                // 但是extFields未获取到相关的值 抛出异常
                                if (!isFieldNullable(field)) {
                                    throw new RemotingCommandException("the custom field <" + fieldName + "> is null");
                                }
                                continue;
                            }
                            // 设置可访问的
                            field.setAccessible(true);
                            // 获取字段类型
                            String type = getCanonicalName(field.getType());
                            // 用于保存value的值
                            Object valueParsed;

                            // 条件成立:说明是String类型
                            if (type.equals(STRING_CANONICAL_NAME)) {
                                valueParsed = value;
                            //  条件成立:说明是Integer或者int类型
                            } else if (type.equals(INTEGER_CANONICAL_NAME_1) || type.equals(INTEGER_CANONICAL_NAME_2)) {
                                valueParsed = Integer.parseInt(value);
                            } else if (type.equals(LONG_CANONICAL_NAME_1) || type.equals(LONG_CANONICAL_NAME_2)) {
                                valueParsed = Long.parseLong(value);
                            } else if (type.equals(BOOLEAN_CANONICAL_NAME_1) || type.equals(BOOLEAN_CANONICAL_NAME_2)) {
                                valueParsed = Boolean.parseBoolean(value);
                            } else if (type.equals(DOUBLE_CANONICAL_NAME_1) || type.equals(DOUBLE_CANONICAL_NAME_2)) {
                                valueParsed = Double.parseDouble(value);
                            } else {
                                throw new RemotingCommandException("the custom field <" + fieldName + "> type is not supported");
                            }

                            // 设置属性值
                            field.set(objectHeader, valueParsed);

                        } catch (Throwable e) {
                            log.error("Failed field [{}] decoding", fieldName, e);
                        }
                    }
                }
            }

            objectHeader.checkFields();
        }

        return objectHeader;
    }

至此以路由注册请求举例,将整个请求链路梳理清楚了。 当然后续会为大家介绍Netty配置的一些Handler的用处,让大家更深的理解整个请求流程。

其他的请求流程也是类似的,会根据不同的code值进行不同的业务处理

processResponseCommand()方法解析

既然是处理响应命令的,那么肯定是向对端发送了请求,然后对端返回了结果,需要对结果进行处理。

后续我们会详细讲解 请求发送逻辑,这里先简要说一下。

以生产者同步向NameServer请求获取路由信息为例,会生成请求唯一id “opaque”和 ResponseFuture对象,然后以opaque为key,value为ResponseFuture对象写入到responseTbale表中。之后,生产者线程会通过ResponseFuture进行线程阻塞,当NameServer将响应结果发送给生产者的时候,并且携带生产者生成的opaque值,生产者此时就需要执行 processResponseCommand() 方法的逻辑了。

其实简单想一下也知道后续需要干什么,根据opaqueresponseTbale映射表中获取对应的ResponseFutrue,然后唤醒内部的生产者线程,并且将响应结果写入到 ResponseFuture中,生产者线程可以从阻塞的状态恢复到运行态,并且根据 ResponseFuture 中的响应结果进行后续的处理逻辑。

再简单提一下,ResponseFuture内部实现线程阻塞的方式其实就是通过JUC包下的CountDownLatch,遗忘了CountDownLatch的同学可以去关注我的JUC相关章节。

    public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {
        // 获取唯一id
        final int opaque = cmd.getOpaque();
         // 获取请求时放入的future 如果未获取到说明可能 请求超时了
        final ResponseFuture responseFuture = responseTable.get(opaque);
        if (responseFuture != null) {
            // 设置结果
            responseFuture.setResponseCommand(cmd);

            // 从映射表中移除
            responseTable.remove(opaque);

            // 条件成立:说明是异步 执行异步回调逻辑
            if (responseFuture.getInvokeCallback() != null) {
                executeInvokeCallback(responseFuture);
            } else {
                // 走这里 说明是同步 生产者线程此时正处于阻塞
                // putResponse内部会调用 this.countDownLatch.countDown()
                responseFuture.putResponse(cmd);
                responseFuture.release();
            }
        } else {
            log.warn("receive response, but not matched any request, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
            log.warn(cmd.toString());
        }
    }

上述代码的关键点 是 opaque,注意这里的opaque不是NameServer生产的,而是生产者将opaque的值发送给NameServer,然后NameServer又将此值返回了,也就是说整个通信过程opaque的值都是不变的。

还有一处代码关键点:设置响应结果到ResponseFuture,并且唤醒其内部阻塞的线程

后续生产者线程被唤醒并且获取到结果,然后进行后续的操作。至此整个处理响应命令的流程也就介绍完毕了,后续为大家继续介绍请求发送相关的。

invokeSyncImpl()方法解析

此方法用于向对端发送数据,是同步的。

以生产者向NameServer获取路由信息为例,生产者会调用此方法进行发送数据,等待NameServer返回路由数据就行处理。

代码流程总结:以生产者向NameServer获取路由信息为例

(1) 生成唯一请求id “opaque”

(2) 生成ResponseFutrue对象

(3) 以opaque为key,value为ResponseFutrue,放入responseTable映射表中

(4) 调用Netty通道的writeAndFlush()方法

(5) 生产者线程进行阻塞等待NameServer返回路由数据

(6)返回响应结果

至于创建Channel并且连接Channel以及后续的处理不在此方法,此方法是负责单纯的发送逻辑

    // 参数一:channel
    // 参数二:请求对象
    // 参数三:更新后的超时时间
    public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
        final long timeoutMillis)
        throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
        // 获取opaque的值 是一个自增的整数值  requestId.getAndIncrement() 类似于请求唯一id
        final int opaque = request.getOpaque();

        try {
            // 生成responseFuture对象
            // 参数一:channel
            // 参数二:请求对象
            // 参数三:更新后的超时时间
            // 参数四:由于是同步调用 因此无回调 为null
            // 参数五:是否是单向请求 不是 因此为null
            final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
            // 放入响应映射表中
            this.responseTable.put(opaque, responseFuture);
            
            final SocketAddress addr = channel.remoteAddress();
            // 将数据写入到对端 成功写入到对端以后会回调监听器
            channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture f) throws Exception {
                    if (f.isSuccess()) {
                        responseFuture.setSendRequestOK(true);
                        return;
                    } else {
                        responseFuture.setSendRequestOK(false);
                    }
                    // 执行到这里,说明发送消息失败了
                    
                    // 从请求表中移除自身
                    responseTable.remove(opaque);
                    // 设置错误原因
                    responseFuture.setCause(f.cause());
                    // 设置结果 唤醒在responseFuture阻塞的线程
                    responseFuture.putResponse(null);
                    log.warn("send a request command to channel <" + addr + "> failed.");
                }
            });

            // responseFuture内部存在一个countDownLatch
            // 以生产者向namrsrv发送消息为例,写namesrv发送数据以后,生产者线程会调用responseFuture内部的countDownLatch阻塞自己
            // 等待namesrv响应数据,如果在指定的超时时间没不存在结果,生产者线程会从阻塞状态更改为运行态,会抛出异常
            RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);

            // 执行到这里,有两种可能
            // 1.在指定超时时间内namrsrv返回了数据
            // 2.到达超时时间

            // 条件成立:说明 到达超时时间
            if (null == responseCommand) {
                if (responseFuture.isSendRequestOK()) {
                    throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
                        responseFuture.getCause());
                } else {
                    throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
                }
            }

            // 执行到这里,说明在指定时间内返回了数据 返回响应对象
            return responseCommand;
        } finally {
            // 移除 请求缓存映射
            this.responseTable.remove(opaque);
        }
    }

当生产者线程阻塞后,NameServer返回数据给生产者Client,根据“opaque”在responseTable映射表中获取ResponseFuture对象,将生产者线程唤醒,生产者线程继续执行后续逻辑

inokeAsyncImpl()方法解析

整个异步过程其实是和同步过程有点类似的,区别在于生产者线程不会阻塞,而是指定一个异步回调函数保存在ResponseFuture,NameServer返回数据后,根据根据“opaque”在responseTable映射表中获取ResponseFuture对象,执行异步回调函数逻辑

    // 参数一:channel
    // 参数二:请求对象 网络交互对象
    // 参数三:超时时间
    // 参数四:结果回调函数
    public void invokeAsyncImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis,
        final InvokeCallback invokeCallback)
        throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
        long beginStartTime = System.currentTimeMillis();
        final int opaque = request.getOpaque();
        // 尝试获取信号量 用于限制异步并发数量  65536
        boolean acquired = this.semaphoreAsync.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
        // 条件成立:获取成功
        if (acquired) {
            // 后续用于释放获取到的信号量
            final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreAsync);
            long costTime = System.currentTimeMillis() - beginStartTime;
            // 条件成立:说明已经到达超时时间 需要抛出异常
            if (timeoutMillis < costTime) {
                once.release();
                throw new RemotingTimeoutException("invokeAsyncImpl call timeout");
            }

            // 生成ResponseFuture对象
            // 参数一:channel
            // 参数二:opaque 请求id
            // 参数三:超时时间
            // 参数四:回调函数
            // 参数五:后续用于释放 获取到的信号量
            // responseFuture内部保存 回调函数 可以利用ResponseFuture执行回调函数
            final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis - costTime, invokeCallback, once);
            // 写入到请求映射表中 所有已发送未完成的请求都会缓存在此表中,用于判断是否超时以及实现线程阻塞等逻辑
            this.responseTable.put(opaque, responseFuture);
            try {
                // 将数据写入对端
                channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture f) throws Exception {
                        if (f.isSuccess()) {
                            // 写入对端成功,设置响应结果
                            responseFuture.setSendRequestOK(true);
                            return;
                        }
                        // 执行到这里 说明写入失败
                        requestFail(opaque);
                        log.warn("send a request command to channel <{}> failed.", RemotingHelper.parseChannelRemoteAddr(channel));
                    }
                });
            } catch (Exception e) {
                responseFuture.release();
                log.warn("send a request command to channel <" + RemotingHelper.parseChannelRemoteAddr(channel) + "> Exception", e);
                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
            }
        } else {
            if (timeoutMillis <= 0) {
                throw new RemotingTooMuchRequestException("invokeAsyncImpl invoke too fast");
            } else {
                String info =
                    String.format("invokeAsyncImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreAsyncValue: %d",
                        timeoutMillis,
                        this.semaphoreAsync.getQueueLength(),
                        this.semaphoreAsync.availablePermits()
                    );
                log.warn(info);
                throw new RemotingTimeoutException(info);
            }
        }
    }

invokeOnewayImpl()方法解析

整个发送单向请求的逻辑是最简单的,因为不需要获取到结果,因此也不需要将请求缓存到responseTable中。

    // 参数一:channel 通道 数据通信
    // 参数二:数据交互对象
    // 参数三:超时时间
    public void invokeOnewayImpl(final Channel channel, final RemotingCommand request, final long timeoutMillis)
        throws InterruptedException, RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException {
        // 标记此次请求为单向请求 其实是将flag字段的低2位的值 更改位1
        request.markOnewayRPC();
        // 获取单向请求并发限制信号量
        boolean acquired = this.semaphoreOneway.tryAcquire(timeoutMillis, TimeUnit.MILLISECONDS);
        // 条件成立:获取成功
        if (acquired) {
            // 此类用于释放获取的信号量
            final SemaphoreReleaseOnlyOnce once = new SemaphoreReleaseOnlyOnce(this.semaphoreOneway);
            try {
                // 数据写入到对端
                channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
                    @Override
                    public void operationComplete(ChannelFuture f) throws Exception {
                        // 写入完成 回调监听器的此方法
                        // 释放信号量
                        once.release();
                        if (!f.isSuccess()) {
                            log.warn("send a request command to channel <" + channel.remoteAddress() + "> failed.");
                        }
                    }
                });
            } catch (Exception e) {
                once.release();
                log.warn("write send a request command to channel <" + channel.remoteAddress() + "> failed.");
                throw new RemotingSendRequestException(RemotingHelper.parseChannelRemoteAddr(channel), e);
            }
        } else {
            if (timeoutMillis <= 0) {
                throw new RemotingTooMuchRequestException("invokeOnewayImpl invoke too fast");
            } else {
                String info = String.format(
                    "invokeOnewayImpl tryAcquire semaphore timeout, %dms, waiting thread nums: %d semaphoreOnewayValue: %d",
                    timeoutMillis,
                    this.semaphoreOneway.getQueueLength(),
                    this.semaphoreOneway.availablePermits()
                );
                log.warn(info);
                throw new RemotingTimeoutException(info);
            }
        }
    }
    
  

NettyRemotingServer

NettyRemotingServer是属于服务端的通信层核心类。

启动Netty的逻辑就在NettyRemotingServer内部,谁会应用NettyRemotingServer?NameServer启动的时候会调用NettyRemotingServer.start()方法进行启动Netty服务器,Broker启动的时候也会调用NettyRemotingServer.start()方法进行启动Netty服务器。

并且Netty服务器启动的时候需要配置一些ChannelHandler,而所需要的ChannelHandler都属于NettyRemotingServer的内部类,因此还是非常有必要探究一下此类的。

我们已经介绍过NettyRemotingAbstract了,NettyRemotingServer是继承NettyRemotingAbstract类的。

看一下整体核心属性结构图:

看一下类的结构

简单看一下此类的 同步发送请求、异步发送请求、单向发送请求的逻辑,其实非常简单,只是调用基类的实现逻辑而已。

还有 注册相关的逻辑,注册处理器、注册RPC钩子(程序员扩展)等

介绍完这些简单的逻辑,我们介绍一下NettyRemotingServer也就是网络层服务器通信对象的启动流程

以及给出整个启动后的架构图

NetttRemotingServer启动流程

首先来说肯定是NettyRemotingServer的构造器,在讲解构造器方法之前,先来介绍一下NettyRemotingServer的一些属性

public class NettyRemotingServer extends NettyRemotingAbstract implements RemotingServer {   
    // 服务端启动类
    private final ServerBootstrap serverBootstrap;

    // workerzu
    private final EventLoopGroup eventLoopGroupSelector;
    // boss组
    private final EventLoopGroup eventLoopGroupBoss;
    // netty服务器启动所需要的网络配置参数
    private final NettyServerConfig nettyServerConfig;

    // 公共线程池
    private final ExecutorService publicExecutor;
    
    // 代码省略...

    // netty启动配置的一系列ChannelHandler
    private HandshakeHandler handshakeHandler;
    private NettyEncoder encoder;
    private NettyConnectManageHandler connectionManageHandler;
    private NettyServerHandler serverHandler;
    
    // 代码省略...
    
    
    // 代码省略...
}

下面手整个NettyRemotingServer创建逻辑

流程总结:

简单来讲,生成线程池,创建Netty所需要的boss组和worker组,形成Reactor线程模型。

主要注意的是:当如果发现操作系统平台是Linux内核平台时候,会使用性能更好的EpollEventLoopGroup,而不是使用普通的NioEventLoop

如果是Linux内核下则创建EpollEventLoopGroup,否则创建NioEventLoopGroup

两者有什么区别? 谁的性能更高?

答案是:EpollEventLoopGroup

为什么?stackoverflow也解释过

If you are running on linux you can use EpollEventLoopGroup and so get better performance,

less GC and have more advanced features that are only available on linux.

如果您在linux上运行,您可以使用EpollEventLoopGroup,从而获得更好的性能,更少的GC,并拥有只有在linux上可用的更高级的特性。

  • Netty的 epoll transport 使用 epoll edge-triggered 而 java的 nio 使用 level-triggered

  • Netty的 epoll transport 暴露了更多的nio没有的配置参数, 如 TCP_CORK, SO_REUSEADDR等。

  • C代码,更少GC,更少synchronized

所以epoll相对性能更高一点

    public NettyRemotingServer(final NettyServerConfig nettyServerConfig,
        final ChannelEventListener channelEventListener) {
        // 创建两个juc包下的Semaphore对象,用于并发控制
        super(nettyServerConfig.getServerOnewaySemaphoreValue(), nettyServerConfig.getServerAsyncSemaphoreValue());
        // netty服务端启动类
        this.serverBootstrap = new ServerBootstrap();
        // netty服务端配置类
        this.nettyServerConfig = nettyServerConfig;
        this.channelEventListener = channelEventListener;

        int publicThreadNums = nettyServerConfig.getServerCallbackExecutorThreads();
        if (publicThreadNums <= 0) {
            publicThreadNums = 4;
        }

        // 创建4个线程大小的线程池 公共线程池
        this.publicExecutor = Executors.newFixedThreadPool(publicThreadNums, new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "NettyServerPublicExecutor_" + this.threadIndex.incrementAndGet());
            }
        });

        /*
         * 铺垫:下面根据是否是linux内核创建了不同的EventLoopGroup
         * 如果是Linux内核下则创建EpollEventLoopGroup,否则创建NioEventLoopGroup
         * 两者有什么区别? 谁的性能更高?
         * 答案是:EpollEventLoopGroup
         * 为什么?stackoverflow也解释过
         * If you are running on linux you can use EpollEventLoopGroup and so get better performance,
         * less GC and have more advanced features that are only available on linux.
         * 如果您在linux上运行,您可以使用EpollEventLoopGroup,从而获得更好的性能,更少的GC,并拥有只有在linux上可用的更高级的特性。
         * - Netty的 epoll transport 使用 epoll edge-triggered 而 java的 nio 使用 level-triggered
           - Netty的 epoll transport 暴露了更多的nio没有的配置参数, 如 TCP_CORK, SO_REUSEADDR等。
           - C代码,更少GC,更少synchronized
         * 所以epoll相对性能更高一点
         *
         */
        // 条件成立:说明是linux内核并且打开了Epoll
        if (useEpoll()) {
            // 生成boss组 epoll的 线程数为1
            this.eventLoopGroupBoss = new EpollEventLoopGroup(1, new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyEPOLLBoss_%d", this.threadIndex.incrementAndGet()));
                }
            });

            //  创建worker组 epoll的
            this.eventLoopGroupSelector = new EpollEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);
                private int threadTotal = nettyServerConfig.getServerSelectorThreads();

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyServerEPOLLSelector_%d_%d", threadTotal, this.threadIndex.incrementAndGet()));
                }
            });
        } else {
            // 生成boss组 线程数为1
            this.eventLoopGroupBoss = new NioEventLoopGroup(1, new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyNIOBoss_%d", this.threadIndex.incrementAndGet()));
                }
            });

            // worker组 线程数默认情况下为8
            this.eventLoopGroupSelector = new NioEventLoopGroup(nettyServerConfig.getServerSelectorThreads(), new ThreadFactory() {
                private AtomicInteger threadIndex = new AtomicInteger(0);
                private int threadTotal = nettyServerConfig.getServerSelectorThreads();

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, String.format("NettyServerNIOSelector_%d_%d", threadTotal, this.threadIndex.incrementAndGet()));
                }
            });
        }

        loadSslContext();
    }

创建完NettyRemotingServer以后,接下来就需要执行NettyRemotingServer.start()逻辑了

start()逻辑总结:

  1. 生成EventExcuterGroup组用于执行ChannelHandler内部的处理逻辑
  2. 生成启动所需要Netty配置的ChannelHandler对象 (编解码实例、NettyServerHandler等)
  3. 使用Netty启动类进行配置相关参数以及handler(后续会仔细讲)
  4. 调用bind()方法启动端口,开始进行监听来自客户端的连接
  5. 启动一个定时任务,用于扫描过期请求的

如果对Netty没有基础的同学可以去看笔者写的Netty相关文章,否则这里可能非常迷惑。

    @Override
    public void start() {
        // 创建 默认事件执行器组 线程用于执行channelHandler逻辑
        this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
            nettyServerConfig.getServerWorkerThreads(),
            new ThreadFactory() {

                private AtomicInteger threadIndex = new AtomicInteger(0);

                @Override
                public Thread newThread(Runnable r) {
                    return new Thread(r, "NettyServerCodecThread_" + this.threadIndex.incrementAndGet());
                }
            });


        // 创建Netty的ChannelHandler对象
        prepareSharableHandlers();



        ServerBootstrap childHandler =
            // 设置我们的boss组和worker组  boss组用于处理accept事件 worker组处理read和write事件
            this.serverBootstrap.group(this.eventLoopGroupBoss, this.eventLoopGroupSelector)
                 // 根据是否是Linux内核 创建不同的socketChannel用于accept
                .channel(useEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                 // 服务端处理客户端的连接请求是顺序处理的,当服务器没有过多的线程处于连接请求时,需要将客户端的连接请求放入到队列中
                 // 而SO_BACKLOG指定的是队列的大小
                 // 对应的是tcp/ip协议,具体实现为操作系统层面的协议栈代码。
                 // listen函数中的 backlog 参数,用来初始化服务端可连接队列
                 // 内核要维护 两个队列 一个是 未连接队列(syns queue) 另外一个是已连接队列(accept queue)
                 // 未连接队列保存的是 客户端已经发来Syn连接请求 代表至少一个Syn已到达,但是还未完成三次握手
                 // 已连接队列保存的是 已经完成三次握手 但是还未调用 socket库的accept方法
                 // SO_BACKLOG的作用是 当 syncQueue + acceptQueue > SO_BACKLOG时候,新的连接会被TCP内核拒绝掉
                .option(ChannelOption.SO_BACKLOG, nettyServerConfig.getServerSocketBacklog())
                 // SO_REUSEADDR  地址复用相关
                 // 一般来说,一个端口释放后会等待两分钟之后才能再被使用,SO_REUSEADDR是让端口释放后立即就可以再次被使用
                .option(ChannelOption.SO_REUSEADDR, true)
                 // 保活机制,如果开启SO_KEEPALIVE后,当 客户端向服务端发送消息后,服务端没向客户端回复,
                 // 那么客户端可能不知道 服务器是否已经挂掉了,解决办法是 客户端当超过一定时间后自动给服务器发送一个空报文,等待服务端返回
                 // 类似于心跳机制
                 // 为什么要关掉保护机制? 几个原因
                 // 1. 超时的默认时间为2小时,意味这只有当服务端未向客户端响应数据,客户端需要2小时后才能发现服务端的问题,参数是可配置的
                 // 2. 保活机制是属于传输层的,当发现连接挂掉后不能执行应用层的相应逻辑
                 // 3. Namesrv实现了应用层的心跳机制 用于处理Broker下线问题
                 // 4. 不能判断连接是否可用,只能判断连接是否存活,TCP连接中的另外一方突然断电关闭连接,那么对端是无法知晓的。
                .option(ChannelOption.SO_KEEPALIVE, false)
                 // childOption用于指定worker组线程 也就是指 客户端已经和服务端完成了三次握手,进入了read或者write的步骤
                 // 控制是否开启Nagle算法,提高较慢的广域网传输效率
                 // 通俗解释:减少需要传输的数据次数,优化网络 既然相同的数据要减少传输次数,那么必要导致通信过程中数据包的增多
                .childOption(ChannelOption.TCP_NODELAY, true)
                 // 指定accept监听的端口 9876
                .localAddress(new InetSocketAddress(this.nettyServerConfig.getListenPort()))
                 // 指定ChannelHandler相关的类
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    public void initChannel(SocketChannel ch) throws Exception {
                        // handshakeHandler、encoder、connectionManageHandler、serverHandler都是标注了Sharable注解
                        // 代表公用Handler,不需要为每个socket连接创建相应的handler
                        // inboundHandler: handshakeHandler -> decoder -> idleState -> connectManager -> serverHandler
                        // outboundHandler : encoder -> idleState -> connectManager -> serverHandler
                        ch.pipeline()
                             // handshakeHandler 当客户端配置了useTls = true,为服务端动态创建SSLHandler,并动态删除自己
                             // ssl相关的,不在本次考虑范围
                            .addLast(defaultEventExecutorGroup, HANDSHAKE_HANDLER_NAME, handshakeHandler)
                            .addLast(defaultEventExecutorGroup,
                                encoder,
                                new NettyDecoder(),
                                // 心跳机制,用来检查远端是否存活 配置为120秒
                                // 简单来说,就是当 120内 不存在 读|写的时候
                                // IdleStateHandler会通过pipeline传递userEventTriggered()方法,
                                // 并且内容类型为IdleStateEvent
                                // connectionManageHandler 用于接受此事件,然后进行处理
                                new IdleStateHandler(0, 0, nettyServerConfig.getServerChannelMaxIdleTimeSeconds()),
                                // 用于当一定时间内发现对端连接未进行读|写数据进行关闭通道
                                connectionManageHandler,
                                // rocketMq逻辑处理对象 核心 所有请求会被封装为RemotingCommand对象,
                                // 然后被serverHandler内部的逻辑处理
                                serverHandler
                            );
                    }
                });
        //  下面如果配置了socket接受缓冲区、发送缓冲区、写缓冲区高水位、低水位等 则使用配置的参数
        if (nettyServerConfig.getServerSocketSndBufSize() > 0) {
            log.info("server set SO_SNDBUF to {}", nettyServerConfig.getServerSocketSndBufSize());
            childHandler.childOption(ChannelOption.SO_SNDBUF, nettyServerConfig.getServerSocketSndBufSize());
        }
        if (nettyServerConfig.getServerSocketRcvBufSize() > 0) {
            log.info("server set SO_RCVBUF to {}", nettyServerConfig.getServerSocketRcvBufSize());
            childHandler.childOption(ChannelOption.SO_RCVBUF, nettyServerConfig.getServerSocketRcvBufSize());
        }
        if (nettyServerConfig.getWriteBufferLowWaterMark() > 0 && nettyServerConfig.getWriteBufferHighWaterMark() > 0) {
            log.info("server set netty WRITE_BUFFER_WATER_MARK to {},{}",
                    nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark());
            childHandler.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(
                    nettyServerConfig.getWriteBufferLowWaterMark(), nettyServerConfig.getWriteBufferHighWaterMark()));
        }

        // 开启Netty内存池管理
        if (nettyServerConfig.isServerPooledByteBufAllocatorEnable()) {
            childHandler.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        }

        try {
            // 绑定端口,sync()同步等待
            ChannelFuture sync = this.serverBootstrap.bind().sync();

            InetSocketAddress addr = (InetSocketAddress) sync.channel().localAddress();
            // 保存端口
            this.port = addr.getPort();
        } catch (InterruptedException e1) {
            throw new RuntimeException("this.serverBootstrap.bind().sync() InterruptedException", e1);
        }


        if (this.channelEventListener != null) {
            this.nettyEventExecutor.start();
        }

        // 注册定时任务 3秒后开始执行 执行周期为1秒
        this.timer.scheduleAtFixedRate(new TimerTask() {

            @Override
            public void run() {
                try {
                    // 定期调用以扫描过期的请求
                    NettyRemotingServer.this.scanResponseTable();
                } catch (Throwable e) {
                    log.error("scanResponseTable exception", e);
                }
            }
        }, 1000 * 3, 1000);
    }

整个NettyRemotingServer启动流程就介绍完成了,上面配置的一些参数以及一些Handler在后续会为大家继续讲解

NettyRemotingServer启动参数配置

ChannelOption.SO_BACKLOG

服务端处理客户端的连接请求是顺序处理的,当服务器没有多余的线程处理连接请求的时候,需要将客户的连接请求放入到队列中,而SO_BACKLOG指定的是队列的大小

对应的是tcp/ip协议,具体实现为操作系统层面的协议栈。

此参数配置对应listen函数中的 backlog 参数,内核要维护两个队列,一个是未连接队列(syns queue)另外一个是全连接队列(accept queue)。

未连接队列(syncQueue) :客户端已经发来Syn连接请求,也就是至少进行了一次握手,此时客户端的 TCP 状态未 SYN_SEND, 但是还未完成三次握手的请求会被放入到此队列

全连接队列(acceptQueue) :与客户端已经完成了三次握手,accpet系统调用需要从此队列进行消费新连接

而RocektMQ给定backlog默认值为1024,

当 syncQueue + accept > SO_BACKLOG ,新的连接会被TCP内核拒绝掉

ChannelOption.SO_REUSEADDR

地址复用相关,打开后可以让端口释放后立即可以再次被使用,如果没有打开的话,一个端口释放后等待两分钟之后才能再被使用

ChannelOption.SO_KEEPAlIVE

连接保活机制,如果开启此参数,那么当向对端发送消息后,服务端长时间未响应,那么会主动关闭此连接

那RocketMQ为什么选择关掉此连接?有几个原因

  1. 超时的时间为2小时,意味着只有当对端未响应数据时长超过2小时,才会主动关闭连接
  2. 保活机制是传输层的,当关闭连接后不能执行应用层的响应逻辑
  3. RocketMQ实现了属于自身应用的心跳机制,例如NameServer与Broker定时发送心跳包,定时检查是否存在Broker下线问题
  4. Netty提供应用层的心跳保活机制,IdleStateHandler
  5. 不能判断连接是否可用,只能判断连接是否存活

ChannelOption.TCP_NODELAY

控制是否开启Nagle算法,提高较慢的广域网传输效率

通俗解释:减少需要传输的数据次数,优化网络,既然相同的数据要减少传输次数,那么必然导致通信过程中数据包长度的增加

NettyRemotingServer配置的Handler解析

NettyRemotingServer启动配置的Handler根据上图看到。当客户端请求过来的时候会依次根据配置的Handler进行执行

如下图:

  • in:入栈处理器
  • out:出栈处理器
  • duplex:既是入栈处理器同时也是出栈处理器

我们一个一个Handler进行介绍

大多数Handler都属于NettyRemotingServer的内部类

NettyEncoder

顾名思义,Netty的编码器。用于将RocketMQ的通信层协议传输对象RemotingCommand编码为byte[]写入到对端。

MessageLength:消息总长度 int类型 4个字节

HeaderLength + Serialization type : 消息头长度 int类型 前一个字节为序列化类型 后三个字节为header长度

DataHeader:真正的header数据内容

Message Body:消息体

@ChannelHandler.Sharable
public class NettyEncoder extends MessageToByteEncoder<RemotingCommand> {
    private static final InternalLogger log = InternalLoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING);

    @Override
    public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out)
        throws Exception {
        try {
            // 编码头部
            ByteBuffer header = remotingCommand.encodeHeader();
            out.writeBytes(header);
            // 编码body内容
            byte[] body = remotingCommand.getBody();
            if (body != null) {
                out.writeBytes(body);
            }
        } catch (Exception e) {
            log.error("encode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e);
            if (remotingCommand != null) {
                log.error(remotingCommand.toString());
            }
            RemotingUtil.closeChannel(ctx.channel());
        }
    }
}

首先来看header编码的逻辑

    public ByteBuffer encodeHeader() {
        return encodeHeader(this.body != null ? this.body.length : 0);
    }
    public ByteBuffer encodeHeader(final int bodyLength) {
        // 1> header length size
        int length = 4;

        // 2> header data length
        byte[] headerData;
        // header数据编码为byte数组
        headerData = this.headerEncode();

        length += headerData.length;

        // 3> body data length
        length += bodyLength;

        // 申请 消息总长度 + header长度 + header内容  大小的byteBuffer
        ByteBuffer result = ByteBuffer.allocate(4 + length - bodyLength);


        // length为整个数据总长度
        result.putInt(length);

        // header length
        result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC));

        // header data
        result.put(headerData);

        result.flip();

        return result;
    }

上面返回了ByteBuffer,ByteBuffer中的内容为消息总长度 + header长度 + header内容 。

接下来是真正header编码的逻辑,默认序列化类型为JSON,但是为了方便代码观看,我们以ROCKETMQ序列化类型为例进行介绍分析

    private byte[] headerEncode() {
        this.makeCustomHeaderToNet();
        if (SerializeType.ROCKETMQ == serializeTypeCurrentRPC) {
            return RocketMQSerializable.rocketMQProtocolEncode(this);
        } else {
            return RemotingSerializable.encode(this);
        }
    }
    public static byte[] rocketMQProtocolEncode(RemotingCommand cmd) {
        // String remark
        byte[] remarkBytes = null;
        int remarkLen = 0;
        if (cmd.getRemark() != null && cmd.getRemark().length() > 0) {
            remarkBytes = cmd.getRemark().getBytes(CHARSET_UTF8);
            remarkLen = remarkBytes.length;
        }

        // HashMap<String, String> extFields
        byte[] extFieldsBytes = null;
        int extLen = 0;
        if (cmd.getExtFields() != null && !cmd.getExtFields().isEmpty()) {
            extFieldsBytes = mapSerialize(cmd.getExtFields());
            extLen = extFieldsBytes.length;
        }

        int totalLen = calTotalLen(remarkLen, extLen);

        ByteBuffer headerBuffer = ByteBuffer.allocate(totalLen);
        // int code(~32767)
        headerBuffer.putShort((short) cmd.getCode());
        // LanguageCode language
        headerBuffer.put(cmd.getLanguage().getCode());
        // int version(~32767)
        headerBuffer.putShort((short) cmd.getVersion());
        // int opaque
        headerBuffer.putInt(cmd.getOpaque());
        // int flag
        headerBuffer.putInt(cmd.getFlag());
        // String remark
        if (remarkBytes != null) {
            headerBuffer.putInt(remarkBytes.length);
            headerBuffer.put(remarkBytes);
        } else {
            headerBuffer.putInt(0);
        }
        // HashMap<String, String> extFields;
        if (extFieldsBytes != null) {
            headerBuffer.putInt(extFieldsBytes.length);
            headerBuffer.put(extFieldsBytes);
        } else {
            headerBuffer.putInt(0);
        }

        return headerBuffer.array();
    }

其实整体流程就是顺序将值进行顺序写入而已。

header的图如下:

NettyDecoder

顾名思义,Netty的解码器。用于将对端发送来的byte[]数组数据解码为RocketMQ通信层协议传输对象RemotingCommand。

NettyDecoder继承LengthFieldBasedFrameDecoder,Netty默认提供的帧解码器,并且可以解决TCP的“粘包”、“半包”问题,具体LengthFieldBasedFrameDecoder原理可以看笔者的Netty相关文章

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.rocketmq.remoting.netty;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import java.nio.ByteBuffer;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import org.apache.rocketmq.remoting.common.RemotingUtil;
import org.apache.rocketmq.logging.InternalLogger;
import org.apache.rocketmq.logging.InternalLoggerFactory;
import org.apache.rocketmq.remoting.protocol.RemotingCommand;

public class NettyDecoder extends LengthFieldBasedFrameDecoder {
    private static final InternalLogger log = InternalLoggerFactory.getLogger(RemotingHelper.ROCKETMQ_REMOTING);

    private static final int FRAME_MAX_LENGTH =
        Integer.parseInt(System.getProperty("com.rocketmq.remoting.frameMaxLength", "16777216"));

    public NettyDecoder() {
        // 参数一:最大帧长度 16777216 16k
        // 参数二:长度域开始偏移量
        // 参数三:长度域长度
        // 参数四:长度调整参数
        // 参数五:接受到的数据包去除前initialBytesToStrip位
        // 整体的配置生效后:传输的数据格式为 前4个字节为总数据长度,根据总数据长度截取后续对应的数据部分
        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;
            }
            ByteBuffer byteBuffer = frame.nioBuffer();

            // 返回 RemotingCommand 对象
            return RemotingCommand.decode(byteBuffer);
        } catch (Exception e) {
            // 打印日志相关
            log.error("decode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e);
            // 关闭通道
            RemotingUtil.closeChannel(ctx.channel());
        } finally {
            // 使用完数据帧以后,需要释放buffer
            // 否则,造成 内存泄漏
            if (null != frame) {
                frame.release();
            }
        }

        return null;
    }
}

首先根据总消息长度获取整个消息内容,而调用super.decode()会返回一个帧,一个数据帧代表一个完整的数据内容

其实就是根据上图的frameLength提取出来frame,然后调用RemotingCommand.decode(frame),进行解码为网络层协议传输对象RemotingCommand

此时需要将数据帧(frame)解码成如下图的部分,

    public static RemotingCommand decode(final ByteBuffer byteBuffer) throws RemotingCommandException {
        int length = byteBuffer.limit();
        // 获取数据帧的前4个字节代表 header的长度
        int oriHeaderLen = byteBuffer.getInt();
        // length & 0xFFFFFF 获取 header长度
        int headerLength = getHeaderLength(oriHeaderLen);

        // 存放header内容的数据
        byte[] headerData = new byte[headerLength];
        // 将header头的数据写入到headerData数组中
        byteBuffer.get(headerData);

        // getProtocolType(oriHeaderLen) 获取到对应的SerializeType 序列化类型 JSON、ROCKETMQ
        RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen));

        // length 为 byteBuffer的容量
        // 4为"header"的长度  表示后续需要读取多少个字节表示完整的header数据长度
        // headerLength为header真正有效数据的长度
        // bodyLength代表数据体的长度
        int bodyLength = length - 4 - headerLength;
        byte[] bodyData = null;
        if (bodyLength > 0) {
            bodyData = new byte[bodyLength];
            byteBuffer.get(bodyData);
        }
        // 将解析的body数据赋值到cmd对象中
        cmd.body = bodyData;

        // 返回cmd对象
        return cmd;
    }

可以看到,上述整个解析流程非常清晰,而上述代码headerDecode()是真正将header数据解析为RemotingCommand的过程。

    private static RemotingCommand headerDecode(byte[] headerData, SerializeType type) throws RemotingCommandException {
        switch (type) {
            case JSON:
                // 将JSON转换为RemotingCommand对象
                RemotingCommand resultJson = RemotingSerializable.decode(headerData, RemotingCommand.class);
                // 设置序列化类型
                resultJson.setSerializeTypeCurrentRPC(type);
                return resultJson;
            case ROCKETMQ:
                // 根据header数据构建RemotingCommand对象
                RemotingCommand resultRMQ = RocketMQSerializable.rocketMQProtocolDecode(headerData);
                // 设置序列化类型
                resultRMQ.setSerializeTypeCurrentRPC(type);
                return resultRMQ;
            default:
                break;
        }

        return null;
    }

虽然默认格式为JSON,但是为了方便讲述,这里以ROCKMQ协议格式为例进行讲解,

    public static RemotingCommand rocketMQProtocolDecode(final byte[] headerArray) throws RemotingCommandException {
        // 创建RemotingCommand对象
        RemotingCommand cmd = new RemotingCommand();
        // 将header数据使用byteBuf包装
        ByteBuffer headerBuffer = ByteBuffer.wrap(headerArray);
        // int code(~32767)
        // 获取前两个字节 为相应的code码
        cmd.setCode(headerBuffer.getShort());
        // LanguageCode language
        // 设置语言类型 JAVA、C、C++等等
        cmd.setLanguage(LanguageCode.valueOf(headerBuffer.get()));
        // int version(~32767)
        // 设置版本号
        cmd.setVersion(headerBuffer.getShort());
        // int opaque
        // 设置 opaque 请求唯一id
        cmd.setOpaque(headerBuffer.getInt());
        // int flag
        // 设置flag标记位
        cmd.setFlag(headerBuffer.getInt());
        // String remark
        int remarkLength = headerBuffer.getInt();
        if (remarkLength > 0) {
            if (remarkLength > headerArray.length) {
                throw new RemotingCommandException("RocketMQ protocol decoding failed, remark length: " + remarkLength + ", but header length: " + headerArray.length);
            }
            byte[] remarkContent = new byte[remarkLength];
            headerBuffer.get(remarkContent);
            cmd.setRemark(new String(remarkContent, CHARSET_UTF8));
        }

        // HashMap<String, String> extFields
        // 获取extFields的长度 Map的size
        int extFieldsLength = headerBuffer.getInt();
        if (extFieldsLength > 0) {
            if (extFieldsLength > headerArray.length) {
                throw new RemotingCommandException("RocketMQ protocol decoding failed, extFields length: " + extFieldsLength + ", but header length: " + headerArray.length);
            }
            byte[] extFieldsBytes = new byte[extFieldsLength];
            headerBuffer.get(extFieldsBytes);
            cmd.setExtFields(mapDeserialize(extFieldsBytes));
        }
        return cmd;
    }

上述代码执行完成后,就将byte[]数据的header解析成了RocketMQ应用识别的RomotingCommand,如下图:

整个header数据如下图:

整个解码器流程也已经讲解完毕。

IdleStateHandler

Netty框架提供的心跳机制Handler,可以直接拿来应用,简单来说,就是可以在指定的时间内(可配置的)Channel不存在 读 | 写的时候,IdleStateHandler会通过channelPipeline传递userEventTriggered()方法,并且内容类型为IdleStateEvent事件,后续的Handler可以重写userEventTriggered()方法,根据此事件进行关闭通道操作。

而RocketMQ配置的IdleStateHandler参数如下:

nettyServerConfig.getServerChannelMaxIdleTimeSeconds()的值默认为120s,也就是2分钟,而NameServer检测Broker是否下线的时间也是120s,接下来直接为大家分析一波源码,整个源码流程也是比较简单的,首先来看构造器

    // 参数一:读空闲超时时间  rocketMq传入的是 0 代表禁用
    // 参数二:写空闲超时时间   rocketMq传入的是 0 代表禁用
    // 参数三:读/写空闲超时时间   rocketMq传入的是 120 代表120秒期间没有读或者写数据
    // 则通过channelPipeline触发userEventTriggered事件
    public IdleStateHandler(
            int readerIdleTimeSeconds,
            int writerIdleTimeSeconds,
            int allIdleTimeSeconds) {

        this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,
             TimeUnit.SECONDS);
    }

进行重载

  // 参数一:读空闲超时时间  rocketMq传入的是 0 代表禁用
    // 参数二:写空闲超时时间   rocketMq传入的是 0 代表禁用
    // 参数三:读/写空闲超时时间   rocketMq传入的是 120 代表120秒期间没有读或者写数据
    public IdleStateHandler(
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {

        // 参数一:默认false 不打印信息相关
        // 参数二:读空闲超时时间  rocketMq传入的是 0 代表禁用
        // 参数三:写空闲超时时间   rocketMq传入的是 0 代表禁用
        // 参数四:读/写空闲超时时间   rocketMq传入的是 120 代表120秒期间没有读或者写数据
        this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
    }

接下来为赋值操作,

    // 参数一:默认false 不打印信息相关
    // 参数二:读空闲超时时间  rocketMq传入的是 0 代表禁用
    // 参数三:写空闲超时时间   rocketMq传入的是 0 代表禁用
    // 参数四:读/写空闲超时时间   rocketMq传入的是 120 代表120秒期间没有读或者写数据
    public IdleStateHandler(boolean observeOutput,
            long readerIdleTime, long writerIdleTime, long allIdleTime,
            TimeUnit unit) {
        ObjectUtil.checkNotNull(unit, "unit");

        this.observeOutput = observeOutput;

        if (readerIdleTime <= 0) {
            readerIdleTimeNanos = 0;
        } else {
            readerIdleTimeNanos = Math.max(unit.toNanos(readerIdleTime), MIN_TIMEOUT_NANOS);
        }
        if (writerIdleTime <= 0) {
            writerIdleTimeNanos = 0;
        } else {
            writerIdleTimeNanos = Math.max(unit.toNanos(writerIdleTime), MIN_TIMEOUT_NANOS);
        }
        if (allIdleTime <= 0) {
            allIdleTimeNanos = 0;
        } else {
            allIdleTimeNanos = Math.max(unit.toNanos(allIdleTime), MIN_TIMEOUT_NANOS);
        }
    }

犹豫我们指定的readerIdleTime和writerIdleTime都为0,那么readerIdleTimeNanos和writerIdleTimeNanos都为0,而allIdleTimeNanos为120s。

而我们知道,当我们与对端成功建立通道以后,会在整个ChannelPipeline中传播channelActive()方法,而IdleStateHandler的channelActive()方法如下:

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        // This method will be invoked only if this handler was added
        // before channelActive() event is fired.  If a user adds this handler
        // after the channelActive() event, initialize() will be called by beforeAdd().
        initialize(ctx);
        super.channelActive(ctx);
    }
    private void initialize(ChannelHandlerContext ctx) {
        // Avoid the case where destroy() is called before scheduling timeouts.
        // See: https://github.com/netty/netty/issues/143
        switch (state) {
        case 1:
        case 2:
            return;
        default:
             break;
        }

        state = 1;
        // 省略部分无需关注代码

        // 更新lastReadTime、lastWriteTime为当前纳秒时间
        lastReadTime = lastWriteTime = ticksInNanos();
        // 注册定时任务 并且将定时任务结果 赋值给Future
        // rocketMq指定的是120秒后执行AllIdleTimeoutTask.run方法
        // 因此我们核心关注AllIdleTimeoutTask.run()方法
        if (allIdleTimeNanos > 0) {
            allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
                    allIdleTimeNanos, TimeUnit.NANOSECONDS);
        }
    }

而schedule则是属于NioEventLoopGroup实现的定时任务了,底层其实是NioEventLoop内部存在一个延时队列,关键点在于AllIdleTimeoutTask.run()方法

    private final class AllIdleTimeoutTask extends AbstractIdleTask {

        AllIdleTimeoutTask(ChannelHandlerContext ctx) {
            super(ctx);
        }

        @Override
        protected void run(ChannelHandlerContext ctx) {

            // 假设我们指定是120s allIdleTimeNanos为纳秒表示
            // 理解为120s转换为纳秒
            long nextDelay = allIdleTimeNanos;
            // 条件成立,说明当前channel没有正在读数据
            if (!reading) {
                // 当前纳秒时间 - Math.max(上一次读时间,上一次写时间)
                // 假设上一次读时间为 3秒之前
                // 那么 nextDelay 为 2秒
                // nextDelay = 120s - 当前时间 - 上一次读 | 上一次写时间
                nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
            }
            // 条件成立:说明已经超时 120秒内未 读|写
            if (nextDelay <= 0) {
                // Both reader and writer are idle - set a new timeout and
                // notify the callback.
                // 读取器和写入器都是空闲的-设置一个新的超时时间并通知回调
                allIdleTimeout = schedule(ctx, this, allIdleTimeNanos, TimeUnit.NANOSECONDS);

                
                boolean first = firstAllIdleEvent;
                firstAllIdleEvent = false;

                try {
                    if (hasOutputChanged(ctx, first)) {
                        return;
                    }

                    // 生成一个 IdleStateEvent 事件 ALL_IDLE相关的
                    IdleStateEvent event = newIdleStateEvent(IdleState.ALL_IDLE, first);
                    // 传播此事件
                    channelIdle(ctx, event);
                } catch (Throwable t) {
                    ctx.fireExceptionCaught(t);
                }
            } else {
                // 执行到这里,说明期间存在读|写 没超时,继续注册定时任务
                // Either read or write occurred before the timeout - set a new
                // timeout with shorter delay.
                allIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
            }
        }
    }

可以看到,上面整个流程也是非常清晰的,当发现指定的时候内无数据进行 读|写 的时候,会进行方法传播,

传播的逻辑如下:

    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        ctx.fireUserEventTriggered(evt);
    }

上面整体流程介绍完成了,有2个点需要解释一下,一个是上一次读 | 上一次写时间,这个时间每次write的时候都会更新上一次写时间,每次传播channelCompelte()方法的时候都会更新上一次读时间,而上一次读 | 上一次写时间在第一次创建的时候被赋值为当前时间,另外一个是下图的代码处:

首先来看此处,reading明显是一个boolean值,默认为false,

什么时候会更新为true呢?

其实是当Channel内传播channelRead()的时候,代表整个channel有数据可读,那么会将reading更新为true,此时很明显,通道是存在数据传输的,因此心跳机制可以省略此次检查。

什么时候reading会被更新为false?

当Channel内传播channelReadComplete()的时候,会更新此参数为false,并且更新最后一次读的时间

具体代码如下:

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // 执行到此方法,说明数据读取完毕
        if ((readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) && reading) {
            // 更新最后一次读的时间 为当前纳秒时间 System.nanoTime()
            lastReadTime = ticksInNanos();
            // 设置正在读为false
            reading = false;
        }
        ctx.fireChannelReadComplete();
    }
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 执行到此方法,说明通道传递了数据 处于正在读数据中
        if (readerIdleTimeNanos > 0 || allIdleTimeNanos > 0) {
            // 更新标志
            reading = true;
            // 第一次读 和 all 事件设置为true
            firstReaderIdleEvent = firstAllIdleEvent = true;
        }
        ctx.fireChannelRead(msg);
    }

当发现通道指定时间无数据读|写的时候,Channel会传播userEventTriggered()方法,那RocketMQ提供了哪个类来重写此方法完成通道关闭的操作呢?其实是依靠的我们下一个需要讲的Handler,NettyConnectManagerHandler

NettyConnectManagerHandler

顾名思义,此Handler是用于管理Channel连接的。我们直接来看userEventTriggered()方法

    // 当mq配置时间 默认120秒 期间此channel没有进行 读|写操作的时候,那么通道内会传播此方法
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            if (evt instanceof IdleStateEvent) {
                IdleStateEvent event = (IdleStateEvent) evt;
                //
                if (event.state().equals(IdleState.ALL_IDLE)) {
                    final String remoteAddress = RemotingHelper.parseChannelRemoteAddr(ctx.channel());
                    log.warn("NETTY SERVER PIPELINE: IDLE exception [{}]", remoteAddress);
                    // 关闭通道
                    RemotingUtil.closeChannel(ctx.channel());
                    if (NettyRemotingServer.this.channelEventListener != null) {
                        NettyRemotingServer.this
                            .putNettyEvent(new NettyEvent(NettyEventType.IDLE, remoteAddress, ctx.channel()));
                    }
                }
            }

            ctx.fireUserEventTriggered(evt);
        }

可以看到,上述两个比较关键的操作

  1. 关闭通道
  2. 向NettyRemotingServer内部队列中提交了一个Netty空闲事件(NettyConnectManager是内部类,因此可以直接访问NettyRemotingServer)

提交事件这个逻辑有点类似于线程池提交任务,内部会存在一个线程不停的消费这个队列中的事件,当发现是Netty空闲事件的时候会进行通道关闭后的后续时间,比如NameServer移除此通道对应的Broker信息,因为代表Broker已经和NameServer失去连接了。

比较关键的是需要理解这个提交事件,事件被其他线程消费的这个思想,有点类似于线程池。因为线程池也是提交任务者和执行任务者是不同的。

整个Chnanel配置的Handler介绍已经接近尾声了,也是我们最贴近RocketMQ应用层面的一个Handler。

NettyServerHandler。

NettyServerHandler

此Handler的逻辑非常简单,这个Handler只会关注RemotingCommand对象然后调用NettyRemotingServer的处理消息逻辑。

processMessageReceived()方法流程在介绍NettyRemotingAbstract的时候已经详细介绍过了,至此其实整个NettyRemotingServer就介绍完毕了,其实还是非常简单的,对不对?

这里带大家串一下流程:

以NameServer接受到Broker的路由注册为例,首先是将数据解码为RemotingCommand对象,然后NettyServerHandler调用处理消息逻辑,此时会判断是请求命令还是响应命令,由于路由注册操作属于Broker请求NameServer,因此是请求命令,会根据协议码code判断是路由注册操作,执行真正的路由注册逻辑

    // 参数一:netty层面 channelHandler上下文
    // 参数二:rocketMQ通信层交互对象
    public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
        final RemotingCommand cmd = msg;
        if (cmd != null) {
            // 根据 命令类型 判断是 请求命令还是响应命令
            // 具体判断逻辑其实是请求头中的flag属性的
            // flag字段低1位 为0 代表是请求命令
            // flag字段低1位 为1 代表是响应命令
            switch (cmd.getType()) {
                case REQUEST_COMMAND:
                    // 处理请求命令逻辑
                    processRequestCommand(ctx, cmd);
                    break;
                case RESPONSE_COMMAND:
                    // 处理响应命令逻辑
                    processResponseCommand(ctx, cmd);
                    break;
                default:
                    break;
            }
        }
    }

由于字数原因,下个章节再为大家介绍一下NettyRemotingClient类的介绍。