BestHttp耗时统计数据的源码分析

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
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值