|
MFC非同期ソケット (クライアント編2)
前回の続きで、非同期ソケットの送受信の方法を解説していきます。まず、送信と受信では処理に違いがあります。送信の場合は、こちらのタイミングで送信します。今回作っているプログラムで言えば、「送信」ボタンが押されたタイミングで送信処理をします。送信はSend()関数を呼びます。
これに対して、受信の場合は、相手が送信したタイミングによるので、こちらでは突然コールバック関数OnReceive()が呼ばれることになります。OnReceive()が呼ばれたときに、OnReceive()の関数の中でRecieve()関数を使い、受信します。
では、OnSend()というコールバックはいつ呼ばれるのでしょうか?これは送信できる状態になったときに呼び出されます。具体的には、ソケットを接続した直後と、大量のデータを送信しようとして一度では送れない場合に、次のデータが送れるようになる度に呼ばれます。
詳しく見てみましょう。まず、送信の場合です。送信処理でSend()関数を呼びますが、ここでは4通りの結果が考えられます。それは、
・送信したいデータをすべて送信した
・送信したいデータの一部だけ送信した
・エラー WSAEWOULDBLOCKを返した
・その他のエラー
です。送信したいデータの一部だけ送信した場合は、送信したいバイト数に達するまで、Send()関数を繰り返して呼びます。エラー
WSAEWOULDBLOCKが返ってきた場合は、ソケットがブロックされています。このブロックが解除されるとOnSend()が呼ばれるので、一度関数を抜けて、続きはOnSend()関数で送信します。
OnSend()関数の中でも同じように処理します。とにかく、WSAEWOULDBLOCKが返ってきたときは、一度関数を抜けて、再びOnSend()が呼ばれるまで送信を中断します。

