ダイアログベースの MFC アプリだけで、XML メッセージを受信して 20 秒以内に結果を返信する小さな TCP サーバーを作ろうとすると、UI フリーズやメッセージ境界など思った以上に落とし穴が多くなります。この記事では、ワーカースレッド方式を中心に「固まらないダイアログ」「20 秒タイムアウト」「XML 1 メッセージ単位の処理」を満たすサンプル構成を、具体的な MFC/C++ コード付きで整理していきます。
ダイアログベース MFC で簡易 TCP サーバーを作る要件整理
今回のゴールは次の通りです。
- MFC のダイアログ(
CDialogEx)アプリの中だけで完結 - クライアントから送られてくる XML を
<ROOT> ... </ROOT>まで受信 - 受信開始から 20 秒以内に以下のいずれかを返信する
<ROOT><Result good="true" Percent="100"/></ROOT>
または
<ROOT><Result good="false"/></ROOT>
- 受信した XML と返信した XML をダイアログ上の Edit などに表示する
- UI は絶対に固まらない(停止ボタンもきちんと効く)
「とりあえず accept(), recv() をボタンハンドラから呼ぶだけ」の実装では、ほぼ確実に次の問題にぶつかります。
| 問題点 | 何が起きるか | 対処の方針 |
|---|---|---|
UI スレッドで accept()/recv() | ダイアログがフリーズ、停止ボタンも反応しない | ワーカースレッド or 非同期ソケットに分離 |
| メッセージ境界未考慮 | XML が途中で切れて届く/複数まとまって届く | バッファに蓄積し、</ROOT> までを 1 メッセージとみなす |
| 20 秒制限の未実装 | クライアントが待ち続けてしまう | 受信開始時刻を記録し、20 秒で強制タイムアウト |
| ローカル固定バインド | 127.0.0.1 からしか接続できない | INADDR_ANY で待ち受け、必要に応じて制限 |
| 停止処理不足 | アプリ終了時にソケットが残る/再起動でポート使用中 | shutdown(), closesocket()、イベントで停止制御 |
| UI 更新がスレッド非安全 | ワーカースレッドから Edit を直接操作してクラッシュ | PostMessage 経由で UI スレッドにログを渡す |
| XML サイズ無制限 | 巨大入力でメモリを圧迫される危険 | サイズ上限(例:1MB)超過で異常終了扱い |
この記事では、これらの課題をすべてカバーする「ワーカースレッド方式」を軸に構成します。MFC らしいメッセージ駆動方式(WSAAsyncSelect / CAsyncSocket)についても後半で簡単に触れます。
アーキテクチャ設計:UI スレッドとサーバースレッドの分離
まずは全体像をシンプルにイメージしておきます。
- UI スレッド:ボタン・Edit・ログ表示などを担当
- サーバースレッド:
socket(),bind(),listen(),accept(),recv(),send() - 停止イベント:UI からサーバースレッドに「そろそろ終了して」の合図を送る
- ログメッセージ:サーバー側から UI 側へ
PostMessage(WM_APP_...)で通知
| コンポーネント | 役割 | ポイント |
|---|---|---|
| MFC ダイアログ | 画面表示・Start/Stop ボタン | Winsock 初期化、スレッド起動・停止を管理 |
| サーバースレッド | TCP サーバーループ | accept() & 1 接続ずつ XML を受信・返信 |
| 停止イベント | 安全な終了トリガー | UI から SetEvent して accept() を解除 |
| ログメッセージ | 受信/送信の可視化 | UI スレッドだけが Edit を操作する |
この構成にしておくと、UI スレッド側は「ボタンを押されたらスレッドを起動/停止するだけ」で済み、ソケットの細かい処理はすべてサーバースレッドに閉じ込めることができます。
実装前提:プロジェクト設定と基本メンバー
以下は、標準的な MFC ダイアログアプリ(UNICODE 有効)を新規作成した前提で話を進めます。
Winsock のインクルードとライブラリ
stdafx.h もしくは適切なヘッダーに次を追加しておきます。
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "Ws2_32.lib")
ダイアログクラスのメンバー変数
例としてダイアログクラス名を CXmlServerDlg とします。ヘッダに次のようなメンバーを用意します。
class CXmlServerDlg : public CDialogEx
{
// 〜略〜
protected:
// サーバースレッド
CWinThread* m_pServerThread;
// リッスンソケット(accept 用)
SOCKET m_listenSocket;
// 停止イベント
HANDLE m_hStopEvent;
// ポート番号
UINT m_nPort;
// ログ表示用(複数行 Edit など)
CEdit m_editLog;
// スレッド用ヘルパ
static UINT __stdcall ServerThreadProc(LPVOID pParam);
UINT ServerThreadMain();
// ログ追加(スレッドセーフにするため PostMessage 経由)
void PostLog(LPCTSTR pszText);
// XML を受信してレスポンスを返す
void HandleClient(SOCKET clientSock);
// メッセージ ID
enum { WM_APP_LOG = WM_APP + 1 };
// メッセージハンドラ
afx_msg LRESULT OnAppLog(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
};
WM_APP_ から始まるメッセージ ID はアプリケーション内で自由に使えるので、ログ通知に利用します。
Winsock の初期化とクリーンアップ
OnInitDialog() 内で Winsock を初期化し、アプリ終了時にクリーンアップします。
BOOL CXmlServerDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
WSADATA wsaData = {};
int r = ::WSAStartup(MAKEWORD(2, 2), &wsaData);
if (r != 0) {
AfxMessageBox(_T("WSAStartup に失敗しました。"));
EndDialog(IDCANCEL);
return FALSE;
}
m_pServerThread = nullptr;
m_listenSocket = INVALID_SOCKET;
m_hStopEvent = nullptr;
m_nPort = 12345; // 必要に応じて UI から変更
return TRUE;
}
void CXmlServerDlg::OnDestroy()
{
StopServer(); // 後述:停止処理を関数化しておく
::WSACleanup();
CDialogEx::OnDestroy();
}
ここで呼んでいる StopServer() はこの後で定義します。アプリ終了時に必ずサーバースレッドとソケットを止めるためです。
Start/Stop ボタンからサーバースレッドを制御する
サーバー起動処理
「開始」ボタンに対応するハンドラを次のように実装します。
void CXmlServerDlg::OnBnClickedButtonStart()
{
if (m_pServerThread != nullptr) {
AfxMessageBox(_T("すでにサーバーが起動しています。"));
return;
}
// 停止イベントを作成(手動リセット, 初期は非シグナル)
m_hStopEvent = ::CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!m_hStopEvent) {
AfxMessageBox(_T("イベント作成に失敗しました。"));
return;
}
// サーバースレッド起動
m_pServerThread = AfxBeginThread(
ServerThreadProc,
this,
THREAD_PRIORITY_NORMAL,
0,
CREATE_SUSPENDED);
if (!m_pServerThread) {
AfxMessageBox(_T("サーバースレッドの起動に失敗しました。"));
::CloseHandle(m_hStopEvent);
m_hStopEvent = nullptr;
return;
}
// 終了を待ちたいので AutoDelete を無効
m_pServerThread->m_bAutoDelete = FALSE;
m_pServerThread->ResumeThread();
PostLog(_T("[INFO] サーバーを起動しました。\r\n"));
}
ここでは ServerThreadProc を C ランタイム互換の静的関数として定義し、その中からインスタンスメンバー ServerThreadMain() を呼び出すスタイルを採用します。
UINT __stdcall CXmlServerDlg::ServerThreadProc(LPVOID pParam)
{
CXmlServerDlg* pThis = reinterpret_cast<CXmlServerDlg*>(pParam);
return pThis->ServerThreadMain();
}
サーバー停止処理
「停止」ボタンや終了時などから呼び出す共通関数として StopServer() を用意しておくと管理しやすくなります。
void CXmlServerDlg::StopServer()
{
if (m_pServerThread == nullptr)
return;
// スレッドに停止要求
if (m_hStopEvent) {
::SetEvent(m_hStopEvent);
}
// accept() を解除するためリッスンソケットを閉じる
if (m_listenSocket != INVALID_SOCKET) {
::closesocket(m_listenSocket);
m_listenSocket = INVALID_SOCKET;
}
// スレッド終了待ち(最大 3 秒)
::WaitForSingleObject(m_pServerThread->m_hThread, 3000);
delete m_pServerThread;
m_pServerThread = nullptr;
if (m_hStopEvent) {
::CloseHandle(m_hStopEvent);
m_hStopEvent = nullptr;
}
PostLog(_T("[INFO] サーバーを停止しました。\r\n"));
}
void CXmlServerDlg::OnBnClickedButtonStop()
{
StopServer();
}
StopServer() は次の 2 点が重要です。
SetEvent()でサーバースレッド側に「止まってよ」という合図を送るclosesocket(m_listenSocket)でブロッキング中のaccept()を解除する
これによって、サーバースレッドは 20 秒待たなくても速やかに抜けることができます。
サーバースレッドのメインループ:listen / accept
次に ServerThreadMain() の中身を実装します。ここでは、1 接続ずつ順番に処理する単純なモデルを採用します(同時接続が不要な想定)。
UINT CXmlServerDlg::ServerThreadMain()
{
// リッスンソケット作成
m_listenSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_listenSocket == INVALID_SOCKET) {
PostLog(_T("[ERROR] socket() に失敗しました。\r\n"));
return 1;
}
// SO_REUSEADDR を有効化(再起動時のポート占有を防ぐ)
BOOL bReuse = TRUE;
::setsockopt(
m_listenSocket,
SOL_SOCKET,
SO_REUSEADDR,
(const char*)&bReuse,
sizeof(bReuse));
sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons((u_short)m_nPort);
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0 で待ち受け
if (::bind(m_listenSocket, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
PostLog(_T("[ERROR] bind() に失敗しました。\r\n"));
::closesocket(m_listenSocket);
m_listenSocket = INVALID_SOCKET;
return 1;
}
if (::listen(m_listenSocket, SOMAXCONN) == SOCKET_ERROR) {
PostLog(_T("[ERROR] listen() に失敗しました。\r\n"));
::closesocket(m_listenSocket);
m_listenSocket = INVALID_SOCKET;
return 1;
}
PostLog(_T("[INFO] ポートで待ち受け開始。\r\n"));
// 停止要求が来るまで accept() を繰り返す
while (true) {
// 停止イベントがシグナルならループを抜ける
if (m_hStopEvent &&
::WaitForSingleObject(m_hStopEvent, 0) == WAIT_OBJECT_0) {
break;
}
// クライアント接続待ち(ブロッキング)
sockaddr_in clientAddr = {};
int addrLen = sizeof(clientAddr);
SOCKET clientSock = ::accept(m_listenSocket, (sockaddr*)&clientAddr, &addrLen);
if (clientSock == INVALID_SOCKET) {
int err = ::WSAGetLastError();
// 停止のために closesocket() された場合などはエラーになる
if (err == WSAEINTR || err == WSAENOTSOCK) {
break; // 終了方向のエラーなのでループを抜ける
}
// 一時的なエラーならログだけ残して続行
CString msg;
msg.Format(_T("[WARN] accept() エラー: %d\r\n"), err);
PostLog(msg);
continue;
}
// 接続 1 件を処理
HandleClient(clientSock);
::shutdown(clientSock, SD_BOTH);
::closesocket(clientSock);
}
if (m_listenSocket != INVALID_SOCKET) {
::closesocket(m_listenSocket);
m_listenSocket = INVALID_SOCKET;
}
PostLog(_T("[INFO] サーバースレッド終了。\r\n"));
return 0;
}
ここまでで、「UI とは別スレッドで TCP サーバーが listen/accept を行う」土台が出来ました。あとは HandleClient() で XML を受信し、20 秒以内に返信するロジックを作り込んでいきます。
XML メッセージ受信と 20 秒タイムアウトの実装
つぎに「メッセージ境界」と「20 秒制限」をしっかり扱う HandleClient() を実装します。
基本戦略
recv()で受け取ったバイト列をstd::stringに蓄積- その中から
</ROOT>が見つかるまで繰り返し受信 - 最初に見つかった
<ROOT>...</ROOT>を 1 メッセージとして切り出す - 受信開始から 20 秒経っても
</ROOT>が来なければタイムアウト - サイズ上限(例:1MB)を超えたら異常扱い
HandleClient 実装例
void CXmlServerDlg::HandleClient(SOCKET clientSock)
{
// 受信タイムアウト(ミリ秒単位)。ここでは 2000ms ごとにエラーにする。
int recvTimeout = 2000;
::setsockopt(clientSock, SOL_SOCKET, SO_RCVTIMEO,
(const char*)&recvTimeout, sizeof(recvTimeout));
const size_t MAX_XML_SIZE = 1024 * 1024; // 1MB 上限
std::string buffer;
buffer.reserve(4096);
// 受信開始時刻
auto t0 = std::chrono::steady_clock::now();
bool completed = false;
bool success = false;
std::string xml;
PostLog(_T("[INFO] クライアント接続。\r\n"));
while (true) {
// 停止要求が来ていたら中断
if (m_hStopEvent &&
::WaitForSingleObject(m_hStopEvent, 0) == WAIT_OBJECT_0) {
PostLog(_T("[INFO] 停止要求によりクライアント処理を中断。\r\n"));
break;
}
// 20 秒経過チェック
auto now = std::chrono::steady_clock::now();
auto elapsedSec =
std::chrono::duration_cast<std::chrono::seconds>(now - t0).count();
if (elapsedSec >= 20) {
PostLog(_T("[WARN] 受信タイムアウト(20秒超過)。\r\n"));
break;
}
char temp[1024];
int n = ::recv(clientSock, temp, sizeof(temp), 0);
if (n == 0) {
// クライアントからの正常切断
PostLog(_T("[INFO] クライアントが切断しました。\r\n"));
break;
}
if (n < 0) {
int err = ::WSAGetLastError();
if (err == WSAETIMEDOUT) {
// SO_RCVTIMEO によるタイムアウト。20 秒まではループ継続。
continue;
}
CString msg;
msg.Format(_T("[ERROR] recv() エラー: %d\r\n"), err);
PostLog(msg);
break;
}
// 受信したバイト列をログ表示用に変換(ここでは簡単に ANSI 前提)
CString recvStr(temp, n);
PostLog(_T("[RX] ") + recvStr + _T("\r\n"));
buffer.append(temp, n);
if (buffer.size() > MAX_XML_SIZE) {
PostLog(_T("[ERROR] XML サイズ上限を超えました。\r\n"));
break;
}
// </ROOT> を探す
std::string endTag = "</ROOT>";
size_t posEnd = buffer.find(endTag);
if (posEnd != std::string::npos) {
size_t msgLen = posEnd + endTag.length();
xml.assign(buffer.begin(), buffer.begin() + msgLen);
completed = true;
break;
}
}
// レスポンス生成
std::string response;
if (completed) {
// ここで XML の妥当性チェックを行うのが理想
// サンプルでは簡単に <ROOT> が含まれているかだけを見る
if (xml.find("<ROOT>") != std::string::npos) {
success = true;
}
if (success) {
response =
"<ROOT><Result good=\"true\" Percent=\"100\"/></ROOT>";
} else {
response =
"<ROOT><Result good=\"false\"/></ROOT>";
}
} else {
// タイムアウトやエラー
response =
"<ROOT><Result good=\"false\"/></ROOT>";
}
// レスポンス送信
if (!response.empty()) {
int totalSent = 0;
while (totalSent < (int)response.size()) {
int sent = ::send(clientSock,
response.data() + totalSent,
(int)response.size() - totalSent,
0);
if (sent <= 0) break;
totalSent += sent;
}
CString sendStr(response.c_str());
PostLog(_T("[TX] ") + sendStr + _T("\r\n"));
}
}
このロジックにより、
- 受信が分割されても
bufferに蓄積して</ROOT>まで待つ - 20 秒以内に完了しなければ
good="false"を返す - 複数メッセージが連なって届いた場合でも、最初の 1 個だけを対象にする
という挙動になります。実運用では、複数メッセージに対応するために「残りのバイト列を buffer に戻して次のループで再利用する」などの拡張を行うと良いでしょう。
ワーカースレッドから安全に UI を更新する(PostMessage パターン)
MFC で「別スレッドから Edit に文字列を追加する」場合、直接 m_editLog.SetWindowText() を呼ぶのは危険です。必ず UI スレッドに処理を戻す必要があります。
ログ用メッセージ送信関数
サーバースレッド側からは「文字列ポインタ」を渡し、UI スレッド側で受け取って Edit に反映、最後に delete する方式にします。
void CXmlServerDlg::PostLog(LPCTSTR pszText)
{
// CString を動的に確保してポインタを送る(受信側で delete)
CString* pStr = new CString(pszText);
::PostMessage(m_hWnd, WM_APP_LOG, 0, (LPARAM)pStr);
}
ログメッセージハンドラ
メッセージマップとハンドラを追加します。
BEGIN_MESSAGE_MAP(CXmlServerDlg, CDialogEx)
// 〜略〜
ON_MESSAGE(WM_APP_LOG, &CXmlServerDlg::OnAppLog)
END_MESSAGE_MAP()
LRESULT CXmlServerDlg::OnAppLog(WPARAM wParam, LPARAM lParam)
{
CString* pStr = reinterpret_cast<CString*>(lParam);
if (!pStr) return 0;
// 既存のテキスト取得
CString current;
m_editLog.GetWindowText(current);
// 末尾に追加(必要なら行数制限などを入れる)
current += *pStr;
m_editLog.SetWindowText(current);
// キャレットを末尾へスクロール
m_editLog.SetSel(current.GetLength(), current.GetLength());
m_editLog.ScrollCaret();
delete pStr;
return 0;
}
このパターンを使えば、サーバースレッド側は PostLog(_T("[INFO] ...")); と呼ぶだけで、UI スレッドが責任を持って Edit を操作してくれます。
メッセージ境界の扱いをもう少しだけ丁寧に考える
TCP はストリームなので、「1 回の recv() が 1 メッセージ」である保証はありません。実際には次のようなパターンが起こります。
- 1 回の
recv()で XML 全体が来る(ラッキーなケース) - 2 回以上に分割されて届く(よくある)
- 2 メッセージ以上が一度に届く(たまにある)
今回の例では「最初の <ROOT>...</ROOT> だけ」を相手にするため、以下のような流れにします。
bufferにrecv()結果を appendbuffer.find("</ROOT>")で終了タグを探す- 見つかった位置までを 1 メッセージとして取り出す
- 残り(2 メッセージ目以降)は、必要なら
buffer.erase()せずに次回に持ち越す
複数メッセージに本格対応したいときは、次のようなループを追加します。
while (true) {
size_t posEnd = buffer.find(endTag);
if (posEnd == std::string::npos) break;
size_t msgLen = posEnd + endTag.length();
std::string oneXml = buffer.substr(0, msgLen);
// oneXml を処理
// 残りを buffer に戻す
buffer.erase(0, msgLen);
}
ただし、サーバー側で 1 接続 1 メッセージというプロトコルを決めてしまう(メッセージ送信後にクライアント側が切断する)なら、そこまで作り込まずとも現実問題あまり困らないケースも多いです。
20 秒タイムアウトの考え方と注意点
「20 秒以内に応答する」という要件を満たすには、次の 2 つを区別して考えるとスッキリします。
- 受信タイムアウト:
recv()が一定時間何も受け取らなかったときのタイムアウト - トータルタイムアウト:接続開始から応答完了までに許される時間(ここでは 20 秒)
SO_RCVTIMEO で設定できるのは「受信タイムアウト」だけです。なので、サンプルでは次のように組み合わせています。
SO_RCVTIMEO = 2 秒にして、細かくrecv()を抜けるstd::chrono::steady_clockで「接続からの経過時間」を毎ループ測る- 合計が 20 秒を超えたら、XML の完成に関わらず
good="false"を返す
これにより、「クライアントが途中で送るのをやめた」「とても遅い回線で一部だけ届いている」といった状況でも、20 秒で強制的に切り上げることができます。
スレッド方式 vs メッセージ駆動方式(WSAAsyncSelect / CAsyncSocket)
ここまで紹介してきたのは「ワーカースレッド方式」です。もうひとつの代表的な方法が WSAAsyncSelect や MFC の CAsyncSocket を使う「メッセージ駆動方式」です。
| 方式 | 特徴 | 向いているケース |
|---|---|---|
| ワーカースレッド方式 | 実装が直感的。accept()/recv() をそのまま書ける。 | ソケットプログラミングの入門・既にスレッドを使っているアプリ |
| メッセージ駆動方式 (WSAAsyncSelect / CAsyncSocket) | スレッド不要。UI メッセージとして FD_READ 等を処理。 | MFC 的な「メッセージ駆動」がしっくりくる人、接続数が多い場合 |
WSAAsyncSelect 方式のざっくり構成
メッセージ駆動方式では、おおまかに次のような手順になります。
- UI スレッド内で
socket(),bind(),listen()を実行 WSAAsyncSelect(m_listenSocket, m_hWnd, WM_APP_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE)を設定OnSocketNotify(WPARAM, LPARAM)のようなメッセージハンドラを作り、FD_ACCEPT/FD_READ/FD_CLOSEごとに処理を分岐FD_READが来るたびにrecv()→ バッファ蓄積 →</ROOT>検出- 20 秒の管理には
SetTimer()を使い、タイマーメッセージでタイムアウト判定
サーバースレッドを立てない代わりに、「どのソケットに対して今どこまで受信しているか」「タイマ ID と接続の対応付け」などを自前で管理する必要が出てきます。その分スケーラブルですが、初心者には少し難易度が上がるので、まずはワーカースレッド方式で確実に動くものを作るのがおすすめです。
堅牢化のヒント:本番を見据えるならここまでやりたい
ここまで実装できれば、単体テスト用の簡易サーバーとしては十分使えますが、「本番運用」を意識するなら次のような改善も検討するとよいでしょう。
メッセージ区切りの明示(長さプレフィックス or HTTP)
- 生の TCP ではメッセージ境界がないため、サーバー側が常に
</ROOT>を探す必要がある - 代わりに、アプリ独自プロトコルとして「先頭 4 バイトにメッセージ長」を付けると解析が非常に楽になる
- あるいは HTTP の
Content-Lengthを利用して、XML を HTTP POST のボディとして送る
HTTP 化してしまえば、civetweb のような組み込み Web サーバーライブラリを使う道も開けます。「MFC アプリに HTTP サーバーを内蔵して、XML API を提供する」という構成も、テストツールとしては扱いやすい選択肢です。
XML のパースとバリデーション
サンプルでは <ROOT> が含まれているかどうかだけで good="true" を返していますが、実際には次のようなチェックを行うべきです。
- XML としてパースできるか(構文エラーがないか)
- ルート要素が必ず
<ROOT>であること - 特定のタグや属性(例:
<Request ...>)が必須かどうか
Windows 限定であれば、XmlLite や MSXML(SAX モード)を使ってストリーミングに近いかたちでパースする実装も可能です。「</ROOT> を見つけたらパースを行い、内容に応じて Percent を動的に変える」といったロジックも自然に書けます。
文字コードの扱い
XML の文字コードが UTF-8 なのか Shift_JIS なのか、MFC の CString(UTF-16)との相互変換をどうするか、もあらかじめ決めておきましょう。
- クライアントにも「UTF-8 固定」「ASCII 固定」などの前提条件を共有
- 受信側では
MultiByteToWideChar()で UTF-16 へ変換 →CStringに格納 → 表示 - 返信する XML も、UTF-8 で送るならワイド文字からマルチバイトへ明示的に変換
テストツール用途であれば、まずは ASCII/UTF-8 の範囲だけを扱い、文字コードまわりは割り切ってしまうのも現実的な選択です。
複数同時接続への拡張
要件として「同時接続は 1 クライアントだけで良い」なら、今回の 1 スレッドモデルで十分です。それ以上を求める場合は:
accept()したら、接続ごとにワーカースレッドを新規作成する- あるいは IOCP(I/O Completion Port)などを使って高負荷対応のサーバーにする
といった構成が選択肢になります。MFC ダイアログベースのツールであれば、多くの場合は 1 接続ずつ順番に処理するだけで十分なことが多いでしょう。
まとめ:UI とソケット処理をきれいに分ければ怖くない
この記事で紹介した構成をざっくり振り返ると、次のようになります。
- UI スレッドとサーバースレッドをはっきり分ける
- サーバースレッドは
listen()→accept()→ 1 接続をHandleClient()で処理 HandleClient()ではstd::stringバッファに蓄積しながら</ROOT>を待つ- 受信開始から 20 秒を超えたら
good="false"を返して切断 - ワーカースレッドからのログは
PostMessage(WM_APP_LOG)で UI スレッドに渡す - 停止処理では、停止イベント +
closesocket(listen)でaccept()を解除
このパターンさえ押さえておけば、「ちょっとしたテスト用サーバー」を MFC ダイアログの中だけで実装するのはそれほど難しくありません。まずは記事のコードをベースに最小構成を動かし、そこから XML 内容に応じたビジネスロジックや、ログの保存、HTTP 化などを段階的に拡張していくと、堅牢で使いやすいテストツールに育てていけるはずです。

コメント