Długość zawartości nie jest wysyłana, gdy włączona jest kompresja gzip w Apache?

13

Byłbym bardzo wdzięczny za pomoc w zrozumieniu tego zachowania Apache.

Komunikuję się z PHP z aplikacji iPhone Objective-C w aplikacji / json. Kompresja Gzip jest włączona na serwerze i żądana przez klienta.

Z mojego .htaccess:

AddOutputFilterByType DEFLATE text/html text/plain text/xml application/x-httpd-php application/json

W przypadku małych żądań Apache ustawia nagłówek „Content-Length”. Na przykład (te wartości są wyprowadzane w celu C z nagłówka):

Connection = "Keep-Alive";
"Content-Encoding" = gzip;
"Content-Length" = 185;     <-------------
"Content-Type" = "application/json";
Date = "Wed, 22 Sep 2010 12:20:27 GMT";
"Keep-Alive" = "timeout=3, max=149";
Server = Apache;
Vary = "Accept-Encoding";
"X-Powered-By" = "PHP/5.2.13";
"X-Uncompressed-Content-Length" = 217;

X-Uncompressed-Content-Length to nagłówek, który dodaję do rozmiaru nieskompresowanego ciągu JSON.

Jak widać, to żądanie jest bardzo małe (217 bajtów).

Oto nagłówki z większego żądania (282888 bajtów):

Connection = "Keep-Alive";
"Content-Encoding" = gzip;
"Content-Type" = "application/json";
Date = "Wed, 22 Sep 2010 12:20:29 GMT";
"Keep-Alive" = "timeout=3, max=148";
Server = Apache;
"Transfer-Encoding" = Identity;
Vary = "Accept-Encoding";
"X-Powered-By" = "PHP/5.2.13";
"X-Uncompressed-Content-Length" = 282888;

Zauważ, że długość treści nie jest podana.

Moje pytania:

  1. Dlaczego Apache nie wysyła Content-Length dla większego żądania?
  2. Czy fakt, że ustawiono „Contend-Encoding = gzip”, oznacza, że ​​kompresja gzip nadal działa w przypadku większego żądania, mimo że nie mogę zweryfikować różnicy wielkości?
  3. Czy istnieje sposób, w jaki mogę skłonić Apache do uwzględnienia faktycznej długości treści w przypadku tych większych żądań, aby dokładniej zgłaszać wykorzystanie danych użytkownikom?

Tej aplikacji można używać w przypadku planów danych, które są drogie, dlatego pragnę zgłosić użytkownikowi rzeczywiste użycie, a nie 30-70% zawyżone zużycie (kilkaset dodatkowych KB może nie brzmieć dużo - ale te plany mogą kosztować od 1 USD i 10 USD za MB!).

Z góry dziękuję.

William Denniss
źródło

Odpowiedzi:

14

Dodatek do odpowiedzi Martina Fjordvaldsa:

Apache używa kodowania fragmentarycznego tylko wtedy, gdy rozmiar skompresowanego pliku jest większy niż DeflateBufferSize. Zwiększenie tego rozmiaru bufora uniemożliwi zatem serwerowi szyfrowanie fragmentaryczne również w przypadku większych plików, powodując wysyłanie długości treści nawet w przypadku spakowanych danych.

Więcej informacji jest dostępnych tutaj: http://httpd.apache.org/docs/2.2/mod/mod_deflate.html#deflatebuffersize

Philippe
źródło
Niezłe. Jest to prawdopodobnie najszybszy sposób rozwiązania tego problemu. Jeśli ktoś potrzebuje wyższego poziomu dostosowywania (np. Fragmentuje niektóre żądania, a nie inne), zapoznaj się z moją odpowiedzią serverfault.com/a/183856/54957, aby uzyskać ręczne rozwiązanie.
William Denniss,
7

Wygląda na to, że Apache wykonuje kodowanie fragmentaryczne, co oznacza, że ​​może wysyłać dane podczas gzipowania, zamiast czekać na pełną odpowiedź. Jest to dość standardowa praktyka, ale nie znam wystarczająco Apache, aby stwierdzić, czy można ją wyłączyć.

