博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

.NET Core 中正确使用 HttpClient 的姿势

Posted on 2022-08-27 00:31  火冰·瓶  阅读(413)  评论(0编辑  收藏  举报

另外一篇详解:https://blog.csdn.net/u010476739/article/details/119782562

 

为了更方便在服务端调用 HTTP 请求,微软在 .NET Framework 4.x 的时候引入了 HttpClient。但 HttpClient 有很多严重问题,一直饱受诟病,比如 InfoQ 的这篇文章 t.cn/Evzy80y,吐槽了 HttpClient 不能立即关闭连接、性能消耗严重等的问题。

.NET Core 2.1 开始引入的 HttpClientFactory 解决了 HttpClient 的所有痛点。有了 HttpClientFactory,我们不需要关心如何创建 HttpClient,又如何释放它。通过它可以创建具有特定业务的 HttpClient,而且可以很友好的和 DI 容器结合使用,更为灵活。下面以  Core 为例介绍 HttpClient 新的三种正确使用方式。

一、直接使用方式

不管是哪种方式,都要先注册服务,对于  Core 应用则是在 Startup.cs 文件的 ConfigureServices 添加如果代码:

services.AddHttpClient();

然后在 Controller 中通过构造注入的试获得 IHttpClientFactory,然后通过它来创建 HttpClient 对象。示例代码:

public class ValuesController : BaseController
{
    private readonly IHttpClientFactory _httpClientFactory;
    public ValuesController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    [HttpGet]
    public async Task<ActionResult> Get()
    {
        var client = _httpClientFactory.CreateClient();
        client.BaseAddress = new Uri("http://api.github.com");
        string result = await client.GetStringAsync("/");
        return Ok(result);
    }
}

这种使用方式适合一次性的 HTTP 请求调用,弊端是如果多次都要请求 github 的接口,那就得写很多重复代码配置 HttpClient。

二、命名式使用方式

命名式的使用方式可以解决第一种方式的问题,对于特定域名的 HTTP 调用,可以只做一次配置,然后在多个地方通过名称拿到相同的 Client 来使用。

首先在 Startup.cs 注册服务的地方添加带有特定名称的 HttpClient 服务,并添加需要的配置,如下示例:

services.AddHttpClient();
services.AddHttpClient("github", c =>
{
    c.BaseAddress = new Uri("https://api.github.com/");
    c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
    c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
});

注意这里添加了两次 AddHttpClient,一次是没有命名的,一次是有命名的。在 Controller 中的使用方式是:

public class ValuesController : BaseController
{
    private readonly IHttpClientFactory _httpClientFactory;

    public ValuesController(IHttpClientFactory httpClientFactory)
    {
        _httpClientFactory = httpClientFactory;
    }

    [HttpGet]
    public async Task<ActionResult> Get()
    {
        var client = _httpClientFactory.CreateClient("github");
        string result = await client.GetStringAsync("/");
        return Ok(result);
    }
}

但如果使用很频繁,这种获取 Client 的方式还是有一点麻烦。下面介绍第三种方式,可以在使用的时候方便一点,但配置要麻烦些。

三、类型化使用方式

类型化的使用方式可以预先把配置放到自定义的 HttpClient 中,然后在需要的地方都过依赖注入直接拿到 HttpClient 实例。

我们来看具体的例子,首先自定义一个 HttClient 类,比如访问 github 接口的类可以这样定义:

public class GitHubClient
{
    public HttpClient Client { get; private set; }

    public GitHubClient(HttpClient httpClient)
    {
        httpClient.BaseAddress = new Uri("https://api.github.com/");
        httpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
        httpClient.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");
        Client = httpClient;
    }

    public async Task<string> GetData()
    {
        return await _client.GetStringAsync("/");
    }
}

这里的 GetData 就是额外扩展的共用方法。使用前需要为这个类注册服务:

services.AddHttpClient<GitHubClient>();

然后在需要的地方(比如 Controller)中这样使用:

public class ValuesController : BaseController
{
    private readonly GitHubClient _gitHubClient;;

    public ValuesController(GitHubClient gitHubClient)
    {
        _gitHubClient = gitHubClient;
    }

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

类型化的方式和命名式的方式相比,虽然在配置的时候会麻烦一点,但它有两个好处:一是不用通过名称字符串来获得 Client,而是直接通过依赖注入获得具体的 Client;二是可以在自定义的 HttpClient 类中扩展共用的方法。

 

四、注意事项

1.添加header出现FormatException异常

//httpClient.DefaultRequestHeaders.Add("Authorization", auth);  //如果auth中包含=、-等符号,就会抛异常
httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", auth);

2.post传递图片

        /// <summary>
        /// 上传图片文件
        /// </summary>
        /// <param name="url"></param>
        /// <param name="filePath"></param>
        /// <param name="headers"></param>
        /// <returns></returns>
        public static string UploadFileApi(string url, string filePath, Dictionary<string, string> headers = null)
        {

            string str = "";
            //上传图片到服务器
            using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                HttpClient client = new HttpClient();
                MultipartFormDataContent form = new MultipartFormDataContent();//表单
                StreamContent fileContent = new StreamContent(fs);//图片stream
                fileContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");
                fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");
                fileContent.Headers.ContentDisposition.FileName = filePath;
                fileContent.Headers.ContentDisposition.Name = "file";
                form.Add(fileContent);
                if (headers != null)
                {
                    foreach (var header in headers)
                        client.DefaultRequestHeaders.Add(header.Key, header.Value);
                }

                HttpResponseMessage res = client.PostAsync(url, form).Result;
                str = res.Content.ReadAsStringAsync().Result;
            }
            return str;

        }

调用如下:

string url="http://localhost:3800/api/login/login1";//调用地址
string filePath="C://1.jpg";//文件在服务器的绝对路径
string result = UploadFileApi(url, filePath, null);

 尽管PostAsync有四个重载函数, 但是接受的都是HttpContent, 而查看源码可以看到, HttpContent是一个抽象类

那我们就不可能直接创建HttpContent的实例, 而需要去找他的实现类, 经过一番研究, 发现了, 如下四个:

MultipartFormDataContent、FormUrlEncodedContent、StringContent、StreamContent

和上面的总结进行一个对比就能发现端倪:

MultipartFormDataContent=》multipart/form-data

FormUrlEncodedContent=》application/x-www-form-urlencoded

StringContent=》application/json等

StreamContent=》binary