非同期通信(サーバー側)

■WinSockによる非同期通信

WinSockAPIで非同期通信は「APIを使用」「スレッドを使用」のどちらでも実装可能ですが
今回はAPIを使用して非道通信を行う方法の紹介をします。

●WSAAsyncSelect関数
	WinSockの非同期通信で使用する関数は同期通信時に使用した関数を使用します。
	ただ、その関数を使用はプロシージャで行い、そのために必要な関数として
	WSAAsyncSelect関数を使用します。
		
	・関数詳細
		関数名:
			WSAAsyncSelect
				
		戻り値:
			成功:0
			失敗:SOCKET_ERROR(-1)

		引数:
			SOCKET:
				設定を行うソケット
			
			HWND:
				イベント通知を行うウィンドウのハンドル
				
			int:
				メッセージ通知で使用されるID
				WM_COMMANDやWM_CLOSEなどのメッセージIDの自前版
			
			long:
				メッセージで送られるイベント

			イベント一例:
				FD_READ:
					データ受信通知

				FD_ACCEPT:
					接続依頼通知

				FD_CONNECT:
					接続完了通知

				FD_CLOSE:
					接続切断通知

		内容:
			ソケット、イベント通知用ハンドル、通知イベントの種類の指定を行い、
			非同期処理を開始します。
			イベント通知はウィンドウハンドルのプロシージャに送信され、
			送られるイベントは指定した内容のみに限定されます。

		使用箇所:
			WSAAsyncSelect関数の使用箇所はソケット作成直後

			例:
				
				socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
				if (WSAAsyncSelect(m_ServerSocket, 
							whandle, 
							WM_ASYNC, 
							FD_ACCEPT))
				{
					closesocket(m_ServerSocket);
					return FALSE;
				}
				

	・WM_USER
		WSAAsyncSelectの第三引数で指定するメッセージIDは開発側で指定を行いますが、
		このIDは「WM_USER」に加算を行ったIDを使用します。
		「WM_USER」はWinAPIが用意している定数で、アプリケーション独自のメッセージを
		作成するために用意されています。
		使用方法は「#define 独自メッセージID (WM_USER + 値)」という形で行います。

	・イベント通知の内容
		非同期通信のイベントの通知はプロシージャに送信されますが、
		その際のプロシージャの各パラメータは以下のようになっています。

		プロシージャ関数(HWND dialog_handle, UINT message, WPARAM wp, LPARAM lp)
			・メッセージID
				WSAAsyncSelectの第三引数で指定したメッセージIDが送信されてきた場合
				「UINT message」に格納されています。

			・イベントID
				非同期通信のイベントIDは「LPARAM lp」に格納されています。
				LPARAMからIDを取り出すにはWSAGETSELECTEVENTマクロを使用します。

				例:
					WORD id = WSAGETSELECTEVENT(lp);

			・ソケット
				イベント通知を受けたソケットは「WPARAM wp」に格納されています。
				ソケットはイベントIDのようにマクロを通す必要がありませんので
				そのまま使用することができます。

				例:
					closesocket(wp);

■チャットアプリ(サーバー側)の処理の流れ

今回作成するチャットアプリの通信処理の流れは以下のような流れになります。

	①.WinScok開始

	②.ダイアログ作成

	③.ポート番号取得

	④.サーバー用ソケット作成

	⑤.非同期通信設定(イベントは通信許可依頼通知(FD_ACCEPT))

	⑥.ソケットとポート番号を結びつけ

	⑦.待機状態へ

	⑧.クライアントからの通信を許可

	⑨.通信許可で作成されたソケットを非同期設定
	  (イベントは終了通知(FD_CLOSE)と受信通知(FD_READ))

	⑩.データ受信

	⑪.全てのクライアントに受信した内容を送信する

	⑫.通信が切断されたクライアントソケットを閉じる

	⑬.全ての通信用ソケットを閉じる

	⑭.サーバーソケットを閉じる

	⑮.WinSock終了

■サンプルコード

