C#实现HTTP服务器:处理文件上传---解析MultipartFormDataContent

完整项目托管地址:https://github.com/sometiny/http

HTTP还有重要的一块:文件上传。
这篇文章将详细讲解下,前面实现了同一个链接处理多个请求,为了方便,我们独立写了一个HTTP基类,专门处理HTTP请求。
https://github.com/sometiny/http/blob/main/src/Http/HttpServerBase.cs
本类实现了简单的路由功能,路由功能后续可以使用正则或path2regexp去处理,以处理更复杂的路由请求。
增加了对静态文件的处理,没匹配到的路由都会进入OnResource逻辑。
增加了WebRoot和UploadTempDir的设置,WebRoot目录下的静态文件在HTTP请求时都会自动加载,不需要单独写路由。
UploadTempDir用来临时保存上传的文件。

1、上传简介
上传文件时,使用Content-Type: multipart/form-data; boundary=[BOUNDARY]标头来告诉服务器,请求实体为multipart/form-data编码。
服务器根据编码协议解析multipart/form-data的内容即可,其中[BOUNDARY]为一个请求实体“块”的结束或开始标识,用于解析实体内容。
在浏览器中,为form标签增加enctype="multipart/form-data"属性时,浏览器会自动生成对应的上传标头。
例如下面的标头,为Chrome浏览器生成的:

Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuHXBXxnxXp0aCz08

2、上传时到底传送了什么格式的数据
话不多说,直接上代码,直观点。
先从这里https://github.com/sometiny/http/tree/main/bin/Release把web目录及其内容放到你自己的Debug或Release编译结果目录下。

实现一个测试服务器
注意,继承的是HttpServerBase基类,在OnReceivedPost方法显示下浏览器发送的内容。

public class HttpServer : HttpServerBase
{public HttpServer() : base(){//设置Web根目录//方便输出静态文件WebRoot = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "web"));UplaodTempDir = AppDomain.CurrentDomain.BaseDirectory + "uploads";//注册一些路由RegisterRoute("/", OnIndex);RegisterRoute("/post", OnReceivedPost);}/// <summary>/// 首页路由处理程序,跳转到index.html/// </summary>/// <param name="request"></param>/// <param name="stream"></param>private bool OnIndex(HttpRequest request, Stream stream){//跳转到页面HttpResponser responser = new ChunkedResponser(301);responser.ContentType = "text/html; charset=utf-8";responser["Location"] = "/index.html";responser.Write(stream, "Redirect To '/index.html'");responser.End(stream);return true;}/// <summary>/// 处理POST数据/// </summary>/// <param name="request"></param>/// <param name="stream"></param>/// <returns></returns>private bool OnReceivedPost(HttpRequest request, Stream stream){HttpResponser responser = new ChunkedResponser();responser.ContentType = "text/html; charset=utf-8";responser.Write(stream, "<style type=\"text/css\">body{font-size:12px;}</style>");responser.Write(stream, "<h4>上传表单演示</h4>");responser.Write(stream, $"<a href=\"/index.html\">返回</a><br />");responser.Write(stream, $"ContentType:{request.ContentType}<br />");responser.Write(stream, $"Boundary:{request.Boundary}<br />");///这里输出下浏览器发送来的请求responser.Write(stream, $"<pre>{Encoding.UTF8.GetString( request.RequestBody)}</pre>");responser.End(stream);return true;}
}

运行服务器,浏览器访问:http://127.0.0.1:4189/index.html,展示如下一个表单。