次は受信の場合です。こちらはOnReceive()から始まります。OnReceive()の中でReceive()を呼んで、受信します。ここでもWSAEWOULDBLOCKが返ってきたときは、一度関数を抜けて、再びOnReceive()が呼ばれるまで受信を中断します。
また、受信処理に限っては、1回のOnReceive()呼び出しに対して、Receive()関数は1回しか呼んではいけません。ここで指定バイト数が受信できなかったときは、次回のOnReceive()で続きを受信します。
では、コードのほうを見ていきましょう。まずはソケットクラスのほうです。Create()関数はオーバーライドして、ダイアログクラスのポインタを受け取るようにしています。
OnConnect()、OnSend()、OnReceive()関数は、それぞれダイアログクラスのコールバックを呼んでいます。
// Socket作成
BOOL CClientASock::Create(CAsyncClientDlg *dlgP)
{
m_dlgP = dlgP;
return CAsyncSocket::Create();
}
// Receive通知
void CClientASock::OnReceive(int nErrorCode)
{
if (m_dlgP) m_dlgP->OnReceive(nErrorCode);
CAsyncSocket::OnReceive(nErrorCode);
}
// Send通知
void CClientASock::OnSend(int nErrorCode)
{
if (m_dlgP) m_dlgP->OnSend(nErrorCode);
CAsyncSocket::OnSend(nErrorCode);
}
// Connect通知
void CClientASock::OnConnect(int nErrorCode)
{
if (m_dlgP) m_dlgP->OnConnect(nErrorCode);
CAsyncSocket::OnConnect(nErrorCode);
}
|
次はダイアログクラスのほうです。「接続」ボタンが押されたときのイベントハンドラです。
// "接続"ボタン押下
void CAsyncClientDlg::OnBnClickedBtnCnct()
{
unsigned int port = 0;
int err = 0;
UpdateData();
m_sock.Close();
// (1)ソケット作成
if (!err) if (!m_sock.Create(this)) err = 1;
// (2)ポート取得
if (!err) if (_stscanf_s(m_xvEditPort, _T("%d"), &port) != 1) err = 1;
// (3)接続
if (!err)
{
if (!m_sock.Connect(m_xvEditIP, port)) err = 1;
// WSAEWOULDBLOCKはエラーとしない
if (err) if (m_sock.GetLastError() == WSAEWOULDBLOCK) err = 0;
}
// (4)エラー表示
if (err) {DispErr(m_sock.GetLastError()), m_sock.Close();}
return;
}
|
(1)ソケット作成
オーバーライドしたCreate()関数を使って、接続用のソケットを作成します。ここでthisポインタを渡しています。
(2)ポート取得
DDX変数からポート番号を取得しています。
(3)接続
CAsyncsocket::Connect()関数で指定したサーバ・ポートに接続します。通常はWSAEWOULDBLOCKが返ります。接続相手が見つからないなどのエラーは、実際に接続してみないとわからないので、この時点ではエラーは返ってきません。このようなエラーは、OnConnect()の引数で取得できます。
(4)エラー表示
エラー表示は別関数にしています。CAsyncSocket::GetLastError()で最後に発生したエラーのエラーコードを取得し、APIのFormatMessage()関数でエラーメッセージを取得しています。
// エラー表示
void CAsyncClientDlg::DispErr(int code)
{
LPVOID lpMsgBuf = NULL;
::FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, code,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
m_xvEditLog += _T("Error : ");
m_xvEditLog += static_cast<LPTSTR>(lpMsgBuf);
UpdateData(FALSE);
LocalFree(lpMsgBuf);
return;
}
|
次は、接続の通知関数です。この関数は接続が完了、または接続エラー時に呼ばれます。ここではエラー発生時の処理をしています。
// Connect通知
void CAsyncClientDlg::OnConnect(int nErrorCode)
{
if (nErrorCode) {DispErr(nErrorCode), m_sock.Close();}
return;
}
|
次は「送信」ボタンが押されたときのイベントハンドラです。今回は簡単のため、20バイトの固定サイズのデータを送信しています。Send()関数でWSAEWOULDBLOCKが返ってきた場合は、一度関数を抜けて、続きはOnSend()で送信します。そのため、送信したバイト数の合計を、メンバ変数m_sendSumに保存しています。
// "送信"ボタン押下
void CAsyncClientDlg::OnBnClickedBtnSend()
{
CString sendStr;
int send;
LPCSTR byteCP = NULL;
int err = 0;
UpdateData();
// 送信(20バイト固定)
if (!err)
{
sendStr = m_xvEditMes;
while (sendStr.GetLength() < 20) sendStr += _T(" ");
sendStr = sendStr.Left(20);
if (m_sendSum >= 20) m_sendSum = 0;
// Send()は20バイト送るまで繰り返す
while (m_sendSum < 20)
{
byteCP = static_cast<LPCSTR>(sendStr) +m_sendSum;
send = m_sock.Send(byteCP, 20 -m_sendSum);
if (send == SOCKET_ERROR) err = 1;
// WSAEWOULDBLOCKはエラーとしない。次のOnSendまで制御を戻す。
if (err) if (m_sock.GetLastError() == WSAEWOULDBLOCK) {err = 0; break;}
if (err) break;
m_sendSum += send;
}
}
if (!err)
{
if (m_sendSum >= 20)
{
m_sendSum = 0;
m_xvEditLog += _T("Send : ");
m_xvEditLog += sendStr +_T("\r\n");
}
}
// エラー表示
if (err)
{
DispErr(m_sock.GetLastError());
m_sock.Close();
}
UpdateData(FALSE);
return;
}
|
次は、送信の通知関数です。この関数は接続完了後に1回呼び出されます。また、送信が1度に終わらなかった場合も呼び出されます。その場合は、続きを送信するようにします。
// Send通知
void CAsyncClientDlg::OnSend(int nErrorCode)
{
int err = 0;
if (nErrorCode) err = 1;
// 続きを送信
if (!err) if (m_sendSum) OnBnClickedBtnSend();
// エラー表示
if (err)
{
DispErr(nErrorCode);
m_sock.Close();
}
return;
}
|
次は受信通知関数です。この関数は、ソケットがデータを受信するたびに呼び出されます。ここでは簡単のため、20バイトの固定サイズデータを受信しています。一度に受信できなかった場合は、次回のOnReceive()呼び出しで続きを受信します。そのため、受信したバイト数の合計m_recvSumと文字列m_recvStrをメンバ変数に保存しています。
// Receive通知
void CAsyncClientDlg::OnReceive(int nErrorCode)
{
int recv;
LPSTR byteP = NULL;
int err = 0;
UpdateData();
if (nErrorCode) err = 1;
// 受信(20バイト固定)
if (!err)
{
byteP = m_recvStr.GetBuffer(21);
if (m_recvSum >= 20) m_recvSum = 0;
// Receive()は一回のみ
if (m_recvSum < 20)
{
recv = m_sock.Receive(byteP +m_recvSum, 20 -m_recvSum);
if (recv == SOCKET_ERROR || recv == 0) err = 1;
if (!err) m_recvSum += recv;
// WSAEWOULDBLOCKはエラーとしない。次のOnReceiveまで制御を戻す。
if (err) if (m_sock.GetLastError() == WSAEWOULDBLOCK) err = 0;
}
byteP[20] = '\0';
m_recvStr.ReleaseBuffer();
}
if (!err)
{
if (m_recvSum >= 20)
{
m_recvSum = 0;
m_xvEditLog += _T("Recv : ");
m_xvEditLog += m_recvStr +_T("\r\n");
m_sock.Close();
}
}
// エラー表示
if (err)
{
DispErr(nErrorCode ? nErrorCode : m_sock.GetLastError());
m_sock.Close();
}
UpdateData(FALSE);
return;
}
|
非同期ソケットの特長は、関数がすぐに処理を返すので、使う側のスレッドを拘束しないことですね。今回のようにダイアログなどのUIスレッドと組み合わせて使う場合は、とても有効です。次回は非同期ソケットのサーバ側を作ってみましょう。
|