從零開始實現multipart/form-data資料提交

beetlex發表於2020-07-09

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

}

  

 

相關文章