• http 协议文件上传 - mongoose


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

    1. //#define MG_EV_HTTP_MULTIPART_REQUEST 121 /* struct http_message */
    2. //#define MG_EV_HTTP_PART_BEGIN 122 /* struct mg_http_multipart_part */
    3. //#define MG_EV_HTTP_PART_DATA 123 /* struct mg_http_multipart_part */
    4. //#define MG_EV_HTTP_PART_END 124 /* struct mg_http_multipart_part */
    5. /* struct mg_http_multipart_part */
    6. //#define MG_EV_HTTP_MULTIPART_REQUEST_END 125

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

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

    main 函数:

    1. int main(int argc, char *argv[])
    2. {
    3. struct mg_mgr mgr;
    4. mg_mgr_init(&mgr, nullptr);
    5. int port = 8190;
    6. char buf[5] = {0};
    7. snprintf(buf, sizeof(buf), "%d", port);
    8. struct mg_connection *con = mg_bind(&mgr, buf, eventHandler);
    9. if(con == NULL) {
    10. errorf("mg_bind fail\n");
    11. return -1;
    12. }
    13. mg_set_protocol_http_websocket(con);
    14. infof("listen ip[%s], port[%d]....\n", inet_ntoa(con->sa.sin.sin_addr), port);
    15. //uri是/fileUpload 时调用函数fileUpload
    16. mg_register_http_endpoint(con, "/fileUpload", fileUpload);
    17. while (1)
    18. {
    19. mg_mgr_poll(&mgr, 100);
    20. }
    21. mg_mgr_free(&mgr);
    22. return 0;
    23. }

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

    fileUpload 函数:

    1. void fileUpload(mg_connection* nc, const int ev, void* data)
    2. {
    3. //用户指针,用于保存文件大小,文件名
    4. struct FileInfo *userData = nullptr;
    5. //当事件ev是 MG_EV_HTTP_MULTIPART_REQUEST 时,data类型是http_message
    6. struct http_message *httpMsg = nullptr;
    7. if(MG_EV_HTTP_MULTIPART_REQUEST == ev)
    8. {
    9. httpMsg = (struct http_message*)data;
    10. //初次请求时,申请内存
    11. if(userData == nullptr)
    12. {
    13. userData = (struct FileInfo *)malloc(sizeof(struct FileInfo));
    14. memset(userData, 0, sizeof(struct FileInfo));
    15. }
    16. }
    17. else // 已经不是第一次请求了,nc->user_data 先前已经指向 userData,所以可以用了
    18. {
    19. userData = (struct FileInfo *)nc->user_data;
    20. }
    21. //当事件ev是 MG_EV_HTTP_PART_BEGIN/MG_EV_HTTP_PART_DATA/MG_EV_HTTP_PART_END 时,data类型是mg_http_multipart_part
    22. struct mg_http_multipart_part *httpMulMsg = nullptr;
    23. if(ev >= MG_EV_HTTP_PART_BEGIN && ev <= MG_EV_HTTP_PART_END)
    24. {
    25. httpMulMsg = (struct mg_http_multipart_part*)data;
    26. }
    27. switch(ev)
    28. {
    29. case MG_EV_HTTP_MULTIPART_REQUEST:
    30. {
    31. ///query_string为请求地址中的变量
    32. char filePath[32] = {0};
    33. std::string key("filePath");
    34. //从请求地址里获取 key 对应的值,所以这个需要和请求地址里的 key 一样
    35. //这里从地址中获取文件要上传到哪个路径
    36. if(mg_get_http_var(&httpMsg->query_string, key.c_str(), filePath, sizeof(filePath)) > 0)
    37. {
    38. tracef("upload file request, %s = %s\n", key.c_str(), filePath);
    39. }
    40. //保存路径,且 nc->user_data 指向该内存,下次请求就可以直接用了
    41. if(userData != nullptr)
    42. {
    43. snprintf(userData->filePath, sizeof(userData->filePath), "%s", filePath);
    44. nc->user_data = (void *)userData;
    45. }
    46. }
    47. break;
    48. case MG_EV_HTTP_PART_BEGIN: ///这一步获取文件名
    49. tracef("upload file begin!\n");
    50. if(httpMulMsg->file_name != NULL && strlen(httpMulMsg->file_name) > 0)
    51. {
    52. tracef("input fileName = %s\n", httpMulMsg->file_name);
    53. //保存文件名,且新建一个文件
    54. if(userData != nullptr)
    55. {
    56. snprintf(userData->fileName, sizeof(userData->fileName), "%s%s", userData->filePath, httpMulMsg->file_name);
    57. userData->fp = fopen(userData->fileName, "wb+");
    58. //创建文件失败,回复,释放内存
    59. if(userData->fp == NULL)
    60. {
    61. mg_printf(nc, "%s",
    62. "HTTP/1.1 500 file fail\r\n"
    63. "Content-Length: 25\r\n"
    64. "Connection: close\r\n\r\n"
    65. "Failed to open a file\r\n");
    66. nc->flags |= MG_F_SEND_AND_CLOSE;
    67. free(userData);
    68. nc->user_data = nullptr;
    69. return;
    70. }
    71. }
    72. }
    73. break;
    74. case MG_EV_HTTP_PART_DATA:
    75. // tracef("upload file chunk size = %lu\n", httpMulMsg->data.len);
    76. if(userData != nullptr && userData->fp != NULL)
    77. {
    78. size_t ret = fwrite(httpMulMsg->data.p, 1, httpMulMsg->data.len, userData->fp);
    79. if(ret != httpMulMsg->data.len)
    80. {
    81. mg_printf(nc, "%s",
    82. "HTTP/1.1 500 write fail\r\n"
    83. "Content-Length: 29\r\n\r\n"
    84. "Failed to write to a file\r\n");
    85. nc->flags |= MG_F_SEND_AND_CLOSE;
    86. return;
    87. }
    88. }
    89. break;
    90. case MG_EV_HTTP_PART_END:
    91. tracef("file transfer end!\n");
    92. if(userData != NULL && userData->fp != NULL)
    93. {
    94. mg_printf(nc,
    95. "HTTP/1.1 200 OK\r\n"
    96. "Content-Type: text/plain\r\n"
    97. "Connection: close\r\n\r\n"
    98. "Written %ld of POST data to a file\n\n",
    99. (long)ftell(userData->fp));
    100. //设置标志,发送完成数据(如果有)并且关闭连接
    101. nc->flags |= MG_F_SEND_AND_CLOSE;
    102. //关闭文件,释放内存
    103. fclose(userData->fp);
    104. tracef("upload file end, free userData(%p)\n", userData);
    105. free(userData);
    106. nc->user_data = NULL;
    107. }
    108. break;
    109. case MG_EV_HTTP_MULTIPART_REQUEST_END:
    110. tracef("http multipart request end!\n");
    111. break;
    112. default:
    113. break;
    114. }
    115. }

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

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

    1. {
    2. ///query_string为请求地址中的变量
    3. char filePath[32] = {0};
    4. std::string key("filePath");
    5. //从请求地址里获取 key 对应的值,所以这个需要和请求地址里的 key 一样
    6. //这里从地址中获取文件要上传到哪个路径
    7. if(mg_get_http_var(&httpMsg->query_string, key.c_str(), filePath, sizeof(filePath)) > 0)
    8. {
    9. tracef("upload file request, %s = %s\n", key.c_str(), filePath);
    10. }
    11. //保存路径,且 nc->user_data 指向该内存,下次请求就可以直接用了
    12. if(userData != nullptr)
    13. {
    14. snprintf(userData->filePath, sizeof(userData->filePath), "%s", filePath);
    15. nc->user_data = (void *)userData;
    16. }
    17. }

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

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

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

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

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

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

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

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

  • 相关阅读:
    vue 实现高德坐标转GPS坐标
    Nginx配置限流
    Java 反射的应用 - 对象转Map
    东南电子开启申购:客户集中度较高,预计上市时市值约18亿元
    Contrastive Search Decoding——一种对比搜索解码文本生成算法
    数据分析 -- numpy
    C++ 三大特性之多态(二) 多态的实现原理
    uniapp中使用axios打包到小程序时报 TypeError: adapter is not a function
    LeetCode 320 周赛
    git你学“废”了吗?——使用git进行版本回退操作
  • 原文地址:https://blog.csdn.net/tianyexing2008/article/details/126642550