Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 14861] ファイルの概要

このコミットは、Goのnet/httpパッケージにおいて、HTTPレスポンスのチャンク処理の前にデータをバッファリングするメカニズムを導入するものです。これにより、特に小さなレスポンスの場合に、Content-Lengthヘッダを正確に計算して設定できるようになり、パフォーマンスの向上とHTTP/1.0クライアントとの互換性改善が図られています。また、Content-Typeの自動検出(スニッフィング)も改善されています。

コミット

commit bef4cb475c0638ab5193f75f2683b35a7c7f6547
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Jan 11 10:03:43 2013 -0800

    net/http: buffer before chunking
    
    This introduces a buffer between writing from a handler and
    writing chunks.  Further, it delays writing the header until
    the first full chunk is ready.  In the case where the first
    full chunk is also the final chunk (for small responses), that
    means we can also compute a Content-Length, which is a nice
    side effect for certain benchmarks.
    
    Fixes #2357
    
    R=golang-dev, dave, minux.ma, rsc, adg, balasanjay
    CC=golang-dev
    https://golang.org/cl/6964043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/bef4cb475c0638ab5193f75f2683b35a7c7f6547

元コミット内容

ハンドラからの書き込みとチャンクの書き込みの間にバッファを導入します。さらに、最初の完全なチャンクが準備できるまでヘッダの書き込みを遅延させます。最初の完全なチャンクが最終チャンクでもある場合(小さなレスポンスの場合)、Content-Lengthを計算できるようになり、特定のベンチマークにとって良い副次効果となります。

Issue #2357 を修正します。

変更の背景

この変更の主な背景には、Goのnet/httpパッケージにおけるHTTPレスポンスのチャンク処理の挙動に関する課題がありました。具体的には、Issue #2357で報告された問題に対応しています。

従来のnet/httpのチャンク処理では、ハンドラがデータを書き込むとすぐにチャンクヘッダが付与されて送信される傾向がありました。この挙動は、特に小さなレスポンスの場合にいくつかの問題を引き起こしていました。

  1. Content-Lengthの欠如: HTTP/1.1では、レスポンスボディのサイズが事前に不明な場合に「チャンク転送エンコーディング(Chunked Transfer Encoding)」を使用します。しかし、レスポンスボディが非常に小さい場合でもチャンク形式で送信されることがあり、この場合Content-Lengthヘッダは付与されません。Content-Lengthヘッダは、クライアントがレスポンスボディの終わりを正確に知るために重要であり、特にHTTP/1.0のkeep-alive接続の維持や、一部のプロキシやキャッシュの動作に影響を与える可能性がありました。
  2. パフォーマンスのオーバーヘッド: 小さなデータ片が頻繁に書き込まれると、そのたびにチャンクヘッダが付与され、ネットワーク上で多くの小さなパケットが送信されることになります。これは、チャンクヘッダ自体のオーバーヘッドと、TCP/IPレベルでの効率の低下を招き、パフォーマンスに悪影響を与える可能性がありました。
  3. Content-Typeスニッフィングの課題: net/httpは、Content-Typeヘッダが明示的に設定されていない場合に、レスポンスボディの最初の数バイトを「スニッフィング」して適切なContent-Typeを推測する機能を持っています。しかし、チャンク処理がすぐに開始されると、スニッフィングに必要な十分なデータがバッファリングされる前にヘッダが送信されてしまい、正確なContent-Typeの検出が困難になることがありました。

このコミットは、これらの問題を解決するために、ハンドラからの書き込みと実際のチャンク送信の間にバッファを導入し、ヘッダの送信を遅延させることで、より効率的で柔軟なレスポンス処理を実現することを目的としています。

前提知識の解説

HTTPチャンク転送エンコーディング (Chunked Transfer Encoding)

HTTP/1.1で導入された転送エンコーディングの一種で、メッセージボディの長さを事前に知らなくても、動的に生成されるコンテンツやストリーミングデータなどを送信できるようにするメカニズムです。

  • 仕組み: メッセージボディは、複数の「チャンク」に分割されて送信されます。各チャンクは、そのチャンクのデータ長(16進数)とCRLF(改行コード)で始まり、その後に実際のデータとCRLFが続きます。メッセージの終わりは、長さが0のチャンク(0\r\n\r\n)で示されます。
  • 利点:
    • サーバーは、レスポンスボディ全体のサイズが確定するのを待たずに、データの送信を開始できます。
    • 大きなファイルをストリーミングしたり、リアルタイムでデータを生成したりする際に特に有用です。
  • Content-Lengthとの関係: Transfer-Encoding: chunkedヘッダが存在する場合、Content-Lengthヘッダは存在してはなりません。これらは互いに排他的なメカニズムです。

