[インデックス 1386] ファイルの概要
このコミットは、Go言語の初期のネットワークライブラリ (src/lib/net
) において、ネットワークリスニングアドレスの指定方法を改善し、より簡潔な形式をサポートするように変更を加えるものです。具体的には、ポート番号のみを指定する形式(例: ":9999"
)を、すべてのネットワークインターフェースでリッスンする形式(例: "0.0.0.0:9999"
)のエイリアスとして許可するように拡張しています。
コミット
commit 33907d1346f13f0ce30a3b3fce73965df8248b74
Author: Russ Cox <rsc@golang.org>
Date: Fri Dec 19 15:52:21 2008 -0800
allow Listen on ":9999" as an alias for "0.0.0.0:9999"
R=r
DELTA=21 (12 added, 0 deleted, 9 changed)
OCL=21653
CL=21653
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/33907d1346f13f0ce30a3b3fce73965df8248b74
元コミット内容
このコミットの目的は、Go言語のネットワークパッケージにおいて、Listen
操作時にホストアドレスを省略してポート番号のみを指定する記法(例: ":9999"
)をサポートすることです。これにより、開発者は明示的に"0.0.0.0"
(IPv4のすべてのインターフェース)や"[::]"
(IPv6のすべてのインターフェース)を指定することなく、簡潔に「すべての利用可能なインターフェースで指定されたポートをリッスンする」という意図を表現できるようになります。
変更の背景
ネットワークアプリケーションを開発する際、サーバーが特定のポートで接続を待ち受ける場合、通常はどのIPアドレスで待ち受けるかを指定する必要があります。一般的な選択肢としては、特定のIPアドレス(例: "127.0.0.1"
)を指定するか、すべての利用可能なネットワークインターフェースで待ち受けるために"0.0.0.0"
(IPv4の場合)または"[::]"
(IPv6の場合)を指定します。
しかし、多くのプログラミング言語やフレームワークのネットワークライブラリでは、利便性のためにホスト部分を省略してポート番号のみを指定する記法(例: ":8080"
)をサポートしています。この記法は、暗黙的に「すべてのインターフェースでリッスンする」ことを意味し、開発者が冗長な"0.0.0.0"
や"[::]"
を記述する手間を省きます。
このコミットは、Go言語の初期のネットワークパッケージにこの一般的な慣習を取り入れ、APIの使いやすさと直感性を向上させることを目的としています。これにより、Go言語のネットワークプログラミングが他の言語の慣習に近づき、より自然な記述が可能になります。
前提知識の解説
ネットワークアドレスとポート
- IPアドレス: ネットワーク上のデバイスを一意に識別するための番号です。IPv4(例:
192.168.1.1
)とIPv6(例:2001:0db8::1
)があります。 - ポート番号: IPアドレスと組み合わせて、特定のアプリケーションやサービスを識別するための番号です。0から65535までの範囲で、よく知られたサービスには特定のポートが割り当てられています(例: HTTPは80番、HTTPSは443番)。
ホスト:ポート
形式: ネットワークサービスのアドレスは通常、IPアドレス:ポート番号
またはホスト名:ポート番号
の形式で表現されます(例:127.0.0.1:8080
,localhost:8080
)。0.0.0.0
(IPv4): 「Unspecified Address(未指定アドレス)」と呼ばれ、IPv4において「すべての利用可能なネットワークインターフェース」を意味します。サーバーがこのアドレスでリッスンすると、そのマシンが持つすべてのIPアドレス(例:127.0.0.1
、ローカルネットワークのIP、グローバルIPなど)からの接続を受け入れます。[::]
(IPv6): IPv6における「Unspecified Address」です。IPv4の0.0.0.0
と同様に、すべての利用可能なIPv6ネットワークインターフェースからの接続を受け入れます。角括弧は、IPv6アドレスがコロンを含むため、ホストとポートを区別するために必要です。
ネットワークソケットプログラミングの基本
- ソケット: ネットワーク通信のエンドポイントです。アプリケーションはソケットを通じてデータの送受信を行います。
bind
: ソケットを特定のIPアドレスとポート番号に「結合」する操作です。これにより、そのソケットが指定されたアドレスとポートで接続を待ち受ける準備ができます。listen
:bind
されたソケットを、着信接続を待ち受ける状態にする操作です。accept
:listen
状態のソケットで、新しい着信接続を受け入れる操作です。これにより、クライアントとの個別の通信に使用される新しいソケットが作成されます。connect
: クライアント側で、リモートサーバーのソケットに接続を確立する操作です。
Go言語の初期のネットワークパッケージ
このコミットは2008年に行われたものであり、Go言語がまだ公開される前の非常に初期の段階のコードベースに対する変更です。当時のGoのネットワークパッケージは、現在のnet
パッケージとは異なる構造やAPIを持っていました。しかし、基本的な概念(IPアドレス、ポート、ソケット、Listen/Dial)は共通しています。
技術的詳細
このコミットの主要な技術的変更点は、HostPortToIP
関数にmode
引数を追加し、ホスト部分が空文字列の場合の挙動を"listen"
モードとそれ以外のモードで分岐させることです。
-
HostPortToIP
関数のシグネチャ変更:- 変更前:
func HostPortToIP(net string, hostport string) (ip []byte, iport int, err *os.Error)
- 変更後:
func HostPortToIP(net, hostport, mode string) (ip []byte, iport int, err *os.Error)
- 新たに
mode
という文字列引数が追加されました。この引数は、アドレスが「リッスン」目的で使用されるのか、「ダイヤル(接続)」目的で使用されるのかを示すために利用されます。
- 変更前:
-
HostPortToIP
関数内のロジック変更:SplitHostPort
関数でhostport
文字列がhost
とport
に分割された後、host
が空文字列(""
)であるかどうかがチェックされます。- もし
host
が空文字列であり、かつmode
が"listen"
である場合、addr
変数にIPnoaddr
(おそらく0.0.0.0
または::
を表す内部定数)が設定されます。これは、ホストが指定されていない場合は「すべてのインターフェースでリッスンする」という意図を表現します。 host
が空文字列であり、かつmode
が"listen"
でない場合(つまり"dial"
モードなど)、MissingAddress
エラーが返されます。これは、接続を開始する際には接続先ホストが明確に指定されている必要があるためです。- この変更により、
":9999"
のような形式がListen
操作で有効になり、Dial
操作では引き続き完全なアドレス指定が求められるようになります。
-
InternetSocket
関数のシグネチャ変更と呼び出し元の更新:InternetSocket
関数もHostPortToIP
関数と同様にmode
引数を追加し、その引数をHostPortToIP
に渡すように変更されました。DialTCP
、DialUDP
、ListenTCP
といった高レベルのネットワーク関数がInternetSocket
を呼び出す際に、それぞれの操作の性質に応じて"dial"
または"listen"
という文字列リテラルをmode
引数として渡すようになりました。DialTCP
とDialUDP
は"dial"
を渡します。ListenTCP
は"listen"
を渡します。
-
SplitHostPort
関数の堅牢性向上:host[0] == '['
という条件の前にlen(host) > 0
というチェックが追加されました。これにより、空のhost
文字列に対してhost[0]
にアクセスしようとした際のパニック(実行時エラー)を防ぎ、より安全なコードになっています。
-
テストケースの追加:
src/lib/net/tcpserver_test.go
にDoTest(t, "tcp", ":9997", "127.0.0.1:9997");
という新しいテストケースが追加されました。これは、":9997"
という簡潔な形式が正しく127.0.0.1:9997
(または0.0.0.0:9997
)として解釈され、接続が確立できることを検証します。
これらの変更により、Go言語のネットワークAPIは、より直感的で一般的なネットワークプログラミングの慣習に沿ったものとなり、開発者の利便性が向上しました。
コアとなるコードの変更箇所
src/lib/net/net.go
--- a/src/lib/net/net.go
+++ b/src/lib/net/net.go
@@ -41,7 +41,7 @@ func SplitHostPort(hostport string) (host, port string, err *os.Error) {
port = hostport[i+1:len(hostport)];
// Can put brackets around host ...
- if host[0] == '[' && host[len(host)-1] == ']' {
+ if len(host) > 0 && host[0] == '[' && host[len(host)-1] == ']' {
host = host[1:len(host)-1]
} else {
// ... but if there are no brackets, no colons.
@@ -65,15 +65,26 @@ func JoinHostPort(host, port string) string {
// 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) {
+func HostPortToIP(net, hostport, mode string) (ip []byte, iport int, err *os.Error) {
var host, port string;
host, port, err = SplitHostPort(hostport);
if err != nil {
return nil, 0, err
}
+ var addr []byte;
+ if host == "" {
+ if mode == "listen" {
+ addr = IPnoaddr; // wildcard - listen to all
+ } else {
+ return nil, 0, MissingAddress;
+ }
+ }
+
// Try as an IP address.
- addr := ParseIP(host);
+ if addr == nil {
+ addr = ParseIP(host);
+ }
if addr == nil {
// Not an IP address. Try as a DNS name.
hostname, addrs, err := LookupHost(host);
@@ -279,20 +290,20 @@ func (c *ConnBase) SetLinger(sec int) *os.Error {
// PreferIPv4 here should fall back to the IPv4 socket interface when possible.
const PreferIPv4 = false
-func InternetSocket(net, laddr, raddr string, proto int64) (fd *FD, err *os.Error) {
+func InternetSocket(net, laddr, raddr string, proto int64, mode string) (fd *FD, err *os.Error) {
// Parse addresses (unless they are empty).
var lip, rip []byte;
var lport, rport int;
var lerr, rerr *os.Error;
if laddr != "" {
- lip, lport, lerr = HostPortToIP(net, laddr);
+ lip, lport, lerr = HostPortToIP(net, laddr, mode);
if lerr != nil {
return nil, lerr
}
}
if raddr != "" {
- rip, rport, rerr = HostPortToIP(net, raddr);
+ rip, rport, rerr = HostPortToIP(net, raddr, mode);
if rerr != nil {
return nil, rerr
}
@@ -370,7 +381,7 @@ export func DialTCP(net, laddr, raddr string) (c *ConnTCP, err *os.Error) {
if raddr == "" {
return nil, MissingAddress
}
- fd, e := InternetSocket(net, laddr, raddr, syscall.SOCK_STREAM);
+ fd, e := InternetSocket(net, laddr, raddr, syscall.SOCK_STREAM, "dial");
if e != nil {
return nil, e
}
@@ -397,7 +408,7 @@ export func DialUDP(net, laddr, raddr string) (c *ConnUDP, err *os.Error) {
if raddr == "" {
return nil, MissingAddress
}
- fd, e := InternetSocket(net, laddr, raddr, syscall.SOCK_DGRAM);
+ fd, e := InternetSocket(net, laddr, raddr, syscall.SOCK_DGRAM, "dial");
if e != nil {
return nil, e
}
@@ -477,7 +488,7 @@ export type ListenerTCP struct {
}
export func ListenTCP(net, laddr string) (l *ListenerTCP, err *os.Error) {
- fd, e := InternetSocket(net, laddr, "", syscall.SOCK_STREAM);
+ fd, e := InternetSocket(net, laddr, "", syscall.SOCK_STREAM, "listen");
if e != nil {
return nil, e
}
src/lib/net/tcpserver_test.go
--- a/src/lib/net/tcpserver_test.go
+++ b/src/lib/net/tcpserver_test.go
@@ -79,6 +79,7 @@ export func TestTcpServer(t *testing.T) {
DoTest(t, "tcp", "0.0.0.0:9997", "127.0.0.1:9997");
DoTest(t, "tcp", "[::]:9997", "[::ffff:127.0.0.1]:9997");
DoTest(t, "tcp", "[::]:9997", "127.0.0.1:9997");
+ DoTest(t, "tcp", ":9997", "127.0.0.1:9997");
DoTest(t, "tcp", "0.0.0.0:9997", "[::ffff:127.0.0.1]:9997");
}
コアとなるコードの解説
src/lib/net/net.go
-
SplitHostPort
関数の修正:if len(host) > 0 && host[0] == '[' && host[len(host)-1] == ']' {
この変更は、host
文字列が空の場合にhost[0]
にアクセスしようとして発生する可能性のあるランタイムパニックを防ぐための防御的な修正です。len(host) > 0
のチェックが追加されたことで、より堅牢になりました。 -
HostPortToIP
関数の変更:- シグネチャの変更:
func HostPortToIP(net, hostport, mode string) (ip []byte, iport int, err *os.Error)
mode
引数が追加され、この関数がアドレスを解析する目的("listen"
または"dial"
)を区別できるようになりました。 - ホストが空の場合の処理:
このブロックがこのコミットの核心です。var addr []byte; if host == "" { if mode == "listen" { addr = IPnoaddr; // wildcard - listen to all } else { return nil, 0, MissingAddress; } } // ... if addr == nil { addr = ParseIP(host); }
SplitHostPort
によって解析されたhost
が空文字列(例:":9999"
の場合)であるかをチェックします。- もし
mode
が"listen"
であれば、addr
にIPnoaddr
(すべてのインターフェースを意味する内部定数)を割り当てます。これにより、":9999"
が"0.0.0.0:9999"
(または"[::]:9999"
)として扱われるようになります。 - もし
mode
が"listen"
でなければ(例えば"dial"
の場合)、MissingAddress
エラーを返します。これは、接続を開始する際には接続先ホストが明確である必要があるためです。 if addr == nil { addr = ParseIP(host); }
の部分は、host
が空でなく、かつIPnoaddr
が設定されていない場合に、通常のIPアドレス解析(ParseIP
)を続行することを示しています。
- もし
- シグネチャの変更:
-
InternetSocket
関数の変更:- シグネチャの変更:
func InternetSocket(net, laddr, raddr string, proto int64, mode string) (fd *FD, err *os.Error)
mode
引数が追加され、この関数も呼び出し元から操作の目的を受け取れるようになりました。 HostPortToIP
へのmode
の伝播:lip, lport, lerr = HostPortToIP(net, laddr, mode);
rip, rport, rerr = HostPortToIP(net, raddr, mode);
InternetSocket
は、受け取ったmode
引数をHostPortToIP
関数にそのまま渡します。これにより、アドレス解析のロジックが操作の目的(リッスンかダイヤルか)に基づいて適切に動作するようになります。
- シグネチャの変更:
-
高レベルネットワーク関数の呼び出し変更:
DialTCP
、DialUDP
、ListenTCP
といった関数がInternetSocket
を呼び出す際に、それぞれ適切なmode
文字列を渡すようになりました。DialTCP
とDialUDP
は接続を開始する操作なので、"dial"
を渡します。ListenTCP
は接続を待ち受ける操作なので、"listen"
を渡します。 この明示的なmode
の指定により、HostPortToIP
関数内でホストが空の場合の挙動が制御されます。
src/lib/net/tcpserver_test.go
- 新しいテストケースの追加:
DoTest(t, "tcp", ":9997", "127.0.0.1:9997");
この行は、ホスト部分が省略された":9997"
というアドレスが、期待通りにローカルホストのIPアドレス(127.0.0.1
)に解決され、テストが成功することを確認します。これは、このコミットで導入された新機能が正しく動作することを検証するものです。
これらの変更は、Go言語のネットワークAPIの使いやすさを向上させるとともに、内部的なアドレス解決ロジックをより柔軟かつ堅牢にするための重要なステップでした。
関連リンク
- Go言語の公式ドキュメント(現在の
net
パッケージ): https://pkg.go.dev/net - Go言語の初期のコミット履歴(GitHub): https://github.com/golang/go/commits/master
参考にした情報源リンク
- https://github.com/golang/go/commit/33907d1346f13f0ce30a3b3fce73965df8248b74
- 一般的なネットワークプログラミングにおける
0.0.0.0
や[::]
の概念。 - Go言語の
net
パッケージの進化に関する一般的な知識。