在 DotNetty 中實現同步請求

1、背景

DotNetty 自己是一個優秀的網絡通信框架,不過它是基於異步事件驅動來處理另外一端的響應,須要在單獨的 Handler 去處理相應的返回結果。而在咱們的實際使用當中,尤爲是 客戶端程序 基本都是 請求-響應 模型,在發送了數據時候須要等待服務器的響應才能進行下一步操做,若是服務器返回的是錯誤信息,則須要進行特殊的處理。html

相似於下面這種方式:git

public async void Button1_Click()
{
    var result = await DotNettyClient.SendData("Hello");
    
    if(result == "Error")
    {
        throw new Exception("服務器返回錯誤!");
    }
    
    Console.WriteLine($"Hello {result}");
}

2、解決思路

參閱了大部分資料以後,發如今 Java 的 Netty 當中可使用 Future / Promise 來實現,那麼 C# 是否有相似的組件呢?答案是有的,他們對應的就是 TaskTaskCompletionSource,前者是給調用者的任務,然後者則是用於設置響應任務的結果。github

那麼咱們就能夠這麼來處理,當客戶端發送請求時,附帶惟一的一個請求 ID,並將 TaskCompletionSource 放在一個請求字典當中,請求 ID 做爲字典的 Key,值是 TaskCompletionSource,以後返回一個 Task。當客戶端接收到服務器響應的時候,經過 TaskCompletionSource 設置以前那個 Task 的結果,這樣咱們接收到響應以後,就會從以前 await 的地方繼續執行。服務器

這裏我本身的需求僅僅是相似於 同步阻塞式 的操做,因此我直接使用一個隊列來作簡單處理,並無用惟一的請求 ID 來表示不一樣的請求,也沒有使用字典,由於我能夠 保證在同一時間內有且僅有一個客戶端請求被髮起,並且也作了響應的超時處理機制。網絡

3、代碼實現

實現起來超級簡單,只須要在發起請求的時候,建立一個 TaskCompletionSource<TResponse> 對象。這個泛型參數指的是你想要的返回值類型,這裏我以 TResponse 代替,下面的 DEMO 我會用 string 類型進行演示。框架

建立好一個 TaskCompletionSource<TResponse> 以後,在發送方法裏面,咱們能夠將其對象放在一個先進先出的隊列當中,而後將其 Task 屬性做爲發送方法的返回值。異步

咱們再來處處理服務器響應的 Handler 當中,從隊列裏面拿去以前存放的 TaskCompletionSource<TResponse> 對象,調用其 SetResult() 方法,將具體響應進行設置。async

經過以上的操做,咱們在發送數據的時候,就可使用 await 關鍵字等待服務端的響應,但不會阻塞線程,當客戶端接收到服務端響應時,就會恢復到以前 await 的位置繼續執行。ide

數據發送方法:線程

public static class DotNettyClient
{
    static DotNettyClient()
    {
        RequestQueue = new Queue<TaskCompletionSource<string>>();
    }
    
    public static Queue<TaskCompletionSource<string>> RequestQueue { get; set; }
    
    public static async Task<string> SendData(string data)
    {
        var resultTask = new TaskCompletionSource<string>();
        
        var buffer = new Unpooled.Buffer();
        buffer.WriteBytes(Encoding.UTF8.GetBytes(data));
        await _clientChannel.WriteAndFlushAsync(buffer);
        
        RequestQueue.Enqueue(resultTask);
        
        return await resultTask.Task;
    }
}

服務端響應處理:

public class ProtocolHandler : ChannelHandlerAdapter
{
    public override void ChannelRead(IChannelHandlerContext context, object message)
    {
        if(message is string response)
        {
            if(!DotNettyClient.RequestQueue.TryDequeue(out TaskCompletionSource<string> result)) return;
            result.SetResult(response);
        }
    }
}

這裏我就再也不編寫解析器,主要說明一下代碼的思路,下面在使用的時候就如同第一節說的同樣,直接使用 await 關鍵字等待響應結果便可。

4、缺陷

在這裏我並無展現多個異步請求的狀況,若是是用戶同時發起多個請求的時候,你能夠經過數據的惟一 ID 來標識每個請求,讀取時根據惟一 ID 從字典獲取數據,這樣在接收服務端響應的時候就能處理這種狀況了。

5、參考資料

相關文章
相關標籤/搜索