HTTP Content-Length ヘッダ

HTTPメッセージボディの正確なバイト長を示すヘッダです。

  • 仕組み: サーバーがレスポンスボディの全長を事前に知っている場合に設定されます。クライアントは、このヘッダの値に基づいて、メッセージボディの終わりを判断します。
  • 利点:
    • クライアントは、レスポンスボディの受信完了を正確に知ることができます。
    • HTTP/1.0のkeep-alive接続を維持するために重要です。
    • プロキシやキャッシュがコンテンツを効率的に処理するために利用されます。

HTTP Content-Type スニッフィング (DetectContentType)

Content-Typeヘッダがサーバーから提供されない、または不正確な場合に、WebブラウザやHTTPクライアントがレスポンスボディの内容を検査して、そのメディアタイプ(MIMEタイプ)を推測するプロセスです。

  • 仕組み: 通常、レスポンスボディの最初の数バイト(Goのnet/httpではデフォルトで512バイト)を読み込み、既知のファイルシグネチャやパターン(例: HTMLの<html>タグ、JPEGのマジックナンバーなど)と照合して、適切なContent-Typeを決定します。
  • Goのnet/httpにおけるDetectContentType: Goのnet/httpパッケージには、このスニッフィング機能を提供するDetectContentType関数があります。これは、Content-Typeヘッダが設定されていない場合に自動的に呼び出され、レスポンスボディの最初の部分に基づいて適切なMIMEタイプを推測します。
  • 課題: スニッフィングは便利ですが、セキュリティ上の脆弱性(MIMEタイプ混同攻撃など)を引き起こす可能性もあります。そのため、サーバーは常に正確なContent-Typeヘッダを送信し、可能であればX-Content-Type-Options: nosniffヘッダを使用してブラウザのスニッフィングを無効にすることが推奨されます。

Go net/http パッケージの役割

Goの標準ライブラリであるnet/httpパッケージは、HTTPクライアントとサーバーの実装を提供します。このパッケージは、HTTPプロトコルの詳細(ヘッダの解析、ボディの読み書き、チャンク処理など)を抽象化し、開発者が高レベルなAPIでWebアプリケーションを構築できるようにします。

  • http.ResponseWriter: HTTPレスポンスを書き込むためのインターフェースです。ハンドラはこのインターフェースを通じて、ステータスコード、ヘッダ、レスポンスボディをクライアントに送信します。
  • 自動チャンク処理: net/httpサーバーは、Content-Lengthヘッダが設定されていない場合に、自動的にチャンク転送エンコーディングを使用してレスポンスを送信します。

技術的詳細

このコミットは、net/httpパッケージのHTTPレスポンス処理フローを大幅に再構築しています。主な変更点は、ハンドラからの書き込みと実際のネットワークへの書き込みの間に新しいバッファリング層とchunkWriter構造体を導入したことです。

新しい書き込みフロー

変更後のレスポンス書き込みフローは以下のようになります。

  1. *response (ResponseWriter): ハンドラがhttp.ResponseWriterインターフェースを通じてデータを書き込みます。
  2. (*response).w (*bufio.Writer): *responseは、bufferBeforeChunkingSize(デフォルト2048バイト)のバッファを持つ*bufio.Writerをラップします。ハンドラからの最初の書き込みは、まずこのバッファに蓄積されます。
  3. chunkWriter: (*response).wは、その出力先としてchunkWriterをラップしています。chunkWriterは、ヘッダの最終決定(Content-LengthContent-Typeの設定)と、必要に応じたチャンクヘッダの書き込みを担当します。
  4. conn.buf (bufio.Writer): chunkWriterは、最終的に接続のバッファ(conn.buf、デフォルト4KB)に書き込みます。
  5. rwc (net.Conn): conn.bufは、実際のネットワーク接続(net.Conn)にデータをフラッシュします。

