今回は、マルチキャスト送信プログラム(UDPMulticastSender for Windows)を作ります。
先ずはマルチキャストアドレスの復習です。
(IPv6のアドレスについても参考にしてください)
IPv4マルチキャストアドレス
クラスDのアドレスです
トップビットが「1110」なので、アドレスの範囲は224.0.0.0 ~ 239.255.255.255です。
この範囲で次のものが予約済みですので、自分のプログラムでは使用しないようにします。
224.0.0.0 ~ 224.0.0.255(予約済み)
ローカルネットワーク制御ブロック
同一セグメント上で使用するアドレス。この範囲のアドレスは通常、TTL値「1」で
送信されるため、ルータで転送されません。
224.0.0.1 サブネット上の全てのマルチキャスト対応のホスト
224.0.0.2 サブネット上の全てのマルチキャスト対応のルータ
224.0.0.5 全てのOSPFルータ
224.0.0.6 全てのOSPF DR/BDRルータ
224.0.0.9 全てのRIPv2ルータ
224.0.0.10 全てのEIGRPルータ
224.0.0.13 全てのPIMルータ
224.0.0.18 全てのVRRPルータ
グローバルスコープのアドレスの範囲は224.0.1.0 ~ 238.255.255.255です。
これらは組織間やインターネットで使用するためのアドレスです。
アドレスの一部はアプリケーションのためにICANNで予約されています。
プライベートスコープのアドレスの範囲は239.0.0.0 ~ 239.255.255.255です。
これらは、組織内で使用するためのアドレスです。
「239.0.0.0/24」と「239.128.0.0/24」は使用しない事が推奨されています。
今回は、238.1.0.1, 239.1.0.1あたりを使用することにします
IPv6マルチキャストアドレス
IPv4に比べてシンプルです。
先頭の8ビットが「1111 1111」から始まります。
つまり、FF00::/8ということです。
FFの次の4ビットはフラグです。
0: 永続的(予約されています)
1: 一時的(ユーザが自由に使えます)
その次の4ビットはスコープです。
2: リンクローカル 同じリンク上の全てのノードに送信
8: 組織ローカル 同じ拠点内の全てのノードに送信
E: グローバル
最後の112ビットはグループIDです。
ここは好きに使えます。
今回は、FF1E::1, FF12::1あたりを使用することにします。
ユニキャストとマルチキャストのプログラムの概要を比較してみましょう。
Senderでは送信条件をソケットに設定します。
Receiverでは、マルチキャストグループへの参加/離脱を行います。
UDPMulticastSenderに必要な設定(1)
マルチキャスト送信時のインターフェースの指定
(*)送信インターフェースを指定しないと動作しない場合があります。
IPv4では送信元アドレス、IPv6では送信元インターフェースを指定します
IPv4(送信元インターフェースのアドレスを指定する)
in_addr ifAddr;
setsockopt(socket, IPPROTO_IP, IP_MULTICAST_IF, &ifAddr, sizeof(ifAddr));
IPv6(送信元インターフェースのインデックスを指定する)
uint_t ifindex;
(*)Windows では DWORD ifindex;
setsockopt(socket, IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex, sizeof(ifindex));
これは、RFC 3493:Basic Socket Interface Extensions for IPv6に記述されてます。
UDPMulticastSenderに必要な設定(2)
マルチキャスト送信時のTTL、ホップリミットの制御
これは、必要に応じて指定します。
IPv4
DWORD dwTtl;
setsockopt(socket, IPPROTO_IP, IP_MULTICAST_TTL, (char *)&dwTtl, sizeof(dwTtl));
IPv6
DWORD dwHoplimit;
setsockopt(socket, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (char *)&dwHoplimit,
sizeof(dwHoplimit));
これも、RFC 3493:Basic Socket Interface Extensions for IPv6に記述されてます。
(*)TTLの目安
0 同じホストに制限される
1 同じサブネットに制限される
32 同じサイトに制限される
64 同じ地域に制限される
128 同じ大陸に制限される
255 配信範囲内で制限されない
UDPMulticastSenderに必要な設定(3)
マルチキャスト送信時のループバックの制御
送信したマルチキャストを同じホストで取得するかどうかを設定します。
どうもWindowsでは0を指定しても機能しないようです。
IPv4
DWORD dwLoop; // 0/1
setsockopt(socket, IPPROTO_IP, IP_MULTICAST_LOOP, (char *)&dwLoop,
sizeof(dwLoop));
IPv6
DWORD dwLoop; // 0/1
setsockopt(socket, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (char *)&dwLoop,
sizeof(dwLoop));
これも、RFC 3493:Basic Socket Interface Extensions for IPv6に記述されてます。
それではUDPMulticastSender for Windowsを作ることにしましょう。
プログラム起動時に送信先マルチキャスト(マルチキャストグループ、ポート)と送信元
インターフェースを指定できるようにします。
(*)送信元インターフェース名は次のものを指定します。
Windows10:イーサネット、Wi-Fi など
Linux:eth0、wlan0 など
プログラム起動引数 <Multicast IP> <DestPort> [<InterfaceName>] メニュー m:メッセージ送信 q:終了 入力されたメッセージを5秒間隔で送信し続ける
UDPMulticastSenderの作成(Windows)
UDPClient for Windowsをコピーしてフォルダ名をUDPMulticastSenderに変更します。
以下のファイルはそのまま(変更なく)使用します。
define.h
MySyncObject.cpp, MySyncObject.h
RingBuff.cpp, RingBuff.h
SendRecvThread.cpp, SendRecvThread.h
stdThread.cpp, stdThread.h
ThreadJob.cpp, ThreadJob.h
プロジェクト名、ソリューション名をUDPMulticastSenderに変更します。
UDPClient.cppをUDPMulticastSender.cppに変更します。
送信元インターフェースの指定方法がIPv4とIPv6で異なるのでインターフェース名から
それぞれ必要な情報を取得する関数を以前作成したEnumInterface for Windowsを
参考に作成します。
インターフェースインデックス(IPv6用)
インターフェースに設定されたユニキャストIPアドレス(IPv4用)
sockaddr *GetInterfaceInfo(int iFamily, LPCSTR szIFName, DWORD *pdwIndex)
【UDPMulticastSender.cpp】 #include “AtlBase.h” // ★USES_CONVERSION, W2Aを使うため // 関数の宣言 sockaddr *GetInterfaceInfo(int iFamily, LPCSTR szIFName, DWORD *pdwIndex); // ★インターフェースインデクス、ユニキャストアドレスの取得 //============================================== // Function // ★指定されたインターフェースの情報を取得する // Parameter // int iFamily [in]アドレスファミリ // LPCSTR szIFName [in]インターフェース名 // DWORD *pdwIndex [in/out]インターフェースインデックス // Return // 指定条件のインターフェースの最初のユニキャストアドレス //============================================== sockaddr *GetInterfaceInfo(int iFamily, LPCSTR szIFName, DWORD *pdwIndex) { PIP_ADAPTER_ADDRESSES pAdapterAddresses = NULL, pAA; PIP_ADAPTER_UNICAST_ADDRESS pUnicast; sockaddr *pAddr, *pAddrRet = NULL; char szAddr[NI_MAXHOST]; DWORD dwRet, dwSize; USES_CONVERSION; *pdwIndex = 0; if (szIFName == NULL) goto L_END; // ネットワークアダプタリストの一覧を格納するために // 必要なバッファサイズを取得する dwRet = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, NULL, &dwSize); if (dwRet != ERROR_BUFFER_OVERFLOW) goto L_END; if ((pAdapterAddresses = (PIP_ADAPTER_ADDRESSES)malloc(dwSize)) == NULL) goto L_END; // アダプタリストの取得 dwRet = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, pAdapterAddresses, &dwSize); if (dwRet != ERROR_SUCCESS) goto L_END; // アダプタ一覧を出力 pAA = pAdapterAddresses; while (pAA != NULL) { // ★インターフェース名が指定のものでなければ、次へ if (strcmp(W2A(pAA->FriendlyName), szIFName) != 0) goto L_NEXT_IF; *pdwIndex = pAA->IfIndex; // Unicode から ShiftJIS に変換 fprintf(stderr, “Adapter Name : %s\n”, W2A(pAA->FriendlyName)); fprintf(stderr, “IfIndex : %u\n”, pAA->IfIndex); // ★送信元として指定できるのはユニキャストアドレス pUnicast = pAA->FirstUnicastAddress; while (pUnicast) { // ユニキャストIPアドレスを列挙 pAddr = pUnicast->Address.lpSockaddr; // ★ファミリが指定のものでなければ次へ if (pAddr->sa_family != iFamily) goto L_NEXT_ADDR; if (pAddr->sa_family == AF_INET) { inet_ntop(AF_INET, &((struct sockaddr_in *)pAddr)->sin_addr, szAddr, sizeof(szAddr)); fprintf(stderr, ” IPv4: %s/%d\n”, szAddr, pUnicast->OnLinkPrefixLength); pAddrRet = (sockaddr *)calloc(1, sizeof(sockaddr_in)); memcpy(pAddrRet, pAddr, sizeof(sockaddr_in)); } else if (pAddr->sa_family == AF_INET6) { inet_ntop(AF_INET6, &((struct sockaddr_in6 *)pAddr)->sin6_addr, szAddr, sizeof(szAddr)); fprintf(stderr, ” IPv6: %s/%d\n”, szAddr, pUnicast->OnLinkPrefixLength); pAddrRet = (sockaddr *)calloc(1, sizeof(sockaddr_in6)); memcpy(pAddrRet, pAddr, sizeof(sockaddr_in6)); } // ★見つかったのでリターンする(取り合えず最初に見つかったユニキャストアドレスを使います) if (pAddrRet != NULL) goto L_END; L_NEXT_ADDR: pUnicast = pUnicast->Next; } fprintf(stderr, “\n”); L_NEXT_IF: pAA = pAA->Next; } L_END: SAFE_FREE(pAdapterAddresses) return(pAddrRet); }
送信ソケットの設定を行う関数を追加します。
(1)送信元インターフェースの設定
(2)TTL/HopLimitsの設定
(3)ループバックの設定
void SetSendSocketOption(SOCKET fd, int iFamily, LPCSTR szIFName);
【UDPMulticastSender.cpp】 // 関数の宣言 void SetSendSocketOption(SOCKET fdS, int iFamily, LPCSTR szIFName); // ★送信ソケットの設定 //============================================== // function // ★送信ソケットの設定 // 送信元インターフェース, ループバック, TTL/HopLimitsの設定 // parameter // SOCKET fd [in]ソケット // int iFamily [in]アドレスファミリ // LPCSTR szIFName [in]送信元インターフェース名/NULL // return // TRUE/FALSE //============================================== void SetSendSocketOption(SOCKET fd, int iFamily, LPCSTR szIFName) { sockaddr *pAddr = NULL; int iRet; DWORD dwFIndex; // 1.送信元インターフェースの設定(インターフェース名が指定されたとき) if ((pAddr = GetInterfaceInfo(iFamily, szIFName, &dwFIndex)) != NULL) { if (iFamily == AF_INET) iRet = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, (char *)&(((sockaddr_in *)pAddr)->sin_addr), sizeof(in_addr)); else if (iFamily == AF_INET6) iRet = setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, (char *)&dwFIndex, sizeof(dwFIndex)); } // 2.TTL/HopLimitsの設定 DWORD dwTTL = 32; if (iFamily == AF_INET) iRet = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, (char *)&dwTTL, sizeof(dwTTL)); else if (iFamily == AF_INET6) iRet = setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, (char *)&dwTTL, sizeof(dwTTL)); // 3.ループバックの設定(Windowsで0をしていたたとき動作しないようです) DWORD dwLoop = 1; if (iFamily == AF_INET) iRet = setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (char *)&dwLoop, sizeof(dwLoop)); else if (iFamily == AF_INET6) iRet = setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, (char *)&dwLoop, sizeof(dwLoop)); SAFE_FREE(pAddr) }
以下の関数は使用しないので削除します。関数の宣言も削除します
BOOL ShowInterfaceIndex(addrinfo *pAddr); // インターフェースを表示する
BOOL SetHopLimit(SOCKET fd, int iFamily, int iLimit); // ホップリミットを設定する
ソケットの作成関数にインターフェース名を渡すように変更します。
BOOL CreateSocket(LPCSTR szDestAddress, WORD wDestPort, LPCSTR pszIFName)
この関数からvoid SetSendSocketOption(SOCKET fd, int iFamily, LPCSTR szIFName);
を呼び出します。
またマルチキャストの送信元に対して送り返すことはないので送信元ポートをバインド
しないようにします。
【UDPMulticastSender.cpp】 // 関数の宣言 BOOL CreateSocket(LPCSTR szDestAddress, WORD wDestPort, LPCSTR pszIFName); // ★ソケット作成と設定 //============================================== // function // ★UDPソケットの作成 // 送受信スレッドを開始する // parameter // LPCSTR szDestAddress [in]送信先アドレス(マルチキャストアドレス) // WORD wDestPort [in]送信先ポート // LPCSTR pszIFName [in]送信IF名/NULL // return // TRUE/FALSE //============================================== BOOL CreateSocket(LPCSTR szDestAddress, WORD wDestPort, LPCSTR pszIFName) { BOOL fRet = TRUE; SOCKET fd = INVALID_SOCKET; addrinfo hints, *pres = NULL, *pTemp = NULL; char szPort[NI_MAXSERV]; memset(&hints, 0, sizeof(hints)); hints.ai_socktype = SOCK_DGRAM; // UDP DISABLE_C4996 sprintf(szPort, “%d”, wDestPort); // ポート番号 ENABLE_C4996 // アドレスを指定してgetaddrinfoを実行 if (getaddrinfo(szDestAddress, szPort, &hints, &pres) == 0) { pTemp = pres; while (pTemp != NULL) { // socketを実行 if ((fd = socket(pTemp->ai_family, pTemp->ai_socktype, pTemp->ai_protocol)) == INVALID_SOCKET) goto L_NEXT; // ★送信ソケットの設定 SetSendSocketOption(fd, pTemp->ai_family, pszIFName); // socketが作れたのでこのソケット採用することにして抜ける fRet = TRUE; break; L_NEXT: pTemp = pTemp->ai_next; } } if (fd == INVALID_SOCKET) // ソケット作成に失敗 { DispErrorMsg(“Err:socket”); fRet = FALSE; goto L_END; } // 送受信スレッドを開始する m_pConInfo = (ConnectionInfoRec *)calloc(1, sizeof(ConnectionInfoRec)); m_pConInfo->fdClient = fd; m_pConInfo->ai_addr = (sockaddr *)calloc(pTemp->ai_addrlen, sizeof(BYTE)); m_pConInfo->ai_addrlen = pTemp->ai_addrlen; memcpy(m_pConInfo->ai_addr, pTemp->ai_addr, pTemp->ai_addrlen); m_pConInfo->pCMySyncObject = m_pCMySyncObject; m_pCSendRecvThread = new CSendRecvThread(m_pConInfo); m_pCSendRecvThread->Begin(); L_END: if (pres != NULL) freeaddrinfo(pres); return(fRet); }
main関数を次のように変更します。
起動パラメータで送信元インターフェースを取得
メッセージ送信を5秒ごとに繰り返す(何回目かを送信内容に追加)
【UDPMulticastSender.cpp】 #define SEND_INTERVAL (5 * 1000) // ★メッセージ送信間隔 int main(int argc, char *argv[]) { char szKeyInBuff[81]; LPSTR pszIFName = NULL; // ★送信元インターフェース名 int iSize = 0; LPBYTE pbDest = NULL; DWORD dwPevTime = 0, dwNow; // ★前回メッセージを送信した時刻 DWORD dwInterval = SEND_INTERVAL; // ★送信間隔 int iCount = 0; // ★何回目の文字列送信か char szCount[81]; // ★回数を文字列に LPBYTE pbSendMsg = NULL; // ★送信文字列 // EnableESC(stderr); // ★動作を確認するためECSシーケンスによる画面制御をOFFに m_pCMySyncObject = new CMySyncObject(); m_pCMySyncObject->Initialize(); // ★起動パラメータチェック 送信元ポートを指定可能に if ((argc != 3) && (argc != 4)) { fprintf(stderr, “%s <Multicast IP> <DestPort> [<InterfaceName>]\n”, argv[0]); goto L_END; } if (argc == 4) pszIFName = argv[3]; // ソケットライブラリの初期化 if (InitSocketLib() == FALSE) goto L_END; // ★ソケットの作成 ソケットの設定 送受信スレッドを開始する if (CreateSocket(argv[1], (WORD)atol(argv[2]), pszIFName) == FALSE) goto L_END; // メニューの表示 DispMenu(); while (1) { Sleep(10); // Sleepを入れておく方がCPU負荷のことを考えると良いでしょう if (KillZombei() == TRUE) // 致命的エラーならプログラム終了 break; // キーボードから入力された文字列が’q’なら終了 // ‘m’ならメッセージを入力後は送信する switch (GetKeyString(szKeyInBuff, sizeof(szKeyInBuff)- 1)) { case 0: // 入力なし、何もしない break; case CMD_QUIT_CHAR: // ‘q’入力終了 goto L_END; case CMD_SEND_MSG_CHAR: // ‘m’入力szKeyInBuffに文字列が格納されている // S-JISをUTF-8に変換して送信 iSize = 0; pbDest = NULL; if (strlen(szKeyInBuff) > 0) // “”で送信停止 { ConvSJistoUtf8((LPBYTE)szKeyInBuff, NULL, &iSize); pbDest = (LPBYTE)calloc(iSize, sizeof(BYTE)); ConvSJistoUtf8((LPBYTE)szKeyInBuff, pbDest, &iSize); // 送信は送信データをセットするだけ、実際の送信はCSendRecvThreadで実施 // ★メッセージパケットの送信(メッセージ+回数) iCount = 0; DISABLE_C4996 sprintf(szCount, “%d”, iCount); ENABLE_C4996 // メッセージに回数をくっつける pbSendMsg = (LPBYTE)calloc(iSize + strlen(szCount), sizeof(BYTE)); memcpy(pbSendMsg, szCount, strlen(szCount)); memcpy(pbSendMsg + strlen(szCount), pbDest, iSize); SendMessagePacket((char *)pbSendMsg, iSize + (int)strlen(szCount)); dwPevTime = timeGetTime(); // 送信した時刻を覚える ++iCount; SAFE_FREE(pbSendMsg) } // メニューの表示 DispMenu(); break; } // ★メッセージがあるとき5秒経過してたら送信 if (iSize > 0) { dwNow = timeGetTime(); if (GetdwInterval(dwNow, dwPevTime) >= dwInterval) { DISABLE_C4996 sprintf(szCount, “%d”, iCount); ENABLE_C4996 pbSendMsg = (LPBYTE)calloc(iSize + strlen(szCount), sizeof(BYTE)); memcpy(pbSendMsg, szCount, strlen(szCount)); memcpy(pbSendMsg + strlen(szCount), pbDest, iSize); SendMessagePacket((char *)pbSendMsg, iSize + (int)strlen(szCount)); dwPevTime = dwNow; ++iCount; SAFE_FREE(pbSendMsg) } } } SAFE_FREE(pbDest) // ★メッセージ領域開放 L_END: // すべてのソケットの破棄 送受信スレッド破棄 Stop(); // ソケットライブラリの開放 UninitSocketLib(); m_pCMySyncObject->Uninitialize(); SAFE_DELETE(m_pCMySyncObject) return(0); }
これでUDPMulticastSender for Windowsが完成しました。
動かしてみましょう。
WireSharkを使って、マルチキャストデータが送信されていることを確認しましょう。
次回はこのマルチキャストデータを受信する、UDPMulticastReceiverをつくること
にします。
マルチキャストグループ:ff1e::1
ポート番号:10000
インターフェース:イーサネット
を指定して起動します。
C:\>UDPMulticastSender.exe ff1e::1 10000 イーサネット Adapter Name : イーサネット IfIndex : 7 IPv6: 2001:ce8:132:4332:7985:ee4e:75:1c54/64 q:quit m:メッセージ送信 :m 送信メッセージを入力してください : UUUUUUUUUUUUU
パケットキャプチャの結果は下図の通りです。
Linux版も同様の対応で作成することができます。
以下はプロジェクトのリンクです。
UDPMulticastSender for Windows
UDPMulticastSender for Linux