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),我们相信连接就是在这里面完成的。
每次发送消息都重新创建一个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服务端的应用。