基于 cronet 的单链接性能信息收集

背景

公司的一款基于网络云盘的产品,需要统计每个链接到各个服务器节点的性能,以便后台做更优的调度。常用的性能指标有 DNS 解析耗时、连接耗时、ssl 握手耗时、首分片耗时、总的发送接收字节数、总的请求耗时以及基于它们计算的平均速度等。早先的基于 boost 的版本这些都很好统计,后来该产品底层网络库换成 cronet 就不好统计了,我的工作就是基于 cronet 重新收集上述性能信息。

cronet 网络编程

正式开搞前先简单看下 cronet 网络编程范式与之前有何不同:

#include <string>
#include <thread>
#include <iostream>
#include <cronet/cronet_c.h>

// 回调:重定向
void on_redirect_received(Cronet_UrlRequestCallback* callback,
                         Cronet_UrlRequest* request,
                         Cronet_UrlResponseInfo* info,
                         const char* new_location) {
    std::cout << "Redirect to: " << new_location << std::endl;
    Cronet_UrlRequest_FollowRedirect(request);
}


// 回调:响应开始
void on_response_started(Cronet_UrlRequestCallback* callback,
                        Cronet_UrlRequest* request,
                        Cronet_UrlResponseInfo* info) {
    std::cout << "Response started" << std::endl;
    Cronet_Buffer* buffer = Cronet_Buffer_Create();
    Cronet_Buffer_InitWithAlloc(buffer, 4096); // 4KB缓冲区
    Cronet_UrlRequest_Read(request, buffer);
}

// 回调:读取完成
void on_read_completed(Cronet_UrlRequestCallback* callback,
                      Cronet_UrlRequest* request,
                      Cronet_UrlResponseInfo* info,
                      Cronet_Buffer* buffer,
                      uint64_t bytes_read) {
    // 处理数据
    if (bytes_read > 0) {
        const char* data = static_cast<const char*>(Cronet_Buffer_GetData(buffer));
        std::cout << "Read " << bytes_read << " bytes" << std::endl;
        std::cout << data << std::endl; 
    }
    
    // 释放当前buffer
    Cronet_Buffer_Destroy(buffer);

    // 继续读取(如果还有数据且未完成)
    if (bytes_read > 0) {
        Cronet_Buffer* new_buffer = Cronet_Buffer_Create();
        Cronet_Buffer_InitWithAlloc(new_buffer, 4096);
        Cronet_UrlRequest_Read(request, new_buffer);
    } else {
        std::cout << "Read completed" << std::endl;
    }
}

// 回调:请求成功
void on_succeeded(Cronet_UrlRequestCallback* callback,
                 Cronet_UrlRequest* request,
                 Cronet_UrlResponseInfo* info) {
    std::cout << "Request succeeded" << std::endl;
}

// 回调:请求失败
void on_failed(Cronet_UrlRequestCallback* callback,
              Cronet_UrlRequest* request,
              Cronet_UrlResponseInfo* info,
              Cronet_Error* error) {
    std::cout << "Request failed" << std::endl;
}

// 回调:取消
void on_canceled(Cronet_UrlRequestCallback* callback,
                Cronet_UrlRequest* request,
                Cronet_UrlResponseInfo* info) {
    std::cout << "Request cancelled" << std::endl;
}

// Executor
void executor_func(Cronet_Executor *executor, Cronet_Runnable *runnable) {
    Cronet_Runnable_Run(runnable);
}