●事前準備
	プロジェクト「ChatAppServer」を作成し、
	以下のファイルをダウンロードしてプロジェクトに追加して下さい。

	リソースヘッダ
	リソースファイル

●main.cpp

#include <crtdbg.h>
#include "NetWork.h"

LRESULT CALLBACK ServerProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE instance_handle, 
			HINSTANCE prev_instance_handle, 
			LPSTR cmd_line, 
			int cmd_show)
{
	// 通信開始
	NetWork::GetInstance().Run(instance_handle, MAKEWORD(1, 1));

	// メモリリーク検出
	_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

	return 0;
}

●NetWork.h

#ifndef NETWORK_H_
#define NETWORK_H_

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define WIN32_LEAN_AND_MEAN

#include <Windows.h>
#include <WinSock2.h>
#include <list>

#pragma comment(lib, "Ws2_32.lib")

class NetWork
{
public:
	/*
		インスタンス取得
			
			戻り値:
				NetWorkクラスのインスタンス
		
			引数:
				なし
	
			内容:
				NetWorkクラスのインスタンスのゲッター
	*/
	static NetWork &GetInstance()
	{
		static NetWork s_instance;

		return s_instance;
	}

	/*
		通信実行
			
			戻り値:
				正常終了:0以外
				異常終了:0

			引数:
				instance_handle:
					このアプリのインスタンスハンドル

				version:
					実行するWinSockのバージョン

			内容:
				WinSockによる通信を実行する
				この通信ではダイアログによるやり取りを行うので、
				通信が終了するまで呼び出し側に制御が戻らないので注意
	*/
	int Run(HINSTANCE instance_handle, WORD version = MAKEWORD(1, 1));

	/*
		ダイアログ用プロシージャ
		
			戻り値:
				メッセージを自分で処理した:TRUE
				メッセージを処理していない:FALSE

			引数:
				dialog_handle:
					メッセージを受信したダイアログのハンドル

				UINT:
					受信したメッセージ

				WPARAM:
					メッセージオプション1(メッセージによって異なる)

				LPARAM:
					メッセージオプション2(メッセージによって異なる)

			内容:
				ダイアログが受信したメッセージを処理するためのプロシージャ
				ダイアログの初期化、終了はここで行う
				非同期通信のイベント取得IDとしてWM_ASYNCを
				受信メッセージIDとして使用している
	*/
	static LRESULT CALLBACK ServerProc(HWND dialog_handle, 
						UINT message,
						WPARAM wp,
						LPARAM lp);

	/*
		受信テキストの取得
	
			戻り値:
				受信テキスト

			引数:
				なし

			内容:
				今まで受信してきたデータ(文字列)の取得
	*/
	std::string GetInfoText(void);

private:
	// シングルトンなのでコンストラクタはprivateで設定
	NetWork() :
		m_PortNo(0),
		m_ServerSocket(NULL),
		m_InfoText(""),
		m_ConnectSocketList(0)
	{}

	/*
		サーバー用ソケット作成
		
			戻り値:
				成功:true
				失敗:false

			引数:
				window_handle:
					非同期通信のイベントを処理するウィンドウハンドル

				port_no:
					サーバーに設定するポート番号

			内容:
				サーバー用のソケットを非同期モードで作成し、待機状態にする
	*/
	BOOL MakeServerSocket(HWND window_handle, int port_no);

	/*
		クライアントからの通信許可の受け入れ
			
			戻り値:
				受け入れ成功:true
				受け入れ失敗:false

			引数:
				window_handle:
					受け入れ後に作成される非同期通信ソケットの
					イベントを処理するウィンドウハンドル

			内容:
				クライアントからの通信の受け入れを行い、
				通信用ソケットリストに追加して管理する
	*/
	BOOL Accept(HWND dialog_handle);

	/*
		データ受信
		
			戻り値:
				受信成功:true
				受信失敗:false

			引数:
				socket:
					データ受信を行うソケット
					
			内容:
				クライアントが送信してきたデータの受信を行う
				受信できたデータはリストに追加する
				最後に全てのクライアントソケットに対して
				受信したデータの送信を行う
	*/
	BOOL Receive(SOCKET socket);

