从零开始实现multipart/form-data数据提交

时间:2024-01-27 07:34:01

在HTTP服务应用中进行数据提交一般都使用application/json,application/x-www-form-urlencodedmultipart/form-data这几种内容格式。这几种格式的处理复杂度处理起来和前面定义的先后顺序一样由易到难。不过现有工具都提供了完善的功能在提交这些数据的时候都比较方便了;不过要自己手动基础协议写起,那multipart/form-data的处理规范还是要相对复杂些。最近在写webapi管理和性能测试工具(https://github.com/IKende/WebBenchmark)时为了得到更可控的时间线和性能,在实现并没有用到任何应用组件都是从HTTP基础协议写起,在这时介绍一下如何在基础HTTP协议的基础上提交multipart/form-data数据.(如果你没有什么特别的需求还是不要这么干)

multipart/form-data

这种格式一般配合多数据类型提交使用,如常用的数据表单和文件结合。这种格式有着自己的处理规范和application/jsonapplication/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中的每项数据都分别有HeaderBody和整个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/jsonapplication/x-www-form-urlencoded的处理,相对于multipart/form-data来说这两个处理就更加简单了;下面包括:POST,GET,PUT,DELETE和三种数据格式提交的完整代码函(在BeetleX的pipestream帮助下这些协议的处理还是比较简单的)

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);
    }

}