Perché qualcuno dovrebbe utilizzare dati multipart / form per dati misti e trasferimenti di file?


14

Sto lavorando in C # e sto comunicando tra 2 app che sto scrivendo. Mi è piaciuta l'API Web e JSON. Ora sono nel punto in cui sto scrivendo una routine per inviare un record tra i due server che include alcuni dati di testo e un file.

Secondo Internet dovrei usare una richiesta multipart / form-data come mostrato qui:

Domanda SO "Moduli multipart da client C #"

Fondamentalmente scrivi manualmente una richiesta che segue un formato simile al seguente:

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

Copiato da RFC 1867 - Caricamento file basato su modulo in HTML

Questo formato è abbastanza angosciante per qualcuno che è abituato ai bei dati JSON. Quindi ovviamente la soluzione è creare una richiesta JSON e Base64 codifica il file e finisce con una richiesta come questa:

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

E possiamo utilizzare la serializzazione e la deserializzazione JSON ovunque desideriamo. Inoltre, il codice per inviare questi dati è abbastanza semplice. È sufficiente creare la classe per la serializzazione JSON e quindi impostare le proprietà. La proprietà della stringa di file è impostata in poche righe banali:

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

Niente più delimitatori e intestazioni sciocchi per ogni oggetto. Ora la domanda rimanente è la prestazione. Quindi l'ho profilato. Ho un set di 50 file di esempio che avrei bisogno di inviare attraverso il cavo che vanno da 50 KB a 1,5 MB circa. Per prima cosa ho scritto alcune righe per semplicemente lo streaming del file in un array di byte per confrontarlo con la logica che scorre nel file e poi lo converte in un flusso Base64. Di seguito sono riportati i 2 blocchi di codice che ho profilato:

Stream diretto al profilo 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

Streaming ed Encode per profilare la creazione di una richiesta 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

I risultati furono che la lettura semplice impiegava sempre 0ms, ma che la codifica Base64 impiegava fino a 5ms. Di seguito sono riportati i tempi più lunghi:

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

Tuttavia, in produzione non scriveresti mai ciecamente dati multipart / form senza prima controllare il delimitatore giusto? Quindi ho modificato il codice dei dati del modulo in modo che controllasse i byte delimitatori nel file stesso per assicurarsi che tutto sarebbe stato analizzato correttamente. Non ho scritto un algoritmo di scansione ottimizzato, quindi ho solo ridotto il delimitatore in modo da non perdere molto 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;

Ora i risultati mi mostrano che il metodo form-data sarà in realtà significativamente più lento. Di seguito sono riportati i risultati con tempi> 0 ms per entrambi i metodi:

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

Non sembra che un algoritmo ottimizzato farebbe molto meglio visto che il mio delimitatore era lungo solo 5 caratteri. Non è comunque 3 volte migliore, il che è il vantaggio prestazionale di fare una codifica Base64 invece di controllare i byte di file per un delimitatore.

Ovviamente la codifica Base64 aumenterà le dimensioni come mostrato nella prima tabella, ma non è poi così male anche con UTF-8 compatibile con Unicode e comprimerebbe bene se lo si desidera. Ma il vero vantaggio è che il mio codice è carino, pulito e facilmente comprensibile e non fa male ai miei occhi guardare così tanto il payload della richiesta JSON.

Quindi perché mai qualcuno non dovrebbe semplicemente codificare i file Base64 in JSON invece di utilizzare multipart / form-data? Ci sono gli standard, ma questi cambiano relativamente spesso. Gli standard sono davvero solo suggerimenti, giusto?

Risposte:


16

multipart/form-dataè un costrutto creato per i moduli HTML. Come hai scoperto, il positivo di multipart/form-dataè che la dimensione del trasferimento è più vicina alla dimensione dell'oggetto che viene trasferito, dove in una codifica del testo dell'oggetto la dimensione viene gonfiata sostanzialmente. Puoi capire che la larghezza di banda di Internet era un bene più prezioso dei cicli della CPU quando il protocollo è stato inventato.

Secondo Internet dovrei usare una richiesta multipart / form-data

multipart/form-dataè il miglior protocollo per i caricamenti del browser perché è supportato da tutti i browser. Non c'è motivo di usarlo per le comunicazioni da server a server. La comunicazione da server a server di solito non è basata su form. Gli oggetti di comunicazione sono più complessi e richiedono nidificazione e tipi: i requisiti che JSON gestisce bene. La codifica Base64 è una soluzione semplice per il trasferimento di oggetti binari in qualunque formato di serializzazione si scelga. I protocolli binari come CBOR o BSON sono ancora migliori perché si serializzano su oggetti più piccoli rispetto a Base64 e sono abbastanza vicini a JSON da essere (dovrebbe essere) una semplice estensione di una comunicazione JSON esistente. Non sono sicuro delle prestazioni della CPU rispetto a Base64.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.