IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> http 协议文件上传 - mongoose -> 正文阅读

[网络协议]http 协议文件上传 - mongoose

? ? ? ? 用 mongoose 源码搭建的 http 服务在上一篇文章里已经实现了文件的下载,那文件上传是否也可以支持呢?答案是支持的。这里涉及到几个消息事件,其定义为:

//#define MG_EV_HTTP_MULTIPART_REQUEST 121 /* struct http_message */
//#define MG_EV_HTTP_PART_BEGIN 122        /* struct mg_http_multipart_part */
//#define MG_EV_HTTP_PART_DATA 123         /* struct mg_http_multipart_part */
//#define MG_EV_HTTP_PART_END 124          /* struct mg_http_multipart_part */
/* struct mg_http_multipart_part */
//#define MG_EV_HTTP_MULTIPART_REQUEST_END 125

单个文件上传的完整过程,就是这几个事件的触发过程。因为上传只是触发事件,而实际如何处理还得用户决定,所以 mongoose 提供了用户函数注册,当请求指定 uri 时,调用指定回调函数。注册函数接口为:

mg_register_http_endpoint(con, "/fileUpload", fileUpload);

main 函数:

int main(int argc, char *argv[])
{   
    struct mg_mgr mgr;
 
    mg_mgr_init(&mgr, nullptr);
 
    int port = 8190;
    char buf[5] = {0};
    snprintf(buf, sizeof(buf), "%d", port);
    struct mg_connection *con = mg_bind(&mgr, buf, eventHandler);
 
    if(con == NULL) {
        errorf("mg_bind fail\n");
        return -1;
    }
 
    mg_set_protocol_http_websocket(con);
    infof("listen ip[%s], port[%d]....\n", inet_ntoa(con->sa.sin.sin_addr), port); 

    //uri是/fileUpload 时调用函数fileUpload
    mg_register_http_endpoint(con, "/fileUpload", fileUpload);

    while (1)
    {
        mg_mgr_poll(&mgr, 100);
    }
    
    mg_mgr_free(&mgr);
    return 0;
}

这个回调何时调用呢?当请求地址为:http://10.91.90.99:8190/fileUpload?时调用,即 uri=/fileUpload。

fileUpload 函数:

void fileUpload(mg_connection* nc, const int ev, void* data)
{
    //用户指针,用于保存文件大小,文件名
    struct FileInfo *userData = nullptr;
 
    //当事件ev是 MG_EV_HTTP_MULTIPART_REQUEST 时,data类型是http_message
    struct http_message *httpMsg = nullptr;
    if(MG_EV_HTTP_MULTIPART_REQUEST == ev)
    {
        httpMsg = (struct http_message*)data;
        //初次请求时,申请内存
        if(userData == nullptr)
        {
            userData = (struct FileInfo *)malloc(sizeof(struct FileInfo));
            memset(userData, 0, sizeof(struct FileInfo));
        }
    }
    else // 已经不是第一次请求了,nc->user_data 先前已经指向 userData,所以可以用了
    {
        userData = (struct FileInfo *)nc->user_data;
    }
 
    //当事件ev是 MG_EV_HTTP_PART_BEGIN/MG_EV_HTTP_PART_DATA/MG_EV_HTTP_PART_END 时,data类型是mg_http_multipart_part
    struct mg_http_multipart_part *httpMulMsg = nullptr;
    if(ev >= MG_EV_HTTP_PART_BEGIN && ev <= MG_EV_HTTP_PART_END)
    {
        httpMulMsg = (struct mg_http_multipart_part*)data;
    }

    switch(ev) 
    {
        case MG_EV_HTTP_MULTIPART_REQUEST:
            {   
                ///query_string为请求地址中的变量
                char filePath[32] = {0};
                std::string key("filePath"); 
                //从请求地址里获取 key 对应的值,所以这个需要和请求地址里的 key 一样
                //这里从地址中获取文件要上传到哪个路径
                if(mg_get_http_var(&httpMsg->query_string, key.c_str(), filePath, sizeof(filePath)) > 0) 
                {
                    tracef("upload file request, %s = %s\n", key.c_str(), filePath); 
                }

                //保存路径,且 nc->user_data 指向该内存,下次请求就可以直接用了
                if(userData != nullptr)
                {
                    snprintf(userData->filePath, sizeof(userData->filePath), "%s", filePath);
                    nc->user_data = (void *)userData;                 
                }
            }
 
            break;
        case MG_EV_HTTP_PART_BEGIN:  ///这一步获取文件名
            tracef("upload file begin!\n");
            if(httpMulMsg->file_name != NULL && strlen(httpMulMsg->file_name) > 0)
            {
                tracef("input fileName = %s\n", httpMulMsg->file_name);
                //保存文件名,且新建一个文件
                if(userData != nullptr)
                {
                    snprintf(userData->fileName, sizeof(userData->fileName), "%s%s", userData->filePath, httpMulMsg->file_name);
                    userData->fp = fopen(userData->fileName, "wb+");

                    //创建文件失败,回复,释放内存
                    if(userData->fp == NULL) 
                    {
                        mg_printf(nc, "%s", 
                            "HTTP/1.1 500 file fail\r\n"
                            "Content-Length: 25\r\n"
                            "Connection: close\r\n\r\n"
                            "Failed to open a file\r\n");

                        nc->flags |= MG_F_SEND_AND_CLOSE;
                        free(userData);
                        nc->user_data = nullptr;     
                        return;
                    }                    
                }

            }
            break;
        case MG_EV_HTTP_PART_DATA:
            // tracef("upload file chunk size = %lu\n", httpMulMsg->data.len);
            if(userData != nullptr && userData->fp != NULL) 
            {
                size_t ret = fwrite(httpMulMsg->data.p, 1, httpMulMsg->data.len, userData->fp);
                if(ret != httpMulMsg->data.len)
                {
                    mg_printf(nc, "%s",
                    "HTTP/1.1 500 write fail\r\n"
                    "Content-Length: 29\r\n\r\n"
                    "Failed to write to a file\r\n");

                    nc->flags |= MG_F_SEND_AND_CLOSE;
                    return;
                }     
            }
            break;
        case MG_EV_HTTP_PART_END:
            tracef("file transfer end!\n");
            if(userData != NULL && userData->fp != NULL)
            {
                mg_printf(nc,
                    "HTTP/1.1 200 OK\r\n"
                    "Content-Type: text/plain\r\n"
                    "Connection: close\r\n\r\n"
                    "Written %ld of POST data to a file\n\n",
                (long)ftell(userData->fp));

                //设置标志,发送完成数据(如果有)并且关闭连接
                nc->flags |= MG_F_SEND_AND_CLOSE;
                
                //关闭文件,释放内存
                fclose(userData->fp);
                tracef("upload file end, free userData(%p)\n", userData);
                free(userData);
                nc->user_data = NULL;       
            }           
            break;
        case MG_EV_HTTP_MULTIPART_REQUEST_END:
            tracef("http multipart request end!\n");
            break;
        default:
            break;
    }
}

