[インデックス 1365] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージに、ホスト名とポート名のルックアップ機能を追加するものです。これにより、IPアドレスやポート番号の直接指定だけでなく、DNSによるホスト名解決や/etc/services
ファイルに基づくポート名解決が可能になり、ネットワークプログラミングの利便性と柔軟性が大幅に向上しました。
コミット
commit 83348f956ec9eeb1a9202d135c680f36d1f39a9c
Author: Russ Cox <rsc@golang.org>
Date: Thu Dec 18 15:42:39 2008 -0800
host and port name lookup
R=r,presotto
DELTA=1239 (935 added, 281 deleted, 23 changed)
OCL=21041
CL=21539
---
src/lib/net/Makefile | 35 +++-
src/lib/net/dialgoogle_test.go | 89 +++++++++
src/lib/net/dnsclient.go | 215 +++++++++++++++++++++
src/lib/net/dnsconfig.go | 109 +++++++++++
src/lib/net/dnsmsg.go | 8 +
src/lib/net/ip.go | 60 +-----\n src/lib/net/ip_test.go | 53 +++++
src/lib/net/net.go | 71 +++----\n src/lib/net/parse.go | 156 +++++++++++++++
src/lib/net/parse_test.go | 46 +++++
src/lib/net/port.go | 68 +++++++
src/lib/net/port_test.go | 59 ++++++\n test/tcpserver.go => src/lib/net/tcpserver_test.go | 49 ++---\n src/run.bash | 1 +\n test/dialgoogle.go | 111 -----------\n 15 files changed, 886 insertions(+), 244 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/83348f956ec9eeb1a9202d135c680f36d1f39a9c
元コミット内容
host and port name lookup
R=r,presotto
DELTA=1239 (935 added, 281 deleted, 23 changed)
OCL=21041
CL=21539
変更の背景
このコミットが行われた2008年12月は、Go言語がまだ一般に公開される前の初期開発段階でした。当時のnet
パッケージは、ネットワーク接続を確立するためにIPアドレスとポート番号を直接指定する必要がありました。しかし、実際のアプリケーション開発では、人間が覚えやすいホスト名(例: www.google.com
)やサービス名(例: http
、ftp
)を使用して接続先を指定する方が一般的であり、利便性が高いです。
このコミットの背景には、Go言語のネットワークスタックをより実用的にし、一般的なネットワークアプリケーションの要件を満たすための基盤を構築するという目的がありました。具体的には、以下の機能が不足していました。
- ホスト名解決 (DNSルックアップ): ホスト名から対応するIPアドレスを取得する機能。これはインターネット上のサービスに接続する上で不可欠です。
- ポート名解決:
/etc/services
のようなシステムファイルに定義されたサービス名(例:http
はポート80)からポート番号を取得する機能。これにより、開発者はポート番号を直接覚える必要がなくなります。
これらの機能が導入されることで、Go言語のnet
パッケージはより高レベルな抽象化を提供し、開発者がより簡単にネットワークアプリケーションを構築できるようになります。
前提知識の解説
このコミットの変更内容を理解するためには、以下の前提知識が必要です。
1. DNS (Domain Name System)
DNSは、インターネット上のコンピュータやサービスに割り当てられたドメイン名(例: www.example.com
)を、コンピュータが通信に利用するIPアドレス(例: 192.0.2.1
)に変換(名前解決)するための分散型データベースシステムです。
- DNSクエリ: クライアントがDNSサーバーに対して名前解決を要求するプロセス。
- DNSレコード: ドメイン名とそれに関連する情報(IPアドレス、メールサーバーなど)をマッピングするデータ。
- Aレコード: ホスト名とIPv4アドレスのマッピング。
- AAAAレコード: ホスト名とIPv6アドレスのマッピング。
- CNAMEレコード: あるドメイン名が別のドメイン名のエイリアスであることを示す。名前解決の際に、エイリアス先のドメイン名で再度ルックアップが行われます。
- リゾルバ: クライアントからの名前解決要求を受け付け、DNSサーバーに問い合わせを行うソフトウェアモジュール。通常、オペレーティングシステムに組み込まれています。
/etc/resolv.conf
: Unix系システムにおいて、DNSリゾルバの設定(使用するDNSサーバーのIPアドレス、検索ドメインなど)を記述するファイル。
2. /etc/services
ファイル
/etc/services
は、Unix系システムにおいて、サービス名とそれに対応するポート番号およびプロトコル(TCP/UDP)のマッピングを定義するファイルです。例えば、http
は通常TCPポート80にマッピングされます。これにより、アプリケーションはポート番号の代わりにサービス名を使用して接続先を指定できます。
3. Go言語の初期のパッケージ構造
Go言語の初期(2008年頃)は、現在のパッケージ構造とは異なり、標準ライブラリのソースコードはsrc/lib
ディレクトリ以下に配置されていました。例えば、net
パッケージはsrc/lib/net
にありました。また、テストファイルもtest
ディレクトリに置かれていることがありましたが、このコミットでは一部のテストが対応するパッケージのディレクトリ内に移動されています。
4. Makefile
とビルドプロセス
Go言語の初期のビルドシステムは、現在のようなgo build
コマンドが確立される前であり、Makefile
が重要な役割を果たしていました。Makefile
は、ソースファイルのコンパイル順序や依存関係を定義し、ライブラリ(.a
ファイル)の作成を制御していました。このコミットでは、新しいソースファイルが追加されたため、net
パッケージのMakefile
が更新されています。
5. once.Do
関数
sync
パッケージ(当時はonce
パッケージとして独立していた可能性もある)のonce.Do
関数は、指定された関数がプログラムの実行中に一度だけ実行されることを保証するためのメカニズムです。これは、設定ファイルの読み込みやリソースの初期化など、一度だけ行われるべき処理に非常に有用です。このコミットでは、DNS設定やサービスポートの読み込みに利用されています。
技術的詳細
このコミットは、Go言語のnet
パッケージに以下の主要な機能とコンポーネントを導入しています。
1. DNSクライアントの実装 (dnsclient.go
, dnsconfig.go
, dnsmsg.go
)
dnsmsg.go
: DNSメッセージのヘッダ、質問、回答などの構造体と、DNSレコードタイプ(A, CNAMEなど)、クラス(INET)、レスポンスコード(Success, NameErrorなど)の定数を定義しています。これはDNSプロトコルのパケットフォーマットをGoの構造体で表現するための基盤です。dnsconfig.go
:/etc/resolv.conf
ファイルを解析し、DNSサーバーのリスト、検索ドメイン、ndots(絶対パスと見なすドットの数)、タイムアウト、リトライ回数などの設定を読み込む機能を提供します。これにより、システムのリゾルバ設定をGoアプリケーションが利用できるようになります。ParseIP
関数を使用して、設定ファイル内のネームサーバーがIPアドレスであることを検証しています。dnsclient.go
: 実際のDNSクエリを送信し、応答を処理するDNSクライアントロジックを実装しています。Exchange
関数: DNSサーバーへのUDP接続を確立し、DNSクエリメッセージを送信して応答を受信します。リトライロジックも含まれています。Answer
関数: 受信したDNS応答メッセージから、指定されたホスト名に対応するIPアドレス(Aレコード)やCNAMEレコードを抽出します。CNAMEレコードが見つかった場合は、そのエイリアス名で再帰的に解決を試みるロジック(最大10回のリダイレクトループ検出)も含まれています。TryOneName
関数: 単一のDNSサーバーに対して名前解決を試みます。LookupHost
関数:net
パッケージの外部に公開される主要なDNSルックアップ関数です。dnsconfig.go
で読み込んだ設定(検索ドメインなど)に基づいて、ホスト名の解決を試みます。まず、ホスト名がルート化されているか(末尾にドットがあるか)または十分なドット数を持つ場合は、そのままの名前で解決を試みます。それ以外の場合は、設定された検索ドメインを付加して解決を試みます。
2. ポート名ルックアップの実装 (port.go
)
port.go
:/etc/services
ファイルを解析し、サービス名とポート番号のマッピングを読み込む機能を提供します。ReadServices
関数:/etc/services
ファイルを読み込み、サービス名、プロトコル(tcp/udp)、ポート番号のマップをメモリに構築します。コメント行(#
以降)は無視されます。LookupPort
関数: 指定されたプロトコル(tcp
,udp
,tcp4
,tcp6
,udp4
,udp6
)とサービス名に基づいて、対応するポート番号をルックアップします。once.Do
を使用して、ReadServices
が一度だけ実行されることを保証しています。
3. ユーティリティ関数の抽出と追加 (parse.go
)
parse.go
: 文字列操作やファイルI/Oに関する汎用的なユーティリティ関数が追加されています。これらは元々ip.go
や他のファイルに散らばっていたり、新しく必要になったりしたものです。File
型と関連メソッド (Open
,Close
,ReadLine
,GetLineFromData
): ファイルを効率的に行単位で読み込むための簡易的な実装。ByteIndex
,CountAnyByte
,SplitAtBytes
,GetFields
: 文字列内のバイト検索、バイト出現回数カウント、指定バイトでの文字列分割、空白文字でのフィールド分割など、文字列処理に役立つ関数。Atoi
,Xtoi
: 10進数および16進数文字列を整数に変換する関数。これらは元々ip.go
に存在していましたが、より汎用的なparse.go
に移動されました。
4. net
パッケージの変更 (net.go
, ip.go
)
net.go
:HostPortToIP
関数が大幅に強化されました。以前はホストとポートが数値リテラルである必要がありましたが、この変更により、ホスト名に対してLookupHost
を呼び出してDNS解決を行い、ポート名に対してLookupPort
を呼び出してサービスポート解決を行うようになりました。これにより、"www.google.com:http"
のような文字列を直接処理できるようになりました。SplitHostPort
とJoinHostPort
関数が、新しいparse.go
のByteIndex
関数を利用するように変更されました。LookupHost
関数がnet
パッケージの外部に公開されました。
ip.go
:Atoi
とXtoi
関数がparse.go
に移動され、ip.go
からはそれらの関数を呼び出すように変更されました。
5. テストの追加と整理
dialgoogle_test.go
: 新しいDNSルックアップ機能を使用してGoogleに接続するテストが追加されました。IPv4およびIPv6アドレス、ホスト名、サービス名(http
)など、様々な形式のアドレス指定をテストしています。ip_test.go
:ParseIP
関数のテストが追加され、IPv4およびIPv6アドレスのパースが正しく行われることを検証しています。parse_test.go
: 新しく追加されたparse.go
のReadLine
関数のテストが追加されました。port_test.go
:LookupPort
関数のテストが追加され、/etc/services
からのポートルックアップが正しく行われることを検証しています。tcpserver.go
がsrc/lib/net/tcpserver_test.go
に移動され、net
パッケージのテストスイートの一部となりました。
コアとなるコードの変更箇所
このコミットのコアとなる変更は、src/lib/net/net.go
のHostPortToIP
関数と、新しく追加されたsrc/lib/net/dnsclient.go
のLookupHost
関数、そしてsrc/lib/net/port.go
のLookupPort
関数です。
src/lib/net/net.go
(変更箇所)
// Convert "host:port" into IP address and port.
// For now, host and port must be numeric literals.
// Eventually, we'll have name resolution.
func HostPortToIP(net string, hostport string) (ip *[]byte, iport int, err *os.Error) {
host, port, err := SplitHostPort(hostport);
if err != nil {
return nil, 0, err
}
- // TODO: Resolve host.
-
+ // Try as an IP address.
addr := ParseIP(host);
if addr == nil {
- return nil, 0, UnknownHost
+ // Not an IP address. Try as a DNS name.
+ hostname, addrs, err := LookupHost(host);
+ if err != nil {
+ return nil, 0, err
+ }
+ if len(addrs) == 0 {
+ return nil, 0, UnknownHost
+ }
+ addr = ParseIP(addrs[0]);
+ if addr == nil {
+ // should not happen
+ return nil, 0, BadAddress
+ }
}
- // TODO: Resolve port.
-
- p, ok := xdtoi(port);
- if !ok || p < 0 || p > 0xFFFF {
- return nil, 0, UnknownPort
+ p, i, ok := Dtoi(port, 0);
+ if !ok || i != len(port) {
+ p, ok = LookupPort(net, port);
+ if !ok {
+ return nil, 0, UnknownPort
+ }
}
+ if p < 0 || p > 0xFFFF {
+ return nil, 0, BadAddress
+ }
return addr, p, nil
}
src/lib/net/dnsclient.go
(新規追加ファイルの一部)
export func LookupHost(name string) (name1 string, addrs *[]string, err *os.Error) {
// TODO(rsc): Pick out obvious non-DNS names to avoid
// sending stupid requests to the server?
once.Do(&LoadConfig);
if cfg == nil {
err = DNS_MissingConfig;
return;
}
// If name is rooted (trailing dot) or has enough dots,
// try it by itself first.
rooted := len(name) > 0 && name[len(name)-1] == '.';
if rooted || strings.count(name, ".") >= cfg.ndots {
rname := name;
if !rooted {
rname += ".";
}
// Can try as ordinary name.
addrs, aerr := TryOneName(cfg, rname);
if aerr == nil {
return rname, addrs, nil;
}
err = aerr;
}
if rooted {
return
}
// Otherwise, try suffixes.
for i := 0; i < len(cfg.search); i++ {
newname := name+"."+cfg.search[i];
if newname[len(newname)-1] != '.' {
newname += "."
}
addrs, aerr := TryOneName(cfg, newname);
if aerr == nil {
return newname, addrs, nil;
}
err = aerr;
}
return
}
src/lib/net/port.go
(新規追加ファイルの一部)
export func LookupPort(netw, name string) (port int, ok bool) {
once.Do(&ReadServices);
switch netw {
case "tcp4", "tcp6":
netw = "tcp";
case "udp4", "udp6":
netw = "udp";
}
m, mok := services[netw];
if !mok {
return
}
port, ok = m[name];
return
}
コアとなるコードの解説
net.go
のHostPortToIP
関数
この関数は、"host:port"
形式の文字列を受け取り、対応するIPアドレス(*[]byte
型)とポート番号(int
型)を返します。変更前はホストとポートが数値リテラルである必要がありましたが、このコミットにより、名前解決のロジックが組み込まれました。
- ホスト名の解決:
- まず、
ParseIP(host)
を呼び出して、host
が直接IPアドレスとしてパースできるか試みます。 - もしIPアドレスとしてパースできない場合(
addr == nil
)、それはホスト名であると判断し、新しく追加されたLookupHost(host)
関数を呼び出してDNSルックアップを行います。 LookupHost
がエラーを返した場合や、解決されたIPアドレスがない場合は、適切なエラー(UnknownHost
など)を返します。LookupHost
が成功した場合、返されたIPアドレスのリストの最初の要素をParseIP
で再度パースし、addr
に設定します。
- まず、
- ポート名の解決:
- まず、
Atoi(port, 0)
を呼び出して、port
が直接数値としてパースできるか試みます。 - もし数値としてパースできない場合、それはサービス名であると判断し、新しく追加された
LookupPort(net, port)
関数を呼び出してポートルックアップを行います。 LookupPort
が失敗した場合、UnknownPort
エラーを返します。- ポート番号が0から65535の範囲外である場合は、
BadAddress
エラーを返します。
- まず、
この変更により、net.Dial
などの高レベルなネットワーク関数が、ホスト名やサービス名を受け付けて、内部で自動的に名前解決を行うことができるようになりました。
dnsclient.go
のLookupHost
関数
この関数は、Go言語のnet
パッケージにおけるホスト名解決の主要なエントリポイントです。
- 設定の読み込み:
once.Do(&LoadConfig)
を使用して、/etc/resolv.conf
からDNS設定(cfg
)を一度だけ読み込みます。設定が読み込めない場合はDNS_MissingConfig
エラーを返します。 - ルート化された名前または十分なドット数:
- ホスト名が末尾にドットを持つ(例:
www.example.com.
)場合、それは「ルート化された名前」と見なされ、絶対的なドメイン名として扱われます。 - または、ホスト名に含まれるドットの数が
cfg.ndots
(/etc/resolv.conf
のoptions ndots
で設定される、デフォルトは1)以上の場合も、絶対的なドメイン名として扱われます。 - これらの条件を満たす場合、
TryOneName
関数を呼び出して、そのままの名前でDNSルックアップを試みます。成功すればその結果を返します。
- ホスト名が末尾にドットを持つ(例:
- 検索ドメインの適用:
- 上記で解決できなかった場合、かつホスト名がルート化されていない場合、
/etc/resolv.conf
のsearch
オプションで指定された検索ドメインをホスト名に付加して、順次DNSルックアップを試みます。 - 例えば、ホスト名が
myhost
で、検索ドメインがexample.com
の場合、myhost.example.com.
として解決を試みます。 - いずれかの検索ドメインで解決に成功すれば、その結果を返します。
- 上記で解決できなかった場合、かつホスト名がルート化されていない場合、
- エラー処理: すべての試行が失敗した場合、最後に発生したエラーを返します。
このロジックは、一般的なUnix系システムのDNSリゾルバの動作(特に/etc/resolv.conf
の設定に基づく検索パスの適用)を模倣しています。
port.go
のLookupPort
関数
この関数は、サービス名からポート番号を解決します。
- サービス設定の読み込み:
once.Do(&ReadServices)
を使用して、/etc/services
ファイルからサービス名とポート番号のマッピングを一度だけ読み込みます。 - プロトコルの正規化:
tcp4
,tcp6
,udp4
,udp6
のようなネットワークタイプが指定された場合、それぞれtcp
またはudp
に正規化します。これは、/etc/services
が通常、tcp
とudp
でサービスを区別するためです。 - マップからのルックアップ: 正規化されたネットワークタイプ(
netw
)とサービス名(name
)を使用して、メモリ上のサービスマップから対応するポート番号を検索します。 - 結果の返却: ポート番号が見つかればその値と
true
を、見つからなければ0とfalse
を返します。
この関数により、開発者は"http"
や"ssh"
といったサービス名を使ってネットワーク接続のポートを指定できるようになり、コードの可読性と移植性が向上します。
関連リンク
- Go言語の初期のコミット履歴 (GitHub)
- DNS (Domain Name System) - Wikipedia
- resolv.conf(5) - Linux man page
- services(5) - Linux man page
参考にした情報源リンク
- Go言語のソースコード (GitHub)
- Go言語の初期の設計に関する議論 (Go Mailing List archivesなど) (具体的なスレッドは特定できませんでしたが、当時の議論の雰囲気を把握するために参照しました)
- DNSプロトコルに関するRFC (RFC 1034, RFC 1035など) (DNSクライアントの実装を理解するために参照しました)
- Unix系システムのネットワーク設定ファイルに関するドキュメント (
/etc/resolv.conf
や/etc/services
の役割を再確認するために参照しました) - Go言語の
sync.Once
のドキュメント (once.Do
の挙動を理解するために参照しました) - Go言語の
net
パッケージのドキュメント (現在のnet
パッケージの機能と比較するために参照しました)