我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。
这些代码大部分以Linux为目标但部分代码是纯C++的,可以在任何平台上使用。
本系列的源码位于httpd目录下。系列入口:
编程实战:自己编写HTTP服务器(系列1:概述和应答)-CSDN博客
通过HTTP上传文件需要服务端支持,html虽然支持类型为“file”的input,能选择文件,但是服务器还必须正确支持才行,毕竟如果写一个html就能往服务器上放文件可就太乱了。
目录
一、html和http的file支持
一个上传文件的form很简单:
<form id="upload-form" action="/Upload.asp" method="post" enctype="multipart/form-data" >
<input type="file" id="upload" name="upload" />
<input type="submit" value="Upload" />
</form>
确实很简单,最重要的秘密是必须编码为“multipart/form-data”,当然也必须用POST。
“multipart/form-data”的格式也很简单:
multipart/form-data; boundary=ASDFGHJKL
--ASDFGHJKL
Content-Disposition: form-data; name="upload"; filename="文件名"
Content-Type: text/plain
文件内容
--ASDFGHJKL--
注意一下格式细节,其中片段分隔符boundary是“ASDFGHJKL”,实际会更长一些,并且可能以几个“-”打头,容易和后面引导的“--”混淆。
片段以“--”加分隔符开始,是一行,按照http规则,以“\r\n”为行结束。
然后跟两个头标,格式和http头标完全相同。
再后面是一个空行表示头标部分结束。
投标部分的后面就是数据,没有数据长度,要根据结束标志判断,结束标志是“--”加分隔符再加“--”。这个例子中我上传的是txt文件,所以所以内容类型为text/plain,如果上传的是rar文件,则类型为字节流。
文件内容和结束标志之间一定有“\r\n”分隔,我上传的文本文件最后没有换行,但结束标志前增加了“\r\n”,如果文件最后有换行,会怎么样我没有验证(我猜还是追加换行然后是结束标志)。
二、服务端代码
2.1 服务端入口
我将上传功能做成了内置页面,页面地址为“/Upload.asp”,主代码中添了这么一段:
else if ("/Upload.asp" == m_request.GetResource())
{
if (doPageUpload(&connectdata))
{
m_respond.Flush(m_s);
}
else
{
m_respond.Flush(m_s);
m_s.Close();//所有此类页面都可能无法预先确定输出长度
isKeepAlive = false;
}
}
具体位置在myhttpserver.h的SocketProcess函数中。
2.2 实际功能
实际功能位于doPageUpload函数:
bool doPageUpload(CConnectData* pCD)
{
CHttpRequest& m_request = pCD->m_request;//请求
CHttpRespond& m_respond = pCD->m_respond;//应答
CMySocket& m_s = pCD->m_s;//连接
m_respond.Init();
m_respond.AddHeaderNoCache();
m_respond.AddHeaderContentTypeByFilename("*.html");
m_respond.AppendBodyHtmlStart("Uplaod");
m_respond.AppendBody(m_request.RequestHtmlReport());
m_respond.AppendBody("<HR/>");
m_respond.AppendBody(m_request.GetFullRequest());
m_respond.AppendBody("<HR/>");
m_respond.AppendBody(m_request.GetContent().data());
m_respond.AppendBody("<HR/>");
string content_type = m_request.GetContentType();
string a = "multipart/form-data; boundary=";
string boundary;
size_t pos = content_type.find(a);
if (0 != pos)
{
m_respond.AppendBody("未识别的内容类型,仅支持 multipart/form-data<P>");
}
else
{
boundary = content_type.substr(a.size());
string boundary_begin = "--" + boundary + "\r\n";
string boundary_end = "--" + boundary + "--\r\n";
size_t part_pos;
size_t pos_next_find = 0;
while (CBuffer::npos != (part_pos = m_request.GetContent().find(boundary_begin, pos_next_find)))
{
size_t part_end = m_request.GetContent().find(boundary_end, part_pos + boundary_begin.size());
if (CBuffer::npos == part_end)
{
m_respond.AppendBody("数据内容不完整<P>");
break;
}
size_t part_header_end = m_request.GetContent().find("\r\n\r\n");
string part_header = m_request.GetContent().substr(part_pos + boundary_begin.size(), part_header_end - part_pos - boundary_begin.size());
size_t pos_file_name_begin;
string filename_head="filename=\"";
pos_file_name_begin = part_header.find(filename_head);
if (string::npos == pos_file_name_begin)
{
m_respond.AppendBody("没有文件名<P>");
}
else
{
size_t pos_file_name_end = part_header.find("\"", pos_file_name_begin + filename_head.size());
if (string::npos == pos_file_name_end)
{
m_respond.AppendBody("文件名格式问题<P>");
}
else
{
//thelog << pos_file_name_begin << " " << pos_file_name_end << " " << pos_file_name_end - pos_file_name_begin - filename_head.size() << endi;
string filename = part_header.substr(pos_file_name_begin + filename_head.size(), pos_file_name_end - pos_file_name_begin - filename_head.size());
if (filename.size() == 0)
{
m_respond.AppendBody("没有文件名<P>");
}
else
{
m_respond.AppendBody(filename);
CEasyFile file;
size_t pos_filedata = part_header_end + 4;
long filesize = part_end - pos_filedata - 2;//前面还有\r\n
if (!file.WriteFile(filename.c_str(), m_request.GetContent().data() + pos_filedata, filesize))
{
m_respond.AppendBody("写入失败<P>");
}
else
{
char buf[256];
sprintf(buf, "写入成功 字节数%ld<P>", filesize);
m_respond.AppendBody(buf);
char path[1024];
m_respond.AppendBody(getcwd(path, 1024));
}
}
}
}
pos_next_find = part_end + boundary_end.size();
m_respond.AppendBody("<HR />");
}
}
string form = "<form id=\"upload-form\" action=\"/Upload.asp\" method=\"post\" enctype=\"multipart/form-data\" >\
<input type=\"file\" id=\"upload\" name=\"upload\" /> <br />\
<input type=\"submit\" value=\"Upload\" />\
</form>";
m_respond.AppendBody(form);
m_respond.AppendBodyHtmlEnd();
m_respond.Flush(m_s);
return true;
}
功能相当的简单。前面是输出了一下请求信息,方便调试。中间提取文件名和内容,然后直接把内容写到文件中,相当的粗暴。最后是输出了form,提交到自身。
三、更多讨论
我这里是只是验证功能,正常情形这种功能肯定是和特定需求相关的,除了保存文件肯定还有很多相关处理,以这个为基础,做成特定页面当然是没有问题的。
我这里是接收完整个请求才处理的,因为以前没考虑这个功能。如果文件很大,恐怕这个方式不行,需要专门优化。
(这里是结束)