cm_httpclient_create创建客户端实例,回调函数触发时机分析

先看下cm_http.h头文件的定义,这里只保留了http回调相关的数据结构体



/**HTTP回调事件*/
typedef enum{
    CM_HTTP_CALLBACK_EVENT_REQ_START_SUCC_IND=1,        /*!< 请求启动成功事件,连接建立并发送请求头已开始 */
    CM_HTTP_CALLBACK_EVENT_RSP_HEADER_IND,              /*!< 接收到响应头事件,进入响应阶段 */
    CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND,             /*!< 接收到响应体数据块事件 (可能多次回调)*/
    CM_HTTP_CALLBACK_EVENT_RSP_END_IND,                 /*!< 响应接收结束(响应阶段完成) */
    CM_HTTP_CALLBACK_EVENT_ERROR_IND,                   /*!< 请求过程或响应过程出现错误(如连接失败/超时/解析失败等) */
}cm_httpclient_callback_event_e;
 
 
/**HTTP异常状态码
 * 运行过程中的异常事件码,通过回调在请求执行中途上报(函数已返回成功后)。
 * 它描述执行过程中出现的问题(DNS 失败、连接/握手超时、SSL 失败、接收超时、解析失败等)。
*/
typedef enum{
    CM_HTTP_EVENT_CODE_DNS_FAIL=1,                      /*!< DNS解析失败 */
    CM_HTTP_EVENT_CODE_CONNECT_FAIL,                    /*!< 连接服务器失败 */
    CM_HTTP_EVENT_CODE_CONNECT_TIMEOUT,                 /*!< 连接超时 */
    CM_HTTP_EVENT_CODE_SSL_CONNECT_FAIL,                /*!< SSL握手失败 */
    CM_HTTP_EVENT_CODE_CONNECT_BREAK,                   /*!< 连接异常断开 */
    CM_HTTP_EVENT_CODE_WAITRSP_TIMEOUT,                 /*!< 等待响应超时 */
    CM_HTTP_EVENT_CODE_DATA_PARSE_FAIL,                 /*!< 数据解析失败 */
    CM_HTTP_EVENT_CODE_CACHR_NOT_ENOUGH,                /*!< 缓存空间不足 */
    CM_HTTP_EVENT_CODE_DATA_DROP,                       /*!< 数据丢包 */
    CM_HTTP_EVENT_CODE_WRITE_FILE_FAIL,                 /*!< 写文件失败 */
    CM_HTTP_EVENT_CODE_UNKNOWN=255,                     /*!< 未知错误 */
}cm_httpclient_error_event_e;
 
 
/**HTTP结果码
 * 接口返回码,表示你刚调用的函数是否立即成功或失败(参数错误、没有空闲客户端、连接/发送失败等)。
 * 这是函数调用的同步结果
*/
 
typedef enum{
    CM_HTTP_RET_CODE_OK = 0,                            /*!< 成功 */
    CM_HTTP_RET_CODE_OPERATION_NOT_ALLOWED = 3,         /*!< 操作不被允许 3 */
    CM_HTTP_RET_CODE_MALLOC_FAIL = 23,                  /*!< 内存分配失败 23 */
    CM_HTTP_RET_CODE_PARAM_ERROR = 50,                  /*!< 参数错误 50 */
    CM_HTTP_RET_CODE_UNKNOWN_ERROR = 650,               /*!< 未知错误 650 */
    CM_HTTP_RET_CODE_NO_MORE_FREE_CLIENT = 651,         /*!< 没有空闲客户端 651 */
    CM_HTTP_RET_CODE_CLIENT_NOT_CREATE = 652,           /*!< 客户端未创建 652 */
    CM_HTTP_RET_CODE_CLIENT_IS_BUSY = 653,              /*!< 客户端忙 653 */
    CM_HTTP_RET_CODE_URL_PARSE_FAIL = 654,              /*!< URL解析失败 654 */
    CM_HTTP_RET_CODE_SSL_NOT_ENABLE = 655,              /*!< SSL未使能 655 */
    CM_HTTP_RET_CODE_CONNECT_FAIL = 656,                /*!< 连接失败 656 */
    CM_HTTP_RET_CODE_SEND_DATA_FAIL = 657,              /*!< 数据发送失败 657 */
    CM_HTTP_RET_CODE_OPEN_FILE_FAIL = 658,              /*!< 打开文件失败 658 */
} cm_httpclient_ret_code_e;
 
 
 
/**HTTP实例句柄*/
typedef void *cm_httpclient_handle_t;
 
