Netty在RocketMQ中的应用----客户端

RocketMQ中角色有Producer、Comsumer、Broker和NameServer,它们之间的通讯是通过Netty实现的。在之前的文章RocketMQ是如何通讯的?中,对RocketMQt通讯进行了一些介绍,但是底层Netty的细节涉及的比较少,这一篇将作为其中的一个补充。

Netty客户端启动配置

Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class)//
            .option(ChannelOption.TCP_NODELAY, true) // 禁用Nagle算法,避免小分组报文导致高延迟
            .option(ChannelOption.SO_KEEPALIVE, false)// 禁用tcp探活机制,由应用层保证心跳机制
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis()) // 连接超时,默认3秒
            .option(ChannelOption.SO_SNDBUF, nettyClientConfig.getClientSocketSndBufSize()) // 发送缓冲默认65535
            .option(ChannelOption.SO_RCVBUF, nettyClientConfig.getClientSocketRcvBufSize())
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(
                        defaultEventExecutorGroup,
                        new NettyEncoder(),
                        new NettyDecoder(),
                        new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()),
                        new NettyConnectManageHandler(), // 连接管理
                        new NettyClientHandler()); // 业务处理
                }
            });

写过Netty应用的人对上面这段代码应该不陌生,NettyEncoder和NettyDecoder用于消息编解码,NettyConnectManager管理着连接,并负责将相应情况封装成NettyEvent并分发出去。NettyClientHandler用于处理业务逻辑,将具体的业务封装成线程任务,由线程池调度。

何时连接服务端

在NettyRemotingClient中的invokeSync方法中,调用了getAndCreateChannel(addr),我们相信连接就是在这里面完成的。
Netty在RocketMQ中的应用----客户端_第1张图片
每次发送消息都重新创建一个Channel是不现实的,我们需要复用,将创建的channel存下来。客户端将Channel存在了一个table中:

private final ConcurrentMap<String /* addr */, ChannelWrapper> channelTables = new ConcurrentHashMap<String, ChannelWrapper>();

key是服务端的地址,value是Channel的一个包装类。
现在,当需要发送MQ消息时,我们根据地址从table中找到ChannelFutrue,如果没有或者ChannelFuture处于不可用状态,我们就去重新创建,如下所示:

if (createNewConnection) {
                    ChannelFuture channelFuture = this.bootstrap.connect(RemotingHelper.string2SocketAddress(addr));
                    log.info("createChannel: begin to connect remote host[{}] asynchronously", addr);
                    cw = new ChannelWrapper(channelFuture);
                    this.channelTables.put(addr, cw);
                }

创建完成之后,就开始连接:

channelFuture.awaitUninterruptibly(this.nettyClientConfig.getConnectTimeoutMillis())

发送消息

在同步发送消息中,客户端将ResponseFuture临时存入一个responseTable表中,主键是opaque,可以将其理解为id,然后就直接发送消息:

 final ResponseFuture responseFuture = new ResponseFuture(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.putResponse(null);
                    PLOG.warn("send a request command to channel <" + addr + "> failed.");
                }
            });

            RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);

然后就是阻塞等待返回结果或者超时。

处理返回消息

我们假设服务端已经正确接收到消息并处理返回了,那么在NettyClientHandler中就可以接收到返回消息:

 @Override
        protected void channelRead0(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
            processMessageReceived(ctx, msg);
        }

然后再看processMessageReceived方法:

public void processMessageReceived(ChannelHandlerContext ctx, RemotingCommand msg) throws Exception {
        final RemotingCommand cmd = msg;
        if (cmd != null) {
            switch (cmd.getType()) {
                case REQUEST_COMMAND:
                    processRequestCommand(ctx, cmd);
                    break;
                case RESPONSE_COMMAND:
                    processResponseCommand(ctx, cmd);
                    break;
                default:
                    break;
            }
        }
    }

这里进来的消息有两种类型,一种是客户端发出去的请求消息后,服务的返回来的消息;一种是服务的发过来的请求消息。处理返回消息,在方法processResponseCommand中。

public void processResponseCommand(ChannelHandlerContext ctx, RemotingCommand cmd) {
        final int opaque = cmd.getOpaque();
        final ResponseFuture responseFuture = responseTable.get(opaque);
        if (responseFuture != null) {
            responseFuture.setResponseCommand(cmd);

            responseFuture.release();

            responseTable.remove(opaque);

            if (responseFuture.getInvokeCallback() != null) {
                executeInvokeCallback(responseFuture);
            } else {
                responseFuture.putResponse(cmd);
            }
        } else {
            PLOG.warn("receive response, but not matched any request, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
            PLOG.warn(cmd.toString());
        }
    }

看到了没?最关键的一步就在这里,当客户端接收到返回消息后,会从responseTable找到对应的responseFuture,如果是同步则将结果塞进去,如果是异步,则调用注册的回调,参数就是返回消息。

RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);

在同步调用中,发送消息后,在超时时间内将结果放入了responseFuture,这里就会立即返回,这里利用了countDownLatch达到了线程同步效果。

 public RemotingCommand waitResponse(final long timeoutMillis) throws InterruptedException {
        this.countDownLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
        return this.responseCommand;
    }

    public void putResponse(final RemotingCommand responseCommand) {
        this.responseCommand = responseCommand;
        this.countDownLatch.countDown();
    }

关于客户端,目前就讲这么多,下一篇会讲Netty服务端的应用。

你可能感兴趣的:(RocketMQ源码分析系列,Netty系列)