今回は、IPv6/IPv4のどちらでUDPClientが動作していても同時に対応できるUDPServerを
作ってみましょう。
ポイントは、AI_PASSIVE、ポート番号を指定してgetaddrinfoをコールし、取得した
アドレス情報(すべてのfamily)に対してソケットを作成しbindします。
各ソケットに対して送受信スレッドを作ります。
プログラム起動引数 受信ポート番号 キーが押されるまで送信されてきたメッセージパケットを受信し文字列を表示する キーが押されたら終了する
Linux版UDPサーバプログラムを作成しましょう。
ファイル→新規作成→プロジェクト→クロスプラットホーム→
メイクファイルプロジェクト
名前:UDPServer
ソリューションプラットフォームはARM
SimpleServerTransFileから流用できるソースをコピーして追加します。
makefile
define.h
MySyncObject.h, MySyncObject.cpp
RingBuff.h, RingBuff.cpp
SendRecvThread.h, SendRecvThread.cpp
stdThread.h, stdThread.cpp
ThreadJob.h, ThreadJob.cpp
SimpleServer.cpp
makefileを変更します。
SimpleServer.cppをUDPServer.cppに名前を変更しTCP部分、機能を変更します。
CSendRecvThreadのTCP部分、機能を変更します。
SimpleServer.cppから名前を変更したUDPServer.cppについて、以下の変更を行います。
UDPには接続という概念がないので、接続ごと(acceptの結果)に送受信スレッドを作る
ことができません
bindに成功した分の送受信スレッドを作ります(有効なインターフェースの数分)。
・送受信スレッドの数=作成したソケット数
・listen, accept関係は削除
・接続中の他のクライアントというのは存在しないのでチャットメッセージ配信はなし
SerializeSendData()削除
・接続中・切断の概念がないのでゾンビ状態になった送受信スレッドの破棄はなし
KillZombei()削除
・bindしたUDPソケットをそのまま送受信スレッドのコンストラクタのパラメータとして
渡します
そのため、パラメータConnectionInfoRecを次のように変更します。
【SendRecvThread.h】 typedef struct { SOCKET fdClient; // bind済みUDPソケット CMySyncObject *pCMySyncObject; // 同期オブジェクト } ConnectionInfoRec;
これらの変更を行った後のUDPServer.cppは次のようになります。
変更部分には★印を記しています。
【UDPServer.cpp】 #include “stdThread.h” #include “SendRecvThread.h” // 送受信スレッドを使うため #include “MySyncObject.h” // 同期オブジェクトを使うため #include “define.h” #define MAX_SOCKET_NUM (20) // 待機用ソケットの最大数 // 関数の宣言 BOOL CreateAndBindSocket(WORD wPort); // 接続待ち用ソケットの作成と名前付け BOOL DestroySocket(SOCKET &fd); // ソケットの破棄 void Stop(); // すべてのソケットを破棄する BOOL CheckKey(); // キー入力の検査 // 変数の宣言 SOCKET m_fdServer[MAX_SOCKET_NUM]; // ★bind済みUDPソケット int m_iSockCount; // ★bind済みUDPソケットの数 CSendRecvThread *m_pCSendRecvThread[MAX_SOCKET_NUM] = { NULL }; //★ ConnectionInfoRec *m_pConInfo[MAX_SOCKET_NUM] = { NULL }; //★ CMySyncObject *m_pCMySyncObject = NULL; int main(int argc, char *argv[]) { int iRet = -1; int ii; Locate(1, 1, 2); m_pCMySyncObject = new CMySyncObject(); m_pCMySyncObject->Initialize(); // 起動パラメータチェック if (argc != 2) { fprintf(stderr, “Usage: %s <Server Port>\n”, argv[0]); goto L_END; } // 変数の初期化 for (ii = 0; ii < MAX_SOCKET_NUM; ++ii) m_fdServer[ii] = INVALID_SOCKET; // ★UDPソケットの作成とbind // bind済みのソケットがm_fdServer[]に格納される if (CreateAndBindSocket((WORD)atol(argv[1])) == FALSE) goto L_END; // ★bind済みソケットに対して送受信スレッドの作成 for (ii = 0; ii < m_iSockCount; ++ii) { m_pConInfo[ii] = (ConnectionInfoRec *)calloc(1, sizeof(ConnectionInfoRec)); m_pConInfo[ii]->pCMySyncObject = m_pCMySyncObject; m_pConInfo[ii]->fdClient = m_fdServer[ii]; m_pCSendRecvThread[ii] = new CSendRecvThread(m_pConInfo[ii]); m_pCSendRecvThread[ii]->Begin(); } // ★キーボーからの終了確認 while (1) { usleep(10 * 1000); // 10msec待ち if (CheckKey()) { fprintf(stderr, ” OK:Abort by key\n”); break; } } iRet = 0; L_END: // すべてのソケットの破棄 Stop(); m_pCMySyncObject->Uninitialize(); SAFE_DELETE(m_pCMySyncObject) return(iRet); } //============================================== // function // ★送受信用ソケットの作成とbindの実施 // parameter // WORD wPort [in]ポート番号 // return // TRUE/FALSE //============================================== BOOL CreateAndBindSocket(WORD wPort) { BOOL fRet = TRUE; struct addrinfo hints, *pres = NULL, *pTemp = NULL; char szPort[NI_MAXSERV]; int on; fprintf(stderr, “CreateAndBindSocket()\n”); m_iSockCount = 0; // ★データグラム型ですべてのインターフェースで受信可能なアドレス情報の条件 memset(&hints, 0, sizeof(hints)); hints.ai_socktype = SOCK_DGRAM; // ★UDP hints.ai_flags = AI_PASSIVE; DISABLE_C4996 sprintf(szPort, “%d”, wPort); ENABLE_C4996 if (getaddrinfo(NULL, szPort, &hints, &pres) == 0) { // 順番にpresの中に格納されている情報を使用する pTemp = pres; while ((pTemp != NULL) && (m_iSockCount < MAX_SOCKET_NUM)) { // Socketの作成, bindの実行 // ソケット作成 m_fdServer[m_iSockCount] = socket(pTemp->ai_family, pTemp->ai_socktype, pTemp->ai_protocol); if (m_fdServer[m_iSockCount] == INVALID_SOCKET) goto L_NEXT; fprintf(stderr, “%d %d %d %d\n”, (int)m_fdServer[m_iSockCount], pTemp->ai_family, AF_INET, AF_INET6); #ifdef IPV6_V6ONLY // IPv6ソケットでIPv4射影アドレスを使用しないように設定 if (pTemp->ai_family == AF_INET6) { on = 1; if (setsockopt(m_fdServer[m_iSockCount], IPPROTO_IPV6, IPV6_V6ONLY, (char *)&on, sizeof(on)) < 0) { DispErrorMsg(“Err:setsockopt”); goto L_NEXT; } } #endif // ★UDPではSO_REUSEADDR使わない // bindの実行 if (bind(m_fdServer[m_iSockCount], pTemp->ai_addr, (int)pTemp->ai_addrlen) == SOCKET_ERROR) { // bindに失敗したら、そのソケットを破棄して次のソケットの処理を行う DispErrorMsg(“Err:bind”); DestroySocket(m_fdServer[m_iSockCount]); goto L_NEXT; } ++m_iSockCount; L_NEXT: pTemp = pTemp->ai_next; // 次の情報を処理する } freeaddrinfo(pres); } if (m_iSockCount == 0) // ひとつもbindできなかったときは失敗を返す fRet = FALSE; return(fRet); } // 変更なし BOOL DestroySocket(SOCKET &fd) //============================================== // function // ★ソケットの破棄、受信スレッドの破棄 // parameter // なし // return // なし //============================================== void Stop() { int ii; fprintf(stderr, “Stop()\n”); //受信スレッドの破棄, 接続済みソケットの破棄 for (ii = 0; ii < m_iSockCount; ++ii) // ★ { if (m_pCSendRecvThread[ii] != NULL) { m_pCSendRecvThread[ii]->End(); m_pCSendRecvThread[ii]->WaitForEnd(); SAFE_DELETE(m_pCSendRecvThread[ii]); DestroySocket(m_pConInfo[ii]->fdClient); SAFE_FREE(m_pConInfo[ii]) } } } // 変更なし BOOL CheckKey()
CSendRecvThreadを変更します。
・CRecvFileThread関連の削除
m_pCRecvFileThread, KillZombei()
・接続という概念がない
このスレッドを利用して受信する際、相手(送信元)はデータ受信するまで
わかりません
また送信機能(sendto())については、誰に送信するかを指定しないと送信
できません。
今回は取り合えず、最後に受信した相手に送信することにします。
・チャットメッセージ分配関係を削除
GetDataForDistributeChat(), m_pCRingBuffForDistributeChat,
SetDataForDistributeChat(), SendMessagePacket()
これらの変更を行った後のSendRecvThread.hは次のようになります。
変更部分には★印を記しています。
【SendRecvThread.h】 #pragma once #include “ThreadJob.h” #include “define.h” // RecvMessagePacketの引数のため class CMySyncObject; // このクラスの使用することを宣言 class CRingBuff; // CRingBuffを使用するため #define SEND_BUFF_SIZE (1024 * 64) // 送信リングバッファのサイズ #if 1 #define SENDBUFSIZE (1024 + sizeof(HeaderRec)) // 一度に送信するサイズの最大値(共通ヘッダ分を加えてあります) #else #define SENDBUFSIZE (1024 * 4) // TCPで送信速度を1Mbpsより上げたいときは断片化処理をTCPのスタックに任せる // スタック(send)に渡すサイズを増やす #endif #define RECV_BUFF_SIZE (1024 * 64) // 受信リングバッファのサイズ // ★UDPは受信パケットサイズより小さなバッファでrecvfromを呼ぶと残りが破棄されるのでこのサイズには注意 // sendtoで送信されるサイズより小さくしない #define RCVBUFSIZE (1024 * 4) // ★一度に読む最大受信サイズ typedef struct { SOCKET fdClient; // ★bind済みUDPソケット CMySyncObject *pCMySyncObject; // 同期オブジェクト } ConnectionInfoRec; class CSendRecvThread : public CThreadJob { public: CSendRecvThread(ConnectionInfoRec *pConInfo); // パラメータをコンストラクタで渡す ~CSendRecvThread(); // 基底クラスの関数をオーバーライドする // C++11で明示的にoverrideを書くことが出来るようになりました // 基底クラスの当該関数にvirtualが書いていないとエラーを出してくれます UINT DoWork() override; // DoRecvで実施している内容を記述 BOOL SetSendData(char *pcData, int iSize); // 送信データの設定 BOOL IsZombie(); // このスレッドはゾンビ状態か private: ConnectionInfoRec *m_pConInfo; // コンストラクタで渡されるパラメータを格納 // このスレッド実行中領域が確保されていること BOOL m_fIamZombie; // ゾンビ状態かどうかを保持 CRingBuff *m_pCRingBuffSend; // 送信データ格納用リングバッファ CRingBuff *m_pCRingBuffRecv; // 受信データ格納用リングバッファ DWORD m_dwPrevSentTime; // 前回送信した時刻(msec) DWORD m_dwSendInterval; // 次回送信までの間隔(msec) int GetSendData(char **ppcData); // 送信データの取得 int AnalyzeDataRecv(); int RecvMessagePacket(HeaderRec *pHeader); BOOL SendMessagePacket(char *pcData, int iSize);// ★単純化のためこのクラス内で送信する DWORD CalcNextSendInterval(int iSentSize); // 次回送信までの間隔 BOOL CanSendNow(DWORD dwNow); // 送信して良い時刻になったか };
SendRecvThread.cppの変更点はTCP(send, recv)をUDP(sendto, recvfrom)に変更します。
recvfromは、受信バッファ(スタック)にあるパケットより小さなバッファサイズで取得に
行くとバッファに入らないパケットの残りは破棄されてしまうので注意が必要です。
また、TCPの時に使用したファイル受信機能などスレッド化した箇所については削除
しています。
CChatMsgThread, CRecvFileThread
KillZombei, RecvFileSendPacket, GetDataForDistributeChat
変更点には★を記しています。
【SendRecvThread.cpp】 #include “SendRecvThread.h” #include “MySyncObject.h” // CMySyncObjectを使うため #include “RingBuff.h” // CRingBuffを使うため // 送信速度 #define SEND_BPS (1000000.0) // 送信速度1Mbps //============================================== // function // コンストラクタ // parameter // ConnectionInfoRec *pConInfo [in]機能に必要な情報 // return // なし //============================================== CSendRecvThread::CSendRecvThread(ConnectionInfoRec *pConInfo) { m_pConInfo = pConInfo; m_fIamZombie = FALSE; m_pCRingBuffSend = new CRingBuff(SEND_BUFF_SIZE); // 送信リングバッファの構築 m_pCRingBuffRecv = new CRingBuff(RECV_BUFF_SIZE); // 受信リングバッファの構築 m_dwPrevSentTime = 0; // 初回送信はすぐに m_dwSendInterval = 0; // } //============================================== // function // デストラクタ // parameter // なし // return // なし //============================================== CSendRecvThread::~CSendRecvThread() { SAFE_DELETE(m_pCRingBuffSend) // 送信リングバッファの破棄 SAFE_DELETE(m_pCRingBuffRecv) // 受信リングバッファの破棄 } //============================================== // function // ★機能を記述した関数 // parameter // なし // return // 0:正常 -1:エラー発生 //============================================== UINT CSendRecvThread::DoWork() { BOOL fRet = TRUE; char szRecvBuffer[RCVBUFSIZE]; // 受信バッファ int iRecvSize; pollfd fds[1] = { 0 }; char *pcData = NULL; // 未送信データ int iSendSize = 0; // 未送信データサイズ DWORD dwNow; // 送信チェックした時刻を覚えるため sockaddr_storage PeerAddr; // ★送信元相手アドレス情報 socklen_t iPeerLen; // ★ char szPeerAddr[NI_MAXHOST]; // ★相手アドレス,ポート番号 char szServiceNo[NI_MAXSERV]; // をgetnameinfoで取得 fprintf(stderr, “DoWork()\n”); fds[0].fd = m_pConInfo-<fdClient; while (!m_fStopFlag) { // 送信データがあるかリングバッファを調べる iSendSize = m_pCRingBuffSend-<GetReadableSize(); fds[0].events = POLLIN | POLLRDHUP; // 受信と相手側からの切断イベントを設定 // チェックのために現在時刻を取得する dwNow = timeGetTime(); // 送信可能時刻かつ未送信データがあるときだけ送信可能検査 if ((iSendSize < 0) && (CanSendNow(dwNow) == TRUE)) fds[0].events |= POLLOUT; // 書き込み可能を追加 poll(fds, 1, 10); // ★切断の概念がないのでエラーなどでは終わらない if (fds[0].revents & POLLRDHUP) // 相手側からの切断(これは発生しないはず) fprintf(stderr, “Disconnected pollrdhup\n”); if (fds[0].revents & POLLERR) // エラー発生 DispErrorMsg(“Err:DoWork”); // 受信処理 if (fds[0].revents & POLLIN) // 受信データイベント { // ★UDPではパケットサイズより小さいサイズで受信すると残りは破棄される // ここで受信しなければ、次回のrevents検査で受信が行われる if (m_pCRingBuffRecv-<GetWriteableSize() < RCVBUFSIZE) { iPeerLen = sizeof(PeerAddr); // 送信元アドレス情報を格納する構造体のサイズ // ★recvfromでは0バイトのデータ受信がありうる if ((iRecvSize = recvfrom(m_pConInfo-<fdClient, szRecvBuffer, RCVBUFSIZE, 0, (sockaddr *)&PeerAddr, &iPeerLen)) < 0) { DispErrorMsg(“Err:recvfrom”); } else { // ★送信元の表示 getnameinfo((sockaddr *)&PeerAddr, iPeerLen, szPeerAddr, NI_MAXHOST, szServiceNo, NI_MAXSERV, NI_NUMERICHOST | NI_NUMERICSERV); fprintf(stderr, “recvfrom socket:%d Addr:%s Port:%s\n:”, m_pConInfo-<fdClient, szPeerAddr, szServiceNo); // 取得したデータすぐに受信リングバッファに書きこむ // 受信リングバッファに書き込むのはこのスレッドだけなので、すべて書き込めるはず m_pCRingBuffRecv-<Write((LPBYTE)szRecvBuffer, iRecvSize); } } } // 受信リングバッファに格納されているデータの解析を行う // 複数のパケットが格納されている可能性があるので、reventsの結果とは無関係に // 解析を行うようにする // ★エラーで終了しない if (AnalyzeDataRecv() == -1) DispErrorMsg(“Err:Packet format”); // ★sendtoで指定すべきあて先をどうするかがわからないので // 取りあえず最後に受信した相手に送り返しておきましょう // 送信処理 if (fds[0].revents & POLLOUT) // 送信可能ならsend実施 { // 送信したいデータの取得(PATH_MTUより小さくなるように取得する) iSendSize = GetSendData(&pcData); // ★ if (sendto(m_pConInfo-<fdClient, pcData, iSendSize, 0, (sockaddr *)&PeerAddr, iPeerLen) != iSendSize) { DispErrorMsg(“Err:sendto”); } SAFE_FREE(pcData) // 送信時刻と次回送信までの時間をセット m_dwPrevSentTime = dwNow; m_dwSendInterval = CalcNextSendInterval(iSendSize); } } m_pConInfo-<pCMySyncObject-<Lock(); m_fIamZombie = TRUE; m_pConInfo-<pCMySyncObject-<UnLock(); SAFE_FREE(pcData) return((fRet == TRUE) ? 0 : -1); } // 変更なし BOOL CSendRecvThread::SetSendData(char *pcData, int iSize) // 変更なし int CSendRecvThread::GetSendData(char **ppcData) // 変更なし BOOL CSendRecvThread::IsZombie() //============================================== // function // ★受信データの解析 // 受信リングバッファに格納されているデータを調べる // parameter // なし // return // 0:パケットが揃っていない // 1:パケットを受信したので処理を行った // -1:エラーが発生した //============================================== BOOL CSendRecvThread::AnalyzeDataRecv() { int iRet = 0; HeaderRec Header; WORD wCmd; // データサイズを調べる int iSize = m_pCRingBuffRecv-<GetReadableSize(); if (iSize < sizeof(HeaderRec)) // ヘッダサイズに満たないときは何もしない goto L_END; // ヘッダ部を借り読み込みする m_pCRingBuffRecv-<ReadWithoutUpdateHeadPoint((LPBYTE)&Header, sizeof(HeaderRec)); // ヘッダ部の解析 if (memcmp(Header.bMagicData, MAGIC_STRING, strlen(MAGIC_STRING)) != 0) { iRet = -1; // 識別子が違うのでエラー goto L_END; } wCmd = ntohs(Header.wCommand); Locate(1, 10, 1); fprintf(stderr, “CMD:%d”, wCmd); switch (wCmd) { case CMD_MSG_DATA: iRet = RecvMessagePacket(&Header); break; default: // 知らないコマンド iRet = -1; break; } L_END: return(iRet); } //============================================== // function // ★メッセージコマンドの受信 // parameter // HeaderRec *pHeader [in]仮読みしたヘッダ // return // 0:パケットが揃っていない // 1:パケットを受信したので処理を行った // -1:エラーが発生した //============================================== BOOL CSendRecvThread::RecvMessagePacket(HeaderRec *pHeader) { int iRet = 0; MsgDataRec *pMsgData; int iMsgSize, iSize; char *pszMsg = NULL; iMsgSize = ntohs(pHeader-<wDataLen); // データが足りないときは何もしない iSize = m_pCRingBuffRecv-<GetReadableSize(); if (iSize < iMsgSize + sizeof(HeaderRec)) goto L_END; // パケット全体を受信しているので読み込みを実施する pMsgData = (MsgDataRec *)calloc(iMsgSize + sizeof(HeaderRec), sizeof(BYTE)); m_pCRingBuffRecv-<Read((LPBYTE)pMsgData, iMsgSize + sizeof(HeaderRec)); // NULLターミネート分を追加して確保 pszMsg = (char *)calloc(iMsgSize + 1, sizeof(char)); memcpy(pszMsg, pMsgData-<bMsgData, iMsgSize); // ★送信のためにメッセージデータをパケット化してセット // 誰に送られるかはタイミング次第 SendMessagePacket(pszMsg, iMsgSize); m_pConInfo-<pCMySyncObject-<Lock(); fprintf(stderr, “Msg:recv %s\n”, pszMsg); m_pConInfo-<pCMySyncObject-<UnLock(); SAFE_FREE(pMsgData) SAFE_FREE(pszMsg) iRet = 1; L_END: return(iRet); } //============================================== // function // ★メッセージパケットの送信 // 送信リングバッファに格納する // parameter // char *pcData [in]UTF-8データ(NULLターミネートなし) // int iSize [in]データサイズ // retun // なし //============================================== BOOL CSendRecvThread::SendMessagePacket(char *pcData, int iSize) { MsgDataRec *pMsgData = NULL; int iPacketSize = sizeof(HeaderRec) + iSize; // パケットサイズ BOOL fRet = FALSE; pMsgData = (MsgDataRec *)calloc(iPacketSize, sizeof(BYTE)); // パケット全体のエリアを確保 memcpy(pMsgData-<header.bMagicData, MAGIC_STRING, strlen(MAGIC_STRING)); pMsgData-<header.wCommand = htons(CMD_MSG_DATA); pMsgData-<header.wDataLen = htons(iSize); memcpy(pMsgData-<bMsgData, pcData, iSize); // 送信のためにメッセージパケットデータをセット fRet = SetSendData((char *)pMsgData, iPacketSize); SAFE_FREE(pMsgData) return(fRet); } // 変更なし DWORD CSendRecvThread::CalcNextSendInterval(int iSentSize) // 変更なし
Windows版も同様の対応で作成することができます。
以下はプロジェクトのリンクです。
UDPSever for Windows
UDPServer for Linux
動かしてみましょう
以下の説明では、UDPServer(Linux)に対して3台のUDPClientを動かしています。
UDPServerはIPv6のクライアント、IPv4のクライアントからのでデータを受信できて
いることがわかります。
【UDPServer】 $ ./UDPServer 10000 CreateAndBindSocket() 3 2 2 10 4 10 2 10 DoWork() DoWork() recvfrom socket:4 Addr:fe80::11a0:ecd4:2cf8:6e5c%wlan0 Port:55012 ←IPv6から受信 :CMD:100Msg:recv aaaaaa recvfrom socket:4 Addr:fe80::7985:ee4e:75:1c54%wlan0 Port:54145 ←IPv6から受信 :CMD:100Msg:recv bbbbb recvfrom socket:3 Addr:192.168.101.68 Port:58626 ←IPv4から受信 :CMD:100Msg:recv ccccc
【UDPClient(1) IPv6】 $ ./UDPClient fe80::11a0:ecd4:2cf8:6e5c 10000 Index:0 HopLimitOrg 64 HopLimitNew 10 q:quit m:メッセージ送信 : m 送信メッセージを入力してください : aaaaaa q:quit m:メッセージ送信 : recv from Addr:fe80::11a0:ecd4:2cf8:6e5c%wlan0 Port:10000 :CMD:100 Msg:recv aaaaaa
【UDPClient(2) IPv6】 >UDPClient.exe fe80::11a0:ecd4:2cf8:6e5c 10000 Index:0 HopLimitOrg 128 HopLimitNew 10 q:quit m:メッセージ送信 : 送信メッセージを入力してください : bbbbb q:quit m:メッセージ送信 : recv from Addr:fe80::11a0:ecd4:2cf8:6e5c%7 Port:10000 :CMD:100 Msg:recv bbbbb
【UDPClient(3) IPv4】 >UDPClient.exe 192.168.101.55 10000 TTL 128 TTLNew 10 q:quit m:メッセージ送信 : 送信メッセージを入力してください : ccccc q:quit m:メッセージ送信 : recv from Addr:192.168.101.55 Port:10000 :CMD:100 Msg:recv ccccc
同じファミリのクライアントUDPClient(1), UDPClient(2)からの受信を見てみましょう。
recvfrom socket:4 Addr:fe80::11a0:ecd4:2cf8:6e5c%wlan0 Port:55012 ←IPv6から受信
:CMD:100Msg:recv aaaaaa
recvfrom socket:4 Addr:fe80::7985:ee4e:75:1c54%wlan0 Port:54145 ←IPv6から受信
:CMD:100Msg:recv bbbbb
と当然ですが、同じソケット4で受信をしています。
つまり、同じスレッドで受信してるということです。
TCPでは、クライアントごとのスレッドで受信していたので、クライアントごとの処理を
実行することは容易でした。(スレッドごとに独立しているので)
UDPではどうすれば良いでしょうか。この2つのクライアントからの受信を区別するための
何らかの方法(識別子)が必要になります。
(*)IPアドレスは使えません。同じ端末で2つクライアントソフトが動作するかも
知れないからです。
実用的なUDPのプログラム、例えばTCPで作った簡易チャットでさえ、このままでは実現
できません。
各クライアントごとに識別子を作って、サーバで受信(同一スレッドで)したものを識別子に
従って別のバッファに格納し、そのバッファの処理を個々のスレッドで実施するという
方法もあるでしょう。とても複雑そうです。
ここでは、イニシエーション・ネゴシエーションの部分はTCPで記述し、データ転送を
UDPで行う(場合によっては、UDPで行うクライアントとTCPで行うクライアントが混在する)
ようなシステムを考えてみようと思います。
これについては、ちょっと時間を空けてから解説することにしましょう。