ネットワークを扱ってみよう等と思ったが、Winsockの日本語リファレンスはどこですか(・ω・`
複雑なのを作ると後で読めなくなる困るので、シンプルなのを一つ作ってみようという個人的メモ
サーバー クライアント ↓ ↓ WSAStartup WSAStartup ↓ ↓ socket socket ↓ | bind | ↓ | listen ↓ ↓ ←――――― connect accept ―――――→ | ↓ ↓ send/recv ←――――→ send/recv ↓ ↓ closesocket closesocket ↓ ↓ WSACleanup WSACleanupという感じになるようです。
さて、ここで面倒そうな点は、
accept,send,recv等はブロックされる(処理が完了するまで実行が停止する)ので、他のことが出来ない。
setsockopt等で非ブロックにすればすぐに戻ってくるわけですが、データが全部送れなかった時にどうするか・・・。
アプリケーションで再送処理なんてやってられません。
なのでこのあたりはスレッド立てて、バッファ用意して何とかすることにします。
スレッド立てたら立てたで、送信受信はいいんですが、接続、切断がが非同期に来て結構面倒です。
さらにサーバーで1コネクション1スレッド立ててたら体が持ちません。
調べると、WSAEventSelectというものがあるらしいです。これは、何かしらの事柄が起こった時に
Eventをシグナル状態にして知らせてくれるもので、WSAWaitForMultipleEventsを使用すれば
WSA_MAXIMUM_WAIT_EVENTSまで同時に待ち受けることが出来る。
WSA_MAXIMUM_WAIT_EVENTSはwinsock2.hに記述されていて、値はMAXIMUM_WAIT_OBJECTSでした。
MAXIMUM_WAIT_OBJECTSはwinnt.hに記述されていて値は64。
1スレッド64ソケット扱えるなら、小規模としては十分でしょうか。
そろそろクラス設計に入りますね。
ネットワークの概要は、
サーバーはクライアントからの接続を待ちうけ、
通信用にソケットを生成して通信する。
なので、このあたりをクラス化しておけばいいでしょう。
具体的には、CSocketは通信を担当し、CServerは接続を待ちうけCSocketを生成する。
CClientは、CSocketを継承して実装し接続機能を付加する。
ネットゲームで使うことを視野に入れて完全ポーリング型で実装する。
という形で行ってみましょう。
こうですか?わかりませんっ><
さてさて、解説を交えながら実際に組んでいきましょうか
まずは必要ヘッダとライブラリ
#include <winsock2.h> #pragma comment(lib,"ws2_32.lib")これは問題ありませんね。
//ソケットのインターフェイスですよう class ISock { protected: ISock(){}; virtual ~ISock(){}; public: //------------------------------------------------ //送受信用 //buffer :送受信バッファ //bufferlen :バッファ長さ //戻り値 :送受信に成功したバイト数 //備考 :この関数の戻り値は通常buffferlenと同じである。 // そうでなければエラー、またはバッファオーバーフロー、アンダーフローが発生している virtual int Send(const void* buffer,DWORD bufferlen)=0; virtual int Recv(void* buffer,DWORD bufferlen)=0; //------------------------------------------------ //接続が生きているかチェックする //戻り値 :FALSEを返した場合接続は無効になっている virtual BOOL IsSocketValid()=0; //読み込めるサイズを取得 virtual DWORD GetRecvableSize()=0; //接続情報を取得する virtual sockaddr_in* GetAddr()=0; };こいつはただのインターフェイスで、中身は何もありません(笑
//------------------------------------------------ // CSockクラス // これを使用してアプリケーションからの送受信を行う。 //------------------------------------------------ class CSock : public ISock; { friend class CServer; protected: SOCKET m_Sock; //ソケット sockaddr_in m_Addr; //接続情報 CRingBufferM m_CSendBuffer; CRingBufferM m_CRecvBuffer; BOOL m_bValid; CRITICAL_SECTION m_cs; public: CSock(); virtual ~CSock(); //内部関数 //実際の送受信 int _Send(); int _Recv(); //------------------------------------------------ //初期化 //sock :接続されたソケット //buffersize :送受信用バッファサイズ //addr :接続情報(オプション //戻り値 :成功すれば0 virtual int Init(SOCKET sock,DWORD buffersize,sockaddr_in* paddr=0); void Release(); virtual int Send(const void* buffer,DWORD bufferlen); virtual int Recv(void* buffer,DWORD bufferlen); virtual BOOL IsSocketValid(){return m_bValid;} //------------------------------------------------ //取得用 SOCKET GetSock(){return m_Sock;} DWORD GetRecvableSize(){return m_CRecvBuffer.GetReadableSize();} CRingBuffer* GetRecvBuffer(){return &m_CRecvBuffer;} CRingBuffer* GetSendBuffer(){return &m_CSendBuffer;} };そして、
CRingBufferM m_CSendBuffer; CRingBufferM m_CRecvBuffer;はリングバッファ用のクラスです
ret=send(m_Sock,(const char*)(pBuffer+*pHead),*pTail-*pHead,0); if(ret==SOCKET_ERROR){ if(WSAGetLastError()!=WSAEWOULDBLOCK){ m_bValid=FALSE; } }else{ :これくらいのもんです。
//クライアントクラス class CClient : public CSock { protected: char m_StrAddr[_MAX_PATH]; WSADATA m_WSAData; int m_bConnecting; //接続しているか(0:未接続、1接続要求中、2接続中、-1接続失敗 BOOL m_bThreadValid; //スレッドを停止するときにこのフラグを下げる DWORD m_dwThreadID; //スレッドID WSAEVENT m_Event; DWORD m_dwBufferSize; //バッファサイズ static DWORD CALLBACK _ThreadProc(void*); public: CClient(WORD ver=2,DWORD BufferSize=0); virtual ~CClient(); //サーバーへの接続を行う virtual HRESULT Connect(const char* addr,WORD Port); //サーバーからの接続を切断する virtual HRESULT Close(); //現在接続中かを調べる virtual int IsConnecting(); virtual DWORD ThreadProc(); //スレッド };これがクライアントの本体になります。 とは言っても、CSockクラスに接続機能がついただけのものですが。
HRESULT CClient::Connect(const char* addr,WORD Port) { if(m_bThreadValid)Close(); Init(0,m_dwBufferSize==0 ? STK_NETWORK_DEFAULT_BUFFER_SIZE : m_dwBufferSize,0); //バッファの初期化だけ行う CSock::m_Addr.sin_family=AF_INET; CSock::m_Addr.sin_port=htons(Port); memcpy(m_StrAddr,addr,lstrlen(addr)); m_bThreadValid=TRUE; m_bConnecting=1; CreateThread(0,0,_ThreadProc,(void*)this,0,&m_dwThreadID); return S_OK; }やっと説明っぽくなって参りました
CSock::m_Addr.sin_family=AF_INET; CSock::m_Addr.sin_port=htons(Port);sockaddr_in構造体のsin_familyにAF_INET。これはそうするものです。そうヘルプに書いてあります。 同構造体のsin_port=htons(Port);では、接続先ポートを指定しています。htons関数は
CreateThread(0,0,_ThreadProc,(void*)this,0,&m_dwThreadID);これはメンバ関数にコールバックする際の常套手段です。staticなメンバ関数_ThreadProcと、thisを渡しておくことで
DWORD CClient::ThreadProc() { //ソケット作成 CSock::m_Sock=socket(AF_INET,SOCK_STREAM,0); if(m_Sock==INVALID_SOCKET){ m_bConnecting=-1; return 1; } //アドレスの解決 CSock::m_Addr.sin_addr.S_un.S_addr = inet_addr(m_StrAddr); if(CSock::m_Addr.sin_addr.S_un.S_addr==INADDR_NONE){ //失敗したのでgethostbynameを試す hostent *phost; phost=gethostbyname(m_StrAddr); if(phost==NULL){ closesocket(m_Sock); m_bConnecting=-1; return 1; //失敗 } CopyMemory(&CSock::m_Addr.sin_addr.S_un.S_addr,phost->h_addr_list[0],4); } //接続 if(connect(m_Sock,(sockaddr*)&(CSock::m_Addr),sizeof(CSock::m_Addr))==SOCKET_ERROR){ closesocket(m_Sock); m_bConnecting=-1; return 1; //接続失敗 } //接続完了 m_Event=WSACreateEvent(); WSAEventSelect(m_Sock,m_Event,FD_READ | FD_WRITE | FD_CLOSE); m_bConnecting=2; while(m_bThreadValid){ DWORD ret=WSAWaitForMultipleEvents(1,&m_Event,FALSE,WSA_INFINITE,FALSE); if(ret==WSA_WAIT_FAILED){ m_bConnecting=-1; } WSANETWORKEVENTS event; WSAEnumNetworkEvents(m_Sock,m_Event,&event); if(event.lNetworkEvents & FD_READ){ CSock::_Recv(); } if(event.lNetworkEvents & FD_WRITE){ CSock::_Send(); } if(event.lNetworkEvents & FD_CLOSE){ break; } } m_bThreadValid=FALSE; m_bConnecting=0; CSock::m_bValid=FALSE; closesocket(m_Sock); m_Sock=INVALID_SOCKET; return 0; }初めに、socket関数でソケットを作成しています。TCP/IPなので、SOCK_STREAMで作っておきます
case RQ_STARTLISTEN: //待ち受け開始 { sockaddr_in addr; addr.sin_family=AF_INET; addr.sin_port=htons((WORD)rs.Value); addr.sin_addr.S_un.S_addr=INADDR_ANY; m_SockList[0]=socket(AF_INET,SOCK_STREAM,0); //書き換え if(m_SockList[0]==INVALID_SOCKET)break; BOOL Use=TRUE; setsockopt(m_SockList[0],SOL_SOCKET,SO_REUSEADDR,(const char*)&Use,sizeof(Use)); if(bind(m_SockList[0],(const sockaddr*)&addr,sizeof(addr))==SOCKET_ERROR){ break; } if(WSAEventSelect(m_SockList[0],m_EventList[0],FD_ACCEPT)==SOCKET_ERROR){ break; } if(listen(m_SockList[0],10)==SOCKET_ERROR){ break; } } break;これが、STARTLISTEN要求を処理する部分(スレッド側です
RequestProc(); //リクエストの処理 //接続は来てるかな? WSANETWORKEVENTS events; if(WSAEnumNetworkEvents(m_SockList[0],m_EventList[0],&events)==SOCKET_ERROR){ break; } if(events.lNetworkEvents & FD_ACCEPT){ sockaddr_in addr; int len=sizeof(addr); SOCKET sock=accept(m_SockList[0],(sockaddr*)&addr,&len); if(sock==SOCKET_ERROR){ CSock* lpCSock=new CSock; WSAEVENT event=WSACreateEvent(); WSAEventSelect(sock,event,FD_READ | FD_WRITE | FD_CLOSE); lpCSock->Init(sock,m_BufferSize,&addr); EnterCriticalSection(&m_cs); m_NewCSockList.push_back(lpCSock); m_dwNumConnections++; LeaveCriticalSection(&m_cs); m_CSockList.push_back(lpCSock); m_SockList.push_back(sock); m_EventList.push_back(event); } }リクエストの処理、あとFD_ACCEPTフラグを調べ、これが立っている場合は接続が来ているのでaccept関数を呼び出してやります。