	/*
		ソケットを閉じる
			
			戻り値:
				なし
					
			引数:
				soket:
					閉じる対象となっているソケット

			内容:
				通信できなくなったソケットを閉じて、
				通信リストから外す
	*/
	void CloseSocket(SOCKET sock);

	/*
		通信終了
			
			戻り値:
				なし

			引数:
				なし

			内容:
				通信を終了するために通信用、サーバー用のソケットを閉じる
	*/
	void EndNetWork();

public:
	static const UINT WM_ASYNC = (WM_USER + 1);	// 非同期通信イベントID

private:
	const LPWSTR DialogName = TEXT("SERVER_DLG");	// ダイアログのリソース名

	WSADATA m_WsaData;				// WinSockets情報
	int m_PortNo;					// ポート番号
	SOCKET m_ServerSocket;				// サーバー用ソケット
	std::list<SOCKET> m_ConnectSocketList;	// 通信用ソケットリスト
	std::string m_InfoText;				// 受信内容表示用文字列
};

#endif

●NetWork.cpp

#include <windowsx.h>
#include "NetWork.h"
#include "resource.h"

/*
	通信実行
		
		戻り値:
			正常終了:0以外
			異常終了:0

		引数:
			instance_handle:
				このアプリのインスタンスハンドル

			version:
				実行するWinSockのバージョン

		内容:
			WinSockによる通信を実行する
			この通信ではダイアログによるやり取りを行うので、
			通信が終了するまで呼び出し側に制御が戻らないので注意
*/
int NetWork::Run(HINSTANCE instance_handle, WORD version)
{
	// ①.WinSock開始
	if (WSAStartup(version, &m_WsaData) != 0)
	{
		return -1;
	}

	// ②.ダイアログ作成
	int ret = DialogBox(instance_handle, 
				DialogName, 
				NULL, 
				(DLGPROC)&NetWork::ServerProc);

	// ⑮.WinSock終了
	WSACleanup();

	return ret;
}

/*
	サーバー用ソケット作成
		
		戻り値:
			成功:TRUE
			失敗:FALSE

		引数:
			window_handle:
				非同期通信のイベントを送信するウィンドウハンドル

			port_no:
				サーバーに設定するポート番号

		内容:
			サーバー用のソケットを非同期モードで作成し、待機状態にする
*/
BOOL NetWork::MakeServerSocket(HWND window_handle, int port_no)
{
	m_PortNo = port_no;
	// ④.サーバー用ソケット作成
	m_ServerSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	if (m_ServerSocket == INVALID_SOCKET)
	{
		return FALSE;
	}

	// ⑤.非同期通信設定(イベントは通信許可依頼通知(FD_ACCEPT))
	if (WSAAsyncSelect(m_ServerSocket, 
				window_handle, 
				NetWork::WM_ASYNC, 
				FD_ACCEPT))
	{
		closesocket(m_ServerSocket);
		return FALSE;
	}

	SOCKADDR_IN sock_addr;
	memset(&sock_addr, 0, sizeof(SOCKADDR_IN));
	sock_addr.sin_family = AF_INET;
	sock_addr.sin_port = htons(m_PortNo);
	sock_addr.sin_addr.s_addr = INADDR_ANY;

	// ⑥.ソケットとポート番号を結びつけ
	if (bind(m_ServerSocket, (SOCKADDR*)&sock_addr, sizeof(SOCKADDR_IN)) ==
									SOCKET_ERROR)
	{
		closesocket(m_ServerSocket);
		return FALSE;
	}

	// ⑦.待機状態へ
	if (listen(m_ServerSocket, 0) == SOCKET_ERROR)
	{
		closesocket(m_ServerSocket);
		return FALSE;
	}

	return TRUE;
}

/*
	クライアントからの通信の受け入れ

		戻り値:
			受け入れ成功:true
			受け入れ失敗:false

		引数:
			window_handle:
				受け入れ後に作成される非同期通信ソケットの
				イベントを処理するウィンドウハンドル

		内容:
			クライアントからの通信の受け入れを行い、
			通信用ソケットリストに追加して管理する
*/
BOOL NetWork::Accept(HWND window_handle)
{
	SOCKET socket;
	SOCKADDR sock_addr;
	int len = sizeof(SOCKADDR);

	memset(&sock_addr, 0, sizeof(SOCKADDR));

	// ⑧.クライアントからの通信を許可
	socket = accept(m_ServerSocket, &sock_addr, &len);
	if (socket == INVALID_SOCKET)
	{
		MessageBox(window_handle, 
				TEXT("通信許可をしませんでした"), 
				TEXT("エラー"), 
				MB_OK);
		return FALSE;
	}

	// ⑨.通信許可で作成されたソケットを
	//     非同期設定(使用イベントは終了通知(FD_CLOSE)と受信通知(FD_READ))
	int ret = WSAAsyncSelect(socket, window_handle, WM_ASYNC, FD_CLOSE | FD_READ);
	if (ret != 0)
	{
		closesocket(socket);
		MessageBox(NULL, 	
				TEXT("通信用ソケットの非同期化を失敗しました"), 
				TEXT("エラー"), 
				MB_OK);
		return FALSE;
	}

	// ソケットリストに追加
	m_ConnectSocketList.push_back(socket);

	return TRUE;
}

/*
	データ受信
		
		戻り値:
			受信成功:true
			受信失敗:false

		引数:
			socket:
				データ受信を行うソケット

		内容:
			クライアントが送信してきたデータの受信を行う
			受信できたデータはリストに追加する
			最後に全てのクライアントソケットに対して
			受信したデータの送信を行う
*/
BOOL NetWork::Receive(SOCKET socket)
{
	char buff[1024];
	memset(buff, 0, sizeof(char)* 1024);

	// ⑩.データ受信
	int ret = recv(socket, buff, 1024, 0);
	if (ret == SOCKET_ERROR)
	{
		MessageBox(NULL, 
			TEXT("データの受信に失敗しました"), 
			TEXT("受信エラー"), 
			MB_OK);
		return FALSE;
	}

	strcat_s(buff, "\r\n");
	std::string str = buff;
	m_InfoText += buff;

	// 全てのクライアントに受信した内容を送信する
	for (auto itr = m_ConnectSocketList.begin(); itr != m_ConnectSocketList.end(); ++itr)
	{
		send(*itr, buff, strlen(buff), 0);
	}

	return TRUE;
}

/*
	ソケットを閉じる
			
		戻り値:
			なし

		引数:
			soket:
				閉じる対象となっているソケット

		内容:
			通信できなくなったソケットを閉じて、
			通信リストから外す
*/
void NetWork::CloseSocket(SOCKET sokcet)
{
	for (auto itr = m_ConnectSocketList.begin(); itr != m_ConnectSocketList.end(); ++itr)
	{
		if (*itr == sokcet)
		{
			// ⑪.ソケットを閉じる
			shutdown(sokcet, SD_BOTH);
			closesocket(sokcet);
			// リストから削除する
			m_ConnectSocketList.erase(itr);
			break;
		}
	}
}

/*
	通信終了
			
		戻り値:
			なし

		引数:
			なし

		内容:
			通信を終了するために通信用、サーバー用のソケットを閉じる
*/
void NetWork::EndNetWork()
{
	// ⑫.全ての通信用ソケットを閉じる
	if (m_ConnectSocketList.size() > 0)
	{
		for (auto itr = m_ConnectSocketList.begin(); itr != m_ConnectSocketList.end(); ++itr)
		{
			shutdown(*itr, SD_BOTH);
			closesocket(*itr);
		}
	}

	// ソケットリストをクリア
	m_ConnectSocketList.clear();

	// サーバー用ソケットを閉じる
	if (m_ServerSocket != NULL && m_ServerSocket != INVALID_SOCKET)
	{
		// ⑬.サーバーソケットを閉じる
		closesocket(m_ServerSocket);
	}
}

