这是我参与8月更文挑战的第23天,活动详情查看: 8月更文挑战
本文使用源码地址:simple-rpc
Netty客户端发起调用,重点需要解决三个问题:
- 选择合适的序列化协议,解决Netty传输过程中出现的半包/粘包问题。
- 发挥长连接的优势,对Netty的Channel通道进行复用。
- Netty是异步框架,客户端发起服务调用后同步等待获取调用结果。
对于问题1在分布式服务框架-底层通信(2)服务端实现中已经说过了,这里就不再赘述了。
对于问题2,为了使得Channel能够服务,我们编写一个Channel连接工厂类,对于每一个服务提供者,预先生成一个保存Channel的阻塞队列。
对于问题3,Netty是异步编程框架,客户端发起请求之后,不会同步等待结果返回,需要自己实现同步等待机制。
下面让我们来看下具体的代码实现。
Channel复用
还记得我们在客户端服务引入的时候有这样一段代码:
NettyChannelPoolFactory.getInstance().initChannelPoolFactory(serviceMetadata)
这里就是完成Channel连接池的创建,让我们一起看下到底做了什么。
public void initChannelPoolFactory(Map<String, List<Provider>> providerMap) {
// 服务提供者信息
Collection<List<Provider>> metaDataCollection = providerMap.values();
// 获取服务提供者地址列表
Set<InetSocketAddress> socketAddressSet = Sets.newHashSet();
for (List<Provider> serviceMetaDataModels : metaDataCollection) {
for (Provider provider : serviceMetaDataModels) {
String serverIp = provider.getServerIp();
int serverPort = provider.getServerPort();
InetSocketAddress inetSocketAddress = new InetSocketAddress(serverIp, serverPort);
socketAddressSet.add(inetSocketAddress);
}
}
// 根据服务提供者地址列表初始化Channel阻塞队列,并以地址为Key,地址
// 对应的Channel阻塞队列为value,存入channelPoolMap
for (InetSocketAddress inetSocketAddress : socketAddressSet) {
try {
int realChannelConnectSize = 0;
while (realChannelConnectSize < CHANNEL_CONNECT_SIZE) {
Channel channel = null;
while (channel == null) {
// 若channel不存在,则注册新的Netty Channel
channel = registerChannel(inetSocketAddress);
}
// 计数器,初始化的时候存入阻塞队列的Netty Channel个数不超过CHANNEL_CONNECT_SIZE
realChannelConnectSize++;
// 将新注册的Netty Channel存入阻塞队列channelArrayBlockingQueue
// 并将阻塞队列channelArrayBlockingQueue作为value存入channelPoolMap
ArrayBlockingQueue<Channel> channelArrayBlockingQueue = CHANNEL_POOL_MAP.get(inetSocketAddress);
if (channelArrayBlockingQueue == null) {
channelArrayBlockingQueue = new ArrayBlockingQueue<>(CHANNEL_CONNECT_SIZE);
CHANNEL_POOL_MAP.put(inetSocketAddress, channelArrayBlockingQueue);
}
boolean offer = channelArrayBlockingQueue.offer(channel);
if (!offer) {
log.debug("channelArrayBlockingQueue fail");
}
}
} catch (Exception e) {
log.error("initChannelPoolFactory error", e);
}
}
}
首先解析在容器启动时服务引入时通过注册中心获取到的服务提供者列表,然后根据解析到的信息(由服务提供者IP和端口号组成的InetSocketAddress)为每个服务注册Channel并添加到阻塞队列中。这里注册的Channel数量由realChannelConnectSize进行控制,保证初始化的时候存入阻塞队列的Netty Channel个数不超过CHANNEL_CONNECT_SIZE。而为服务提供者注册新的Channel的工作交由registerChannel()方法实现,代码如下:
public Channel registerChannel(InetSocketAddress socketAddress) {
try {
EventLoopGroup group = new NioEventLoopGroup(10);
Bootstrap bootstrap = new Bootstrap();
bootstrap.remoteAddress(socketAddress);
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyEncoderHandler(SERIALIZE_TYPE));
ch.pipeline().addLast(new NettyDecoderHandler(Response.class, SERIALIZE_TYPE));
ch.pipeline().addLast(new NettyClientBizHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect().sync();
final Channel channel = channelFuture.channel();
final CountDownLatch countDownLatch = new CountDownLatch(1);
final List<Boolean> isSuccessHolder = Lists.newArrayListWithCapacity(1);
channelFuture.addListener(future -> {
if (future.isSuccess()) {
isSuccessHolder.add(Boolean.TRUE);
} else {
log.error("registerChannel fail , {}", future.cause().getMessage());
isSuccessHolder.add(Boolean.FALSE);
}
countDownLatch.countDown();
});
countDownLatch.await();
if (Boolean.TRUE.equals(isSuccessHolder.get(0))) {
return channel;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("registerChannel fail", e);
}
return null;
}
这里就很简单了,如果看过之前Netty客户端启动过程的这里可以跳过了。下面我们再简要介绍一下:
-
客户端启动的引导类是
Bootstrap; -
通过
bootstrap.group()指定线程模型; -
接着指定IO模型为
NioSocketChannel,表示IO模型为NIO; -
ChannelOption.TCP_NODELAY:表示是否开启Nagle算法,true表示关闭,false表示开启,通俗地说,如果要求高实时性,有数据发送时就马上发送,就关闭,如果需要减少发送次数减少网络交互,就开启。 -
通过
handler()方法添加主要的客户端业务处理逻辑:NettyEncoderHandler:注册Netty编码器。NettyDecoderHandler:注册Netty解码器。NettyClientBizHandler:注册客户端业务处理逻辑Handler,这部分后面再详细说。
-
通过
bootstrap.connect()进行建连; -
通过
channelFuture.channel()获取我们所需要的Channel,但是在这之前我们需要知道是否建连成功。由于bootstrap.connect()是异步的,所以我们通过addListener()方法注册监听器,监听Channel是否建立成功。
到此为止,我们为完成了Netty Channel的建立,客户端和服务端之间已经能够完成通信了,并且也通过为每个服务预添加阻塞队列的方式完成Channel复用。
异步转同步
在问题3中已经说了,Netty是异步编程框架,客户端发起请求之后,不会同步等待结果返回,需要自己实现同步等待机制。那如何实现呢?具体思路是:为每次请求新建一个阻塞队列,返回结果的时候,存入该阻塞队列,若在超时时间内返回结果值,则调用端将该结果从阻塞队列中取出返回给调用方。否则超时,返回NULL。

我们来看下具体的代码实现,首先我们定义返回结果的包装类ResponseWrapper,由保存返回结果的阻塞队列BlockingQueue<Response>和返回时间responseTime组成。
同时定义了判断返回结果是否超时过期的方法isExpire()。然后我们需要定义一个保存和操作返回结果的数据容器类RevokerResponseHolder,其有如下方法组成:
- 初始化返回结果容器,使用
requestUniqueKey唯一标识本次调用;
public static void initResponseData(String requestUniqueKey) {
RESPONSE_WRAPPER_MAP.put(requestUniqueKey, ResponseWrapper.of());
}
- 将Netty调用异步返回结果放入阻塞队列;
public static void putResultValue(Response response) {
long currentTimeMillis = System.currentTimeMillis();
ResponseWrapper responseWrapper = RESPONSE_WRAPPER_MAP.get(response.getUniqueKey());
responseWrapper.setResponseTime(currentTimeMillis);
responseWrapper.getResponseBlockingQueue().add(response);
RESPONSE_WRAPPER_MAP.put(response.getUniqueKey(), responseWrapper);
}
- 从阻塞队列中获取异步返回结果。
public static Response getValue(String requestUniqueKey, long timeout) {
ResponseWrapper responseWrapper = RESPONSE_WRAPPER_MAP.get(requestUniqueKey);
try {
return responseWrapper.getResponseBlockingQueue().poll(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("get value error", e);
} finally {
RESPONSE_WRAPPER_MAP.remove(requestUniqueKey);
}
return null;
}
其中RESPONSE_WRAPPER_MAP是一个ConcurrentHashMap用于持有ResponseWrapper。到这里已经可以将异步请求结果转为同步返回了,但是还有一个问题需要我们处理下,那就是对于过期数据的删除。我们在静态代码块中创建定时器,再根据在ResponseWrapper类中提供的isExpire()方法实现。代码如下:
static {
executorService = new ScheduledThreadPoolExecutor(1, new RemoveExpireThreadFactory("simple-rpc", false));
executorService.scheduleWithFixedDelay(() -> {
for (Map.Entry<String, ResponseWrapper> entry : RESPONSE_WRAPPER_MAP.entrySet()) {
boolean expire = entry.getValue().isExpire();
if (expire) {
RESPONSE_WRAPPER_MAP.remove(entry.getKey());
}
}
}, 1, 20, TimeUnit.MILLISECONDS);
}
小结
至此我们完成了Channel的复用和同步等待Netty调用结果返回数据的代码实现。我们将在NettyClientBizHandler中获取Netty异步调用返回的结果,并将该结果保存到RevokerResponseHolder中。