 我们什么都不上传,直接点提交,看看服务器收到些什么内容。

 

绿色部分就是我们收到的请求实体内容。

编码分析
实例中的[BOUNDARY]为:----WebKitFormBoundarydyAItGK9UU5xVhZq
3、首行:--[BOUNDARY]\r\n 在boundary前面补两个中横线(-)。
首行读取完毕后,即开始表单项的读取。

4、非文件表单项:
每行一个标头,直到空行,空行后表单内容开始。这里跟Http请求

Content-Disposition: form-data; name="name"测试hello world!
------WebKitFormBoundarydyAItGK9UU5xVhZq

说明:

Content-Disposition: form-data; name="[表单项名称]"\r\n\r\n[内容]\r\n--[BOUNDARY]

5、文件表单项
标头的读取和结束标志跟上面的一致。
只是Content-Disposition会多一个filename属性,因为我们没选择文件,filename值为空。
同时,会提供一个Content-Type标头来标识文件类型。
不排除序应用程序会提供更多的标头,我们只要读取到空行,关心我们需要的标头即可。

Content-Disposition: form-data; name="image"; filename=""
Content-Type: application/octet-stream------WebKitFormBoundarydyAItGK9UU5xVhZq

说明:

Content-Disposition: form-data; name="[表单项名称]"; filename=""\r\nContent-Type: application/octet-stream\r\n\r\n[文件内容]\r\n--[BOUNDARY]

6、如何确定结束标识之后是下一个表单项,还是请求实体的结尾。
读取到表单内容结束标识后,再往前读取两个字节。
如果两个字节为\r\n,代表后面还有其他的表单项。
如果两个字节为--,代表所有表单项已读取完毕,请求实体也读完了。

7、实现上传请求实体的解析。
解析使用了两个类。
将请求实体解析为Form和Files:https://github.com/sometiny/http/blob/main/src/Http/Utils/HttpMultipartFormDataParser.cs
Multipart数据读取的辅助流:https://github.com/sometiny/http/blob/main/src/Http/Streams/MultipartReadStream.cs
辅助流里面实现了核心的数据解析,内部用到了BoyerMoore字符串查找算法。
我们主要是讲解协议原理,协议解析这部分可以不用关心,我写的数据解析也可能不是很严格(我不会告诉你,我写完后就看不懂了)。

修改我们上面实现的服务器中OnReceivePost方法,我们这次把上传的表单和文件列出来。

private bool OnReceivedPost(HttpRequest request, Stream stream)
{HttpResponser responser = new ChunkedResponser();responser.ContentType = "text/html; charset=utf-8";responser.Write(stream, "<style type=\"text/css\">body{font-size:12px;}</style>");responser.Write(stream, "<h4>上传表单演示</h4>");responser.Write(stream, $"<a href=\"/index.html\">返回</a><br />");responser.Write(stream, $"ContentType:{request.ContentType}<br />");responser.Write(stream, $"Boundary:{request.Boundary}<br />");#region 输出解析后的上传内容responser.Write(stream, $"<h5>上传表单数据:</h5>");foreach (string formName in request.Form.Keys){responser.Write(stream, $"{formName}: {request.Form[formName]}<br />");}responser.Write(stream, $"<h5>上传文件列表:</h5>");foreach (FileItem file in request.Files){responser.Write(stream, $"{file.Name}: {file.FileName}, {file.TempFile}<br />");}#endregion#region 输出解析前的上传内容,不能同时与上面代码块运行//responser.Write(stream, $"<pre style=\"font-family:'microsoft yahei',arial; color: green\">{Encoding.UTF8.GetString( request.RequestBody)}</pre>");#endregionresponser.End(stream);return true;
}

运行服务器,浏览器访问:http://127.0.0.1:4189/index.html,现在我们选择几个文件,为了方便演示,建议选择有少量文本的文本文件。
头像我选择了两个文件,微信选择了一个。

 提交表单。下面可以看到,服务器正确处理了表单数据和三个文件数据。
FileItem对象保存了表单名,文件名,文件类型和文件的临时保存路径,可以将文件移动到应用实际的目录。