/*
	受信テキストの取得
			
		戻り値:
			受信テキスト

		引数:
			なし

		内容:
			今まで受信してきたデータ(文字列)の取得
*/
std::string NetWork::GetInfoText(void)
{
	return m_InfoText;
}

/*
	ダイアログ用プロシージャ
			
		戻り値:
			メッセージを自分で処理した:TRUE
			メッセージを処理していない:FALSE

		引数:
			dialog_handle:
				メッセージを受信したダイアログのハンドル

			UINT:
				受信したメッセージ

			WPARAM:
				メッセージオプション1(メッセージによって異なる)

			LPARAM:
				メッセージオプション2(メッセージによって異なる)

		内容:
			ダイアログが受信したメッセージを処理するためのプロシージャ
			ダイアログの初期化、終了はここで行う
			非同期通信のイベント取得IDとしてWM_ASYNCを
			受信メッセージIDとして使用している
*/
LRESULT CALLBACK NetWork::ServerProc(HWND dialog_handle, 
					UINT message, 
					WPARAM wp, 
					LPARAM lp)
{
	HWND item_handle;
	BOOL is_success = false;
	int tmp_port_no = 0;

	switch (message)
	{
	// ダイアログの初期化
	case WM_INITDIALOG:
		return false;
		break;
	// 「×」ボタンクリック
	case WM_CLOSE:
	case IDC_ENDBTN:
		NetWork::GetInstance().EndNetWork();
		// ダイアログ終了
		EndDialog(dialog_handle, 1);
		break;
	// イベントメッセージ
	case WM_COMMAND:
		switch (LOWORD(wp))
		{
		// 開始ボタン
		case IDC_STARTBTN:
			// ③.ポート番号取得
			tmp_port_no = GetDlgItemInt(dialog_handle, 
							IDC_PORT, 
							&is_success, 
							true);
		
			if (is_success == TRUE)
			{
				bool is_make = NetWork::GetInstance().MakeServerSocket(dialog_handle, tmp_port_no);
				if (is_make == FALSE)
				{
					MessageBox(NULL, 
						TEXT("ソケットの作成に失敗しました。"), 
						TEXT("ソケット作成エラー"), 
						MB_OK);
				} else {
					item_handle = GetDlgItem(dialog_handle, IDC_STARTBTN);
					if (item_handle != NULL)
					{
						EnableWindow(item_handle, FALSE);
						MessageBox(NULL, 
							TEXT("ソケットを作成しました"), 
							TEXT("ソケット作成成功"), 
							MB_OK);
					}
				}
			} else {
				MessageBox(NULL, 
					TEXT("数値以外が入力されています"),
					TEXT("入力エラー"), 
					MB_OK);
			}
			break;
		}
		break;
	// 非同期通信イベント
	case NetWork::WM_ASYNC:
		switch (WSAGETSELECTEVENT(lp))
		{
		// 通信許可
		case FD_ACCEPT:
			if (NetWork::GetInstance().Accept(dialog_handle) == FALSE)
			{
				NetWork::GetInstance().EndNetWork();
			}
			break;
		// 受信
		case FD_READ:
			if (NetWork::GetInstance().Receive(wp) == TRUE)
			{
				HWND info = GetDlgItem(dialog_handle, IDC_INFO);
				if (info != NULL)
				{
					// 表示内容の取得
					std::string all_str = NetWork::GetInstance().GetInfoText();

					// 表示内容の更新
					SetDlgItemTextA(dialog_handle, IDC_INFO, all_str.c_str());

					// 表示位置の更新
					SendMessage(GetDlgItem(dialog_handle, IDC_INFO), 
							EM_LINESCROLL, 
							0, 
							Edit_GetLineCount(info));
				}
			}
			break;
		// 終了
		case FD_CLOSE:
			NetWork::GetInstance().CloseSocket(wp);
			break;
			default:
			return FALSE;
			break;
		}
		break;
	default:
		return FALSE;
	}

	return TRUE;
}