Martin Fjordvald
źródło
Dzięki za informację, wskazałeś mi właściwy kierunek, a ja go rozwiązałem.
William Denniss,
Przyjęty. Wszystkim, którzy czytają to pytanie - proszę przeczytać moją odpowiedź, aby uzyskać szczegółowe rozwiązanie. Zasadniczo można uniknąć dzielenia na fragmenty (a tym samym zerowej długości treści) poprzez ręczne buforowanie i kompresowanie odpowiedzi.
William Denniss,
Trochę mylące jest to, że zaakceptowana odpowiedź nie jest odpowiedzią na pierwotne pytanie, ale raczej czymś, co pomogło ci ją uzyskać. Być może powinieneś zaakceptować odpowiedź, którą zamieściłeś poniżej, aby wszystko było bardziej jasne.
redbmk
@redbmk fair point, po prostu nie chciałem wydawać się niewdzięczny. Philippe ma idealną prostą naprawę, więc zaakceptowałem jego.
William Denniss,
5

OK, udało mi się to rozwiązać. Jak słusznie zauważa Martin F, Apache dzieli odpowiedź na części, więc rozmiar zawartości nie jest znany. Dla wielu osób jest to pożądane (strona ładuje się szybciej). Kosztem tego jest brak możliwości zgłoszenia postępu pobierania.

Dla takich jak ja, którzy naprawdę chcą zgłosić postęp pobierania, jeśli użyjesz automatycznego wsparcia gzip dla Apache lub PHP, niewiele możesz zrobić. Rozwiązaniem jest zrobienie tego ręcznie. To łatwiejsze niż się wydaje:

Jeśli wysyłasz całe pliki, jest to świetny przykład w PHP wymuszania pojedynczej porcji (z Content-Length): http://www.php.net/manual/en/function.ob-start.php # 94741

Jeśli wysyłasz wygenerowane dane, użyj gzencode, aby zakodować swoje dane, jak w powyższym przykładzie. Warunkiem jest, aby wszystkie dane wyjściowe były przechowywane w zmiennej (możesz użyć ob_start, aby to zrobić, jeśli potrzebujesz buforować, a następnie uzyskać zawartość bufora).

        // $replyBody is the entire contents of your reply

        header("Content-Type: application/json");  // or whatever yours is

        // checks if gzip is supported by client
        $pack = true;
        if(empty($_SERVER["HTTP_ACCEPT_ENCODING"]) || strpos($_SERVER["HTTP_ACCEPT_ENCODING"], 'gzip') === false)
        {
            $pack = false;
        }

        // if supported, gzips data
        if($pack) {
            header("Content-Encoding: gzip");
            $replyBody = gzencode($replyBody, 9, FORCE_GZIP);
        }

        // compressed or not, sets the Content-Length           
        header("Content-Length: " . mb_strlen($replyBody, 'latin1'));

        // outputs reply & exits
        echo $replyBody;
        exit;

I voila!

Kolejną wielką zaletą robienia tego samemu jest to, że możesz ustawić poziom kompresji. Jest to świetne dla mojej aplikacji mobilnej, ponieważ mogę ustawić najwyższy poziom kompresji (więc moi użytkownicy płacą mniej za dane!) - podczas gdy serwer prawdopodobnie używa tylko średniego poziomu kompresji w celu uzyskania lepszego kompromisu między procesorem a rozmiarem. Poziomy kompresji to coś, co uważam, że możesz zmienić tylko wtedy, gdy możesz edytować httpd.conf (którego na hostingu współdzielonym nie mogę).

Zachowałem więc moją DEFLATE .htaccess dla wszystkiego oprócz mojej aplikacji / odpowiedzi json, które teraz koduję w powyższy sposób.

Jeszcze raz dziękuję Martin F., dałeś mi iskrę potrzebną do rozwiązania tego :)

William Denniss
źródło
1
Nawiasem mówiąc, oszczędności z danymi JSON (z mocno powtarzanymi kluczami) są ogromne , o 77% mniejsze w jednym przypadku. To wielka sprawa za 1 USD za MB ...
William Denniss,
1
Prawdopodobnie powinieneś po prostu użyć strlen($replyBody)zamiast mb_strlen($replyBody, 'latin1'). Długość treści to tylko liczba bajtów (nie znaków), co daje ci strlen (). Używanie mb_strlen () z rodzajem „latin1” działa, ponieważ znaki latin1 mają zawsze 8 bitów, ale może mieć problemy z kodowaniem, które produkuje bajty, które nie są poprawnymi znakami latin1.
orrd