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

[インデックス 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"モードとそれ以外のモードで分岐させることです。

  1. 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という文字列引数が追加されました。この引数は、アドレスが「リッスン」目的で使用されるのか、「ダイヤル(接続)」目的で使用されるのかを示すために利用されます。
  2. HostPortToIP関数内のロジック変更:

    • SplitHostPort関数でhostport文字列がhostportに分割された後、hostが空文字列("")であるかどうかがチェックされます。
    • もしhostが空文字列であり、かつmode"listen"である場合、addr変数にIPnoaddr(おそらく0.0.0.0または::を表す内部定数)が設定されます。これは、ホストが指定されていない場合は「すべてのインターフェースでリッスンする」という意図を表現します。
    • hostが空文字列であり、かつmode"listen"でない場合(つまり"dial"モードなど)、MissingAddressエラーが返されます。これは、接続を開始する際には接続先ホストが明確に指定されている必要があるためです。
    • この変更により、":9999"のような形式がListen操作で有効になり、Dial操作では引き続き完全なアドレス指定が求められるようになります。
  3. InternetSocket関数のシグネチャ変更と呼び出し元の更新:

    • InternetSocket関数もHostPortToIP関数と同様にmode引数を追加し、その引数をHostPortToIPに渡すように変更されました。
    • DialTCPDialUDPListenTCPといった高レベルのネットワーク関数がInternetSocketを呼び出す際に、それぞれの操作の性質に応じて"dial"または"listen"という文字列リテラルをmode引数として渡すようになりました。
      • DialTCPDialUDP"dial"を渡します。
      • ListenTCP"listen"を渡します。
  4. SplitHostPort関数の堅牢性向上:

    • host[0] == '['という条件の前にlen(host) > 0というチェックが追加されました。これにより、空のhost文字列に対してhost[0]にアクセスしようとした際のパニック(実行時エラー)を防ぎ、より安全なコードになっています。
  5. テストケースの追加:

    • src/lib/net/tcpserver_test.goDoTest(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

  1. SplitHostPort関数の修正: if len(host) > 0 && host[0] == '[' && host[len(host)-1] == ']' { この変更は、host文字列が空の場合にhost[0]にアクセスしようとして発生する可能性のあるランタイムパニックを防ぐための防御的な修正です。len(host) > 0のチェックが追加されたことで、より堅牢になりました。

  2. 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"であれば、addrIPnoaddr(すべてのインターフェースを意味する内部定数)を割り当てます。これにより、":9999""0.0.0.0:9999"(または"[::]:9999")として扱われるようになります。
      • もしmode"listen"でなければ(例えば"dial"の場合)、MissingAddressエラーを返します。これは、接続を開始する際には接続先ホストが明確である必要があるためです。
      • if addr == nil { addr = ParseIP(host); } の部分は、hostが空でなく、かつIPnoaddrが設定されていない場合に、通常のIPアドレス解析(ParseIP)を続行することを示しています。
  3. 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関数にそのまま渡します。これにより、アドレス解析のロジックが操作の目的(リッスンかダイヤルか)に基づいて適切に動作するようになります。
  4. 高レベルネットワーク関数の呼び出し変更:

    • DialTCPDialUDPListenTCPといった関数がInternetSocketを呼び出す際に、それぞれ適切なmode文字列を渡すようになりました。
      • DialTCPDialUDPは接続を開始する操作なので、"dial"を渡します。
      • ListenTCPは接続を待ち受ける操作なので、"listen"を渡します。 この明示的なmodeの指定により、HostPortToIP関数内でホストが空の場合の挙動が制御されます。

src/lib/net/tcpserver_test.go

  1. 新しいテストケースの追加: DoTest(t, "tcp", ":9997", "127.0.0.1:9997"); この行は、ホスト部分が省略された":9997"というアドレスが、期待通りにローカルホストのIPアドレス(127.0.0.1)に解決され、テストが成功することを確認します。これは、このコミットで導入された新機能が正しく動作することを検証するものです。

これらの変更は、Go言語のネットワークAPIの使いやすさを向上させるとともに、内部的なアドレス解決ロジックをより柔軟かつ堅牢にするための重要なステップでした。

関連リンク

参考にした情報源リンク