上一篇文章讲到 http 的 MIME 类型 http MIME 类型 里有一个 multipart 多部分对象集合类型,这个类型 http 指南里有讲到:MIME 中的 multipart(多部分)电子邮件报文中包含多个报文,它们合在一起作为单一的复杂报文发送。每一部分都是独立的,有各自的描述及内容的集;不同的部分之间用分界字符串连接在一起。HTTP 也支持多部分主体,不过,通常只用在下列两种情形之一:提交填写好的表格,或是作为承载若干文档片段的范围响应。
前端技术不懂,这里只用 postman 用为客户端来做示例。表格和文档形式,是不是像这样的呢?
当你选中其中一种情形时,http 的 Headers 里就会多出一个 Content-Type 表明这是一个多部分集合的类型报文:
表格情形还没试验过,这里主要讲文档情形的。所以呢,用客户端 postman 可以向服务端发送大的或是小的文档。下面给出服务端的例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string>
#include <map>
#include <mutex>
#include <sstream>
#include <iostream>
#include "mongoose.h"
#include "../logFormatPrt/log.h"
#define FILE_NAME_LEN 128
#define FILE_PATH_LEN 32
void eventHandler(struct mg_connection *nc, int event, void *eventData);
void fileUpload(mg_connection* nc, const int ev, void* data);
bool validPath(const char *path);
struct userData
{
int index;
};
struct FileInfo
{
FILE *fp; //打开新文件的指针
char fileName[FILE_NAME_LEN]; //文件名,包含路径
char filePath[FILE_PATH_LEN]; //文件路径
size_t size; //文件大小,暂时没有用到
size_t byteWrite;//已经写的字节数
};
//用postman 测试,linux需要关闭防火墙,否则收不到数据
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, nullptr);
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;
}
//触发的事件依次为:
//#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
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 为请求地址中的变量, key 名称约定好
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, locate: %s = %s\n", key.c_str(), filePath);
}
if(!validPath(filePath))
{
tracef("no such directory of %s\n", filePath);
std::string header;
std::string body("no suce directory");
header.append("HTTP/1.1 500 file fail").append("\r\n");
header.append("Connection: close").append("\r\n");
header.append("Content-Length: ").append(std::to_string(body.length())).append("\r\n").append("\r\n");
header.append(body).append("\r\n");
mg_send(nc, header.c_str(), header.length());
nc->flags |= MG_F_SEND_AND_CLOSE;
}
//保存路径,且 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)
{
if(userData->filePath[strlen(userData->filePath)] == '/')
{
snprintf(userData->fileName, sizeof(userData->fileName), "%s%s", userData->filePath, httpMulMsg->file_name);
}
else
{
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;
}
userData->byteWrite += ret;
}
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 %lu bytes of POST data to a file\n\n",
userData->byteWrite);
//设置标志,发送完成数据(如果有)并且关闭连接
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;
}
else
{
mg_printf(nc,
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain\r\n"
"Connection: close\r\n\r\n"
"Written 0 of POST data to a file\n\n");
}
break;
case MG_EV_HTTP_MULTIPART_REQUEST_END:
tracef("http multipart request end!\n");
break;
default:
break;
}
}
bool validPath(const char *path)
{
struct stat st;
if(lstat(path, &st) == 0)
{
return true;
}
return false;
}
如果想要直接编译则需要把头文件 #include "../logFormatPrt/log.h" 去掉,编译选项还得加上 -lssl -lcrypto,Makefile 如下:
#中间文件存放目录,如.o 和 .d 文件
COMPILE_DIR = compile
BIN_DIR = bin
# 可编译arm版本
#CROSS = arm-himix200-linux-
CC = gcc -m32
CPP = $(CROSS)g++ -std=c++11 -m32
CFLAGS = -Werror -g
LIB = -lpthread -lssl -lcrypto
#CPP_SRCS = $(wildcard *.cpp)
CPP_SRCS = $(shell ls -t | grep "\.cpp$$" | head -1)
CPP_OBJS = $(patsubst %.cpp, $(COMPILE_DIR)/%.o, $(CPP_SRCS))
CPP_DEP = $(patsubst %.cpp, $(COMPILE_DIR)/%.cpp.d, $(CPP_SRCS))
C_SRCS = mongoose.c
C_OBJS = $(patsubst %.c, $(COMPILE_DIR)/%.o, $(C_SRCS))
C_DEP = $(patsubst %.c, $(COMPILE_DIR)/%.c.d, $(C_SRCS))
OBJS = $(CPP_OBJS) $(C_OBJS)
DEP_ALL = $(CPP_DEP) $(C_DEP)
$(shell if [ ! -d $(COMPILE_DIR) ]; then mkdir $(COMPILE_DIR); fi)
$(shell if [ ! -d $(BIN_DIR) ]; then mkdir $(BIN_DIR); fi)
BIN =
ifeq ($(target), ) #如果是空的
BIN = a.out
else
BIN := $(target)
endif
TARGET=$(BIN_DIR)/$(BIN)
all: $(TARGET)
-include $(DEP_ALL)
$(TARGET): $(OBJS)
$(CPP) $(CFLAGS) $^ -o $@ $(LIB)
$(COMPILE_DIR)/%.o: %.cpp $(COMPILE_DIR)/%.cpp.d
$(CPP) $(CFLAGS) -c $< -o $@
$(COMPILE_DIR)/%.cpp.d: %.cpp
$(CPP) $(CFLAGS) -MM -E -c $< -o $@
@sed 's/.*\.o/$(subst /,\/,$(dir $@))&/g' $@ > $@.tmp
@mv $@.tmp $@
$(COMPILE_DIR)/%.o: %.c $(COMPILE_DIR)/%.c.d
$(CC) $(CFLAGS) -c $< -o $@
$(COMPILE_DIR)/%.c.d: %.c
$(CC) $(CFLAGS) -MM -E -c $< -o $@
@sed 's/.*\.o/$(subst /,\/,$(dir $@))&/g' $@ > $@.tmp
@mv $@.tmp $@
.PHONY: clean
clean:
rm -rf $(COMPILE_DIR) $(BIN_DIR)
大到一个G的文件,上传也是没有问题的,1.3G 的文件:
如果问我客户端怎么写,这个我是不会的,但 postman 里有个“代码片段”选项,可以翻译成不同的编码语言的代码:
而在 Mongoose.c 源码里,是这样处理 multipart 类型的报文的,判断头部字段 Content-Type 为 multipart 时,交由相关处理函数进行处理,然后就 return 了,不再后续处理了。