转载 c#:HttpClient使用详解
c#:HttpClient使用详解
环境:
- window10
- vs2019
- .netcore 3.1
- centos 7.6
一、在c#中发送http请求的方式
本部分参考:《WebClient, HttpClient, HttpWebRequest ,RestSharp之间的区别与抉择》
在c#中常见发送http请求的方式如下:
-
HttpWebRequest:
.net 平台原生提供,这是.NET创建者最初开发用于使用HTTP请求的标准类。使用HttpWebRequest可以让开发者控制请求/响应流程的各个方面,如 timeouts, cookies, headers, protocols。
.
关于使用HttpWebRequest上传和下载文件,可参考:《c#使用Http上传下载文件》 -
WebClient:
.net 平台原生提供,WebClient是一种更高级别的抽象,是HttpWebRequest为了简化最常见任务而创建的,但也因此缺少了HttpWebRequest的灵活性。
-
HttpClient
:.net 平台原生提供,也是这次主讲的内容。
-
RestSharp:
开源项目,它是基于HttpWebRequest做的二次封装,这里不再说明,可参考:《c#: 使用restsharp发送http请求、下载文件》。
-
Flurl:
开源项目,基于
HttpClient
做的二次封装,项目地址:https://github.com/tmenier/Flurl
在.net core平台下推荐使用HttpClient
。
二、HttpClient介绍
本部分参考: 《MSDN: SocketsHttpHandler 》
在System.Net.Http命名空间下的HttpClient
是.net core平台最常用的http请求工具,它直接基于Socket开发,提供了异步友好的代码编写方式。
下面是简单示例:
看样子使用起来挺简单的。其实隐藏了一些细节,如果我们要对这些细节进行配置的话,建议写成下面的形式:
// 在.net core 2.1之后,默认所有的http请求都会交给SocketsHttpHandler处理
var socketsHttpHandler = new SocketsHttpHandler()
{
AllowAutoRedirect = true,// 默认为true,是否允许重定向
MaxAutomaticRedirections = 50,//最多重定向几次,默认50次
//MaxConnectionsPerServer = 100,//连接池中统一TcpServer的最大连接数
UseCookies = false// 是否自动处理cookie
};
var client = new HttpClient(socketsHttpHandler);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
三、HttpClient相关的类
- HttpClient类:提供用户调用的入口;
- HttpRequestMessage类:表示用户请求消息;
- HttpResponseMessage类:表示http响应消息;
上面是我们最常见的类,除此之外还有:
- HttpMessageInvoker:表示发起http消息的入口,HttpClient类就是继承了它,但也仅有HttpClient继承它;
HttpMessageHandler
:虽然HttpMessageInvoker表示http消息的入口,但发送http消息还要靠HttpMessageHandler,事实上,HttpMessageInvoker内部就封装者一个HttpMessageHandler;SocketsHttpHandler
:继承HttpMessageHandler,它是.net core2.1之后事实上的HttpMessageHandler,也就是说我们代码中发送http消息基本用的就是它;- HttpClientHandler:也继承自HttpMessageHandler,但其内部封装者SocketsHttpHandler,一般情况下http请求是转发给内部的SocketsHttpHandler处理的;
- DelegatingHandler:也继承自HttpMessageHandler,不过它是一个抽象类,旨在提供一个http请求管道的基类;
四、HttpClient使用时的注意事项
HttpClient类旨在提供一个用户入口,其内部管理着不同服务器的TCP连接池,如下图所示:
所以,当我们需要发起http请求时,最好使用全局单例的HttpClient,而不是每次都new一个HttpClient。
另外,由于TCP本身在断开连接的时候需要4次挥手动作,而其中又有一个等待时间,所以我们即使将HttpClient.Dispose()掉也会造成这个TCP连接短时间内无法断开(最长要持续4分钟),如果遇到高并发的话,很可能端口就不够用了。正是因为这个原因,微软又出了一个IHttpClientFactory帮助我们建立可复用的HttpClient。
注意:上面缓存连接的时候是以传入的地址前缀做key,而不是最终解析的ip地址,所以,HttpClient对DNS解析不太友好。
HttpClient是线程安全的,里面封装了链接池,使用DnSpy验证如下:
五、HttpClient的使用配置
当我们发送http请求时,我们需要关注一些事情,比如:
-
是否自动处理cookie;
默认HttpClient是自动处理cookie的,即:上一个请求返回的cookie,可能会随着下次请求发送出去。
然而,最佳的使用方式是多次请求使用相同的HttpClient所以这个cookie隔离性就很差,我们可以在创建HttpClient的时候进行配置禁用cookie自动处理:var socketsHttpHandler = new SocketsHttpHandler() { UseCookies = false,// 是否自动处理cookie }; var client = new HttpClient(socketsHttpHandler);
- 1
- 2
- 3
- 4
- 5
-
是否自动重定向以及最多重定向几次;
默认HttpClient自动处理重定向请求,并且最多重定向50次,一般我们不需要修改这个配置,但我们做测试的话,可以向下面写法:
var socketsHttpHandler = new SocketsHttpHandler() { AllowAutoRedirect=true,//是否自动重定向 MaxAutomaticRedirections=50//自动重定向的最大次数 }; var client = new HttpClient(socketsHttpHandler);
- 1
- 2
- 3
- 4
- 5
- 6
-
内部TCP链接池的设置;
这个地方有三个配置项:
- MaxConnectionsPerServer: 每个url(如:http://www.baidu.com:80)最多有几个链接,默认是int.MaxValue。注意:url是不带路径及参数;
- PooledConnectionIdleTimeout: 每个TCP链接空闲的时间,因为TCP长时间不用也要及时释放嘛,此处默认2分钟;
- PooledConnectionLifetime: 每个TCP链接从创建开始存活的时间,默认是不限制的,一般也不用设置这个参数;
直接看代码示例:
var socketsHttpHandler = new SocketsHttpHandler() { //每个请求连接的最大数量,默认是int.MaxValue,可以认为是不限制 MaxConnectionsPerServer = 100, //连接池中TCP连接最多可以闲置多久,默认2分钟 PooledConnectionIdleTimeout = TimeSpan.FromMinutes(2), //连接最长的存活时间,默认是不限制的,一般不用设置 PooledConnectionLifetime = Timeout.InfiniteTimeSpan, }; var client = new HttpClient(socketsHttpHandler);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
-
是否压缩;
默认是不压缩,如果设置开启压缩的话,http请求头中会自动加上
Accept-Encoding: gzip
(当然你得设置压缩选项是gzip),如果后台也支持这种压缩的话,就会把消息体压缩并在响应头中添加Content-Encoding: gzip
。asp.net core添加压缩支持参考:《ASP.NET Core中的响应压缩》
设置的代码示例如下:var socketsHttpHandler = new SocketsHttpHandler() { //默认是None,即不压缩 AutomaticDecompression = DecompressionMethods.GZip, }; var client = new HttpClient(socketsHttpHandler);
- 1
- 2
- 3
- 4
- 5
- 6
-
超时设置;
有三个配置项:
- ConnectTimeout: 连接时超时时间,默认不限制
- Expect100ContinueTimeout: 等待返回100状态码的时间,默认1秒,根据msdn解释,当请求头有
Expect: 100-continue
的时候,服务端应返回100状态码 - Timeout: 等待响应超时时间,默认:100秒。
看下面的代码示例:
var socketsHttpHandler = new SocketsHttpHandler() { //建立TCP连接时的超时时间,默认不限制 ConnectTimeout = Timeout.InfiniteTimeSpan, //等待服务返回statusCode=100的超时时间,默认1秒 Expect100ContinueTimeout = TimeSpan.FromSeconds(1), }; var client = new HttpClient(socketsHttpHandler); //等待响应超时时间,默认:100秒。 client.Timeout = TimeSpan.FromSeconds(100);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
-
响应头数据大小限制;
MaxResponseHeadersLength: http响应头最大字节数(单位:KB),默认:64,即:http响应头最大64KB,一般不用设置
看代码设置:var socketsHttpHandler = new SocketsHttpHandler() { MaxResponseHeadersLength = 64, //单位: KB }; var client = new HttpClient(socketsHttpHandler);
- 1
- 2
- 3
- 4
- 5
-
关于Drain的配置;
没有搞懂什么是Drain,好像是:当关闭连接时需要从这个连接中排出未使用的数据,当排出超时或排出的字节数超出限制时就直接把连接关闭了,而不是放到池子里重用。对应的配置项:
MaxResponseDrainSize、ResponseDrainTimeout。 -
关于BaseAddress的配置:可以给HttpClient设置基地址,当HttpClient发送的请求不包含前缀时,将自动拼接上,否则不予拼接,如下:
var client = new HttpClient(); client.BaseAddress = new Uri("http://192.168.0.9:9000/"); var url = "/index.html"; await client.GetAsync();// 真实地址是: http://192.168.0.9:9000/index.html
- 1
- 2
- 3
- 4
-
默认的http版本、请求头:
默认的http版本是1.1,实验设置2.0没效果;
默认请求头为空,可以自己设置如下:var client = new HttpClient(); client.DefaultRequestVersion = HttpVersion.Version20; client.DefaultRequestHeaders.Add("machine-id", "1");
- 1
- 2
- 3
-
关于SslOptions:
可以在这里面配置SSL相关的东西。这里我只实验了一种场景,即:访问https网站时由于网站自身的证书不规范导致报错:“AuthenticationException: The remote certificate is invalid according to the validation procedure.”
在我们使用HttpWebRequest的时候我们可以通过下面回调设置:httpWebRequest.ServerCertificateValidationCallback = (sender, cer, chain, err) => true;
在HttpClient时,我们对应的设置为:var socketsHandler = new SocketsHttpHandler() { SslOptions = new System.Net.Security.SslClientAuthenticationOptions() { RemoteCertificateValidationCallback = (sender, cer, chain, err) => true } }; var httpClient = new HttpClient(socketsHandler);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
需要注意: 这是由于网站自身的证书不规范造成的,我们正常访问 https://www.baidu.com 即使不加这个配置也是正常的。
验证相关:PreAuthenticate、Credentials,不过这两个怎么实验都看不到效果,应该也用不到。
代理相关:Proxy、UseProxy、DefaultProxyCredentials 这几个未做实验。
六、HttpClient提供的方法
6.1 通过Get请求数据
var httpClient = new HttpClient();
var url = "http://localhost:9000/index.html";
var response = await httpClient.GetAsync(url);
var str = await response.Content.ReadAsStringAsync();
- 1
- 2
- 3
- 4
6.2 通过Get下载文件
现在我们要下载一个文件,假如这个文件不超过2G且不需要下载进度提示,那么我们可以如下操作:
var httpClient = new HttpClient();
var response = await httpClient.GetAsync("http://localhost:9000/middledata.mp4");//763M
var fileStream = new FileStream("e:\\middle.db", FileMode.OpenOrCreate, FileAccess.Write);
await response.Content.CopyToAsync(fileStream);
fileStream.Close();
- 1
- 2
- 3
- 4
- 5
但考虑到下载的文件会过大,比如:3GB,这个时候首先HttpClient的缓冲区就不够用了,因为它最大设置的是:int.MaxValue=2^31-1≈2GB,看下面的报错代码:
var httpClient = new HttpClient();
//默认缓冲大小为: 2147483647=int.MaxValue=2^31-1≈2GB,如果下载的文件过大就会报异常: "Cannot write more bytes to the buffer than the configured maximum buffer size: 2147483647."
//可以手动设置缓冲区大小,但最大就是int.MaxValue,再大就会报错: "Buffering more than 2147483647 bytes is not supported."
httpClient.MaxResponseContentBufferSize = 2L << 32; //调成4GB,发现报错
- 1
- 2
- 3
- 4
这个时候我们就不能一次性获取Http响应报文的全部内容了,需要如下操作:
var httpClient = new HttpClient();
var url = "http://localhost:9000/bigdata.mp4";//3GB大小
//注意:因为太大,必须指定 HttpCompletionOption.ResponseHeadersRead,即:拿到响应头就返回
var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
var fileStream = new FileStream("e:\\bigdata.db", FileMode.OpenOrCreate, FileAccess.Write);
// 虽然上面指定拿到响应头就返回,但这里依然可以拿到下载的文件流
await response.Content.CopyToAsync(fileStream);
fileStream.Close();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
现在,需要对这个大文件加上下载进度提示,我们需要事先获取文件的大小,这通过http响应头的Content-Length可以获取到(但并不总能获取到):
var httpClient = new HttpClient();
var url = "http://localhost:9000/bigdata.mp4";
var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
var totalLength = response.Content.Headers.ContentLength;
var contentStream = await response.Content.ReadAsStreamAsync();
var fileStream = new FileStream("e:\\bigdata.db", FileMode.OpenOrCreate, FileAccess.Write);
byte[] buffer = new byte[5 * 1024];//5KB缓存
long readLength = 0L;
int length;
while ((length = await contentStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
readLength += length;
if (totalLength > 0)
{
Console.WriteLine("下载进度: " + Math.Round((double)readLength / totalLength.Value * 100, 2) + "%");
}
else
{
Console.WriteLine("已下载: " + Math.Round((readLength / 1024.0), 2) + "KB");
}
fileStream.Write(buffer, 0, length);
}
fileStream.Close();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
6.3 通过Post请求数据: application/x-www-form-urlencoded
var httpClient = new HttpClient();
var url = "http://192.168.0.9:9000/Demo/PostUrlCode";
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("name","小明"),
new KeyValuePair<string, string>("age","20")
}));
var str = await response.Content.ReadAsStringAsync();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
上面的请求报文:
POST /Demo/PostUrlCode HTTP/1.1
Host: 192.168.0.9:9000
Content-Type: application/x-www-form-urlencoded
Content-Length: 30
name=%E5%B0%8F%E6%98%8E&age=20
- 1
- 2
- 3
- 4
- 5
- 6
对应的asp.net core后台:
//注意:asp.net core webapi和mvc模式解析参数的时候存在差别,这里是asp.net core webapi
[HttpPost]
public string PostUrlCode([FromForm] string name, [FromForm] int age)
{
var str = "";
foreach (var header in Request.Headers)
{
str += $"{header.Key}: {header.Value.ToString()}\r\n";
}
return $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {name} {age} \r\n{str}";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
http响应报文体:
2021-08-19 19:35:04.804 小明 20
Content-Type: application/x-www-form-urlencoded
Host: 192.168.0.9:9000
Content-Length: 30
- 1
- 2
- 3
- 4
如果想传递数组,发送请求时如下:
var httpClient = new HttpClient();
var url = "http://192.168.0.9:9000/Demo/PostUrlCode";
var response = await httpClient.PostAsync(url, new FormUrlEncodedContent(new List<KeyValuePair<string, string>>()
{
new KeyValuePair<string, string>("names[0]","小明"),
new KeyValuePair<string, string>("names[1]","小红"),
new KeyValuePair<string, string>("age","20")
}));
var str = await response.Content.ReadAsStringAsync();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
对应的请求报文:
POST /Demo/PostUrlCodeArr HTTP/1.1
Host: 192.168.0.9:9000
Content-Type: application/x-www-form-urlencoded
Content-Length: 70
names%5B0%5D=%E5%B0%8F%E6%98%8E&names%5B1%5D=%E5%B0%8F%E7%BA%A2&age=20
- 1
- 2
- 3
- 4
- 5
- 6
此时asp.net core后台:
//注意:asp.net core webapi和mvc模式解析参数的时候存在差别,这里是asp.net core webapi
[HttpPost]
public string PostUrlCodeArr([FromForm] string[] names, [FromForm] int age)
{
var str = "";
foreach (var header in Request.Headers)
{
str += $"{header.Key}: {header.Value.ToString()}\r\n";
}
return $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {string.Join(",", names)} {age} \r\n{str}";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
http响应报文体:
2021-08-19 19:37:16.874 小明,小红 20
Content-Type: application/x-www-form-urlencoded
Host: 192.168.0.9:9000
Content-Length: 70
- 1
- 2
- 3
- 4
6.4 使用Post请求数据:application/json
var httpClient = new HttpClient();
var url = "http://192.168.0.9:9000/Demo/PostUrlJson"
var response = await httpClient.PostAsync(
url,
new StringContent(
Newtonsoft.Json.JsonConvert.SerializeObject(new { Name = "小明", Id = 1 }),
Encoding.UTF8,
"application/json")
);
var str = await response.Content.ReadAsStringAsync();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
产生的请求报文:
POST /Demo/PostUrlJson HTTP/1.1
Host: 192.168.0.9:9000
Content-Type: application/json; charset=utf-8
Content-Length: 24
{"Name":"小明","Id":1}
- 1
- 2
- 3
- 4
- 5
- 6
此时asp.net core后台:
[HttpPost]
public string PostUrlJson([FromBody] RequestModel req)
{
var str = "";
foreach (var header in Request.Headers)
{
str += $"{header.Key}: {header.Value.ToString()}\r\n";
}
return $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {req.Name} {req.Id} \r\n{str}";
}
public class RequestModel
{
public int Id { get; set; }
public string Name { get; set; }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
返回的http响应体:
2021-08-19 19:51:00.901 小明 1
Content-Type: application/json; charset=utf-8
Host: 192.168.0.9:9000
Content-Length: 24
- 1
- 2
- 3
- 4
6.5 通过Post上传文件:multipart/form-data
此处假设上传的文件不大。
上传代码如下:
var httpClient = new HttpClient();
var url = "http://localhost:9000/Demo/PostMulti";
var content = new MultipartFormDataContent();
content.Add(new StringContent("小明"), "name");
content.Add(new StringContent("18"), "age");
//注意:要指定filename,即:test.txt,否则后台不认为是一个文件,而是普通的参数
content.Add(new ByteArrayContent(System.IO.File.ReadAllBytes("e:\\test.txt")), "file", "test.txt");
var response = await httpClient.PostAsync(url, content);
var str = await response.Content.ReadAsStringAsync();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
这里用到的文件很小:
// test.txt
this is file.
- 1
- 2
产生的请求报文如下:
POST /Demo/PostMulti HTTP/1.1
Host: 192.168.0.9:9000
Content-Type: multipart/form-data; boundary="ecc429f8-1f51-43a6-af5b-0fc8a88da513"
Content-Length: 451
--ecc429f8-1f51-43a6-af5b-0fc8a88da513
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=name
小明
--ecc429f8-1f51-43a6-af5b-0fc8a88da513
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=age
18
--ecc429f8-1f51-43a6-af5b-0fc8a88da513
Content-Disposition: form-data; name=file; filename=test.txt; filename*=utf-8''test.txt
this is file.
--ecc429f8-1f51-43a6-af5b-0fc8a88da513--
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
后台aspet core代码如下:
[HttpPost]
public string PostMulti([FromForm] string name, [FromForm] int age)
{
var str = "";
foreach (var header in Request.Headers)
{
str += $"{header.Key}: {header.Value.ToString()}\r\n";
}
if (Request.Form.Files.Count > 0)
{
var file = Request.Form.Files[0];
var fileName = file.FileName;
var fileLength = file.Length;
using var stream = file.OpenReadStream();
var bytearr = new byte[fileLength];
stream.ReadAsync(bytearr);
var fileContent = Encoding.UTF8.GetString(bytearr);
str += "\r\n" + fileContent;
}
return $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {name} {age} \r\n{str}";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
对应的http响应报文体:
2021-08-19 22:40:25.626
Content-Type: multipart/form-data; boundary="ecc429f8-1f51-43a6-af5b-0fc8a88da513"
Host: 192.168.0.9:9000
Content-Length: 451
this is file.
- 1
- 2
- 3
- 4
- 5
- 6
现在,客户端要上传一个大文件,该如何操作?
我们知道作为一个web服务器一般是不会允许上传太大文件的,所以这里首先要说一下服务器端的限制。
假设asp.net core直接在Kestrel下面运行,那么它将有如下限制:
- kestrel限制请求体最大为:28M;
- formbody最大为:128M
先看第一个限制:
可引发异常 “Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Request body too large”
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.ConfigureKestrel(options =>
{
//options.Limits.MaxRequestBodySize = 30000000L;//默认约28M
//options.Limits.MaxRequestBodySize = 2 * 2L << 30;//指定最大2G
options.Limits.MaxRequestBodySize = null;//去掉限制
});
webBuilder.UseStartup<Startup>();
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
第二个限制:
可引发异常:“Failed to read the request form. Multipart body length limit 134217728 exceeded.”
public void ConfigureServices(IServiceCollection services)
{
services.Configure<FormOptions>(x =>
{
//x.MultipartBodyLengthLimit = 134217728;//默认128MB
x.MultipartBodyLengthLimit = 5 * 2L << 30;//这里手动设置为5GB,这么大的数值仅用于演示
});
services.AddControllers();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
除了服务器端的限制,客户端也有限制,那就是因为上传的文件太大,导致等待响应时间超时,引发的异常为:System.Threading.Tasks.TaskCanceledException:“A task was canceled.”。
解决办法:
var httpClient = new HttpClient();
httpClient.Timeout = Timeout.InfiniteTimeSpan;//仅用于演示,将时间改为无限长
- 1
- 2
其实上传大文件的时候遇到的限制,无非就是时间和空间的。在做项目的时候注意其配置就可以了。
下面演示上传大文件:
var httpClient = new HttpClient();
httpClient.Timeout = Timeout.InfiniteTimeSpan;
var url = "http://192.168.0.9:9000/Demo/PostMulti2";
var content = new MultipartFormDataContent();
content.Add(new StringContent("小明"), "name");
content.Add(new StringContent("18"), "age");
var filepath = @"E:\BaiduNetdiskDownload\Docker实战.pdf";//97.6MB
filepath = @"E:\BaiduNetdiskDownload\dnSpy_v6.14.zip";//151.6MB
filepath = @"E:\BaiduNetdiskDownload\dotnetfx35.exe";//231MB
filepath = @"E:\BaiduNetdiskDownload\Photoshop_13_LS3(cs6)安装包.7z";//1.12GB
filepath = @"E:\BaiduNetdiskDownload\cn_windows_10_business_editions_version_2004_updated_june_2020_x64_dvd_49d8dbba.iso";//4.83GB
using var fileStream = new FileStream(filepath, FileMode.Open, FileAccess.Read);
using var streamContent = new StreamContent(fileStream, 2048);
content.Add(streamContent, "file", "bigdata.db");
var response = await httpClient.PostAsync(url, content);
var str = await response.Content.ReadAsStringAsync();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
放开限制后,上面的最大文件4.83G也是可以上传的。
对了,后端的代码为:
[HttpPost]
public async Task<string> PostMulti2([FromForm] string name, [FromForm] int age)
{
var str = "";
Console.WriteLine($"{DateTime.Now.ToString("F")} comming... ");
foreach (var header in Request.Headers)
{
str += $"{header.Key}: {header.Value.ToString()}\r\n";
}
if (Request.Form.Files.Count > 0)
{
var file = Request.Form.Files[0];
var fileName = file.FileName;
var fileLength = file.Length;
using var stream = file.OpenReadStream();
var filepath = "e:\\bigdataupload.db";
if (System.IO.File.Exists(filepath))
{
System.IO.File.Delete(filepath);
}
using var destStream = new FileStream("e:\\bigdataupload.db", FileMode.CreateNew, FileAccess.Write);
await stream.CopyToAsync(destStream);
str += $"\r\nfileName={fileName}\r\n{fileLength}=fileLength";
}
else
{
str += "no file";
}
return $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {name} {age} \r\n{str}";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
七、使用DelegatingHandler实现http请求拦截管道
类比asp.net core的请求管道,借助DelegatingHandler我们也能在HttpClient中轻松实现中间拦截。
拦截的核心是使用DelegatingHandler,它继承自HttpMessageHandler,并且封装了一个HttpMessageHandler,这样就允许我们对HttpMessageHandler进行层层封装,每封装的一层就可认为是asp.net core中的中间件,封装完成后将最外层的DelegatingHandler交给HttpClient去使用便完成了构建过程。
下面演示构造的两个中间件,拦截示意图:
代码如下:
class Program
{
static async Task Main(string[] args)
{
var httpClient = new HttpClient(new InterceptAMessageHandler(new InterceptBMessageHandler(new SocketsHttpHandler())));
var resposne = await httpClient.GetAsync("http://www.baidu.com");
Console.WriteLine("ok");
}
}
public class InterceptAMessageHandler : DelegatingHandler
{
public InterceptAMessageHandler(HttpMessageHandler innerHandler) : base(innerHandler)
{
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} InterceptAMessageHandler before Send");
var response = await base.SendAsync(request, cancellationToken);
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} InterceptAMessageHandler after Send");
return response;
}
}
public class InterceptBMessageHandler : DelegatingHandler
{
public InterceptBMessageHandler(HttpMessageHandler innerHandler) : base(innerHandler)
{
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} InterceptBMessageHandler before Send");
var response = await base.SendAsync(request, cancellationToken);
Console.WriteLine($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} InterceptBMessageHandler after Send");
return response;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
运行效果:
八、FAQ
8.1 向请求头里放非标准的 Authorization 引发报错
比如:
var req = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(url),
};
req.Headers.Add("Authorization", "test:123");
// System.FormatException:“The format of value 'test:123' is invalid.”
- 1
- 2
- 3
- 4
- 5
- 6
- 7
解决办法:使用 req.Headers.TryAddWithoutValidation("Authorization","test:123")
设置长连接与短连接
HttpRequestHeaders.ConnectionClose = true
=>Connection: close
HttpRequestHeaders.ConnectionClose = false
=>Connection: Keep-Alive
-
HttpRequestHeaders.ConnectionClose = null
=>Connection: Keep-Alive
-
HttpWebRequest.KeepAlive = true
=>Connection: Keep-Alive
HttpWebRequest.KeepAlive = false
=>Connection: close
关闭POST前使用100-Continue协议
var c = new HttpClient();
c.DefaultRequestHeaders.ExpectContinue = false;