Winsock2@小規模クライアント/サーバ用クラス


ネットワークを扱ってみよう等と思ったが、Winsockの日本語リファレンスはどこですか(・ω・`
複雑なのを作ると後で読めなくなる困るので、シンプルなのを一つ作ってみようという個人的メモ


8/7 リファレンスを別ページに移動

お品書き。


流れ

         サーバー                 クライアント
           ↓                        ↓
       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")
これは問題ありませんね。

まずは送受信用インターフェイスを設計しておきます。

ISockインターフェイス

//ソケットのインターフェイスですよう
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;
};
こいつはただのインターフェイスで、中身は何もありません(笑
コンストラクタ、デストラクタ共にprotectedになっているので、継承しない限り生成も解体も出来ません。
無茶苦茶やってる気がしなくもないですが、気にしない気にしない。
そして、これを継承してCSockクラス
こいつで中身を実装、このクラス一つが接続一つに該当します。
送受信バッファは効率を考えるとやっぱりリングバッファでしょうか。

CSockクラス

//------------------------------------------------
//  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;
はリングバッファ用のクラスです
CRingBufferMはこれのメンバをCriticalSectionで守ったマルチスレッドVerです。
さて、CSockは、Init関数で接続されたソケットを受け取るので送受信処理くらいしかありません。
Send/Recv関数でデータをリングバッファに押し込み、_Send/_Recv関数で実際に送出します。
_Send/_Recv関数はリングバッファを直に操作しているのでごちゃごちゃしていますが、内容は

CSock::_Send関数

	ret=send(m_Sock,(const char*)(pBuffer+*pHead),*pTail-*pHead,0);
	if(ret==SOCKET_ERROR){
		if(WSAGetLastError()!=WSAEWOULDBLOCK){
			m_bValid=FALSE;
		}
	}else{
	  :
	
これくらいのもんです。
送信してみて、失敗したらWSAGetLastErrorを呼び出しエラーを調べます。
それがもしWSAWOULDBLOCKだった場合、今は送信できないということなので保留、 そうでなければ通信が途絶えたと判断して有効フラグを下げています。
これは_Sendのコードなのですが、Recvも似たようなものです。

そしてさらにこれを継承してCClient

CClientクラス

//クライアントクラス
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クラスに接続機能がついただけのものですが。

では接続部分について説明しておきます。

CClient::Connect関数

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::Initを呼び出しているのですが、これには本来接続されたソケットを渡します。
しかしこの地点ではまだ接続されていないので、バッファの初期化に必要な値だけを渡しています。 こうすることで、送受信は出来ないものの、Send/Recvでバッファに詰め込むことだけはできます。
オブジェクト指向というには怪しいですが気にしない気にしない。
CSock::m_Sockについては接続完了したら書き換えることにします(゚∀゚;
	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関数は
"ホストバイトオーダーからTCP/IPネットワークバイトオーダーに変換する"
なんて書いておりますが、単にビッグエンディアンに変換するだけであります。ネットワークではビッグエンディアンが基本のようです。 他はアドレスを適当に保存して(これの解決には時間かかるので・・・)あとはスレッドを立ててそちらへまわします。
	CreateThread(0,0,_ThreadProc,(void*)this,0,&m_dwThreadID);
	
これはメンバ関数にコールバックする際の常套手段です。staticなメンバ関数_ThreadProcと、thisを渡しておくことで
this->ThreadProc();
という感じでメンバ関数にコールバック出来ます。
CreateThreadで作成したスレッド内でCランタイム関数を呼び出すと小さなメモリリークが発生するので注意が必要です

そして処理はスレッドへ移り、時代は幕末へと移り変わっていきます。

CClient::ThreadProc関数

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で作っておきます
次に、保存しておいたアドレス文字列をホスト名に解決します。 まずは、inet_addr関数で"127.0.0.1"だとかの形だと仮定して呼び出してみます。 失敗したら、"hogehoge.com"のような形だと思われるので、gethostbyname関数で解決します。
あとは、connectにこれらを渡して呼び出してやれば接続できるはずです。

接続が完了したら、WSACreateEventでイベントを作ります。普通のイベントと似たようなものです。
そしてWSAEventSelectで捕まえるネットワークイベントを設定。ここでソケットにイベントが関連付けられ、ソケットは非ブロック型になります。
あとはWSAWaitForMultipleEvents関数でイベントが発生するのを待つだけです。
発生したらWSAEnumNetworkEvents関数で発生したイベントを調べ、立っているフラグによって処理。
これをFD_CLOSEが来るか、m_bThreadValidが偽になるまで続けます。
クライアントは単純なもんです。

問題はサーバーなのですが、こいつは説明するにはかなり難儀です。
スレッドはコンストラクタで立て、メインスレッドからはリクエストという形でキューに要求を積んで行き、 ワーカースレッドで処理します。

CServer::RequestProc関数

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要求を処理する部分(スレッド側です
m_SockList(std::vectorです)と、m_EventList[0]をListen用に固定で使用します。 ちなみに0番目のイベントは、メインスレッドからリクエストが来たときにもシグナル状態になります(ケチりました)。
listenの時に指定するポートは、自分の待ちうけ用ポートです。
あとは、WinSockの仕様で、一定条件が絡むと暫くポートが使えないことを回避するため setsockoptで使用中ポートにbind出来るようにしておきます。
次にbind、ここでソケットとポートを結び付けます。
あとはWSAEventSelectを呼び出しFD_ACCEPTイベントを引っ掛けて、listenで待ち受けるだけです。
そして、例によってWSAWaitForMultipleEventsで待ち、0番目のイベントがシグナルになった場合は

CServer::ThreadProc関数

	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関数を呼び出してやります。
これで相手との通信用ソケットが帰ってくるので、新しいEventを作成し、WSAEventSelectCSockで関連付けて、 CSockをnewして新しいソケットを設定してやります。そしてCSock*を新規接続リストに追加して、 残りのEventやらSOCKETもvector等に追加して管理してやれば普通に回っていきます。


使い方

クライアント側

今日はココまで。