.Net 5 网络改进
起因

HTTP更好的错误处理
在.Net Core 3.1中,对Http这一块进行很多改进,收到很多请求要求对HttpClient增加区分 请求超时和取消Http请求 功能.最初需要自定义CancellationToken才可以实现.class Program
{
private static readonly HttpClient _client = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(10)
};
static async void Main(string[] args)
{
var cts = new CancellationTokenSource();
try
{
using var response = await _client.GetAsync("http://localhost:5000/sleepFor?secons=100", cts.Token);
}
catch (TaskCanceledException e) when (cts.IsCancellationRequested)
{
Console.WriteLine("取消请求:" + e.Message);
}
catch (TaskCanceledException e)
{
Console.WriteLine("请求超时:" + e.Message);
}
}
}
在这次调整改进,为了兼容性,可以通过TaskCanceledException过滤内部异常,来判断是否是超时.
class Program
{
private static readonly HttpClient _client = new HttpClient()
{
Timeout = TimeSpan.FromSeconds(10)
};
static async void Main(string[] args)
{
var cts = new CancellationTokenSource();
try
{
using var response = await _client.GetAsync("http://localhost:5000/sleepFor?secons=100", cts.Token);
}
catch (TaskCanceledException e) when (e.InnerException is TimeoutException)
{
Console.WriteLine("请求超时:" + e.Message);
}
catch (TaskCanceledException e)
{
Console.WriteLine("取消请求:" + e.Message);
}
}
}
另一个改进就是在HttpRequestException添加HttpStatusCode,在请求响应时调用EnsureSuccessStatusCode,在异常时,可以通过HttpStatusCode进行判断和处理.
class Program
{
private static readonly HttpClient _client = new HttpClient();
static async void Main(string[] args)
{
try
{
using var response = await _client.GetAsync("http://localhost:5000/doesNotExists");
response.EnsureSuccessStatusCode();
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("找不到doesNotExists:" + e.Message);
}
}
}
在HttpClient的辅助方法,如GetStringAsync/GetByteArrayAsync/GetStreamAsync没有返回HttpResponseMessage,而是在内部调用EnsureSuccessStatusCode,所以这几个辅助方法,不用再次调用EnsureSuccessStatusCode.
class Program
{
private static readonly HttpClient _client = new HttpClient();
static async void Main(string[] args)
{
try
{
//GetStringAsync/GetByteArrayAsync/GetStreamAsync不需要调用EnsureSuccessStatusCode
//这几个方法内部都已经调用过了EnsureSuccessStatusCode
using var stream = await _client.GetStreamAsync("http://localhost:5000/doesNotExists");
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("找不到doesNotExists:" + e.Message);
}
}
}
HttpRequestException增加新的外部可以使用的构造函数,可以手动创建带有StatusCode的异常.
class Program
{
private static readonly HttpClient _client = new HttpClient();
static async void Main(string[] args)
{
try
{
using var response = await _client.GetAsync("http://localhost:5000/doesNotExists");
if (response.StatusCode >= HttpStatusCode.BadRequest)
{
//主动将大于400的状态码,进行抛出异常
throw new HttpRequestException("请求异常", inner: null, response.StatusCode);
}
}
catch (HttpRequestException e) when (e.StatusCode == HttpStatusCode.NotFound)
{
Console.WriteLine("找不到doesNotExists:" + e.Message);
}
}
}
上边这些改进,都是社区进行改进的.
统一的Http跨平台实现
在.Net Core早期Http实现是要依赖WinHttpHandler(Windows)和CurlHandler(Linux/Mac).- WinHttpHandler 是基于Windows的WinHttp
- CurlHandler 是基于curl库,curl库可以在Linux/Unix/Mac运行的.
#引入WinHttpHandler库
dotnet add package system.net.http.winhttphandler
class Program
{
//WinHttpHandler 只支持Windows,如非必要的还是尽量不要使用WinHttpHandler
//CurlHandler 代码被移除,没有单独的nuget包
private static readonly HttpClient _client = new HttpClient(new WinHttpHandler());
static async void Main(string[] args)
{
using var response = await _client.GetAsync("http://localhost:5000/doesNotExists");
}
}
为SocketHttpHandler增加扩展性
HttpClient是一个高级的API,使用比较方便简单,但在个别情况下,不够灵活,也没法更细的力度的控制,所以我们为SocketHttpHandler增加两个扩展地方:- ConnectCallBack传递DnsEndPonit来确定Remote EndPoint,通过HttpMessageRequest创建和初始化连接对象.
- SocketHttpHandler提供连接池,创建的连接对象可以处理多个请求.
- 返回一个新的Stream(流)
- ConnectCallBack不应该创建TLS,TLS主要在SocketHttpHandler处理
- 没有设置ConnectCallBack,会调用默认的实现
private static async ValueTask<Stream> DefaultConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
socket.NoDelay = true;
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
// The stream should take the ownership of the underlying socket,
// closing it when it's disposed.
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
PlantextStreamFilter 允许在新打开的连接对象定制处理.该回调是连接对象创建完成之后(包括TLS握手),是在任何Http请求之前调用.使用注意事项:
- 传递一个Stream(流),写
- 相同的Stream(流)也将被后续的请求使用
- 使用Stream作为返回值,该Stream可以被修改,也可以把Stream自定义
- 自定义处理Stream
socketsHandler.PlaintextStreamFilter = (context, token) =>
{
Console.WriteLine($"Request {context.InitialRequestMessage} --> negotiated version {context.NegotiatedHttpVersion}");
return ValueTask.FromResult(context.PlaintextStream);
};
通过SocketHttpHandler 2个扩展创建连接的过程:
- 通过ConnectCallBack打开TCP连接
- SocketHttpHandler内部在有需要TLS,会创建TLS
- PlantextStreamFilter 如果没有注册,不进行调用
HttpClient同步方法 Send
一般我们建议使用异步网络API,以获得更好的性能和可伸缩性,在个别情况HttpClient需要同步阻塞等待.所以同步API也是必要的. SendAsync存在可伸缩性问题的,是因为多个线程完成单个操作,如UI线程死锁.为了避免这个问题,我们加入SendAsync同步版本Send, Send使用注意事项:- Send仅支持Http/1.1,是因为在Http/2连接对象是多路复用的.
- Send不能与ConnectCallBack一起使用.
- 如果使用SocketHttpHandler不是默认的实现,则必须要重写SocketHttpHandler中的Send函数,否则默认的HttpMessageHandler就会抛出异常.
- 如果对HttpContent自定义,这需要重写HttpContent中的SerializerToStream.
Http 2 版本选择
这里主要说的Http 2使用TLS和不使用TLS(h2c),主要是Http 2默认是使用TLS,请求和响应是加密的,不是明文的.在.Net 5中加入HttpVersionPolicy,通过设置HttpVersionPolicy来决定要不要使用TLS.class Program
{
private static readonly HttpClient _client = new HttpClient()
{
// Allow only HTTP/2, no downgrades or upgrades.
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
DefaultRequestVersion = HttpVersion.Version20
};
static async Task Main()
{
try
{
// Request clear-text http, no https.
// The call will internally create a new request corresponding to:
// new HttpRequestMessage(HttpMethod.Get, "http://localhost:5001/h2c")
// {
// Version = HttpVersion.Version20,
// VersionPolicy = HttpVersionPolicy.RequestVersionExact
// }
using var response = await _client.GetAsync("http://localhost:5001/h2c");
}
catch (HttpRequestException ex)
{
// Handle errors, including when h2c connection cannot be established.
Console.WriteLine("Error: " + ex.Message);
}
}
}
Http2 多连接
Http2 支持多个请求在单个TCP连接(connection)进行复用,在Http2规范中,只有一个TCP连接打开到服务器中,这个很适合浏览器,解决了在Http1.1中,多个请求多个连接的问题.class Program
{
private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
{
// Enable multiple HTTP/2 connections.
EnableMultipleHttp2Connections = true,
// Log each newly created connection and create the connection the same way as it would be without the callback.
ConnectCallback = async (context, token) =>
{
Console.WriteLine($"New connection to {context.DnsEndPoint} with request:{Environment.NewLine}{context.InitialRequestMessage}");
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true };
await socket.ConnectAsync(context.DnsEndPoint, token).ConfigureAwait(false);
return new NetworkStream(socket, ownsSocket: true);
},
})
{
// Allow only HTTP/2, no downgrades or upgrades.
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
DefaultRequestVersion = HttpVersion.Version20
};
static async Task Main()
{
// Burst send 2000 requests in parallel.
var tasks = new Task[2000];
for (int i = 0; i < tasks.Length; ++i)
{
tasks[i] = _client.GetAsync("http://localhost:5001/");
}
await Task.WhenAll(tasks);
}
}
EnableMultipleHttp2Connections这行如果注释掉或者改为false,在创建新连接的时候只调用一次ConnectionCallBack,在控制台上打印输出的信息.
可配置Ping
public class SocketsHttpHandler
{
//省略部分代码
// The client will send PING frames to the server if it hasn't receive any frame on the connection for this period of time.
public TimeSpan KeepAlivePingDelay { get; set; } = Timeout.InfiniteTimeSpan;
// The client will close the connection if it doesn't receive PING ACK frame within the timeout.
public TimeSpan KeepAlivePingTimeout { get; set; } = TimeSpan.FromSeconds(20);
// Whether the client will send PING frames only if there are any active streams on the connection or even if it's idle.
public HttpKeepAlivePingPolicy KeepAlivePingPolicy { get; set; } = HttpKeepAlivePingPolicy.Always;
//省略部分代码
}
public enum HttpKeepAlivePingPolicy
{
// PING frames are sent only if there are active streams on the connection.
WithActiveRequests,
// PING frames are sent regardless if there are any active streams or not.
Always
}
KeepAlivePingDelay(Timeout.InfinitieTimeSpan)的默认值表示此功能通常已关闭,PING帧不会自动发送到服务器。客户端仍将回复接收到的PING帧,无法关闭。要启用自动ping,必须将KeepAlivePingDelay更改为1分钟:
class Program
{
private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
{
KeepAlivePingDelay = TimeSpan.FromSeconds(60) //改为1分钟
});
}
只有当与服务器没有活跃的通信时,才会发送PING帧. 每一个从服务器收到的帧都会重置延迟,只有在没有收到KeepAlivePingDelay帧后,才会发送一个PING帧.服务器得到KeepAlivePingTimeout来应答.如果没有,就会被认为是丢失和断开的.该算法会定期检查延迟和超时.但最多每秒钟检查一次.将KeepAlivePingDelay或KeepAlivePingTimeout设置为较小的值将导致异常.