2017年10月27日 星期五

C# HttpClient WebAPI : 14. 呼叫 Web API 的逾時時間限制用法

當我們在進行電腦資源存取的時候,例如:檔案、資料庫、網路、記憶體、系統資源等等,需要特別小心操作,最保險的方式就是將這些存取資源的敘述,使用 try 將這些敘述包含在內,這樣,當有任何例外異常發生的時候,就可以捕捉到這些例外異常問題。
可是,有個問題是比較棘手的,那就是發生逾期的問題,在我們進行 HttpClient 的相關請求操作的時候,也許是因為網路品質、斷線、伺服器的效能或者後端程式碼撰寫品質不佳影響,會造成用戶端長時間無法得到結果。
若我們使用 HttpClient 的時候,若沒有特別指定 HttpClient.Timeout 屬性值, 他的預設逾期時間.aspx)為 100 秒,這樣的時間,對於一般的程式應用似乎有點過長,因此,在這篇文章中,我們將來學習 HttpClient 逾期的程式碼設計方式。

了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式


呼叫 Web API 的逾時時間限制用法

在我們所設計的逾期 Web API,只要您呼叫這個 Web API,他將會延遲5秒鐘之後,才會將結果回傳到用戶端;因此,在 JsonPutAsync 方法中,我們將會接受到一個秒數參數,讓我們可以設定 HttpClient 的逾期時間。我們使用這樣的敘述 client.Timeout = TimeSpan.FromSeconds(sec); 來設定這個 HttpClient 各種操作的最大逾期時間。
若進行 HttpClient 的網路存取操作的時候,若得到回傳結果的時間,大於這裡設定的逾期時間,這個時候 HttpClient 物件將會丟出例外異常,底下是這時間逾期的呼叫堆疊範例。
   於 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   於 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   於 System.Net.Http.HttpClient.<FinishSendAsyncBuffered>d__58.MoveNext()
--- 先前擲回例外狀況之位置中的堆疊追蹤結尾 ---
   於 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   於 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   於 System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   於 LongTimeAccess.Program.<JsonPutAsync>d__1.MoveNext() 於 D:\Vulcan\GitHub\CSharpNotes\WebAPI\LongTimeAccess\Program.cs: 行 77
而這個時間逾期的例外異常,其 Message 屬性將會得到 工作已取消。 的字串內容。
若您有參考這份 HttpClient 使用文章中的寫法,您將會發現到,我們有將 HttpClient 的所有存取動作,都使用 Try 包裹起來,也就是說,我們可以捕捉到任何 HttpClient 所發出的例外異常。
private static async Task<APIResult> JsonPutAsync(int sec = 4)
{
    APIResult fooAPIResult;
    using (HttpClientHandler handler = new HttpClientHandler())
    {
        using (HttpClient client = new HttpClient(handler))
        {
            try
            {
                client.Timeout = TimeSpan.FromSeconds(sec);
                #region 呼叫遠端 Web API
                string FooUrl = $"http://vulcanwebapi.azurewebsites.net/api/values";
                HttpResponseMessage response = null;

                #region  設定相關網址內容
                var fooFullUrl = $"{FooUrl}/LongTimeGet";

                // Accept 用於宣告客戶端要求服務端回應的文件型態 (底下兩種方法皆可任選其一來使用)
                //client.DefaultRequestHeaders.Accept.TryParseAdd("application/json");
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

                // Content-Type 用於宣告遞送給對方的文件型態
                //client.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");

                response = await client.GetAsync(fooFullUrl);
                #endregion
                #endregion

                #region 處理呼叫完成 Web API 之後的回報結果
                if (response != null)
                {
                    if (response.IsSuccessStatusCode == true)
                    {
                        // 取得呼叫完成 API 後的回報內容
                        String strResult = await response.Content.ReadAsStringAsync();
                        fooAPIResult = JsonConvert.DeserializeObject<APIResult>(strResult, new JsonSerializerSettings { MetadataPropertyHandling = MetadataPropertyHandling.Ignore });
                    }
                    else
                    {
                        fooAPIResult = new APIResult
                        {
                            Success = false,
                            Message = string.Format("Error Code:{0}, Error Message:{1}", response.StatusCode, response.RequestMessage),
                            Payload = null,
                        };
                    }
                }
                else
                {
                    fooAPIResult = new APIResult
                    {
                        Success = false,
                        Message = "應用程式呼叫 API 發生異常",
                        Payload = null,
                    };
                }
                #endregion
            }
            catch (Exception ex)
            {
                fooAPIResult = new APIResult
                {
                    Success = false,
                    Message = ex.Message,
                    Payload = ex,
                };
            }
        }
    }

    return fooAPIResult;
}

觸發的 Web API 動作

這個範例中,將會指向 URL http://vulcanwebapi.azurewebsites.net/api/values/LongTimeGet ,此時,將會觸發 Web API 伺服器上的 Values 控制器(Controller)的 public async Task<APIResult> LongTimeGet()動作(Action),其該動作的原始碼如下所示。
這個 Web API 動作,將會回傳一個 APIData 的 JSON 資料。
[HttpGet("LongTimeGet")]
public async Task<APIResult> LongTimeGet()
{
    APIResult foo;

    await Task.Delay(5000);
    foo = new APIResult()
    {
        Success = true,
        Message = "透過 Get 方法",
        Payload = new APIData()
        {
            Id = 777,
            Name = "Vulcan01"
        }
    };
    return foo;
}

進行測試

