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,因此也是复用了其请求发送逻辑。
流程总结:
- 获取或者创建Channel
- 调用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的基础之上扩展了通信模块。