BestHttp耗时数据分析
文章目录
BestHttp插件有自身有带有耗时统计的模块,但是各个耗时信息可能理解的不是很清楚。
请求流程
当我们使用BestHttp进行做HTTP请求的时候典型的方法如下:
HTTHPRequest request = new HTTPRequest(new Uri(url), OnRequestFinished);
request.Send();
当我们通过 new 来生成 HTTHPRequest 的时候,时间统计模块就初始化了:
public sealed class HTTPRequest : IEnumerator, IEnumerator<HTTPRequest> {
public HTTPRequest(Uri uri, HTTPMethods methodType, bool isKeepAlive, bool disableCache, OnRequestFinishedDelegate callback) {
// ...
this.Timing = new TimingCollector();
}
}
TimingCollector是BestHttp中用来存储对应时间的时间的地方,简化后的代码如下:
public sealed class TimingCollector {
/// <summary>
/// When the TimingCollector instance created.
/// </summary>
public DateTime Start { get; private set; }
/// <summary>
/// List of added events.
/// </summary>
public List<TimingEvent> Events { get; private set; }
public TimingCollector() {
this.Start = DateTime.Now;
}
/// <summary>
/// Add an event. Duration is calculated from the previous event or start of the collector.
/// </summary>
public void Add(string name) {
DateTime prevEventAt = this.Start;
if (this.Events.Count > 0)
prevEventAt = this.Events[this.Events.Count - 1].When;
this.Events.Add(new TimingEvent(name, DateTime.Now - prevEventAt));
}
/// <summary>
/// Add an event with a known duration.
/// </summary>
public void Add(string name, TimeSpan duration) {
this.Events.Add(new TimingEvent(name, duration));
}
}
我们可以看到,在执行构造函数的时候记录了当前时间作为网络请求的开始时间。 后续通过Add
操作来添加对应的事件并设置耗时。观察代码后我们可以看到这些事件名称所对应的字符串都存储在 TimingEventNames.cs
文件中,同时也可以在BestHttp官方文档中给出了这些事件的定义,具体可以看Timing API.代码如下:
public static class TimingEventNames
{
public const string Queued = "Queued";
public const string Queued_For_Redirection = "Queued for redirection";
public const string DNS_Lookup = "DNS Lookup";
public const string TCP_Connection = "TCP Connection";
public const string Proxy_Negotiation = "Proxy Negotiation";
public const string TLS_Negotiation = "TLS Negotiation";
public const string Request_Sent = "Request Sent";
public const string Waiting_TTFB = "Waiting (TTFB)";
public const string Headers = "Headers";
public const string Loading_From_Cache = "Loading from Cache";
public const string Writing_To_Cache = "Writing to Cache";
public const string Response_Received = "Response Received";
public const string Queued_For_Disptach = "Queued for Dispatch";
public const string Finished = "Finished in";
public const string Callback = "Callback";
}
各个事件发生的时机
1. Full Cache Load
当 HTTPRequest 对象调用Send方法后, 首先回去去判断这个请求有没有在缓存中存储,如果有在缓存中存在的话就会添加 “Full Cache Load” 事件, 耗时为当前时间减请求开始时间。
// HTTPManager.cs
public static HTTPRequest SendRequest(HTTPRequest request) {
// ...
#if !BESTHTTP_DISABLE_CACHING
// If possible load the full response from cache.
if (Caching.HTTPCacheService.IsCachedEntityExpiresInTheFuture(request))
{
DateTime started = DateTime.Now;
PlatformSupport.Threading.ThreadedRunner.RunShortLiving<HTTPRequest>((req) => {
if (Connections.ConnectionHelper.TryLoadAllFromCache("HTTPManager", req, req.Context)) {
req.Timing.Add("Full Cache Load", DateTime.Now - started);
req.State = HTTPRequestStates.Finished;
}
else {
// If for some reason it couldn't load we place back the request to the queue.
request.State = HTTPRequestStates.Queued;
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, RequestEvents.Resend));
}
}, request);
}
else
#endif
{
request.State = HTTPRequestStates.Queued;
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, RequestEvents.Resend));
}
return request;
}
2. Queued 和 Queued for redirection
如果没有从缓存中取到对应的数据,那么这个请求就会存入请求队列的中。 这个时候这个请求的事件会被标记为 RequestEvents.Resend
. 入队后的请求将会在 HTTPManager中的OnUpdate的事件中被调用。根据设置事件找到对应的 主机(Host)的连接(HTTPConnection)。另外多说下这里有进行连接复和最大连接数量的控制。
拿到对应的连接后,再通过连接(ConnectionBase,实际上可能为FileConnection或者HTTPConnection, 我们这里只讨论HTTPConnection)的 Process
方法进行实际的处理。而 Process 最终会通过多线程来进行处理。
这时如果是重定向的请求或者是普通请求就会记录下该事件的时间。代码如下:
// HTTPConnection.cs
public sealed class HTTPConnection : ConnectionBase {
protected override void ThreadFunc() {
if (this.CurrentRequest.IsRedirected)
this.CurrentRequest.Timing.Add(TimingEventNames.Queued_For_Redirection);
else
this.CurrentRequest.Timing.Add(TimingEventNames.Queued);
}
}
3. DNS Lookup
通过连接(HTTPConnection)中的 TCPConnector 中的客户端 TcpClient。 假设连接从头开始进行建立连接。那么首先通过域名获取IP地址, 获取IP地址完成后完成记录 DNS Lookup
时间:
try {
if (Client.ConnectTimeout > TimeSpan.Zero) {
// https://forum.unity3d.com/threads/best-http-released.200006/page-37#post-3150972
using (System.Threading.ManualResetEvent mre = new System.Threading.ManualResetEvent(false)) {
IAsyncResult result = System.Net.Dns.BeginGetHostAddresses(uri.Host, (res) => { try { mre.Set(); } catch { } }, null);
bool success = mre.WaitOne(Client.ConnectTimeout); // 这里在等待
if (success) {
addresses = System.Net.Dns.EndGetHostAddresses(result);
} else {
throw new TimeoutException("DNS resolve timed out!");
}
}
} else {
addresses = System.Net.Dns.GetHostAddresses(uri.Host);
}
}
finally {
request.Timing.Add(TimingEventNames.DNS_Lookup);
}
4. TCP Connection
通过域名获取到地址后就需要连接到对应的服务器上, TcpClient 实际上是使用 C# 的 Socket 进行的连接:
// TCPClient.cs
client = new Socket(family, SocketType.Stream, ProtocolType.Tcp);
// ...
var mre = new System.Threading.ManualResetEvent(false);
IAsyncResult result = client.BeginConnect(remoteEP, (res) => mre.Set(), null);
active = mre.WaitOne(ConnectTimeout);
if (active)
client.EndConnect(result);
else
{
try { client.Disconnect(true); }
catch { }
throw new TimeoutException("Connection timed out!");
}
在连接完成后记录 TCP Connection
事件:
// TCPConnector.cs
try {
Client.Connect(addresses, uri.Port, request);
}
finally {
request.Timing.Add(TimingEventNames.TCP_Connection);
}
5. Proxy Negotiation
连接完成后如有有设置Proxy则进行 Proxy Negotiation
事件的打点:
if (request.HasProxy) {
try {
request.Proxy.Connect(this.Stream, request);
}
finally {
request.Timing.Add(TimingEventNames.Proxy_Negotiation);
}
}
6. TLS Negotiation
TLS协商的时间,如果启用了TLS进行TLS协商,协商完成后记录事件:
if (isSecure)
{
DateTime tlsNegotiationStartedAt = DateTime.Now;
// ... 进行对应的TLS协商操作
request.Timing.Add(TimingEventNames.TLS_Negotiation, DateTime.Now - tlsNegotiationStartedAt);
}
7. Request Sent
等所有连接操作完成后,HTTPConnection会设置处理函数(requestHandler),这里以 HTTP1Handler
为例。当执行函数被调用时会将数据写到Stream
中去,等将数据写完后记录下 Request Sent
事件:
// HTTP1Handler.cs
// Write the request to the stream
this.conn.CurrentRequest.QueuedAt = DateTime.MinValue;
this.conn.CurrentRequest.ProcessingStarted = DateTime.UtcNow;
this.conn.CurrentRequest.SendOutTo(this.conn.connector.Stream);
this.conn.CurrentRequest.Timing.Add(TimingEventNames.Request_Sent);
8. Waiting (TTFB) 和 Headers
发送完数据后需要对HTTP的消息结构进行分析。
等接收完status line
后将会记录 Waiting (TTFB)
事件。
等接收完HTTP headers
后将会记录 Headers
事件。
public virtual bool Receive(int forceReadRawContentLength = -1, bool readPayloadData = true, bool sendUpgradedEvent = true) {
try {
// Read out 'HTTP/1.1' from the "HTTP/1.1 {StatusCode} {Message}"
statusLine = ReadTo(Stream, (byte)' ');
} catch {
// ...
}
if (!this.IsProxyResponse)
baseRequest.Timing.Add(TimingEventNames.Waiting_TTFB);
// ...
//Read Headers
ReadHeaders(Stream);
if (!this.IsProxyResponse)
baseRequest.Timing.Add(TimingEventNames.Headers);
// ...
}
10. Response Received
等接受数据完成后记录 Response Received
事件. 由于底层是使用 Socket 连接,使用 NetworkStream
进行数据的读写(收发,同步),简化代码如下;
// HTTP1Handler.cs
// Receive response from the server
bool received = Receive(this.conn.CurrentRequest);
this.conn.CurrentRequest.Timing.Add(TimingEventNames.Response_Received);
11. Queued for Dispatch 和 Finished in 以及 Callback
当网络请求完成后HTTPRequest
的状态将会被改变为 HTTPRequestStates.Finished
, 而在RequestEvent
状态发送改变后,在下一次的upadte里面将会进行处理并记录下 Finished in
事件。完成后如果这个请求有回调,那么将调用回调并在回调完成后记录 Callback
事件。
// RequestEvent.cs
internal static void HandleRequestStateChange(RequestEventInfo @event) {
HTTPRequest source = @event.SourceRequest;
switch (@event.State) {
case HTTPRequestStates.Queued:
source.QueuedAt = DateTime.UtcNow;
if ((!source.UseStreaming && source.UploadStream == null) || source.EnableTimoutForStreaming)
BestHTTP.Extensions.Timer.Add(new TimerData(TimeSpan.FromSeconds(1), @event.SourceRequest, AbortRequestWhenTimedOut));
break;
case HTTPRequestStates.ConnectionTimedOut:
case HTTPRequestStates.TimedOut:
case HTTPRequestStates.Error:
case HTTPRequestStates.Aborted:
source.Response = null;
goto case HTTPRequestStates.Finished;
case HTTPRequestStates.Finished:
// ... 执行一些处理
source.Timing.Add(TimingEventNames.Queued_For_Disptach);
source.Timing.Add(TimingEventNames.Finished, DateTime.Now - source.Timing.Start); // 注意: 结束事件为开始时间 到 当前时间
if (source.Callback != null) {
try {
source.Callback(source, source.Response);
source.Timing.Add(TimingEventNames.Callback);
}
catch (Exception ex) {}
}
break;
}
}
特殊情况
在发送请求时,会添加两个事件, 一个是将请求的状态变更为Queued一个是Resend事件。代码如下:
request.State = HTTPRequestStates.Queued;
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, RequestEvents.Resend));
当前设置请求的状态时,会添加一个状态改变的事件。 在状态事件改变的情况下状态为 Queued
的情况下将会记录进定时器里。 等计时完成后将结束这个请求。
// RequestEvent.cs
internal static void HandleRequestStateChange(RequestEventInfo @event) {
switch (@event.State) {
// ...
case HTTPRequestStates.Queued:
source.QueuedAt = DateTime.UtcNow;
if ((!source.UseStreaming && source.UploadStream == null) || source.EnableTimoutForStreaming)
BestHTTP.Extensions.Timer.Add(new TimerData(TimeSpan.FromSeconds(1), @event.SourceRequest, AbortRequestWhenTimedOut));
break;
}
}
private static bool AbortRequestWhenTimedOut(DateTime now, object context)
{
HTTPRequest request = context as HTTPRequest;
if (request.State >= HTTPRequestStates.Finished)
return false; // don't repeat
// Protocols will shut down themselves
if (request.Response is IProtocol)
return false;
if (request.IsTimedOut)
{
HTTPManager.Logger.Information("RequestEventHelper", "AbortRequestWhenTimedOut - Request timed out. CurrentUri: " + request.CurrentUri.ToString(), request.Context);
request.Abort();
return false; // don't repeat
}
return true; // repeat
}