非同期通信(クライアント側)

■チャットアプリ(クライアント側)の処理の流れ

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

	①.WinScok開始

	②.ダイアログ作成

	③.ポート番号、URL(IP)の取得

	④.クライアントソケット作成

	⑤.非同期通信設定
	  (イベントは接続完了(FD_CONNECT)、受信(FD_READ)、通信切断(FD_CLOSE))

	⑥.URLからサーバーの情報を取得する
	  成功 => ⑧
	  失敗 => ⑦

	⑦.IPからサーバーの情報を取得

	⑧.サーバーへ接続依頼

	⑨.データ送信

	⑩.データ受信

	⑪.通信切断で通信を終わる

	⑫.クライアントソケットを閉じる

	⑬.WinSock終了

■サーバーからの情報取得

同期通信ではサーバーからの情報取得は「gethostbyname」
または「gethostbyaddr」を使用していましたが、
これらの関数は同期関数なので、通信結果がでるまで処理が先に進みません。
なので、非同期通信では代わりの関数として「WSAAsyncGetHostByName」
「WSAAsyncGetHostByAddr」関数を使用します。

●WSAAsyncGetHostByName
	戻り値:
		HANDLE:
			識別用ハンドル
	
	引数:
		HALDLE:
			プロシージャを送信するウィンドウハンドル
	
		u_int:
			プロシージャに送信するメッセージID
	
		const char *:
			URL
		
		char*:
			サーバー情報格納用のバッファ
		
		int:
			サーバー情報格納用バッファのサイズ
		
	内容:
		引数で指定したURLからサーバー情報を取得します。
		通信の結果は指定したウィンドウハンドルとメッセージIDをプロシージャに送信されます。
		戻り値で返されるハンドルは通信をキャンセルする際に使用するためのハンドルです。

●WSAAsyncGetHostByAddr
	戻り値:
		HANDLE:
			識別用ハンドル
	
	引数:
		HALDLE:
			プロシージャを送信するウィンドウハンドル
		
		u_int:
			プロシージャに送信するメッセージID
		
		const char *:
			IPアドレス
	
		int:
			アドレスのサイズ(IP4の場合は4バイト)
		
		int:
			通信方法の種類(IP4はAF_INET)
		
		char*:
			サーバー情報格納用のバッファ
		
		int:
			サーバー情報格納用バッファのサイズ
		
	内容:
		引数で指定したIPアドレスからサーバー情報を取得します。
		WSAAsyncGetHostByNameと同じように引数で指定したウィドウハンドルの
		メッセージIDにサーバー情報の取得結果が送られてきます。
		WSAAsyncGetHostByNameとは異なりIPアドレスの種類と通信方法の指定を行う必要があります。
		戻り値で返されるハンドルは通信をキャンセルする際に使用するためのハンドルです。

■サンプルコード

●事前準備
	プロジェクト「ChatAppClient」を作成し、
	以下のファイルをダウンロードしてプロジェクトに追加して下さい。
	リソースヘッダ
	リソースファイル
	
●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 ClientProc(HWND dialog_handle, UINT message, WPARAM wp, LPARAM lp);

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

			引数:
				なし

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


	/*
		アドレス名(URL、IPアドレス)のゲッター
	*/
	char *GetAddressName() 
	{
		return m_AddressName;  
	}

	/*
		ホスト情報のバッファのゲッター
	*/
	char *GetHostInfoBuff()
	{
		return m_HostInfoBuff;
	}

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

	/*
		クライアントソケット作成
			戻り値:
				成功:true
				失敗:false

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

			内容:
				クライアントソケットを非同期モードで作成し、待機状態にする
	*/
	BOOL MakeClientSocket(HWND window_handle);

	/*
		接続用パラメータの初期化
			戻り値:
				TRUE:初期化成功
				FALSE:初期化失敗

			引数:
				HWND:ダイアログのハンドル

			内容:
				ポート番号やアドレスなど、通信で必要なパラメータを設定する
	*/
	BOOL InitConnectParam(HWND handle);

	/*
		通信接続
			戻り値:
				成功:TRUE
				失敗:FALSE
			
			引数:
				なし
			
			内容:
				サーバーへ接続依頼をする
	*/
	BOOL Connect(void);

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

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

	/*
				
	*/
	void Send(HWND handle);

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

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

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

			引数:
				なし

			内容:
				通信を終了するために通信用、サーバー用のソケットを閉じる
	*/
	void EndNetWork();
	
public:
	static const UINT WM_ASYNC = (WM_USER + 1);		// 非同期通信イベントID
	static const UINT WM_SERVER_BY_NAME = (WM_USER + 2);
	static const UINT WM_SERVER_BY_ADDRESS = (WM_USER + 3);
	static const UINT ADDRESS_BUFF_SIZE = 256;
	static const UINT HOST_INFO_BUFF = 256;

private:
	const LPWSTR DialogName = TEXT("CLIENT_DLG");	// ダイアログのリソース名
	WSADATA m_WsaData;				// WinSockets情報

	int m_PortNo;					// ポート番号
	char m_AddressName[ADDRESS_BUFF_SIZE];		// アドレス情報
	std::string m_HandleName;			// ハンドルネーム
	char m_HostInfoBuff[MAXGETHOSTSTRUCT];
	SOCKET m_ClientSocket;				// クライアント用ソケット
	SOCKADDR_IN m_HostInfo;				// ホスト情報
	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)
{
	// ①.WinScok開始
	if (WSAStartup(version, &m_WsaData) != 0)
	{
		return -1;
	}

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

	// ⑬.WinSock終了
	WSACleanup();

	return ret;
}

/*
	接続用パラメータの初期化
		戻り値:
			TRUE:初期化成功
			FALSE:初期化失敗

		引数:
			HWND:ダイアログのハンドル

		内容:
			ポート番号やアドレスなど、通信で必要なパラメータを設定する
*/
BOOL NetWork::InitConnectParam(HWND handle)
{
	BOOL is_success = false;
	char buff[128];

	m_PortNo = GetDlgItemInt(handle, IDC_PORT_TXT, &is_success, true);
	if (is_success == FALSE)
	{
		MessageBox(NULL, TEXT("ポート番号に正しい数値が入力されていません。"), TEXT("ポート番号エラー"), MB_OK);
		return FALSE;
	}

	GetDlgItemTextA(handle, IDC_ADDRESS_TXT, m_AddressName, NetWork::ADDRESS_BUFF_SIZE);
	if (strlen(m_AddressName) == 0)
	{
		MessageBox(NULL, TEXT("アドレスが入力されていません。"), TEXT("アドレスエラー"), MB_OK);
		return FALSE;
	}

	GetDlgItemTextA(handle, IDC_HN_TXT, buff, 128);
	if (strlen(buff) == 0)
	{
		strcpy_s(buff, "名無し");
	}
	m_HandleName = buff;

	memset(m_AddressName, 0, sizeof(char)* ADDRESS_BUFF_SIZE);
	memset(m_HostInfoBuff, 0, sizeof(char) * MAXGETHOSTSTRUCT);

	return TRUE;
}

/*
	クライアントソケット作成
		戻り値:
			成功:TRUE
			失敗:FALSE

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

		内容:
			クライアントのソケットを非同期モードで作成し、待機状態にする
*/
BOOL NetWork::MakeClientSocket(HWND handle)
{
	// ③.ポート番号、URL(IP)の取得
	if (InitConnectParam(handle) == FALSE)
	{
		return FALSE;
	}
	// ④.クライアントソケット作成
	m_ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (m_ClientSocket == INVALID_SOCKET)
	{
		MessageBox(NULL, TEXT("ソケット作成に失敗しました。"), TEXT("ソケット作成エラー"), MB_OK);
		return FALSE;
	}

	// ⑤.非同期通信設定(イベントは通信許可依頼通知(FD_CONNECT)、受信(FD_READ)、終了(FD_CLOSE))
	if (WSAAsyncSelect(m_ClientSocket, handle, NetWork::WM_ASYNC, FD_CONNECT | FD_READ | FD_CLOSE))
	{
		closesocket(m_ClientSocket);
		MessageBox(NULL, TEXT("非同期設定に失敗しました。"), TEXT("非同期設定エラー"), MB_OK);
		return FALSE;
	}

	return TRUE;
}

/*
	通信接続
		戻り値:
			成功:TRUE
			失敗:FALSE
	
		引数:
			なし
		
		内容:
			サーバーへ接続依頼をする
*/
BOOL NetWork::Connect(void)
{
	LPHOSTENT phost = NULL;
	unsigned int addr;
	SOCKADDR_IN sock_addr;

	// サーバーへ接続するための準備
	phost = (LPHOSTENT)m_HostInfoBuff;

	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 = *((unsigned long*)phost->h_addr);

	// クライアントソケットをサーバーのソケットに接続する
	if (connect(m_ClientSocket, (SOCKADDR*)&sock_addr, sizeof(sock_addr)) == SOCKET_ERROR)
	{
		if (WSAGetLastError() == WSAEWOULDBLOCK)
		{
			return FALSE;
		}
	}

	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 += str;

	return TRUE;
}

void NetWork::Send(HWND handle)
{
	char buff[1024 + 128];

	strcpy_s(buff, m_HandleName.c_str());
	strcat_s(buff, ":\r\n");
	char *pbuff = buff + strlen(buff);
	GetDlgItemTextA(handle, IDC_SEND_TXT, pbuff, 1024);
	if (strlen(buff) == 0)
	{
		return;
	}

	send(m_ClientSocket, buff, strlen(buff), 0);
}

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

		引数:
			なし

		内容:
			通信を終了するためにソケットを閉じてWinSockを終了する
