使用HttpClient的优解

新工做入职不久,目前仍然还在适应环境当中,笔者不得不说看别人的源码实在是使人痛苦。所幸前些日子终于将工做流畅地看了一遍,接下来就是熟悉框架技术的阶段了。html

也正是在看源码的过程中,有一个比较明显的用法细节引发了个人注意,我发现一位同事在请求远程Web Api时,虽然使用了 HttpClient 类,可是在用法上彷佛有些欠考虑。代码抽象出来就是如下的模样:数据库

using(var client = new HttpClient())
{
    //do something
}

咱们知道 using 关键字经常和实现了 IDisposable 接口的类型一块儿使用(如数据库链接和文件流操做),用于释放对象机资源(关于GC回收的相关知识可参考个人另外一篇博文《CLR和.Net对象生存周期》),可是对于 HttpClient 这样直接和TCP/IP协议打交道的类型倒是未必( HttpClient 继承了 HttpMessageInvoker 类, HttpMessageInvoker 实现了 IDisposable 接口,实现上是比较经典的代理模式),翻看一些国内外的文章都能看到对在 using 关键字中使用 HttpClient 的吐槽。事实是否是真的这样呢,其实只要作一个小实验就能够了。编程

让咱们先试着运行如下代码性能优化

class Program
    {
        static void Main(string[] args)
        {
            for (int i = 0; i < 10000; i++)
            {
                using (var client = new HttpClient())
                {
                    var result = client.GetAsync("http://www.baidu.com").Result;
                    Console.WriteLine(result.StatusCode);
                }
            }
            
            Console.ReadKey();
        }
    }

不出意外就会提示如下错误:框架

相关错误信息

单纯为了解决问题而言,咱们能够经过减少 HttpClientTimeout 属性加快回收速度(修改系统变量可能会引起其余的问题),但实际上,这仍是由于 HttpClient 消耗了太多套接字链接的关系。为了验证这个问题,咱们可使用TcpView这个小工具来查看下项目运行时的 TCP 链接数,若是你下载了代码运行后,会发现 TCP 链接和疯狗同样向上猛蹿。虽然还会有套接字回收的现象,可是和增长的速度相比确实是杯水车薪。函数

TCP链接数

因此这时候咱们须要换一种写法:工具

class Program
    {
        private static readonly HttpClient Client=new HttpClient();

        static void Main(string[] args)
        {
            for (int i = 0; i < 10000; i++)
            {
            
            
                var result = Client.GetAsync("http://www.baidu.com").Result;
                Console.WriteLine(result.StatusCode);
            }

            Console.ReadKey();
        }
    }

更换以上写法后,咱们会发现不管咱们将循环上限如何调整,也不会出现套接字链接资源不足的状况了,而TCPView的结果也好看得多,甚至若是咱们每次都测试传输时间的话,咱们会发现单次调用 HttpClient 而言,第二种代码比第一种代码要快得多。其实这很好理解,HttpClient内部维持一个专有的链接池,每一个HttpClient实例的请求相互隔绝,加快速度的缘由是由于重用了套接字,去除了套接字从新创建链接的过程。这也很好地解释了dudu园长的那一篇博客 《C#中HttpClient使用注意:预热与长链接》中的“预热”说法。盗一张图来讲明一下套接字的使用状况。性能

所以,在使用 HttpClient 时咱们知道如下几件小事测试

  • 将其定义为单例模式(由单独的HttpClient维护链接池)
  • 不要使用using关键字包裹(无效,套接字资源不会跟随释放)
  • 尽可能不要额外改变 HttpClient 的一些特殊行为(如上文中的TimeOut)
  • 当你须要配置不一样的Http请求时,容许生成并使用多个HttpClient

其实HttpClient还有一种使用隐患,DNS-Bug,这种作法国外也有同僚给出了相应的解释和解决方案,详情请见《Singleton HttpClient? Beware of this serious behaviour and how to fix it优化

单例模式扩展开来也有不少的说法,根据C#的一些规范,在编程中我推荐三种作法

A. 静态构造器

这种方式适用于如上代码场景,使用静态构造器确保静态字段的实例化。

class Program
    {

        private static readonly HttpClient Client;

        static Program()
        {
            Client=new HttpClient();
        }

        static void Main(string[] args)
        {
            //do something
        }
    }

B. HttpClientHelper

单例模式中,经典的双重检查锁定机制。

public static class HttpClientHelper
    {

        private static readonly object LockObj = new object();

        private static HttpClient _client;

        public static HttpClient HttpClient {
            get
            {
                if (_client == null)
                {
                    lock (LockObj)
                    {
                        if (_client == null)
                        {
                            _client= new HttpClient();
                        }
                    }
                }
                
                return _client;
            }
        }
    }

C. HttpClientHelper

这是在编程规范中推荐的一种的作法,经过使用静态构造函数可以精确保证Client变量可以在它第一次被使用前被实例化。

固然你也能够直接使用内联的方式进行初始化,这样能够对类型进行性能优化,不过变量的初始化时间就没法进行精准控制了

public sealed class HttpClientHelper
    { 

        private HttpClientHelper(){}
        
        public static readonly HttpClient Client;

        static HttpClientHelper()
        {
            Client=new HttpClient();   
        }
    }

寄语

多点实践多点总结,为认识更深入的代码世界而奋斗。

相关文章
相关标签/搜索