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

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

このコミットは、Go言語の標準ライブラリnetパッケージにおけるUDPコネクションの振る舞いを修正するものです。具体的には、net.Dialによって「接続済み」状態になったUDPConnに対して、宛先アドレスを明示的に指定するWriteToメソッド(およびWriteToUDP)が呼び出された際に、エラーを返すように変更されています。これにより、UDPソケットのセマンティクスがより明確になり、予期せぬ動作を防ぎます。

コミット

commit 974fa755573cbcad4e6ff48e4faae25ffa2cca43
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Fri Jan 27 01:31:42 2012 +0900

    net: make WriteTo fail when UDPConn is already connected

    Fixes #2773.

    R=rsc
    CC=golang-dev
    https://golang.org/cl/5571056

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

https://github.com/golang/go/commit/974fa755573cbcad4e6ff48e4faae25ffa2cca43

元コミット内容

このコミットは、netパッケージのUDPコネクションに関する以下の変更を行っています。

  1. netFD構造体にisConnectedというブーリアンフィールドを追加。
  2. net.DialによってUDPコネクションが確立された際に、このisConnectedフラグをtrueに設定。
  3. UDPConnが既に接続済み(isConnectedtrue)である場合、WriteToUDPメソッドがErrWriteToConnectedという新しいエラーを返すように変更。
  4. この変更の動作を検証するための新しいテストファイルsrc/pkg/net/udp_test.goを追加。

変更の背景

この変更は、GoのIssue #2773「net: WriteTo on connected UDPConn should fail」を解決するために行われました。

UDP(User Datagram Protocol)は通常、コネクションレスなプロトコルであり、各データグラムは独立して送信され、宛先アドレスを含みます。しかし、多くのソケットAPI(POSIXソケットなど)では、UDPソケットを特定のピアアドレスに「接続」する機能(connectシステムコール)を提供しています。Go言語のnet.Dial("udp", ...)もこの機能を利用し、内部的にUDPソケットを接続済み状態にします。

接続済みUDPソケットの主な利点は以下の通りです。

  • Writeメソッド(宛先アドレスを指定しない)を使用できるようになる。
  • Readメソッドが、接続されたピアからのデータのみを受け取るようになる。
  • 一部のOSでは、connectによってルーティング情報がキャッシュされ、連続するデータグラム送信のパフォーマンスが向上する可能性がある。

問題は、接続済みUDPソケットに対して、宛先アドレスを明示的に指定するsendto(GoのWriteToに相当)を呼び出した際の振る舞いが、OSによって異なっていた点です。

  • 一部のOS(例: Linux)では、接続済みソケットに対して異なる宛先アドレスでsendtoを呼び出すと、EINVAL(無効な引数)エラーを返します。
  • 他のOSでは、エラーを返さずに、データが接続済みのアドレスに送信されてしまったり、ソケットの接続状態が壊れてしまったりする可能性がありました。

このようなプラットフォーム間の非一貫性や、開発者が意図しない動作を防ぐため、Goのnetパッケージは、接続済みUDPConnに対してWriteToが呼び出された場合に、明示的にエラーを返すように統一的な振る舞いを導入しました。これにより、開発者はコードの意図を明確にし、予期せぬデータ送信を防ぐことができます。

前提知識の解説

  • UDP (User Datagram Protocol): TCPと異なり、コネクションの確立や信頼性のあるデータ転送の保証を行わない、軽量なプロトコルです。各データグラムは独立して送信され、宛先アドレスを含みます。
  • コネクションレスソケット: UDPソケットの一般的な形態で、データを送信するたびに宛先アドレスを指定します(例: sendtoシステムコール、GoのWriteTo)。
  • 接続済みUDPソケット: connectシステムコール(Goのnet.Dialに相当)によって、特定のピアアドレスに関連付けられたUDPソケットです。この状態では、sendシステムコール(GoのWriteに相当)を使用して、事前に接続されたアドレスにデータを送信できます。また、recv(GoのRead)は接続されたピアからのデータのみを受け取ります。
  • net.Dial: Go言語でネットワークコネクションを確立するための関数です。TCPだけでなく、UDPに対しても使用でき、UDPの場合はソケットを特定のピアアドレスに「接続済み」状態にします。
  • net.ListenPacket: UDPなどのパケット指向プロトコルで、任意の送信元からのデータを受信するために使用されます。この関数で作成されたPacketConnは、通常「コネクションレス」状態です。
  • WriteToWrite:
    • WriteTo(b []byte, addr Addr) (n int, err error): 宛先アドレスを明示的に指定してデータを送信します。主にコネクションレスソケットで使用されます。
    • Write(b []byte) (n int, err error): 宛先アドレスを指定せずにデータを送信します。主にTCPのようなコネクション指向ソケット、またはDialによって接続済みになったUDPソケットで使用されます。

