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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージ内の server.go ファイルに対して行われた変更を記録しています。具体的には、HTTPサーバーが新しい接続を処理する serve() メソッドにおける接続クローズのロジックを簡素化することを目的としています。以前のコミット (6971049) のフォローアップとして、エラーハンドリングと接続クローズの処理がより効率的かつ堅牢になるように改善されています。

コミット

net/http: simplify serve() connection close

Followup to 6971049.

R=bradfitz
CC=golang-dev
https://golang.org/cl/6970049

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

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

元コミット内容

commit c850d0f34a7db9f6df54c1ca99e14e19859baaa0
Author: Dave Cheney <dave@cheney.net>
Date:   Fri Dec 21 15:14:38 2012 +1100

    net/http: simplify serve() connection close
    
    Followup to 6971049.
    
    R=bradfitz
    CC=golang-dev
    https://golang.org/cl/6970049

変更の背景

このコミットは、Go言語の net/http パッケージにおけるHTTPサーバーの接続処理の改善を目的としています。コミットメッセージに「Followup to 6971049」とあるように、以前のコミット 6971049 で導入された変更に関連しています。

元の serve() メソッドでは、接続のクローズ処理が defer c.close() と、panic が発生した場合の recover ブロック内の両方で行われていました。これは冗長であり、特に panic が発生しなかった場合には c.close() が二重に呼び出される可能性がありました(ただし、c.close() は冪等であるため、直接的な問題にはならないかもしれませんが、コードの意図が不明瞭になります)。

また、panic 発生時の接続クローズ処理は、c.rwc != nil という条件で c.rwc.Close() を呼び出していました。しかし、c.rwc (read-write closer) が nil でない場合でも、接続がハイジャックされている(c.hijacked()true)場合には、サーバー側で接続をクローズすべきではありません。ハイジャックされた接続は、アプリケーションがそのライフサイクルを完全に制御するため、サーバーが勝手にクローズすると予期せぬ動作を引き起こす可能性があります。

このコミットは、これらの問題を解決し、serve() メソッドの接続クローズロジックを簡素化し、より堅牢にすることを目的としています。具体的には、defer c.close() を削除し、panic 発生時および tls.Handshake() エラー発生時にのみ c.close() を呼び出すように変更することで、接続クローズの責任を一元化し、ハイジャックされた接続の扱いを明確にしています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念と net/http パッケージの基本的な知識が必要です。

  1. net/http パッケージ: Go言語でHTTPクライアントおよびサーバーを実装するための標準ライブラリです。HTTPリクエストのルーティング、レスポンスの生成、ミドルウェアの適用など、Webアプリケーション開発に必要な機能を提供します。
  2. http.Serverhttp.conn:
    • http.Server: HTTPサーバー全体を管理する構造体です。リスニングアドレス、ハンドラー、タイムアウトなどの設定を持ちます。
    • http.conn: 個々のクライアント接続を表す内部的な構造体です。この構造体が、クライアントからのリクエストを読み込み、レスポンスを書き込むための低レベルな処理を担当します。
  3. serve() メソッド: http.conn 型のメソッドで、新しいクライアント接続が確立された際に呼び出され、その接続を通じてHTTPリクエストを処理し、レスポンスを返す一連の処理を管理します。このメソッドは通常、新しいゴルーチンで実行されます。
  4. defer ステートメント: Go言語のキーワードで、defer に続く関数呼び出しを、その関数がリターンする直前(またはパニックから回復する直前)に実行するようにスケジュールします。リソースの解放(ファイルクローズ、ロック解除など)によく使用されます。
  5. panicrecover():
    • panic: プログラムの実行を停止させるGo言語の組み込み関数です。通常、回復不可能なエラーや予期せぬ状況が発生した場合に呼び出されます。
    • recover(): panic から回復するための組み込み関数です。defer された関数内で呼び出された場合、現在のゴルーチンで発生した panic の値を捕捉し、そのゴルーチンの実行を継続させることができます。recover()nil 以外の値を返した場合、panic が発生したことを意味します。
  6. tls.ConnHandshake():
    • tls.Conn: TLS (Transport Layer Security) プロトコルを介して通信を行うネットワーク接続を表す構造体です。HTTPS通信などで使用されます。
    • Handshake(): TLS接続の確立プロセス(ハンドシェイク)を実行するメソッドです。クライアントとサーバー間で暗号化パラメータのネゴシエーションを行います。
  7. c.hijacked(): http.conn のメソッドで、現在の接続がHTTPサーバーによって「ハイジャック」されているかどうかを示します。接続がハイジャックされると、サーバーはそれ以降の接続の管理(クローズなど)を行わず、アプリケーションが直接TCP接続を制御するようになります。これはWebSocketのようなプロトコルでよく使用されます。

