|
MFCソケット通信の基本 (クライアント編)
今回はソケット通信を扱います。ソケット通信はコンピュータ(の中のプロセス)間でデータを送受信するための基本となる仕組みです。異なるOS間であっても、同一コンピュータ内のプロセス間でも通信ができます。ソケットについての細かい説明は省略しますが、ここでは、MFCのソケットクラスを使って、最も簡単にデータ通信をするプログラムを作成してみます。
実装を始める前に、まず必要最低限の知識を説明しておきましょう。ソケットにはサーバとクライアントがあります。通常は1つのサーバプロセスに対して、複数のクライアントがコネクションを張るような形になります。
一度コネクションが確立されると、あとはサーバ・クライアント間で自由にデータのやり取りができます。ソケットが提供するのはデータ送受信の仕組みだけで、データの中身については何も決まりはありません。
これは、データ送受信の順番や、データ長、文字コードなど、通信に関する様々な決まりは、あらかじめサーバ・クライアント間で決めておかなければいけないことを意味します。
データの大きさについても決まりはなく、何GBのデータでも送れます。ですが、当然のことながら一度にすべて送信できるわけではなく、受信側も一度にすべて受信できるわけではありません。そのときのネットワークの負荷や、マシンの負荷の都合で少しずつ断続的に送られてくることもあります。
MFCでは、このようなパケットを送受信する場合、2つの実装方法があります。一つは、次のデータが送られてくるまでずっと待ち続ける方法。もう一つは分割されたデータが送られてくるたびにコールバック関数が呼び出され、受信する方法です。
前者は"ブロッキング"と呼ばれます。この方法は、Receive()関数を呼ぶと、データが到着するまで関数が制御を返しません。後者は"非ブロッキング"と呼ばれます。この方法ではReceive()関数は必ずすぐに制御を返します。データが届いていなければエラーを返します。
このため、MFCのソケットクラスは2種類あり、非ブロッキングのほうはCAsyncSocket、ブロッキングのほうはCSocketというクラスを使います。とは言っても、CSocketはCAsyncSocketの派生クラスです。大きな違いは、ブロックするかしないかということだけです。Receive()やSend()でデータを送受信することに違いはありません。
この2つで言うと、CSocketの方が高度にソケットを抽象化しているので、プログラムを単純化できます。プログラム構造的に言うと、CAsyncSocketはイベントドリブン型、CSocketは逐次処理型とも言えます。初めてMFCのソケットを使うのであれば、まずはCSocketを使って通信してみるのが一番手っ取り早いです。
ということで、ここではCSocketクラスを使った最も単純な通信プログラムを作ります。
では、具体的な作業に入りましょう。いつも通りダイアログベースでプログラムを作りますが、ソケットを使うときは、MFCアプリケーションウィザードの"高度な機能"で"Windowsソケット"をチェックします。こうすると、必要なヘッダのインクルードと、ソケットの初期化処理が追加されます。

