RocketMQ源码学习---网络通信篇

序言

本篇文章主要讨论RocketMQ中网络通信的具体实现。对于一个消息中间件来说,它的作用一般会有

  • 解耦
  • 限流
  • 消息存储
  • 消息转发或者广播
    这几个功能。这几个功能都离不开网络通信,下面先从宏观角度讲一下网络通信在MQ中的作用。

阅读提示:文章篇幅较长,源码解析有点多,如果对细节不感兴趣可以跳过源码部分,或者可以按照我上一篇博客搭建源码阅读环境。地址:RocketMQ源码调试环境搭建

网络通信

还是看图说话比较好,在没有MQ解耦的系统中,一次简单的RPC系统调用如下所示:

在加入了MQ后,系统调用会如下所示:

原先的1次RPC会变成目前的“2”次RPC。这里的2之所以打引号是因为从系统边界来看是两次RPC,但是对于MQ本身来说,会附加上很多MQ内部需要的网络调用。

如果再具体一点,就会如下所示:

上图中的broker就是MQ中的服务端,负责消息的转储和分发。一般来说MQ分为有broker无broker两种设计,比如kafka,actimemq,rabbitmq以及RocketMQ就是有broker设计,而类似zeroMQ和AKKA就是无broker设计,我个人觉得后者更偏向于一种带有消息范式的编程模型(没有深入研究,有偏差的地方欢迎指正讨论)。

数据的传输在MQ中都是以这种RPC数据流的方式进行传输,所以一个良好的网络通信设计在MQ中非常重要。

设计要素

那么,对于一个中间件的RPC网络通信来说,到底需要哪些设计要素才能满足它的需求?
我认为下面几点是必须要满足的。

  • 编解码处理(负责通信中的编码和解码,序列化,通信协议涉及等必要功能)
  • 双向消息处理(包括同步或异步,MQ中有异步消息的功能)
  • 单向消息处理(一般指心跳消息或者注册消息这样的类型)
  • 业务层处理(如何实现业务上对消息的分类处理?如何构建出一个完善的业务处理机制?)

具体架构实现

老规矩,看图说话,先上一张UML图先(RocketMQ和大多数中间件一样,使用了著名的Netty作为网络通讯框架):

RemotingService:以RemotingService为最上层接口,提供了
三个接口:

void start();
void shutdown();
void registerRPCHook(RPCHook rpcHook);

NettyRemotingAbstract:netty处理的抽象类,封装了netty处理的公共方法,比如下面这一段对消息的总体处理:

RemotingClient/RemotingSever:这两个接口继承了最上层接口,同时提供了client和server所必需的方法,下面这个就是RemotingClient的方法:

public RemotingCommand invokeSync(final String addr, final RemotingCommand request,
                                  final long timeoutMillis) throws InterruptedException, RemotingConnectException,
        RemotingSendRequestException, RemotingTimeoutException;


public void invokeAsync(final String addr, final RemotingCommand request, final long timeoutMillis,
                        final InvokeCallback invokeCallback) throws InterruptedException, RemotingConnectException,
        RemotingTooMuchRequestException, RemotingTimeoutException, RemotingSendRequestException;


public void invokeOneway(final String addr, final RemotingCommand request, final long timeoutMillis)
        throws InterruptedException, RemotingConnectException, RemotingTooMuchRequestException,
        RemotingTimeoutException, RemotingSendRequestException;


public void registerProcessor(final int requestCode, final NettyRequestProcessor processor,
                              final ExecutorService executor);


public boolean isChannelWriteable(final String addr);

BrokerOuterAPI:是broker和别的模块通信的类,封装了NettyRemotingClient。

MQClientImpl:是客户端和broker与nameserver通信的类,也封装了NettyRemotingClient。

UML图下方紫色区域都是对编码解码和事件处理的一些类,没有全部罗列出来,其中RemotingCommand非常重要,是所有传输数据的封装,下面会详细讲解。

协议设计与编码解码

作为网络通信模块,协议设计和编码解码是最基础和重要的,在介绍具体的网络协议设计前,我觉得应该把RemotingCommand的具体内容详细说明一下,这个类是传输过程中对所有数据的封装,不但包含了所有的数据结构,还包含了编码解码操作。

RemotingCommand

 private static final int RPC_TYPE = 0; // 0, REQUEST_COMMAND rpc类型的标注,一种是普通的RPC请求
 private static final int RPC_ONEWAY = 1; // 0, 这种ONEWAY 是指单向RPC,比如心跳包

 private static final Map<Class<? extends CommandCustomHeader>, Field[]> clazzFieldsCache =
        new HashMap<Class<? extends CommandCustomHeader>, Field[]>();//**CommandCustomHader**是所有headerData都要实现的接口,后面的Field[]就是解析header所对应的成员属性,所以这个map就是解析时候的字段缓存,下面两个map也是分别对应类名缓存和注解缓存。
 private static final Map<Class, String> canonicalNameCache = new HashMap<Class, String>();
 // 1, RESPONSE_COMMAND
 private static final Map<Field, Annotation> notNullAnnotationCache = new HashMap<Field, Annotation>();
private static AtomicInteger requestId = new AtomicInteger(0);//这里的requestId是RPC请求的序号,每次请求的时候都会increment一下,同时后面会讲到的responseTable会用这个requestId作为key。   

private int code;//这里的code是用来区分request类型的
private LanguageCode language = LanguageCode.JAVA;//区分语言种类
private int version = 0;//RPC版本号
private int opaque = requestId.getAndIncrement();//这里的opaque就是requestId
private int flag = 0;//区分是普通RPC还是onewayRPC得标志
private String remark;//标注信息
private HashMap<String, String> extFields;//存放本次RPC通信中所有的extFeilds,extFeilds其实就可以理解成本次通信的包头数据
private transient CommandCustomHeader customHeader; //包头数据,注意transient标记,不会被序列化
private transient byte[] body; //body数据,注意transient标记,不会被序列化

重要的成员变量都在上面了,给大家展现一下一次心跳注册的报文:

[
code=103,//这里的103对应的code就是broker向nameserver注册自己的消息
language=JAVA,
version=137,
opaque=58,//这个就是requestId
flag(B)=0,
remark=null,
extFields={
    brokerId=0,
    clusterName=DefaultCluster,
    brokerAddr=125.81.59.113: 10911,
    haServerAddr=125.81.59.113: 10912,
    brokerName=LAPTOP-SMF2CKDN
},
serializeTypeCurrentRPC=JSON

]

现在再来看一下协议设计:

上面这个截图是RocketMQ代码里自带的注释,传输内容主要分为4部分内容:

  • 1、length(总长度,用4个字节存储)
  • 2、header length (包头长度)
  • 3、header data(包头数据)
  • 4、body data(数据包数据)

OK,来具体看一下具体怎么实现的,先看encode编码:

headerEncode()首先将extField放入到这个对象的feildMap中(上面有写过),然后将这个RemotingCommand序列化成byte[]字节数组,序列化使用的是阿里的fastJson:


其中的markProtocolType方法是将RPC类型和headerData长度编码,放到一个byte[4]数组中,实现的比较巧妙。
(当然也有可能是我接触位运算很少,可能C语言里面这种设计很常见)

OK,说完了编码,再看看解码:
解码就是编码的一个逆向流程:

现在编码解码就先到此为止,下面来看RPC的事件处理

RPC事件处理和数据流转

NettyRemotingAbstract这个抽象类包含了很多公共数据处理,也包含了很多重要的数据结构,先介绍一下NettyRemotingAbstract的成员属性。

  • NetttyRemotingAbstract:

    //单向RPC信号量,控制线程个数
    protected final Semaphore semaphoreOneway;
    
    //异步RPC信号量,控制线程个数
    protected final Semaphore semaphoreAsync;
    
    //responseTable,存放异步请求的ResponseFuture
    protected final ConcurrentHashMap<Integer /* opaque */, ResponseFuture> responseTable =
        new ConcurrentHashMap<Integer, ResponseFuture>(256);
    //processorTable,存放注册的processor,key是request Code,对应的是<processor,ExecutorService//线程池(一般使用的是共用的public processor)> 
    protected final HashMap<Integer/* request code */, Pair<NettyRequestProcessor, ExecutorService>> processorTable =
        new HashMap<Integer, Pair<NettyRequestProcessor, ExecutorService>>(64);
    //netty事件处理内部类
    protected final NettyEventExecuter nettyEventExecuter = new NettyEventExecuter();
    //默认的事件处理器,处理一些公共的消息
    protected Pair<NettyRequestProcessor, ExecutorService> defaultRequestProcessor;
    
    
    public NettyRemotingAbstract(final int permitsOneway, final int permitsAsync) {
        this.semaphoreOneway = new Semaphore(permitsOneway, true);
        this.semaphoreAsync = new Semaphore(permitsAsync, true);
    }
    //添加ChannelEvent接口
    public abstract ChannelEventListener getChannelEventListener();
    
    //添加NettyEvent事件
    public void putNettyEvent(final NettyEvent event) {
        this.nettyEventExecuter.putNettyEvent(event);
    }
    

先讲一下业务层的事件处理:
首先上述的processorTable负责存储各个requestCode对应的processor,RemotingServer和RemotingClient中都有

void registerProcessor(final int requestCode, final NettyRequestProcessor processor,
                       final ExecutorService executor);

这个接口,在注册的时候会把processor和对应的request Code 装载进processorTable中,在处理事件时就会去processorTable中去取对应的processor:

然后再讲一下对几种通信方式的处理:

  • 同步消息(一次broker和NameServer通信为例子):

1、通过调用invekeSyncImpl来发出请求,此时会创建一个responseFuture,并put到responseTable中,key是opaque,
2、请求到达server端后会进行对应处理,然后回写response
3、根据opaque取出对应的responseFuture并把response放进去。
  • 单向消息:由于不需要回复,不需要创建responseFuture
  • 异步消息:

    在同步消息的基础上会检测remotingCommand是否有回调函数,如果有会执行回调函数。

  • Tips:
    小的设计技巧:
    1、通过countDownLatch来控制等待网络通信时间
    2、通过两个AtomicBoolean的CAS方法来控制RPC只执行了一次

Netty通信层设计

  • 编解码处理

这里的编码解码分别使用了
Netty里面的MessageToByteEncoderLengthFieldBasedFrameDecoder进行编码解码,这一对工具是比较常用的编解码方式,具体实现和原理我这里就不细说了。

  • Netty事件处理

Netty事件处理最重要的两个类就是

  • NettyConnetManageHandler
  • NettyEventExecuter

前者继承自ChannelDuplexHandler,可以监控connect,disconnect,close等事件,每个事件过来存入NettyEventExecuter的队列里面。

NettyEventExecuter是一个线程,不停地从队列里面取出事件进行相应处理。

class NettyEventExecuter extends ServiceThread {
    private final LinkedBlockingQueue<NettyEvent> eventQueue = new LinkedBlockingQueue<NettyEvent>();
    private final int maxSize = 10000;


    public void putNettyEvent(final NettyEvent event) {
        System.out.println("put event: "+event.getType());
        if (this.eventQueue.size() <= maxSize) {
            this.eventQueue.add(event);
        } else {
            plog.warn("event queue size[{}] enough, so drop this event {}", this.eventQueue.size(), event.toString());
        }
    }


    @Override
    public void run() {
        plog.info(this.getServiceName() + " service started");

        final ChannelEventListener listener = NettyRemotingAbstract.this.getChannelEventListener();

        while (!this.isStoped()) {
            try {
                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;

                    }
                }
            } catch (Exception e) {
                plog.warn(this.getServiceName() + " service has exception. ", e);
            }
        }

        plog.info(this.getServiceName() + " service end");
    }

结尾总结

详细阅读RocketMQ的过程中收获了很多关于网络通信设计的知识,其中对于netty的使用和消息的设计让我收益很多,当然对于MQ来说,网络设计并没有真正的RPC框架那么复杂,不需要考虑很多第三方调用问题和并发量问题,因为瓶颈一般不会卡在网络这一层,不管怎么说还是学习到了很多。由于本人水平有限,如果读者有什么问题或者文章哪里有错误的地方,欢迎大家指正和探讨,我的邮箱 ma.rong@nexuslink.cn。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值