MFCダイアログでTCPサーバー実装:XMLメッセージを20秒以内に返信する方法

ダイアログベースの MFC アプリだけで、XML メッセージを受信して 20 秒以内に結果を返信する小さな TCP サーバーを作ろうとすると、UI フリーズやメッセージ境界など思った以上に落とし穴が多くなります。この記事では、ワーカースレッド方式を中心に「固まらないダイアログ」「20 秒タイムアウト」「XML 1 メッセージ単位の処理」を満たすサンプル構成を、具体的な MFC/C++ コード付きで整理していきます。

目次

ダイアログベース MFC で簡易 TCP サーバーを作る要件整理

今回のゴールは次の通りです。

  • MFC のダイアログ(CDialogEx)アプリの中だけで完結
  • クライアントから送られてくる XML を <ROOT> ... </ROOT> まで受信
  • 受信開始から 20 秒以内に以下のいずれかを返信する
&lt;ROOT&gt;&lt;Result good="true" Percent="100"/&gt;&lt;/ROOT&gt;

または

&lt;ROOT&gt;&lt;Result good="false"/&gt;&lt;/ROOT&gt;
  • 受信した 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 &lt;winsock2.h&gt;
#include &lt;ws2tcpip.h&gt;
#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), &amp;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-&gt;m_bAutoDelete = FALSE;
    m_pServerThread-&gt;ResumeThread();

    PostLog(_T("[INFO] サーバーを起動しました。\r\n"));
}

ここでは ServerThreadProc を C ランタイム互換の静的関数として定義し、その中からインスタンスメンバー ServerThreadMain() を呼び出すスタイルを採用します。

UINT __stdcall CXmlServerDlg::ServerThreadProc(LPVOID pParam)
{
    CXmlServerDlg* pThis = reinterpret_cast&lt;CXmlServerDlg*&gt;(pParam);
    return pThis-&gt;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-&gt;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*)&amp;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*)&amp;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 &amp;&amp;
            ::WaitForSingleObject(m_hStopEvent, 0) == WAIT_OBJECT_0) {
            break;
        }

        // クライアント接続待ち(ブロッキング)
        sockaddr_in clientAddr = {};
        int addrLen = sizeof(clientAddr);
        SOCKET clientSock = ::accept(m_listenSocket, (sockaddr*)&amp;clientAddr, &amp;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*)&amp;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 &amp;&amp;
            ::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&lt;std::chrono::seconds&gt;(now - t0).count();

        if (elapsedSec &gt;= 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 &lt; 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() &gt; MAX_XML_SIZE) {
            PostLog(_T("[ERROR] XML サイズ上限を超えました。\r\n"));
            break;
        }

        // </ROOT&gt; を探す
        std::string endTag = "&lt;/ROOT&gt;";
        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 の妥当性チェックを行うのが理想
        // サンプルでは簡単に &lt;ROOT&gt; が含まれているかだけを見る
        if (xml.find("&lt;ROOT&gt;") != std::string::npos) {
            success = true;
        }

        if (success) {
            response =
                "&lt;ROOT&gt;&lt;Result good=\"true\" Percent=\"100\"/&gt;&lt;/ROOT&gt;";
        } else {
            response =
                "&lt;ROOT&gt;&lt;Result good=\"false\"/&gt;&lt;/ROOT&gt;";
        }
    } else {
        // タイムアウトやエラー
        response =
            "&lt;ROOT&gt;&lt;Result good=\"false\"/&gt;&lt;/ROOT&gt;";
    }

    // レスポンス送信
    if (!response.empty()) {
        int totalSent = 0;
        while (totalSent &lt; (int)response.size()) {
            int sent = ::send(clientSock,
                              response.data() + totalSent,
                              (int)response.size() - totalSent,
                              0);
            if (sent &lt;= 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, &amp;CXmlServerDlg::OnAppLog)
END_MESSAGE_MAP()

LRESULT CXmlServerDlg::OnAppLog(WPARAM wParam, LPARAM lParam)
{
    CString* pStr = reinterpret_cast&lt;CString*&gt;(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> だけ」を相手にするため、以下のような流れにします。

  1. buffer に recv() 結果を append
  2. buffer.find("</ROOT>") で終了タグを探す
  3. 見つかった位置までを 1 メッセージとして取り出す
  4. 残り(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 方式のざっくり構成

メッセージ駆動方式では、おおまかに次のような手順になります。

  1. UI スレッド内で socket(), bind(), listen() を実行
  2. WSAAsyncSelect(m_listenSocket, m_hWnd, WM_APP_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE) を設定
  3. OnSocketNotify(WPARAM, LPARAM) のようなメッセージハンドラを作り、FD_ACCEPT / FD_READ / FD_CLOSE ごとに処理を分岐
  4. FD_READ が来るたびに recv() → バッファ蓄積 → </ROOT> 検出
  5. 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 化などを段階的に拡張していくと、堅牢で使いやすいテストツールに育てていけるはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次