Dlaczego ktokolwiek miałby używać danych wieloczęściowych / formularzy do mieszanych danych i transferów plików?

14

Pracuję w C # i komunikuję się między 2 aplikacjami, które piszę. Polubiłem Web API i JSON. Teraz jestem w punkcie, w którym piszę procedurę wysyłania rekordu między dwoma serwerami, który zawiera dane tekstowe i plik.

Według Internetu mam użyć żądania wieloczęściowego / formularza danych, jak pokazano tutaj:

Pytanie SO „Formularze wieloczęściowe z klienta C #”

Zasadniczo piszesz zapytanie ręcznie w takim formacie:

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

Skopiowano z RFC 1867 - Przesyłanie plików w formacie HTML w formacie HTML

Ten format jest dość niepokojący dla kogoś, kto jest przyzwyczajony do ładnych danych JSON. Oczywiście rozwiązaniem jest utworzenie żądania JSON i kodowanie pliku Base64 i zakończenie takiego żądania:

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

I możemy korzystać z serializacji i deserializacji JSON w dowolnym miejscu. Ponadto kod do wysyłania tych danych jest dość prosty. Wystarczy utworzyć klasę do serializacji JSON, a następnie ustawić właściwości. Właściwość ciągu pliku jest ustawiona w kilku trywialnych wierszach:

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

Nigdy więcej głupich separatorów i nagłówków dla każdego elementu. Teraz pozostałym pytaniem jest wydajność. Więc profilowałem to. Mam zestaw 50 przykładowych plików, które musiałbym wysłać za pomocą drutu o zakresie od 50 KB do około 1,5 MB. Najpierw napisałem kilka wierszy, aby po prostu przesłać strumieniowo w pliku do tablicy bajtów, aby porównać to z logiką przesyłaną strumieniowo w pliku, a następnie przekonwertować na strumień Base64. Poniżej 2 fragmenty kodu, które profilowałem:

Bezpośrednie przesyłanie strumieniowe do profilu danych wieloczęściowych / formularzy

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

Przesyłaj strumieniowo i koduj do profilu, tworząc żądanie 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

W rezultacie prosty odczyt zawsze zajmował 0 ms, ale kodowanie Base64 trwało 5 ms. Poniżej znajdują się najdłuższe czasy:

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

Jednak w produkcji nigdy nie pisałeś na ślepo danych wieloczęściowych / formularzy bez uprzedniego sprawdzenia ogranicznika, prawda? Zmodyfikowałem więc kod danych formularza, aby sprawdzał bajty separatora w samym pliku, aby upewnić się, że wszystko zostanie poprawnie przeanalizowane. Nie napisałem zoptymalizowanego algorytmu skanowania, więc po prostu zmniejszyłem separator, aby nie marnował dużo czasu.

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;

Teraz wyniki pokazują, że metoda form-data będzie faktycznie znacznie wolniejsza. Poniżej znajdują się wyniki z czasami> 0ms dla każdej metody:

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

Nie wydaje się, żeby zoptymalizowany algorytm lepiej sprawdziłby się, ponieważ mój ogranicznik miał tylko 5 znaków. W każdym razie nie trzykrotnie lepiej, co jest zaletą wynikającą z kodowania Base64 zamiast sprawdzania bajtów pliku w poszukiwaniu separatora.

Oczywiście kodowanie Base64 zwiększy rozmiar, jak pokazałem w pierwszej tabeli, ale tak naprawdę nie jest tak źle nawet z UTF-8 obsługującym Unicode i dobrze by się skompresowało, jeśli zajdzie taka potrzeba. Ale prawdziwą korzyścią jest to, że mój kod jest ładny, przejrzysty i łatwo zrozumiały i nie zaszkodzi moim oczom spojrzeć na ładunek żądania JSON.

Dlaczego więc, u licha, nikt nie miałby po prostu kodować Base64 w JSON zamiast korzystać z danych wieloczęściowych / formularzy? Istnieją Standardy, ale zmieniają się stosunkowo często. Normy są tak naprawdę tylko sugestiami, prawda?

Ian
źródło

Odpowiedzi:

16

multipart/form-datato konstrukcja stworzona dla formularzy HTML. Jak odkryłeś, zaletą multipart/form-datajest to, że rozmiar transferu jest bliższy rozmiarowi przesyłanego obiektu - gdzie w kodowaniu tekstowym obiektu rozmiar jest znacznie zawyżony. Można zrozumieć, że przepustowość Internetu była cenniejszym towarem niż cykle procesora, kiedy wynaleziono protokół.

Według Internetu mam użyć żądania wieloczęściowego / formularza danych

multipart/form-datajest najlepszym protokołem do przesyłania z przeglądarki, ponieważ jest obsługiwany przez wszystkie przeglądarki. Nie ma powodu, aby używać go do komunikacji między serwerami. Komunikacja między serwerami zwykle nie jest oparta na formularzach. Obiekty komunikacyjne są bardziej złożone i wymagają zagnieżdżenia oraz typów - wymagań, które JSON dobrze obsługuje. Kodowanie Base64 to proste rozwiązanie do przesyłania obiektów binarnych w dowolnym wybranym formacie serializacji. Protokoły binarne, takie jak CBOR lub BSON, są jeszcze lepsze, ponieważ serializują do mniejszych obiektów niż Base64, i są wystarczająco blisko JSON, aby (powinno być) łatwe rozszerzenie istniejącej komunikacji JSON. Nie jestem pewien wydajności procesora w porównaniu do Base64.

Samuel
źródło