1. 程式人生 > 實用技巧 >(精華)2020年9月13日 C#基礎知識點 網路程式設計HttpClient詳解

(精華)2020年9月13日 C#基礎知識點 網路程式設計HttpClient詳解

(精華)2020年9月13日 C#基礎知識點 網路程式設計HttpClient詳解

一、HttpClient用法

HttpClient 提供的方法:

GetAsync(String)    //以非同步操作將GET請求傳送給指定的URI 
GetAsync(URI)   //以非同步操作將GET請求傳送給指定的URI 
GetAsync(String, HttpCompletionOption)  //以非同步操作的HTTP完成選項傳送GET請求到指定的URI 
GetAsync(String, CancellationToken)     //以非同步操作的取消標記傳送GET請求到指定URI 
GetAsync(Uri, HttpCompletionOption)     //以非同步操作的HTTP完成選項傳送GET請求到指定的URI 
GetAsync(Uri, HttpCompletionOption, CancellationToken)  //以非同步操作的HTTP完成選項和取消標記傳送DELETE請求到指定的URI 
GetAsync(Uri, HttpCompletionOption, CancellationToken)  //以非同步操作的HTTP完成選項和取消標記傳送DELETE請求到指定的URI 
GetByteArrayAsync(String)   //將GET請求傳送到指定URI並在非同步操作中以位元組陣列的形式返回響應正文 
GetByteArrayAsync(Uri)  //將GET請求傳送到指定URI並在一非同步操作中以位元組陣列形式返回響應正文 
GetHashCode     //用作特定型別的雜湊函式,繼承自Object 
GetStreamAsync(String)  //將GET請求傳送到指定URI並在非同步操作中以流的形式返回響應正文 
GetStreamAsync(Uri)     //將GET請求傳送到指定URI並在非同步操作以流的形式返回響應正文 
GetStreamAsync(String)  //將GET請求傳送到指定URI並在非同步操作中以字串的形式返回響應正文 
GetStringAsync(Uri)     //將GET請求傳送到指定URI並在非同步操作中以字串形式返回響應正文
using(var httpClient = new HttpClient())
{<!-- -->
    //other codes
}

以上用法是不推薦的,HttpClient 這個物件有點特殊,雖然繼承了 IDisposable 介面,但它是可以被共享的(或者說可以被複用),且執行緒安全。從專案經驗來看,推薦在整個應用的生命週期內複用 HttpClient 例項,而不是每次RPC請求的時候就例項化一個,在高併發的情況下,會造成Socket資源的耗盡。

1.1:基本的使用

public class Program
{<!-- -->
   private static readonly HttpClient _httpClient = new HttpClient();
   static void Main(string[] args)
   {<!-- -->
       HttpAsync();
       Console.WriteLine("Hello World!");
       Console.Read();
   }
   public static async void HttpAsync()
   {<!-- -->
       for (int i = 0; i < 10; i++)
       {<!-- -->
           var result = await _httpClient.GetAsync("http://www.baidu.com");
           if (result .IsSuccessStatusCode)
            {<!-- -->
                Console.WriteLine($"響應狀態碼: {(int)response.StatusCode} {response.ReasonPhrase}");
                var responseBody = await response.Content.ReadAsStringAsync();
                Console.WriteLine($"響應內容:{responseBody}");
            }
       }
   }
}

1.2:自定義SendAsync和響應頭的使用

