在原始碼分析 Dubbo 通訊篇之網路核心類一文中已給出 Dubbo netty client 的啟動流程,如下圖:
以 Dubbo 協議為例,DubboProtocol#refer 中,在建立 Invoker 時,通過 getClient 方法,開始 Client(連線的)建立過程,先重點看一下:private ExchangeClient[] getClients(URL url) { // @1
// whether to share connection
boolean service_share_connect = false;
int connections = url.getParameter(Constants.CONNECTIONS_KEY, 0); // @2
// if not configured, connection is shared, otherwise, one connection for one service
if (connections == 0) {
service_share_connect = true;
connections = 1;
}
ExchangeClient[] clients = new ExchangeClient[connections]; // @3
for (int i = 0; i < clients.length; i++) {
if (service_share_connect) {
clients[i] = getSharedClient(url); // @4
} else {
clients[i] = initClient(url); // @5
}
}
return clients;
}
複製程式碼
程式碼@1:引數 URL ,服務提供者 URL。
程式碼@2:獲取 <dubbo:reference connections = "" />,預設0表示客戶端對同一個服務提供者的所有服務,使用共享一個連線,如果該值有設定,則使用非共享的客戶端,所謂的共享客戶端,以 Netty 為例,也即客戶端對同一服務提供者發起的不同服務,使用同一個客戶端 ( NettyClient)進行請求的傳送與接收。
程式碼@3,根據 connections,建立 ExchangeClients 陣列。
程式碼@4:如果使用共享連線,則呼叫 getSharedClient 獲取共享連線,如果客戶端未建立,則建立客戶端。
程式碼@5,如果不使用共享連線,呼叫 initClient 建立客戶端,其建立時序圖如上圖所示。 接下來,還是以 Netty4 為例,探究一下 Dubbo NettyClient 的建立細節。
1、NettyClient類圖
首先從全貌上大概看一下 NettyClient 物件所持有的屬性:
- AbstractPeer
- private final ChannelHandler handler : 事件處理Handler。
- private volatile URL url :該協議的第一個服務提供者的URL,Server只需要用到URL中的引數,與具體某一個服務沒什麼關係。
- AbstractEndpoint
- private Codec2 codec :編碼解碼器。
- private int timeout : 超時時間
- private int connectTimeout :連線超時時間 注,如果通過dubbo:provider改變codec,不同的服務提供者引用的預設服務提供者引數不同,那這個只能是以第一個為主了,應該不科學?
- AbstractClient
- protected static final String CLIENT_THREAD_POOL_NAME = "DubboClientHandler" Dubbo 客戶端執行緒名稱。 private static final AtomicInteger CLIENT_THREAD_POOL_ID = new AtomicInteger() 客戶端執行緒池ID自增器。
- private static final ScheduledThreadPoolExecutor reconnectExecutorService 客戶端連線重連執行緒池。
- private final Lock connectLock = new ReentrantLock() 客戶端連線服務端獨佔鎖,保證一個客戶端同時只會一個執行緒在執行連線動作。
- private final boolean send_reconnect 訊息傳送時,如果當前客戶端未連線,是否發起重連操作。
- private final AtomicInteger reconnect_count = new AtomicInteger(0) 記錄重連的次數。
- private final AtomicBoolean reconnect_error_log_flag = new AtomicBoolean(false) 連線出錯後是否列印過ERROR日誌。
- private final int reconnect_warning_period :對連線異常,以WARN級別日誌輸出的頻率,預設第一次是以Error日誌,然後每出現reconnect_warning_period次後,就列印一次warn級別日誌。
- private final long shutdown_timeout :關閉服務的超時時間。
- protected volatile ExecutorService executor 客戶端執行緒池。
- private volatile ScheduledFuture< ?> reconnectExecutorFuture = null 重連的Future。
- 11、private long lastConnectedTime = System.currentTimeMillis() 上一次重連時間戳。
- NettyClient
- private static final NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup(Constants.DEFAULT_IO_THREADS, new DefaultThreadFactory("NettyClientWorker", true)); IO執行緒組,同一個JVM中所有的客戶端公用一個IO執行緒組,且執行緒數固定為(32與CPU核數+1的最小值)。
- private Bootstrap bootstrap :Netty客戶端啟動例項。
- private volatile Channel channel:客戶端連線,請copy其引用使用。
2、原始碼分析NettyClient
2.1 原始碼分析建構函式
public NettyClient(final URL url, final ChannelHandler handler) throws RemotingException { // @1
super(url, wrapChannelHandler(url, handler)); // @2
}
複製程式碼
程式碼@1:url:服務提供者URL;ChannelHandler handler:事件處理器。
程式碼@2:wrapChannelHandler在講解NettyServer時已重點分析,構造其事件轉發模型(Dispatch)。
接下來重點分析其父類的構造方法:
public AbstractClient(URL url, ChannelHandler handler) throws RemotingException {
super(url, handler); // @1
send_reconnect = url.getParameter(Constants.SEND_RECONNECT_KEY, false);
shutdown_timeout = url.getParameter(Constants.SHUTDOWN_TIMEOUT_KEY, Constants.DEFAULT_SHUTDOWN_TIMEOUT);
// The default reconnection interval is 2s, 1800 means warning interval is 1 hour.
reconnect_warning_period = url.getParameter("reconnect.waring.period", 1800); // @2
try {
doOpen(); // @3
} catch (Throwable t) {
close();
throw new RemotingException(url.toInetSocketAddress(), null,
"Failed to start " + getClass().getSimpleName() + " " + NetUtils.getLocalAddress()
+ " connect to the server " + getRemoteAddress() + ", cause: " + t.getMessage(), t);
}
try {
// connect.
connect(); // @4
if (logger.isInfoEnabled()) {
logger.info("Start " + getClass().getSimpleName() + " " + NetUtils.getLocalAddress() + " connect to the server " + getRemoteAddress());
}
} catch (RemotingException t) {
if (url.getParameter(Constants.CHECK_KEY, true)) {
close();
throw t;
} else {
logger.warn("Failed to start " + getClass().getSimpleName() + " " + NetUtils.getLocalAddress()
+ " connect to the server " + getRemoteAddress() + " (check == false, ignore and retry later!), cause: " + t.getMessage(), t);
}
} catch (Throwable t) {
close();
throw new RemotingException(url.toInetSocketAddress(), null,
"Failed to start " + getClass().getSimpleName() + " " + NetUtils.getLocalAddress()
+ " connect to the server " + getRemoteAddress() + ", cause: " + t.getMessage(), t);
}
executor = (ExecutorService) ExtensionLoader.getExtensionLoader(DataStore.class) // @5
.getDefaultExtension().get(Constants.CONSUMER_SIDE, Integer.toString(url.getPort()));
ExtensionLoader.getExtensionLoader(DataStore.class)
.getDefaultExtension().remove(Constants.CONSUMER_SIDE, Integer.toString(url.getPort()));
}
複製程式碼
程式碼@1:呼叫父類的構造其,初始化url、ChannelHandler。 程式碼@2:初始化send_reconnect 、shutdown_timeout、reconnect_warning_period(預設1小時列印一次日誌) 程式碼@3:呼叫doOpen初始化客戶端呼叫模型,後續重點分析。 程式碼@4:呼叫connect方法,向服務端發起TCP連線。 程式碼@5:獲取執行緒池,並從快取中移除。 1.2 doOpen
protected void doOpen() throws Throwable {
NettyHelper.setNettyLoggerFactory();
final NettyClientHandler nettyClientHandler = new NettyClientHandler(getUrl(), this); // @1
bootstrap = new Bootstrap(); // @2
bootstrap.group(nioEventLoopGroup) // @3
.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 (getTimeout() < 3000) { // @4
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000);
} else {
bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getTimeout());
}
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); // @5
}
});
}
複製程式碼
程式碼@1:建立 NettyClientHandler。
程式碼@2:建立 Netty 客戶端啟動例項 bootstrap.
程式碼@3:客戶端繫結IO執行緒組(池),注意,一個 JVM 中所有的 NettyClient 共享其 IO 執行緒。
程式碼@4:設定連線超時時間,最小連線超時時間為3s。
程式碼@5:設定編碼器、事件聯結器,當觸發事件後,將呼叫nettyClientHandler中相關的方法。
2.3 doConnect
protected void doConnect() throws Throwable {
long start = System.currentTimeMillis();
ChannelFuture future = bootstrap.connect(getConnectAddress()); // @1
try {
boolean ret = future.awaitUninterruptibly(3000, TimeUnit.MILLISECONDS); // @2
if (ret && future.isSuccess()) {
Channel newChannel = future.channel();
try {
// Close old channel
Channel oldChannel = NettyClient.this.channel; // copy reference
if (oldChannel != null) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close old netty channel " + oldChannel + " on create new netty channel " + newChannel);
}
oldChannel.close();
} finally {
NettyChannel.removeChannelIfDisconnected(oldChannel);
}
}
} finally {
if (NettyClient.this.isClosed()) {
try {
if (logger.isInfoEnabled()) {
logger.info("Close new netty channel " + newChannel + ", because the client closed.");
}
newChannel.close();
} finally {
NettyClient.this.channel = null;
NettyChannel.removeChannelIfDisconnected(newChannel);
}
} else {
NettyClient.this.channel = newChannel;
}
}
} else if (future.cause() != null) {
throw new RemotingException(this, "client(url: " + getUrl() + ") failed to connect to server "
+ getRemoteAddress() + ", error message is:" + future.cause().getMessage(), future.cause());
} else {
throw new RemotingException(this, "client(url: " + getUrl() + ") failed to connect to server "
+ getRemoteAddress() + " client-side timeout "
+ getConnectTimeout() + "ms (elapsed: " + (System.currentTimeMillis() - start) + "ms) from netty client "
+ NetUtils.getLocalHost() + " using dubbo version " + Version.getVersion());
}
} finally {
if (!isConnected()) {
//future.cancel(true);
}
}
}
複製程式碼
程式碼@1:呼叫bootstrap.connect方法發起TCP連線。
程式碼@2:future.awaitUninterruptibly,連線事件只等待3S,這裡寫成固定了,顯然沒有與doOpen方法中ChannelOption.CONNECT_TIMEOUT_MILLIS保持一致。
關於 NettyClient 的介紹就將到這裡了,下一篇將會分析編碼解碼。
作者介紹:丁威,《RocketMQ技術內幕》作者,RocketMQ 社群優秀佈道師、CSDN2019部落格之星TOP10,維護公眾號:中介軟體興趣圈目前已陸續發表原始碼分析Java集合、Java 併發包(JUC)、Netty、Mycat、Dubbo、RocketMQ、Mybatis等原始碼專欄。可以點選連結加入中介軟體知識星球 ,一起探討高併發、分散式服務架構,交流原始碼。