技術的詳細

このコミットの技術的な核心は、net/http/server.go 内の (*conn).serve() メソッドにおける接続クローズロジックの変更にあります。

変更前は、serve() メソッドの冒頭に以下の defer ステートメントがありました。

defer func() {
    err := recover()
    if err == nil {
        return
    }

    const size = 4096
    buf := make([]byte, size)
    buf = buf[:runtime.Stack(buf, false)]
    log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)

    if c.rwc != nil { // may be nil if connection hijacked
        c.rwc.Close()
    }
}()
defer c.close() // ここが削除される

このコードでは、defer c.close() が常に接続の最後に呼び出されるようにスケジュールされていました。また、panic が発生した場合に備えて recover() を含む defer ブロックがあり、その中で c.rwc.Close() が呼び出されていました。

変更後のコードは以下のようになります。

defer func() {
    if err := recover(); err != nil { // recover() の結果を直接評価
        const size = 4096
        buf := make([]byte, size)
        buf = buf[:runtime.Stack(buf, false)]
        log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
    }
    if !c.hijacked() { // ハイジャックされていない場合のみ c.close() を呼び出す
        c.close()
    }
}()
// defer c.close() は削除された

// ... (中略) ...

if tlsConn, ok := c.rwc.(*tls.Conn); ok {
    if err := tlsConn.Handshake(); err != nil {
        // c.close() は削除された
        return
    }
    // ...
}

主な変更点は以下の通りです。

  1. defer c.close() の削除: serve() メソッドの冒頭にあった defer c.close() が削除されました。これにより、接続クローズの責任が panic ハンドリングの defer ブロックに一元化されます。
  2. panic ハンドリングの簡素化と改善:
    • recover() の結果を直接 if ステートメントで評価するように変更され、コードがより簡潔になりました。
    • panic が発生した場合の接続クローズロジックが if !c.hijacked() { c.close() } に変更されました。これにより、接続がハイジャックされている場合にはサーバーが接続をクローズしないという、より正確な動作が保証されます。以前は c.rwc != nil という条件でしたが、これはハイジャックされた接続でも c.rwcnil でない場合があるため、不正確でした。
  3. tls.Handshake() エラー時の c.close() 削除: tls.Handshake() がエラーを返した場合の c.close() 呼び出しが削除されました。これは、panic ハンドリングの defer ブロックが最終的に c.close() を呼び出すため、ここでの明示的なクローズは不要になったためです。

これらの変更により、serve() メソッドは以下の点で改善されました。

  • 簡潔性: 接続クローズのロジックが重複なく、一箇所に集約されました。
  • 堅牢性: panic 発生時や tls.Handshake() エラー時でも、接続がハイジャックされているかどうかを適切に判断し、サーバーが不必要に接続をクローズするのを防ぎます。
  • 正確性: c.hijacked() のチェックにより、ハイジャックされた接続のライフサイクル管理がアプリケーションに委ねられるというHTTPサーバーの設計原則がより厳密に守られます。

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