在程式進入點函式,我們先設定要使用 4 秒的逾期時間設定(伺服器端所指定的 Web API 動作,需要花費至少 5 秒的時間,才會回傳到用戶端上),因此,我們將會得到這樣失敗的結果;接著,我們將用戶端的逾期時間設定為 6 秒鐘,結果是可以順利得到遠端 Web API 的回傳結果。。
static void Main(string[] args)
{
    Console.WriteLine("遠端 Web API 需要花費 5 秒鐘,才會回傳結果內容");
    Console.WriteLine($"使用 Get 方法呼叫 Web API ,並且會逾期的結果 ( HttpClient 限時 4 秒內要完成 )");
    var foo = JsonPutAsync(4).Result;
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    if (foo.Success == true)
    {
        var item = JsonConvert.DeserializeObject<APIData>(foo.Payload.ToString());
        Console.WriteLine($"Id : {item.Id}");
        Console.WriteLine($"Name : {item.Name}");
        Console.WriteLine($"Filename : {item.Filename}");
    }
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Continue...{Environment.NewLine}");
    Console.ReadKey();

    Console.WriteLine($"使用 Get 方法呼叫 Web API ,並且不會逾期的結果 ( HttpClient 限時 6 秒內要完成 )");
    foo = JsonPutAsync(6).Result;
    Console.WriteLine($"結果狀態 : {foo.Success}");
    Console.WriteLine($"結果訊息 : {foo.Message}");
    if (foo.Success == true)
    {
        var item = JsonConvert.DeserializeObject<APIData>(foo.Payload.ToString());
        Console.WriteLine($"Id : {item.Id}");
        Console.WriteLine($"Name : {item.Name}");
        Console.WriteLine($"Filename : {item.Filename}");
    }
    Console.WriteLine($"");

    Console.WriteLine($"Press any key to Exist...{Environment.NewLine}");
    Console.ReadKey();

}

執行結果

這個測試將會輸出底下內容
遠端 Web API 需要花費 5 秒鐘,才會回傳結果內容
使用 Get 方法呼叫 Web API ,並且會逾期的結果 ( HttpClient 限時 4 秒內要完成 )
結果狀態 : False
結果訊息 : 工作已取消。

Press any key to Continue...

使用 Get 方法呼叫 Web API ,並且不會逾期的結果 ( HttpClient 限時 6 秒內要完成 )
結果狀態 : True
結果訊息 : 透過 Get 方法
Id : 777
Name : Vulcan01
Filename :

Press any key to Exist...

HTTP 傳送與接收原始封包

讓我們來看看,這個 Web API 的呼叫動作中,在請求 (Request) 與 反應 (Response) 這兩個階段,會在網路上傳送了那些 HTTP 資料
  • 請求 (Request)
    首先,我們發出一個 GET 請求,底下是一般的 Http GET 請求的封包內容。
GET http://vulcanwebapi.azurewebsites.net/api/values/LongTimeGet HTTP/1.1
Accept: application/json
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
    底下是使用 Fiddler 側錄下 Http 封包的內容,雖然 C# 用戶端因為逾期時間為 4 秒,沒有在 5 秒後收到回傳結果,而在 C# 用戶端產生了例外異常,不過,伺服器還是會在 5 秒後將結果回傳回來(此時,C# 用戶端程式已經終止執行了),不過, Fiddler 還是有側錄下來。
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:16:31 GMT

65
{"success":true,"message":"透過 Get 方法","payload":{"id":777,"name":"Vulcan01","filename":null}}
0
  • 請求 (Request)
    在這裡的第一行中,您將會看到了完整有查詢字串 (Query String) 的 URL,在問號之後的內容,將會傳送到後端 Web API 動作函式的參數內,此時的 Id值為 777。
GET http://vulcanwebapi.azurewebsites.net/api/values/LongTimeGet HTTP/1.1
Accept: application/json
Host: vulcanwebapi.azurewebsites.net
Connection: Keep-Alive
  • 反應 (Response)
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Content-Type: application/json; charset=utf-8
Server: Kestrel
X-Powered-By: ASP.NET
Set-Cookie: ARRAffinity=9d3635139ab6649f453417d1e9047b7ed7a79b7bef031b04afeb6a2c58b33d4e;Path=/;HttpOnly;Domain=vulcanwebapi.azurewebsites.net
Date: Mon, 23 Oct 2017 02:16:40 GMT

65
{"success":true,"message":"透過 Get 方法","payload":{"id":777,"name":"Vulcan01","filename":null}}
0

相關文章索引

C# HttpClient WebAPI 系列文章索引


了解更多關於 [HttpClient Class] 的使用方式
了解更多關於 [使用 async 和 await 進行非同步程式設計] 的使用方式



關於 Xamarin 在台灣的學習技術資源

Xamarin 實驗室 粉絲團
歡迎加入 Xamarin 實驗室 粉絲團,在這裡,將會經常性的貼出各種關於 Xamarin / Visual Studio / .NET 的相關消息、文章、技術開發等文件,讓您可以隨時掌握第一手的 Xamarin 方面消息。
Xamarin.Forms @ Taiwan
歡迎加入 Xamarin.Forms @ Taiwan,這是台灣的 Xamarin User Group,若您有任何關於 Xamarin / Visual Studio / .NET 上的問題,都可以在這裡來與各方高手來進行討論、交流。
Xamarin 實驗室 部落格
Xamarin 實驗室 部落格 是作者本身的部落格,這個部落格將會專注於 Xamarin 之跨平台 (Android / iOS / UWP) 方面的各類開技術探討、研究與分享的文章,最重要的是,它是全繁體中文。
Xamarin.Forms 系列課程
Xamarin.Forms 系列課程 想要快速進入到 Xamarin.Forms 的開發領域,學會各種 Xamarin.Forms 跨平台開發技術,例如:MVVM、Prism、Data Binding、各種 頁面 Page / 版面配置 Layout / 控制項 Control 的用法等等,千萬不要錯過這些 Xamarin.Forms 課程


沒有留言:

張貼留言