int main() {
    // 1. 创建引擎
    Cronet_EnginePtr engine = Cronet_Engine_Create();
    Cronet_EngineParamsPtr params = Cronet_EngineParams_Create();
    Cronet_Engine_StartWithParams(engine, params);
    
    // 3. 创建回调
    Cronet_UrlRequestCallbackPtr callback = Cronet_UrlRequestCallback_CreateWith(
        on_redirect_received,  // 重定向回调
        on_response_started,
        on_read_completed,
        on_succeeded,
        on_failed,
        on_canceled
    );
    
    // 4. 配置请求
    Cronet_UrlRequestParamsPtr req_params = Cronet_UrlRequestParams_Create();
    Cronet_UrlRequestParams_http_method_set(req_params, "GET");
    
    // 添加请求头
    Cronet_HttpHeaderPtr header = Cronet_HttpHeader_Create();
    Cronet_HttpHeader_name_set(header, "User-Agent");
    Cronet_HttpHeader_value_set(header, "Cronet-C-Client");
    Cronet_UrlRequestParams_request_headers_add(req_params, header);
    
    // 5. 创建执行器
    Cronet_ExecutorPtr executor = Cronet_Executor_CreateWith(executor_func);
    
    // 6. 创建并启动请求
    Cronet_UrlRequestPtr request = Cronet_UrlRequest_Create();
    Cronet_UrlRequest_InitWithParams(request, engine, 
                                     "http://httpbin.org/get", 
                                     req_params, callback, executor);
    Cronet_UrlRequest_Start(request);
    
    // 7. 等待请求完成
    std::this_thread::sleep_for(std::chrono::seconds(15));
    
    // 8. 清理资源
    Cronet_UrlRequest_Destroy(request);
    Cronet_HttpHeader_Destroy(header);
    Cronet_UrlRequestParams_Destroy(req_params);
    Cronet_Executor_Destroy(executor);
    Cronet_UrlRequestCallback_Destroy(callback);
    Cronet_EngineParams_Destroy(params);
    Cronet_Engine_Destroy(engine);
    
    return 0;
}

上面是 deepseek 生成的 cronet 基于 C 语言的示例,运行后有以下输出:

$ ./cronet_conn_stat
Response started
Read 279 bytes
{
  "args": {},
  "headers": {
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "Cronet-C-Client",
    "X-Amzn-Trace-Id": "Root=1-69425f84-67dbe33a06e303cf4c611b72"
  },
  "origin": "111.108.111.133",
  "url": "http://httpbin.org/get"
}

Request succeeded

与 libcurl 相比,Cronet_UrlRequest_Start 类似 curl_easy_perform 的角色,但变为异步执行,它会立即返回,之后通过回调不断通知连接上的事件,因此示例中是通过 sleep 15 秒来阻塞主线程的,工程实践中这个完全可以和消息、事件循环集成在一起,从而提高线程并发能力;与 boost 相比 (特别是基于 boost::asio::ip::tcp 版本的实现),完全不需要主动 async_resolve、async_connect、async_handeshake 以及 async_write,只需要在 on_read_completed 回调中无脑 Cronet_UrlRequest_Read 即可,底层过程 cronet 都帮你包办了,达到节省心智负担的目的。

不过这也带来一个问题,就是之前可以手动打桩计算的各种耗时,现在都看不到了,最多能获取个首分片耗时和总请求耗时,其中首分片这还包含了解析、连接、ssl 握手时长的耗时,相对失真了都。

cronet 对链接性能信息的支持

cronet 其实也有接口统计链接层的一些信息,主要通过下面的接口获取:

 ///////////////////////
// Struct Cronet_Metrics.
CRONET_EXPORT Cronet_MetricsPtr Cronet_Metrics_Create(void);
CRONET_EXPORT void Cronet_Metrics_Destroy(Cronet_MetricsPtr self);
...
// Cronet_Metrics getters.
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_request_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_dns_start_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_dns_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_connect_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_connect_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_ssl_start_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_ssl_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_sending_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_sending_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_push_start_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_push_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_response_start_get(
    const Cronet_MetricsPtr self);
CRONET_EXPORT
Cronet_DateTimePtr Cronet_Metrics_request_end_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
bool Cronet_Metrics_socket_reused_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
int64_t Cronet_Metrics_sent_byte_count_get(const Cronet_MetricsPtr self);
CRONET_EXPORT
int64_t Cronet_Metrics_received_byte_count_get(const Cronet_MetricsPtr self);

主要是通过 Cronet_Metrics_xxx 的接口获取,所需的 dns、connect、ssl、request 耗时都有,耗时是通过接口返回的两个时间做差值得到的,举例来说:

