Netty
是一個非同步的、基於事件驅動的網路應用框架,用以快速開發高效能、高可靠性的網路 IO
程式。
一、非同步模型
同步I/O : 需要程式去真正的去操作I/O;
非同步I/O:核心在I/O操作完成後再通知應用程式操作結果。
怎麼去理解同步和非同步?
同步:
比如服務端傳送資料給客戶端,客戶端中的處理器(繼承一個入站處理器
即可),可以去重寫channelRead0
方法,那麼該方法觸發的時候,其實必須得伺服器有訊息發過來,客戶端才能去讀寫,兩者必須是有先後
順序,這就是所謂的同步
。- 非同步:客戶端在服務端傳送資料來之前就已經返回資料給了使用者,但客戶端已經告訴服務端資料到了要通過訂閱的方式(大名鼎鼎的
觀察者模式
),文章最後已經附上傳送門,理解設計模式
比如上一篇關於Netty
的AttributeKey
和AttributeMap
的原理和使用,這裡不妨講講它的缺點
二、非同步模型存在的問題
使用流程
Step1 使用 AttributeKey 設定 key 值和 k-v 對,為 channel 獲取 值做準備
建立一個處理器 NettyClientHandler
繼承 SimpleChannelInboundHandler<RpcResponse>
,它已經實現了 入站處理器相關的功能,只要重寫它的 channelRead0
方法即可
public class NettyClientHandler extends SimpleChannelInboundHandler<RpcResponse> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, RpcResponse msg) throws Exception {
try {
AttributeKey<RpcResponse> key = AttributeKey.valueOf(msg.getRequestId());
ctx.channel().attr(key).set(msg);
ctx.channel().close();
} finally {
ReferenceCountUtil.release(msg);
}
}
}
記得將該 處理器 加入到 客戶端 bootStrap
的 handler()
方法中,需要 通過預設的 初始化器new ChannelInitializer<SocketChannel>()
(也是一個處理器)去初始化處理器鏈,我是通過匿名內部類去重寫 initChannel
方法的,最後addLast()
剛剛自己寫的處理器即可。
建立伺服器和客戶端,這裡不再贅述,這篇文章對剛入門的幫助不大,可到文章最後取經拿服務端和客戶端。
Step2 使用 channel 的 attr 方法,獲取 k-v 值
客戶端這裡NettyClient
通過使用者呼叫 sendRequest()
方法,去向服務端傳送資訊,返回值是服務端發回的訊息,我們都知道,資訊都是在處理器獲取的,也就是在channelRead0
方法中,所以我們要在sendRequest()
方法中,獲取服務端傳來的值,通過下面程式碼獲取
@Override
public Object sendRequest(RpcRequest rpcRequest) throws RpcException {
// 通過 host 和 port 獲取 channel
//省略
// 寫入 channel 讓 服務端 去 讀 request
channel.writeAndFlush(rpcRequest);
// 獲取 k-v 對
RpcResponse rpcResponse = channel.attr(key).get();
}
相信你們當中有一部分發覺了異樣,sendRequest()
方法和channelRead0()
不會同步,就是說你傳送資料後,會立馬執行到 獲取 k-v
的程式碼,不能阻塞
住等待 channelRead0()
方法把 k-v
值 set
進去
最後測試到,客戶端拿不到值,總是為null
那怎麼保持使用非同步操作,並且可以順利拿到值呢?
那麼就得通過future
來實現,就是先返回值,但值還是沒有的,後面讓使用者自己用future
的方法get
阻塞拿值,說白了,還是要去同步,只是同步由CPU
轉到了使用者
自己手中,慢慢品
三、使用CompletableFuture 解決非同步問題
CompletableFuture
使用方法
CompletableFuture<RpcResponse> resultFuture = new CompletableFuture<>();
/**complete 執行結束後,狀態發生改變,則 說明 值已經傳到了,complete 是 (被觀察者)
通知類的通知方法,通知 觀察者 ,get 方法將 不再阻塞,可以獲取到值
*/
resultFuture .complete(msg);
/**獲取 正確結果,get 是阻塞操作,所以 先把 resultFuture 作為 返回值 返回,再 get
獲取值
*/
RpcResponse rpcResponse = resultFuture.get();
// 獲取 錯誤結果, 拋 異常 處理
resultFuture.completeExceptionally(future.cause());
所以我們要做的就是在channelRead0()
中 做 complete()
,最後 使用者直接 get
得到資料即可,只要把sendRequest()
方法的返回型別改為CompletableFuture
就可以了。
簡單來說就是通過使用這個CompletableFuture
,讓 response
不至於返回後是null,因為我們自己new
了一個CompletableFuture
類,這個類會被通知,並把結果告知給它
需要注意的是,在 客戶端的sendRequest()
方法拿到的 CompletableFuture<RpcResponse>
和在channelRead0()
拿到的必須為同一個,可以設計成單例模式
,這裡是很泛化的單例,通用
public class SingleFactory {
private static Map<Class, Object> objectMap = new HashMap<>();
private SingleFactory() {}
/**
* 使用 雙重 校驗鎖 實現 單例模式
* @param clazz
* @param <T>
* @return
*/
public static <T> T getInstance(Class<T> clazz) {
Object instance = objectMap.get(clazz);
if (instance == null) {
synchronized (clazz) {
if (instance == null) {
try {
instance = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}
}
return clazz.cast(instance);
}
}
下面這樣實現是因為涉及到多個客戶端併發訪問同一個伺服器,設計的原因如下:
- 如果是同一個客戶端要採用發起多個執行緒去請求服務端,設計時如果多個執行緒的
rpcRequest
請求id
一樣,那麼要考慮執行緒安全 - 如果是不同客戶端發起請求服務端,又要保證執行緒之間對
CompleteFuture
是執行緒安全的,確保效能,不能用讓所有執行緒共享同一個CompleteFuture
,這樣通知會變為不定向,不可用,因此考慮使用map
暫時快取所有CompleteFuture
,更加高效
public class UnprocessedRequests {
/**
* k - request id
* v - 可將來獲取 的 response
*/
private static ConcurrentMap<String, CompletableFuture<RpcResponse>> unprocessedResponseFutures = new ConcurrentHashMap<>();
/**
* @param requestId 請求體的 requestId 欄位
* @param future 經過 CompletableFuture 包裝過的 響應體
*/
public void put(String requestId, CompletableFuture<RpcResponse> future) {
System.out.println("put" + future);
unprocessedResponseFutures.put(requestId, future);
}
/**
* 移除 CompletableFuture<RpcResponse>
* @param requestId 請求體的 requestId 欄位
*/
public void remove(String requestId) {
unprocessedResponseFutures.remove(requestId);
}
public void complete(RpcResponse rpcResponse) {
CompletableFuture<RpcResponse> completableFuture = unprocessedResponseFutures.remove(rpcResponse.getRequestId());
completableFuture.complete(rpcResponse);
System.out.println("remove" + completableFuture);
}
}
傳送門:
設計模式:https://gitee.com/fyphome/git-res/tree/master/design-patterns
或者:https://github.com/Fyupeng/java/tree/main/design_patterns
服務端和客戶端的實現:https://github.com/Fyupeng/java/tree/main/NettyPro/src/main/java/com/fyp/netty/groupchat
四、結束語
評論區可留言,可私信,可互相交流學習,共同進步,歡迎各位給出意見或評價,本人致力於做到優質文章,希望能有幸拜讀各位的建議!
與51cto同步:https://blog.51cto.com/fyphome
與csdn同步:https://blog.csdn.net/F15217283411
交流技術,尋求同志。
—— 嗝屁小孩紙 QQ:1160886967