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

[インデックス 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サポート: GetAddrInfoWW はワイド文字(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.gosysInit() 関数が変更されています。この関数は、Goプログラムが起動する際にシステム固有の初期化を行うために呼び出されます。 変更点:

	if syscall.LoadGetAddrInfo() == nil {
		lookupPort = newLookupPort
		lookupIP = newLookupIP
	}

syscall.LoadGetAddrInfo() は、システムが GetAddrInfoW 関数をロードできるかどうかをチェックします。もしロードに成功した場合(つまり、LoadGetAddrInfo()nil を返す場合)、それはシステムが GetAddrInfoW をサポートしていることを意味します。この条件が真の場合、グローバル変数 lookupPortlookupIP が、それぞれ新しい実装である newLookupPortnewLookupIP に設定されます。これにより、Goのネットワークスタックは、実行時に利用可能な最適なAPIを動的に選択できるようになります。古いWindowsバージョンでは GetAddrInfoW が利用できないため、このチェックによって後方互換性が保たれます。

2. lookupPortlookupIP の初期化

src/pkg/net/lookup_windows.go に以下のグローバル変数が追加されました。

var (
	lookupPort = oldLookupPort
	lookupIP   = oldLookupIP
)

これらの変数は、LookupPort および LookupIP 関数の実際の内部実装を指す関数ポインタとして機能します。デフォルトでは、従来の oldLookupPortoldLookupIP が割り当てられます。しかし、前述の sysInit() 関数で GetAddrInfoW が利用可能と判断された場合、これらのポインタは newLookupPortnewLookupIP に上書きされます。このメカニズムにより、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() が成功した場合に lookupPortlookupIP を新しい実装 (newLookupPort, newLookupIP) に設定する条件分岐が追加されました。

src/pkg/net/lookup_windows.go

  • グローバル変数 lookupPortlookupIP が追加され、デフォルトで oldLookupPortoldLookupIP を指すように初期化されました。
  • 既存の 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を使用するように切り替える点にあります。

  1. 動的ディスパッチの導入: src/pkg/net/fd_windows.gosysInit() 関数は、Goプログラムが起動する際に一度だけ実行される初期化ルーチンです。ここで syscall.LoadGetAddrInfo() を呼び出すことで、実行中のWindowsシステムが GetAddrInfoW APIをサポートしているかどうかが確認されます。

    • もし GetAddrInfoW が利用可能であれば(syscall.LoadGetAddrInfo()nil を返す)、Goの net パッケージ内部で使用される lookupPort および lookupIP という関数ポインタが、それぞれ新しい実装である newLookupPort および newLookupIP に設定されます。
    • これにより、Goのネットワークコードは、古いWindowsシステムでは従来の oldLookupPort を使用し、新しいWindowsシステムでは newLookupPort を使用するという、透過的なフォールバックメカニズムを実現しています。
  2. newLookupPort の実装: src/pkg/net/lookup_windows.go に追加された newLookupPort 関数は、サービス名(例: "http")とネットワークタイプ(例: "tcp")を受け取り、対応するポート番号を返します。

    • まず、入力されたネットワークタイプ(tcp4, tcp6, udp4, udp6)に基づいて、ソケットタイプ(SOCK_STREAM または SOCK_DGRAM)を決定します。
    • 次に、syscall.AddrinfoW 構造体を hints として準備します。これは GetAddrInfoW に渡す検索条件を指定するためのものです。FamilyAF_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環境での互換性が向上します。

関連リンク

参考にした情報源リンク