--- a/src/pkg/net/http/server.go
+++ b/src/pkg/net/http/server.go
@@ -702,25 +702,19 @@ func (c *conn) closeWriteAndWait() {
 // Serve a new connection.
 func (c *conn) serve() {
 	defer func() {
-		err := recover()
-		if err == nil {
-			return
+		if err := recover(); err != nil {
+			const size = 4096
+			buf := make([]byte, size)
+			buf = buf[:runtime.Stack(buf, false)]
+			log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
 		}
-
-		const size = 4096
-		buf := make([]byte, size)
-		buf = buf[:runtime.Stack(buf, false)]
-		log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
-
-		if c.rwc != nil { // may be nil if connection hijacked
-			c.rwc.Close()
+		if !c.hijacked() {
+			c.close()
 		}
 	}()
-	defer c.close()
 
 	if tlsConn, ok := c.rwc.(*tls.Conn); ok {
 		if err := tlsConn.Handshake(); err != nil {
-\t\t\tc.close()\n \t\t\treturn
 \t\t}\n \t\tc.tlsState = new(tls.ConnectionState)\n

コアとなるコードの解説

上記の diff は、src/pkg/net/http/server.go ファイル内の (*conn).serve() メソッドに対する変更を示しています。

  1. defer ブロックの変更:
    • 削除された部分:
      err := recover()
      if err == nil {
          return
      }
      // ... (panic ログ出力) ...
      if c.rwc != nil { // may be nil if connection hijacked
          c.rwc.Close()
      }
      
      この部分では、まず recover() を呼び出し、panic が発生していなければ早期リターンしていました。panic が発生した場合はスタックトレースをログに出力し、c.rwcnil でない場合に c.rwc.Close() を呼び出して接続をクローズしていました。この c.rwc != nil のチェックは、接続がハイジャックされている場合でも c.rwcnil でない可能性があるため、不正確でした。
    • 追加された部分:
      if err := recover(); err != nil {
          const size = 4096
          buf := make([]byte, size)
          buf = buf[:runtime.Stack(buf, false)]
          log.Printf("http: panic serving %v: %v\n%s", c.remoteAddr, err, buf)
      }
      if !c.hijacked() {
          c.close()
      }
      
      新しいコードでは、recover() の結果を直接 if ステートメントで評価し、panic が発生した場合のみログ出力を行います。そして、panic の有無にかかわらず、if !c.hijacked() という条件で c.close() を呼び出すようになりました。これにより、接続がハイジャックされていない場合にのみ c.close() が実行されることが保証され、より正確な接続管理が可能になります。c.close() メソッドは、内部で c.rwc.Close() を呼び出す責任を持ちます。
  2. defer c.close() の削除:
    • defer func() { ... }() の直後にあった defer c.close() の行が完全に削除されました。これにより、接続クローズのロジックが panic ハンドリングを含む単一の defer ブロックに集約され、コードの重複が解消されました。
  3. tls.Handshake() エラーハンドリングの変更:
    • 削除された部分:
      if err := tlsConn.Handshake(); err != nil {
          c.close() // ここが削除される
          return
      }
      
      TLSハンドシェイクが失敗した場合に、明示的に c.close() を呼び出していました。
    • 変更後:
      if err := tlsConn.Handshake(); err != nil {
          return
      }
      
      c.close() の呼び出しが削除されました。これは、ハンドシェイクエラーが発生した場合でも、関数がリターンする際に、冒頭の defer ブロックが最終的に c.close() を呼び出すため、ここでの明示的なクローズは不要になったためです。これにより、コードがさらに簡潔になりました。

これらの変更は、serve() メソッドの堅牢性と保守性を向上させ、特に panic 発生時や接続ハイジャック時の動作をより正確に定義しています。

関連リンク

参考にした情報源リンク

  • Go言語の defer ステートメントに関する公式ドキュメントやブログ記事
  • Go言語の panicrecover に関する公式ドキュメントやブログ記事
  • Go言語 net/http パッケージのドキュメント
  • Go言語 crypto/tls パッケージのドキュメント
  • Go言語のコミット履歴と関連するコードレビューコメント (CL 6970049)
  • Dave Cheney氏のブログやGoに関する記事 (Goコミュニティにおける著名な貢献者の一人であるため)
  • Go言語のソースコード (src/pkg/net/http/server.go)
  • Go言語の runtime.Stack 関数に関するドキュメント
  • Go言語の log.Printf 関数に関するドキュメント
  • Go言語の net/http パッケージにおける接続ハイジャックの概念に関する情報