[インデックス 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システムがGetAddrInfoWAPIをサポートしているかどうかが確認されます。- もし
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 -
GetAddrInfoWfunction: https://learn.microsoft.com/en-us/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo - Microsoft Docs -
getservbynamefunction: https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-getservbyname - Go
syscallpackage documentation: https://pkg.go.dev/syscall - Go
netpackage documentation: https://pkg.go.dev/net - Go
unsafepackage 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