Cronet_DateTimePtr start = Cronet_Metrics_connect_start(metrics);
Cronet_DateTimePtr end = Cronet_Metrics_connect_end(metrics);
if (start && end) {
    int64_t start_ms = Cronet_DateTime_value_get(start);
    int64_t end_ms = Cronet_DateTime_value_get(end);
    int64_t connect = (start_ms > 0 && end_ms > 0) ? (end_ms - start_ms) : 0;
    // printf("connect elapse %lld\n", connect);
}

注意返回的 Cronet_DateTime 对象到毫秒值,还需要调用一个接口,莫法子,C 语言的接口就是这么废柴~

现在的关键落到了如何获取 Cronet_Metrics 对象,发现只有一个接口可以:

// Cronet_RequestFinishedInfo getters.
CRONET_EXPORT
Cronet_MetricsPtr Cronet_RequestFinishedInfo_metrics_get(
    const Cronet_RequestFinishedInfoPtr self);

需要输入 Cronet_RequestFinishedInfo 对象,这又是个什么东东,经过一番搜索,发现唯一途径是通过一个回调:

// The app implements abstract interface Cronet_RequestFinishedInfoListener by
// defining custom functions for each method.
typedef void (*Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc)(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error);

这个回调又是经由 Cronet_RequestFinishedInfoListener 对象设置的:

///////////////////////
// Abstract interface Cronet_RequestFinishedInfoListener is implemented by the
// app.

// There is no method to create a concrete implementation.

// Destroy an instance of Cronet_RequestFinishedInfoListener.
CRONET_EXPORT void Cronet_RequestFinishedInfoListener_Destroy(
    Cronet_RequestFinishedInfoListenerPtr self);
// Set and get app-specific Cronet_ClientContext.
...
// The app implements abstract interface Cronet_RequestFinishedInfoListener by
// defining custom functions for each method.
typedef void (*Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc)(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error);
// The app creates an instance of Cronet_RequestFinishedInfoListener by
// providing custom functions for each method.
CRONET_EXPORT Cronet_RequestFinishedInfoListenerPtr
Cronet_RequestFinishedInfoListener_CreateWith(
    Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc
        OnRequestFinishedFunc);

看这个 CreateWith 接口,它的唯一参数就是上面声明的用户回调。这一系列接口其实是创建了一个侦听器,之后还需要关联到引擎才能生效:

void on_request_finished_listener(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error)
{
}

...

int main() {
    // 1. 创建引擎
    Cronet_EnginePtr engine = Cronet_Engine_Create();
    Cronet_EngineParamsPtr params = Cronet_EngineParams_Create();
    Cronet_Engine_StartWithParams(engine, params);
    ...
    // 5. 创建执行器
    Cronet_ExecutorPtr executor = Cronet_Executor_CreateWith(executor_func);
    ...
    Cronet_RequestFinishedInfoListenerPtr listener = Cronet_RequestFinishedInfoListener_CreateWith(on_request_finished_listener);
    if (listener) {
        Cronet_Engine_AddRequestFinishedListener(engine, listener, executor);
        std::cout << "request finished listener registered" << std::endl;
    }
    else {
        std::cout << "setup request finished listener failed, no connection statistic provided" << std::endl;
    }
    
    ...
        
    // 8. 清理资源
    Cronet_UrlRequest_Destroy(request);
    Cronet_HttpHeader_Destroy(header);
    Cronet_UrlRequestParams_Destroy(req_params);
    
    if (listener) {
        Cronet_Engine_RemoveRequestFinishedListener(engine, listener);
        Cronet_RequestFinishedInfoListener_Destroy(listener);
    }
    
    Cronet_Executor_Destroy(executor);
    Cronet_UrlRequestCallback_Destroy(callback);
    Cronet_EngineParams_Destroy(params);
    Cronet_Engine_Destroy(engine);
    
    return 0;
}

关联侦听器的接口如下:

CRONET_EXPORT
void Cronet_Engine_AddRequestFinishedListener(
    Cronet_EnginePtr self,
    Cronet_RequestFinishedInfoListenerPtr listener,
    Cronet_ExecutorPtr executor);
CRONET_EXPORT
void Cronet_Engine_RemoveRequestFinishedListener(
    Cronet_EnginePtr self,
    Cronet_RequestFinishedInfoListenerPtr listener);

