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

[インデックス 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)やサービス名(例: httpftp)を使用して接続先を指定する方が一般的であり、利便性が高いです。

このコミットの背景には、Go言語のネットワークスタックをより実用的にし、一般的なネットワークアプリケーションの要件を満たすための基盤を構築するという目的がありました。具体的には、以下の機能が不足していました。

  1. ホスト名解決 (DNSルックアップ): ホスト名から対応するIPアドレスを取得する機能。これはインターネット上のサービスに接続する上で不可欠です。
  2. ポート名解決: /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"のような文字列を直接処理できるようになりました。
    • SplitHostPortJoinHostPort関数が、新しいparse.goByteIndex関数を利用するように変更されました。
    • LookupHost関数がnetパッケージの外部に公開されました。
  • ip.go: AtoiXtoi関数がparse.goに移動され、ip.goからはそれらの関数を呼び出すように変更されました。

5. テストの追加と整理

  • dialgoogle_test.go: 新しいDNSルックアップ機能を使用してGoogleに接続するテストが追加されました。IPv4およびIPv6アドレス、ホスト名、サービス名(http)など、様々な形式のアドレス指定をテストしています。
  • ip_test.go: ParseIP関数のテストが追加され、IPv4およびIPv6アドレスのパースが正しく行われることを検証しています。
  • parse_test.go: 新しく追加されたparse.goReadLine関数のテストが追加されました。
  • port_test.go: LookupPort関数のテストが追加され、/etc/servicesからのポートルックアップが正しく行われることを検証しています。
  • tcpserver.gosrc/lib/net/tcpserver_test.goに移動され、netパッケージのテストスイートの一部となりました。

コアとなるコードの変更箇所

このコミットのコアとなる変更は、src/lib/net/net.goHostPortToIP関数と、新しく追加されたsrc/lib/net/dnsclient.goLookupHost関数、そしてsrc/lib/net/port.goLookupPort関数です。

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.goHostPortToIP関数

この関数は、"host:port"形式の文字列を受け取り、対応するIPアドレス(*[]byte型)とポート番号(int型)を返します。変更前はホストとポートが数値リテラルである必要がありましたが、このコミットにより、名前解決のロジックが組み込まれました。

  1. ホスト名の解決:
    • まず、ParseIP(host)を呼び出して、hostが直接IPアドレスとしてパースできるか試みます。
    • もしIPアドレスとしてパースできない場合(addr == nil)、それはホスト名であると判断し、新しく追加されたLookupHost(host)関数を呼び出してDNSルックアップを行います。
    • LookupHostがエラーを返した場合や、解決されたIPアドレスがない場合は、適切なエラー(UnknownHostなど)を返します。
    • LookupHostが成功した場合、返されたIPアドレスのリストの最初の要素をParseIPで再度パースし、addrに設定します。
  2. ポート名の解決:
    • まず、Atoi(port, 0)を呼び出して、portが直接数値としてパースできるか試みます。
    • もし数値としてパースできない場合、それはサービス名であると判断し、新しく追加されたLookupPort(net, port)関数を呼び出してポートルックアップを行います。
    • LookupPortが失敗した場合、UnknownPortエラーを返します。
    • ポート番号が0から65535の範囲外である場合は、BadAddressエラーを返します。

この変更により、net.Dialなどの高レベルなネットワーク関数が、ホスト名やサービス名を受け付けて、内部で自動的に名前解決を行うことができるようになりました。

dnsclient.goLookupHost関数

この関数は、Go言語のnetパッケージにおけるホスト名解決の主要なエントリポイントです。

  1. 設定の読み込み: once.Do(&LoadConfig)を使用して、/etc/resolv.confからDNS設定(cfg)を一度だけ読み込みます。設定が読み込めない場合はDNS_MissingConfigエラーを返します。
  2. ルート化された名前または十分なドット数:
    • ホスト名が末尾にドットを持つ(例: www.example.com.)場合、それは「ルート化された名前」と見なされ、絶対的なドメイン名として扱われます。
    • または、ホスト名に含まれるドットの数がcfg.ndots/etc/resolv.confoptions ndotsで設定される、デフォルトは1)以上の場合も、絶対的なドメイン名として扱われます。
    • これらの条件を満たす場合、TryOneName関数を呼び出して、そのままの名前でDNSルックアップを試みます。成功すればその結果を返します。
  3. 検索ドメインの適用:
    • 上記で解決できなかった場合、かつホスト名がルート化されていない場合、/etc/resolv.confsearchオプションで指定された検索ドメインをホスト名に付加して、順次DNSルックアップを試みます。
    • 例えば、ホスト名がmyhostで、検索ドメインがexample.comの場合、myhost.example.com.として解決を試みます。
    • いずれかの検索ドメインで解決に成功すれば、その結果を返します。
  4. エラー処理: すべての試行が失敗した場合、最後に発生したエラーを返します。

このロジックは、一般的なUnix系システムのDNSリゾルバの動作(特に/etc/resolv.confの設定に基づく検索パスの適用)を模倣しています。

port.goLookupPort関数

この関数は、サービス名からポート番号を解決します。

  1. サービス設定の読み込み: once.Do(&ReadServices)を使用して、/etc/servicesファイルからサービス名とポート番号のマッピングを一度だけ読み込みます。
  2. プロトコルの正規化: tcp4, tcp6, udp4, udp6のようなネットワークタイプが指定された場合、それぞれtcpまたはudpに正規化します。これは、/etc/servicesが通常、tcpudpでサービスを区別するためです。
  3. マップからのルックアップ: 正規化されたネットワークタイプ(netw)とサービス名(name)を使用して、メモリ上のサービスマップから対応するポート番号を検索します。
  4. 結果の返却: ポート番号が見つかればその値とtrueを、見つからなければ0とfalseを返します。

この関数により、開発者は"http""ssh"といったサービス名を使ってネットワーク接続のポートを指定できるようになり、コードの可読性と移植性が向上します。

関連リンク

参考にした情報源リンク