1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go言語:WindowsでUDPマルチキャストが受信できない!?同一PC内で複数プロセスが受信に失敗する罠と解決策

1
Posted at

Go言語でマルチキャストを使用したローカルデバイス検出機能を実装している際、「Linuxでは動くのに、Windowsで動かすと同一PC内でマルチキャストが全く受信できない」、あるいは**「複数プロセスを立ち上げるとバインドエラーになるか、一部のプロセスにしかパケットが届かない」**という問題に直面したことはありませんか?

この記事では、Windows 11環境においてGo言語でUDPマルチキャストを実装する際の「OS特有の罠」と、標準ライブラリの機能だけ(外部パッケージなし)でこれを解決する具体的な実装方法を解説します。


と、いうAI記事なのです。(ここは手書き)

Windowsアプリを開発中、複数PCとの通信をマルチキャストで管理しようとし、ついでに1台のPC内での複数のアプリ間通信もできそうだと思ってGo言語で書いていたわけです。

ところが、複数PC間での通信はうまくいくのに、1台のPC内でアプリを複数起動した場合データが送られてこない!
単純にそのことをAI達に聞いても「ソケット・ループバックに関わるWindows特有の制限です」とか言われて解決にならなかったりする。そんなバカな!と。納得できるか!と。

そのため検証を続けたら、
・Goの標準ライブラリでは? → できない
・Pythonで記述したら1台のPC内でも送受信できる? → できる。
となったため、Antigravityに検証してもらった結果を纏めたのが本記事です。

はい、AIさん続きをお願い。


発生する問題

同一のWindowsマシン上で、以下のような構成でテストを行います。

  • 受信プロセス A (ポート: 53317, グループ: 224.0.0.167)
  • 受信プロセス B (ポート: 53317, グループ: 224.0.0.167)
  • 送信プロセス A (グループ 224.0.0.167:53317 宛に送信)

Goの標準ライブラリで最も一般的な実装である net.ListenMulticastUDP を使って受信側を実装すると、以下の現象が発生します。

  • 複数プロセス(受信AとB)を立ち上げてもエラーにはならない(一見Listenできているように見える)。
  • しかし、送信側からマルチキャストを送信しても、受信側(A、Bともに)に1パケットも届かない

※ 不思議なことに、これをPythonの socket ライブラリで書き直すと、何の設定もせずともあっさりと動作します。


原因は「バインド先アドレス」と「SO_REUSEADDR」のOS差

Goの標準ライブラリ net.ListenMulticastUDP の実装を追っていくと、Windowsにおいて内部でマルチキャストIPアドレスそのもの(224.0.0.167:53317)に対してソケットをバインドしようとします。

しかし、Windowsのネットワークスタック(Winsock)には以下の挙動が存在します。

  1. マルチキャストアドレスへ直接バインドするとループバックを受信できない
    Windowsでは、マルチキャストアドレスに直接バインドされたソケットは、同一PC内(ローカルループバック)から送信されたマルチキャストパケットをルーティングしてくれません。
    これを正しく受信するためには、ソケットをワイルドカードアドレス(0.0.0.0)にバインドしたうえで、IP_ADD_MEMBERSHIP(マルチキャストグループへの参加要求)を設定する必要があります。
  2. SO_REUSEADDR の自動設定が不十分
    同一ポートに複数の受信プロセスをバインドするためには、ソケット作成時に SO_REUSEADDR オプションを明示的に有効にする必要があります。Goの標準的なListen処理はWindowsにおいてこのオプションを適切に設定しきれない場合があります。

解決策:syscall を使用して 0.0.0.0 にバインドする

この問題は、外部パッケージ(golang.org/x/net など)を使わなくても、Goの標準ライブラリの syscall パッケージを少し組み合わせるだけで解決可能です。

以下が、Windows 11環境で完全に動作する受信側の実装コードです。

package main

import (
	"context"
	"fmt"
	"log"
	"net"
	"syscall"
)

