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

[インデックス 16253] ファイルの概要

コミット

commit ca6b1f3eeb8f8bd658b6bd5b425184b65d42bc8c
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Tue Apr 30 17:47:39 2013 -0700

    net: do not call syscall.Bind twice on windows
    
    Fixes #5355.
    
    R=golang-dev, mikioh.mikioh, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/8966046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ca6b1f3eeb8f8bd658b6bd5b425184b65d42bc8c

元コミット内容

net: do not call syscall.Bind twice on windows

このコミットは、Windows環境においてネットワーク関連の処理で syscall.Bind が二重に呼び出される問題を修正します。

変更の背景

このコミットは、Go言語のネットワークパッケージ (net) におけるWindows固有のバグを修正するために作成されました。コミットメッセージに Fixes #5355 とあるように、GoプロジェクトのIssue 5355で報告された問題に対応しています。

具体的には、Windowsの ConnectEx APIを使用する際に、ソケットが事前にバインドされている必要があるという要件があります。しかし、既存の実装では、ソケットが既にバインドされているにもかかわらず、syscall.Bind が再度呼び出される可能性がありました。この二重バインドは、予期せぬエラーやパフォーマンスの問題を引き起こす可能性がありました。

この問題は、特に Dialer を使用してローカルアドレスを指定して接続を試みる場合に顕著に現れたと考えられます。DialerLocalAddr フィールドが設定されている場合、Goのネットワークスタックは指定されたローカルアドレスにソケットをバインドしようとします。しかし、ConnectEx の内部処理で再度バインドが試みられることで、問題が発生していました。

前提知識の解説

Go言語の net パッケージ

Go言語の net パッケージは、ネットワークI/Oプリミティブを提供します。TCP/IP、UDP、Unixドメインソケットなど、様々なネットワークプロトコルを扱うための機能が含まれています。このパッケージは、低レベルのソケット操作を抽象化し、開発者が簡単にネットワークアプリケーションを構築できるように設計されています。

syscall パッケージ

syscall パッケージは、オペレーティングシステムが提供する低レベルのシステムコールへのインターフェースを提供します。これにより、GoプログラムからOS固有の機能(ファイル操作、プロセス管理、ネットワークソケット操作など)を直接呼び出すことができます。ネットワークプログラミングにおいては、ソケットの作成、バインド、接続、リスニングなどの操作に syscall パッケージが利用されます。

syscall.Bind

syscall.Bind は、ソケットを特定のローカルアドレス(IPアドレスとポート番号)にバインドするためのシステムコールです。サーバーアプリケーションが特定のポートで接続を待ち受ける場合や、クライアントが特定のローカルポートから接続を開始する場合に利用されます。ソケットはバインドされることで、そのアドレスとポートを通じて通信が可能になります。

Windowsの ConnectEx API

ConnectEx は、WindowsソケットAPI (Winsock) の拡張関数の一つで、非同期I/O操作を効率的に行うために設計されています。特に、クライアントソケットの接続処理を高速化するために使用されます。ConnectEx を使用するソケットは、事前に Bind 関数によってローカルアドレスにバインドされている必要があります。この要件は、従来の connect 関数とは異なる点であり、ConnectEx の効率的な動作を保証するために重要です。

DialerLocalAddr

Goの net パッケージにおける Dialer 構造体は、ネットワーク接続を確立するための設定をカプセル化します。DialerLocalAddr フィールドは、発信接続に使用するローカルアドレスを指定するために使用されます。このフィールドが設定されている場合、Goは接続を確立する前に、指定されたローカルアドレスにソケットをバインドしようとします。

技術的詳細

このコミットの核心は、Windows環境における netFD.connect メソッドの変更にあります。netFD.connect は、ネットワーク接続を確立する際に内部的に呼び出される関数です。

変更前は、netFD.connectConnectEx を使用する場合、syscall.Bind を無条件に呼び出してソケットをバインドしていました。しかし、DialerLocalAddr が設定されている場合、ソケットは既に socket 関数内でバインドされている可能性があります。このため、netFD.connect 内での syscall.Bind の呼び出しは二重バインドとなり、エラーを引き起こす原因となっていました。

修正では、netFD.connect のシグネチャが変更され、la (ローカルアドレス) パラメータが追加されました。この la パラメータは、ソケットが既にバインドされているかどうかを示すために使用されます。

具体的には、fd_windows.gonetFD.connect 関数内で、la == nil の場合にのみ syscall.Bind を呼び出すように変更されました。これにより、ソケットが既にバインドされている場合は二重バインドが回避され、ConnectEx の要件を満たしつつ、不要な syscall.Bind の呼び出しによる問題を解決しています。

