¿Por qué alguien usaría multipart / form-data para datos mixtos y transferencias de archivos?

14

Estoy trabajando en C # y estoy haciendo alguna comunicación entre 2 aplicaciones que estoy escribiendo. Me han llegado a gustar la API web y JSON. Ahora estoy en el punto donde estoy escribiendo una rutina para enviar un registro entre los dos servidores que incluye algunos datos de texto y un archivo.

De acuerdo con Internet, se supone que debo usar una solicitud multiparte / datos de formulario como se muestra aquí:

Pregunta SO "Formularios multiparte del cliente C #"

Básicamente, escribe una solicitud manualmente que sigue un 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 de RFC 1867 - Carga de archivos basada en formularios en HTML

Este formato es bastante angustioso para alguien que está acostumbrado a buenos datos JSON. Entonces, obviamente, la solución es crear una solicitud JSON y Base64 codificar el archivo y terminar con una solicitud como esta:

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

Y podemos hacer uso de la serialización y deserialización JSON en cualquier lugar que deseemos. Además de eso, el código para enviar estos datos es bastante simple. Simplemente cree su clase para la serialización JSON y luego configure las propiedades. La propiedad de cadena de archivo se establece en unas pocas líneas triviales:

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

No más delimitadores tontos y encabezados para cada elemento. Ahora la pregunta restante es el rendimiento. Así que lo describí. Tengo un conjunto de 50 archivos de muestra que necesitaría enviar a través del cable que van desde 50 KB a 1,5 MB más o menos. Primero escribí algunas líneas para simplemente transmitir en el archivo a una matriz de bytes para comparar eso con la lógica que se transmite en el archivo y luego convertirlo en una secuencia de Base64. A continuación se muestran los 2 fragmentos de código que perfilé:

Transmisión directa al perfil multiparte / datos de formulario

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

Transmitir y codificar al perfil creando solicitud 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

Los resultados fueron que la lectura simple siempre tomó 0 ms, pero que la codificación Base64 tomó hasta 5 ms. A continuación se muestran los tiempos más largos:

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

Sin embargo, en la producción nunca escribiría a ciegas datos multiparte / formulario sin verificar primero su delimitador, ¿verdad? Así que modifiqué el código de datos de formulario para que verificara los bytes delimitadores en el archivo en sí para asegurarme de que todo se analizara correctamente. No escribí un algoritmo de escaneo optimizado, así que hice el delimitador pequeño para que no perdiera mucho tiempo.

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;

Ahora los resultados me muestran que el método de datos de formulario en realidad será significativamente más lento. A continuación se muestran resultados con tiempos> 0 ms para cualquier método:

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

No parece que un algoritmo optimizado sea mucho mejor ya que mi delimitador tiene solo 5 caracteres. De todos modos, no es 3 veces mejor, que es la ventaja de rendimiento de hacer una codificación Base64 en lugar de verificar los bytes del archivo para un delimitador.

Obviamente, la codificación Base64 inflará el tamaño como lo muestro en la primera tabla, pero realmente no es tan malo incluso con UTF-8 con capacidad Unicode y se comprimiría bien si lo desea. Pero el beneficio real es que mi código es agradable y limpio y fácilmente comprensible, y tampoco perjudica a mis ojos mirar la carga útil de la solicitud JSON.

Entonces, ¿por qué alguien no codificaría simplemente los archivos Base64 en JSON en lugar de usar multipart / form-data? Existen los Estándares, pero estos cambian con relativa frecuencia. Los estándares son realmente sugerencias de todos modos, ¿verdad?

Ian
fuente

Respuestas:

16

multipart/form-dataes una construcción creada para formularios HTML. Como ha descubierto, lo positivo de multipart/form-dataes que el tamaño de transferencia es más cercano al tamaño del objeto que se transfiere, donde en una codificación de texto del objeto el tamaño se infla sustancialmente. Puede comprender que el ancho de banda de Internet era un producto más valioso que los ciclos de CPU cuando se inventó el protocolo.

De acuerdo con Internet, se supone que debo usar una solicitud de datos multiparte / formulario

multipart/form-dataes el mejor protocolo para cargar navegadores porque es compatible con todos los navegadores. No hay razón para usarlo para la comunicación de servidor a servidor. La comunicación de servidor a servidor generalmente no se basa en formularios. Los objetos de comunicación son más complejos y requieren anidamiento y tipos, los requisitos que JSON maneja bien. La codificación Base64 es una solución simple para transferir objetos binarios en cualquier formato de serialización que elija. Los protocolos binarios como CBOR o BSON son aún mejores porque se serializan en objetos más pequeños que Base64, y están lo suficientemente cerca de JSON que (debería ser) una extensión fácil a una comunicación JSON existente. No estoy seguro sobre el rendimiento de la CPU frente a Base64.

Samuel
fuente