首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計疑難解答

MFC UDP 聊天程序多線程實現:用 AfxBeginThread 循環接收數據,移除接收按鈕實現自動監聽

     編輯:程式設計疑難解答

核心問題

單線程 MFC 應用中,`while` 循環接收 UDP 數據會阻塞 UI 線程導致界面凍結 (「程序會死掉」)。需創建工作線程持續監聽,主線程僅負責發送與界面更新。

MFC 多線程架構設計

+------------------+          +------------------------+
|   UI 主線程      |          |   工作線程             |
|  (CWinApp/CView) |          |  (AfxBeginThread)      |
|                  |          |                        |
| - 發送按鈕       |  共享    | - socket (全局/成員)   |
| - 顯示列表       | <------> | - 循環 recvfrom()      |
| - 發送邏輯       |  數據/   | - PostMessage 通知 UI  |
| - 啟動線程       |  同步    | - 退出標誌控制         |
+------------------+          +------------------------+

關鍵代碼實現

1. 類成員變量聲明

// ChatDlg.h
class CChatDlg : public CDialogEx {
    // ...
protected:
    SOCKET m_hSocket;                // UDP Socket
    CWinThread* m_pRecvThread;       // 接收線程指針
    BOOL m_bStopRecv;                // 線程退出標誌
    CRITICAL_SECTION m_csSocket;     // Socket 同步 (可選)
    // UI 控件
    CListBox m_lstMessages;
    CEdit m_edtSend;
    // ...
};

2. 初始化 Socket 與啟動線程 (OnInitDialog / 連接按鈕)

BOOL CChatDlg::OnInitDialog() {
    // ... 初始化 UI ...
    
    // 初始化 Winsock (僅一次)
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
        AfxMessageBox(_T("Winsock 初始化失敗"));
        return FALSE;
    }
    
    // 創建 UDP Socket
    m_hSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (m_hSocket == INVALID_SOCKET) { /* 錯誤處理 */ }
    
    // 綁定本地端口
    sockaddr_in localAddr = {0};
    localAddr.sin_family = AF_INET;
    localAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    localAddr.sin_port = htons(LOCAL_PORT); // 如 8000
    bind(m_hSocket, (sockaddr*)&localAddr, sizeof(localAddr));
    
    // 初始化同步對象
    InitializeCriticalSection(&m_csSocket);
    m_bStopRecv = FALSE;
    
    // 啟動接收線程
    m_pRecvThread = AfxBeginThread(RecvThreadProc, this);
    if (!m_pRecvThread) { /* 錯誤處理 */ }
    
    return TRUE;
}

3. 接收線程函數 (靜態/全局)

UINT __cdecl RecvThreadProc(LPVOID pParam) {
    CChatDlg* pDlg = static_cast(pParam);
    char buffer[1024];
    sockaddr_in fromAddr;
    int fromLen = sizeof(fromAddr);
    
    while (!pDlg->m_bStopRecv) {
        // 非阻塞或 select 輪詢避免長期阻塞無法退出
        fd_set readSet;
        FD_ZERO(&readSet);
        FD_SET(pDlg->m_hSocket, &readSet);
        timeval timeout = {0, 100000}; // 100ms 超時
        
        int ret = select(0, &readSet, NULL, NULL, &timeout);
        if (ret == SOCKET_ERROR) break;
        if (ret == 0) continue; // 超時,檢查 m_bStopRecv
        
        // 有數據到達
        int nRecv = recvfrom(pDlg->m_hSocket, buffer, sizeof(buffer)-1, 0,
                             (sockaddr*)&fromAddr, &fromLen);
        if (nRecv > 0) {
            buffer[nRecv] = '\0';
            CString msg;
            msg.Format(_T("[%s:%d] %s"), 
                       inet_ntoa(fromAddr.sin_addr), ntohs(fromAddr.sin_port),
                       CA2W(buffer, CP_UTF8)); // 注意編碼轉換
            
            // 關鍵:PostMessage 通知 UI 線程更新 (線程安全)
            pDlg->PostMessage(WM_USER_RECV_MSG, 0, (LPARAM)new CString(msg));
        }
    }
    return 0;
}

4. UI 線程處理自定義消息更新界面

// 在消息映射中添加
ON_MESSAGE(WM_USER_RECV_MSG, OnRecvMsg)

// 定義消息 ID
#define WM_USER_RECV_MSG (WM_USER + 100)

// 處理函數
LRESULT CChatDlg::OnRecvMsg(WPARAM wParam, LPARAM lParam) {
    CString* pMsg = (CString*)lParam;
    if (pMsg) {
        m_lstMessages.AddString(*pMsg);
        delete pMsg; // 釋放堆內存
    }
    return 0;
}

5. 發送按鈕處理 (主線程)

void CChatDlg::OnBnClickedBtnSend() {
    CString strText;
    m_edtSend.GetWindowText(strText);
    if (strText.IsEmpty()) return;
    
    sockaddr_in toAddr = {0};
    toAddr.sin_family = AF_INET;
    toAddr.sin_addr.s_addr = inet_addr(TARGET_IP); // 目標 IP
    toAddr.sin_port = htons(TARGET_PORT);
    
    sendto(m_hSocket, CW2A(strText, CP_UTF8), strText.GetLength(), 0,
           (sockaddr*)&toAddr, sizeof(toAddr));
    
    m_edtSend.SetWindowText(_T(""));
    // 可選:本地回顯
    m_lstMessages.AddString(_T("[Me] ") + strText);
}

6. 程序退出清理

void CChatDlg::OnDestroy() {
    CDialogEx::OnDestroy();
    m_bStopRecv = TRUE;           // 通知線程退出
    if (m_pRecvThread) {
        WaitForSingleObject(m_pRecvThread->m_hThread, 2000); // 等待線程結束
    }
    DeleteCriticalSection(&m_csSocket);
    closesocket(m_hSocket);
    WSACleanup();
}

關鍵點總結

  1. UI 線程不做阻塞操作:接收循環放入 `AfxBeginThread` 創建的工作線程。
  2. select + 超時實現可中斷阻塞:純 `recvfrom` 無法優雅退出,`select` 定期檢查退出標誌。
  3. 線程間通信用 PostMessage:工作線程**絕不**直接操作 UI 控件,通過 `PostMessage` 投遞消息讓 UI 線程更新。
  4. 移除「接收按鈕」:線程啟動後自動持續監聽,無需用戶干預。
  5. 編碼統一:網絡傳輸建議統一 UTF-8,MFC 使用 `CA2W`/`CW2A` 配合 `CP_UTF8` 轉換。

參考資源

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved