[インデックス 10905] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージにDialTimeout
関数を追加し、ネットワーク接続確立時のタイムアウト機能を提供することを目的としています。これにより、ネットワークの応答がない場合にDial
関数が無限にブロックされる問題を解決し、より堅牢なネットワークアプリケーションの構築を可能にします。
コミット
commit 964309e2fdd7f1e1b7b7e0c601446dc85d5d41bf
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Dec 20 13:17:39 2011 -0800
net: DialTimeout
Fixes #240
R=adg, dsymonds, rsc, r, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/5491062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/964309e2fdd7f1e1b7b7e0c601446dc85d5d41bf
元コミット内容
このコミットは、net
パッケージにDialTimeout
関数を導入します。この関数は、既存のDial
関数と同様にネットワーク接続を確立しますが、指定されたタイムアウト期間内に接続が確立されない場合にエラーを返します。タイムアウトには、名前解決にかかる時間も含まれます。この変更は、Issue #240で報告された、Dial
が応答しないホストに対して無限にブロックされる問題を修正します。
変更の背景
Go言語の初期のnet
パッケージにおけるDial
関数は、ネットワーク接続を試みる際にタイムアウトの概念を持っていませんでした。これは、特に以下のようなシナリオで問題を引き起こしました。
- 応答しないホストへの接続試行: ネットワーク上のホストがダウンしている、または特定のポートでリッスンしていない場合、
Dial
関数は接続が確立されるまで無限に待機し続ける可能性がありました。これにより、アプリケーションがハングアップしたり、リソースが枯渇したりするリスクがありました。 - ネットワークの遅延や不安定性: ネットワークが遅延している、または不安定な環境では、接続確立に時間がかかることがあり、その間アプリケーションがブロックされることでユーザーエクスペリエンスが低下しました。
- リソース管理の困難さ: 無限にブロックされる接続は、ゴルーチンやファイルディスクリプタなどのシステムリソースを消費し続け、アプリケーション全体の安定性やスケーラビリティに悪影響を及ぼしました。
これらの問題に対処するため、Issue #240 (net: timeout support for Dial
) が提起され、Dial
関数にタイムアウト機能を追加する必要性が議論されました。このコミットは、その議論の結果として、DialTimeout
関数を導入することで、これらの課題を解決し、より堅牢で応答性の高いネットワークアプリケーションの開発を可能にしました。
前提知識の解説
このコミットの変更内容を理解するためには、以下の前提知識が役立ちます。
1. ネットワークプログラミングの基本
- TCP/IP: インターネットの基盤となるプロトコルスイート。TCP (Transmission Control Protocol) は信頼性の高いコネクション指向の通信を提供し、IP (Internet Protocol) はデータのルーティングを担当します。
- ソケット: ネットワーク通信のエンドポイント。アプリケーションはソケットを通じてデータを送受信します。
- 接続 (Connection): クライアントとサーバー間で確立される論理的な通信パス。TCP接続は、3ウェイハンドシェイクと呼ばれるプロセスを経て確立されます。
- ダイヤル (Dial): クライアントがサーバーへの接続を開始する操作。Go言語の
net.Dial
関数は、この操作を抽象化しています。 - タイムアウト (Timeout): 特定の操作が完了するまでに許容される最大時間。ネットワーク操作においてタイムアウトを設定することは、アプリケーションの応答性と堅牢性を確保するために不可欠です。
2. Go言語の並行処理
Go言語は、軽量なスレッドであるゴルーチン (Goroutine) と、ゴルーチン間の安全な通信を可能にするチャネル (Channel) を用いた並行処理を強力にサポートしています。
- ゴルーチン:
go
キーワードを使って関数を呼び出すことで、新しいゴルーチンが生成され、その関数が並行して実行されます。 - チャネル: ゴルーチン間で値を送受信するための通信メカニズム。チャネルは、データの同期と通信を同時に行い、共有メモリによる競合状態を避けるのに役立ちます。
select
ステートメント: 複数のチャネル操作を待機し、準備ができた最初の操作を実行するために使用されます。これは、タイムアウト処理や複数のイベントソースからの入力を処理する際に非常に強力です。
3. Go言語のnet
パッケージ
net
パッケージは、ネットワークI/Oプリミティブへのポータブルなインターフェースを提供します。
net.Conn
インターフェース: 汎用的なネットワーク接続を表すインターフェースで、Read
、Write
、Close
などのメソッドを持ちます。net.Addr
インターフェース: ネットワークアドレスを表すインターフェース。net.OpError
: ネットワーク操作中に発生したエラーを詳細に記述するための構造体。操作の種類、ネットワークタイプ、アドレス、および根本的なエラーを含みます。time.Duration
: 時間の長さを表す型。time.Millisecond
やtime.Second
などの定数を使って時間を指定できます。time.Timer
: 指定された期間が経過した後にチャネルに値を送信するオブジェクト。タイムアウト処理によく使用されます。
技術的詳細
DialTimeout
関数の実装は、Go言語の並行処理機能を巧みに利用して、タイムアウトを実現しています。
-
タイムアウトタイマーの開始:
t := time.NewTimer(timeout)
指定されたtimeout
期間が経過すると、t.C
チャネルに値が送信されるタイマーを作成します。defer t.Stop()
により、関数終了時にタイマーが停止され、リソースが解放されます。 -
非同期での接続試行:
go func() { ... }()
新しいゴルーチンを起動し、その中で実際のネットワーク接続(名前解決とdialAddr
呼び出し)を試みます。このゴルーチンは、接続結果(Conn
とerror
)をch
チャネルに送信します。また、名前解決が成功した場合は、解決されたアドレスをresolvedAddr
チャネルに送信します。 -
select
によるタイムアウトと接続結果の待機:select { ... }
select
ステートメントは、t.C
チャネル(タイムアウト)とch
チャネル(接続結果)のいずれかから値が受信されるのを待機します。-
case <-t.C:
(タイムアウト発生): タイマーが期限切れになった場合、このケースが実行されます。 接続試行中に名前解決が完了していた場合(resolvedAddr
チャネルに値がある場合)、そのアドレスを使用してOpError
を構築します。そうでない場合は、元のネットワークとアドレス文字列からstringAddr
を作成して使用します。 最終的に、&timeoutError{}
を内部エラーとして持つOpError
を返します。timeoutError
は、Timeout() bool
メソッドを持つことで、エラーがタイムアウトによるものであることを示すマーカーインターフェースを実装しています。 -
case p := <-ch:
(接続結果の受信): 接続試行ゴルーチンから結果がch
チャネルに送信された場合、このケースが実行されます。 受信したConn
とerror
のペアをそのまま返します。
-
この設計により、DialTimeout
は、接続試行とタイムアウト監視を並行して行い、どちらかのイベントが先に発生した時点で適切な処理を行うことができます。
dialAddr
関数の導入
元のDial
関数から、実際のアドレス解決後の接続処理部分がdialAddr
という新しい内部関数に切り出されています。これにより、Dial
とDialTimeout
の両方で共通の接続ロジックを再利用できるようになり、コードの重複が避けられています。
stringAddr
構造体
stringAddr
は、net.Addr
インターフェースを実装するシンプルな構造体です。これは、DialTimeout
がタイムアウトした場合に、名前解決が完了していなかったとしても、エラーメッセージに元のネットワークとアドレス文字列を含めるために使用されます。これにより、エラー情報がより詳細になります。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
-
src/pkg/net/dial.go
:time
パッケージのインポートが追加されました。Dial
関数のシグネチャがfunc Dial(net, addr string) (Conn, error)
に変更され、戻り値の型が明示されました。Dial
関数から、アドレス解決後の実際の接続ロジックがdialAddr
という新しい内部関数に切り出されました。DialTimeout
関数が新しく追加されました。この関数は、タイムアウト機能を持つネットワーク接続を確立します。stringAddr
という新しい型と、そのNetwork()
およびString()
メソッドが追加されました。これは、タイムアウトエラー発生時にアドレス情報を提供するために使用されます。
-
src/pkg/net/dial_test.go
:DialTimeout
関数の動作を検証するための新しいテストファイルが追加されました。TestDialTimeout
というテスト関数が含まれており、異なるオペレーティングシステム(Linux, Darwinなど)でのタイムアウト動作をテストするためのプラットフォーム固有のロジックが含まれています。特に、LinuxではTCPのバックログを埋めることでタイムアウトを誘発し、Darwinでは到達不能なIPアドレスへの接続を試みることでタイムアウトをテストしています。
コアとなるコードの解説
src/pkg/net/dial.go
package net
import (
"time" // timeパッケージがインポートされる
)
// Dial関数の実装が変更され、実際の接続処理はdialAddrに委譲される
func Dial(net, addr string) (Conn, error) {
addri, err := resolveNetAddr("dial", net, addr)
if err != nil {
return nil, err
}
return dialAddr(net, addr, addri) // dialAddrを呼び出す
}
// dialAddrは、アドレス解決後の実際の接続処理を行う内部関数
func dialAddr(net, addr string, addri Addr) (c Conn, err error) {
switch ra := addri.(type) {
case *TCPAddr:
c, err = DialTCP(net, nil, ra)
case *UDPAddr:
c, err = DialUDP(net, nil, ra)
case *IPAddr:
c, err = DialIP(net, nil, ra)
case *UnixAddr:
c, err = DialUnix(net, nil, ra)
default:
return nil, &OpError{"dial", net, addri, ErrUnknownNetwork}
}
return
}
// DialTimeoutは、タイムアウト付きでネットワーク接続を確立する
// タイムアウトには名前解決も含まれる
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error) {
// TODO(bradfitz): the timeout should be pushed down into the
// net package's event loop, so on timeout to dead hosts we
// don't have a goroutine sticking around for the default of
// ~3 minutes.
t := time.NewTimer(timeout) // タイムアウトタイマーを作成
defer t.Stop() // 関数終了時にタイマーを停止
type pair struct { // 接続結果を保持する構造体
Conn
error
}
ch := make(chan pair, 1) // 接続結果を送信するためのチャネル
resolvedAddr := make(chan Addr, 1) // 名前解決されたアドレスを送信するためのチャネル
go func() { // 新しいゴルーチンで非同期に接続を試みる
addri, err := resolveNetAddr("dial", net, addr) // 名前解決
if err != nil {
ch <- pair{nil, err} // エラーがあればチャネルに送信
return
}
resolvedAddr <- addri // 名前解決されたアドレスを送信
c, err := dialAddr(net, addr, addri) // 実際の接続処理
ch <- pair{c, err} // 接続結果をチャネルに送信
}()
select { // タイムアウトまたは接続結果のいずれかを待機
case <-t.C: // タイムアウトが発生した場合
// タイムアウト前に名前解決が完了していれば、そのアドレスを使用
var addri Addr
select {
case a := <-resolvedAddr:
addri = a
default:
addri = &stringAddr{net, addr} // 名前解決が完了していなければ、文字列アドレスを使用
}
err := &OpError{ // OpErrorを構築し、timeoutErrorを内部エラーとして設定
Op: "dial",
Net: net,
Addr: addri,
Err: &timeoutError{}, // タイムアウトエラーを示す
}
return nil, err
case p := <-ch: // 接続結果が受信された場合
return p.Conn, p.error // 接続結果を返す
}
panic("unreachable") // ここには到達しないはず
}
// stringAddrは、net.Addrインターフェースを実装するシンプルな構造体
// タイムアウトエラー発生時にアドレス情報を提供するために使用される
type stringAddr struct {
net, addr string
}
func (a stringAddr) Network() string { return a.net }
func (a stringAddr) String() string { return a.addr }
src/pkg/net/dial_test.go
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package net
import (
"runtime"
"testing"
"time"
)
// newLocalListenerは、テスト用のローカルリスナーを作成するヘルパー関数
func newLocalListener(t *testing.T) Listener {
ln, err := Listen("tcp", "127.0.0.1:0")
if err != nil {
ln, err = Listen("tcp6", "[::1]:0") // IPv6も試す
}
if err != nil {
t.Fatal(err)
}
return ln
}
// TestDialTimeoutはDialTimeout関数の動作をテストする
func TestDialTimeout(t *testing.T) {
ln := newLocalListener(t) // ローカルリスナーを作成
defer ln.Close() // テスト終了時にリスナーを閉じる
errc := make(chan error) // エラーを送信するためのチャネル
const SOMAXCONN = 0x80 // syscallからコピーされたSOMAXCONN (バックログサイズ)
const numConns = SOMAXCONN + 10 // バックログを埋めるための接続数
// TODO(bradfitz): It's hard to test this in a portable
// way. This is unforunate, but works for now.
switch runtime.GOOS { // OSごとに異なるテストロジック
case "linux":
// Linuxでは、カーネルのバックログを埋めることでタイムアウトを誘発
// ユーザー空間が接続を受け入れる前にTCP接続を受け入れ始めるため、
// 多数の接続を起動してカーネルのバックログを埋める。
// その後、タイムアウトエラーが発生することを確認する。
for i := 0; i < numConns; i++ {
go func() {
_, err := DialTimeout("tcp", ln.Addr().String(), 200*time.Millisecond)
errc <- err
}()
}
case "darwin":
// Darwin (OS X 10.7以降) では、listenのバックログを無視して任意の数の接続を受け入れる傾向があるため、
// 意図的に到達不能なIPアドレス (127.0.71.111:80) への接続を試みることでタイムアウトをテスト。
go func() {
_, err := DialTimeout("tcp", "127.0.71.111:80", 200*time.Millisecond)
errc <- err
}()
default:
// その他のOSではテストをスキップ (Windowsなど)
t.Logf("skipping test on %q; untested.", runtime.GOOS)
return
}
connected := 0
for {
select {
case <-time.After(15 * time.Second): // 15秒でタイムアウト
t.Fatal("too slow")
case err := <-errc: // エラーが受信された場合
if err == nil { // 接続が成功した場合
connected++
if connected == numConns {
t.Fatal("all connections connected; expected some to time out") // 全ての接続が成功した場合、エラー (タイムアウトを期待)
}
} else { // エラーが発生した場合
terr, ok := err.(timeout) // エラーがtimeoutインターフェースを実装しているか確認
if !ok {
t.Fatalf("got error %q; want error with timeout interface", err)
}
if !terr.Timeout() {
t.Fatalf("got error %q; not a timeout", err)
}
// タイムアウトエラーが確認されたので、テストは成功
return
}
}
}
}
関連リンク
- GitHubコミット: https://github.com/golang/go/commit/964309e2fdd7f1e1b7b7e0c601446dc85d5d41bf
- Go CL (Code Review): https://golang.org/cl/5491062
- Go Issue #240:
net: timeout support for Dial
(GitHubのGoリポジトリで検索すると見つかります)
参考にした情報源リンク
- Go言語の
net
パッケージドキュメント: https://pkg.go.dev/net - Go言語の
time
パッケージドキュメント: https://pkg.go.dev/time - Go言語の並行処理に関する公式ドキュメントやチュートリアル (例: A Tour of Go - Concurrency): https://go.dev/tour/concurrency/1
- TCP 3-way Handshake: https://www.cloudflare.com/learning/network-layer/what-is-a-tcp-3-way-handshake/ (一般的なTCPハンドシェイクの説明)
- Go issue 240 (Web検索結果): https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHbfDz1Bsd8V2M3NvdA8DgJI9wh-FdmESYxfeJ4Y-KdfHOJpB11NyB0B_OkkPoCaM5mhAeNmi7RxBPDuzlqjmuKjUs4sK-0dHHTJ7bm_2jMVJvOnbbfbbURqxzBMmgkTUWJqg== (Issue #240に関する情報源)