Зачем кому-то использовать multipart / form-data для смешанных данных и передачи файлов?

14

Я работаю в C # и поддерживаю связь между двумя приложениями, которые я пишу. Мне понравились Web API и JSON. Сейчас я нахожусь в точке, где я пишу процедуру для отправки записи между двумя серверами, которая включает в себя некоторые текстовые данные и файл.

Согласно Интернету, я должен использовать запрос multipart / form-data, как показано здесь:

SO Вопрос "Составные формы из C # клиента"

По сути, вы пишете запрос вручную в следующем формате:

Content-type: multipart/form-data, boundary=AaB03x

--AaB03x
content-disposition: form-data; name="field1"

Joe Blow
--AaB03x
content-disposition: form-data; name="pics"; filename="file1.txt"
Content-Type: text/plain

 ... contents of file1.txt ...
--AaB03x--

Скопировано из RFC 1867 - Загрузка файлов на основе форм в HTML

Этот формат очень огорчает того, кто привык получать хорошие данные JSON. Таким образом, очевидно, что решение состоит в том, чтобы создать запрос JSON, а Base64 кодировать файл и получить запрос, подобный следующему:

{
    "field1":"Joe Blow",
    "fileImage":"JVBERi0xLjUKJe..."
}

И мы можем использовать сериализацию и десериализацию JSON где угодно. Кроме того, код для отправки этих данных довольно прост. Вы просто создаете свой класс для сериализации JSON и затем устанавливаете свойства. Свойство строки файла устанавливается в несколько тривиальных строк:

using (FileStream fs = File.Open(file_path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    byte[] file_bytes = new byte[fs.Length];
    fs.Read(file_bytes, 0, file_bytes.Length);
    MyJsonObj.fileImage = Convert.ToBase64String(file_bytes);
}

Нет больше глупых разделителей и заголовков для каждого элемента. Теперь оставшийся вопрос - производительность. Так что я это профилировал. У меня есть набор из 50 файлов примеров, которые мне нужно будет отправить по проводам, в диапазоне от 50 КБ до 1,5 МБ или около того. Сначала я написал несколько строк, чтобы просто передать файл в байтовый массив, чтобы сравнить его с логикой, которая передается в файл, а затем преобразовал его в поток Base64. Ниже приведены 2 фрагмента кода, которые я профилировал:

Прямой поток в профиль multipart / form-data

var timer = new Stopwatch();
timer.Start();
using (FileStream fs = File.Open(file_path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    byte[] test_data = new byte[fs.Length];
    fs.Read(test_data, 0, test_data.Length);
}
timer.Stop();
long test = timer.ElapsedMilliseconds;
//Write time elapsed and file size to CSV file

Поток и кодирование в профиль для создания запроса JSON

var timer = new Stopwatch();
timer.Start();
using (FileStream fs = File.Open(file_path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    byte[] file_bytes = new byte[fs.Length];
    fs.Read(file_bytes, 0, file_bytes.Length);
    ret_file = Convert.ToBase64String(file_bytes);
}
timer.Stop();
long test = timer.ElapsedMilliseconds;
//Write time elapsed, file size, and length of UTF8 encoded ret_file string to CSV file

В результате простое чтение всегда занимало 0 мс, а кодирование Base64 - до 5 мс. Ниже приведены самые длинные времена:

File Size  |  Output Stream Size  |  Time
1352KB        1802KB                 5ms
1031KB        1374KB                 7ms
463KB         617KB                  1ms

Тем не менее, на производстве вы никогда не будете просто слепо писать multipart / form-data без предварительной проверки вашего разделителя, верно? Поэтому я изменил код данных формы, чтобы он проверял байты-разделители в самом файле, чтобы убедиться, что все будет в порядке. Я не писал оптимизированный алгоритм сканирования, поэтому просто уменьшил разделитель, чтобы он не тратил много времени.

var timer = new Stopwatch();
timer.Start();
using (FileStream fs = File.Open(file_path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
    byte[] test_data = new byte[fs.Length];
    fs.Read(test_data, 0, test_data.Length);
    string delim = "--DXX";
    byte[] delim_checker = Encoding.UTF8.GetBytes(delim);

    for (int i = 0; i <= test_data.Length - delim_checker.Length; i++)
    {
        bool match = true;
        for (int j = i; j < i + delim_checker.Length; j++)
        {
            if (test_data[j] != delim_checker[j - i])
            {
                match = false;
                break;
            }
        }
        if (match)
        {
            break;
        }
    }
}
timer.Stop();
long test = timer.ElapsedMilliseconds;

Теперь результаты показывают мне, что метод данных формы будет значительно медленнее. Ниже приведены результаты со временем> 0 мс для любого метода:

File Size | FormData Time | Json/Base64 Time
181Kb       1ms             0ms
1352Kb      13ms            4ms
463Kb       4ms             5ms
133Kb       1ms             0ms
133Kb       1ms             0ms
129Kb       1ms             0ms
284Kb       2ms             1ms
1031Kb      9ms             3ms

Похоже, что оптимизированный алгоритм был бы намного лучше, поскольку мой разделитель был всего 5 символов. В любом случае, не в три раза лучше, что является преимуществом в производительности при выполнении кодирования Base64 вместо проверки байтов файла на наличие разделителя.

Очевидно, что кодировка Base64 будет увеличивать размер, как я показываю в первой таблице, но на самом деле это не так уж плохо, даже с UTF-8, поддерживающим Unicode, и при желании хорошо сжимается. Но реальным преимуществом является то, что мой код приятный, чистый и легко понятный, и мне не мешает смотреть на полезную нагрузку JSON-запроса.

Так с какой стати кто-то не просто кодирует файлы Base64 в JSON вместо использования multipart / form-data? Стандарты есть, но они меняются относительно часто. Стандарты - это всего лишь предложения, верно?

Ян
источник

Ответы:

16

multipart/form-dataэто конструкция, созданная для HTML-форм. Как вы обнаружили, положительным multipart/form-dataявляется то, что размер передачи ближе к размеру передаваемого объекта - где в текстовой кодировке объекта размер существенно завышен. Вы можете понять, что пропускная способность интернета была более ценным товаром, чем циклы процессора, когда был изобретен протокол.

Согласно Интернету я должен использовать запрос multipart / form-data

multipart/form-dataэто лучший протокол для загрузки в браузер, потому что он поддерживается всеми браузерами. Нет причин использовать его для межсерверной связи. Связь между серверами обычно не основана на форме. Объекты связи являются более сложными и требуют вложенности и типов - требований, которые JSON хорошо выполняет. Кодировка Base64 - это простое решение для передачи двоичных объектов в любом формате сериализации, который вы выберете. Двоичные протоколы, такие как CBOR или BSON , даже лучше, потому что они сериализуются в меньшие объекты, чем Base64, и они достаточно близки к JSON, чтобы это (должно быть) легкое расширение существующей связи JSON. Не уверен насчет производительности процессора против Base64.

Самуил
источник