这样整个流程就串起来了:在 Cronet_Engine 初始化时创建并关联一个 Cronet_RequestFinishedInfoListener 对象,该对象持有一个 Cronet_RequestFinishedInfoListener_OnRequestFinished 类型的用户回调,当连接结束时 cronet 会将性能信息通过该回调通知到用户,用户通过回调的第二个参数 request_info 获取链接性能信息,即 Cronet_RequestFinishedInfo -> Cronet_Metrics,再通过后者的一系列接口获取感兴趣的信息。

从整个流程可以看出:

* 性能信息只有在连接关闭时才能获取到

* 性能信息并不是关联到单链接 (Cronet_UrlRequest),而是关联到全局 (Cronet_Engine)

* 可以关联多个 Listener 对象,但感觉没什么必要

性能信息投递

回到业务层面,每个下载任务包含若干链接,在任务结束时 (成功、失败或取消) 对链接信息进行上报,平时这些信息是由链接对象管理的,因此需要将位于全局回调的性能信息进行投递。

用户定义的链接对象一般是关联到 Cronet_UrlRequest,即通过下面的接口:

///////////////////////
// Concrete interface Cronet_UrlRequest.

CRONET_EXPORT void Cronet_UrlRequest_SetClientContext(
    Cronet_UrlRequestPtr self,
    Cronet_ClientContext client_context);
CRONET_EXPORT Cronet_ClientContext
Cronet_UrlRequest_GetClientContext(Cronet_UrlRequestPtr self);

顺便插一句,cronet 中各种对象都支持设置用户数据,命名也非常统一: XXX_Get/SetClientContext。

这样就可以通过 Cronet_UrlRequest 对象找到关联的链接对象,回过头来再看性能信息的回调:

// The app implements abstract interface Cronet_RequestFinishedInfoListener by
// defining custom functions for each method.
typedef void (*Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc)(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error);

这里提供的不是 Cronet_UrlRequest 而是 Cronet_UrlResponseInfo,两者对不上,于是问题演化为如何通过 Cronet_UrlResponseInfo 找到 Cronet_UrlRequest。

梳理 cronet 请求生命周期:

 

image

一般有这么几条路径:

* onResponseStarted -> onReadCompleted -> onSucceeded

* onCanceled / onResponseStarted -> onCanceled / onResponseStarted -> onReadCompleted -> onCanceled

* onFailed / onResponseStarted -> onReadCompleted -> onFailed

302 重定向就不单独列出了,可以在 follow 和 cancel 中选一种继续。结合相关的回调函数原型观察:

// The app implements abstract interface Cronet_UrlRequestCallback by defining
// custom functions for each method.
typedef void (*Cronet_UrlRequestCallback_OnRedirectReceivedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info,
    Cronet_String new_location_url);
typedef void (*Cronet_UrlRequestCallback_OnResponseStartedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info);
typedef void (*Cronet_UrlRequestCallback_OnReadCompletedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info,
    Cronet_BufferPtr buffer,
    uint64_t bytes_read);
typedef void (*Cronet_UrlRequestCallback_OnSucceededFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info);
typedef void (*Cronet_UrlRequestCallback_OnFailedFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info,
    Cronet_ErrorPtr error);
typedef void (*Cronet_UrlRequestCallback_OnCanceledFunc)(
    Cronet_UrlRequestCallbackPtr self,
    Cronet_UrlRequestPtr request,
    Cronet_UrlResponseInfoPtr info);

发现它们都提供 Cronet_UrlRequest & Cronet_UrlResponseInfo 两个对象,于是一个大胆的想法诞生了:在所有回调中建立二者的映射关系,最终在侦听器回调中再通过 Cronet_UrlResponseInfo 反查 Cronet_UrlRequest !

这个想法是可行的,特别是 Cronet_RequestFinishedInfoListener_OnRequestFinishedFunc 保证在上述所有回调之后被调用。

整合在一起

假设上述关系通过全局 rr_map 变量映射在一起,那么最终的 listener 回调可以这样实现:

