Por que alguém usaria multipart / form-data para dados mistos e transferências de arquivos?

14

Estou trabalhando em c # e fazendo alguma comunicação entre dois aplicativos que estou escrevendo. Passei a gostar da API da Web e JSON. Agora estou no ponto em que estou escrevendo uma rotina para enviar um registro entre os dois servidores que inclui alguns dados de texto e um arquivo.

De acordo com a Internet, devo usar uma solicitação multipart / form-data, como mostrado aqui:

SO Pergunta "Formulários de várias partes do cliente C #"

Basicamente, você escreve uma solicitação manualmente que segue um formato como este:

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--

Copiado do RFC 1867 - Upload de arquivo baseado em formulário em HTML

Esse formato é bastante angustiante para alguém que está acostumado a obter bons dados JSON. Então, obviamente, a solução é criar uma solicitação JSON e o Base64 codificar o arquivo e terminar com uma solicitação como esta:

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

E podemos usar a serialização e desserialização JSON em qualquer lugar que desejarmos. Além disso, o código para enviar esses dados é bastante simples. Você acabou de criar sua classe para serialização JSON e, em seguida, defina as propriedades. A propriedade string do arquivo é configurada em algumas linhas triviais:

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

Não há mais delimitadores e cabeçalhos tolos para cada item. Agora, a questão restante é desempenho. Então eu perfilei isso. Eu tenho um conjunto de 50 arquivos de exemplo que eu precisaria enviar através do fio que varia de 50 KB a 1,5 MB ou mais. Primeiro, escrevi algumas linhas para simplesmente transmitir o arquivo para uma matriz de bytes para comparar isso com a lógica que transmite no arquivo e depois o converte em um fluxo Base64. Abaixo estão os 2 pedaços de código que eu criei perfil:

Stream direto para o perfil 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

Transmita e Codifique no perfil criando a solicitação 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

Os resultados foram que a leitura simples sempre levava 0ms, mas a codificação Base64 demorava 5ms. Abaixo estão os tempos mais longos:

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

No entanto, na produção, você nunca escreveria cegamente dados de várias partes / formulários sem primeiro verificar seu delimitador, certo? Então, modifiquei o código de dados do formulário para verificar os bytes delimitadores no próprio arquivo para garantir que tudo seria analisado corretamente. Como não escrevi um algoritmo de varredura otimizado, reduzi o delimitador para que não perdesse muito tempo.

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;

Agora, os resultados estão me mostrando que o método form-data será realmente significativamente mais lento. Abaixo estão os resultados com tempos> 0ms para qualquer um dos métodos:

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

Não parece que um algoritmo otimizado se sairia muito melhor, visto que meu delimitador tinha apenas 5 caracteres. De qualquer forma, não é 3x melhor, que é a vantagem de desempenho de se fazer uma codificação Base64 em vez de verificar se há um delimitador nos bytes do arquivo.

Obviamente, a codificação Base64 aumentará o tamanho, como mostro na primeira tabela, mas não é tão ruim assim, mesmo com UTF-8 com capacidade para Unicode e seria compactada bem, se desejado. Mas o benefício real é que meu código é agradável, limpo e facilmente compreensível, e não machuca meus olhos olhar tanto para a carga útil de solicitação JSON.

Então, por que diabos alguém não simplesmente codificaria o Base64 em JSON, em vez de usar multipart / form-data? Existem padrões, mas estes mudam com relativa frequência. Padrões são realmente apenas sugestões de qualquer maneira, certo?

Ian
fonte

Respostas:

16

multipart/form-dataé uma construção criada para formulários HTML. Como você descobriu, o positivo de multipart/form-dataé que o tamanho da transferência está mais próximo do tamanho do objeto que está sendo transferido - onde, em uma codificação de texto do objeto, o tamanho é inflado substancialmente. Você pode entender que a largura de banda da Internet era uma mercadoria mais valiosa do que os ciclos de CPU quando o protocolo foi inventado.

De acordo com a Internet, eu devo usar uma solicitação multipart / form-data

multipart/form-dataé o melhor protocolo para upload de navegadores porque é suportado por todos os navegadores. Não há razão para usá-lo para comunicação servidor a servidor. A comunicação entre servidores geralmente não é baseada em formulário. Os objetos de comunicação são mais complexos e requerem aninhamento e tipos - os requisitos que o JSON lida bem. A codificação Base64 é uma solução simples para transferir objetos binários em qualquer formato de serialização que você escolher. Protocolos binários como CBOR ou BSON são ainda melhores porque serializam para objetos menores que Base64 e são próximos o bastante do JSON para que (deva ser) uma extensão fácil para uma comunicação JSON existente. Não tenho certeza sobre o desempenho da CPU vs. Base64.

Samuel
fonte