ASP.NETCore发出HTTP请求

一、 类使用存在的问题

HttpClient类的使用所存在的问题,百度搜索的文章一大堆,好多都是单纯文字描述,让人感觉不太好理解,为了更好理解HttpClient使用存在的问题,下面让我们通过代码跟示例来描述。

using(var client = new HttpClient())

传统关闭连接方法如上述代码所示,但当使用using语句释放HttpClient对象的时候,套接字(socket)也不会立即释放,下面我们通过请求aspnetmonsters站点的示例来验证下:

class Program

{

static void Main(string[] args)

 {

       Console.WriteLine("Starting connections");

       var g = GetAsync();

       g.Wait();

       Console.WriteLine("Connections done");

       Console.ReadKey();

   }

   static async Task GetAsync()

   {

       for (int i = 0; i < 5; i++)

       {

           using (var client = new HttpClient())

           {

               var result = await client.GetAsync("http://aspnetmonsters.com/");

               Console.WriteLine(result.StatusCode);

           }

       }

   }

}

输出结果:

请输入图片描述

控制台打印出五条请求站点返回状态的信息,下面我们通过netstat工具打印出五个请求连接套接字状态:

请输入图片描述

应用程序已经运行结束了(结束连接),但是打印结果显示连接状态仍然是TIME_WAIT,也就是说在此状态期间仍然在观察是否有数据包进入连接(如果连接等待中有任何数据包仍然会通过),因为它们可能在某个地方被网络延迟,这是我从tcpstate窃取的TCP / IP状态图。

请输入图片描述

Windows将在此状态下保持连接240秒(由其设置[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\TcpTimedWaitDelay])。Windows可以快速打开新套接字的速度有限,因此如果您耗尽连接池,那么您可能会看到如下错误:

请输入图片描述

而怎么做才可以减少套接字的浪费呢?我们在上述代码中把每次循环中创建的HttpClient对象拉到Main外定义为一个共享的静态实例:

class Program

{

   private static HttpClient client = new HttpClient();

   static void Main(string[] args)

   {

       Console.WriteLine("Starting connections");

       var g = GetAsync();

       g.Wait();

       Console.WriteLine("Connections done");

       Console.ReadKey();

   }

   static async Task GetAsync()

   {

       for (int i = 0; i < 5; i++)

       {

           var result = await client.GetAsync("http://aspnetmonsters.com/");

           Console.WriteLine(result.StatusCode);

       }

   }

}

应用程序运动完毕之后,我们再通过netstat工具打印出五个请求连接套接字状态,这时候会看到信息如下:

请输入图片描述

通过共享一个实例,减少了套接字的浪费,实际上由于套接字重用而传输快一点。

总结:

在创建HttpClient实例的时候,最好是静态(static )实例。

不要用using包装HttpClient对象。

在.NET Core 2.1版本之后引入的 HttpClientFactory解决了HttpClient的所有痛点。有了 HttpClientFactory,我们不需要关心如何创建HttpClient,又如何释放它。

通过它可以创建具有特定业务的HttpClient,而且可以很友好的和 DI 容器结合使用,更为灵活。下面以 ASP.NET Core为例介绍HttpClientFactory的四种使用方式。

二、HttpClientFactory 的多种使用方式

可以通过多种使用方式在应用程序中使用HttpClientFactory。

2.1、直接使用HttpClientFactory

在Startup.ConfigureServices方法中,通过在IServiceCollection上调用AddHttpClient扩展方法可以注册IHttpClientFactory服务。

services.AddHttpClient();

注册服务后,我们新建BasicUsageModel类使用IHttpClientFactory创建HttpClient实例:

public class BasicUsageModel

{

   private readonly IHttpClientFactory _clientFactory;

   public IEnumerable<GitHubBranch> Branches { get; private set; }

   public bool GetBranchesError { get; private set; }

   public BasicUsageModel(IHttpClientFactory clientFactory)

   {

       _clientFactory = clientFactory;

   }

   public async Task OnGet()

   {

       var request = new HttpRequestMessage(HttpMethod.Get,   "https://api.github.com/repos/aspnet/AspNetCore.Docs/branches");

       request.Headers.Add("Accept", "application/vnd.github.v3+json");

       request.Headers.Add("User-Agent", "HttpClientFactory-Sample");

       var client = _clientFactory.CreateClient();

       var response = await client.SendAsync(request);

       if (response.IsSuccessStatusCode)

       {

           Branches = await response.Content.ReadAsAsync<IEnumerable<GitHubBranch>>();

       }

       else

       {

           GetBranchesError = true;

           Branches = Array.Empty<GitHubBranch>();

       }

   }

}

public class GitHubBranch

{

   public string name { get; set; }

}

以这种方式直接在使用IHttpClientFactory的类中调用CreateClient方法创建HttpClient实例。然后在Controller中调用BasicUsageModel类:

public class HomeController : Controller

{

   private readonly IHttpClientFactory _clientFactory;

   public HomeController(IHttpClientFactory clientFactory)

