輔助連結
Dubbo系列之 (一)SPI擴充套件
Dubbo系列之 (二)Registry註冊中心-註冊(1)
Dubbo系列之 (三)Registry註冊中心-註冊(2)
Dubbo系列之 (四)服務訂閱(1)
Dubbo系列之 (五)服務訂閱(2)
Dubbo系列之 (六)服務訂閱(3)
Dubbo系列之 (七)鏈路層那些事(1)
Dubbo系列之 (七)鏈路層那些事(2)
讓我們以自己編寫的TCP的思想,來看dubbo的網路層。
1、網路層結構圖
Netty,讓我們的編寫TCP變的非常簡單,並且它在業界運用極其廣泛。Dubbo底層預設實現也是通過Netty。
org.apache.dubbo.remoting.transport.netty4.NettyServer就是其預設實現方式。它的類關係如下:
從上面的類圖,我們可以知道其設計的理念。
- 網路通訊,基本都是點對點通訊,每個通訊端可以稱為一個終端,記名為Endpont。它具有獲取本地地址getLocalAddress(),傳送訊息send(),關閉服務close(),並且可以獲取得到處理訊息的控制程式碼getChannelHandler() 等基本功能。
2)一般進行網路通訊,都認為是一個遠端伺服器,它具有很多個客戶端與其通訊,所以持有獲取所有通訊通道的getChannels()方法,判斷雙端是否可以通訊isBound()方法等。並且它實現Resetable,IdleSensible 介面,說明遠端服務端可以有重置功能,也可以處於空閒。它必定是一個通訊終端,所以繼承Endpoint,記名為RemotingServer。 - 從網路通訊的雙端的角度來講,客戶端和服務端是一對對等端,記名為AbstractPeer,它具有通道資訊處理能力,所以實現ChannelHandler。ChannelHandler是用來處理通訊鏈路上的訊息處理器。
4)為了讓程式更具有通用性,抽取為AbstractServer,子類實現其doOpen()和doClose()方法。
5)NettyServer 就是其網路通訊客戶端的一個具體的實現。
6)相應的客戶端的示意圖如下:
2、NettyServer 內部持有物件
1)Map<String, Channel> channels 持有與該服務端通訊的TCP通訊Channel,key為host:port。它是NettyServerHandler內部channels的引用,NettyServerHandler就是Netty處理訊息的控制程式碼,由我們自己編寫。
2)io.netty.channel.Channel channel netty監聽網路連線的channel,由bind()方法返回
3)內部監聽執行緒為1個,執行緒名為NettyServerBoss。
4)內部IO執行緒為可用處理器+1,最多為32個執行緒,執行緒名為NettyServerWorker。
5)NettyServerHandler 為dubbo的訊息處理器,編解碼器對訊息編解碼後,就是扔給它處理。它內部持有一個ChannelHandler,即是NettyServer這個類,從類圖上可知NettyServer實現了ChannelHandler 這個介面。
6、NettyServer 持有一個外部傳進來的ChannelHandler。並會對其進行封裝,截圖如下(截圖比較清晰):
6)NettyCodecAdapter,為dubbo內部的編解碼器,內部持有編碼器InternalEncoder,解碼器為InternalDecoder。
7)Dubbo 的業務執行緒名為DubboServerHandler。之後再日誌上看到這個名字不要在陌生了。
3、直接new NettyServer?
答案,肯定不是,直接在框架上new,之後肯定沒有擴充套件性可言。沒事情,我們包裹一層即可,命名為傳輸層Transporter,它就有bind,connect功能。
NettyTransporter就是其具體的實現之一。
public class NettyTransporter implements Transporter {
public static final String NAME = "netty";
@Override
public RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException {
return new NettyServer(url, handler);
}
@Override
public Client connect(URL url, ChannelHandler handler) throws RemotingException {
return new NettyClient(url, handler);
}
}
4、Transporter 是一個SPI擴充套件,它的工具類Transporters
既然是一個SPI擴充套件,必然需要通過ExtensionLoader.getExtensionLoader()進行載入,Transporters其工具類的靜態方法getTransporter()就是通過ExtensionLoader進行載入的,然後其靜態方法Transporters.bind()來獲取RemotingServer,即NettyServer。
public static RemotingServer bind(URL url, ChannelHandler... handlers) throws RemotingException {
if (url == null) {
throw new IllegalArgumentException("url == null");
}
if (handlers == null || handlers.length == 0) {
throw new IllegalArgumentException("handlers == null");
}
ChannelHandler handler;
if (handlers.length == 1) {
handler = handlers[0];
} else {
handler = new ChannelHandlerDispatcher(handlers);
}
return getTransporter().bind(url, handler);
}
值得注意的是,可以為其新增多個ChannelHandler,如果為多個,則對其封裝為ChannelHandlerDispatcher,然後傳遞給NettyServer。
5、Transporters#bind()傳入的ChannelHandler到底是誰?
先引入一個名詞Exchanger,我稱為資訊交換回執器,它其實是對Transporter的封裝。Exchanger 同樣具有bind()和connect()方法。
Exchanger的bind()和connect()接收一個ExchangeHandler的訊息處理控制程式碼,ExchangeHandler 也是一個ChannelHandler,也是用來處理通道訊息的,但它對其進行的增強,有一個回執的方法reply()。即接收一個訊息後,可以進行回執,通過reply()。
6、Exchanger 是一個SPI擴充套件,它的工具類Exchangers
和Transporters一樣,工具類Exchangers同樣的方式通過ExtensionLoader.getExtensionLoader()來獲取特定的Exchanger,預設為HeaderExchanger。HeaderExchanger#bind()和connect()。
HeaderExchanger是預設的資訊交換回執器。可以看到HeaderExchangeServer 和HeaderExchangeClient分別接收RemoteServer 和Client。並且通過Transporters呼叫傳入。ExchangeHandler會被包裹成HeaderExchangeHandler,接著在被包裹為DecodeHandler。
從這裡我們就可以解答第五個問題,Transporters#bind()傳入的ChannelHandler 其實是DecodeHandler。為啥這裡需要DecodeHandler,其實是服務端或者客戶端在收到對等端的訊息位元組資料後,首先解析的是頭部資訊,body資訊是沒有在netty的編解碼中進行解析的,是到了真正處理訊息的時候,通過Decodehandler#received()內部進行解碼的。
7、Exchangers的bind()和connect()過載方法太多?
實際上,框架真正用到的是如下:
其他最終都是會調到這個方法,有時間的同學可以自己呼叫下其他的過載方法。
8、Exchangers#bind()和connect()的ExchangeHandler 最終是什麼?
這個問題請檢視DubboProtocol的成員變數ExchangeHandler。接下來,我們來分析這個ExchangeHandler 到底做了什麼。
DubboProtocol內部new 了一個ExchangeHandlerAdapter 物件,也就是ExchangeHandler。該handler主要處理已經Invocation型別的訊息。
首先,看下received()方法。
@Override
public void received(Channel channel, Object message) throws RemotingException {
if (message instanceof Invocation) {
reply((ExchangeChannel) channel, message);
} else {
super.received(channel, message);
}
}
如果訊息的型別是Invocation,那麼呼叫reply方法進行訊息應答,如果不是呼叫超類,也就是
ExchangeHandlerAdapter的received方法,該方法是空的,所以即會丟棄該訊息內容。
那我們來看下,relpy方法主要乾了什麼?註釋如下程式碼塊。
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
/**
*
* 如果訊息型別不是Invocation,那麼會丟擲異常,一般情況下不會,在received方法上已經進行的訊息型別判斷。
*/
if (!(message instanceof Invocation)) {
throw new RemotingException(channel, "Unsupported request: "
+ (message == null ? null : (message.getClass().getName() + ": " + message))
+ ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress());
}
Invocation inv = (Invocation) message;
//這裡得到的就是服務Invoker根據inv。
Invoker<?> invoker = getInvoker(channel, inv);
// need to consider backward-compatibility if it's a callback
// 看是不是dubbo 回撥,之後看下回撥內容
if (Boolean.TRUE.toString().equals(inv.getObjectAttachments().get(IS_CALLBACK_SERVICE_INVOKE))) {
String methodsStr = invoker.getUrl().getParameters().get("methods");
boolean hasMethod = false;
if (methodsStr == null || !methodsStr.contains(",")) {
hasMethod = inv.getMethodName().equals(methodsStr);
} else {
String[] methods = methodsStr.split(",");
for (String method : methods) {
if (inv.getMethodName().equals(method)) {
hasMethod = true;
break;
}
}
}
if (!hasMethod) {
logger.warn(new IllegalStateException("The methodName " + inv.getMethodName()
+ " not found in callback service interface ,invoke will be ignored."
+ " please update the api interface. url is:"
+ invoker.getUrl()) + " ,invocation is :" + inv);
return null;
}
}
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
//呼叫,這裡就是服務呼叫,這裡會牽扯到dubbo filter 相關內容,在服務呼叫時具體堅決
Result result = invoker.invoke(inv);
return result.thenApply(Function.identity());
}
從上面的可知道,reply主要就是進行服務的呼叫,核心語句就是 invoker.invoke(inv)。
那麼是如何找到這個服務提供者的服務呢。來看下getInvoker()。
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
boolean isCallBackServiceInvoke = false;
boolean isStubServiceInvoke = false;
int port = channel.getLocalAddress().getPort();
String path = (String) inv.getObjectAttachments().get(PATH_KEY);
// if it's callback service on client side
//如果該呼叫是在客服端,可能會有配置Stub類,通過isStubServiceInvoke標註
isStubServiceInvoke = Boolean.TRUE.toString().equals(inv.getObjectAttachments().get(STUB_EVENT_KEY));
if (isStubServiceInvoke) {
port = channel.getRemoteAddress().getPort();
}
//callback
// 檢視是否是本地回撥
isCallBackServiceInvoke = isClientSide(channel) && !isStubServiceInvoke;
if (isCallBackServiceInvoke) {
path += "." + inv.getObjectAttachments().get(CALLBACK_SERVICE_KEY);
inv.getObjectAttachments().put(IS_CALLBACK_SERVICE_INVOKE, Boolean.TRUE.toString());
}
// 構建serviceKey,通過埠port,路徑(一般是介面的全限定名)path,版本號version,分組group
String serviceKey = serviceKey(
port,
path,
(String) inv.getObjectAttachments().get(VERSION_KEY),
(String) inv.getObjectAttachments().get(GROUP_KEY)
);
//所有的服務匯出都會放在exporterMap物件裡,然後根據key獲取得到DubboExporter
DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
if (exporter == null) {
throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " +
", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + getInvocationWithoutData(inv));
}
// 接著返回Invoker。
return exporter.getInvoker();
}
當服務呼叫方與服務提供方建立連線和斷開連線時,程式碼如下:
@Override
public void connected(Channel channel) throws RemotingException {
invoke(channel, ON_CONNECT_KEY);
}
@Override
public void disconnected(Channel channel) throws RemotingException {
if (logger.isDebugEnabled()) {
logger.debug("disconnected from " + channel.getRemoteAddress() + ",url:" + channel.getUrl());
}
invoke(channel, ON_DISCONNECT_KEY);
}
都是進行呼叫invoke方法。那麼invoke方法主要乾了啥?
private void invoke(Channel channel, String methodKey) {
Invocation invocation = createInvocation(channel, channel.getUrl(), methodKey);
if (invocation != null) {
try {
received(channel, invocation);
} catch (Throwable t) {
logger.warn("Failed to invoke event method " + invocation.getMethodName() + "(), cause: " + t.getMessage(), t);
}
}
}
/**
* FIXME channel.getUrl() always binds to a fixed service, and this service is random.
* we can choose to use a common service to carry onConnect event if there's no easy way to get the specific
* service this connection is binding to.
* @param channel
* @param url
* @param methodKey
* @return
*/
private Invocation createInvocation(Channel channel, URL url, String methodKey) {
String method = url.getParameter(methodKey);
if (method == null || method.length() == 0) {
return null;
}
RpcInvocation invocation = new RpcInvocation(method, url.getParameter(INTERFACE_KEY), new Class<?>[0], new Object[0]);
invocation.setAttachment(PATH_KEY, url.getPath());
invocation.setAttachment(GROUP_KEY, url.getParameter(GROUP_KEY));
invocation.setAttachment(INTERFACE_KEY, url.getParameter(INTERFACE_KEY));
invocation.setAttachment(VERSION_KEY, url.getParameter(VERSION_KEY));
if (url.getParameter(STUB_EVENT_KEY, false)) {
invocation.setAttachment(STUB_EVENT_KEY, Boolean.TRUE.toString());
}
return invocation;
}
9、最後
這一篇文章距離上一次已經很久了,主要是遇到了國慶,自己想放鬆一下,接下來還會繼續努力的分析dubbo的一些內容。