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

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

このコミットは、Go言語のネットワークパッケージ(net)におけるWindows環境でのAccept処理の堅牢性を向上させるためのものです。具体的には、AcceptExシステムコールが返す特定のエラー(WSAECONNRESETERROR_NETNAME_DELETED)を無視し、接続の再試行を可能にすることで、不安定なネットワーク条件下でのサーバーアプリケーションの安定性を高めます。

コミット

commit c7ef348bad102b3427b4242018e92eba17d079ba
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Sun Jan 12 12:20:16 2014 +1100

    net: ignore some errors in windows Accept
    
    Fixes #6987
    
    R=golang-codereviews, dvyukov
    CC=golang-codereviews
    https://golang.org/cl/49490043

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

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

元コミット内容

net: ignore some errors in windows Accept

Fixes #6987

R=golang-codereviews, dvyukov
CC=golang-codereviews
https://golang.org/cl/49490043

変更の背景

この変更は、Go言語のIssue #6987に対応するものです。Windows環境でnet.ListenListener.Accept()を使用してTCPサーバーを構築する際、クライアントが接続を確立する前に切断した場合(特にAcceptExが完了する前にリセットされた場合)、AcceptWSAECONNRESETERROR_NETNAME_DELETEDといったエラーを返すことがありました。これらのエラーは、新しい接続試行に関連するものであり、リスナー自体が問題なく動作しているにもかかわらず、Acceptが失敗し、サーバーが新しい接続を受け付けられなくなる可能性がありました。

この挙動は、特に負荷の高い環境や不安定なネットワーク環境において、サーバーアプリケーションの可用性を低下させる原因となります。このコミットの目的は、これらの特定のエラーを無視し、Acceptループを継続させることで、サーバーがより堅牢に動作し、一時的な接続リセットによってサービスが中断されないようにすることです。

前提知識の解説

Windows Sockets (Winsock) と AcceptEx

Windows環境におけるネットワークプログラミングは、Winsock(Windows Sockets API)を通じて行われます。AcceptExはWinsock APIの一部であり、非同期I/O(Overlapped I/O)をサポートする高度な関数です。これは、新しい接続を受け入れる際に、接続されたソケットの作成と、そのソケットに対する最初のデータ受信を効率的に結合するために使用されます。

通常のaccept関数とは異なり、AcceptExはI/O完了ポート(IOCP)と組み合わせて使用されることが多く、多数の同時接続を効率的に処理するサーバーアプリケーションで利用されます。AcceptExは、接続を受け入れるだけでなく、接続されたソケットのローカルアドレスとリモートアドレスの情報を、事前に割り当てられたバッファに書き込む機能も持っています。

エラーコード WSAECONNRESETERROR_NETNAME_DELETED

  • WSAECONNRESET (10054): このエラーは、接続がピアによって強制的にリセットされたことを示します。TCP接続において、相手側が予期せず接続を終了した場合(例えば、RSTパケットを送信した場合)に発生します。AcceptExのコンテキストでは、クライアントが接続を試みたものの、AcceptExが完了する前に切断してしまった場合に発生する可能性があります。

  • ERROR_NETNAME_DELETED (64): このエラーは、ネットワーク接続が失われたことを示します。これは、ネットワークケーブルが抜かれた、ネットワークアダプタが無効になった、またはリモートホストが利用できなくなったなど、より広範なネットワークの問題に関連して発生することがあります。AcceptExのコンテキストでは、接続試行中にネットワークパスが失われた場合に発生する可能性があります。

これらのエラーは、通常、アプリケーションレベルで処理されるべき「接続の問題」を示すものですが、AcceptExが新しい接続を受け入れる段階で発生すると、リスナーの動作を妨げる可能性があります。このコミットは、これらのエラーがリスナーの継続的な動作を妨げないように、特定の状況下でこれらを無視するアプローチを取っています。

Go言語の net パッケージと syscall パッケージ

Go言語のnetパッケージは、TCP/IPネットワークプログラミングのための高レベルなインターフェースを提供します。このパッケージは、内部的にOS固有のシステムコール(WindowsではWinsock API)を呼び出すためにsyscallパッケージを利用しています。

netFD構造体は、ネットワークファイルディスクリプタ(ソケット)を抽象化し、readLockreadUnlockなどのメソッドを通じて、並行アクセスから保護します。operation構造体は、非同期I/O操作の状態を管理するために使用されます。

技術的詳細

このコミットの主要な変更点は、src/pkg/net/fd_windows.goファイル内のnetFD.acceptメソッドのロジックです。