func main() {
	mcastIP := net.ParseIP("224.0.0.167")
	localIP := net.ParseIP("192.168.11.12") // 自身のLANカードのIPアドレス
	port := 53317

	// 1. バインド前にソケットオプションを設定するためのListenConfigを作成
	lc := net.ListenConfig{
		Control: func(network, address string, c syscall.RawConn) error {
			return c.Control(func(fd uintptr) {
				// Windowsで同一ポートへの複数バインドを許可するためにSO_REUSEADDRを設定
				err := syscall.SetsockoptInt(syscall.Handle(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
				if err != nil {
					log.Printf("Warning: failed to set SO_REUSEADDR: %v", err)
				}
			})
		},
	}

	// 2. マルチキャストIPではなく "0.0.0.0:PORT" にバインドする
	bindAddr := fmt.Sprintf("0.0.0.0:%d", port)
	packetConn, err := lc.ListenPacket(context.Background(), "udp", bindAddr)
	if err != nil {
		log.Fatalf("Error listening on %s: %v", bindAddr, err)
	}
	defer packetConn.Close()

	udpConn, ok := packetConn.(*net.UDPConn)
	if !ok {
		log.Fatalf("Error: connection is not a UDPConn")
	}

	// 3. SyscallConnを経由して後からマルチキャストグループに参加 (IP_ADD_MEMBERSHIP)
	rawConn, err := udpConn.SyscallConn()
	if err != nil {
		log.Fatalf("Error getting RawConn: %v", err)
	}

	var mreq syscall.IPMreq
	copy(mreq.Multiaddr[:], mcastIP.To4())
	copy(mreq.Interface[:], localIP.To4())

	var setoptErr error
	err = rawConn.Control(func(fd uintptr) {
		// Windows用のSetsockoptIPMreqを使用
		setoptErr = syscall.SetsockoptIPMreq(syscall.Handle(fd), syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, &mreq)
	})
	
	if err != nil || setoptErr != nil {
		log.Fatalf("Error setting IP_ADD_MEMBERSHIP: conn_err=%v, opt_err=%v", err, setoptErr)
	}

	fmt.Printf("Listening on %s (Bound to 0.0.0.0, Joined %s)...\n", bindAddr, mcastIP.String())

	// 4. データ受信ループ
	buf := make([]byte, 65535)
	for {
		n, src, err := udpConn.ReadFromUDP(buf)
		if err != nil {
			log.Printf("Error reading: %v", err)
			continue
		}
		fmt.Printf("Received %d bytes from %s: %s\n", n, src.String(), string(buf[:n]))
	}
}

送信側の注意点

送信側は、Goの通常の net.DialUDP で十分に動作します。ただし、同一PC内でテストする場合は、送信元となるローカルインターフェースのIP(例: 192.168.11.12:0)を明示的に指定してバインドし、送信する必要があります。

localAddr, _ := net.ResolveUDPAddr("udp", "192.168.11.12:0")
mcastAddr, _ := net.ResolveUDPAddr("udp", "224.0.0.167:53317")
conn, _ := net.DialUDP("udp", localAddr, mcastAddr)

もしそれでも届かない場合は、送信ソケットに対して IP_MULTICAST_LOOP オプションを明示的に有効(値: 1)に設定してください。


まとめ

Windows環境でUDPマルチキャストを扱う際、「Go標準の net.ListenMulticastUDP はWindowsのループバックマルチキャストでは動作しない」 というのは非常にハマりやすいポイントです。

  • バインド先は 0.0.0.0 にする
  • SO_REUSEADDR をバインド前に設定する
  • IP_ADD_MEMBERSHIP を手動で設定する

これら3つを意識することで、Windowsでも快適にマルチキャストプログラムを開発・テストできるようになります。ぜひ参考にしてください!


最後に(手書き)

8割AIに書いてもらった記事ではありますが、標準ライブラリにも落とし穴があることと、回避方法もあるって記録が残せればそれでよいのです。
…えぇ私自身、ソケットの仕組みを完全にわかってはおりません…

スペシャルサンクス:Antigravity, ChatGPT(無料), Claude(無料)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?