技術的詳細

このコミットの技術的な核心は、netFD構造体にisConnectedという状態フラグを導入し、このフラグに基づいてWriteToUDPの振る舞いを制御することです。

  1. netFD構造体の拡張: netFDは、Goのnetパッケージが内部的に使用するネットワークファイルディスクリプタ(ソケット)の抽象化です。この構造体にisConnected boolフィールドが追加されました。これは、ソケットがDialによって特定のピアに接続されているかどうかを示すシンプルなフラグです。

  2. socket関数でのisConnectedの設定: net/sock.gosocket関数は、新しいソケットを作成し、必要に応じてconnectシステムコールを呼び出す役割を担っています。この関数内で、リモートアドレス(ra)が指定されている場合(つまりnet.Dialが呼び出された場合)、fd.isConnected = trueが設定されます。これにより、netFDインスタンスが接続済み状態であることを内部的に記録します。

  3. WriteToUDPでのエラーチェック: net/udpsock_posix.go(およびWindows版のnet/fd_windows.go)のWriteToUDPメソッドに、以下のチェックが追加されました。

    if c.fd.isConnected {
        return 0, &OpError{"write", c.fd.net, addr, ErrWriteToConnected}
    }
    

    c.fd.isConnectedtrueの場合、つまりUDPConnDialによって接続済みであるにもかかわらず、WriteToUDPが呼び出された場合、新しいエラーErrWriteToConnectedを含むOpErrorが返されます。このErrWriteToConnectedは、"use of WriteTo with pre-connected UDP"というメッセージを持つerrors.Newで定義されたエラーです。

  4. 新しいテストの追加: src/pkg/net/udp_test.goが新規に追加され、この変更の動作を厳密に検証しています。

    • testWriteToConnでは、net.Dialで接続されたUDPConnに対してWriteToUDPWriteToを呼び出し、ErrWriteToConnectedが返されることを確認しています。同時に、接続済みソケットに対する通常のWriteメソッドが引き続き機能することも検証しています。
    • testWriteToPacketConnでは、net.ListenPacketで作成されたコネクションレスなUDPConnに対してWriteToUDPWriteToが正常に機能することを確認しています。また、コネクションレスソケットに対するWriteメソッドがエラーを返すことも検証しています。

これらの変更により、Goのnetパッケージは、接続済みUDPソケットに対するWriteToの振る舞いを明確にし、プラットフォーム間の差異を吸収して、一貫性のあるエラーハンドリングを提供します。

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

  • src/pkg/net/fd.go:

    --- a/src/pkg/net/fd.go
    +++ b/src/pkg/net/fd.go
    @@ -22,15 +22,16 @@ type netFD struct {
     	closing bool
    
     	// immutable until Close
    -	sysfd   int
    -	family  int
    -	sotype  int
    -	sysfile *os.File
    -	cr      chan bool
    -	cw      chan bool
    -	net     string
    -	laddr   Addr
    -	raddr   Addr
    +	sysfd       int
    +	family      int
    +	sotype      int
    +	isConnected bool // 追加
    +	sysfile     *os.File
    +	cr          chan bool
    +	cw          chan bool
    +	net         string
    +	laddr       Addr
    +	raddr       Addr
    
     	// owned by client
     	rdeadline int64
    

    isConnectedフィールドが追加されています。

  • src/pkg/net/fd_windows.go: Windows版のnetFD構造体にも同様にisConnectedフィールドが追加されています。

  • src/pkg/net/sock.go:

    --- a/src/pkg/net/sock.go
    +++ b/src/pkg/net/sock.go
    @@ -49,6 +49,7 @@ func socket(net string, f, t, p int, la, ra syscall.Sockaddr, toAddr func(syscal
     			fd.Close()
     			return nil, err
     		}
    +		fd.isConnected = true // 追加
     	}
    
     	sa, _ := syscall.Getsockname(s)
    

    net.Dialによってリモートアドレスが指定された場合にfd.isConnectedtrueに設定されます。

  • src/pkg/net/udpsock_posix.go:

    --- a/src/pkg/net/udpsock_posix.go
    +++ b/src/pkg/net/udpsock_posix.go
    @@ -9,11 +9,14 @@
     package net
    
     import (
    +	"errors" // 追加
     	"os"
     	"syscall"
     	"time"
     )
    
    +var ErrWriteToConnected = errors.New("use of WriteTo with pre-connected UDP") // 追加
    +
     func sockaddrToUDP(sa syscall.Sockaddr) Addr {
     	switch sa := sa.(type) {
     	case *syscall.SockaddrInet4:
    @@ -182,6 +185,9 @@ func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (int, error) {
     	if !c.ok() {
     		return 0, os.EINVAL
     	}\n
    +	if c.fd.isConnected { // 追加
    +		return 0, &OpError{"write", c.fd.net, addr, ErrWriteToConnected} // 追加
    +	}
     	sa, err := addr.sockaddr(c.fd.family)
     	if err != nil {
     		return 0, &OpError{"write", c.fd.net, addr, err}
    

    ErrWriteToConnectedが定義され、WriteToUDP内でisConnectedのチェックとエラー返却ロジックが追加されています。

  • src/pkg/net/udp_test.go: このファイルは新規追加されており、TestWriteToUDP関数とそのヘルパー関数testWriteToConntestWriteToPacketConnが含まれています。これらは、接続済みUDPコネクションとコネクションレスUDPコネクションの両方でWriteToおよびWriteの振る舞いを検証するテストケースです。

コアとなるコードの解説

このコミットの核心は、UDPConnの内部状態を正確に管理し、その状態に基づいてWriteToメソッドの振る舞いを調整することにあります。

  1. netFD.isConnectedフラグ: このフラグは、UDPConnnet.Dialによって特定のピアに「接続」されているかどうかを示すシンプルな真偽値です。Dialが呼び出されると、内部的にconnectシステムコールが実行され、ソケットが接続済み状態になります。このとき、netFD.isConnectedtrueに設定されます。

  2. WriteToUDPのガード句: WriteToUDPメソッドは、データを特定の宛先アドレスに送信するために使用されます。このメソッドの冒頭に、if c.fd.isConnectedという条件が追加されました。

    • もしc.fd.isConnectedtrueであれば、それはUDPConnが既にDialによって接続済みであることを意味します。接続済みUDPソケットに対して、WriteToのように異なる宛先アドレスを指定してデータを送信しようとすることは、セマンティックな矛盾やプラットフォーム依存の予期せぬ動作を引き起こす可能性があります。
    • このため、このような状況では、WriteToUDPは直ちにErrWriteToConnectedエラーを返します。これにより、開発者は接続済みソケットに対してWriteToを使用すべきではないことを明確に知ることができます。
  3. ErrWriteToConnected: この新しいエラーは、接続済みUDPソケットに対してWriteToが呼び出された場合に返される特定のエラーです。これにより、エラーハンドリングが容易になり、開発者はこの特定のエラーを捕捉して適切な処理を行うことができます。

この変更は、Goのnetパッケージが提供するネットワークAPIの堅牢性と予測可能性を高めるものです。開発者は、Dialで接続したUDPソケットではWriteを、ListenPacketで作成したコネクションレスなUDPソケットではWriteToを使用するという、より明確なガイドラインに従うことができます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(上記コミットの差分)
  • Go Issue #2773の議論
  • POSIX connect(2) man page (UDPソケットのconnectに関する情報)
  • Go netパッケージのドキュメント (UDPConn, Dial, ListenPacket, Write, WriteToなど)