変更前は、netFD.acceptメソッド内で直接AcceptExを呼び出し、エラーが発生した場合はすぐにそれを返していました。

変更後は、netFD.acceptメソッドがacceptOneという新しい内部ヘルパー関数を呼び出すようにリファクタリングされています。acceptOneは単一のAcceptEx操作を実行し、その結果を返します。

netFD.acceptメソッドは、acceptOneをループ内で呼び出すようになりました。このループ内で、acceptOneがエラーを返した場合、そのエラーが*OpError型であり、かつその内部のエラーがsyscall.Errno型であるかをチェックします。さらに、そのerrnosyscall.ERROR_NETNAME_DELETEDまたはsyscall.WSAECONNRESETであるかを判定します。

もしエラーがこれらの特定のエラーコードのいずれかであれば、そのエラーは無視され、ループは継続されます。これにより、AcceptExが一時的な接続リセットによって失敗しても、Acceptメソッドは新しい接続を待ち続けることができます。それ以外のエラーが発生した場合は、ループを抜けてエラーを呼び出し元に返します。

この変更により、Windows環境でのGoのネットワークサーバーは、一時的なネットワークの問題やクライアントの接続リセットに対してより堅牢になります。

また、src/pkg/syscall/ztypes_windows.goファイルには、ERROR_NETNAME_DELETEDWSAECONNRESETの定数が追加され、GoのsyscallパッケージからこれらのWindowsエラーコードにアクセスできるようになっています。

さらに、src/pkg/net/net_windows_test.goに新しいテストケースTestAcceptIgnoreSomeErrorsが追加されています。このテストは、子プロセスを起動し、意図的に接続をリセットすることで、AcceptExWSAECONNRESETなどのエラーを返す状況をシミュレートします。そして、GoのAccept実装がこれらのエラーを適切に無視し、後続の有効な接続を受け入れられることを検証します。これは、cmd.Process.Kill()を使用して子プロセスを強制終了することで、接続リセットをトリガーしています。

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

src/pkg/net/fd_windows.go

  • netFD.accept関数がリファクタリングされ、acceptOneという新しいヘルパー関数が導入されました。
  • netFD.accept内でacceptOneを呼び出すループが追加され、syscall.ERROR_NETNAME_DELETEDsyscall.WSAECONNRESETのエラーが無視されるようになりました。