/**HTTP回调*/
/*
* @param [in] client_handle                         客户端句柄
* @param [in] event                                 回调消息事件
* @param [in] param                                 事件相关对应响应数据
*
* CM_HTTP_CALLBACK_EVENT_REQ_START_SUCC_IND事件类型   传NULL
* CM_HTTP_CALLBACK_EVENT_RSP_HEADER_IND事件类型       传cm_httpclient_callback_rsp_header_param_t
* CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND事件类型      传cm_httpclient_callback_rsp_content_param_t
* CM_HTTP_CALLBACK_EVENT_RSP_END_IND事件类型          传NULL
* CM_HTTP_CALLBACK_EVENT_ERROR_IND事件类型            传cm_httpclient_error_event_e
*/
typedef void (*cm_httpclient_event_callback_func)(cm_httpclient_handle_t client_handle, cm_httpclient_callback_event_e event, void *param);
 
 
/**HTTP相关回调函数*/
typedef struct {
    uint32_t total_len;	                /*!< 数据总长度,不包含消息头(chunked模式时为0) */
    uint32_t sum_len;                   /*!< 已接收长度 */
    uint32_t current_len;               /*!< 当前包长度,本次回调中这块数据的长度 */
    const uint8_t *response_content;    /*!< 指向本次收到的数据块*/
}cm_httpclient_callback_rsp_content_param_t;
 
 
 
/**
 * @brief uri编码(不对字母数字以及-_.!~*'();/?:@&=+$,#进行编码)
 *
 * @param [in] src          原始数据
 * @param [in] len          原始数据长度
 *
 * @return 编码数据/NULL 失败
 *
 * @details uri编码,不对字母数字以及-_.!~*'();/?:@&=+$,#进行编码,返回值为内部分配空间使用完后,需自行释放。不支持中文域名解析。
 */
uint8_t *cm_httpclient_uri_encode(const uint8_t *src, const uint32_t len);
 
/**
 * @brief uri编码(不对字母数字以及-_.!~*'()进行编码)
 *
 * @param [in] src          原始数据
 * @param [in] len          原始数据长度
 *
 * @return 编码数据/NULL 失败
 *
 * @details uri编码,不对字母数字以及-_.!~*'()进行编码,返回值为内部分配空间使用完后,需自行释放。不支持中文域名解析。
 */
uint8_t *cm_httpclient_uri_encode_component(const uint8_t *src, const uint32_t len);
 
/**
 * @brief 创建客户端实例
 *
 * @param [in] url              服务器地址(服务器地址url需要填写完整,例如(服务器url仅为格式示例)"https://39.106.55.200:80")
 * @param [in] callback         客户端相关回调函数(使用cm_httpclient_sync_request()同步接口可忽略该参数,传NULL即可)
 * @param [out] handle          实例句柄
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 创建客户端实例
 */
cm_httpclient_ret_code_e cm_httpclient_create(const uint8_t *url, cm_httpclient_event_callback_func callback, cm_httpclient_handle_t *handle);
 
/**
 * @brief 删除客户端实例
 *
 * @param [in] handle         客户端实例句柄
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 本接口会将close socket(HTTP)操作发送至eloop模块中让其执行close socket操作。本接口返回成功代表操作已发送至eloop中,不代表已完成实例删除操作。用户连续两次调用本接口时建议中间保证100ms的延时。
 */
cm_httpclient_ret_code_e cm_httpclient_delete(cm_httpclient_handle_t handle);
 
/**
 * @brief 检测是否在执行请求
 *
 * @param [in] client           客户端实例句柄
 *
 * @return false 未执行请求/ true 正在执行请求
 *
 * @details 检测是否在执行请求
 */
bool cm_httpclient_is_busy(cm_httpclient_handle_t handle);
 
/**
 * @brief 客户端参数设置
 *
 * @param [in] handle         客户端实例句柄
 * @param [in] cfg            客户端配置
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 客户端参数设置,创建实例后设置,请求过程中不可设置;服务器应答重定向信息的情况下,采用cm_httpclient_set_cfg()配置的参数连接重定向的服务器
 */
cm_httpclient_ret_code_e cm_httpclient_set_cfg(cm_httpclient_handle_t handle, cm_httpclient_cfg_t cfg);
 
/**
 * @brief 终止http连接
 *
 * @param [in] handle         客户端实例句柄
 *
 * @return
 *
 * @details 终止http连接
 */
void cm_httpclient_terminate(cm_httpclient_handle_t handle);
 
/**
 * @brief 设置通用报头
 *
 * @param [in] handle           客户端实例句柄
 * @param [in] header           报头内容
 * @param [in] header_len       长度
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 设置通用报头,实例期间有效,不设置时,发送请求时将自动添加默认报头。
 * 直接引用header指针,不做拷贝,需保证请求过程中不做修改和释放,如外部释放,也
 * 需调用cm_httpclient_custom_header_free接口释放内部设置。
 */