这几个事件类型,MG_EV_HTTP_PART_DATA 会调用多次,取决于上传的文件大小,及一次最大读取数据的大小(即:MG_TCP_IO_SIZE),其他的事件类型只调用一次。

当事件类型是?MG_EV_HTTP_MULTIPART_REQUEST 时,可以从请求地址中获取到指定参数,如我需要知道文件要上传到哪个目录下,则请求地址必须带上某个参数:

{   
	///query_string为请求地址中的变量
	char filePath[32] = {0};
	std::string key("filePath"); 
	//从请求地址里获取 key 对应的值,所以这个需要和请求地址里的 key 一样
	//这里从地址中获取文件要上传到哪个路径
	if(mg_get_http_var(&httpMsg->query_string, key.c_str(), filePath, sizeof(filePath)) > 0) 
	{
		tracef("upload file request, %s = %s\n", key.c_str(), filePath); 
	}

	//保存路径,且 nc->user_data 指向该内存,下次请求就可以直接用了
	if(userData != nullptr)
	{
		snprintf(userData->filePath, sizeof(userData->filePath), "%s", filePath);
		nc->user_data = (void *)userData;                 
	}
}

如上要取得 key("filePath") 对应的值,则请求地址必须这样:http://10.91.90.99:8190/fileUpload?filePath=./? ? ?然后调用 mg_get_http_var() 取得 key 对应的值 ,取到的就是:upload file request, filePath = ./ ,这个可以按业务需要进行决定。这里就用到之前文章中说的 user_data 指针,在数据传输前把信息收集后存到了 user_data 指向的内存里,后续就可以用了,而保存的这个信息定义如下:

struct FileInfo
{
    FILE *fp; //打开新文件的指针
    char fileName[128]; //文件名,包含路径
    char filePath[32]; //文件路径
    size_t size; //文件大小,暂时没有用到
};

MG_EV_HTTP_PART_BEGIN 事件时,可以获取到上传过来的文件名,MG_EV_HTTP_PART_DATA 事件时,可以获取到文件数据,所以这两个就是创建一个文件,然后往里面写数据,在?MG_EV_HTTP_PART_END 时,关闭文件,回复消息给客户端,然后断开连接。

?我用的 postman 进行文件上传测试,用浏览器不知道怎么搞,可能要自己写前端程序,这个已经难倒我了(实际项目中都是前端开发人员开发的,我们要做的就是和他们对接功能)。上传完成后回复的消息:

?用比较软件查看两个文件,他们是一样的,证明上传的文件是正常的。

????????最后,测试时发现一个问题: 当在 fopen() 或 fwrite() 文件出错时,回复消息给客户端,客户端是收不到的。也就是,假如我要在?MG_EV_HTTP_PART_BEGIN 事件时返回错误给客户端,告诉客户端不要发数据了,但实际效果是客户端还是会发数据,直到发送完成,才接收到服务端发出的回应。如下指定一个目录不存在时,创建文件失败:

?数据还是传输完毕了。而客户端收到的回复是这样的,也不是完全正确的:

疑问:客户端只有在完成一个 http 请求后才会接收回应吗?

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-09-04 01:45:01  更:2022-09-04 01:46:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 22:33:13-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码