 移除对OnReceivedPost中对输出解析前的上传内容代码块的注释,并且把输出解析后的上传内容代码块注释掉,刷新页面,可以查看原始未解析的数据。可以对照我们前面对上传数据的编码分析查看下。

 

8、总结
     1、文件上传主要是增加了Content-Type的设置,使服务器能正确处理上传的内容。
     2、请求实体的解析部分,因为不像HttpRequest一样有Content-Length来标识具体的长度,只能用boundary去分析什么时候开始解析,什么时候结束解析。
     3、对于上传的请求,请求实体解析后,ResponseBody就取不到内容了,所以要想看到请求的具体内容,不能调用Form或Files方法,因为这两个方法一旦调用,上传请求就会被自动解析了。

如果使用第三方去解析HTTP multipart/form-data content的话---HttpMultipartParser,无论.net framwork 版本还是net core版本都可以使用,参考地址:  https://github.com/Http-Multipart-Data-Parser/Http-Multipart-Data-Parser,reader/parser of the HTTP multipart/form-data content sent from Windows Runtime via MultipartFormDataContent class. nuget 包管理器直接搜索安装即可使用。

MultipartReader---http://dul.codeplex.com/ ,ASP.NET reader/parser of the HTTP multipart/form-data content sent from Windows Runtime via MultipartFormDataContent class. nuget 包管理器直接搜索安装即可使用。

 

原文链接:https://blog.csdn.net/moasp/article/details/120197381

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/907207.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

达梦数据库使用日寄1

最近一个朋友找过来说可能有个项目可以合作,项目背景是信创重构,于是摸鱼半辈子的老汉开始翻个身选择了解达梦数据库了。虽然项目还没下来,现在只是确定了整个项目的大概架构:达梦数据库+.net core6.0+vue3(内网)+微信小程序(外网)+nginx(反向代理外网访问内网)/双服…

算法常用库函数

1.reverse翻转2.unique去重3.random_shuffle随机打乱 用法与reverse相同 4.sort5.lower_bound/upper_bound二分

阿里邮箱通讯录插件(outlook)安装后不能正常使用

阿里邮箱通讯录插件安装后打开outlook,并未找到Alimail选项。下图为正常显示 1、在文件选项卡中找到“管理COM加载项”,查看是否插件被禁用加载 2、将禁用插件更改为启用。 3、此时Alimail选项已经显示,但是通讯录为灰色,点击设置登录邮箱账号即可。

Cell | 亚洲免疫细胞多样性图谱发布!揭示基因与疾病关联新视角

关键词 亚洲人群、免疫细胞多样性、单细胞测序、遗传变异、精准医学 摘要总结: 这篇文章是2025年3月发表在《Cell》杂志上的一篇研究,标题为“Asian diversity in human immune cells”。这篇文章通过构建覆盖5个亚洲国家的619名健康人群的单细胞免疫图谱(AIDA),探索了人群…

【VsCode】使用Cline+deepseek实现VsCode自动化编程

不知道大家有没有听说过cursor这个工具,类似于AI+VsCode的结合体,只要绑定chatgpt、claude等大模型API,就可以实现对话式自助编程,简单闲聊几句便可开发一个软件应用。 但cursor受限于外网,国内用户玩不了,而且还收费很贵,非常的不接地气。 于是乎就有了平替,VsCode上的…

hbase使用外置zookeeper出现问题--Starting zookeeper ... FAILED TO START

操作系统:CentOS-Stream-9-20250224.1-x86_64-dvd1.iso 问题展示:解决办法: 删除zoo.cfg文件中data目录下除myid以外的所有内容 效果图:疑似原因: 上一次使用kill -9强制关闭了HMaster和HRegionServer

可视化图解算法:递归基础

写递归代码的关键就是找到如何将大问题分解为小问题的规律,并且基于此写出递推公式,然后再推敲终止条件,最后将递推公式和终止条件翻译成代码。1. 示例 周末你带着TA去电影院看电影,TA问你,咱们现在坐在第几排啊?电影院里面太黑了,看不清,没法数,现在你怎么办?这时可…

接口测试——jmeter操作数据库

一、jmeter操作mysql 1、下载数据驱动,安装数据驱动(将数据库的驱动存放好)(1)存放路径a.jre下的lib:C:\Program Files\Java\jre1.8.0_60\libb.存放在jre 下的lib中的ext路径:C:\Program Files\Java\jre1.8.0_60\lib\extc.存放在jmeter下的lib 路径:E:\dcs\two\jmeter\…

使用 VS Code + RooCodeCLine + MCP 实现 ABAP 程序的自动化调整

简介: 本文介绍如何利用 VS Code 结合 RooCode (或 CLine) 以及 MCP (Message Control Protocol) 服务,实现 ABAP 程序的自动化调整,从而提高开发效率。 前置条件:VS Code 环境: 确保已安装 VS Code,并安装了 RooCode 或 CLine 插件。 RooCode/CLine 使用经验: 熟悉 Roo…

阿里云重磅开源 Qwen2.5-Omni-7B:轻量化全模态大模型赋能手机端 AI 应用

3月27日,阿里云宣布了一项重大技术突破,正式向公众开源了其通义系列中的首款全模态大模型——Qwen2.5-Omni-7B!在当今竞争激烈的科技领域,每一次重大的技术发布都像是在平静湖面投下的巨石,激起层层涟漪。而此次Qwen2.5-Omni的发布,无疑是一颗重磅炸弹,瞬间在科技圈引发…

OKR 必须应用于绩效:协同时代的管理闭环构建

如何将 OKR 与绩效管理有效结合?《礼记⋅中庸》有言:“凡事预则立,不预则废,”意思是说,做任何事情,要想成功,都需要提前进行周密的筹划和精心的准备。其中,设定科学合理的目标至关重要。如何设定科学合理的目标?让我们一起听听管理的常识内容合伙人邱昭良博士怎么说。…