public static async Task HttpAsync()
{<!-- -->
    try
    {<!-- -->
        using var client = new HttpClient(new SampleMessageHandler("error"));

        var response = await client.GetAsync(Url);
        response.EnsureSuccessStatusCode();

        ShowHeaders("響應頭:", response.Headers);

        Console.WriteLine($"響應狀態碼: {(int)response.StatusCode} {response.ReasonPhrase}");
        var responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"響應內容:{responseBody}");
    }
    catch (Exception ex)
    {<!-- -->
        Console.WriteLine($"{ex.Message}");
    }
}
public static void ShowHeaders(string title, HttpHeaders headers)
{<!-- -->
    Console.WriteLine(title);
    foreach (var header in headers)
    {<!-- -->
        var value = string.Join(" ", header.Value);
        Console.WriteLine($"Header: {header.Key} Value: {value}");
    }
    Console.WriteLine();
}
public class SampleMessageHandler : HttpClientHandler
{<!-- -->
    private readonly string _displayMessage;
    public SampleMessageHandler(string message)
    {<!-- -->
        _displayMessage = message;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {<!-- -->
        Console.WriteLine($"來自SampleMessageHandler的訊息: {_displayMessage}");
        if (_displayMessage != "error") return base.SendAsync(request, cancellationToken);
        var response = new HttpResponseMessage(HttpStatusCode.BadRequest);
        return Task.FromResult(response);

    }

}

1.3:HttpRequestMessage實現請求

public static async Task HttpAsync()
{<!-- -->
    using var client = new HttpClient();
    var request = new HttpRequestMessage(HttpMethod.Get, Url);

    var response = await client.SendAsync(request);
    if (response.IsSuccessStatusCode)
    {<!-- -->
        Console.WriteLine($"響應狀態碼: {(int)response.StatusCode} {response.ReasonPhrase}");
        var responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"響應內容 {responseBody}");
    }
}

1.4:TCP實現SendAsync資料傳送

public static async Task<string> HttpAsync()
{<!-- -->
    const int readBufferSize = 1024;
    const string hostname = "127.0.0.1";

    try
    {<!-- -->
        using var client = new TcpClient();
        await client.ConnectAsync(hostname, 80);

        var stream = client.GetStream();
        var header = "GET / HTTP/1.1\r\n" +
                     $"Host: {hostname}:80\r\n" +
                     "Connection: close\r\n" +
                     "\r\n";
        var buffer = Encoding.UTF8.GetBytes(header);
        await stream.WriteAsync(buffer, 0, buffer.Length);
        await stream.FlushAsync();

        var ms = new MemoryStream();
        buffer = new byte[readBufferSize];
        var read = 0;
        do
        {<!-- -->
            read = await stream.ReadAsync(buffer, 0, readBufferSize);
            ms.Write(buffer, 0, read);
            Array.Clear(buffer, 0, buffer.Length);
        } while (read > 0);
        ms.Seek(0, SeekOrigin.Begin);
        using var reader = new StreamReader(ms);
        return reader.ReadToEnd();
    }
    catch (SocketException ex)
    {<!-- -->
        Console.WriteLine(ex.Message);
        return null;
    }
}

HttpClient到底層的執行過程圖

二、HttpClient高階用法

public async Task<string> GetAccessTokenAsync()
{<!-- -->
    string uri = "你的URL";
    HttpClientHandler handler = new HttpClientHandler
    {<!-- -->
        //設定是否傳送憑證資訊,有的伺服器需要驗證身份,不是所有伺服器需要
        UseDefaultCredentials = false
    };
    HttpClient httpClient = new HttpClient(handler);
    HttpResponseMessage response = await httpClient.GetAsync(uri);
    response.EnsureSuccessStatusCode();
    //回覆結果直接讀成字串
    string resp = await response.Content.ReadAsStringAsync();
    JObject json = (JObject)JsonConvert.DeserializeObject(resp);
    string accessToken = json["access_token"].ToString();
    //採用流讀資料
    //using (Stream streamResponse = await response.Content.ReadAsStreamAsync())
    //{<!-- -->
    //    StreamReader reader = new StreamReader(streamResponse);
    //    string responseFromServer = reader.ReadToEnd();
    //    JObject res = (JObject)JsonConvert.DeserializeObject(responseFromServer);
    //    accessToken = res["access_token"].ToString();
    //    reader.Close();
    //}
    //獲得許可證憑證
    PostMailAsync(accessToken);
    //關閉響應
    return "success";
}

優化:幫HttpClient預熱
我們採用一種預熱方式,在正式發post請求之前,先發一個head請求:

_httpClient.SendAsync(new HttpRequestMessage {<!-- -->
    Method = new HttpMethod("HEAD"),
    RequestUri = new Uri(BASE_ADDRESS + "/")
})
.Result.EnsureSuccessStatusCode();

經測試,通過這種熱身方法,可以將第一次請求的耗時由2s左右降到1s以內(測試結果是700多ms)。

存在問題
複用 HttpClient 後,依然存在一些問題:

  1. 因為是複用的 HttpClient ,那麼一些公共的設定就沒辦法靈活的調整了,如請求頭的自定義。1. 因為 HttpClient 請求每個 url 時,會快取該 url 對應的主機 ip ,從而會導致 DNS 更新失效( TTL 失效了)
    那麼有沒有辦法解決HttpClient的這些個問題?直到 HttpClientFactory 的出現,這些坑 “完美” 規避掉了。 ## 三、HttpClientFactory
  • HttpClientFacotry 很高效,可以最大程度上節省系統 socket 。(“JUST USE IT AND FXXK SHUT HttpClient 的管理,不需要我們人工進行物件釋放,同時,支援自定義請求頭,支援DNS更新等等等。- 從微軟原始碼分析,HttpClient 繼承自 HttpMessageInvoker ,而 HttpMessageInvoker 實質就是 HttpClient 被放到了“池子”中,工廠每次在 create 的時候會自動判斷是新建還是複用。(預設生命週期為 2 min) ## 3.1.1在 Startup.cs 中進行註冊
public class Startup
{<!-- -->
    public Startup(IConfiguration configuration)
    {<!-- -->
        Configuration = configuration;
    }
    public IConfiguration Configuration {<!-- --> get; }
    public void ConfigureServices(IServiceCollection services)
    {<!-- -->
        //other codes
        services.AddHttpClient("client_1", config => //這裡指定的 name=client_1 ,可以方便我們後期複用該例項
        {<!-- -->
            config.BaseAddress = new Uri("http://client_1.com");
            config.DefaultRequestHeaders.Add("header_1", "header_1");
        });
        services.AddHttpClient("client_2", config =>
        {<!-- -->
            config.BaseAddress = new Uri("http://client_2.com");
            config.DefaultRequestHeaders.Add("header_2", "header_2");
        });
        services.AddHttpClient();
        //other codes
        services.AddMvc().AddFluentValidation();
    }
}

3.1.2使用,這裡直接以 controller 為例,其他地方自行 DI

public class TestController : ControllerBase
{<!-- -->
    private readonly IHttpClientFactory _httpClient;
    public TestController(IHttpClientFactory httpClient)
    {<!-- -->
        _httpClient = httpClient;
    }
    public async Task<ActionResult> Test()
    {<!-- -->
        var client = _httpClient.CreateClient("client_1"); //複用在 Startup 中定義的 client_1 的 httpclient
        var result = await client.GetStringAsync("/page1.html");
        var client2 = _httpClient.CreateClient(); //新建一個 HttpClient
        var result2 = await client.GetStringAsync("http://www.site.com/XXX.html");
        return null;
    }
}

3.2.1:使用自定義類執行 HttpClientFactory 請求 自定義 HttpClientFactory 請求類

public class SampleClient
{<!-- -->
    public HttpClient Client {<!-- --> get; private set; }
    public SampleClient(HttpClient httpClient)
    {<!-- -->
        httpClient.BaseAddress = new Uri("https://api.SampleClient.com/");
        httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
        Client = httpClient;
    }
}

3.2.2在 Startup.cs 中 ConfigureService 方法中註冊 SampleClient

services.AddHttpClient<ISampleClient, SampleClient>();

3.3.3呼叫:

public class ValuesController : Controller
{<!-- -->
    private readonly ISampleClient  _sampleClient;
    public ValuesController(ISampleClient  sampleClient)
    {<!-- -->
        _sampleClient = sampleClient;
    }

    [HttpGet]
    public async Task<ActionResult> Get()
    {<!-- -->
        string result = await _sampleClient.GetData();
        return Ok(result);
    }
}