用 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 请求后才会接收回应吗?