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