RocketMQ通信层源码解析续集-NettyRemotingClient

980 阅读4分钟

NettyRemotingClient

如果大家已经看完了NettyRemotingServer,再来看NettyRemotingClient,是极其简单的,因为整个通信流程是大致相同的,只是Client多了一些步骤并且不需要立刻启动服务器监听端口而已。

请大家思考一下,客户端和服务端最大的区别是什么?

答:客户端需要connect操作,不需要启动Netty服务器进行accpet。

因此,这就是NettyRemotingClient和NettyRemotingServer的最大区别,也就是每次发送请求的时候,首先会根据地址去连接对端,连接成功以后,才可以进行数据发送和接受操作,因此,我们直接来看发送请求逻辑。因为发送请求逻辑里面涉及到了创建并且连接Channel的操作

在讲解请求发送逻辑之前,快速过一下构造器方法和start()方法

    public NettyRemotingClient(final NettyClientConfig nettyClientConfig,
        final ChannelEventListener channelEventListener) {
        super(nettyClientConfig.getClientOnewaySemaphoreValue(), nettyClientConfig.getClientAsyncSemaphoreValue());
        this.nettyClientConfig = nettyClientConfig;
        this.channelEventListener = channelEventListener;

        // 省略部分代码
      
        // 创建公共线程池 默认内部线程数量为4
        this.publicExecutor = Executors.newFixedThreadPool(4, new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);

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

        // 生成wokrer组
        this.eventLoopGroupWorker = new NioEventLoopGroup(1, new ThreadFactory() {
            private AtomicInteger threadIndex = new AtomicInteger(0);

            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, String.format("NettyClientSelector_%d", this.threadIndex.incrementAndGet()));
            }
        });
       // 省略部分代码
    }
    @Override
    public void start() {
        // 生成线程组 内部默认线程数量为4 用于执行配置的ChannelHandler逻辑
        this.defaultEventExecutorGroup = new DefaultEventExecutorGroup(
            nettyClientConfig.getClientWorkerThreads(),
            new ThreadFactory() {

                private AtomicInteger threadIndex = new AtomicInteger(0);

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

        // 通过客户端启动类配置worker组 由于是客户端 因此线程数量为1
        // 而配置的参数和NettyRemotingServer大同小异,这里就不过多介绍了
        // 唯一不同的地方在于 NettyRemotingServer配置的最后一个Handler为NettyServerHandler
        // 而NettyRemotingClient配置的最后一个Handler为NettyClientHandler
        Bootstrap handler = this.bootstrap.group(this.eventLoopGroupWorker).channel(NioSocketChannel.class)
            .option(ChannelOption.TCP_NODELAY, true)
            .option(ChannelOption.SO_KEEPALIVE, false)
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, nettyClientConfig.getConnectTimeoutMillis())
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    if (nettyClientConfig.isUseTLS()) {
                        if (null != sslContext) {
                            pipeline.addFirst(defaultEventExecutorGroup, "sslHandler", sslContext.newHandler(ch.alloc()));
                            log.info("Prepend SSL handler");
                        } else {
                            log.warn("Connections are insecure as SSLContext is null!");
                        }
                    }
                    pipeline.addLast(
                        defaultEventExecutorGroup,
                        new NettyEncoder(),
                        new NettyDecoder(),
                        new IdleStateHandler(0, 0, nettyClientConfig.getClientChannelMaxIdleTimeSeconds()),
                        new NettyConnectManageHandler(),
                        new NettyClientHandler());
                }
            });
        // 省略部分代码 

        // 定时扫描并且移除过期的请求
        this.timer.scheduleAtFixedRate(new TimerTask() {
            @Override
            public void run() {
                try {
                    NettyRemotingClient.this.scanResponseTable();
                } catch (Throwable e) {
                    log.error("scanResponseTable exception", e);
                }
            }
        }, 1000 * 3, 1000);

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

整个上述流程与NettyRemotingServer不同的点在于线程模型不同,NettyRemotingServer的线程模型为主从Reactor多线程模型,而NettyRemotingClient的线程模型更为简单,为Reactor单线程模型。

并且NettyRemotingServer配置的最后一个Handler为NettyServerHandler,而NettyRemotingClient配置的最后一个Handler为NettyClientHandler。

