从零开始实现multipart/form-data数据提交
在HTTP服务应用中进行数据提交一般都使用application/json
,application/x-www-form-urlencoded
和multipart/form-data
这几种内容格式。这几种格式的处理复杂度处理起来和前面定义的先后顺序一样由易到难。不过现有工具都提供了完善的功能在提交这些数据的时候都比较方便了;不过要自己手动基础协议写起,那multipart/form-data
的处理规范还是要相对复杂些。最近在写webapi管理和性能测试工具(https://github.com/IKende/WebBenchmark)时为了得到更可控的时间线和性能,在实现并没有用到任何应用组件都是从HTTP基础协议写起,在这时介绍一下如何在基础HTTP协议的基础上提交multipart/form-data数据.(如果你没有什么特别的需求还是不要这么干)
multipart/form-data
这种格式一般配合多数据类型提交使用,如常用的数据表单和文件结合。这种格式有着自己的处理规范和application/json
和application/x-www-form-urlencoded
有着不同。application/json
相对来说最简单整个数据流是json内容格式,而application/x-www-form-urlencoded
则是以k-v的方式处理,只是对应的值要做Url编写。而multipart/form-data
则用一个特别的分隔符来处理,这个分隔符分为开始分隔和结束分隔符。
分隔符定义
如果使用multipart/form-data
提交数据,那必须在Content-Type
的请求头后面添加; boundary=value
这样一个描述,boundary
的值即是每项数据之间的分隔符
mHeaderCached.Append("Content-Type: ").Append(mCases.ContentType); if (multipartFormData) mHeaderCached.Append("; boundary=").Append(boundary); mHeaderCached.Append("\r\n");
需要怎样定义boundary
值?其实boundary
的定义是没有特别的要求的,就是一个字符串完全看自己的喜好。但最终处理的时候是要有一个规范。
-
开始分隔符
--boundary
-
结束分隔符
--boundary--
开始分隔符必须在每项数据之前写入,简单来说就是有多少项数据就有多少个开始分隔符了;结束分隔符只有一个,就是在提交内容的尾部添加,说明这个提交的内容在这里结束不需要再往下解释。大概格式如下:
-- boundary
数据项
-- boundary
数据项
-- boundary
数据项
-- boundary
数据项
--boundary--
数据项
multipart/form-data
中的每项数据都分别有Header
和Body
和整个HTTP上层协议差不多。
Content-Disposition: form-data; name="fname"\r\n
\r\n
value
\r\n
Content-Disposition
是必须的,描述一下这数据的格式来源,在这里都是form-data
;后面根据不同数据的情况有着不同的属性,每个属性用;
分隔的K-V结构。代码的处理比较简单:
mMemoryData.WriteLine($"Content-Disposition: form-data; name=\"{item.Name}\"");
接下来就是一个空换行然后再写入值,完整代码如下:
mMemoryData.WriteLine($"Content-Disposition: form-data; name=\"{item.Name}\""); mMemoryData.WriteLine(""); mTextBodyCached.Clear(); item.GetTemplate().Execute(mTextBodyCached); mMemoryData.Write(mTextBodyCached); mMemoryData.WriteLine("");
提交文件
提交文件相对来说比值要处理多一些属性,主要包括内容类型,文件名等;其实写起来也不复杂
mMemoryData.WriteLine($"Content-Disposition: form-data; name=\"{item.Name}\"; filename=\"{item.FileName}\""); mMemoryData.WriteLine($"Content-Type: {item.Type}"); mMemoryData.WriteLine(""); var itemBuffer = item.GetBuffer(); mMemoryData.Write(itemBuffer, 0, itemBuffer.Length); mMemoryData.WriteLine("");
以上就是multipart/form-data
普通值和文件提交时写的数据格式,下面看一下这个multipart/form-data
的完整代码
for (int i = 0; i < mCases.FormBody.Count; i++) { var item = mCases.FormBody[i]; mMemoryData.Write("--"); mMemoryData.WriteLine(boundary); if (item.Type == HttpDataType.Bytes) { mMemoryData.WriteLine($"Content-Disposition: form-data; name=\"{item.Name}\"; filename=\"{item.FileName}\""); mMemoryData.WriteLine($"Content-Type: {item.Type}"); mMemoryData.WriteLine(""); var itemBuffer = item.GetBuffer(); mMemoryData.Write(itemBuffer, 0, itemBuffer.Length); mMemoryData.WriteLine(""); } else { mMemoryData.WriteLine($"Content-Disposition: form-data; name=\"{item.Name}\""); mMemoryData.WriteLine(""); mTextBodyCached.Clear(); item.GetTemplate().Execute(mTextBodyCached); mMemoryData.Write(mTextBodyCached); mMemoryData.WriteLine(""); } } if (mCases.FormBody.Count > 0) { mMemoryData.Write("--"); mMemoryData.Write(boundary); mMemoryData.WriteLine("--"); mMemoryData.Flush(); }
这样一个完整的multipart/form-data
提交基础协议代码就处理完成;在webbenchmark
的实现有还有application/json
和application/x-www-form-urlencoded
的处理,相对于multipart/form-data
来说这两个处理就更加简单了;下面包括:POST,GET,PUT,DELETE和三种数据格式提交的完整代码函(在BeetleX的pipestream帮助下这些协议的处理还是比较简单的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | public void Write(PipeStream stream) { string boundary = null ; bool multipartFormData = mCases.ContentType == "multipart/form-data" ; if (multipartFormData) boundary = "----Beetlex.io" + DateTime.Now.ToString( "yyyyMMddHHmmss" ); byte [] bodyData = null ; int bodyLength = 0; if (mHeaderCached == null ) mHeaderCached = new StringBuilder(); mHeaderCached.Clear(); if (mMemoryData == null ) mMemoryData = new PipeStream(); if (mMemoryData.Length > 0) mMemoryData.ReadFree(( int )mMemoryData.Length); if (mTextBodyCached == null ) mTextBodyCached = new StringBuilder(); mTextBodyCached.Clear(); mHeaderCached.Append(mCases.Method).Append( " " ); mUrlTemplate.Execute(mHeaderCached); for ( int i = 0; i < mCases.QueryString.Count; i++) { if (i == 0) { if (mUrlHasParameter) mHeaderCached.Append( "&" ); else mHeaderCached.Append( "?" ); } else { mHeaderCached.Append( "&" ); } mHeaderCached.Append(mCases.QueryString[i].Name); mHeaderCached.Append( "=" ); mCases.QueryString[i].GetTemplate().Execute(mHeaderCached, true ); } mHeaderCached.Append( " " ); mHeaderCached.Append(Protocol).Append( "\r\n" ); foreach ( var item in mCases.Header) { mHeaderCached.Append(item.Name).Append( ": " ); item.GetTemplate().Execute(mHeaderCached); mHeaderCached.Append( "\r\n" ); } mHeaderCached.Append( "Content-Type: " ).Append(mCases.ContentType); if (multipartFormData) mHeaderCached.Append( "; boundary=" ).Append(boundary); mHeaderCached.Append( "\r\n" ); if (multipartFormData) { for ( int i = 0; i < mCases.FormBody.Count; i++) { var item = mCases.FormBody[i]; mMemoryData.Write( "--" ); mMemoryData.WriteLine(boundary); if (item.Type == HttpDataType.Bytes) { mMemoryData.WriteLine($ "Content-Disposition: form-data; name=\"{item.Name}\"; filename=\"{item.FileName}\"" ); mMemoryData.WriteLine($ "Content-Type: {item.Type}" ); mMemoryData.WriteLine( "" ); var itemBuffer = item.GetBuffer(); mMemoryData.Write(itemBuffer, 0, itemBuffer.Length); mMemoryData.WriteLine( "" ); } else { mMemoryData.WriteLine($ "Content-Disposition: form-data; name=\"{item.Name}\"" ); mMemoryData.WriteLine( "" ); mTextBodyCached.Clear(); item.GetTemplate().Execute(mTextBodyCached); mMemoryData.Write(mTextBodyCached); mMemoryData.WriteLine( "" ); } } if (mCases.FormBody.Count > 0) { mMemoryData.Write( "--" ); mMemoryData.Write(boundary); mMemoryData.WriteLine( "--" ); mMemoryData.Flush(); } } else if (mCases.ContentType == "application/json" ) { if (mJsonBodyTemplate != null ) { mJsonBodyTemplate.Execute(mTextBodyCached); } } else { for ( int i = 0; i < mCases.FormBody.Count; i++) { if (i > 0) { mTextBodyCached.Append( "&" ); } mTextBodyCached.Append(mCases.FormBody[i].Name).Append( "=" ); mCases.FormBody[i].GetTemplate().Execute(mTextBodyCached, true ); } } try { if (multipartFormData) { bodyLength = ( int )mMemoryData.Length; if (bodyLength > 0) { bodyData = System.Buffers.ArrayPool< byte >.Shared.Rent(bodyLength); mMemoryData.Read(bodyData, 0, bodyLength); } } else { if (mTextBodyCached.Length > 0) { char [] charbuffer = System.Buffers.ArrayPool< char >.Shared.Rent(mTextBodyCached.Length); try { mTextBodyCached.CopyTo(0, charbuffer, 0, mTextBodyCached.Length); bodyData = System.Buffers.ArrayPool< byte >.Shared.Rent(mTextBodyCached.Length * 6); bodyLength = Encoding.UTF8.GetBytes(charbuffer, 0, mTextBodyCached.Length, bodyData, 0); } finally { System.Buffers.ArrayPool< char >.Shared.Return(charbuffer); } } } mHeaderCached.Append( "Content-Length: " ).Append(bodyLength).Append( "\r\n" ); mHeaderCached.Append( "\r\n" ); stream.Write(mHeaderCached); if (bodyData != null ) { stream.Write(bodyData, 0, bodyLength); } } finally { if (bodyData != null ) System.Buffers.ArrayPool< byte >.Shared.Return(bodyData); } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架