extern on_request_finished(Cronet_ClientContext obj, int64_t connect); 
void on_request_finished_listener(
    Cronet_RequestFinishedInfoListenerPtr self,
    Cronet_RequestFinishedInfoPtr request_info,
    Cronet_UrlResponseInfoPtr response_info,
    Cronet_ErrorPtr error)
{
    int64_t connect = 0; 
    Cronet_MetricsPtr metrics = Cronet_RequestFinishedInfo_metrics(request_info); 
    if (metrics) {
        Cronet_DateTimePtr start = Cronet_Metrics_connect_start(metrics);
        Cronet_DateTimePtr end = Cronet_Metrics_connect_end(metrics);
        if (start && end) {
            int64_t start_ms = Cronet::instance()->Cronet_DateTime_value_get(start);
            int64_t end_ms = Cronet::instance()->Cronet_DateTime_value_get(end);
            connect = (start_ms > 0 && end_ms > 0) ? (end_ms - start_ms) : 0;
        }
    }

    auto it = rr_map.find(response_info); 
    if (it != rr_map.end()) { 
        Cronet_UrlRequestPtr req = it->second;
        Cronet_ClientContext obj = Cronet_UrlRequest_GetClientContext(req); 
        if (obj) {
            on_request_finished(obj, connect);
        }
    }
}

其中 on_request_finished 是用户实现的回调,它的两个参数 obj 和 connect 分别是用户定义的链接层对象与连接耗时。其它的像 dns 耗时、ssl 握手耗时、首分片耗时都可以如法泡制,这里就不一一赘述了。

下面是整合后的示意图:

image

其中演示了 Cronet_UrlResponseInfo 与 Cronet_UrlRequestInfo 之间建立映射的过程,以及整个过程涉及的主要 Cronet 类型和回调。

后记

功能上线后,确实可用,解决了之前 cronet 收集不到链接性能数据的问题

image

其中 connect (xx) 标识的即为连接耗时。

本文完整的 demo 可参考 github 上的 cronet_conn_stat 项目,支持在 mac & windows 上进行验证。

下面是 demo 的一个典型输出:

$ ./cronet_conn_stat
request finished listener registered
Response started
Read 279 bytes
{
  "args": {},
  "headers": {
    "Accept-Encoding": "gzip, deflate",
    "Host": "httpbin.org",
    "User-Agent": "Cronet-C-Client",
    "X-Amzn-Trace-Id": "Root=1-694268b2-27badad81ccc4cdb11ccc5f3"
  },
  "origin": "111.108.111.133",
  "url": "http://httpbin.org/get"
}

request finished listen
request finish, connect elapse 346 ms
Request succeeded

最后一行输出了连接耗时。

早先 deepseek 给的一版示例中,未给 Cronet_Executor 提供回调函数:

    Cronet_ExecutorPtr executor = Cronet_Executor_CreateWith(NULL);

编译正常,但运行到第一个回调时,就会崩溃:

$ ./cronet_conn_stat
request finished listener registered
Segmentation fault: 11

windows 上挂上调度器看甚至有详细的崩溃堆栈:

cronet_crash

明显是在在第一个回调 (on_response_started) 中访问空指针崩了,增加 executor 回调设置后,就正常了。所以有时候 AI 给出的结果也不完全靠谱,还得自己去实际跑跑才行。

github demo 中还有一个开关 (ENABLE_EXECUTOR_THREAD),可以控制是否将各种事件的回调放在一个单独的线程中去执行:

void custom_executor_func(Cronet_Executor *executor, Cronet_Runnable *cronet_task) {
    ExecutorThread* et = (ExecutorThread*)Cronet_Executor_GetClientContext(executor); 
    if (!et) {
        std::cerr << "Executor not initialized!" << std::endl;
        return;
    }

    // 将Cronet的任务包装成std::function
    if (cronet_task) {
        et->postTask([cronet_task]() {
            // 执行Cronet任务
            Cronet_Runnable_Run(cronet_task);
        });
    }
}

为此还引入了一个线程与函数的投递封装类 (ExectorThread),有兴趣的读者可以去研究下。

参考

[1]. blob/main/components/cronet/native/test/url_request_test.cc

[2]. Cronet 请求生命周期

posted @ 2026-01-05 15:14  goodcitizen  阅读(73)  评论(0)    收藏  举报