文章目录
一、前言
本系列为个人Dubbo学习笔记,内容基于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考官方源码分析文章,仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。
在Dubbo笔记⑧ : 消费者启动流程 - ReferenceConfig#get 一文中,我们讲到了消费者引用服务时会调用 RegistryProtocol#refer
和 DubboProtocol#refer
,其中 RegistryProtocol#refer
完成与注册中心的交互工作。DubboProtocol#refer
则是建立了与服务提供者的网络连接。本文我们来看一下DubboProtocol#refer
的实现过程。
本文默认注册中心 zk,服务协议 dubbo,其余按照默认协议。
二、DubboProtocol#refer
DubboProtocol#refer
根据URL 创建了 Invoker,在这里会建立与服务提供者的网络连接。其具体实现如下:
@Override
public <T> Invoker<T> refer(Class<T> serviceType, URL url) throws RpcException {
// 序列化优化
optimizeSerialization(url);
// create rpc invoker.
// 创建与服务提供者的 Netty 连接。
DubboInvoker<T> invoker = new DubboInvoker<T>(serviceType, url, getClients(url), invokers);
invokers.add(invoker);
return invoker;
}
...
// 获取连接客户端。
private ExchangeClient[] getClients(URL url) {
// whether to share connection
// 是否共享连接
boolean service_share_connect = false;
// 获取连接数,默认为0,表示未配置
int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0);
// if not configured, connection is shared, otherwise, one connection for one service
// 如果未配置 connections,则共享连接
if (connections == 0) {
service_share_connect = true;
connections = 1;
}
ExchangeClient[] clients = new ExchangeClient[connections];
for (int i = 0; i < clients.length; i++) {
if (service_share_connect) {
// 获取共享客户端 : getSharedClient 中会从缓存中获取,如果没有命中,则会调用 initClient 方法创建客户端
clients[i] = getSharedClient(url);
} else {
// 初始化新的客户端
clients[i] = initClient(url);
}
}
return clients;
}
这里需要注意的是在创建连接的过程中, 由于一台机器可以提供多个服务,那么消费者在引用这些服务时会考虑是与这些服务建立一个共享连接,还是与每一个服务单独建立一个连接。这里可以通过 connections 设置数量来决定创建多少客户端连接,默认是共享同一个客户端。
下面我们看一下 DubboProtocol#getSharedClient
方法的实现:
/**
* Get shared connection
* 获取共享客户端
*/
private ExchangeClient getSharedClient(URL url) {
String key = url.getAddress();
// 获取带有“引用计数”功能的 ExchangeClient
ReferenceCountExchangeClient client = referenceClientMap.get(key);
if (client != null) {
if (!client.isClosed()) {
// 增加引用计数
client.incrementAndGetCount();
return client;
} else {
referenceClientMap.remove(key);
}
}
locks.putIfAbsent(key, new Object());
synchronized (locks.get(key)) {
if (referenceClientMap.containsKey(key)) {
return referenceClientMap.get(key);
}
// 如果缓存没命中,则创建 ExchangeClient 客户端
ExchangeClient exchangeClient = initClient(url);
// 将 ExchangeClient 实例传给 ReferenceCountExchangeClient,这里使用了装饰模式
client = new ReferenceCountExchangeClient(exchangeClient, ghostClientMap);
referenceClientMap.put(key, client);
ghostClientMap.remove(key);
locks.remove(key);
return client;
}
}
/**
* Create new connection
* 创建一个新的连接
*/
private ExchangeClient initClient(URL url) {
// client type setting.
// 从url获取客户端类型,默认为 netty
String str = url.getParameter(Constants.CLIENT_KEY, url.getParameter(Constants.SERVER_KEY, Constants.DEFAULT_REMOTING_CLIENT));
// 添加编解码和心跳包参数到 url 中
url = url.addParameter(Constants.CODEC_KEY, DubboCodec.NAME);
// enable heartbeat by default
url = url.addParameterIfAbsent(Constants.HEARTBEAT_KEY, String.valueOf(Constants.DEFAULT_HEARTBEAT));
// BIO is not allowed since it has severe performance issue.
// 检测客户端类型是否存在,不存在则抛出异常
if (str != null && str.length() > 0 && !ExtensionLoader.getExtensionLoader(Transporter.class).hasExtension(str)) {
throw new RpcException("Unsupported client type: " + str + "," +
" supported client type is " + StringUtils.join(ExtensionLoader.getExtensionLoader(Transporter.class).getSupportedExtensions(), " "));
}
ExchangeClient client;
try {
// connection should be lazy
// 获取 lazy 配置,并根据配置值决定创建的客户端类型
if (url.getParameter(Constants.LAZY_CONNECT_KEY, false)) {
// 创建懒加载 ExchangeClient 实例
client = new LazyConnectExchangeClient(url, requestHandler);
} else {
// 创建普通 ExchangeClient 实例
client = Exchangers.connect(url, requestHandler);
}
} catch (RemotingException e) {
throw new RpcException("Fail to create remoting client for service(" + url + "): " + e.getMessage(), e);
}
return client;
}
这里我们可以发现,无论是共享连接还是非共享连接,如果需要创建连接都会调用 DubboProtocol#initClient 方法来创建客户端。不过需要注意在创建客户端的时候,可以根据 LAZY_CONNECT_KEY
(默认为 lazy) 配置来决定是否懒加载客户端,如果懒加载客户端,则会在第一次调用服务时才会创建与服务端的连接(也是调用 Exchangers.connect 方法),如果不是懒加载,则在服务启动的时候便会与提供者建立连接。默认是立即加载,即消费者在启动时就会与提供者建立连接。
同时我们可以注意到 在非懒加载的情况下,当URl 转换成 Invoker 时,消费者便已经和提供者建立了链接(通过 Exchangers.connect(url, requestHandler) ),也即是说,默认情况下消费者在启动的时候将所有提供者URL转化为Invoker,即代表消费者启动时便已经和所有的提供者建立了连接。
上面我们看到 DubboProtocol#initClient 中是通过Exchangers.connect(url, requestHandler);
来创建连接,下面我们来看看其具体实现:
二、Exchangers.connect(url, requestHandler);
服务消费者 Exchangers#connect(url, requestHandler)
和 服务提供者的 Exchangers#bind(url, requestHandler)
过程类似,不同的是一个是连接服务,一个是创建服务。
Exchangers.connect(url, requestHandler);
完成的了与服务提供者的网络连接功能。其调用链路与服务提供者 Exchangers.bind(url, requestHandler)
的 过程比较类似,最终创建了NettyClient 客户端与服务提供者建立了连接。
其调用时序图如下:
Exchangers.connect(url, requestHandler);
的实现如下:
public static ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
if (handler == null) {
throw new IllegalArgumentException("handler == null");
}
url = url.addParameterIfAbsent(Constants.CODEC_KEY, "exchange");
// 1. getExchanger(url) 获取的是Exchanger$Adaptive,最终调用的是 HeaderExchanger
// 2. connect(url, handler) 调用的是 HeaderExchanger#connect
return getExchanger(url).connect(url, handler);
}
其中 HeaderExchanger#connect
的实现如下:
@Override
public ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException {
return new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))), true);
}
其中 Transporters#connect 实现如下:
public static Client connect(URL url, ChannelHandler... handlers) throws RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
ChannelHandler handler;
if (handlers == null || handlers.length == 0) {
handler = new ChannelHandlerAdapter();
} else if (handlers.length == 1) {
handler = handlers[0];
} else {
handler = new ChannelHandlerDispatcher(handlers);
}
// 1. getTransporter() 获取的是 Transporter$Adaptive,最终调用的是 NettyTransporter
// 2. connect(url, handler) 调用的是 org.apache.dubbo.remoting.transport.netty4.NettyTransporter#connect
return getTransporter().connect(url, handler);
}
其中 org.apache.dubbo.remoting.transport.netty4.NettyTransporter#connect
的实现如下:
@Override
public Client connect(URL url, ChannelHandler listener) throws RemotingException {
return new NettyClient(url, listener);
}
我们可以看到 服务消费者通过 Exchangers.connect(url, requestHandler);
最后就是创建了一个 NettyClient
,而服务提供者通过 Exchangers#bind(url, requestHandler);
方法创建了 NettyServer
。即消费者创建了一个Netty客户端与服务提供者的Netty服务端进行通信。
下面我们来看一看 NettyClient 具体实现
三、NettyClient
NettyClient 完成了服务消费者与服务提供者的服务连接,其实现即是常规的Netty客户端连接过程。其实现如下:
/**
* NettyClient.
*/
public class NettyClient extends AbstractClient {
private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);
private static final NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(Constants.DEFAULT_IO_THREADS, new DefaultThreadFactory("NettyClientWorker", true));
private Bootstrap bootstrap;
private volatile Channel channel; // volatile, please copy reference to use
public NettyClient(final URL url, final ChannelHandler handler) throws RemotingException {
// super 中会调用 doOpen 方法,连接提供者的服务
// wrapChannelHandler 按照Dubbo 线程模型包装了handler
super(url, wrapChannelHandler(url, handler));
}
@Override
protected void doOpen() throws Throwable {
// 创建 Netty 服务连接
final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this);
bootstrap = new Bootstrap();
bootstrap.group(nioEventLoopGroup)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
//.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout())
.channel(NioSocketChannel.class);
if (getConnectTimeout() < 3000) {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
} else {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConnectTimeout());
}
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
ch.pipeline()//.addLast("logging",new LoggingHandler(LogLevel.INFO))//for debug
// 解码器
.addLast("decoder", adapter.getDecoder())
// 编码其
.addLast("encoder", adapter.getEncoder())
// 业务处理
.addLast("handler", nettyClientHandler);
}
});
}
.....
protected static ChannelHandler wrapChannelHandler(URL url, ChannelHandler handler) {
url = ExecutorUtil.setThreadName(url, CLIENT_THREAD_POOL_NAME);
url = url.addParameterIfAbsent(Constants.THREADPOOL_KEY, Constants.DEFAULT_CLIENT_THREADPOOL);
// Dubbo 线程模型的分发
return ChannelHandlers.wrap(handler, url);
}
}
需要注意的是,当 NettyClient 创建时,在其构造函数中 super(url, wrapChannelHandler(url, handler));
做了两件事
-
wrapChannelHandler(url, handler)
中通过 Dubbo线程模型完成了消息的分发处理。关于Dubbo线程模型,详参:Dubbo笔记⑥ : 服务发布流程 - NettyServer -
通过 super 调用父类构造函数,从而调用了 doOpen 方法。在doOpen 方法中,开启了Netty连接。
在 NettyClient#doOpen
方法中即是常规的 Netty 连接流程,我们无需多说什么,需要注意的是业务消息处理器 nettyClientHandler, 其实现为NettyClientHandler,NettyClientHandler 中并未做过多处理,而是将消息委托给内部ChannelHandler 来处理。
下面我们来看看NettyClientHandler 处理消息的过程,如下:
1. NettyClientHandler
NettyClientHandler 最终请求还是透传给了DubboProtocol#requestHandler
,此时可以发现即无论生产者还是消费者的消息处理,都是通过 DubboProtocol#requestHandler
来处理,关于 DubboProtocol#requestHandler
的内容,我们在之前提供者的文章中已经分析,不再赘述,这里仅仅分析 NettyClientHandler 在将消息交由 DubboProtocol#requestHandler
之前做了什么处理。
/**
* NettyClientHandler
*/
@io.netty.channel.ChannelHandler.Sharable
public class NettyClientHandler extends ChannelDuplexHandler {
private final URL url;
private final ChannelHandler handler;
public NettyClientHandler(URL url, ChannelHandler handler) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
if (handler == null) {
throw new IllegalArgumentException("handler == null");
}
this.url = url;
this.handler = handler;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
handler.connected(channel);
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
handler.disconnected(channel);
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
// 通道读,接收服务端回复消息
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
handler.received(channel, msg);
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
// 通道写,向服务端发送请求
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise);
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
// 如果写入发生错误,请模拟BAD_REQUEST响应,以便调用者可以立即返回而无需等待超时。
// FIXME:不确定这是否是正确的方法,但是exceptionCaught无法按预期工作。
// if error happens from write, mock a BAD_REQUEST response so that invoker can return immediately without
// waiting until timeout. FIXME: not sure if this is the right approach, but exceptionCaught doesn't work
// as expected.
if (promise.cause() != null && msg instanceof Request) {
Request request = (Request) msg;
Response response = new Response(request.getId(), request.getVersion());
response.setStatus(Response.BAD_REQUEST);
response.setErrorMessage(StringUtils.toString(promise.cause()));
handler.received(channel, response);
} else {
handler.sent(channel, msg);
}
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
NettyChannel channel = NettyChannel.getOrAddChannel(ctx.channel(), url, handler);
try {
handler.caught(channel, cause);
} finally {
NettyChannel.removeChannelIfDisconnected(ctx.channel());
}
}
}
可以看到, NettyClientHandler 本身没有做什么工作,而是将消息透传给了 DubboProtocol#requestHandler
,关于 DubboProtocol#requestHandler` 的实现,我们在 Dubbo笔记⑥ : 服务发布流程 - NettyServer 中有过解释,这里不再赘述。
四、总结
DubboProtocol#refer 的作用一句话既可以总结, DubboProtocol#refer 根据URL 中的信息创建了Netty 客户端与服务提供者的NettyServer建立了连接。当消费者进行服务调用时,会调用DubboInvoker#doInvoke 方法,在 这个方法中会通过NettyClient,将调用的方法、方法入参等配置传递给 服务提供者的NettyServer,服务提供者根据这些信息调用本地的方法,并将结果写回通道,完成了一次调用。
以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://dubbo.apache.org/zh/docs/v2.7/dev/source/
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正