また、ここでは説明を簡単にするため、UNICODEには対応しません。プロジェクト設定が"UNICODEを使用する"になっていたら、"マルチバイト文字セットを使用する"か"設定なし"に変更してください。
サーバのIPとポート、送信メッセージと通信ログ用のエディットボックス、送信ボタンを追加しました。エディットボックスにはいつも通りDDX変数、ボタンにはBN_CLICKEDのイベントハンドラを追加しました。(これらの使い方については、ボタンの基本、エディットボックスの基本等を見てください。)
このプログラムがやることは、"送信"ボタンが押されたら、指定したサーバ・ポートに接続してメッセージを送信し、返信を受け取り、ログを表示するだけです。
では、"送信"ボタンが押された時のイベントハンドラを実装します。
// "送信"ボタン押下
void CClientSockDlg::OnBnClickedBtnSend()
{
CSocket sock;
unsigned int port = 0;
CString sendStr, recvStr;
int send, recv, sendSum, recvSum;
LPCSTR byteCP = NULL;
LPSTR byteP = NULL;
int err = 0;
UpdateData();
// (1)ソケット作成
if (!err) if (!sock.Create()) err = 1;
// (2)ポート取得
if (!err) if (_stscanf_s(m_xvEditPort, _T("%d"), &port) != 1) err = 1;
// (3)接続
if (!err) if (!sock.Connect(m_xvEditIP, port)) err = 1;
// (4)送信(20バイト固定)
if (!err)
{
sendStr = m_xvEditMes;
while (sendStr.GetLength() < 20) sendStr += _T(" ");
sendStr = sendStr.Left(20);
sendSum = 0;
while (sendSum < 20)
{
byteCP = static_cast<LPCSTR>(sendStr) +sendSum;
send = sock.Send(byteCP, 20 -sendSum);
if (send == SOCKET_ERROR) {err = 1; break;}
sendSum += send;
}
}
if (!err)
{
m_xvEditLog += _T("Send : ");
m_xvEditLog += sendStr +_T("\r\n");
}
// (5)受信(20バイト固定)
if (!err)
{
byteP = recvStr.GetBuffer(21);
recvSum = 0;
while (recvSum < 20)
{
recv = sock.Receive(byteP +recvSum, 20 -recvSum);
if (recv == SOCKET_ERROR || recv == 0) {err = 1; break;}
recvSum += recv;
}
byteP[20] = '\0';
recvStr.ReleaseBuffer();
}
if (!err)
{
m_xvEditLog += _T("Recv : ");
m_xvEditLog += recvStr +_T("\r\n");
}
// (6)エラー表示
if (err)
{
LPVOID lpMsgBuf = NULL;
::FormatMessage(
FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, sock.GetLastError(),
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf, 0, NULL);
m_xvEditLog += _T("Error : ");
m_xvEditLog += static_cast<LPTSTR>(lpMsgBuf);
UpdateData(FALSE);
LocalFree(lpMsgBuf);
}
// (7)切断
sock.Close();
UpdateData(FALSE);
return;
}
|
ちょっと長いですが、この関数が処理のすべてです。では、順番に見ていきましょう。
(1)ソケット作成
CSocketクラスのインスタンスを作り、CSocket::Create()関数を使い、ソケットを作ります。本当はいくつか引数がありますが、デフォルト値で特に問題はありません。
(2)ポート取得
ソケットとは特に関係ありませんが、CString型のDDX変数からポート番号を取得しています。CStringクラスには文字列から数値に変換する関数はありませんので、ランタイム関数を使っています。
ちなみに、ここで使っている_stscanf_s()という関数は、Visual Studio
2005で追加されたセキュリティ強化版の関数です。
(3)接続
CAsyncSocket::Connect()で接続先のサーバ名(またはIPアドレス)とポート番号を指定して、サーバに接続します。
(4)送信(20バイト固定)
接続が成功したら、サーバにデータを送信します。ここで送信しているということは、サーバ側はこの時点で受信していないといけないということになります。このように、送受信の順番は、あらかじめクライアント・サーバ間の"通信仕様"として決めておかないといけません。
さらに、ここでは文字列を20バイトの固定長データにして送信しています。これも通信仕様として決めておかないといけません。もしデータ長が決められていなければ、受信側は何バイト受信すればいいのかわかりません。データが途切れたら、そこがデータの終わりなのか、それとも回線が混んでいるから途切れているだけなのか、判断ができなくなります。
送信は、CAsyncSocket::Send()関数を使います。この関数はデータを送信しますが、重要なことは、「一度に全部送信できるとは限らない」ということです。巨大なデータの場合は、大抵何回かに分けて送られます。そのため、Send()関数は、実際に送信したバイト数の合計が、送信したいバイト数に達するまで、繰り返して呼ばないといけません。
| virtual int CAsyncSocket::Send(const
void* lpBuf, int nBufLen, int nFlags = 0); |
| 説明: |
ソケットにデータを送信する |
| 引数: |
lpBuf:送信するデータ
nBufLen:送信するデータのバイト数
nFlags:呼び出し方法 |
| 戻り値: |
正常な場合は、送信したバイト数。エラーの場合SOCKET_ERROR |
(5)受信(20バイト固定)
送信が終わったら、サーバからの返答を受信します。受信に関しても送信と同じで、目的のバイト数を受信し終わるまで、CAsyncSocket::Receive()関数を繰り返して呼びます。
| virtual int CAsyncSocket::Receive(void*
lpBuf, int nBufLen, int nFlags = 0); |
| 説明: |
ソケットからデータを受信する |
| 引数: |
lpBuf:受信するデータバッファ
nBufLen:受信するデータのバイト数
nFlags:呼び出し方法 |
| 戻り値: |
正常な場合は、送信したバイト数。すでに接続が閉じている場合は0。エラーの場合SOCKET_ERROR |
今回は簡単のためにデータ長を固定長としましたが、可変長データを送るときは、まずデータ長自体をデータとして送って、そのあとに実際のデータを送る必要があります。こうすると、受信する方は最初にデータ長を受け取るので、そのあと何バイト受信すればいいのかがわかるようになります。
(6)エラー表示
ソケットに関して、何らかのエラーが発生したときは、CAsyncSocket::GetLastError()を呼び出すと、最後に発生したエラーのエラーコードが取得できます。さらにAPIのFormatMessage()関数を使うと、エラーコードに対応するメッセージを取得できます。
(7)切断
CAsyncSocket::Close()関数を呼び出すとソケットが切断されます。このClose()関数は、CAsyncSocketクラスのデストラクタでも呼び出されるので、今回のようにCSocketオブジェクトをローカル変数にとった場合は、明示的に呼び出さなくても、関数から抜けた時点で自動的に呼び出されます。
また、より安全に切断する場合は、CAsyncSocket::ShutDown()を実行してから、Close()を実行します。ShutDown()関数は、ソケットに対してそれ以降の送信、受信、または両方を禁止します。今回は、一回ずつデータをやり取りしたらそれで終わりなので、特にShotDown()は使用していません。
では、ビルドして実行してみます。当然のことながら、クライアントだけでは何もできません。特定のコンピュータの特定のポートにデータを送信しようとするとどうなるか、試してみましょう。
次回はCSocketクラスを使ってサーバソケットを作成します。
|