背景
公司的一款基于网络云盘的产品,需要统计每个链接到各个服务器节点的性能,以便后台做更优的调度。常用的性能指标有 DNS 解析耗时、连接耗时、ssl 握手耗时、首分片耗时、总的发送接收字节数、总的请求耗时以及基于它们计算的平均速度等。早先的基于 boost 的版本这些都很好统计,后来该产品底层网络库换成 cronet 就不好统计了,我的工作就是基于 cronet 重新收集上述性能信息。
cronet 网络编程
正式开搞前先简单看下 cronet 网络编程范式与之前有何不同:
/* by 01022.hk - online tools website : 01022.hk/zh/blood.html */ #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 语言的示例,运行后有以下输出:
/* by 01022.hk - online tools website : 01022.hk/zh/blood.html */ $ ./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 请求生命周期:
一般有这么几条路径:
* 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 握手耗时、首分片耗时都可以如法泡制,这里就不一一赘述了。
下面是整合后的示意图:
其中演示了 Cronet_UrlResponseInfo 与 Cronet_UrlRequestInfo 之间建立映射的过程,以及整个过程涉及的主要 Cronet 类型和回调。
后记
功能上线后,确实可用,解决了之前 cronet 收集不到链接性能数据的问题:
其中 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: 11windows 上挂上调度器看甚至有详细的崩溃堆栈:
明显是在在第一个回调 (on_response_started) 中访问空指针崩了,增加 executor 回调设置后,就正常了。所以有时候 AI 给出的结果也不完全靠谱,还得自己去实际跑跑才行。
githubdemo 中还有一个开关 (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 请求生命周期
本文来自博客园,作者:goodcitizen,转载请注明原文链接:https://www.cnblogs.com/goodcitizen/p/19253773/connection_performance_information_collection_based_on_cronet