*/
void NetWork::EndNetWork()
{
	// ソケットを閉じる
	if (m_ClientSocket != NULL &&
		m_ClientSocket != INVALID_SOCKET)
	{
		// ソケットを閉じる
		closesocket(m_ClientSocket);
	}
}

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

		引数:
			なし

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

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

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

			UINT:
				受信したメッセージ

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

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

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

	switch (message)
	{
	// ダイアログの初期化
	case WM_INITDIALOG:
		EnableWindow(GetDlgItem(dialog_handle, IDC_START_BTN), TRUE);
		EnableWindow(GetDlgItem(dialog_handle, IDC_SEND_BTN), FALSE);
		break;
	// 「×」ボタンクリック
	case WM_CLOSE:
	case IDC_END_BTN:
		// ⑫.クライアントソケットを閉じる
		NetWork::GetInstance().EndNetWork();
		EndDialog(dialog_handle, 1);
		break;
	// イベントメッセージ
	case WM_COMMAND:
		switch (LOWORD(wp))
		{
		// 開始ボタン
		case IDC_START_BTN:
			// ポート番号取得
			if (NetWork::GetInstance().MakeClientSocket(dialog_handle) == TRUE)
			{
				// URLからホスト情報取得
				// ⑥.URLからサーバーの情報を取得する
				WSAAsyncGetHostByName(	dialog_handle, 
							NetWork::WM_SERVER_BY_NAME, 
							NetWork::GetInstance().GetHostInfoBuff(), 
							(char *)NetWork::GetInstance().GetHostInfoBuff(), 
							NetWork::HOST_INFO_BUFF);
			}
			break;
		// 送信ボタン
		case IDC_SEND_BTN:
			// ⑨.データ送信
			NetWork::GetInstance().Send(dialog_handle);
			break;
		}
		break;
	// サーバーの名前取得
	case NetWork::WM_SERVER_BY_NAME:
		if (WSAGETASYNCERROR(lp) == 0)
		{
			// ⑧.サーバーへ接続依頼
			if (NetWork::GetInstance().Connect() == FALSE)
			{
				// 失敗したら次はIPアドレスで試す
				// ⑦.IPからサーバーの情報を取得
				WSAAsyncGetHostByAddr(dialog_handle,
					NetWork::WM_SERVER_BY_ADDRESS,
					NetWork::GetInstance().GetAddressName(),
					sizeof(char*),
					AF_INET,
					NetWork::GetInstance().GetHostInfoBuff(),
					MAXGETHOSTSTRUCT);
			}
		} else {
			// 失敗したら次はIPアドレスで試す
			// ⑦.IPからサーバーの情報を取得
			WSAAsyncGetHostByAddr(dialog_handle,
				NetWork::WM_SERVER_BY_ADDRESS,
				NetWork::GetInstance().GetAddressName(),
				sizeof(char*),
				AF_INET,
				NetWork::GetInstance().GetHostInfoBuff(),
				MAXGETHOSTSTRUCT);
		}
		break;
	// サーバーのアドレス取得
	case NetWork::WM_SERVER_BY_ADDRESS:
		if (WSAGETASYNCERROR(lp) == 0)
		{
			// ⑧.サーバーへ接続依頼
			if (NetWork::GetInstance().Connect() == FALSE)
			{
				MessageBox(NULL, TEXT("接続に失敗しました。"), TEXT("接続エラー"), MB_OK);
			}
		} else {
			int async_error = WSAGETASYNCERROR(lp);
			int error = WSAGetLastError();
			MessageBox(NULL, TEXT("ホストの取得に失敗しました。"), TEXT("ホスト情報取得エラー"), MB_OK);
		}
		break;
	// 非同期通信イベント
	case NetWork::WM_ASYNC:
		switch (WSAGETSELECTEVENT(lp))
		{
		// 接続
		case FD_CONNECT:
			// 接続が完了したのでボタンを使えないようにする
			EnableWindow(GetDlgItem(dialog_handle, IDC_SEND_BTN), TRUE);
			EnableWindow(GetDlgItem(dialog_handle, IDC_START_BTN), FALSE);
			MessageBox(NULL, TEXT("ホストとの接続に成功しました。"), TEXT("接続"), MB_OK);
			break;
		// 受信
		case FD_READ:
			// ⑩.データ受信
			if (NetWork::GetInstance().Receive(wp) == TRUE)
			{
				HWND info = GetDlgItem(dialog_handle, IDC_INFO_TXT);
				if (info != NULL)
				{
					// 表示内容の更新
					SetDlgItemTextA(dialog_handle, IDC_INFO_TXT, NetWork::GetInstance().GetInfoText().c_str());

					// 表示位置の更新
					SendMessage(GetDlgItem(dialog_handle, IDC_INFO_TXT), EM_LINESCROLL, 0, Edit_GetLineCount(info));

				}
			}
			break;
		// ⑪.通信切断で通信を終わる
		case FD_CLOSE:
			// ⑫.クライアントソケットを閉じる
			NetWork::GetInstance().EndNetWork();
			// ダイアログ終了
			EndDialog(dialog_handle, 1);
			break;
		default:
			return FALSE;
			break;
		}
		break;
	default:
		return FALSE;
	}

	return TRUE;
}