由于NettyRemotingClient也是继承NettyRemotingAbstract,因此也是复用了其请求发送逻辑。
流程总结:

  1. 获取或者创建Channel
  2. 调用NettyRemotingAbstract.invokeSyncImpl()方法进行真正的消息发送
    // 参数一:broker地址 或者 namesrv地址 具体看是要发给谁
    // 参数二:消息信息 网络传输对象
    // 参数三:超时时间
    @Override
    public RemotingCommand invokeSync(String addr, final RemotingCommand request, long timeoutMillis)
        throws InterruptedException, RemotingConnectException, RemotingSendRequestException, RemotingTimeoutException {
        // 发送请求开始时间
        long beginStartTime = System.currentTimeMillis();
        // 获取或者创建channel
        // 以生产者为例:生产者向 namesrv 或者 broker 发起socket连接然后获取到连接成功的通道
        final Channel channel = this.getAndCreateChannel(addr);
        if (channel != null && channel.isActive()) {
            try {
                // 执行before rpc 钩子扩展方法
                doBeforeRpcHooks(addr, request);
                // 计算花费时间
                long costTime = System.currentTimeMillis() - beginStartTime;
                // 条件成立:说明已经达到超时时间 扔出超时异常
                if (timeoutMillis < costTime) {
                    throw new RemotingTimeoutException("invokeSync call the addr[" + addr + "] timeout");
                }
                // 参数一:channel
                // 参数二:请求对象
                // 参数三:更新后的超时时间
                RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
                // 执行after rpc 钩子扩展方法
                doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
                return response;
            } catch (RemotingSendRequestException e) {
                log.warn("invokeSync: send request exception, so close the channel[{}]", addr);
                this.closeChannel(addr, channel);
                throw e;
            } catch (RemotingTimeoutException e) {
                if (nettyClientConfig.isClientCloseSocketIfTimeout()) {
                    this.closeChannel(addr, channel);
                    log.warn("invokeSync: close socket because of timeout, {}ms, {}", timeoutMillis, addr);
                }
                log.warn("invokeSync: wait response timeout exception, the channel[{}]", addr);
                throw e;
            }
        } else {
            this.closeChannel(addr, channel);
            throw new RemotingConnectException(addr);
        }
    }

因此我们需要关注的只有getAndCreateChannel()方法的逻辑

getAndCreateChannel()方法解析

当需要和nameServer交互进行获取路由信息的时候,addr为null

当需要和broker交互进行发送消息的时候,addr为具体的broker地址

    /**
     *
     * @param addr 地址 有可能为空,有可能不为空
     * 以生产者举例:当需要和nameServer交互进行获取路由信息的时候,addr为null
     *             当需要和broker交互进行发送消息的时候,addr为具体的broker地址
     * @return 连接成功后返回的channel
     */
    private Channel getAndCreateChannel(final String addr) throws RemotingConnectException, InterruptedException {
        // 条件成立:说明是和nameServer进行交互
        if (null == addr) {
            return getAndCreateNameserverChannel();
        }

        // 执行到这里,说明是和broker进行交互,首先尝试从 缓存 中获取存在的Channel
        ChannelWrapper cw = this.channelTables.get(addr);
        // 条件成立:说明从缓存中获取到Channel 直接返回
        if (cw != null && cw.isOK()) {
            return cw.getChannel();
        }

        // 执行到这里,说明需要进行connect操作,并且connect操作完成后将channel放入"缓存"
        return this.createChannel(addr);
    }

我们首先来看与Broker进行交互的逻辑,并且也不考虑缓存中存在Channel的逻辑,直接来看createChannel逻辑。

    private Channel createChannel(final String addr) throws InterruptedException {
        ChannelWrapper cw = this.channelTables.get(addr);
        if (cw != null && cw.isOK()) {
            return cw.getChannel();
        }

        // 加锁
        if (this.lockChannelTables.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
            try {
                // 是否需要创建new连接
                boolean createNewConnection;
                // 再次从缓存中获取channel
                cw = this.channelTables.get(addr);
                // 条件成立:说明缓存中获取到channel
                if (cw != null) {

                    // 条件成立:说明连接可用 返回连接
                    if (cw.isOK()) {
                        return cw.getChannel();
                    } else if (!cw.getChannelFuture().isDone()) {
                        createNewConnection = false;
                    } else {
                        // 执行到这里,说明连接不可用 移除缓存 重新创建channel
                        this.channelTables.remove(addr);
                        createNewConnection = true;
                    }
                } else {
                    createNewConnection = true;
                }

                // 条件成立:说明需要创建new连接
                if (createNewConnection) {
                    // 进行connect操作连接到对端,这里返回的是Future对象 connect操作是异步进行的,因此可能连接不一定成功,可能仍然处于连接中
                    ChannelFuture channelFuture = this.bootstrap.connect(RemotingHelper.string2SocketAddress(addr));
                    log.info("createChannel: begin to connect remote host[{}] asynchronously", addr);
                    // 包装channelFuture,然后放入channel映射表 方便获取获取
                    cw = new ChannelWrapper(channelFuture);
                    this.channelTables.put(addr, cw);
                }
            } catch (Exception e) {
                log.error("createChannel: create channel exception", e);
            } finally {
                this.lockChannelTables.unlock();
            }
        } else {
            log.warn("createChannel: try to lock channel table, but timeout, {}ms", LOCK_TIMEOUT_MILLIS);
        }

        if (cw != null) {
            ChannelFuture channelFuture = cw.getChannelFuture();
            // 尝试等待3秒,等待连接操作完成,
            if (channelFuture.awaitUninterruptibly(this.nettyClientConfig.getConnectTimeoutMillis())) {
                if (cw.isOK()) {
                    log.info("createChannel: connect remote host[{}] success, {}", addr, channelFuture.toString());
                    return cw.getChannel();
                } else {
                    log.warn("createChannel: connect remote host[" + addr + "] failed, " + channelFuture.toString(), channelFuture.cause());
                }
            } else {
                log.warn("createChannel: connect remote host[{}] timeout {}ms, {}", addr, this.nettyClientConfig.getConnectTimeoutMillis(),
                    channelFuture.toString());
            }
        }

        return null;
    }

