Perché qualcuno dovrebbe usare multipart / form-data per dati misti e trasferimenti di file?

7

Sto lavorando in C # e sto facendo alcune comunicazioni tra 2 app che sto scrivendo. Sono giunto ad apprezzare l'API Web e JSON. Ora sono al 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 una richiesta manualmente che segue un formato come questo:

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 doloroso per qualcuno che è abituato a dati JSON. Quindi ovviamente la soluzione è creare una richiesta JSON e Base64 codifica il file e finire con una richiesta come questa:

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

E possiamo usare la serializzazione e deserializzazione JSON ovunque vorremmo. Inoltre, il codice per inviare questi dati è abbastanza semplice. Devi solo creare la tua classe per la serializzazione JSON e quindi impostare le proprietà. La proprietà della stringa di file è impostata in poche righe:

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

Nessun delimitatore e intestazione più sciocchi per ogni elemento. 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 50KB a 1,5 MB o giù di lì. Per prima cosa ho scritto alcune righe per eseguire semplicemente lo streaming nel file su una matrice di byte per confrontarlo con la logica che scorre nel file e quindi convertirlo in un flusso Base64. Di seguito sono riportati i 2 blocchi di codice che ho profilato:

Direct Stream to Profile 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

Riproduci lo stream e codifica il profilo creando la 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 erano che la lettura semplice richiedeva sempre 0ms, ma che la codifica Base64 richiedeva fino a 5 ms. Di seguito sono riportati i tempi più lunghi:

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

Tuttavia, nella produzione non si scriverebbero mai solo i dati multipart / form senza aver prima controllato correttamente il delimitatore? Così ho modificato il codice del modulo in modo che controllasse i byte delimitatore nel file stesso per assicurarmi che tutto fosse analizzato ok. Non ho scritto un algoritmo di scansione ottimizzato, quindi ho appena ridotto il delimitatore in modo che non sprecherebbe 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à molto più lento. Di seguito sono riportati i risultati con tempi > 0ms 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 rappresenta il vantaggio in termini di prestazioni di eseguire una codifica Base64 invece di controllare i byte del file per un delimitatore.

Ovviamente la codifica Base64 si gonfierà come mostrato nella prima tabella, ma non è poi così male nemmeno con UTF-8 in grado di Unicode e si comprimerebbe bene se lo si desidera. Ma il vero vantaggio è che il mio codice è bello, pulito e facilmente comprensibile e non fa male ai miei occhi per guardare tanto al carico utile della richiesta JSON.

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

    
posta Ian 15.06.2017 - 22:27
fonte

1 risposta

8

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

According to the internet I am supposed to use a multipart/form-data request

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 non è in genere basata sui moduli. 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 qualsiasi formato di serializzazione tu scelga. I protocolli binari come CBOR o BSON sono ancora meglio perché serializzano su oggetti più piccoli rispetto a Base64, e sono abbastanza vicini a JSON da essere (dovrebbe essere) un'estensione facile a una comunicazione JSON esistente. Non sei sicuro delle prestazioni della CPU rispetto a Base64.

    
risposta data 15.06.2017 - 22:41
fonte

Leggi altre domande sui tag