chunkWriterの導入

  • chunkWriterは、*response構造体からチャンク処理とヘッダの最終決定ロジックを分離するために導入されました。
  • chunkWriterは、res(関連する*responseへのポインタ)、headerresponse.handlerHeaderのディープコピー)、wroteHeader(ヘッダが送信されたかを示すフラグ)、chunking(チャンク転送エンコーディングを使用するかどうか)のフィールドを持ちます。

ヘッダの遅延書き込みとContent-Lengthの計算

  • chunkWriter.Write(p []byte)メソッドが呼び出された際、まだヘッダが書き込まれていない場合(!cw.wroteHeader)、cw.writeHeader(p)が呼び出されます。
  • cw.writeHeader(p)は、以下の重要な処理を行います。
    • cw.wroteHeader = trueを設定し、ヘッダが論理的に書き込まれたことを示します。
    • response.handlerHeaderのディープコピーをcw.headerに保存します。これにより、ハンドラがWriteHeader呼び出し後もヘッダを変更しても、既に送信されたヘッダには影響しません。
    • Content-Lengthの計算: ハンドラが終了しており(w.handlerDoneがtrue)、かつContent-Lengthヘッダが設定されておらず、かつ最初の書き込み(p)が空でない場合、pの長さに基づいてContent-Lengthを設定します。これは、特に小さなレスポンスの場合に、チャンクではなく固定長でレスポンスを送信できるようにするための重要な改善です。
    • Content-Typeのスニッフィング: Content-Typeヘッダが設定されていない場合、DetectContentType(p)を使用してpの内容からContent-Typeを推測し、ヘッダに設定します。
    • 最終的なヘッダ(ステータスライン、各種ヘッダフィールド、CRLF)をw.conn.bufに書き込みます。

Content-Typeスニッフィングの改善

  • 以前は、response構造体にneedSniffフラグがあり、sniff()メソッドが別途呼び出されていました。
  • この変更により、chunkWriter.writeHeader内でDetectContentType(p)が直接呼び出されるようになり、最初のデータチャンクが利用可能になった時点でContent-Typeが決定されます。これにより、スニッフィングがより確実に行われるようになります。

バッファリングの導入

  • response構造体は、*bufio.Writerであるwフィールドを持つようになりました。このwは、chunkWriterをラップし、bufferBeforeChunkingSize(2048バイト)のバッファサイズで初期化されます。
  • ハンドラからの書き込みは、まずこのwにバッファリングされます。バッファが満たされるか、Flush()が呼び出されるか、ハンドラが終了するまで、実際のネットワークへの書き込みは遅延されます。
  • これにより、小さな書き込みが結合され、チャンクヘッダの数が減り、ネットワーク効率が向上します。

ReadFromメソッドの変更

  • ReadFromメソッドも、新しいバッファリングとヘッダ遅延書き込みのロジックに合わせて調整されました。
  • needsSniff()がtrueの場合、sniffLen(512バイト)までのデータをio.LimitReaderで読み込み、Content-Typeスニッフィングを可能にします。
  • その後、w.w.Flush()w.cw.flush()を呼び出して、バッファリングされたデータとヘッダをフラッシュします。
  • chunkingモードでない場合、かつio.ReaderFromインターフェースを実装している基盤の接続がある場合、直接ReadFromを使用して効率的なデータ転送を行います。

finishRequestFlushの変更

  • finishRequestメソッドは、ハンドラが終了したことを示すw.handlerDone = trueを設定し、w.w.Flush()w.cw.close()を呼び出して、残りのバッファをフラッシュし、チャンク処理を終了させます(0バイトチャンクの送信など)。
  • Flushメソッドも、w.w.Flush()w.cw.flush()を呼び出すように変更され、バッファリングされたデータを強制的にフラッシュします。

これらの変更により、net/httpサーバーは、より効率的にレスポンスを処理し、特に小さなレスポンスや動的に生成されるコンテンツにおいて、Content-Lengthの利用可能性とパフォーマンスを向上させることができました。

コアとなるコードの変更箇所

このコミットは、主に以下のファイルに影響を与えています。

  • src/pkg/net/http/header.go
  • src/pkg/net/http/serve_test.go
  • src/pkg/net/http/server.go

