[インデックス 13420] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージにおいて、ServeContent
関数がHTTPの複数バイト範囲リクエスト(Multiple Byte Ranges)をサポートするように拡張するものです。これにより、クライアントは単一のリクエストでファイル内の複数の非連続な部分を要求できるようになり、サーバーは multipart/byteranges
コンテンツタイプを使用してそれらの部分を効率的に提供します。
コミット
commit fa6f9b4a3e2c1cd5b3da7786250f3c49c1f40325
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Fri Jun 29 07:44:04 2012 -0700
net/http: support multiple byte ranges in ServeContent
Fixes #3784
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6351052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fa6f9b4a3e2c1cd5b3da7786250f3c49c1f40325
元コミット内容
このコミットは、net/http
パッケージの ServeContent
関数に、HTTPの複数バイト範囲リクエストを処理する機能を追加します。以前は単一のバイト範囲リクエストのみをサポートしていましたが、この変更により、RFC 2616で定義されている multipart/byteranges
コンテンツタイプを使用して、複数のバイト範囲を効率的に提供できるようになります。これにより、クライアントは一度のリクエストでファイルの複数の部分を取得でき、特に大きなファイルのダウンロードやストリーミングにおいて、ネットワーク効率が向上します。
変更の背景
この変更の背景には、Go言語の net/http
パッケージがHTTPのバイト範囲リクエストの完全な仕様をサポートしていなかったという問題があります。具体的には、Issue #3784で報告されたように、ServeContent
関数が単一のバイト範囲リクエストしか処理できず、複数のバイト範囲が指定された場合にはエラーを返していました。
HTTPのバイト範囲リクエストは、クライアントがファイルの特定の部分のみを要求することを可能にする重要な機能です。これは、部分的なダウンロード、中断されたダウンロードの再開、ビデオやオーディオのストリーミング、または大きなファイルの特定の部分へのアクセスなど、多くのシナリオで利用されます。
RFC 2616(HTTP/1.1の仕様)では、クライアントが複数のバイト範囲を要求した場合、サーバーは multipart/byteranges
コンテンツタイプを使用して、各範囲のデータを個別のパートとして含むレスポンスを返すことが規定されています。このコミットは、この仕様に準拠し、net/http
パッケージの堅牢性と互換性を向上させることを目的としています。
前提知識の解説
HTTPバイト範囲リクエスト (HTTP Byte Range Requests)
HTTPバイト範囲リクエストは、HTTP/1.1プロトコルの一部であり、クライアントがサーバーに対して、リソース(ファイルなど)の全体ではなく、その特定の部分(バイト範囲)のみを要求できるようにするメカニズムです。これは、Range
ヘッダーフィールドを使用して行われます。
Range
ヘッダー: クライアントがリクエストに含めるヘッダーで、要求するバイト範囲を指定します。- 例:
Range: bytes=0-499
(最初の500バイト) - 例:
Range: bytes=500-999
(500バイト目から999バイト目まで) - 例:
Range: bytes=-500
(最後の500バイト) - 例:
Range: bytes=500-
(500バイト目から最後まで) - 複数範囲の例:
Range: bytes=0-499, 1000-1499
(最初の500バイトと1000バイト目から1499バイト目まで)
- 例:
HTTPステータスコード
200 OK
: リクエストが成功し、リソース全体が返された場合。206 Partial Content
: クライアントのバイト範囲リクエストが成功し、リソースの部分が返された場合。416 Requested Range Not Satisfiable
: クライアントが要求したバイト範囲がリソースの範囲外である場合。
Content-Range
ヘッダー
サーバーが 206 Partial Content
レスポンスを返す際に含めるヘッダーで、返されるデータのバイト範囲とリソース全体のサイズを示します。
- 例:
Content-Range: bytes 0-499/10000
(10000バイトのリソースのうち、0-499バイト目を返している)
multipart/byteranges
コンテンツタイプ
クライアントが複数のバイト範囲を要求し、サーバーがそれらをすべて返す場合に使用されるMIMEタイプです。この場合、レスポンスボディは複数のパートに分割され、各パートが要求されたバイト範囲のデータを含みます。各パートは独自の Content-Type
ヘッダーと Content-Range
ヘッダーを持ちます。
- MIME Multipart: 複数の異なるデータタイプを単一のメッセージボディに結合するための標準的な方法です。各パートは境界文字列(boundary string)で区切られます。
multipart/byteranges
は、特にバイト範囲リクエストのために設計されたMIME Multipartのサブタイプです。
io.Pipe
と io.CopyN
io.Pipe()
: Go言語のio
パッケージで提供される関数で、メモリ内でパイプを作成します。これは、io.Reader
とio.Writer
のペアを返します。一方の端に書き込まれたデータは、もう一方の端から読み取ることができます。これは、ストリーミング処理や、データを生成するゴルーチンとデータを消費するゴルーチンを接続するのに便利です。io.CopyN(dst Writer, src Reader, n int64)
:src
からdst
へ最大n
バイトをコピーします。これは、特定のバイト数だけをコピーしたい場合に効率的です。
mime/multipart
パッケージ
Go言語の標準ライブラリで、MIME multipartメッセージの作成と解析をサポートします。このコミットでは、multipart.NewWriter
を使用して multipart/byteranges
レスポンスボディを構築しています。
技術的詳細
このコミットの主要な変更は、net/http/fs.go
内の serveContent
関数にあります。
-
複数範囲の検出と処理:
parseRange
関数が、Range
ヘッダーから複数のバイト範囲を正しく解析できるようになりました。len(ranges) > 1
の場合、サーバーはStatusPartialContent
(206) ステータスコードを返し、multipart/byteranges
コンテンツタイプを使用するように設定されます。
-
multipart/byteranges
レスポンスの構築:io.Pipe()
を使用して、sendContent
というio.Reader
が作成されます。これにより、レスポンスボディの書き込みと読み取りが並行して行われます。multipart.NewWriter(pw)
を使用して、multipart/byteranges
メッセージを構築するためのライターが作成されます。- 新しいゴルーチンが起動され、このゴルーチン内で各バイト範囲が処理されます。
- 各範囲に対して
mw.CreatePart()
が呼び出され、個別のMIMEパートが作成されます。 content.Seek()
を使用して、元のコンテンツの正しい開始位置にシークします。io.CopyN(part, content, ra.length)
を使用して、指定されたバイト範囲のデータがMIMEパートにコピーされます。- すべての範囲が処理された後、
mw.Close()
とpw.Close()
が呼び出され、パイプが閉じられます。
- 各範囲に対して
w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
により、レスポンスヘッダーに正しいコンテンツタイプと境界文字列が設定されます。
-
単一範囲の処理の改善:
- 単一のバイト範囲の場合の
Content-Range
ヘッダーの生成ロジックが、httpRange
構造体の新しいメソッドcontentRange()
にカプセル化されました。 - RFC 2616の規定に従い、単一範囲のリクエストに対しては
multipart/byteranges
を使用しないことが保証されます。
- 単一のバイト範囲の場合の
-
parseRange
関数の堅牢性向上:Range
ヘッダーの解析において、空白文字のトリミング (strings.TrimSpace
) が追加され、より柔軟な入力に対応できるようになりました。- 無効な範囲指定に対するエラーハンドリングが改善されました。
-
テストの追加と修正:
fs_test.go
に、複数バイト範囲リクエストのテストケースが追加されました。これらのテストは、multipart/byteranges
レスポンスが正しく構築され、各パートのデータとヘッダーが期待通りであることを検証します。range_test.go
に、parseRange
関数の新しいテストケースが追加され、様々な有効および無効な範囲文字列の解析が正しく行われることを確認します。
コアとなるコードの変更箇所
src/pkg/net/http/fs.go
--- a/src/pkg/net/http/fs.go
+++ b/src/pkg/net/http/fs.go
@@ -11,6 +11,8 @@ import (
"fmt"
"io"
"mime"
+ "mime/multipart"
+ "net/textproto"
"os"
"path"
"path/filepath"
@@ -123,8 +125,9 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
code := StatusOK
// If Content-Type isn't set, use the file's extension to find it.
- if w.Header().Get("Content-Type") == "" {
- ctype := mime.TypeByExtension(filepath.Ext(name))
+ ctype := w.Header().Get("Content-Type")
+ if ctype == "" {
+ ctype = mime.TypeByExtension(filepath.Ext(name))
if ctype == "" {
// read a chunk to decide between utf-8 text and binary
var buf [1024]byte
@@ -141,18 +144,27 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
}
// handle Content-Range header.
- // TODO(adg): handle multiple ranges
sendSize := size
+ var sendContent io.Reader = content
if size >= 0 {
ranges, err := parseRange(r.Header.Get("Range"), size)
- if err == nil && len(ranges) > 1 {
- err = errors.New("multiple ranges not supported")
- }
if err != nil {
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
return
}
- if len(ranges) == 1 {
+ switch {
+ case len(ranges) == 1:
+ // RFC 2616, Section 14.16:
+ // "When an HTTP message includes the content of a single
+ // range (for example, a response to a request for a
+ // single range, or to a request for a set of ranges
+ // that overlap without any holes), this content is
+ // transmitted with a Content-Range header, and a
+ // Content-Length header showing the number of bytes
+ // actually transferred.
+ // ...
+ // A response to a request for a single range MUST NOT
+ // be sent using the multipart/byteranges media type."
ra := ranges[0]
if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
@@ -160,7 +172,41 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
}
sendSize = ra.length
code = StatusPartialContent
- w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", ra.start, ra.start+ra.length-1, size))
+ w.Header().Set("Content-Range", ra.contentRange(size))
+ case len(ranges) > 1:
+ for _, ra := range ranges {
+ if ra.start > size {
+ Error(w, err.Error(), StatusRequestedRangeNotSatisfiable)
+ return
+ }
+ }
+ sendSize = rangesMIMESize(ranges, ctype, size)
+ code = StatusPartialContent
+
+ pr, pw := io.Pipe()
+ mw := multipart.NewWriter(pw)
+ w.Header().Set("Content-Type", "multipart/byteranges; boundary="+mw.Boundary())
+ sendContent = pr
+ defer pr.Close() // cause writing goroutine to fail and exit if CopyN doesn't finish.
+ go func() {
+ for _, ra := range ranges {
+ part, err := mw.CreatePart(ra.mimeHeader(ctype, size))
+ if err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+ if _, err := content.Seek(ra.start, os.SEEK_SET); err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+ if _, err := io.CopyN(part, content, ra.length); err != nil {
+ pw.CloseWithError(err)
+ return
+ }
+ }
+ mw.Close()
+ pw.Close()
+ }()
}
w.Header().Set("Accept-Ranges", "bytes")
@@ -172,11 +218,7 @@ func serveContent(w ResponseWriter, r *Request, name string, modtime time.Time,
w.WriteHeader(code)
if r.Method != "HEAD" {
- if sendSize == -1 {
- io.Copy(w, content)
- } else {
- io.CopyN(w, content, sendSize)
- }
+ io.CopyN(w, sendContent, sendSize)
}
}
@@ -314,6 +356,17 @@ type httpRange struct {
start, length int64
}
+func (r httpRange) contentRange(size int64) string {
+ return fmt.Sprintf("bytes %d-%d/%d", r.start, r.start+r.length-1, size)
+}
+
+func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader {
+ return textproto.MIMEHeader{
+ "Content-Range": {r.contentRange(size)},
+ "Content-Type": {contentType},
+ }
+}
+
// parseRange parses a Range header string as per RFC 2616.
func parseRange(s string, size int64) ([]httpRange, error) {
if s == "" {
@@ -325,11 +378,15 @@ func parseRange(s string, size int64) ([]httpRange, error) {
}
var ranges []httpRange
for _, ra := range strings.Split(s[len(b):], ",") {
+ ra = strings.TrimSpace(ra)
+ if ra == "" {
+ continue
+ }
i := strings.Index(ra, "-")
if i < 0 {
return nil, errors.New("invalid range")
}
- start, end := ra[:i], ra[i+1:]
+ start, end := strings.TrimSpace(ra[:i]), strings.TrimSpace(ra[i+1:])
var r httpRange
if start == "" {
// If no start is specified, end specifies the
@@ -367,3 +424,25 @@ func parseRange(s string, size int64) ([]httpRange, error) {
}
return ranges, nil
}
+
+// countingWriter counts how many bytes have been written to it.
+type countingWriter int64
+
+func (w *countingWriter) Write(p []byte) (n int, err error) {
+ *w += countingWriter(len(p))
+ return len(p), nil
+}
+
+// rangesMIMESize returns the nunber of bytes it takes to encode the
+// provided ranges as a multipart response.
+func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64) {
+ var w countingWriter
+ mw := multipart.NewWriter(&w)
+ for _, ra := range ranges {
+ mw.CreatePart(ra.mimeHeader(contentType, contentSize))
+ encSize += ra.length
+ }
+ mw.Close()
+ encSize += int64(w)
+ return
+}
src/pkg/net/http/fs_test.go
このファイルでは、ServeFileRangeTests
に複数範囲のテストケースが追加され、multipart/byteranges
レスポンスの検証ロジックが実装されています。
src/pkg/net/http/range_test.go
このファイルでは、parseRange
関数のテストケースが拡張され、空白文字を含む範囲指定や、より複雑な複数範囲のシナリオがカバーされています。
コアとなるコードの解説
serveContent
関数内の変更
import
の追加:mime/multipart
とnet/textproto
がインポートされ、MIME multipartメッセージの構築とヘッダー操作が可能になりました。ctype
の初期化:w.Header().Get("Content-Type")
を先に取得し、もし空であればファイル拡張子から推測するように変更されました。これにより、既にContent-Type
が設定されている場合はそれを尊重します。sendContent
の導入:io.Reader
型のsendContent
変数が導入され、単一範囲または複数範囲のどちらの場合でも、最終的にこのリーダーからデータがコピーされるように抽象化されました。switch
文による範囲処理の分岐:len(ranges) == 1
の場合: 以前と同様に単一範囲として処理されますが、Content-Range
ヘッダーの生成にra.contentRange(size)
メソッドが使用されます。RFC 2616の規定に従い、単一範囲ではmultipart/byteranges
を使用しないことがコメントで明記されています。len(ranges) > 1
の場合:io.Pipe()
を使用してパイプを作成し、pr
(Reader) をsendContent
に割り当てます。multipart.NewWriter(pw)
でmw
を作成し、レスポンスのContent-Type
をmultipart/byteranges
に設定します。- 新しいゴルーチンを起動し、その中で各範囲をループ処理します。
mw.CreatePart(ra.mimeHeader(ctype, size))
で各範囲のMIMEパートを作成します。mimeHeader
は新しいヘルパーメソッドで、パートのContent-Range
とContent-Type
ヘッダーを生成します。content.Seek(ra.start, os.SEEK_SET)
で元のコンテンツの読み取り位置を調整します。io.CopyN(part, content, ra.length)
で指定されたバイト数のデータをパートにコピーします。- エラーが発生した場合は
pw.CloseWithError(err)
でパイプを閉じ、ゴルーチンを終了させます。
- すべてのパートが書き込まれた後、
mw.Close()
とpw.Close()
でライターとパイプを閉じます。
io.CopyN(w, sendContent, sendSize)
: 最終的に、sendContent
からw
(ResponseWriter) へデータがコピーされます。これにより、単一範囲でも複数範囲でも統一された方法でレスポンスボディが送信されます。
httpRange
構造体と新しいメソッド
func (r httpRange) contentRange(size int64) string
:Content-Range
ヘッダーの文字列を生成するためのヘルパーメソッドです。これにより、コードの重複が排除され、可読性が向上します。
func (r httpRange) mimeHeader(contentType string, size int64) textproto.MIMEHeader
:multipart/byteranges
の各パートのヘッダー(Content-Range
とContent-Type
)を生成するためのヘルパーメソッドです。
parseRange
関数の変更
strings.TrimSpace(ra)
: 各範囲文字列の先頭と末尾の空白を削除することで、より柔軟な入力に対応できるようになりました。strings.TrimSpace(ra[:i])
,strings.TrimSpace(ra[i+1:])
: 開始と終了のバイトオフセットを解析する際にも空白をトリミングします。
countingWriter
と rangesMIMESize
type countingWriter int64
:io.Writer
インターフェースを実装し、書き込まれたバイト数をカウントするシンプルな型です。
func rangesMIMESize(ranges []httpRange, contentType string, contentSize int64) (encSize int64)
:multipart/byteranges
レスポンス全体のエンコードされたサイズ(各パートのデータサイズとMIME境界、ヘッダーのオーバーヘッドを含む)を計算するためのヘルパー関数です。これは、Content-Length
ヘッダーの値を正確に設定するために使用されます。countingWriter
を利用して、multipart.NewWriter
が書き込む境界やヘッダーのサイズを測定しています。
これらの変更により、net/http
パッケージはHTTPの複数バイト範囲リクエストを完全にサポートし、より堅牢で標準に準拠したファイルサービング機能を提供できるようになりました。
関連リンク
- RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1 (Section 14.16 Content-Range, Section 19.2.1 Multipart/Byte Ranges)
- Go issue #3784: net/http: ServeContent should support multiple byte ranges
参考にした情報源リンク
- 上記のRFC 2616のドキュメント
- Go言語の
net/http
パッケージのドキュメント - Go言語の
mime/multipart
パッケージのドキュメント - Go言語の
io
パッケージのドキュメント - GitHub上のGo言語リポジトリの該当コミットと関連するIssue
- HTTPバイト範囲リクエストに関する一般的なウェブ上の解説記事