前半に作成したSimpleClientMEcho(Windows版、Linux版)とSimpleServerMChat
(Windows版, Linux版)間にファイル送受信機能を追加することにしましょう。
送受信されるデータが何を意味するか(チャットなのか、ファイル送信なのか、
ファイル受信なのか)、データの区切りはどこなのかをエンドポイント間で正しく
認識していなければなりません。
この約束(規約)をアプリケーションプロトコルといいます。
TCPは単にバイトストリームを送受信するプロトコルですので、これらは当然
アプリケーションで実装しなければなりません
例えば、メールはRFC5322でヘッダ部とボディ部の区切りは空行(CRLF)によって分離されます。
今回の拡張では、ファイル送信のようなバイナリデータを扱いますので単純に区切りを
空行にすることができません。
そこで、今回は、次のようなヘッダ部とデータ部で構成されるパケットとして扱う
ことにし、処理を簡単にするために共通ヘッダ部は固定長にします。
共通ヘッダ部
識別子(36Bytes):パケットがこのアプリケーショのものかどうか識別するためのデータ
コマンド(2Bytes):WORD値のデータ種別をネットワークオーダーで格納します。
データ長(2Bytes):WORD値のデータ長(共通ヘッダを含まない)をネットワークオーダー
で格納します。
データ部にはバイナリデータ(可変長)をそのまま格納します。
プログラムでこのパケット構造を実現するには次のヘッダファイルを使います。
MAGIC_STRINGが識別子で、ここではGUIDを使ってみました。
構造体をネットワークに流す場合、データ境界(パッキングアライメント)の考え方が
エンドポイントによって異なる可能性があるので、#pragma pack(push ,1)を使って
強制的に境界を1バイトにします。
MsgDataRecは共通ヘッダにデータ(チャット文字列)をくっつけたものです。
【define.h】 #pragma once #include “stdThread.h” #define MAGIC_DATA_LEN 36 #define CMD_MSG_DATA 100 #define MAGIC_STRING “E2E6F7BF-42B1-4382-AF0B-1F452ED13EB6” #pragma pack(push ,1) // パッキングアライメントを1に //============== // 共通ヘッダ部 //============== typedef struct { BYTE bMagicData[MAGIC_DATA_LEN]; // 不正アクセス防止用(GUIDを使います) WORD wCommand; // ネットワークオーダー WORD wDataLen; // ネットワークオーダー } HeaderRec; //=============================================== // メッセージ送信用構造体 共通ヘッダ部 + データ部 //=============================================== typedef struct { HeaderRec header; BYTE bMsgData[1]; // nullターミネイトを含まないデータ } MsgDataRec; #pragma pack(pop) // パッキングアライメントを戻す
プロジェクトの準備
SimpleClientTransFile(Windows), SimpleServerTransFile(Linux)を作ります。
SimpleClientMEcho(Windows)をコピーし、SimpleClientTransFileに名前変更
プロジェクト名をSimpleClientTransFileに変更します。
SimpleServerPollMChat(Linux)をコピーし、SimpleServerTransFileに名前変更
プロジェクト名をSimpleServerTransFileに変更
makefileのPROGRAMをSimpleServerTransFileに変更します。
define.hをプロジェクトに追加してください。
SimpleClient.cpp, SimpleServer.cppに#include “define.h” を追加します。
makefileの依存関係include部にdefine.hを追加します。
【makefile】 CC=g++ -g3 -O0 #CC=g++ PROGRAM=SimpleServerTransFile OBJS=SimpleServer.o stdThread.o ThreadJob.o SendRecvThread.o MySyncObject.o SRCS=$(OBJS:%.o=%.cpp) INCLUDE=stdThread.h define.h LFLAGS=-lpthread $(PROGRAM):$(OBJS) $(SRCS) $(INCLUDE) $(CC) -o $(PROGRAM) $(SRCS) $(LFLAGS)
先ずは、SimpleClientTransFile(Windows)の送信部をパケット対応に
SimpleServerTransFile(Linux)の受信部をパケット対応に変更します。
下図のような感じになります。
SimpleClientTransFile(Windows)の送信部のパケット対応
メッセージ文字列をそのまま送信していた部分を次のようなパケット化して送信する
関数の呼び出しに置き換えます。
パケット化して送信する関数 【SimpleClient.cpp】 //============================================== // function // メッセージパケットの送信 // parameter // char *pcData [in]UTF-8データ(NULLターミネートなし) // int iSize [in]データサイズ // return // なし //============================================== BOOL 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 = m_pCSendRecvThread-<SetSendData((char *)pMsgData, iPacketSize); SAFE_FREE(pMsgData) return(fRet); }
関数を呼び出すためにプロトタイプ宣言を追加します。
【SimpleClient.cpp】 // 関数の宣言 BOOL InitSocketLib(); // WinSockDLLの初期化 … BOOL EnableESC(FILE *stream); // ESCシーケンス画面制御を有効にする BOOL SendMessagePacket(char *pcData, int iSize);//★ メッセージパケットの送信
メッセージ文字列送信を関数呼び出しに変更します。
int main(int argc, char *argv[]) { char szKeyInBuff[81]; int iSize; LPBYTE pbDest = NULL; EnableESC(stderr); m_pCMySyncObject = new CMySyncObject(); m_pCMySyncObject->Initialize(); // 起動パラメータチェック if (argc != 3) { fprintf(stderr, “Usage: %s <ServerAddress> <ServerPort>\n”, argv[0]); goto L_END; } // ソケットライブラリの初期化 if (InitSocketLib() == FALSE) goto L_END; // ソケットの作成と接続処理 // 送受信スレッドを開始する if (CreateAndConnectSocket(argv[1], (WORD)atol(argv[2])) == FALSE) goto L_END; // メニューの表示 DispMenu(); while (1) { 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; ConvSJistoUtf8((LPBYTE)szKeyInBuff, NULL, &iSize); pbDest = (LPBYTE)calloc(iSize, sizeof(BYTE)); ConvSJistoUtf8((LPBYTE)szKeyInBuff, pbDest, &iSize); // 送信は送信データをセットするだけ、実際の送信はCSendRecvThreadで実施 //========================================== // ★メッセージパケットの送信 SendMessagePacket((char *)pbDest, iSize); //========================================== SAFE_FREE(pbDest) // メニューの表示 DispMenu(); break; } } L_END: // 切断とすべてのソケットの破棄 Stop(); // ソケットライブラリの開放 UninitSocketLib(); m_pCMySyncObject->Uninitialize(); SAFE_DELETE(m_pCMySyncObject) return(0); }
これで、SimpleClientTransFile(Windows)でチャットメッセージをパケット化して
送信する機能の実装は完了です。
SimpleServerTransFile(Linux)の受信部のパケット対応
データ受信後、受信データの解析(正しいデータか判断後、コマンドごとの処理の
呼び出しを行う)関数AnalyzeDataRecvを呼び出すようにします。
AnalyzeDataRecvは、コマンドがチャット文字列受信ならメッセージコマンド処理
関数RecvMessagePacketを呼び出し、ここでパケットからメッセージデータを取り出す。
取り出したメッセージを表示、他のクライアントに配信するためにSetSendDataに渡す。
(*)SetSendDataへ渡す部分は、まだパケット化していません。
SendRecvThread.cppの追加・修正箇所は次の通りです。
【SendRecvThread.cpp】 //============================================== // function // ★修正 機能を記述した関数 // parameter // なし // return // 0:正常 -1:エラー発生 //============================================== UINT CSendRecvThread::DoWork() { BOOL fRet = TRUE; char szRecvBuffer[RCVBUFSIZE + 1]; // 受信バッファ int iRecvSize; pollfd fds[1] = { 0 }; char *pcData = NULL; // 未送信データ int iSendSize = 0; // 未送信データサイズ fprintf(stderr, “DoWork()\n”); fds[0].fd = m_pConInfo->fdClient; while (!m_fStopFlag) { // 未送信のデータがなければ送信したいデータがあるか調べる if (iSendSize == 0) iSendSize = GetSendData(&pcData); fds[0].events = POLLIN | POLLRDHUP; // 受信と相手側からの切断イベントを設定 if (iSendSize > 0) fds[0].events |= POLLOUT; // 書き込み可能を追加 poll(fds, 1, 10); if (fds[0].revents & POLLRDHUP) // 相手側からの切断 { fprintf(stderr, “Disconnected pollrdhup\n”); fRet = FALSE; break; } if (fds[0].revents & POLLERR) // エラー発生 { m_pConInfo->pCMySyncObject->Lock(); DispErrorMsg(“Err:DoWork”); m_pConInfo->pCMySyncObject->UnLock(); fRet = FALSE; break; } if (fds[0].revents & POLLIN) // 受信データイベント { memset(szRecvBuffer, 0, sizeof(szRecvBuffer)); if ((iRecvSize = recv(m_pConInfo->fdClient, szRecvBuffer, RCVBUFSIZE, 0)) <= 0) { m_pConInfo->pCMySyncObject->Lock(); if (iRecvSize == 0) DispErrorMsg(“Disconnected recv”); else DispErrorMsg(“Err:recv”); m_pConInfo->pCMySyncObject->UnLock(); fRet = FALSE; break; } else { //========================================================= // ★受信データを解析関数に渡す if (AnalyzeDataRecv((BYTE *)szRecvBuffer, iRecvSize) == FALSE) { m_pConInfo->pCMySyncObject->Lock(); DispErrorMsg(“Err:Packet format”); m_pConInfo->pCMySyncObject->UnLock(); fRet = FALSE; break; } //========================================================= } } if (fds[0].revents & POLLOUT) // 送信可能ならsend実施 { if (send(m_pConInfo->fdClient, pcData, iSendSize, 0) != iSendSize) { DispErrorMsg(“Err:send”); fRet = FALSE; break; } SAFE_FREE(pcData) // 未送信データなしにセット iSendSize = 0; } } m_pConInfo->pCMySyncObject->Lock(); m_fIamZombie = TRUE; m_pConInfo->pCMySyncObject->UnLock(); SAFE_FREE(pcData) // 未送信データなしにセット iSendSize = 0; return((fRet == TRUE) ? 0 : -1); } //============================================== // function // ★新規追加 受信データの解析 // parameter // BYTE *pbData [in]受信データ // int iSize [in]受信データサイズ // retun // TRUE/FALSE //============================================== BOOL CSendRecvThread::AnalyzeDataRecv(BYTE *pbData, int iSize) { BOOL fRet = FALSE; HeaderRec *pHeader; WORD wCmd; if (iSize < sizeof(HeaderRec)) // ヘッダサイズに満たないときはエラー goto L_END; pHeader = (HeaderRec *)pbData; // ヘッダ部の解析 // 識別子が一致しないときはエラー if (memcmp(pHeader->bMagicData, MAGIC_STRING, strlen(MAGIC_STRING)) != 0) goto L_END; // コマンドを取り出し処理の分岐を行う wCmd = ntohs(pHeader->wCommand); fprintf(stderr, “CMD:%d\n”, wCmd); switch (wCmd) { case CMD_MSG_DATA: fRet = RecvMessagePacket(pbData, iSize); break; default: // 知らないコマンドはエラー break; } L_END: return(fRet); } //============================================== // function // ★新規追加 メッセージコマンドの受信 // parameter // char *pcData [in]メッセージコマンドパケット // int iSize [in]データサイズ // retun // なし //============================================== BOOL CSendRecvThread::RecvMessagePacket(BYTE *pbData, int iSize) { BOOL fRet = FALSE; HeaderRec *pHeader; MsgDataRec *pMsgData; int iMsgSize; char *pszMsg = NULL; pHeader = (HeaderRec *)pbData; iMsgSize = ntohs(pHeader->wDataLen); // データが足りないときはエラー if (iSize < iMsgSize + sizeof(HeaderRec)) goto L_END; // メッセージデータの取り出し pMsgData = (MsgDataRec *)pbData; // NULLターミネート分を追加して確保 pszMsg = (char *)calloc(iMsgSize + 1, sizeof(char)); memcpy(pszMsg, pMsgData->bMsgData, iMsgSize); m_pConInfo->pCMySyncObject->Lock(); fprintf(stderr, “Msg:recv %s\n”, pszMsg); m_pConInfo->pCMySyncObject->UnLock(); // クライアントに分配するためにデータをセット // まだパケット化していない SetSendData(pszMsg, iMsgSize); SAFE_FREE(pszMsg) fRet = TRUE; L_END: return(fRet); }
SendRecvThread.hに新規追加した関数の宣言を記述すれば、完了です。
【SendRecvThread.h】 #pragma once #include “ThreadJob.h” #include “define.h” // ★ class CMySyncObject; // このクラスの使用することを宣言 typedef struct { SOCKET fdClient; // 接続済みソケット(acceptの結果) 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); // 送信データの設定 int GetRecvData(char **ppcData); // 受信データの取得 BOOL IsZombie(); // このスレッドはゾンビ状態か private: ConnectionInfoRec *m_pConInfo; // コンストラクタで渡されるパラメータを格納 // このスレッド実行中領域が確保されていること BOOL m_fIamZombie; // ゾンビ状態かどうかを保持 char *m_pSendData; // 送信データ格納用エリア int m_iSendDataSize; // 送信データ格納用エリアのデータサイズ char *m_pRecvData; // 受信データ格納用エリア int m_iRecvDataSize; // 受信データ格納用エリアのデータサイズ int GetSendData(char **ppcData); // 送信データの取得 BOOL SetRecvData(char *pcData, int iSize); // 受信データの設定 BOOL AnalyzeDataRecv(BYTE *pbData, int iSize); // ★受信データの解析 BOOL RecvMessagePacket(BYTE *pbData, int iSize); // ★メッセージコマンドの受信 };
SimpleServerTransFile(Linux)の送信部分のパケット化、
SimpleClientTransFile(Windows)の受信部分のパケット化を行い、下図のような
動作にするのですが、これについては、次回に行うことにしましょう。