從零開始實現multipart/form-data數據提交

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

}

  

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章