--- a/src/pkg/net/fd_windows.go
+++ b/src/pkg/net/fd_windows.go
@@ -513,12 +513,7 @@ func (fd *netFD) WriteTo(buf []byte, sa syscall.Sockaddr) (int, error) {
 	})\n }\n \n-func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n-\tif err := fd.readLock(); err != nil {\n-\t\treturn nil, err\n-\t}\n-\tdefer fd.readUnlock()\n-\n+func (fd *netFD) acceptOne(toAddr func(syscall.Sockaddr) Addr, rawsa []syscall.RawSockaddrAny, o *operation) (*netFD, error) {\n \t// Get new socket.\n \ts, err := sysSocket(fd.family, fd.sotype, 0)\n \tif err != nil {\n@@ -537,9 +532,7 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n \t}\n \n \t// Submit accept request.\n-\to := &fd.rop\n \to.handle = s\n-\tvar rawsa [2]syscall.RawSockaddrAny\n \to.rsan = int32(unsafe.Sizeof(rawsa[0]))\n \t_, err = rsrv.ExecIO(o, "AcceptEx", func(o *operation) error {\n \t\treturn syscall.AcceptEx(o.fd.sysfd, o.handle, (*byte)(unsafe.Pointer(&rawsa[0])), 0, uint32(o.rsan), uint32(o.rsan), &o.qty, &o.o)\n@@ -556,6 +549,45 @@ func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n \t\treturn nil, &OpError{"Setsockopt", fd.net, fd.laddr, err}\n \t}\n \n+\treturn netfd, nil\n+}\n+\n+func (fd *netFD) accept(toAddr func(syscall.Sockaddr) Addr) (*netFD, error) {\n+\tif err := fd.readLock(); err != nil {\n+\t\treturn nil, err\n+\t}\n+\tdefer fd.readUnlock()\n+\n+\to := &fd.rop\n+\tvar netfd *netFD\n+\tvar err error\n+\tvar rawsa [2]syscall.RawSockaddrAny\n+\tfor {\n+\t\tnetfd, err = fd.acceptOne(toAddr, rawsa[:], o)\n+\t\tif err == nil {\n+\t\t\tbreak\n+\t\t}\n+\t\t// Sometimes we see WSAECONNRESET and ERROR_NETNAME_DELETED is\n+\t\t// returned here. These happen if connection reset is received\n+\t\t// before AcceptEx could complete. These errors relate to new\n+\t\t// connection, not to AcceptEx, so ignore broken connection and\n+\t\t// try AcceptEx again for more connections.\n+\t\toperr, ok := err.(*OpError)\n+\t\tif !ok {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\terrno, ok := operr.Err.(syscall.Errno)\n+\t\tif !ok {\n+\t\t\treturn nil, err\n+\t\t}\n+\t\tswitch errno {\n+\t\tcase syscall.ERROR_NETNAME_DELETED, syscall.WSAECONNRESET:\n+\t\t\t// ignore these and try again\n+\t\tdefault:\n+\t\t\treturn nil, err\n+\t\t}\n+\t}\n+\n \t// Get local and peer addr out of AcceptEx buffer.\n \tvar lrsa, rrsa *syscall.RawSockaddrAny\n \tvar llen, rlen int32

src/pkg/syscall/ztypes_windows.go

  • ERROR_NETNAME_DELETEDWSAECONNRESETの定数が追加されました。
--- a/src/pkg/syscall/ztypes_windows.go
+++ b/src/pkg/syscall/ztypes_windows.go
@@ -11,6 +11,7 @@ const (\n \tERROR_ACCESS_DENIED       Errno = 5\n \tERROR_NO_MORE_FILES       Errno = 18\n \tERROR_HANDLE_EOF          Errno = 38\n+\tERROR_NETNAME_DELETED     Errno = 64\n \tERROR_FILE_EXISTS         Errno = 80\n \tERROR_BROKEN_PIPE         Errno = 109\n \tERROR_BUFFER_OVERFLOW     Errno = 111\n@@ -23,6 +24,7 @@ const (\n \tERROR_IO_PENDING          Errno = 997\n \tERROR_NOT_FOUND           Errno = 1168\n \tWSAEACCES                 Errno = 10013\n+\tWSAECONNRESET             Errno = 10054\n )\n \n const (

src/pkg/net/net_windows_test.go

  • TestAcceptIgnoreSomeErrorsという新しいテストファイルとテストケースが追加されました。
--- /dev/null
+++ b/src/pkg/net/net_windows_test.go
@@ -0,0 +1,146 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package net
+
+import (
+	"bufio"
+	"fmt"
+	"io"
+	"os"
+	"os/exec"
+	"syscall"
+	"testing"
+	"time"
+)
+
+func TestAcceptIgnoreSomeErrors(t *testing.T) {
+	recv := func(ln Listener) (string, error) {
+		c, err := ln.Accept()
+		if err != nil {
+			// Display windows errno in error message.
+			operr, ok := err.(*OpError)
+			if !ok {
+				return "", err
+			}
+			errno, ok := operr.Err.(syscall.Errno)
+			if !ok {
+				return "", err
+			}
+			return "", fmt.Errorf("%v (windows errno=%d)", err, errno)
+		}
+		defer c.Close()
+
+		b := make([]byte, 100)
+		n, err := c.Read(b)
+		if err != nil && err != io.EOF {
+			return "", err
+		}
+		return string(b[:n]), nil
+	}
+
+	send := func(addr string, data string) error {
+		c, err := Dial("tcp", addr)
+		if err != nil {
+			return err
+		}
+		defer c.Close()
+
+		b := []byte(data)
+		n, err := c.Write(b)
+		if err != nil {
+			return err
+		}
+		if n != len(b) {
+			return fmt.Errorf(`Only %d chars of string "%s" sent`, n, data)
+		}
+		return nil
+	}
+
+	if envaddr := os.Getenv("GOTEST_DIAL_ADDR"); envaddr != "" {
+		// In child process.
+		c, err := Dial("tcp", envaddr)
+		if err != nil {
+			t.Fatalf("Dial failed: %v", err)
+		}
+		fmt.Printf("sleeping\n")
+		time.Sleep(time.Minute) // process will be killed here
+		c.Close()
+	}
+
+	ln, err := Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatalf("Listen failed: %v", err)
+	}
+	defer ln.Close()
+
+	// Start child process that connects to our listener.
+	cmd := exec.Command(os.Args[0], "-test.run=TestAcceptIgnoreSomeErrors")
+	cmd.Env = append(os.Environ(), "GOTEST_DIAL_ADDR="+ln.Addr().String())
+	stdout, err := cmd.StdoutPipe()
+	if err != nil {
+		t.Fatalf("cmd.StdoutPipe failed: %v", err)
+	}
+	err = cmd.Start()
+	if err != nil {
+		t.Fatalf("cmd.Start failed: %v\n%s\n", err)
+	}\n\toutReader := bufio.NewReader(stdout)\n\tfor {\n\t\ts, err := outReader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\tt.Fatalf("reading stdout failed: %v", err)\n\t\t}\n\t\tif s == "sleeping\\n" {\n\t\t\tbreak\n\t\t}\n\t}\n\tdefer cmd.Wait() // ignore error - we know it is getting killed\n\n\tconst alittle = 100 * time.Millisecond\n\ttime.Sleep(alittle)\n\tcmd.Process.Kill() // the only way to trigger the errors\n\ttime.Sleep(alittle)\n\n\t// Send second connection data (with delay in a separate goroutine).\n\tresult := make(chan error)\n\tgo func() {\n\t\ttime.Sleep(alittle)\n\t\terr = send(ln.Addr().String(), "abc")\n\t\tif err != nil {\n\t\t\tresult <- err\n\t\t}\n\t\tresult <- nil\n\t}()\n\tdefer func() {\n\t\terr := <-result\n\t\tif err != nil {\n\t\t\tt.Fatalf("send failed: %v", err)\n\t\t}\n\t}()\n\n\t// Receive first or second connection.\n\ts, err := recv(ln)\n\tif err != nil {\n\t\tt.Fatalf("recv failed: %v", err)\n\t}\n\tswitch s {\n\tcase "":\n\t\t// First connection data is received, lets get second connection data.\n\tcase "abc":\n\t\t// First connection is lost forever, but that is ok.\n\t\treturn\n\tdefault:\n\t\tt.Fatalf(`"%s" received from recv, but "" or "abc" expected`, s)\n\t}\n\n\t// Get second connection data.\n\ts, err = recv(ln)\n\tif err != nil {\n\t\tt.Fatalf("recv failed: %v", err)\n\t}\n\tif s != "abc" {\n\t\tt.Fatalf(`"%s" received from recv, but "abc" expected`, s)\n\t}\n}\n```

## コアとなるコードの解説

### `fd_windows.go` の変更

`netFD.accept`関数は、Goの`net`パッケージにおけるWindows固有の`Accept`実装の中核です。この関数は、新しいネットワーク接続を受け入れる役割を担います。

変更前は、`accept`関数内で直接`syscall.AcceptEx`を呼び出し、エラーが発生した場合は即座にそのエラーを返していました。これにより、`AcceptEx`が`WSAECONNRESET`や`ERROR_NETNAME_DELETED`のような一時的なエラーを返した場合でも、`accept`呼び出し全体が失敗し、サーバーが新しい接続を受け付けられなくなる可能性がありました。

変更後、`acceptOne`という新しいヘルパー関数が導入されました。この関数は、単一の`AcceptEx`操作を実行し、その結果(新しい`netFD`とエラー)を返します。

元の`netFD.accept`関数は、`acceptOne`を無限ループ(`for {}`)内で呼び出すように変更されました。
このループの内部では、`acceptOne`がエラーを返した場合に、そのエラーが`*OpError`型であるか、そしてその`Err`フィールドが`syscall.Errno`型であるかをチェックします。
さらに、その`errno`が`syscall.ERROR_NETNAME_DELETED`または`syscall.WSAECONNRESET`のいずれかであるかを`switch`文で判定します。

*   `case syscall.ERROR_NETNAME_DELETED, syscall.WSAECONNRESET:` の場合:
    これらのエラーは、接続がリセットされたり、ネットワーク名が削除されたりといった、一時的な接続の問題を示します。これらのエラーは、新しい接続試行に関連するものであり、リスナー自体が正常に機能している可能性が高いです。そのため、これらのエラーは無視され、`continue`によってループの次のイテレーションに進み、`AcceptEx`を再試行します。これにより、サーバーは一時的な問題にもかかわらず、新しい接続を受け入れ続けることができます。

*   `default:` の場合:
    上記以外のエラーが発生した場合は、そのエラーは無視できない重大な問題であると判断され、`return nil, err`によってエラーが呼び出し元に返され、`accept`ループは終了します。

このロジックにより、GoのWindowsネットワークサーバーは、特定の一般的な一時的エラーを透過的に処理し、より堅牢な動作を実現します。

### `ztypes_windows.go` の変更

このファイルは、Goの`syscall`パッケージがWindows APIとやり取りするために使用する型定義と定数を格納しています。
`ERROR_NETNAME_DELETED`と`WSAECONNRESET`の定数が追加されたことで、GoのコードからこれらのWindowsエラーコードを直接参照できるようになり、`fd_windows.go`でのエラーハンドリングが可能になりました。

### `net_windows_test.go` の追加

`TestAcceptIgnoreSomeErrors`テストは、このコミットの変更が意図通りに機能することを検証するために非常に重要です。
このテストは、以下のようなシナリオをシミュレートします。

1.  メインプロセスがTCPリスナーを起動します。
2.  子プロセスを起動し、その子プロセスにリスナーのアドレスを環境変数で渡します。
3.  子プロセスはリスナーに接続し、意図的に長時間スリープします。
4.  メインプロセスは、子プロセスがスリープしている間に、子プロセスを強制終了(`cmd.Process.Kill()`)します。これにより、子プロセスからの接続が予期せず切断され、`AcceptEx`が`WSAECONNRESET`などのエラーを返す状況を作り出します。
5.  メインプロセスは、強制終了後に別のゴルーチンで新しい接続を送信します。
6.  メインプロセスは、`Listener.Accept()`を呼び出し、最初の接続がエラーで失われたとしても、2番目の接続が正常に受け入れられることを検証します。

このテストは、Goの`Accept`実装が、一時的な接続リセットエラーを適切に無視し、後続の有効な接続を処理できることを実証します。

## 関連リンク

*   Go Issue #6987: [net: ignore some errors in windows Accept](https://github.com/golang/go/issues/6987)
*   Go Code Review: [https://golang.org/cl/49490043](https://golang.org/cl/49490043)

## 参考にした情報源リンク

*   Microsoft Docs: [AcceptEx function](https://learn.microsoft.com/en-us/windows/win32/api/mswsock/nf-mswsock-acceptex)
*   Microsoft Docs: [Winsock Error Codes](https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2)
*   Microsoft Docs: [System Error Codes (60-99)](https://learn.microsoft.com/en-us/windows/win32/debug/system-error-codes--60-99-) (for `ERROR_NETNAME_DELETED`)
*   Go `net` package documentation: [https://pkg.go.dev/net](https://pkg.go.dev/net)
*   Go `syscall` package documentation: [https://pkg.go.dev/syscall](https://pkg.go.dev/syscall)
*   Go `os/exec` package documentation: [https://pkg.go.dev/os/exec](https://pkg.go.dev/os/exec)
*   Go `testing` package documentation: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
*   Go `time` package documentation: [https://pkg.go.dev/time](https://pkg.go.dev/time)
*   Go `fmt` package documentation: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
*   Go `io` package documentation: [https://pkg.go.dev/io](https://pkg.go.dev/io)
*   Go `bufio` package documentation: [https://pkg.go.dev/bufio](https://pkg.go.dev/bufio)
*   Go `os` package documentation: [https://pkg.go.dev/os](https://pkg.go.dev/os)
*   Go `unsafe` package documentation: [https://pkg.go.dev/unsafe](https://pkg.go.dev/unsafe)
*   Go `sync` package documentation: [https://pkg.go.dev/sync](https://pkg.go.dev/sync) (for `readLock`/`readUnlock` context)
*   Go `net` package source code: [https://github.com/golang/go/tree/master/src/net](https://github.com/golang/go/tree/master/src/net)
*   Go `syscall` package source code: [https://github.com/golang/go/tree/master/src/syscall](https://github.com/golang/go/tree/master/src/syscall)
*   Go `src/pkg/net/fd_windows.go` (before this commit): [https://github.com/golang/go/blob/64d56c73e0/src/pkg/net/fd_windows.go](https://github.com/golang/go/blob/64d56c73e0/src/pkg/net/fd_windows.go)
*   Go `src/pkg/syscall/ztypes_windows.go` (before this commit): [https://github.com/golang/go/blob/28cd3f6169/src/pkg/syscall/ztypes_windows.go](https://github.com/golang/go/blob/28cd3f6169/src/pkg/syscall/ztypes_windows.go)
*   Go `src/pkg/net/net_windows_test.go` (after this commit): [https://github.com/golang/go/blob/c7ef348bad102b3427b4242018e92eba17d079ba/src/pkg/net/net_windows_test.go](https://github.com/golang/go/blob/c7ef348bad102b3427b4242018e92eba17d079ba/src/pkg/net/net_windows_test.go)