HTTPの本文サイズとgzip圧縮
同じ日本語の本文を圧縮なし・gzipの二通りで取得します。先頭バイト、gzipの検査値と展開後サイズを開き、curlが自動展開した場合のファイル長とヘッダーの関係も確認します。
説明
HTTP本文の長さは文字数ではなくバイト数で扱います。gzipで圧縮して送る場合は、圧縮された表現の長さと、展開後の本文の長さも異なります。
同じ日本語の本文を圧縮なし・gzipの二通りで取得します。先頭バイト、gzipの検査値と展開後サイズを開き、curlが自動展開した場合のファイル長とヘッダーの関係も確認します。
1圧縮なしの本文を保存
ヘッダーと本文を別ファイルへ保存します。Accept-Encodingは指定しません。
curl -sS -D plain.headers -o plain.binヘッダーと本文を別々に保存 http://127.0.0.1:18080/unicode
cat plain.headers応答ヘッダーを表示Content-Encodingはなく、Content-Lengthはそのままの本文のバイト数です。-Dはヘッダー、-oは本文の保存先で、二つを混ぜずに確認できます。
2文字数と保存バイト数
日本語を含む本文について、文字列の長さとバイト列の長さを比較します。
python3 - <<'PYCODE'
from pathlib import Path
body = Path("plain.bin").read_bytes()保存した本文をバイトで読む
print("characters",len(body.decode("utf-8"))復号後の文字数,"bytes",len(body)本文のバイト数)
print("first line",repr(body.decode("utf-8").splitlines()[0]))
print("first bytes",body[:23].hex(" ")先頭行付近のバイト列)
PYCODE各行には"あいうえお"の5文字があり、それぞれUTF-8で3バイトです。HTMLタグと改行も本文の一部です。Content-Lengthと一致するのはbytesの値で、charactersの値ではありません。
3gzipのまま保存する
Accept-Encodingでgzipを許容し、圧縮された本文をそのまま保存します。
curl -sS --raw -H "Accept-Encoding: gzip"gzipを要求して未展開で保存 -D gzip.headers -o gzip.bin圧縮応答の保存先 http://127.0.0.1:18080/unicode
cat gzip.headers圧縮方式と長さを確認Content-Encoding: gzipが付き、Content-Lengthはgzip.binのバイト数になります。Content-Typeは展開後の表現であるtext/htmlのままです。圧縮方式と本文のメディア型は別の情報です。
4圧縮データの先頭と末尾を調べる
gzipの先頭ヘッダーと、末尾のCRC32・元サイズを読み、展開した内容と照合します。
python3 inspect_gzip.pygzipの構造・検査値・展開結果を照合先頭の1f 8bがgzipの識別子、08はDEFLATEです。末尾8バイトはlittle-endianのCRC32とISIZEで、ISIZEは元サイズの下位32ビットです。この本文では展開後サイズと一致し、CRC32も展開後本文から計算した値と一致します。
5curlで自動展開する
--compressedを付けて取得し、ヘッダーと展開後の本文を保存します。
curl -sS --compressed対応する圧縮を要求して展開 -D decoded.headers -o decoded.txt元のヘッダーと展開後の本文 http://127.0.0.1:18080/unicode
cat decoded.headers転送時の長さを確認curlは対応する圧縮方式を要求し、この応答を展開してdecoded.txtへ保存します。保存したヘッダーには転送時のContent-EncodingとContent-Lengthが残るため、ヘッダーの長さが展開後ファイルの長さと異なっても、それだけで破損とはいえません。
6展開後の同一性を確認する
圧縮なしと自動展開後のバイト列を比較し、圧縮列のサイズも表示します。
python3 - <<'PYCODE'
from pathlib import Path
plain = Path("plain.bin").read_bytes()
decoded = Path("decoded.txt").read_bytes()
packed = Path("gzip.bin").read_bytes()
print("plain",len(plain),"gzip",len(packed),"decoded",len(decoded))三種類の長さ
print("same bytes",plain==decoded展開後の全バイトを比較)
PYCODE展開後は圧縮なしの本文と全バイトが一致します。転送量の比較にはgzipのサイズを、復号後の内容の比較には展開後のバイト列を使います。
7本文が短い場合の圧縮サイズ
短い本文でも常に小さくなるとは限りません。2バイトの本文をgzipへ変換します。
python3 - <<'PYCODE'
import gzip
tiny = b"A\n"
packed = gzip.compress(tiny,mtime=0)短い本文をgzip化
print(len(tiny),len(packed)元と圧縮後のサイズ,gzip.decompress(packed)==tiny内容の復元を確認)
PYCODE元は2バイトでも、gzipのヘッダー・圧縮データ・検査値を付けると大きくなります。今回の繰り返し本文では圧縮効果がありましたが、データの長さや内容で効果は変わります。
まとめ
Content-LengthはそのHTTP本文のバイト数です。UTF-8の文字数や展開後サイズとは異なる場合があります。
Content-Typeは表現の形式、Content-Encodingは適用された圧縮などの方式を表します。
自動展開後のファイルと元のヘッダーを比較するときは、どの段階のサイズを扱っているかを揃えます。
操作履歴と解説を振り返れます。
シェル / curl・HTTP/1.1
curl -sS -D plain.headers -o plain.bin http://127.0.0.1:18080/unicode
cat plain.headersHTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 230 Vary: Accept-Encoding
python3 - <<'PYCODE'
from pathlib import Path
body = Path("plain.bin").read_bytes()
print("characters",len(body.decode("utf-8")),"bytes",len(body))
print("first line",repr(body.decode("utf-8").splitlines()[0]))
print("first bytes",body[:23].hex(" "))
PYCODEcharacters 130 bytes 230 first line '<p>あいうえお</p>' first bytes 3c 70 3e e3 81 82 e3 81 84 e3 81 86 e3 81 88 e3 81 8a 3c 2f 70 3e 0a
curl -sS --raw -H "Accept-Encoding: gzip" -D gzip.headers -o gzip.bin http://127.0.0.1:18080/unicode
cat gzip.headersHTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 47 Vary: Accept-Encoding Content-Encoding: gzip
python3 inspect_gzip.pyplain characters: 130 plain bytes: 230 gzip bytes: 47 header: 1f 8b 08 00 00 00 00 00 00 ff trailer: 20 12 b5 93 e6 00 00 00 CRC32: 93b51220 match: True ISIZE: 230 decoded bytes: 230 decoded equals plain: True
curl -sS --compressed -D decoded.headers -o decoded.txt http://127.0.0.1:18080/unicode
cat decoded.headersHTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 47 Vary: Accept-Encoding Content-Encoding: gzip
python3 - <<'PYCODE'
from pathlib import Path
plain = Path("plain.bin").read_bytes()
decoded = Path("decoded.txt").read_bytes()
packed = Path("gzip.bin").read_bytes()
print("plain",len(plain),"gzip",len(packed),"decoded",len(decoded))
print("same bytes",plain==decoded)
PYCODEplain 230 gzip 47 decoded 230 same bytes True
python3 - <<'PYCODE'
import gzip
tiny = b"A\n"
packed = gzip.compress(tiny,mtime=0)
print(len(tiny),len(packed),gzip.decompress(packed)==tiny)
PYCODE2 22 True
演習完了