http 协议文件上传 - mongoose

本文介绍了使用mongoose构建的HTTP服务如何支持文件上传。通过解析MG_EV_HTTP_MULTIPART_REQUEST等事件,展示了从文件上传开始到结束的整个处理流程,包括文件路径获取、文件名解析、数据写入和文件关闭。在回调函数fileUpload中,针对不同的事件进行相应的操作。在测试过程中,遇到客户端在文件上传出错时仍继续发送数据的问题,引发对HTTP响应及时性的讨论。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

### 使用 MongooseHTTP 实现文件上传 为了实现在 Node.js 中通过 HTTP 协议并利用 Mongoose 库进行文件上传的功能,通常会结合 Express 框架以及 `multer` 中间件来处理文件上传请求。以下是具体实现方式: #### 1. 创建项目结构与初始化依赖项 首先,在项目的根目录下安装必要的包: ```bash npm init -y npm install express multer mongoose ``` 这一步骤确保了开发环境中有足够的工具支持后续功能的构建[^3]。 #### 2. 设置服务器端逻辑 接下来设置基本的服务端代码框架,包括连接 MongoDB 数据库、配置 Multer 处理文件上传等功能: ```javascript const express = require('express'); const multer = require('multer'); const mongoose = require('mongoose'); // 初始化应用实例 const app = express(); // 配置Multer用于解析multipart/form-data表单数据 const upload = multer({ dest: 'uploads/' }); // 连接至MongoDB数据库 mongoose.connect('mongodb://localhost:27017/fileUploadDemo', { useNewUrlParser: true, useUnifiedTopology: true }); // 定义文件元数据模式 const fileMetadataSchema = new mongoose.Schema({ originalname: String, path: String, size: Number }); const FileMeta = mongoose.model('FileMeta', fileMetadataSchema); app.post('/upload', upload.single('file'), async (req, res) => { try { // 将文件信息存入数据库 await FileMeta.create({ originalname: req.file.originalname, path: req.file.path, size: req.file.size }); res.send(`File uploaded successfully`); } catch (error) { console.error(error); res.status(500).send('Server Error'); } }); // 启动服务监听指定端口 const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server running on port ${PORT}`)); ``` 上述代码片段展示了如何使用 Multer 来接收客户端发送过来的文件,并将其保存到本地磁盘的同时记录相关元数据到 MongoDB 中[^4]。 #### 3. 测试接口 可以通过 Postman 或其他 API 调试工具向 `/upload` 接口发起 POST 请求来进行测试,记得选择 form-data 形式的 body 并添加 key 名为 "file" 的字段用来携带要上传的文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值