cm_httpclient_ret_code_e cm_httpclient_custom_header_set(cm_httpclient_handle_t handle, uint8_t *header, uint16_t header_len);
 
/**
 * @brief 释放通用报头
 *
 * @param [in] handle           客户端实例句柄
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 重置客户端内部指针为NULL、长度为0。
 */
cm_httpclient_ret_code_e cm_httpclient_custom_header_free(cm_httpclient_handle_t handle);
 
/**
 * @brief 设置特定报头
 *
 * @param [in] handle           客户端实例句柄
 * @param [in] header           报头内容
 * @param [in] header_len       长度
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 设置特定报头,用于设置报头中,在不同请求时存在变化的字段,
 * 如果通用报头中同样存在该字段,将在发送请求时自动替换为当前设置值。
 * 直接引用header指针,不做拷贝,需保证请求过程中不做修改和释放,如外部释放,也
 * 需调用cm_httpclient_specific_header_free接口释放内部设置。
 */
cm_httpclient_ret_code_e cm_httpclient_specific_header_set(cm_httpclient_handle_t handle, uint8_t *header, uint16_t header_len);
 
/**
 * @brief 释放特定报头
 *
 * @param [in] handle           客户端实例句柄
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details  重置客户端内部指针为NULL、长度为0。
 */
cm_httpclient_ret_code_e cm_httpclient_specific_header_free(cm_httpclient_handle_t handle);
 
/**
 * @brief 开始发送请求
 *异步接口
 * @param [in] handle           客户端实例句柄
 * @param [in] method           请求类型
 * @param [in] path             请求路径
 * @param [in] chunked          是否为chunked模式
 * @param [in] content_length   content数据总长度(chunked模式时无效)
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 开始发送请求,主要为建立连接,并发送报头,将在request_start_func回调中指示开始完成
 * ,后续可发送content数据。
 */
cm_httpclient_ret_code_e cm_httpclient_request_start(cm_httpclient_handle_t handle, cm_httpclient_request_type_e method, const uint8_t *path,
bool chunked, uint32_t content_length);
 
/**
 * @brief 发送消息体
 *  异步接口
 * @param [in] handle           客户端实例句柄
 * @param [in] content          消息体内容
 * @param [in] content_len      消息体长度
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 发送消息体,执行request_start_func回调后可使用。 

 *          socket发送缓存空间64240,单次发送超过64240长度数据时可能会造成缓存区溢出从而导致发送失败。
 */
cm_httpclient_ret_code_e cm_httpclient_request_send(cm_httpclient_handle_t handle, const uint8_t *content, uint32_t content_len);
 
/**
 * @brief 请求发送结束
 * 异步接口
 * @param [in] handle           客户端实例句柄
 *
 * @return 0 成功/其他 失败(见cm_httpclient_ret_code_e)
 *
 * @details 请求发送结束,chunked模式下将发送结束包,普通模式下无实际效果,可不执行。
 */
cm_httpclient_ret_code_e cm_httpclient_request_end(cm_httpclient_handle_t handle);
 
/**
 * @brief 获取响应结果码
 *
 * @param [in] handle           客户端实例句柄
 *
 * @return -1 失败/其他 结果码
 *
 * @details 获取响应结果码。
 */
int32_t cm_httpclient_get_response_code(cm_httpclient_handle_t handle);
 
/**
 * @brief 解析报头
 *
 * @param [in] header           需解析的报头
 * @param [in] key              需要找到的字段key
 * @param [out] value           需要找到的字段数据
 *
 * @return 解析到的数据长度
 *
 * @details 解析报头,用于解析接收到的报头,按键值对进行解析,输入相应key,返回对应value,
 * value输出的为header中的指针位置,未单独分配空间。
 */
uint32_t cm_httpclient_parse_header(const uint8_t *header, const uint8_t *key, uint8_t **value);
 
/**
 * @brief 发送请求(同步接口)
 *
 * @param [in] handle           客户端实例句柄
 * @param [in] param            输入参数(见cm_httpclient_sync_param_t)
 * @param [out] response        响应结果(见cm_httpclient_sync_response_t)
 *
 * @return 见cm_httpclient_ret_code_e
 *
 * @details 发送http请求同步接口,只可用于非chunk模式发送,需先创建实例并完成相关参数设置,
 *响应结果中的数据未内部动态分配空间,使用完后可通过cm_httpclient_sync_free_data接口释放,
 *下次请求时也会自动释放。 

 *          socket发送缓存空间64240,单次发送超过64240长度数据时可能会造成缓存区溢出从而导致发送失败。
 */