   {

       _clientFactory = clientFactory;

   }

   public IActionResult Index()

   {

       BasicUsageModel model = new BasicUsageModel(_clientFactory);

       var task = model.OnGet();

       task.Wait();

       List<GitHubBranch> list = model.Branches.ToList();

       return View(list);

   }

}

2.2、使用命名客户端

如果应用程序需要有许多不同的HttpClient用法(每种用法的服务配置都不同),可以视情况使用命名客户端。

可以在HttpClient中注册时指定命名Startup.ConfigureServices的配置。

services.AddHttpClient("github", c =>

{

c.BaseAddress = new Uri("https://api.github.com/");

// Github API versioning

   c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");

   // Github requires a user-agent

   c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample");

});

上面的代码调用AddHttpClient,同时提供名称“github”。此客户端应用了一些默认配置,也就是需要基址和两个标头来使用GitHub API。

每次调用CreateClient时,都会创建HttpClient 的新实例,并调用配置操作。

要使用命名客户端,可将字符串参数传递到CreateClient。指定要创建的客户端的名称:

public class NamedClientModel : PageModel

{

   private readonly IHttpClientFactory _clientFactory;

   public IEnumerable<GitHubPullRequest> PullRequests { get; private set; }

   public bool GetPullRequestsError { get; private set; }

   public bool HasPullRequests => PullRequests.Any();

   public NamedClientModel(IHttpClientFactory clientFactory)

   {

       _clientFactory = clientFactory;

   }

   public async Task OnGet()

   {

       var request = new HttpRequestMessage(HttpMethod.Get,"repos/aspnet/AspNetCore.Docs/pulls");

       var client = _clientFactory.CreateClient("github");

       var response = await client.SendAsync(request);

       if (response.IsSuccessStatusCode)

       {

           PullRequests = await response.Content.ReadAsAsync<IEnumerable<GitHubPullRequest>>();

       }

       else

       {

           GetPullRequestsError = true;

           PullRequests = Array.Empty<GitHubPullRequest>();

       }

   }

}

public class GitHubPullRequest

{

   public string url { get; set; }

   public int? id { get; set; }

   public string node_id { get; set; }

}

在上述代码中,请求不需要指定主机名。可以仅传递路径,因为采用了为客户端配置的基址。在Controller中调用方法如上个示例。

2.3、使用类型化客户端

什么是“类型化客户端”?它只是DefaultHttpClientFactory注入时配置的HttpClient。

下图显示了如何将类型化客户端与HttpClientFactory结合使用:

请输入图片描述

类型化客户端提供与命名客户端一样的功能,不需要将字符串用作密钥。它们提供单个地址来配置特定HttpClient并与其进行交互。例如,单个类型化客户端可能用于单个后端终结点,并封装此终结点的所有处理逻辑。另一个优势是它们使用 DI 且可以被注入到应用中需要的位置。

类型化客户端在构造函数中接收HttpClient参数:

public class GitHubService

{

   public HttpClient Client { get; }

   public GitHubService(HttpClient client)

   {

       client.BaseAddress = new Uri("https://api.github.com/");

       // GitHub API versioning

       client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");

       // GitHub requires a user-agent

       client.DefaultRequestHeaders.Add("User-Agent","HttpClientFactory-Sample");

       Client = client;

   }

   public async Task<IEnumerable<GitHubIssue>> GetAspNetDocsIssues()

   {

       var response = await Client.GetAsync("/repos/aspnet/AspNetCore.Docs/issues?state=open&sort=created&direction=desc");

       response.EnsureSuccessStatusCode();

       var result = await response.Content.ReadAsAsync<IEnumerable<GitHubIssue>>();

       return result;

   }

}

public class GitHubIssue

{

   public string url { get; set; }

   public int? id { get; set; }

   public string node_id { get; set; }

}

在上述代码中,配置转移到了类型化客户端中。HttpClient对象公开为公共属性。可以定义公开HttpClient功能的特定于API的方法。

GetAspNetDocsIssues方法从GitHub存储库封装查询和分析最新待解决问题所需的代码。

要注册类型化客户端,可在Startup.ConfigureServices中使用通用的AddHttpClient扩展方法,指定类型化客户端类:

services.AddHttpClient();

使用DI将类型客户端注册为暂时客户端。可以直接插入或使用类型化客户端:

public class TypedClientModel : PageModel

{

   private readonly GitHubService _gitHubService;

   public IEnumerable<GitHubIssue> LatestIssues { get; private set; }

   public bool HasIssue => LatestIssues.Any();

   public bool GetIssuesError { get; private set; }

   public TypedClientModel(GitHubService gitHubService)

   {

       _gitHubService = gitHubService;

   }

   public async Task OnGet()

   {

       try

       {

           LatestIssues = await _gitHubService.GetAspNetDocsIssues();

       }

       catch (HttpRequestException)

       {

           GetIssuesError = true;

           LatestIssues = Array.Empty<GitHubIssue>();

       }

   }

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容