(精华)2020年9月13日 C#基础知识点 网络编程HttpClient详解

418 阅读4分钟

一、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 ,那么一些公共的设置就没办法灵活的调整了,如请求头的自定义。
  2. 因为 HttpClient 请求每个 url 时,会缓存该 url 对应的主机 ip ,从而会导致 DNS 更新失效( TTL 失效了)
    那么有没有办法解决HttpClient的这些个问题?直到 HttpClientFactory 的出现,这些坑 “完美” 规避掉了。

三、HttpClientFactory

  • HttpClientFacotry 很高效,可以最大程度上节省系统 socket 。(“JUST USE IT AND FXXK SHUT
    UP”😛)
  • Factory,顾名思义 HttpClientFactory 就是 HttpClient 的工厂,内部已经帮我们处理好了对
    HttpClient 的管理,不需要我们人工进行对象释放,同时,支持自定义请求头,支持DNS更新等等等。
  • 从微软源码分析,HttpClient 继承自 HttpMessageInvoker ,而 HttpMessageInvoker 实质就是
    HttpClientHandler 。
  • HttpClientFactory 创建的 HttpClient ,也即是 HttpClientHandler ,这些个
    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);
    }
}