[インデックス 14853] ファイルの概要
このコミットは、Go言語の標準ライブラリ net
パッケージにおいて、Windows環境でのネットワーク接続確立処理を改善するものです。具体的には、DialTimeout
関数が内部的に利用する接続メカニズムを、従来の非効率な「goroutine-racing」方式から、Windows固有の高性能なWinsock APIである ConnectEx
を利用するように変更しています。これにより、特にWindows環境におけるネットワーク接続のパフォーマンス向上とリソース効率の改善が期待されます。
コミット
commit 810e439859afe4c2b526c4baed9d01fd72d499ed
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Jan 11 12:42:09 2013 +1100
net: use windows ConnectEx to dial (when possible)
Update #2631.
Update #3097.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7061060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/810e439859afe4c2b526c4baed9d01fd72d499ed
元コミット内容
このコミットの元の内容は、Go言語の net
パッケージにおいて、Windows環境でのネットワーク接続処理を最適化するために、ConnectEx
というWindows APIを使用するように変更することです。これにより、既存の非効率な DialTimeout
の実装(goroutine-racing)を改善し、パフォーマンスとリソース管理を向上させることが目的です。コミットメッセージには、関連するGoのIssue番号 #2631
と #3097
が記載されており、これらの問題の解決に貢献することが示唆されています。
変更の背景
この変更の背景には、Go言語の net
パッケージがWindows環境でネットワーク接続を確立する際に抱えていたパフォーマンスとリソース効率の問題があります。
-
既存の
DialTimeout
の非効率性: コミット前のGoのnet
パッケージでは、DialTimeout
関数がWindowsおよびPlan 9環境において「goroutine-racing」という比較的非効率な実装を使用していました。これは、接続試行とタイムアウト処理を別々のgoroutineで並行して実行し、先に完了した方を採用するという方式です。このアプローチは、デッドラインをポーリングサーバーにプッシュダウンする機能が未実装であったために採用されていましたが、リソース消費やパフォーマンスの点で最適ではありませんでした。特に、多数の同時接続を扱うようなシナリオでは、この非効率性が顕著になる可能性がありました。 -
Windows固有の高性能APIの活用: Windowsオペレーティングシステムには、Winsock APIの一部として
ConnectEx
という高度な接続関数が提供されています。ConnectEx
は、非同期I/O (Overlapped I/O) と組み合わせることで、非常に効率的なソケット接続確立を可能にします。従来のconnect
関数と比較して、ConnectEx
は接続確立と同時にデータの送受信を開始できるなど、いくつかの最適化が施されています。Goのnet
パッケージがWindows上で動作する以上、このようなプラットフォーム固有の高性能APIを活用しない手はありませんでした。 -
関連Issueの解決: コミットメッセージに記載されているIssue
#2631
と#3097
は、おそらくWindows環境でのネットワーク関連のパフォーマンス問題や、DialTimeout
の実装に関する懸念を扱っていたと考えられます。このコミットは、ConnectEx
の導入によってこれらのIssueを解決または改善することを目的としています。
これらの背景から、Go開発チームはWindows環境でのネットワーク処理のボトルネックを解消し、より堅牢で高性能なネットワークスタックを提供するために、ConnectEx
の導入を決定したと考えられます。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
1. Winsock API (Windows Sockets API)
Winsockは、Windowsオペレーティングシステムにおけるネットワークプログラミングインターフェースです。Unix系OSにおけるBerkeley Sockets APIに相当し、TCP/IPプロトコルを用いたネットワーク通信を行うための関数群を提供します。Go言語の net
パッケージがWindows上で動作する際には、内部的にこのWinsock APIを呼び出しています。
2. ConnectEx
関数
ConnectEx
は、Winsock 2.0で導入された拡張関数の一つで、非同期I/O (Overlapped I/O) をサポートするソケット接続関数です。従来の connect
関数と比較して、以下の特徴があります。
- 非同期操作:
ConnectEx
は非同期で動作し、呼び出し元スレッドをブロックせずに接続処理をバックグラウンドで実行できます。これは、I/O完了ポート (IOCP) と組み合わせて使用することで、多数の同時接続を効率的に処理するサーバーアプリケーションなどで特に有効です。 - 事前バインドの要件:
ConnectEx
を呼び出す前に、ソケットをローカルアドレスにバインド(bind
関数)しておく必要があります。これは通常のconnect
とは異なる点です。 - データ送信の統合: 接続確立と同時に最初のデータを送信する機能(
lpSendBuffer
引数)も持っています。これにより、ネットワークラウンドトリップを削減し、パフォーマンスを向上させることができます。 SO_UPDATE_CONNECT_CONTEXT
:ConnectEx
で接続が確立された後、ソケットのプロパティを更新するためにsetsockopt
関数でSO_UPDATE_CONNECT_CONTEXT
オプションを使用する必要があります。これにより、ソケットが他のWinsock関数(getsockname
,getpeername
など)で正しく機能するようになります。
3. Go言語のネットワークI/Oモデル
Go言語の net
パッケージは、プラットフォームに依存しない統一されたネットワークI/Oインターフェースを提供しますが、その内部では各OSのネイティブなI/Oメカニズムを利用しています。
- 非同期I/Oとポーリング: Goのランタイムは、効率的なネットワークI/Oのために、OSが提供する非同期I/Oメカニズム(Linuxのepoll、macOS/BSDのkqueue、WindowsのI/O完了ポートなど)を利用しています。これにより、多数のネットワーク接続を少数のゴルーチンで効率的に処理できます。
netFD
構造体:net
パッケージでは、ネットワークファイルディスクリプタ(ソケット)を抽象化するためにnetFD
構造体を使用します。この構造体は、OS固有のソケットハンドルや、I/O操作の状態を管理します。syscall
パッケージ: Goのsyscall
パッケージは、OSのシステムコールやCライブラリ関数への低レベルなインターフェースを提供します。ConnectEx
のようなWindows APIをGoから呼び出す際には、このsyscall
パッケージを介して行われます。
4. DialTimeout
の「goroutine-racing」実装
コミット前のWindowsおよびPlan 9における DialTimeout
の実装は、以下のような特徴を持っていました。
- タイムアウト処理: 接続試行とタイムアウトタイマーをそれぞれ別のgoroutineで開始します。
- 競合: 接続が確立されるか、タイムアウトタイマーが先に発火するかの「競合」状態になります。
- 非効率性: この方式は、OSのI/Oポーリングメカニズムにタイムアウト情報を直接プッシュダウンするのではなく、Goランタイムレベルでタイムアウトを管理するため、リソース消費が大きく、多数の接続を扱う場合にはオーバーヘッドが大きくなる傾向がありました。コミットメッセージの
TODO: remove this once those are implemented.
というコメントは、この非効率な実装を将来的に改善する意図があったことを示しています。
これらの知識を前提として、コミットがどのように ConnectEx
を統合し、Goのネットワークスタックを改善したかを詳細に見ていきます。
技術的詳細
このコミットの技術的詳細は、主に以下の3つの領域にわたります。
-
ConnectEx
APIのGo言語からの呼び出し:src/pkg/syscall/syscall_windows.go
にConnectEx
関数が追加されました。これは、WindowsのConnectEx
APIをGoから呼び出すためのラッパーです。LoadConnectEx
関数が導入され、ConnectEx
関数のアドレスを動的に取得します。これは、ConnectEx
がWinsockの拡張関数であり、すべてのシステムで利用可能とは限らないため、実行時にその存在を確認し、関数ポインタを取得する必要があるためです。WSAIoctl
とSIO_GET_EXTENSION_FUNCTION_POINTER
を使用して、この関数ポインタを取得しています。src/pkg/syscall/ztypes_windows.go
には、ConnectEx
の関数ポインタを取得するために必要なGUID
構造体とWSAID_CONNECTEX
定数が追加されています。また、SO_UPDATE_CONNECT_CONTEXT
という新しいソケットオプションも定義されています。
-
net
パッケージにおけるConnectEx
の統合:src/pkg/net/fd_windows.go
にcanUseConnectEx
関数が追加されました。この関数は、現在のネットワークタイプ(TCP/UDPなど)とシステムがConnectEx
をサポートしているかどうかをチェックします。UDPソケットはConnectEx
でサポートされないため、udp
系のネットワークではfalse
を返します。dialTimeout
関数がWindows固有の実装としてsrc/pkg/net/fd_windows.go
に移動・変更されました。この関数は、canUseConnectEx
の結果に基づいて、ConnectEx
を使用するか、従来のdialTimeoutRace
(goroutine-racing) 実装を使用するかを決定します。netFD
構造体のconnect
メソッドが大幅に修正されました。ConnectEx
を使用する場合、ソケットはまずbind
関数でローカルアドレスにバインドされます。これはConnectEx
の要件です。connectOp
という新しい構造体が定義され、ConnectEx
呼び出しのための非同期I/O操作(anOp
)をカプセル化します。iosrv.ExecIO
を使用してConnectEx
を非同期で実行します。これはGoの内部的なI/Oポーリングメカニズムと統合されています。- 接続成功後、
syscall.Setsockopt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_UPDATE_CONNECT_CONTEXT, ...)
を呼び出してソケットのコンテキストを更新します。これはConnectEx
で確立されたソケットが他のWinsock関数で正しく動作するために必要です。 ConnectEx
が利用できない場合は、引き続きsyscall.Connect
を使用するフォールバックロジックも含まれています。
-
DialTimeout
の実装変更とプラットフォームごとの分離:src/pkg/net/dial.go
からuseDialTimeoutRace
定数とDialTimeout
のWindows/Plan 9固有の分岐ロジックが削除されました。- 代わりに、
dialTimeout
関数が各OS固有のファイル(src/pkg/net/fd_unix.go
,src/pkg/net/fd_plan9.go
,src/pkg/net/fd_windows.go
)に定義され、それぞれのOSに最適な実装が提供されるようになりました。- Unix系OS (
fd_unix.go
) では、従来のデッドラインをポーリングサーバーにプッシュダウンする効率的なdialTimeout
実装が維持されます。 - Plan 9 (
fd_plan9.go
) では、引き続きdialTimeoutRace
が使用されます。コミットメッセージのTODO: fix this on plan9.
は、Plan 9での改善が将来の課題であることを示しています。 - Windows (
fd_windows.go
) では、前述のConnectEx
を利用した新しいdialTimeout
実装が導入されました。
- Unix系OS (
src/pkg/net/dial_windows_test.go
には、ConnectEx
を使用したDialTimeout
がハンドルリークを起こさないことを検証するための新しいテストが追加されました。numHandles
関数を使ってプロセスが保持するハンドルの数を計測し、接続タイムアウト後にハンドルの数が変化しないことを確認しています。
これらの変更により、Goの net
パッケージはWindows環境でより効率的かつ高性能なネットワーク接続確立を実現し、OS固有の機能を最大限に活用できるようになりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は以下のファイルに集中しています。
-
src/pkg/net/dial.go
:useDialTimeoutRace
定数が削除されました。DialTimeout
関数からWindows/Plan 9固有のdialTimeoutRace
への分岐ロジックが削除され、代わりにプラットフォーム固有のdialTimeout
関数を呼び出すように変更されました。
-
src/pkg/net/dial_windows_test.go
:- 新規追加されたファイル。
ConnectEx
を使用したDialTimeout
がハンドルリークを起こさないことを検証するテストTestDialTimeoutHandleLeak
が含まれています。
- 新規追加されたファイル。
-
src/pkg/net/fd_plan9.go
:dialTimeout
関数が追加され、Plan 9では引き続きdialTimeoutRace
を使用するように定義されました。
-
src/pkg/net/fd_unix.go
:dialTimeout
関数が追加され、Unix系OSでは既存の効率的なデッドラインプッシュダウン方式を使用するように定義されました。
-
src/pkg/net/fd_windows.go
:canUseConnectEx
関数が追加され、ConnectEx
の利用可能性をチェックします。dialTimeout
関数がWindows固有の実装として追加され、canUseConnectEx
に基づいてConnectEx
またはdialTimeoutRace
を選択します。connectOp
構造体が追加され、ConnectEx
操作をカプセル化します。netFD
のconnect
メソッドが大幅に修正され、ConnectEx
を使用した接続確立ロジック(ソケットのバインド、ConnectEx
の呼び出し、SO_UPDATE_CONNECT_CONTEXT
の設定)が実装されました。
-
src/pkg/syscall/syscall_windows.go
:LoadConnectEx
関数が追加され、ConnectEx
APIの関数ポインタを動的にロードします。connectEx
およびConnectEx
関数が追加され、GoからWindowsのConnectEx
APIを呼び出すための低レベルなインターフェースを提供します。
-
src/pkg/syscall/ztypes_windows.go
:SO_UPDATE_CONNECT_CONTEXT
定数が追加されました。IOC_OUT
,IOC_IN
,IOC_INOUT
,IOC_WS2
,SIO_GET_EXTENSION_FUNCTION_POINTER
などのWinsock IOCTL関連定数が追加されました。GUID
構造体とWSAID_CONNECTEX
定数が追加され、ConnectEx
の関数ポインタ取得に使用されます。
これらの変更は、GoのネットワークスタックのWindows固有部分を大幅に強化し、パフォーマンスと効率を向上させるためのものです。
コアとなるコードの解説
このコミットの核となる変更は、Windows環境におけるネットワーク接続確立のロジックを、従来の非効率な「goroutine-racing」方式から、Windows固有の高性能な ConnectEx
APIを利用する方式へと移行した点にあります。
src/pkg/net/fd_windows.go
の connect
メソッド
このメソッドは、netFD
(ネットワークファイルディスクリプタ、つまりソケット) を使ってリモートアドレスに接続する際の中心的なロジックを含んでいます。
func (fd *netFD) connect(ra syscall.Sockaddr) error {
if !canUseConnectEx(fd.net) {
// ConnectExが利用できない場合、またはUDPソケットの場合は従来のsyscall.Connectを使用
return syscall.Connect(fd.sysfd, ra)
}
// ConnectEx windows API requires an unconnected, previously bound socket.
// ConnectExの要件:未接続で、事前にバインドされたソケットが必要
var la syscall.Sockaddr
switch ra.(type) {
case *syscall.SockaddrInet4:
la = &syscall.SockaddrInet4{} // IPv4ソケットの場合、空のIPv4アドレスでバインド
case *syscall.SockaddrInet6:
la = &syscall.SockaddrInet6{} // IPv6ソケットの場合、空のIPv6アドレスでバインド
default:
panic("unexpected type in connect") // 未知のソケットタイプ
}
if err := syscall.Bind(fd.sysfd, la); err != nil {
return err // ソケットのバインドに失敗
}
// Call ConnectEx API.
var o connectOp
o.Init(fd, 'w') // connectOpを初期化し、書き込み操作として設定
o.ra = ra // リモートアドレスを設定
_, err := iosrv.ExecIO(&o, fd.wdeadline.value()) // ConnectExを非同期で実行
if err != nil {
return err // ConnectExの実行に失敗
}
// Refresh socket properties.
// ソケットのプロパティを更新(ConnectExで確立されたソケットの必須手順)
return syscall.Setsockopt(fd.sysfd, syscall.SOL_SOCKET, syscall.SO_UPDATE_CONNECT_CONTEXT, (*byte)(unsafe.Pointer(&fd.sysfd)), int32(unsafe.Sizeof(fd.sysfd)))
}
解説:
canUseConnectEx
のチェック: まず、現在のネットワークタイプ(fd.net
)とシステムがConnectEx
をサポートしているかを確認します。UDPソケットの場合や、ConnectEx
がロードできない場合は、従来のsyscall.Connect
にフォールバックします。- ソケットのバインド:
ConnectEx
の重要な要件として、呼び出し前にソケットがローカルアドレスにバインドされている必要があります。ここでは、リモートアドレスのタイプ(IPv4またはIPv6)に応じて、対応する空のローカルアドレス(0.0.0.0
または::
)にソケットをバインドしています。 connectOp
の利用と非同期実行:connectOp
は、ConnectEx
呼び出しに必要な情報(ソケットディスクリプタ、リモートアドレス、Overlapped I/O構造体など)をカプセル化する内部構造体です。o.Init(fd, 'w')
で初期化され、この操作が書き込み(接続)であることを示します。iosrv.ExecIO(&o, fd.wdeadline.value())
は、GoランタイムのI/Oポーリングサーバー(WindowsではI/O完了ポート)を利用して、ConnectEx
を非同期で実行します。これにより、接続処理がバックグラウンドで行われ、呼び出し元のgoroutineはブロックされません。タイムアウトもこのExecIO
の中で処理されます。
SO_UPDATE_CONNECT_CONTEXT
:ConnectEx
で接続が成功した後、ソケットの内部状態を更新するためにSO_UPDATE_CONNECT_CONTEXT
オプションを指定してsetsockopt
を呼び出す必要があります。これは、ConnectEx
がソケットのコンテキストを完全に初期化しないため、他のWinsock関数(例:getsockname
,getpeername
)が正しく機能するようにするために不可欠なステップです。
src/pkg/syscall/syscall_windows.go
の ConnectEx
関連関数
このファイルでは、Windows APIの ConnectEx
をGoから直接呼び出すための低レベルなラッパーが提供されています。
// LoadConnectEx は ConnectEx 関数のアドレスを動的にロードします。
var connectExFunc struct {
once sync.Once
addr uintptr
err error
}
func LoadConnectEx() error {
connectExFunc.once.Do(func() {
var s Handle
s, connectExFunc.err = Socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) // 一時的なソケットを作成
if connectExFunc.err != nil {
return
}
defer CloseHandle(s) // 関数終了時にソケットを閉じる
var n uint32
// WSAIoctl を使って ConnectEx の関数ポインタを取得
connectExFunc.err = WSAIoctl(s,
SIO_GET_EXTENSION_FUNCTION_POINTER,
(*byte)(unsafe.Pointer(&WSAID_CONNECTEX)),
uint32(unsafe.Sizeof(WSAID_CONNECTEX)),
(*byte)(unsafe.Pointer(&connectExFunc.addr)),
uint32(unsafe.Sizeof(connectExFunc.addr)),
&n, nil, 0)
})
return connectExFunc.err
}
// connectEx は ConnectEx APIの低レベルなSyscall呼び出しです。
func connectEx(s Handle, name uintptr, namelen int32, sendBuf *byte, sendDataLen uint32, bytesSent *uint32, overlapped *Overlapped) (err error) {
r1, _, e1 := Syscall9(connectExFunc.addr, 7, uintptr(s), uintptr(name), uintptr(namelen), uintptr(unsafe.Pointer(sendBuf)), uintptr(sendDataLen), uintptr(unsafe.Pointer(bytesSent)), uintptr(unsafe.Pointer(overlapped)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = EINVAL // エラーコードがない場合はEINVAL
}
}
return
}
// ConnectEx は Go から呼び出すための ConnectEx ラッパー関数です。
func ConnectEx(fd Handle, sa Sockaddr, sendBuf *byte, sendDataLen uint32, bytesSent *uint32, overlapped *Overlapped) error {
err := LoadConnectEx() // ConnectEx の関数ポインタをロード
if err != nil {
return errorspkg.New("failed to find ConnectEx: " + err.Error())
}
ptr, n, err := sa.sockaddr() // Sockaddr を C のポインタと長さに変換
if err != nil {
return err
}
return connectEx(fd, ptr, n, sendBuf, sendDataLen, bytesSent, overlapped)
}
解説:
LoadConnectEx
: この関数は、sync.Once
を使用してConnectEx
APIの関数ポインタを一度だけロードします。WSAIoctl
関数とSIO_GET_EXTENSION_FUNCTION_POINTER
IOCTLコードを使って、動的にConnectEx
のアドレスを取得します。これは、ConnectEx
がすべてのWindowsバージョンで利用可能とは限らないため、実行時にその存在を確認し、関数ポインタを取得する必要があるためです。connectEx
: これは、GoのSyscall9
関数を使って、ロードされたConnectEx
の関数ポインタを直接呼び出す低レベルなラッパーです。Syscall9
は、最大9つの引数を持つシステムコールを呼び出すためのGoの組み込み関数です。ConnectEx
(高レベルラッパー): この関数は、LoadConnectEx
を呼び出して関数ポインタが利用可能であることを確認し、Sockaddr
構造体をConnectEx
が期待する形式(ポインタと長さ)に変換してから、低レベルのconnectEx
を呼び出します。
これらの変更により、Goの net
パッケージはWindowsのネイティブな非同期I/O機能を活用し、より効率的でスケーラブルなネットワーク接続処理を実現できるようになりました。特に、多数の同時接続を扱うアプリケーションにおいて、この最適化は大きなパフォーマンス向上をもたらす可能性があります。
関連リンク
- Go Issue 2631: https://github.com/golang/go/issues/2631 (このコミットで
Update
されたIssueの一つ) - Go Issue 3097: https://github.com/golang/go/issues/3097 (このコミットで
Update
されたIssueの一つ) - Go CL 7061060: https://golang.org/cl/7061060 (このコミットに対応するGerritの変更リスト)
参考にした情報源リンク
- Microsoft Learn:
ConnectEx
function: https://learn.microsoft.com/en-us/windows/win32/api/mswsock/nc-mswsock-lpfn_connectex - Microsoft Learn:
SO_UPDATE_CONNECT_CONTEXT
option: https://learn.microsoft.com/en-us/windows/win32/winsock/winsock-ioctls (このページ内でSO_UPDATE_CONNECT_CONTEXT
について言及されています) - Microsoft Learn:
WSAIoctl
function: https://learn.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsaoctl - Microsoft Learn:
SIO_GET_EXTENSION_FUNCTION_POINTER
IOCTL: https://learn.microsoft.com/en-us/windows/win32/winsock/winsock-ioctls (このページ内でSIO_GET_EXTENSION_FUNCTION_POINTER
について言及されています) - Go言語の
net
パッケージのドキュメント (一般的なGoのネットワークI/Oモデルについて): https://pkg.go.dev/net - Go言語の
syscall
パッケージのドキュメント (Windows固有のシステムコールについて): https://pkg.go.dev/syscall - Stack Overflow や技術ブログ記事 (ConnectExの利用例やWinsockの解説): 検索結果に基づいて一般的な理解を深めるために参照しました。