比较经典的地方是 异步connect操作,然后外部线程默认阻塞等待3s,如果3s内连接成功,则返回此channel,否则返回null。

看完了Client(生产者、消费者)与Broker创建Channel的逻辑,再来看连接NameServer的逻辑。

流程总结:

  • 尝试从缓存中获取上次选择的NameServer地址
  • 根据上次选择的NameServer地址到缓存中获取对应的Channel
  • 如果缓存未命中则根据负载均衡算法从NameServer地址列表中选择一个NameServer地址
  • 根据地址创建Channel并返回
    private Channel getAndCreateNameserverChannel() throws RemotingConnectException, InterruptedException {
        // 获取上次被选择的nameServer地址
        String addr = this.namesrvAddrChoosed.get();

        // 条件成立:说明上次存在被选择的nameServer地址
        if (addr != null) {
            // 根据地址到缓存中获取现有的channel
            ChannelWrapper cw = this.channelTables.get(addr);
            // 条件成立:说明存在现有的channel 直接返回
            if (cw != null && cw.isOK()) {
                return cw.getChannel();
            }
        }

        // 执行到这里,说明可能缓存中不存在channel 或者是第一次进行连接nameserver

        // 获取到我们指定的namesrv地址列表 也就是我们创建Producer或者Consumer时候指定的namesrv列表
        // 可能存在多个,因为namesrv支持集群化部署
        final List<String> addrList = this.namesrvAddrList.get();
        // 加锁
        if (this.namesrvChannelLock.tryLock(LOCK_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)) {
            try {
                // 再次获取上次被选择的nameServer地址
                addr = this.namesrvAddrChoosed.get();

                // 条件成立:说明上次存在被选择的nameServer地址
                if (addr != null) {
                    // 再次根据地址到缓存中获取现有的channel
                    ChannelWrapper cw = this.channelTables.get(addr);
                    // 条件成立:说明存在现有的channel 直接返回
                    if (cw != null && cw.isOK()) {
                        return cw.getChannel();
                    }
                }


                // 下面的逻辑是 负载均衡的从namesrv列表中随机获取一个namesrv尝试进行连接创建并且连接channel
                // 并且将channel和选择的namesrv地址都缓存起来,方便下次直接获取
                if (addrList != null && !addrList.isEmpty()) {
                    for (int i = 0; i < addrList.size(); i++) {
                        int index = this.namesrvIndex.incrementAndGet();
                        index = Math.abs(index);
                        index = index % addrList.size();
                        // 随机获取一个namesrv地址
                        String newAddr = addrList.get(index);

                        // 设置被选择的namesrv地址
                        this.namesrvAddrChoosed.set(newAddr);
                        log.info("new name server is chosen. OLD: {} , NEW: {}. namesrvIndex = {}", addr, newAddr, namesrvIndex);
                        // 创建channel
                        Channel channelNew = this.createChannel(newAddr);
                        if (channelNew != null) {
                            return channelNew;
                        }
                    }
                    throw new RemotingConnectException(addrList.toString());
                }
            } finally {
                this.namesrvChannelLock.unlock();
            }
        } else {
            log.warn("getAndCreateNameserverChannel: try to lock name server, but timeout, {}ms", LOCK_TIMEOUT_MILLIS);
        }

        return null;
    }

NettyClientHandler

NettyClientHandler的逻辑和NettyServerHandler的逻辑一摸一样,只是NettyClientHandler是NettyRemotingClient的内部类,而NettyServerHandler是NettyRemotingServer的内部类

总结

至此,整个通信层已经讲解完毕,可以看到设计还是非常不错的,当我们开发过程中有开发网络通信层的需求,可以参考RocketMQ通信层去开发。

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