また、fd_unix.gosock_posix.goconnect メソッドのシグネチャも同様に変更されていますが、これは主にインターフェースの一貫性を保つためであり、Windows固有のバグ修正とは直接関係ありません。Unix系のシステムでは ConnectEx のような事前バインドの要件がないため、la パラメータは通常 nil として渡されます。

dial_test.go に追加された TestDialer は、この修正が正しく機能することを確認するためのテストケースです。このテストは、Dialer を使用してローカルアドレスを指定し、TCP接続を確立するシナリオをシミュレートします。これにより、修正が DialerLocalAddrConnectEx の組み合わせで発生していた問題を解決していることを検証します。

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

  • src/pkg/net/dial_test.go: 新しいテストケース TestDialer の追加。
  • src/pkg/net/fd_unix.go: netFD.connect 関数のシグネチャ変更 (la パラメータの追加)。
  • src/pkg/net/fd_windows.go: netFD.connect 関数のシグネチャ変更と、syscall.Bind の呼び出しロジックの修正。
  • src/pkg/net/sock_posix.go: fd.connect の呼び出し箇所で la パラメータを渡すように変更。

コアとなるコードの解説

src/pkg/net/fd_windows.go の変更

--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -364,22 +364,23 @@ func (o *connectOp) Name() string {
 	return "ConnectEx"
 }
 
-func (fd *netFD) connect(ra syscall.Sockaddr) error {
+func (fd *netFD) connect(la, ra syscall.Sockaddr) error {
 	if !canUseConnectEx(fd.net) {
 		return syscall.Connect(fd.sysfd, ra)
 	}
 	// ConnectEx windows API requires an unconnected, previously bound socket.
-	var la syscall.Sockaddr
-	switch ra.(type) {
-	case *syscall.SockaddrInet4:
-		la = &syscall.SockaddrInet4{}
-	case *syscall.SockaddrInet6:
-		la = &syscall.SockaddrInet6{}
-	default:
-		panic("unexpected type in connect")
-	}
-	if err := syscall.Bind(fd.sysfd, la); err != nil {
-		return err
+	if la == nil { // ここが変更点
+		switch ra.(type) {
+		case *syscall.SockaddrInet4:
+			la = &syscall.SockaddrInet4{}
+		case *syscall.SockaddrInet6:
+			la = &syscall.SockaddrInet6{}
+		default:
+			panic("unexpected type in connect")
+		}
+		if err := syscall.Bind(fd.sysfd, la); err != nil {
+			return err
+		}
 	}
 	// Call ConnectEx API.
 	var o connectOp

この変更が最も重要です。

  • func (fd *netFD) connect(ra syscall.Sockaddr) errorfunc (fd *netFD) connect(la, ra syscall.Sockaddr) error に変更され、la (ローカルアドレス) パラメータが追加されました。
  • 以前は無条件に syscall.Bind(fd.sysfd, la) が呼び出されていましたが、変更後は if la == nil という条件が追加されました。
  • この条件により、lanil でない(つまり、ソケットが既にローカルアドレスにバインドされている)場合は、syscall.Bind の呼び出しがスキップされます。これにより、二重バインドの問題が解決されます。
  • lanil の場合は、以前と同様に ra の型に基づいて適切な Sockaddr が作成され、ソケットがバインドされます。

src/pkg/net/dial_test.go の変更

--- a/src/pkg/net/dial_test.go
+++ b/src/pkg/net/dial_test.go
@@ -372,3 +372,38 @@ func TestDialFailPDLeak(t *testing.T) {
 		}
 	}
 }
+
+func TestDialer(t *testing.T) {
+	ln, err := Listen("tcp4", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("Listen failed: %v", err)
+	}
+	defer ln.Close()
+	ch := make(chan error, 1)
+	go func() {
+		var err error
+		c, err := ln.Accept()
+		if err != nil {
+			ch <- fmt.Errorf("Accept failed: %v", err)
+			return
+		}
+		defer c.Close()
+		ch <- nil
+	}()
+
+	laddr, err := ResolveTCPAddr("tcp4", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("ResolveTCPAddr failed: %v", err)
+	}
+	d := &Dialer{LocalAddr: laddr} // ここでLocalAddrを指定
+	c, err := d.Dial("tcp4", ln.Addr().String())
+	if err != nil {
+		t.Fatalf("Dial failed: %v", err)
+	}
+	defer c.Close()
+	c.Read(make([]byte, 1))
+	err = <-ch
+	if err != nil {
+		t.Error(err)
+	}
+}

このテストケースは、Dialer を使用し、LocalAddr127.0.0.1:0 (OSが利用可能なポートを割り当てる) に設定して接続を試みます。これにより、ソケットが事前にバインドされるシナリオを再現し、fd_windows.go の修正が正しく機能することを確認します。

関連リンク

参考にした情報源リンク