src/pkg/net/http/header.go

  • func (h Header) clone() Header メソッドが追加されました。これは、Headerマップのディープコピーを作成するために使用されます。responsehandlerHeaderchunkWriterに渡される際に、ハンドラがヘッダを後から変更しても影響が出ないようにするために導入されました。

src/pkg/net/http/serve_test.go

  • 既存のテストが、新しいバッファリングとチャンク処理の挙動に合わせて修正されています。
  • TestServerBufferedChunkingテストが有効化されました。このテストは、以前はIssue 2357のためにスキップされていましたが、今回の変更によって修正されたため、再度実行されるようになりました。このテストは、1バイトずつ書き込まれるチャンクレスポンスが、チャンクヘッダが付与される前にバッファリングされることを検証します。
  • w.(Flusher).Flush()の呼び出しが追加され、ヘッダが強制的に送信されるシナリオをテストしています。

src/pkg/net/http/server.go

このファイルが最も広範な変更を受けています。

  • conn構造体: body []byteフィールドが削除されました。これは、以前のContent-Typeスニッフィングのためのバッファでしたが、新しいchunkWriterresponse.wによるバッファリングに置き換えられました。
  • bufferBeforeChunkingSize定数: const bufferBeforeChunkingSize = 2048が追加されました。これは、チャンク処理の前にデータをバッファリングするサイズを定義します。
  • chunkWriter構造体: 新しい構造体chunkWriterが定義されました。
    • res *response: 関連するresponseへのポインタ。
    • header Header: response.handlerHeaderのディープコピー。
    • wroteHeader bool: ヘッダが送信されたかどうかのフラグ。
    • chunking bool: チャンク転送エンコーディングを使用するかどうかのフラグ。
  • chunkWriterのメソッド:
    • Write(p []byte) (n int, err error): データを書き込み、必要に応じてチャンクヘッダを追加します。ヘッダがまだ書き込まれていない場合はwriteHeaderを呼び出します。
    • flush(): ヘッダがまだ書き込まれていない場合はwriteHeaderを呼び出し、conn.bufをフラッシュします。
    • close(): ヘッダがまだ書き込まれていない場合はwriteHeaderを呼び出し、チャンク処理を終了させるための0バイトチャンクを書き込みます。
  • response構造体:
    • wroteHeaderフィールドの意味が「論理的にヘッダが書き込まれた」に変更されました。
    • chunkingフィールドが削除され、chunkWriterに移されました。
    • headerフィールドがhandlerHeaderにリネームされ、ハンドラがアクセスするヘッダを表すようになりました。
    • w *bufio.Writer: 新しいバッファリング層として追加されました。chunkWriterをラップします。
    • cw *chunkWriter: 新しいchunkWriterインスタンスへのポインタ。
    • handlerDone bool: ハンドラが終了したかどうかを示すフラグ。
  • responseのメソッド:
    • Header() Header: w.handlerHeaderを返すように変更されました。
    • WriteHeader(code int): ヘッダの最終決定ロジックがchunkWriter.writeHeaderに移譲されました。ここでは、ステータスコードの設定と、handlerHeaderのクローンをcw.headerに設定する処理が行われます。
    • ReadFrom(src io.Reader) (n int64, err error): Content-Typeスニッフィングとバッファリングの新しいロジックに合わせて大幅に書き換えられました。
    • Write(data []byte) (n int, err error): 実際の書き込み処理がw.w.Write(data)に委譲されるようになりました。これにより、ハンドラからの書き込みはまずresponse.wのバッファに蓄積されます。
    • finishRequest(): ハンドラ終了時の処理が変更され、w.handlerDone = trueを設定し、w.w.Flush()w.cw.close()を呼び出すようになりました。
    • Flush(): w.w.Flush()w.cw.flush()を呼び出すように変更されました。
    • Hijack(): w.cw.flush()を呼び出すロジックが追加され、ハイジャック前にバッファがフラッシュされるようにしました。
  • writeHeader関数の削除: 以前のresponse.writeHeader関数は削除され、そのロジックはchunkWriter.writeHeaderメソッドに統合されました。
  • sniff関数の削除: 以前のresponse.sniff関数は削除され、そのロジックはchunkWriter.writeHeader内でDetectContentTypeを直接呼び出す形に統合されました。

