[インデックス 15086] ファイルの概要
このコミットは、Go言語の net
パッケージにおけるWindows環境でのポートルックアップ処理を改善するものです。具体的には、LookupPort
関数が可能な場合にWindows APIの GetAddrInfoW
を利用するように変更し、より効率的で堅牢な名前解決を実現しています。
コミット
net: use windows GetAddrInfoW in LookupPort when possible
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7252045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6d175e243a0565b3b188dad118ca1e0f845a532a
元コミット内容
commit 6d175e243a0565b3b188dad118ca1e0f845a532a
Author: Alex Brainman <alex.brainman@gmail.com>
Date: Fri Jan 18 17:05:04 2013 +1100
net: use windows GetAddrInfoW in LookupPort when possible
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/7252045
---
src/pkg/net/fd_windows.go | 1 +\
src/pkg/net/lookup_windows.go | 43 ++++++++++++++++++++++++++++++++++++++++---
src/pkg/net/port_test.go | 2 +-\
3 files changed, 42 insertions(+), 4 deletions(-)
diff --git a/src/pkg/net/fd_windows.go b/src/pkg/net/fd_windows.go
index ea6ef10ec1..0bf361d443 100644
--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -37,6 +37,7 @@ func sysInit() {
}
canCancelIO = syscall.LoadCancelIoEx() == nil
if syscall.LoadGetAddrInfo() == nil {
+\t\tlookupPort = newLookupPort
\t\tlookupIP = newLookupIP
}\
}\ndiff --git a/src/pkg/net/lookup_windows.go b/src/pkg/net/lookup_windows.go
index 390fe7f440..b433d0cbbd 100644
--- a/src/pkg/net/lookup_windows.go
+++ b/src/pkg/net/lookup_windows.go
@@ -17,6 +17,11 @@ var (\
serventLock sync.Mutex
)\
\n+var (\n+\tlookupPort = oldLookupPort\n+\tlookupIP = oldLookupIP\n+)\n+\n // lookupProtocol looks up IP protocol name and returns correspondent protocol number.\n func lookupProtocol(name string) (proto int, err error) {\n protoentLock.Lock()\
\tdefer protoentLock.Unlock()\
@@ -40,8 +45,6 @@ func lookupHost(name string) (addrs []string, err error) {\
return\
}\
\n-var lookupIP = oldLookupIP\
-\n func oldLookupIP(name string) (addrs []IP, err error) {\
hostentLock.Lock()\
defer hostentLock.Unlock()\
@@ -92,7 +95,7 @@ func newLookupIP(name string) (addrs []IP, err error) {\
return addrs, nil\
}\
\n-func lookupPort(network, service string) (port int, err error) {\
+func oldLookupPort(network, service string) (port int, err error) {\
switch network {\
case \"tcp4\", \"tcp6\":\
network = \"tcp\"\
@@ -108,6 +111,40 @@ func lookupPort(network, service string) (port int, err error) {\
return int(syscall.Ntohs(s.Port)), nil\
}\
\n+func newLookupPort(network, service string) (port int, err error) {\
+\tvar stype int32\
+\tswitch network {\
+\tcase \"tcp4\", \"tcp6\":\
+\t\tstype = syscall.SOCK_STREAM\
+\tcase \"udp4\", \"udp6\":\
+\t\tstype = syscall.SOCK_DGRAM\
+\t}\
+\thints := syscall.AddrinfoW{\
+\t\tFamily: syscall.AF_UNSPEC,\
+\t\tSocktype: stype,\
+\t\tProtocol: syscall.IPPROTO_IP,\
+\t}\
+\tvar result *syscall.AddrinfoW\
+\te := syscall.GetAddrInfoW(nil, syscall.StringToUTF16Ptr(service), &hints, &result)\
+\tif e != nil {\
+\t\treturn 0, os.NewSyscallError(\"GetAddrInfoW\", e)\
+\t}\
+\tdefer syscall.FreeAddrInfoW(result)\
+\tif result == nil {\
+\t\treturn 0, os.NewSyscallError(\"LookupPort\", syscall.EINVAL)\
+\t}\
+\taddr := unsafe.Pointer(result.Addr)\
+\tswitch result.Family {\
+\tcase syscall.AF_INET:\
+\t\ta := (*syscall.RawSockaddrInet4)(addr)\
+\t\treturn int(syscall.Ntohs(a.Port)), nil\
+\tcase syscall.AF_INET6:\
+\t\ta := (*syscall.RawSockaddrInet6)(addr)\
+\t\treturn int(syscall.Ntohs(a.Port)), nil\
+\t}\
+\treturn 0, os.NewSyscallError(\"LookupPort\", syscall.EINVAL)\
+}\
+\n func lookupCNAME(name string) (cname string, err error) {\
\tvar r *syscall.DNSRecord\
\te := syscall.DnsQuery(name, syscall.DNS_TYPE_CNAME, 0, nil, &r, nil)\
diff --git a/src/pkg/net/port_test.go b/src/pkg/net/port_test.go
index 329b169f34..9e8968f359 100644
--- a/src/pkg/net/port_test.go
+++ b/src/pkg/net/port_test.go
@@ -46,7 +46,7 @@ func TestLookupPort(t *testing.T) {\
\tfor i := 0; i < len(porttests); i++ {\
\t\ttt := porttests[i]\
\t\tif port, err := LookupPort(tt.netw, tt.name); port != tt.port || (err == nil) != tt.ok {\
-\t\t\tt.Errorf(\"LookupPort(%q, %q) = %v, %s; want %v\",\
+\t\t\tt.Errorf(\"LookupPort(%q, %q) = %v, %v; want %v\",\
\t\t\t\ttt.netw, tt.name, port, err, tt.port)\
\t\t}\
\t}\
変更の背景
このコミットの主な背景は、Windows環境におけるネットワークサービス名からポート番号への解決(ポートルックアップ)の信頼性と効率性を向上させることです。従来の getservbyname
のような関数は、古いAPIであり、IPv6のサポートが不十分であったり、非同期処理に対応していなかったりするなどの制約がありました。
Windows Vista以降で導入された GetAddrInfoW
は、より現代的な名前解決APIであり、IPv4とIPv6の両方をサポートし、より柔軟なクエリが可能です。このコミットは、Goの net
パッケージがWindows上でポートルックアップを行う際に、可能であればこの新しいAPIを利用するように切り替えることで、パフォーマンスの向上、IPv6互換性の確保、および将来的な拡張性を提供することを目的としています。
前提知識の解説
1. ネットワークアドレス解決 (Name Resolution)
コンピュータネットワークにおいて、人間が読みやすいホスト名(例: www.example.com
)やサービス名(例: http
, ftp
)を、コンピュータが理解できるIPアドレスやポート番号に変換するプロセスを「名前解決」と呼びます。
- ホスト名からIPアドレスへの解決: DNS (Domain Name System) を利用して行われます。
- サービス名からポート番号への解決: 通常、
/etc/services
(Unix/Linux) やC:\Windows\System32\drivers\etc\services
(Windows) のようなシステムファイル、またはネットワークサービス(例: NIS, LDAP)を参照して行われます。
2. LookupPort
関数
Go言語の net
パッケージには、サービス名とネットワークタイプ(例: tcp
, udp
)を指定して、対応するポート番号を検索する LookupPort
関数があります。例えば、LookupPort("tcp", "http")
は通常 80
を返します。
3. Windows Sockets API (Winsock)
Windowsにおけるネットワークプログラミングの標準APIセットです。ソケットの作成、接続、データの送受信、名前解決など、ネットワーク通信に必要な機能を提供します。
4. getservbyname
(Winsock API)
従来のWinsock APIの一つで、サービス名とプロトコル名(例: tcp
)を指定して、対応するサービスエントリ(ポート番号などを含む)を取得する関数です。この関数は、内部的に services
ファイルなどを参照します。しかし、このAPIはいくつかの制限があります。
- 同期処理: 呼び出しが完了するまでスレッドをブロックします。
- IPv6のサポート: IPv4に特化しており、IPv6のサービス解決には直接対応していません。
- スレッドセーフティ: スレッドセーフではない実装が多く、マルチスレッド環境での利用には注意が必要です。
5. GetAddrInfoW
(Winsock API)
Windows Vista以降で推奨される、より新しい名前解決APIです。ホスト名とサービス名を指定して、ネットワークアドレス構造体(addrinfo
構造体)のリストを取得します。このAPIは getservbyname
の多くの欠点を克服しています。
- IPv4/IPv6の統合サポート: 両方のアドレスファミリーに対応し、単一のAPIで解決できます。
- 非同期処理のサポート:
GetAddrInfoEx
を使用することで非同期処理も可能です(このコミットでは同期版のGetAddrInfoW
を使用)。 - プロトコル独立性: ネットワークタイプ(TCP/UDP)やアドレスファミリー(IPv4/IPv6)をヒントとして指定することで、柔軟な解決が可能です。
- Unicodeサポート:
GetAddrInfoW
のW
はワイド文字(Unicode)を意味し、国際化されたドメイン名やサービス名に対応します。
6. Go言語の syscall
パッケージ
Go言語の syscall
パッケージは、オペレーティングシステム固有のシステムコールやAPIにアクセスするための機能を提供します。Windowsの場合、Winsock APIを含む多くのWindows API関数がこのパッケージを通じて呼び出されます。
syscall.LoadGetAddrInfo()
:GetAddrInfoW
関数がシステムにロード可能かどうかをチェックします。これにより、古いWindowsバージョンでGetAddrInfoW
が利用できない場合でも、Goプログラムがクラッシュしないようにフォールバックメカニズムを提供できます。syscall.AddrinfoW
:GetAddrInfoW
関数に渡すヒント(検索条件)や、結果として返されるアドレス情報を含む構造体です。syscall.AF_UNSPEC
,syscall.AF_INET
,syscall.AF_INET6
: アドレスファミリーを指定する定数です。AF_UNSPEC
はIPv4とIPv6の両方を検索対象とすることを示します。syscall.SOCK_STREAM
,syscall.SOCK_DGRAM
: ソケットタイプを指定する定数です。それぞれTCP (ストリームソケット) とUDP (データグラムソケット) に対応します。syscall.IPPROTO_IP
: IPプロトコルを指定する定数です。syscall.Ntohs()
: ネットワークバイトオーダーからホストバイトオーダーに16ビット整数を変換する関数です。ポート番号は通常ネットワークバイトオーダーで表現されるため、Goのプログラムで利用する際にはホストバイトオーダーに変換する必要があります。
7. unsafe.Pointer
Go言語の unsafe
パッケージは、型安全性をバイパスしてメモリを直接操作するための機能を提供します。unsafe.Pointer
は、任意の型のポインタを保持できる特殊なポインタ型です。このコミットでは、GetAddrInfoW
から返される syscall.AddrinfoW
構造体内のアドレス情報(Addr
フィールド)を、具体的なソケットアドレス構造体(例: syscall.RawSockaddrInet4
)に型キャストするために使用されています。これは、Goの型システムでは直接的なポインタ変換が許可されていないため、低レベルの操作が必要な場合に用いられます。
技術的詳細
このコミットは、Goの net
パッケージがWindows上でポートルックアップを行う際の内部実装を、より現代的な GetAddrInfoW
APIに切り替えることで、堅牢性と互換性を向上させています。
1. 動的なAPI選択
src/pkg/net/fd_windows.go
の sysInit()
関数が変更されています。この関数は、Goプログラムが起動する際にシステム固有の初期化を行うために呼び出されます。
変更点:
if syscall.LoadGetAddrInfo() == nil {
lookupPort = newLookupPort
lookupIP = newLookupIP
}
syscall.LoadGetAddrInfo()
は、システムが GetAddrInfoW
関数をロードできるかどうかをチェックします。もしロードに成功した場合(つまり、LoadGetAddrInfo()
が nil
を返す場合)、それはシステムが GetAddrInfoW
をサポートしていることを意味します。この条件が真の場合、グローバル変数 lookupPort
と lookupIP
が、それぞれ新しい実装である newLookupPort
と newLookupIP
に設定されます。これにより、Goのネットワークスタックは、実行時に利用可能な最適なAPIを動的に選択できるようになります。古いWindowsバージョンでは GetAddrInfoW
が利用できないため、このチェックによって後方互換性が保たれます。
2. lookupPort
と lookupIP
の初期化
src/pkg/net/lookup_windows.go
に以下のグローバル変数が追加されました。
var (
lookupPort = oldLookupPort
lookupIP = oldLookupIP
)
これらの変数は、LookupPort
および LookupIP
関数の実際の内部実装を指す関数ポインタとして機能します。デフォルトでは、従来の oldLookupPort
と oldLookupIP
が割り当てられます。しかし、前述の sysInit()
関数で GetAddrInfoW
が利用可能と判断された場合、これらのポインタは newLookupPort
と newLookupIP
に上書きされます。このメカニズムにより、Goの net
パッケージは、システム環境に応じて透過的に最適な名前解決メカニズムを使用できます。
3. newLookupPort
関数の実装
このコミットの核心は、src/pkg/net/lookup_windows.go
に追加された newLookupPort
関数です。この関数は、サービス名からポート番号を解決するために GetAddrInfoW
を利用します。
- ソケットタイプの設定:
network
引数(例: "tcp4", "udp6")に基づいて、stype
(ソケットタイプ) をsyscall.SOCK_STREAM
(TCP) またはsyscall.SOCK_DGRAM
(UDP) に設定します。 - ヒント構造体の準備:
syscall.AddrinfoW
構造体をhints
として初期化します。Family: syscall.AF_UNSPEC
: アドレスファミリーを特定せず、IPv4とIPv6の両方のアドレスを検索対象とします。Socktype: stype
: 前述で決定したソケットタイプを設定します。Protocol: syscall.IPPROTO_IP
: IPプロトコルを指定します。
GetAddrInfoW
の呼び出し:syscall.GetAddrInfoW(nil, syscall.StringToUTF16Ptr(service), &hints, &result)
を呼び出します。- 最初の引数
nil
は、ホスト名を指定しないことを意味します。ポートルックアップではサービス名のみが必要なためです。 syscall.StringToUTF16Ptr(service)
は、Goの文字列であるサービス名(例: "http")を、GetAddrInfoW
が期待するUTF-16形式のポインタに変換します。&hints
は、検索条件を含むヒント構造体へのポインタです。&result
は、GetAddrInfoW
が結果のアドレス情報リストを格納するポインタです。
- 最初の引数
- エラーハンドリング:
GetAddrInfoW
の呼び出しが失敗した場合、os.NewSyscallError
を使用してエラーを返します。 - 結果の処理:
GetAddrInfoW
が成功した場合、result
には一つ以上のアドレス情報が含まれる可能性があります。このコミットでは、最初の結果 (result
) のアドレス情報を使用します。defer syscall.FreeAddrInfoW(result)
:GetAddrInfoW
によって割り当てられたメモリを解放するために、defer
を使用してFreeAddrInfoW
を呼び出します。これは重要なリソース管理です。addr := unsafe.Pointer(result.Addr)
:result.Addr
は汎用的なソケットアドレス構造体へのポインタです。これをunsafe.Pointer
を介して、具体的なアドレスファミリーに応じた構造体(RawSockaddrInet4
またはRawSockaddrInet6
)にキャストします。- ポート番号の抽出:
syscall.AF_INET
(IPv4) の場合:(*syscall.RawSockaddrInet4)(addr)
にキャストし、a.Port
からポート番号を取得します。syscall.AF_INET6
(IPv6) の場合:(*syscall.RawSockaddrInet6)(addr)
にキャストし、a.Port
からポート番号を取得します。- どちらの場合も、
syscall.Ntohs()
を使用してネットワークバイトオーダーからホストバイトオーダーに変換します。
- 未対応のアドレスファミリー:
AF_INET
またはAF_INET6
以外のファミリーが返された場合、syscall.EINVAL
エラーを返します。
4. oldLookupPort
のリネーム
従来の lookupPort
関数は oldLookupPort
にリネームされました。これは、新しい newLookupPort
との区別を明確にし、動的なAPI選択メカニズムをサポートするためです。
5. テストケースの修正
src/pkg/net/port_test.go
のテストコードがわずかに修正されています。
--- a/src/pkg/net/port_test.go
+++ b/src/pkg/net/port_test.go
@@ -46,7 +46,7 @@ func TestLookupPort(t *testing.T) {
for i := 0; i < len(porttests); i++ {
tt := porttests[i]
if port, err := LookupPort(tt.netw, tt.name); port != tt.port || (err == nil) != tt.ok {
-\t\t\tt.Errorf("LookupPort(%q, %q) = %v, %s; want %v",
+\t\t\tt.Errorf("LookupPort(%q, %q) = %v, %v; want %v",
tt.netw, tt.name, port, err, tt.port)
}
}
エラーメッセージのフォーマット文字列が %s
から %v
に変更されています。これは、err
オブジェクトが error
インターフェースを実装しているため、%v
を使用することでエラーオブジェクトのデフォルトの文字列表現が適切に表示されるようにするためです。機能的な変更ではなく、デバッグ出力の改善です。
コアとなるコードの変更箇所
src/pkg/net/fd_windows.go
sysInit()
関数内で、syscall.LoadGetAddrInfo()
が成功した場合にlookupPort
とlookupIP
を新しい実装 (newLookupPort
,newLookupIP
) に設定する条件分岐が追加されました。
src/pkg/net/lookup_windows.go
- グローバル変数
lookupPort
とlookupIP
が追加され、デフォルトでoldLookupPort
とoldLookupIP
を指すように初期化されました。 - 既存の
lookupPort
関数がoldLookupPort
にリネームされました。 - 新しい関数
newLookupPort(network, service string) (port int, err error)
が追加されました。この関数はGetAddrInfoW
を使用してポートを解決します。
src/pkg/net/port_test.go
TestLookupPort
関数内のt.Errorf
のフォーマット文字列が%s
から%v
に変更されました。
コアとなるコードの解説
このコミットの主要な変更は、Windows環境におけるポートルックアップのロジックを、より現代的で堅牢な GetAddrInfoW
APIを使用するように切り替える点にあります。
-
動的ディスパッチの導入:
src/pkg/net/fd_windows.go
のsysInit()
関数は、Goプログラムが起動する際に一度だけ実行される初期化ルーチンです。ここでsyscall.LoadGetAddrInfo()
を呼び出すことで、実行中のWindowsシステムがGetAddrInfoW
APIをサポートしているかどうかが確認されます。- もし
GetAddrInfoW
が利用可能であれば(syscall.LoadGetAddrInfo()
がnil
を返す)、Goのnet
パッケージ内部で使用されるlookupPort
およびlookupIP
という関数ポインタが、それぞれ新しい実装であるnewLookupPort
およびnewLookupIP
に設定されます。 - これにより、Goのネットワークコードは、古いWindowsシステムでは従来の
oldLookupPort
を使用し、新しいWindowsシステムではnewLookupPort
を使用するという、透過的なフォールバックメカニズムを実現しています。
- もし
-
newLookupPort
の実装:src/pkg/net/lookup_windows.go
に追加されたnewLookupPort
関数は、サービス名(例: "http")とネットワークタイプ(例: "tcp")を受け取り、対応するポート番号を返します。- まず、入力されたネットワークタイプ(
tcp4
,tcp6
,udp4
,udp6
)に基づいて、ソケットタイプ(SOCK_STREAM
またはSOCK_DGRAM
)を決定します。 - 次に、
syscall.AddrinfoW
構造体をhints
として準備します。これはGetAddrInfoW
に渡す検索条件を指定するためのものです。Family
をAF_UNSPEC
に設定することで、IPv4とIPv6の両方のアドレスファミリーを検索対象とします。 syscall.GetAddrInfoW
を呼び出し、サービス名(UTF-16形式に変換したもの)とhints
を渡します。この関数は、サービス名に対応するアドレス情報(ポート番号を含む)のリストをresult
に格納します。GetAddrInfoW
が成功した場合、返されたresult
ポインタが指す最初のAddrinfoW
構造体からポート番号を抽出します。result.Addr
は汎用的なソケットアドレスへのポインタであるため、unsafe.Pointer
を介して具体的なソケットアドレス構造体(RawSockaddrInet4
またはRawSockaddrInet6
)に型キャストし、そこからポート番号を取得します。- 取得したポート番号はネットワークバイトオーダーであるため、
syscall.Ntohs()
を使用してホストバイトオーダーに変換してから返します。 defer syscall.FreeAddrInfoW(result)
は、GetAddrInfoW
によって動的に割り当てられたメモリを、関数が終了する際に確実に解放するための重要な処理です。
- まず、入力されたネットワークタイプ(
この変更により、Goの net
パッケージはWindows環境でより効率的かつ広範な名前解決機能を利用できるようになり、特にIPv6環境での互換性が向上します。
関連リンク
- Go CL 7252045: https://golang.org/cl/7252045
参考にした情報源リンク
- Microsoft Docs -
GetAddrInfoW
function: https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo - Microsoft Docs -
getservbyname
function: https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getservbyname - Go
syscall
package documentation: https://pkg.go.dev/syscall - Go
net
package documentation: https://pkg.go.dev/net - Go
unsafe
package documentation: https://pkg.go.dev/unsafe - ネットワークバイトオーダーとホストバイトオーダー: https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%83%88%E3%82%AA%E3%83%BC%E3%83%80%E3%83%BC