cm_httpclient_ret_code_e cm_httpclient_sync_request(cm_httpclient_handle_t handle, cm_httpclient_sync_param_t param, 
        cm_httpclient_sync_response_t *response);
 
/**
 * @brief 释放响应数据(同步接口)
 *
 * @param [in] handle           客户端实例句柄
 *
 * @return 
 *
 * @details 释放响应数据。
 */
void cm_httpclient_sync_free_data(cm_httpclient_handle_t handle);
 

回调函数的创建和定义如下



       const char *server_url = "http://xxx.com:8080";          //仅作为示例,url不可使用
        const char *url_path   = "/download/system_patch.bin";   //仅作为示例,url path不可使用
        if(s_ota_http_handle == NULL)
        {
            //创建客户端实例,s_ota_http_handle为输出参数,用于保存客户端句柄
            
            cm_httpclient_create((const uint8_t*)server_url, __cm_ota_download_cb, &s_ota_http_handle);//初始化并设置回调函数
        }
 
        if(s_ota_http_handle == NULL)//初始化失败
        {
            cm_demo_printf("http handhle init fail
");
            return;
        }
        //开始发送请求
        cm_httpclient_request_start(s_ota_http_handle, HTTPCLIENT_REQUEST_GET, (const uint8_t*)url_path, false, 0);
 


/**
 *  rief http下载回调函数
 *  
 *  param [in] client_handle http句柄
 *  param [in] event http回调类型
 *  param [in] param http数据
 *
 *  
eturn None
 *  
 *  details 此示例下,http content内容即为差分包内容
 */
static void __cm_ota_download_cb(cm_httpclient_handle_t client_handle, cm_httpclient_callback_event_e event, void *param)
{
    (void)client_handle;
    int ret = 0;
 
    // HTTP 异步下载回调,只有在事件为 CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND 时处理数据
    if(param == NULL || event != CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND)
    {
        return; 
    }
 
    /*写入升级包*/
    cm_httpclient_callback_rsp_content_param_t *ota_pkg = (cm_httpclient_callback_rsp_content_param_t *)param;
    
    //已接收长度等于本地回调数据块的大小时,表示这块数据接收完成,更新升级包总大小
    if(ota_pkg->current_len == ota_pkg->sum_len)
    {
        cm_log_printf(0, "set total
");
        cm_ota_set_otasize(ota_pkg->total_len);
    }
    
    //把本次数据块写入升级包文件
    //这里的文件系统是模组内部可读写的闪存文件系统(如 SPI/NAND/NOR Flash 上的虚拟文件系统),用于暂存 OTA 升级包(差分/整包)。
    //下载的数据先写到这个文件系统文件中,待下载完整后再由 OTA 逻辑从该文件读取并烧录升级区域
    ret = cm_ota_firmware_write((const char*)ota_pkg->response_content, ota_pkg->current_len);
    if(ret)
    {
        return;
    }
 
    //显示下载进度
    cm_log_printf(0, "Writed: %d/%d - %d%%
",
        /*ota_pkg->sum_len*/cm_ota_get_written_size(),
        ota_pkg->total_len, ota_pkg->sum_len * 100 / ota_pkg->total_len);
 
    if(ota_pkg->sum_len >= ota_pkg->total_len)//下载完成
    {
        if(ota_pkg->sum_len > cm_ota_get_written_size())
        {
            cm_demo_printf("download ota pkg err!
");
        }
        else
        {
            cm_demo_printf("download ota pkg complete!
");
        }
    }
}

回调问题的回答

ota_pkg->current_len、ota_pkg->sum_len、ota_pkg->current_len三者什么关系,在一次回调中还是在多次回调中更新?

total_len:整个响应体的总长度;若服务器是 chunked 传输,可能为 0。total_len 在一次 HTTP 响应生命周期内是恒定的:如果服务器返回了 Content-Length,它在第一块数据到来前就由底层 HTTP 客户端填好,后续每次回调值不变;如果是 chunked 传输,通常为 0(或保持 0)。
设置时机:建立连接并收到响应头后,HTTP 客户端解析响应头(含 Content-Length),在首个内容块触发回调前就将 total_len 写入。之后的每次 CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND 回调都会带同一个 total_len。
current_len:本次回调这块数据的长度(单块大小)。
sum_len:到当前回调为止累计收到的长度(含本次块)
HTTP 响应体可能很大或分块传输,底层不会一次性把数据全部交给你,而是按到达/缓冲窗口大小分段推给应用层。每到一段数据就触发一次 CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND 回调,并更新:

current_len:这一段的大小。
sum_len:累计已收到的总字节数(包含当前段)。
total_len:整体大小(非 chunked 时已知;chunked 可能为 0)。
这样做的好处:

节省内存:不用一次性分配整包缓冲。
边收边写:能流式写入文件/闪存,减少 RAM 占用。
进度可视:用 sum_len/total_len 实时显示进度。
适配 chunked/不定长响应:chunked 时总长未知,也能持续接收直到结束。

关系与更新方式:
每次收到一块数据都会触发一次 CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND 回调。
在每次回调里,current_len 是这一块的大小,sum_len 会随着每次回调累加增长,直到等于或超过 total_len(非 chunked 时)。
当 current_len == sum_len 时,通常意味着这是第一块数据(之前累计为 0);示例代码借此在首块时调用 cm_ota_set_otasize 设置总大小。
进度计算用 sum_len / total_len,因此它是在多次回调中逐步更新的。

cm_httpclient_create函数中的回调函数,在什么情况下会被触发?

cm_httpclient_create 里的回调只在你用异步接口时才会触发(你把回调非 NULL 传进去,并调用 cm_httpclient_request_start / cm_httpclient_request_send / cm_httpclient_request_end 这类异步请求流程)。触发时机由事件枚举决定:
不会“调用就立即回调”,回调是按异步网络事件触发:
cm_httpclient_request_start 发起请求后,若成功,会先收到 REQ_START_SUCC,随后网络收响应头/响应体时再收 RSP_HEADER、多次 RSP_CONTENT,结束时 RSP_END,出错则 ERROR。
cm_httpclient_request_send 只是追加发送 body 数据,本身不必然触发新的回调;只有在发送/后续收响应时发生事件或错误才会回调。
cm_httpclient_request_end 表示请求体结束(含 chunked 结束块),本身不保证立刻有回调;后续进入响应阶段才会有 RSP_HEADER/RSP_CONTENT/RSP_END 或 ERROR。
所以回调由“通信进展或错误”驱动,不是每次调用 API 都同步触发。

CM_HTTP_CALLBACK_EVENT_REQ_START_SUCC_IND:连接建立、请求头开始发送完成。
CM_HTTP_CALLBACK_EVENT_RSP_HEADER_IND:收到响应头。
CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND:收到一段响应体数据(可能多次回调,分块推送)。
CM_HTTP_CALLBACK_EVENT_RSP_END_IND:响应体收完。
CM_HTTP_CALLBACK_EVENT_ERROR_IND:请求/响应过程出错(DNS、连接/握手失败、超时、解析失败等)。
如果回调参数传 NULL,或者使用同步接口 cm_httpclient_sync_request,就不会有这些回调。

回调函数参数param由HTTP客户端库在触发回调时填好并传入。它不是用户传的:库根据不同事件传不同的内容指针,例如:

CM_HTTP_CALLBACK_EVENT_RSP_HEADER_IND事件类型:传 cm_httpclient_callback_rsp_header_param_t*
CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND事件类型:传 cm_httpclient_callback_rsp_content_param_t*
CM_HTTP_CALLBACK_EVENT_ERROR_IND事件类型:传 cm_httpclient_error_event_e*(枚举值地址)
CM_HTTP_CALLBACK_EVENT_REQ_START_SUCC_IND / CM_HTTP_CALLBACK_EVENT_RSP_END_IND事件类型:通常传 NULL
你的回调只需要按事件类型把 param 转成对应的结构体/枚举使用。

cm_ota_firmware_write函数,最终将数据写到哪里?
只有在事件为 CM_HTTP_CALLBACK_EVENT_RSP_CONTENT_IND 时处理数据。每次收到一块响应体数据(ota_pkg),先在第一次块达到累计长度时设置总大小 cm_ota_set_otasize,然后把本次数据块写入升级包文件(cm_ota_firmware_write),打印进度,累计长度达到总长度后输出完成或错误提示。
“写入升级包到文件系统”:这里的文件系统是模组内部可读写的闪存文件系统(如 SPI/NAND/NOR Flash 上的虚拟文件系统)。数据先写到这个文件系统文件中,待下载完整后再由 OTA 逻辑从该文件读取并烧录升级区域。

cm_ota_firmware_write 会把传入的固件数据块写入模块内部的 OTA 存储区域,底层实现会选择模块预留的文件系统/Flash 区(用于暂存 OTA 升级包(差分/整包)),不是写到用户自定义路径。你在应用侧只需按顺序调用它流式写入整包,后续 cm_ota_upgrade() 会从这个内部缓存区读取并烧录。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容