單線程 MFC 應用中,`while` 循環接收 UDP 數據會阻塞 UI 線程導致界面凍結 (「程序會死掉」)。需創建工作線程持續監聽,主線程僅負責發送與界面更新。
+------------------+ +------------------------+ | UI 主線程 | | 工作線程 | | (CWinApp/CView) | | (AfxBeginThread) | | | | | | - 發送按鈕 | 共享 | - socket (全局/成員) | | - 顯示列表 | <------> | - 循環 recvfrom() | | - 發送邏輯 | 數據/ | - PostMessage 通知 UI | | - 啟動線程 | 同步 | - 退出標誌控制 | +------------------+ +------------------------+
// 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;
// ...
};
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;
}
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;
}
// 在消息映射中添加
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;
}
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);
}
void CChatDlg::OnDestroy() {
CDialogEx::OnDestroy();
m_bStopRecv = TRUE; // 通知線程退出
if (m_pRecvThread) {
WaitForSingleObject(m_pRecvThread->m_hThread, 2000); // 等待線程結束
}
DeleteCriticalSection(&m_csSocket);
closesocket(m_hSocket);
WSACleanup();
}