在之前的一周里,我利用业余时间帮人做视频下载工具,花了差不多20小时。期间发生的一些事情令我很不愉快,虽然到底是写好了相关类库,但最终成品是不打算做了。好在这段时间算是把 HttpClient 的相关知识复习了一遍,倒也不算白忙一场。
本文便是对此的总结。
------19.10.31 01.14AM-------
朋友联系我,说之前忙疯了,一直没回我。
所以我不生气了,打算认真把这东西做完。
但前面的字就不删了。当作某人的黑历史。
老子的心情还是很不愉悦很不愉悦不愉悦。
----------------------
============ HttpClient 部分 ============
1、创建尽可能少的HttpClient实例
以下代码明显是错误的:
using (var client = new HttpClient()) {
/*todo*/ }
官方文档的 Remark 部分对此有详细的介绍。这么做的后果是频繁调用将耗尽socket数量,造成 SocketException 。
正确的做法是创建尽可能少的实例,将针对某一类请求的 HttpClient 放入类的静态变量中,甚至放入静态工具类中。
2、针对性地分配HttpClient实例
基于第一条,显然整个程序集只使用一个 HttpClient 实例是最理想的情况,例如实现一个只需要处理知乎网站访问请求的程序。
然而很多情况下,一个程序集可能会提供多个网站客户端的实现,此时应当针对性地为每个实现分配一个 HttpClient 实例:
因为 HttpClient 只有几个异步方法是线程安全的,其他成员都非线程安全。
必须采用针对性分配的内容是那些必须通过 HttpClientHandler (在.NET Core中应该使用SocketsHttpHandler)及其派生类来设置的内容。
比如 重定向 ,代理 等等。这种情况下,应当使用 HttpClient(HttpMessageHandler) 构造函数初始化实例。
一个特殊的情况是记录cookie。
虽然我直接使用 HttpClientHandler.UseCookies 来使 HttpClient 能够记录cookie,但另一种常见的做法是通过请求获取的 HttpResponseMessage 与 HttpRequestMessage 来手动记录cookie,并通过 HttpClient.SendAsync 方法发送请求。
这样的好处是只需要一个 HttpClient 实例,缺点在于但一旦需要记录更多的网站cookie,那么就需要很多额外的操作。
3、除非确定数据小于83kb,否则应当用流来读取响应内容
首先让我们来关注两个数字:85000,81920。
一个对象,如果它的大小大于85000字节,那么它将会分配在 大型对象堆(LOH) 上。分配LOH具有极大的开销,所以如果数据大于这个值的情况下,依然直接使用 HttpContent.ReadAsByteArrayAsync 或 HttpContent.ReadAsStringAsync 方法来获取数据,则将会造成巨大的性能损失。
正确的做法是使用 HttpContent.CopyToAsync 方法,或者先通过 HttpContent.ReadAsStreamAsync 方法获取流,然后再进行相关操作。
3.5、缓冲区池化。
CopyToAsync
方法会使用 stream
默认的缓冲区。这个缓冲区的大小就是之前提到的81920。
扩大缓冲区的值会提高IO的效率,而缓冲区的复用可以减少内存分配的开销,避免造成内存碎片化,所以我建议使用缓冲区池。
利用 ConcurrentBag<T> 可以在几行代码之内实现缓冲区池:
private static class BytesPool
{
private static readonly ConcurrentDictionary<int, ConcurrentBag<byte[]>> _BytesPool = new ConcurrentDictionary<int, ConcurrentBag<byte[]>>();
public static byte[] Rent(int size) => _BytesPool.GetOrAdd(size, new ConcurrentBag<byte[]>()).TryTake(out var bytes) ? bytes : new byte[size];
public static void Return(byte[] bytes) => _BytesPool.GetOrAdd(bytes.Length, new ConcurrentBag<byte[]>()).Add(bytes);
}
除此之外,我还为Stream
写了一个扩展方法: