UDPを使ったアプリケーションを作ることを考えてみましょう。
例えばSimpleClientTrandFile, SimpleServerTransFileで実現した機能(チャット、
ファイル送信、ファイル受信)をUDPで実現するにはどうすれば良いかを考えてみます。
UDPClient, UDPServerでは複数のクライアント(同じファミリ)からの送信データを
同一のスレッド、ソケットで受信しているのでユーザを区別して処理をどうするかが
課題です。
【対応方法案】
パケットに送信元が区別できる情報を埋め込む
受信時に送信元情報を調べ、相手ごとに受信リングバッファを分ける
相手先ごとに受信データを処理するスレッドを作る
確かにできそうですが、これを実現するには、
送信元を区別(識別子など)する方法が必要です。
同じホストやプライベートネットワークに送信元がある場合を考えると
IPアドレスでは区別できません。
送受信スレッドがこれまでより複雑になります。
またチャットでは、今サーバを使っている他のユーザにメッセージを送ります。
UDPには接続という概念がないので、この方法だけではだれがサーバを使っているかを
判断することは難しいですね。(生死確認のような仕組みが必要ですね)
それでも、UDPは、再送の仕組みがないので、リアルタイムデータ(音声など)に
適しています。UDPでデータ転送を行うシステムは作ってみたいです。
そこで、データ転送をUDPで行う実用的なシステムとして次のような構造のものを
考えてみることにしましょう。
ネゴシエーション(認証など)などのような失って困るもの(コントロールと呼ぶことに
しましょう)はTCPで行います。その結果だれがサーバを使っているかもわかります。
またTCPの接続に対してユーザごとの送受信スレッドを作ることもできます。
その送受信スレッドで、データ(チャット、ファイル)転送はUDPで行うようにします。
これだとUDPが使えない環境(ファイアウォールなどでUDPを拒否)ではTCPでデータ転送を
行う機能も持つようすることもできます。
実例として、RTSP(Real Time Streaming Protocol)でこの方法が使われています。
ネットワークカメラなどで使われている「映像・音声のリアルタイムな
ストリーミング配信」を行うためのプロトコルです。
RTSPでは、ネゴシエーション(どのような種類のメディアを配信するか、どんな
方法で配信するか)をTCPを使って行っています。
RTSPそのものはhttpに似た文字列で、クライアントとサーバ間でメッセージの交換を
行います
SETUPというメッセージで転送方法を交換します。
ANNOUNCEというメッセージでメディアの種類を交換します。
SDP(Session Description Protocol)で記述方法が決められています。
PLAYというメッセージで配信を開始します。
RTP(Real-time Transport Protocol)パケットにメディア(音声・映像)を
入れて決定した転送方法で配信します。
それでは、SimpleClientTransFile, SimpleServerTransFileのデータ転送として
UDPを使えるようにしてみましょう。
変更内容としては次のような内容です。
・データ転送を行う方法をUDP/TCPの切り替えるコントロールを追加
・CSendRecvThreadでデータ転送をUDPで行うためにUDPソケットを追加
・UDPによる送受信に対応する送信リングバッファ、受信リングバッファを追加
・各機能のリングバッファとのデータ交換をTCP用バッファかUDP用バッファで
行うかを切り替える
(*)今回の実装では、データをロストした時の対応(再送など)には対応していないので
ファイルの送受信については、データをロストして失敗するかも知れません。
ここで、サーバから見たときに、クライアント(通信相手)の区別について、TCPとUDPで
どのようになっているかを確認しておきましょう。
TCPでは、サーバが受信したデータの内容を調べることなく、通信相手を区別できますが
UDPでは区別できません。
そこで、今回はクライアントごとに通信に使用するポート番号をかえることでUDPでも
相手を区別できるようにします。
データ転送方法としてUDPを使うときは、の決定を行う際にサーバ・クライアント間で
使用するポート番号の交換を行うことにします。
データ転送方法の交換を行うコントロールとしてSimpleClientTrandFile,
SimpleServerTransFileに次のような定義を追加します。
【define.h】 // ★データ転送方法 要求/応答 // Client->Server #define CTRL_TRANS_METHOD_REQ 400 // ★データ送受信方法要求(TCP/UDP) // Server->Client #define CTRL_TRANS_METHOD_RES 1400 // ★データ送受信方法応答(TCP/UDP) // ★データ転送方法 要求応答 // データ部 typedef struct { BYTE bMethod; // 0:TCP 1:UDP WORD wUDPSrcPort; // 要求時クライアント送信元ポート番号 // 応答時サーバ送信元ポート番号 } TransMethodInfoRec; // 共通ヘッダ部 + データ部 // ClientからUDP要求を出した時、Serverから応答がTCPならTCPでデータ転送を行う // Serverからの応答がUDPなら返ってきたポート番号をクライアントは送信先として使用する typedef struct { HeaderRec header; TransMethodInfoRec TansMethodInfo; } TransMethodReqRec, TransMethodResRec;
それでは、SimpleClientTransFile(Winodws版)にUDPによるデータ転送機能を追加
してみましょう。
SimpleClient.cpp:データ転送方法の交換のためのメニュー用の定義を追加します。
SendRecvThread.h:UDPのデータ転送のために相手先アドレス情報をパラメータに追加します。
【SimpleClient.cpp】 // メニューで使用する文字 #define CMD_QUIT_CHAR ‘q’ #define CMD_SEND_MSG_CHAR ‘m’ #define CMD_SEND_FILE_CHAR ‘s’ // ファイル送信開始 #define CMD_TRANS_FILE_ABORT_CHAR ‘a’ // ファイル送信/受信中断 #define CMD_RECV_FILE_CHAR ‘r’ // ファイル受信開始(サイズ要求) #define CMD_USE_UDP_CHAR ‘U’ // ★データ送受信をUDPで行う #define CMD_USE_ONLY_TCP_CHAR ‘T’ // ★データ送受信をTCPで行う 【SendRecvThread.h】 typedef struct { SOCKET fdClient; // 接続済みソケット(connectの結果) CMySyncObject *pCMySyncObject; // 同期オブジェクト sockaddr *ai_addr; // ★接続先アドレス(UDPの送信先として使用) size_t ai_addrlen; // ★接続先アドレス長 } ConnectionInfoRec;
SimpleClient.cppでメニューで指定されたデータ転送方法要求をサーバに送信する関数を
呼び出すようにします。
【SimpleClient.cpp】 int main(int argc, char *argv[]) { … while (1) { … switch (GetKeyString(szKeyInBuff, sizeof(szKeyInBuff)- 1)) { … case CMD_TRANS_FILE_ABORT_CHAR: // ‘a’ファイル送信/受信中断 if (m_pCSendRecvThread != NULL) { m_pCSendRecvThread->SendFileAbort(); // 送信中断 m_pCSendRecvThread->RecvFileAbort(); // 受信中断 } break; case CMD_USE_UDP_CHAR: // ★’U’データ送受信をUDPで szKeyInBuffに送信元ポート番号が格納されている if (m_pCSendRecvThread != NULL) { m_pCSendRecvThread->SendTransMethodPacket(1, (WORD)atol(szKeyInBuff)); } DispMenu(); break; case CMD_USE_ONLY_TCP_CHAR: // ★’T’データ送受信をTCPで if (m_pCSendRecvThread != NULL) { m_pCSendRecvThread->SendTransMethodPacket(0, 0); } DispMenu(); break; } } … }
SimpleClient.cppのCreateAndConnectSocket関数で、送受信スレッドを作成するときに
パラメータとして相手先アドレスも渡すようにします。
【SimpleClient.cpp】 BOOL CreateAndConnectSocket(LPCSTR szAddress, WORD wPort) { … if (getaddrinfo(szAddress, szPort, &hints, &pres) == 0) { … ShowInterfaceIndex(pTemp); // HopLimitを取得設定する(ルータ越を考えるときは大きめの値に) // SetHopLimit(fd, pTemp->ai_family, 10); … L_NEXT: pTemp = pTemp->ai_next; } // freeaddrinfo(pres); // ★pTempを使うのでここでは開放しない } … // 送信スレッドを開始する m_pConInfo = (ConnectionInfoRec *)calloc(1, sizeof(ConnectionInfoRec)); m_pConInfo->fdClient = fd; m_pConInfo->pCMySyncObject = m_pCMySyncObject; // ★相手アドレスを渡す m_pConInfo->ai_addr = pTemp->ai_addr; m_pConInfo->ai_addrlen = pTemp->ai_addrlen; m_pCSendRecvThread = new CSendRecvThread(m_pConInfo); m_pCSendRecvThread->Begin(); L_END: if(pres != NULL) // ★アドレスを渡した後に開放 freeaddrinfo(pres); return(fRet); }
SimpleClient.cppのメニュー表示、キーボード入力関数にデータ転送方法の指定を
追加します。
【SimpleClient.cpp】 //============================================== // function // メニューの表示 // parameter // なし // return // なし //============================================== void DispMenu() { // ★データ転送方法の指定(U,T)を追加 Locate(1, 8, 2); fprintf(stderr, “%c:quit %c:メッセージ送信 %c:ファイル送信 %c:中断\n%c:データ送受信にUDPを使用する %c:データ送受信にTCPを使用する : “, CMD_QUIT_CHAR, CMD_SEND_MSG_CHAR, CMD_SEND_FILE_CHAR, CMD_TRANS_FILE_ABORT_CHAR, CMD_USE_UDP_CHAR, CMD_USE_ONLY_TCP_CHAR); } //============================================== // function // キーボード入力(改行まで)の取得 // 入力がないときはすぐにリターン // parameter // LPSTR pszString [in/out]データ格納用バッファ // int iSize [in]バッファのサイズ // return // 0:入力なし CMD_QUIT_CHAR:終了 CMD_SEND_MSG_CHAR:メッセージ送信 // CMD_SEND_FILE_CHAR:ファイル送信 //============================================== int GetKeyString(LPSTR pszString, int iSize) { … if (_kbhit()) { … switch (_getch()) { … // ★Uコマンド 送信元ポート番号入力 case CMD_USE_UDP_CHAR: fprintf(stderr, “\nUDP送信元ポート番号を入力してください : “); fgets(pszString, iSize, stdin); if (strlen(pszString) > 0) { // 改行まで読み込んでいるので if (pszString[strlen(pszString) – 1] == ‘\n’) pszString[strlen(pszString) – 1] = 0; iRet = CMD_USE_UDP_CHAR; } break; // Tコマンド case CMD_USE_ONLY_TCP_CHAR: iRet = CMD_USE_ONLY_TCP_CHAR; break; default: break; } } return(iRet); }
CSendRecvThreadにUDPでのデータ転送用にUDPソケット、UDP用送受信リングバッファを
追加します。
送信時、コントロールはTCPで行うので、強制的にTCPを使用する機能を追加します。
送信、受信をTCP・UDPで行うため、それぞれ送信関数、受信関数を作り、DoWork関数を
単純化します。
【SendRecvThread.h】 #pragma once … typedef struct { SOCKET fdClient; // 接続済みソケット(connectの結果) CMySyncObject *pCMySyncObject; // 同期オブジェクト sockaddr *ai_addr; // ★接続先アドレス(UDPの送信先として使用) size_t ai_addrlen; // ★接続先アドレス長 } ConnectionInfoRec; class CSendRecvThread : public CThreadJob { public: … UINT DoWork() override; // DoSendで実施している内容を記述 // ★送信データの設定 コントロール用に強制的にTCPで送信する機能を追加 BOOL SetSendData(char *pcData, int iSize, BOOL fForceTCP = FALSE); … BOOL SendMessagePacket(char *pcData, int iSize); // メッセージ送信 BOOL SendTransMethodPacket(BYTE bMethod, WORD wUDPSrcPort); // ★データ転送方法要求送信 private: … CRingBuff *m_pCRingBuffSend; // 送信データ格納用リングバッファ(TCP) CRingBuff *m_pCRingBuffRecv; // 受信データ格納用リングバッファ(TCP) CRingBuff *m_pCRingBuffSendUDP; // ★送信データ格納用リングバッファ(UDP) CRingBuff *m_pCRingBuffRecvUDP; // ★受信データ格納用リングバッファ(UDP) sockaddr_storage m_PeerAddr; // ★相手先情報(UDP送信用) socklen_t m_iPeerAddrLen; // ★相手先情報の長さ SOCKET m_fdUDP; // ★データ送受信方法がUDP使用時のソケット BYTE m_bTarnsMethod; // ★0:TCP 1:UDP DWORD m_dwPrevSentTime; // 前回送信した時刻(msec)(TCP) DWORD m_dwSendInterval; // 次回送信までの間隔(msec)(TCP) DWORD m_dwPrevSentTimeUDP; // ★前回送信した時刻(msec)UDP DWORD m_dwSendIntervalUDP; // ★次回送信までの間隔(msec)UDP … int GetSendData(char **ppcData, int iTcpUdp); // ★送信データの取得(TCP/UDPで参照バッファ切り替え) BOOL RecvTCPData(fd_set *rfds); // ★TCP受信 BOOL RecvUDPData(fd_set *rfds); // ★UDP受信 BOOL SendTCPData(fd_set *wfds, DWORD dwNow); // ★TCP送信 BOOL SendUDPData(fd_set *wfds, DWORD dwNow); // ★UDP送信 int AnalyzeDataRecv(int iTcpUdp); // ★受信データの解析(TCP/UDPで参照バッファ切り替え) int RecvMessagePacket(HeaderRec *pHeader, int iTcpUdp); // ★メッセージ受信(TCP/UDPで参照バッファ切り替え) int RecvFileSendResPacket(HeaderRec *pHeader, int iTcpUdp); // ★ファイル送信コマンド/応答の受信処理(TCP/UDPで参照バッファ切り替え) int RecvFileRecvResPacket(HeaderRec *pHeader, int iTcpUdp); // ★ファイル受信コマンド/応答の受信処理(TCP/UDPで参照バッファ切り替え) int RecvTransMethodResPacket(HeaderRec *pHeader); // ★データ送受信方法応答の受信処理(TCPのみ) BOOL MakePeerUDPSockaddr(WORD wPeerPort); // ★UDP通信用に相手先sockaddrを作る(ポート番号付与) DWORD CalcNextSendInterval(int iSentSize); // 次回送信までの間隔 BOOL CanSendNow(DWORD dwNow, int iTcpUdp); // ★送信して良い時刻になったか(TCP/UDPで参照変数切り替え) BOOL KillZombei(); // 不要なスレッドを破棄する };
CSendRecvThreadで作成したUDPソケットを破棄するための宣言をSendRecvThread.cppに
追加します。
【SendRecvThread.cpp】 extern BOOL DestroySocket(SOCKET &fd); // ★UDPソケットを破棄するため SimpleClietn.cppにある
UDP送受信のための変数の初期化をコンストラクタに追加します。
・送受信データ格納用リングバッファ作成
・送信帯域制御用変数の初期化
・UDPソケット格納用変数の初期化
・データ転送方法(TCP/UDP)格納用変数の初期化
・送信先アドレスの保存(ポート番号はUDP使用が決定した時に設定)
【SendRecvThread.cpp】 //============================================== // function // コンストラクタ // parameter // ConnectionInfoRec *pConInfo [in]機能に必要な情報 // return // なし //============================================== CSendRecvThread::CSendRecvThread(ConnectionInfoRec *pConInfo) { m_pConInfo = pConInfo; m_fIamZombie = FALSE; m_pCRingBuffSend = new CRingBuff(SEND_BUFF_SIZE); // 送信リングバッファの構築(TCP) m_pCRingBuffRecv = new CRingBuff(RECV_BUFF_SIZE); // 受信リングバッファの構築(TCP) m_pCRingBuffSendUDP = new CRingBuff(SEND_BUFF_SIZE);// ★送信リングバッファの構築(UDP) m_pCRingBuffRecvUDP = new CRingBuff(RECV_BUFF_SIZE);// ★受信リングバッファの構築(UDP) m_dwPrevSentTime = 0; // 初回送信はすぐに(TCP) m_dwSendInterval = 0; // m_dwPrevSentTimeUDP = 0; // ★初回送信はすぐに(UDP) m_dwSendIntervalUDP = 0; // ★ m_pCSendFileThread = NULL; // ファイル送信実施スレッド m_pCRecvFileThread = NULL; // ファイル受信実施スレッド m_fdUDP = INVALID_SOCKET; // ★データ送受信UDP時のUDPソケット m_bTarnsMethod = 0; // ★0:TCP 1:UDP CTRL_TRANS_METHOD_RES受信で確定 // ★相手の情報を覚える(ポートはTCPの接続で使用したものなのでUDP使用時に変更する) m_iPeerAddrLen = (socklen_t)m_pConInfo->ai_addrlen; memcpy(&m_PeerAddr, m_pConInfo->ai_addr, m_iPeerAddrLen); // チャットメッセージ処理を常に使えるように準備する m_pCChatMsgThread = new CChatMsgThread(this); m_pCChatMsgThread->Begin(); }
UDP送受信のために追加した変数の開放をデストラクタに追加します。
【SendRecvThread.cpp】 //============================================== // function // デストラクタ // parameter // なし // return // なし //============================================== CSendRecvThread::~CSendRecvThread() { if (m_pCChatMsgThread != NULL) // このクラスが破棄されるときにチャットメッセージ処理スレッドを破棄 { m_pCChatMsgThread->End(); m_pCChatMsgThread->WaitForEnd(); } SAFE_DELETE(m_pCChatMsgThread) if (m_pCSendFileThread != NULL) // もし動いていたら止める(中断も必要でしょう) { m_pCSendFileThread->End(); m_pCSendFileThread->WaitForEnd(); } SAFE_DELETE(m_pCSendFileThread) if (m_pCRecvFileThread != NULL) // もし動いていたら止める(中断も必要でしょう) { m_pCRecvFileThread->End(); m_pCRecvFileThread->WaitForEnd(); } SAFE_DELETE(m_pCRecvFileThread) SAFE_DELETE(m_pCRingBuffSend) // 送信リングバッファの破棄(TCP) SAFE_DELETE(m_pCRingBuffRecv) // 受信リングバッファの破棄(TCP) SAFE_DELETE(m_pCRingBuffSendUDP) // ★送信リングバッファの破棄(UDP) SAFE_DELETE(m_pCRingBuffRecvUDP) // ★受信リングバッファの破棄(UDP) DestroySocket(m_fdUDP); // ★UDPソケットの破棄 }
DoWorkで行っている送受信処理にUDPの送受信処理を追加します。
・受信については、TCPは常に受信します。
UDPはデータ転送方法がUDPの時だけ受信します。
・送信については、コントロールはTCPでそのほかは、データ転送方法に応じて処理を
行います。
TCPは、送信リングバッファにデータがあれば常に送信します。
UDPは、データ転送方法がUDPの時だけ送信します。
・可読性を良くするために、受信関数、送信関数を独立させます。
【SendRecvThread.cpp】 //============================================== // function // 機能を記述した関数(DoSend, DoRecvの内容を記述する) // parameter // なし // return // 0:正常 -1:エラー発生 //============================================== UINT CSendRecvThread::DoWork() { BOOL fRet = TRUE; fd_set wfds, rfds; struct timeval tv; int iSendSize = 0; // 未送信データサイズ DWORD dwNow; // 送信チェックした時刻を覚えるため while (!m_fStopFlag) { // 不要なスレッドを破棄 // チャットメッセージ処理スレッドがエラーなら終了 if (KillZombei() == TRUE) { fRet = FALSE; break; } tv.tv_sec = 0; tv.tv_usec = 10 * 1000; // 10msec FD_ZERO(&rfds); FD_ZERO(&wfds); // 受信データ検査用fd_setは常にセットする FD_SET(m_pConInfo->fdClient, &rfds); // 送信可能時刻かつ未送信データがあるときだけ送信可能検査用fd_setにセットする // チェックのために現在時刻を取得する timeBeginPeriod(1); // タイマーの最小精度を1msecにする dwNow = timeGetTime(); timeEndPeriod(1); // タイマーの最小精度を戻す // 送信データがあるかリングバッファを調べる(TCP) iSendSize = m_pCRingBuffSend->GetReadableSize(); // ★TCPの送信可能時刻かつ未送信データがあるか if ((iSendSize > 0) && (CanSendNow(dwNow, 0) == TRUE)) FD_SET(m_pConInfo->fdClient, &wfds); // ★データ転送方法がUDPの時、UDPの送信のための処理 if (m_bTarnsMethod == 1) { // ★受信データ検査用fd_setは常にセットする FD_SET(m_fdUDP, &rfds); // ★送信データがあるかリングバッファを調べる(UDP) iSendSize = m_pCRingBuffSendUDP->GetReadableSize(); // ★UDPの送信可能時刻かつ未送信データがあるか if ((iSendSize > 0) && (CanSendNow(dwNow, 1) == TRUE)) FD_SET(m_fdUDP, &wfds); } select(FD_SETSIZE, &rfds, &wfds, NULL, &tv); // タイムアウトまでSleepと同等 // ★受信処理TCP if((fRet = RecvTCPData(&rfds)) == FALSE) break; // ★受信処理UDP if ((fRet = RecvUDPData(&rfds)) == FALSE) break; // 受信リングバッファに格納されているデータの解析を行う // 複数のパケットが格納されている可能性があるので、FD_ISSETの結果とは無関係に // 解析を行うようにする // ★TCP受信データの解析 if (AnalyzeDataRecv(0) == -1) // 返値が-1の時がエラー { DispErrorMsg(“Err:Packet format TCP”); fRet = FALSE; break; } // ★UDP受信データの解析 if (AnalyzeDataRecv(1) == -1) // 返値が-1の時がエラー { DispErrorMsg(“Err:Packet format UDP”); fRet = FALSE; break; } // ★送信処理(TCP) if ((fRet = SendTCPData(&wfds, dwNow)) == FALSE) break; // ★送信処理(UDP) if ((fRet = SendUDPData(&wfds, dwNow)) == FALSE) break; } m_pConInfo->pCMySyncObject->Lock(); m_fIamZombie = TRUE; m_pConInfo->pCMySyncObject->UnLock(); return((fRet == TRUE) ? 0 : -1); }
可読性を良くするために、独立させた受信関数(TCP、UDP)、送信関数(TCP、UDP)を
追加します。
【SendRecvThread.cpp】 //============================================== // function // ★TCPの受信 // 受信リングバッファに格納 // parameter // fd_set *rfds [in] // return // TRUE/FALSE:切断/エラー //============================================== BOOL CSendRecvThread::RecvTCPData(fd_set *rfds) { BOOL fRet = TRUE; char szRecvBuffer[RCVBUFSIZE]; // 受信データを一時的に格納 int iRecvSize; if (FD_ISSET(m_pConInfo->fdClient, rfds)) // 受信データがあればrecv実施 { // 受信リングバッファに空きがあれば取得する // ここで受信しなければ、次回のFD_ISSETで受信データありがセットされるので // 次回に取得することができる iRecvSize = min(m_pCRingBuffRecv->GetWriteableSize(), RCVBUFSIZE); if (iRecvSize > 0) { if ((iRecvSize = recv(m_pConInfo->fdClient, szRecvBuffer, iRecvSize, 0)) <= 0) { if (iRecvSize == 0) DispErrorMsg(“Disconnected recv”); else DispErrorMsg(“Err:recv”); fRet = FALSE; } else { // 取得したデータすぐに受信リングバッファに書きこむ // 受信リングバッファに書き込むのはこのスレッドだけなので、すべて書き込めるはず m_pCRingBuffRecv->Write((LPBYTE)szRecvBuffer, iRecvSize); } } } return(fRet); } //============================================== // function // ★UDPの受信 // 受信リングバッファに格納 // parameter // fd_set *rfds [in] // return // TRUE //============================================== BOOL CSendRecvThread::RecvUDPData(fd_set *rfds) { BOOL fRet = TRUE; char szRecvBuffer[RCVBUFSIZE]; // 受信データを一時的に格納 int iRecvSize; sockaddr_storage PeerAddr; // 送信元アドレス情報格納エリア socklen_t iPeerLen; if (FD_ISSET(m_fdUDP, rfds)) // 受信データがあればrecvfrom実施 { // UDPではパケットサイズより小さいサイズで受信すると残りは破棄される // ここで受信しなければ、次回の検査で受信が行われる if (m_pCRingBuffRecvUDP->GetWriteableSize() > RCVBUFSIZE) { iPeerLen = sizeof(PeerAddr); // 送信元アドレス情報を格納する構造体のサイズ if ((iRecvSize = recvfrom(m_fdUDP, szRecvBuffer, RCVBUFSIZE, 0, (sockaddr *)&PeerAddr, &iPeerLen)) < 0) { DispErrorMsg(“Err:recvfrom”); fRet = FALSE; } else { char szPeerAddr[NI_MAXHOST]; // 相手アドレス,ポート番号 char szServiceNo[NI_MAXSERV]; // をgetnameinfoで取得 // 送信元の表示 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_fdUDP, szPeerAddr, szServiceNo); // 取得したデータすぐに受信リングバッファに書きこむ // 受信リングバッファに書き込むのはこのスレッドだけなので、すべて書き込めるはず m_pCRingBuffRecvUDP->Write((LPBYTE)szRecvBuffer, iRecvSize); } } } return(TRUE); } //============================================== // function // ★TCPの送信 // 送信リングバッファのデータを送信する // parameter // fd_set *wfds [in] // DWORD dwNow [in]現在時刻 // return // TRUE/FALSE:切断/エラー //============================================== BOOL CSendRecvThread::SendTCPData(fd_set *wfds, DWORD dwNow) { BOOL fRet = TRUE; char *pcData = NULL; // 未送信データ int iSendSize = 0; // 未送信データサイズ if (FD_ISSET(m_pConInfo->fdClient, wfds)) // 送信可能ならsend実施 { // 送信したいデータの取得(PATH_MTUより小さくなるように取得する) iSendSize = GetSendData(&pcData, 0); if (send(m_pConInfo->fdClient, pcData, iSendSize, 0) != iSendSize) { DispErrorMsg(“Err:send\n”); fRet = FALSE; } SAFE_FREE(pcData) // 送信時刻と次回送信までの時間をセット m_dwPrevSentTime = dwNow; m_dwSendInterval = CalcNextSendInterval(iSendSize); } SAFE_FREE(pcData) return(fRet); } //============================================== // function // ★UDPの送信 // 送信リングバッファのデータを送信する // parameter // DWORD dwNow [in]現在時刻 // fd_set *wfds [in] // return // TRUE //============================================== BOOL CSendRecvThread::SendUDPData(fd_set *wfds, DWORD dwNow) { BOOL fRet = TRUE; char *pcData = NULL; // 未送信データ int iSendSize = 0; // 未送信データサイズ if ((m_bTarnsMethod == 1) && (FD_ISSET(m_fdUDP, wfds))) { // 送信したいデータの取得(PATH_MTUより小さくなるように取得する) iSendSize = GetSendData(&pcData, 1); if (sendto(m_fdUDP, pcData, iSendSize, 0, (sockaddr *)&m_PeerAddr, m_iPeerAddrLen) != iSendSize) { DispErrorMsg(“Err:sendto”); fRet = FALSE; } SAFE_FREE(pcData) // 送信時刻と次回送信までの時間をセット m_dwPrevSentTimeUDP = dwNow; m_dwSendIntervalUDP = CalcNextSendInterval(iSendSize); } SAFE_FREE(pcData) return(fRet); }
送信リングバッファにデータを書き込む関数にTCP, UDPの切り替え機能を追加します。
コントロールは常にTCPで行うの強制的にTCPの送信リングバッファに書きこめる機能も
追加します。
【SendRecvThread.cpp】 //============================================== // function // ★送信データの設定(TCP/UDP) // parameter // char *pcData [in]送信データ // int iSize [in]データ長 // BOOL fForceTCP [in]必ずTCP(コントロール) // return // TRUE/FALSE //============================================== BOOL CSendRecvThread::SetSendData(char *pcData, int iSize, BOOL fForceTCP) { BOOL fRet = FALSE; CRingBuff *pCRingBuff = ((fForceTCP == FALSE) && (m_bTarnsMethod == 1)) ? m_pCRingBuffSendUDP : m_pCRingBuffSend; // 送信リングバッファに空きがないときは書き込まない fRet = pCRingBuff->Write((LPBYTE)pcData, iSize); return(fRet); }
送信リングバッファからデータを読み込む関数にTCP, UDPの切り替え機能を追加します。
【SendRecvThread.cpp】 //============================================== // function // ★送信データの取得(TCP/UDP) // parameter // char **ppcData [in/out]送信データ // int iTcpUdp [in]0:TCP 1:UDP // return // データ長 //============================================== int CSendRecvThread::GetSendData(char **ppcData, int iTcpUdp) { int iSize = 0; CRingBuff *pCRingBuff = (iTcpUdp == 1) ? m_pCRingBuffSendUDP : m_pCRingBuffSend; // 送信データがあるかリングバッファのデータサイズを調べる if ((iSize = pCRingBuff->GetReadableSize()) > 0) { // 送信サイズをPATH_MTUより小さくしておく(1024+共通ヘッダなら大丈夫) iSize = min(iSize, SENDBUFSIZE); *ppcData = (char *)calloc(iSize, sizeof(char)); iSize = pCRingBuff->Read((LPBYTE)*ppcData, iSize); } return(iSize); }
受信データの解析関数に対象とする受信リングバッファをTCP, UDPで切り替え機能を
追加します。
各機能を実現するスレッドのバッファにデータを書き込む処理もTCP, UDPで切り替え
ようにします。ただしコントロールについては、TCPのみ処理を行います。
【SendRecvThread.cpp】 //============================================== // function // ★受信データの解析 // 受信リングバッファに格納されているデータを調べる // parameter // int iTcpUdp [in]0:TCP 1:UDP // return // 0:パケットが揃っていない // 1:パケットを受信したので処理を行った // -1:エラーが発生した //============================================== int CSendRecvThread::AnalyzeDataRecv(int iTcpUdp) { int iRet = 0; HeaderRec Header; WORD wCmd; CRingBuff *pCRingBuff = (iTcpUdp == 1) ? m_pCRingBuffRecvUDP : m_pCRingBuffRecv; // データサイズを調べる int iSize = pCRingBuff->GetReadableSize(); if (iSize < sizeof(HeaderRec)) // ヘッダサイズに満たないときは何もしない goto L_END; // ヘッダ部を借り読み込みする pCRingBuff->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, 7, 1); fprintf(stderr, “CMD:%d\n”, wCmd); switch (wCmd) { case CMD_MSG_DATA: // チャットメッセージコマンドの処理 iRet = RecvMessagePacket(&Header, iTcpUdp); break; case CMD_SEND_FILE_START_RES: // ファイル送信コマンド/応答の処理 case CMD_SEND_FILE_END_RES: case CMD_SEND_FILE_ABORT_RES: iRet = RecvFileSendResPacket(&Header, iTcpUdp); break; case CMD_RECV_FILE_SIZE_RES: // ファイル受信コマンド/応答の処理 case CMD_RECV_FILE_ABORT_RES: case CMD_RECV_FILE_ING: case CMD_RECV_FILE_END_REQ: iRet = RecvFileRecvResPacket(&Header, iTcpUdp); break; case CTRL_TRANS_METHOD_RES: // ★データ送受信方法応答 if (iTcpUdp == 0) // TCPのみ iRet = RecvTransMethodResPacket(&Header); else iRet = -1; break; default: // 知らないコマンドなのでエラー iRet = -1; break; } L_END: return(iRet); }
各機能を実現するスレッドのバッファにデータを書き込む処理にTCP, UDPで切り替える
機能を追加します。
【SendRecvThread.cpp】 //============================================== // function // ★メッセージコマンドの受信 // parameter // HeaderRec *pHeader [in]仮読みしたヘッダ // int iTcpUdp [in]0:TCP 1:UDP // return // 0:パケットが揃っていない // 1:パケットを受信したので処理を行った // -1:エラーが発生した //============================================== int CSendRecvThread::RecvMessagePacket(HeaderRec *pHeader, int iTcpUdp) { int iRet = 0; int iDataSize, iSize, iPacketSize; LPBYTE pbPacket = NULL; CRingBuff *pCRingBuff = (iTcpUdp == 1) ? m_pCRingBuffRecvUDP : m_pCRingBuffRecv; iDataSize = ntohs(pHeader->wDataLen); iPacketSize = iDataSize + sizeof(HeaderRec); iSize = pCRingBuff->GetReadableSize(); if (iSize < iPacketSize) goto L_END; pbPacket = (LPBYTE)calloc(iPacketSize, sizeof(BYTE)); // パケット全体を仮読み pCRingBuff->ReadWithoutUpdateHeadPoint(pbPacket, iPacketSize); if (m_pCChatMsgThread != NULL) { // チャットメッセージ処理スレッドに渡せたら読み込んだことにする if (m_pCChatMsgThread->m_pCRingBuffCmd->Write(pbPacket, iPacketSize) == TRUE) { pCRingBuff->UpdateHeadPoint(iPacketSize); iRet = 1; } } else { // チャットメッセージ処理スレッドがいなければエラー切断 pCRingBuff->UpdateHeadPoint(iPacketSize); iRet = -1; } SAFE_FREE(pbPacket); L_END: return(iRet); } //============================================== // function // ★ファイル送信コマンド/応答(S→C)をファイル送信スレッドに渡す // parameter // HeaderRec *pHeader [in]仮読みしたヘッダ // int iTcpUdp [in]0:TCP 1:UDP // return // 0:パケットが揃っていない, ファイル受信側が満杯 // 1:パケットを受信したので処理を行った // -1:エラーが発生した //============================================== int CSendRecvThread::RecvFileSendResPacket(HeaderRec *pHeader, int iTcpUdp) { int iRet = 0; int iDataSize, iSize, iPacketSize; LPBYTE pbPacket = NULL; CRingBuff *pCRingBuff = (iTcpUdp == 1) ? m_pCRingBuffRecvUDP : m_pCRingBuffRecv; iDataSize = ntohs(pHeader->wDataLen); iPacketSize = iDataSize + sizeof(HeaderRec); iSize = pCRingBuff->GetReadableSize(); if (iSize < iPacketSize) goto L_END; pbPacket = (LPBYTE)calloc(iPacketSize, sizeof(BYTE)); // パケット全体を仮読み pCRingBuff->ReadWithoutUpdateHeadPoint(pbPacket, iPacketSize); // ファイル送信スレッドに渡せたら読み込んだことにする if (m_pCSendFileThread != NULL) { if (m_pCSendFileThread->m_pCRingBuffCmd->Write(pbPacket, iPacketSize) == TRUE) { pCRingBuff->UpdateHeadPoint(iPacketSize); iRet = 1; } } else pCRingBuff->UpdateHeadPoint(iPacketSize); // ファイル送信スレッドがないときは読み込んだことにする SAFE_FREE(pbPacket); L_END: return(iRet); } //============================================== // function // ★ファイル受信コマンド/応答(S→C)をファイル受信スレッドに渡す // parameter // HeaderRec *pHeader [in]仮読みしたヘッダ // int iTcpUdp [in]0:TCP 1:UDP // return // 0:パケットが揃っていない, ファイル受信側が満杯 // 1:パケットを受信したので処理を行った // -1:エラーが発生した //============================================== int CSendRecvThread::RecvFileRecvResPacket(HeaderRec *pHeader, int iTcpUdp) { int iRet = 0; int iDataSize, iSize, iPacketSize; LPBYTE pbPacket = NULL; CRingBuff *pCRingBuff = (iTcpUdp == 1) ? m_pCRingBuffRecvUDP : m_pCRingBuffRecv; iDataSize = ntohs(pHeader->wDataLen); iPacketSize = iDataSize + sizeof(HeaderRec); iSize = pCRingBuff->GetReadableSize(); if (iSize < iPacketSize) goto L_END; pbPacket = (LPBYTE)calloc(iPacketSize, sizeof(BYTE)); // パケット全体を仮読み pCRingBuff->ReadWithoutUpdateHeadPoint(pbPacket, iPacketSize); // ファイル送信スレッドに渡せたら読み込んだことにする if (m_pCRecvFileThread != NULL) { if (m_pCRecvFileThread->m_pCRingBuffCmd->Write(pbPacket, iPacketSize) == TRUE) { pCRingBuff->UpdateHeadPoint(iPacketSize); iRet = 1; } } else // ファイル受信スレッドがないのでエラー { iRet = -1; pCRingBuff->UpdateHeadPoint(iPacketSize); // ファイル送信スレッドがないときは読み込んだことにする } SAFE_FREE(pbPacket); L_END: return(iRet); }
データ転送方法応答の内容に従ってデータ転送方法応答を処理する関数を追加します。
・データ転送方法変数に応答で指定された値を設定
・応答がUDPの時:sendtoで使用するアドレスの作成(ポート番号の設定)
・応答がTCPの時:転送方法要求時に作成したUDPソケットの破棄
【SendRecvThread.cpp】 //============================================== // function // ★データ転送方法応答の受信処理 // parameter // HeaderRec *pHeader [in]仮読みしたヘッダ // return // 0:パケットが揃っていない, ファイル受信側が満杯 // 1:パケットを受信したので処理を行った // -1:エラーが発生した //============================================== int CSendRecvThread::RecvTransMethodResPacket(HeaderRec *pHeader) { int iRet = 0; int iDataSize, iSize, iPacketSize; LPBYTE pbPacket = NULL; TransMethodReqRec *pTransMethod; iDataSize = ntohs(pHeader->wDataLen); iPacketSize = iDataSize + sizeof(HeaderRec); iSize = m_pCRingBuffRecv->GetReadableSize(); if (iSize < iPacketSize) goto L_END; pbPacket = (LPBYTE)calloc(iPacketSize, sizeof(BYTE)); // パケット全体を読む m_pCRingBuffRecv->Read(pbPacket, iPacketSize); pTransMethod = (TransMethodReqRec *)pbPacket; m_bTarnsMethod = pTransMethod->TansMethodInfo.bMethod; Locate(12, 7, 1); fprintf(stderr, “TransMethod:%s”, (m_bTarnsMethod == 1) ? “UDP” : “TCP”); // UDP通信用に相手先sockaddrを作る if (m_bTarnsMethod == 1) // UDP m_bTarnsMethod = MakePeerUDPSockaddr(ntohs(pTransMethod->TansMethodInfo.wUDPSrcPort)); if(m_bTarnsMethod == 0) // TCP { DestroySocket(m_fdUDP); // UDPで要求していたら破棄 } iRet = 1; L_END: return(iRet); }
データ転送方法応答がUDPのときsendtoで使用するアドレス情報を作成する関数
を追加します。
アドレスはコンストラクタで設定済みなので、ここではポートを変更します。
【SendRecvThread.cpp】 //============================================== // function // ★UDP通信用に相手先sockaddrを作る // m_PeerAddrのポート番号を変更 // parameter // WORD wPeerPort [in]相手先ポート // return // TRUE/FALSE //============================================== BOOL CSendRecvThread::MakePeerUDPSockaddr(WORD wPeerPort) { BOOL fRet = FALSE; char szAddr[NI_MAXHOST], szPort[NI_MAXSERV] = {0}; addrinfo hints, *pres = NULL; // アドレスの取り出し getnameinfo((sockaddr *)&m_PeerAddr, m_iPeerAddrLen, szAddr, sizeof(szAddr), NULL, 0, NI_NUMERICHOST); DISABLE_C4996 sprintf(szPort, “%d”, wPeerPort); // ポート番号 ENABLE_C4996 memset(&hints, 0, sizeof(hints)); // アドレスとポート番号を指定 if (getaddrinfo(szAddr, szPort, &hints, &pres) == 0) { memcpy(&m_PeerAddr, pres->ai_addr, pres->ai_addrlen); m_iPeerAddrLen = (socklen_t)pres->ai_addrlen; freeaddrinfo(pres); fRet = TRUE; } return(fRet); }
送信可能時刻になったかを調べる関数にTCP, UDPの切り替え機能を追加します。
【SendRecvThread.cpp】 //============================================== // function // ★今送信可能時刻か // parameter // DWORD dwNow [in]現在時刻 // int iTcpUdp [in]0:TCP 1:UDP // return // TRUE/FALSE //============================================== BOOL CSendRecvThread::CanSendNow(DWORD dwNow, int iTcpUdp) { BOOL fRet = FALSE; DWORD dwPrevSentTime = (iTcpUdp == 1) ? m_dwPrevSentTimeUDP : m_dwPrevSentTime; DWORD dwSendInterval = (iTcpUdp == 1) ? m_dwSendIntervalUDP : m_dwSendInterval; if (GetdwInterval(dwNow, dwPrevSentTime) >= dwSendInterval) fRet = TRUE; return(fRet); }
データ転送方法要求パケットの送信関数を追加します。
UDPを指定するときはUDPソケットを作成し受信準備をします。
UDPを指定するときは要求パケットに送信元ポート番号をセットします。
TCPを指定するときは送信元ポート番号は不要ですので0をセットします。
転送方法の確定は、応答パケット受信で行います。
応答パケットでTCPが指定されたときは、作成済みUDPソケットは破棄されます。
これはコントロールなのでTCPで行います。
【SendRecvThread.cpp】 //============================================== // function // ★データ送受信方法のネゴシエーション // UDP指定時はUDPソケットを作成する // 確定は応答受信時 // parameter // BYTE bMethod [in]0:TCP 1:UDP // WORD wUDPSrcPort [in]UDP送信元ポート番号/0 // return // TRUE/FALSE //============================================== BOOL CSendRecvThread::SendTransMethodPacket(BYTE bMethod, WORD wUDPSrcPort) { TransMethodReqRec *pTransMethod = NULL; int iPacketSize = sizeof(TransMethodReqRec); // パケットサイズ BOOL fRet = FALSE; addrinfo hints, *pres = NULL; char szPort[NI_MAXSERV] = { 0 }, szAddr[NI_MAXHOST] = {0}; sockaddr_storage Addr; socklen_t iAddrLen; int on; if (m_bTarnsMethod == bMethod) // 変更がないときは何もしない { fRet = TRUE; goto L_END; } // UDPの時はUDPソケットを作る if (bMethod == 1) { DestroySocket(m_fdUDP); // UDPソケットを作成する if ((m_fdUDP = socket(m_PeerAddr.ss_family, SOCK_DGRAM, 0)) == INVALID_SOCKET) goto L_END; // bindする memset(&hints, 0, sizeof(hints)); hints.ai_family = m_PeerAddr.ss_family; hints.ai_socktype = SOCK_DGRAM; // UDP // すべてのインターフェースで受信可 hints.ai_flags = AI_PASSIVE; DISABLE_C4996 sprintf(szPort, “%d”, wUDPSrcPort); // ポート番号 ENABLE_C4996 if (getaddrinfo(NULL, szPort, &hints, &pres) != 0) goto L_END; #ifdef IPV6_V6ONLY // IPv6ソケットでIPv4射影アドレスを使用しないように設定 if (m_PeerAddr.ss_family == AF_INET6) { on = 1; if (setsockopt(m_fdUDP, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&on, sizeof(on)) < 0) { DispErrorMsg(“Err:setsockopt”); goto L_END; } } #endif if (bind(m_fdUDP, pres->ai_addr, (int)pres->ai_addrlen) == SOCKET_ERROR) { DispErrorMsg(“Err:bind”); goto L_END; } // wUDPSrcPortが0のときは、動的なポートが割り当てられるので調べる iAddrLen = sizeof(Addr); getsockname(m_fdUDP, (sockaddr *)&Addr, &iAddrLen); getnameinfo((sockaddr *)&Addr, iAddrLen, szAddr, sizeof(szAddr), szPort, sizeof(szPort), NI_NUMERICHOST | NI_NUMERICSERV); } pTransMethod = (TransMethodReqRec *)calloc(iPacketSize, sizeof(BYTE)); // パケット全体のエリアを確保 memcpy(pTransMethod->header.bMagicData, MAGIC_STRING, strlen(MAGIC_STRING)); pTransMethod->header.wCommand = htons(CTRL_TRANS_METHOD_REQ); pTransMethod->header.wDataLen = htons(sizeof(TransMethodInfoRec)); pTransMethod->TansMethodInfo.bMethod = bMethod; pTransMethod->TansMethodInfo.wUDPSrcPort = htons((WORD)atol(szPort)); // TCPの時は意味がないので0をセット // コントロールは強制的にTCPで送信 fRet = SetSendData((char *)pTransMethod, iPacketSize, TRUE); SAFE_FREE(pTransMethod) L_END: if(pres != NULL) freeaddrinfo(pres); if (fRet == FALSE) DestroySocket(m_fdUDP); return(fRet); }
これで完成です。
Linux版も同様の考え方で作成することができます。
これらのプログラムの完全なプロジェクトは以下のリンクから入手できます。
SimpleClientTransFile for Windows TCP/UDP対応
SimpleClientTransFile for Linux TCP/UDP対応
次回サーバ側を作成して動作を確認することにしましょう。