これらの変更は、HTTPレスポンスの生成と送信の内部メカニズムを根本的に変更し、より堅牢で効率的な処理を実現しています。

コアとなるコードの解説

このコミットの核心は、net/httpサーバーがHTTPレスポンスをクライアントに送信する方法を根本的に変更した点にあります。特に、chunkWriter構造体の導入と、response構造体におけるバッファリング層の再構築が重要です。

chunkWriter構造体とその役割

chunkWriterは、HTTPレスポンスのヘッダの最終決定と、チャンク転送エンコーディングの管理を担当する新しいコンポーネントです。

  • ヘッダの最終決定: chunkWriter.writeHeader(p []byte)メソッドがその主要な役割を担います。
    • このメソッドは、response.WriteHeaderが呼び出された後、またはハンドラが最初のデータを書き込んだ際に、一度だけ呼び出されます。
    • ここで、Content-Lengthヘッダがまだ設定されておらず、かつハンドラが既に終了している(w.handlerDoneがtrue)場合、最初のデータチャンクpの長さに基づいてContent-Lengthを設定します。これにより、小さなレスポンスでもContent-Lengthが付与される可能性が高まります。
    • Content-Typeヘッダが設定されていない場合、DetectContentType(p)を使用してpの内容からContent-Typeを推測し、ヘッダに設定します。
    • Transfer-Encoding: chunkedヘッダの追加や、Connection: closeヘッダの管理など、HTTPプロトコルに準拠したヘッダの調整を行います。
    • 最終的に、ステータスラインとすべてのヘッダフィールドをw.conn.buf(実際のネットワークバッファ)に書き込みます。
  • チャンク処理の管理:
    • chunkWriter.Write(p []byte)メソッドは、chunkingフラグがtrueの場合、書き込まれるデータpの前にチャンクサイズ(16進数)とCRLFを付与し、データpの後にCRLFを付与します。
    • chunkWriter.close()メソッドは、チャンク処理を終了させるために、長さ0の最終チャンク(0\r\n\r\n)を書き込みます。

response構造体におけるバッファリング層

response構造体は、http.ResponseWriterインターフェースの実装であり、ハンドラからの書き込みを受け取ります。

  • w *bufio.Writer: response構造体には、wという新しいフィールドが追加されました。これは*bufio.Writerのインスタンスであり、その出力先はchunkWriterです。
    • bufio.NewWriterSize(w.cw, bufferBeforeChunkingSize)で初期化され、bufferBeforeChunkingSize(2048バイト)の内部バッファを持ちます。
    • ハンドラがresponse.Write(data []byte)を呼び出すと、データはまずこのwの内部バッファに蓄積されます。
    • このバッファリングにより、小さな書き込みが結合され、chunkWriterに渡されるデータの塊が大きくなります。これにより、チャンクヘッダの数が減り、ネットワーク効率が向上します。
  • ヘッダの遅延: response.Writeが呼び出されても、response.wのバッファが満たされるか、response.Flush()が呼び出されるか、ハンドラが終了するまで、実際のHTTPヘッダの送信は遅延されます。この遅延が、Content-LengthContent-Typeをより正確に決定する機会を提供します。

処理の流れの要約

  1. ハンドラがhttp.ResponseWriter.Write()を呼び出す。
  2. データはresponse.wbufio.Writer)の内部バッファに蓄積される。
  3. response.wのバッファが満たされるか、Flush()が呼び出されるか、ハンドラが終了すると、バッファの内容がchunkWriter.Write()に渡される。
  4. chunkWriter.Write()は、まだヘッダが送信されていない場合、chunkWriter.writeHeader()を呼び出してヘッダを最終決定し、Content-LengthContent-Typeを設定する。
  5. chunkWriter.Write()は、必要に応じてチャンクヘッダを付与し、データをconn.buf(実際のネットワークバッファ)に書き込む。
  6. conn.bufがフラッシュされると、データがネットワーク経由でクライアントに送信される。

この新しいアーキテクチャにより、Goのnet/httpサーバーは、より柔軟かつ効率的にHTTPレスポンスを処理できるようになり、特に動的に生成されるコンテンツや小さなレスポンスにおいて、パフォーマンスとプロトコル準拠が改善されました。

関連リンク

参考にした情報源リンク