:C/S架構(gòu)與消息驅(qū)動Socket編程)
簡介這份資源面向Windows平臺網(wǎng)絡編程初學者與C開發(fā)者聚焦基于TCP協(xié)議的C/S架構(gòu)通信實現(xiàn)幫助讀者理解Socket套接字在MFC框架下的封裝與調(diào)用方式。壓縮包共4個文件包含2個cpp源文件與2個h頭文件分別對應客戶端連接對話框與服務器端對話框的核心實現(xiàn)整體約6KB體積輕量便于快速閱讀與移植。資源圍繞CSocket類展開涉及Winsock初始化、套接字創(chuàng)建、綁定監(jiān)聽、接受連接、連接服務器以及數(shù)據(jù)收發(fā)與關(guān)閉等關(guān)鍵環(huán)節(jié)可作為學習MFC網(wǎng)絡編程的入門范例。目前已有1260人學習下載適合希望掌握TCP通信流程、理解客戶端與服務器交互邏輯的開發(fā)者參考也可作為課程實驗或小型項目的基礎(chǔ)代碼骨架便于在此基礎(chǔ)上擴展業(yè)務功能。1. 從零拆一個 MFC TCP 通信程序C/S 架構(gòu)到底能解決什么問題很多人第一次接觸 socket 網(wǎng)絡編程是在控制臺里敲socket()、bind()、listen()那一套跑通了卻不知道怎么套到 Windows 窗口程序里。這份資源給的是一個基于 socket 通信、用 MFC 實現(xiàn) TCP 通信的 C/S 架構(gòu)程序說白了就是把 TCP 服務端和客戶端都塞進 MFC 的對話框框架里用 Windows 消息機制驅(qū)動收發(fā)而不是靠while(1)死循環(huán)硬扛。它適合兩類人一類是學過 TCP 三次握手、知道 TCP 和 UDP 區(qū)別但沒在 MFC 里真正落地過的學生或轉(zhuǎn)崗工程師另一類是要給工控上位機、設備調(diào)試工具做通信模塊的從業(yè)者需要一個能直接改、能編譯、能抓包驗證的骨架。C/S 架構(gòu)在這里不是概念而是服務端先listen等連接、客戶端connect發(fā)起握手、雙方各自維護一個SOCKET句柄并靠OnReceive回調(diào)收數(shù)據(jù)的完整閉環(huán)。下面按「資源是什么 → 怎么用 → 坑在哪」的順序拆開講。2. MFC 與 Socket 的接法為什么不用阻塞循環(huán)而用消息驅(qū)動2.1 選型理由MFC 的 CAsyncSocket 與原生 Winsock 怎么選MFC 里做 TCP 通信有兩條路。一條是直接用 Winsock 的socket、connect、send、recv另一條是用 MFC 封裝好的CAsyncSocket和CSocket。這份資源走的是 MFC 封裝路線原因很實際MFC 的窗口線程本身要跑消息循環(huán)如果你在 UI 線程里寫一個阻塞的recv窗口立刻卡死點關(guān)閉都沒反應。CAsyncSocket把 socket 句柄和窗口消息綁定底層用WSAAsyncSelect注冊網(wǎng)絡事件收到數(shù)據(jù)時投遞一個自定義消息由OnReceive虛函數(shù)處理UI 線程該刷新刷新、該響應響應。常見做法是服務端用CAsyncSocket派生一個CListenSocket負責Listen和OnAccept再派生一個CClientSocket負責每個連接的OnReceive、OnClose客戶端直接派生一個CClientSocket調(diào)Connect。這樣每個連接是一個對象生命周期清晰不會出現(xiàn)句柄滿天飛的情況。提示CAsyncSocket的回調(diào)是在窗口線程里執(zhí)行的OnReceive里不要做耗時操作否則照樣卡 UI。要處理大塊數(shù)據(jù)先把數(shù)據(jù)拷到緩沖區(qū)再投遞到工作線程。2.2 服務端搭建從 Listen 到 OnAccept 的完整步驟服務端核心是「監(jiān)聽 socket 只負責接受連接通信 socket 每個連接一個」。下面是一個可抄的骨架基于對話框程序在InitDialog里啟動監(jiān)聽。// ListenSocket.h class CListenSocket : public CAsyncSocket { public: CListenSocket() {} virtual ~CListenSocket() {} virtual void OnAccept(int nErrorCode); }; // ListenSocket.cpp void CListenSocket::OnAccept(int nErrorCode) { if (nErrorCode 0) { // 有新連接進來創(chuàng)建一個通信 socket CClientSocket* pClient new CClientSocket(); // Accept 把新連接的句柄交給 pClient if (Accept(*pClient)) { // 把 pClient 存進列表方便后續(xù)廣播或管理 theApp.m_clientList.AddTail(pClient); } else { delete pClient; } } CAsyncSocket::OnAccept(nErrorCode); }邏輯說明OnAccept是CAsyncSocket在監(jiān)聽 socket 收到連接請求時自動調(diào)用的虛函數(shù)。Accept的參數(shù)是一個空的CClientSocket對象調(diào)用成功后這個對象就持有了新連接的 socket 句柄后續(xù)它的OnReceive會被觸發(fā)。參數(shù)nErrorCode為 0 表示正常非 0 要查WSAGetLastError。啟動監(jiān)聽的代碼放在對話框初始化里// 在對話框 OnInitDialog 中 if (!m_listenSocket.Create(8888, SOCK_STREAM)) { AfxMessageBox(_T(監(jiān)聽端口創(chuàng)建失敗)); return FALSE; } if (!m_listenSocket.Listen(5)) { AfxMessageBox(_T(Listen 失敗)); return FALSE; }Create的第二個參數(shù)SOCK_STREAM指定 TCP端口 8888 可以換成任意未占用端口。Listen(5)里的 5 是等待隊列長度不是最大連接數(shù)別搞混。常見做法是設 5 到 10太大沒意義太小高并發(fā)時會丟連接。2.3 客戶端連接與收發(fā)Connect 和 OnReceive 的配合客戶端比服務端少一個監(jiān)聽環(huán)節(jié)直接Create再Connect。注意Connect是異步的返回FALSE且GetLastError為WSAEWOULDBLOCK是正常的真正的連接結(jié)果在OnConnect里判斷。// ClientSocket.cpp void CClientSocket::OnConnect(int nErrorCode) { if (nErrorCode 0) { // 連接成功可以發(fā)數(shù)據(jù)了 CString strMsg _T(hello server); Send(strMsg, strMsg.GetLength() * sizeof(TCHAR)); } else { AfxMessageBox(_T(連接服務端失敗)); } CAsyncSocket::OnConnect(nErrorCode); } void CClientSocket::OnReceive(int nErrorCode) { if (nErrorCode 0) { char buf[4096] {0}; int nRead Receive(buf, sizeof(buf) - 1); if (nRead 0) { buf[nRead] \0; // 把收到的數(shù)據(jù)顯示到界面注意跨線程問題 theApp.m_pMainDlg-AppendText(CString(buf)); } else if (nRead 0) { // 對端正常關(guān)閉 Close(); } } CAsyncSocket::OnReceive(nErrorCode); }Receive返回 0 表示對端關(guān)閉了連接返回SOCKET_ERROR要查錯誤碼WSAEWOULDBLOCK表示當前沒數(shù)據(jù)可讀不是錯誤。發(fā)送時Send的字節(jié)數(shù)要按實際數(shù)據(jù)算發(fā)CString時用GetLength() * sizeof(TCHAR)在 Unicode 工程里TCHAR是wchar_t占 2 字節(jié)這個點后面避坑章節(jié)還會提。3. 編譯環(huán)境與工程配置讓 MFC 和 Winsock 真正跑起來3.1 Visual Studio 里 MFC 組件的安裝與工程創(chuàng)建MFC 不是默認安裝的。Visual Studio 安裝器里要勾「使用 C 的桌面開發(fā)」右側(cè)「安裝詳細信息」里再勾「適用于最新 v143 生成工具的 C MFC」。離線環(huán)境的話安裝器支持下載后離線布局但這一步按官方安裝器走就行不展開。創(chuàng)建工程時選「MFC 應用」應用程序類型選「基于對話框」項目名隨意。生成后你會看到App、Dlg、stdafx或pch這幾個核心文件。MFC 四大類在這里對應CWinApp是應用對象CDialogEx是主窗口CAsyncSocket是網(wǎng)絡對象CString是字符串封裝。工程屬性里不需要額外鏈接ws2_32.lib因為 MFC 的 socket 類已經(jīng)處理了但如果你混用原生 Winsock API就要在鏈接器輸入里加ws2_32.lib。3.2 端口、IP 與防火墻三個必須確認的參數(shù)參數(shù)位置常見值說明端口號Create第一個參數(shù)88881024 以下需管理員權(quán)限建議用 8000 以上IP 地址Connect第一個參數(shù)127.0.0.1 或局域網(wǎng) IP本機測試用回環(huán)跨機測試用實際網(wǎng)卡 IP等待隊列Listen參數(shù)5不是最大連接數(shù)是未 accept 的排隊上限防火墻是新手最容易翻車的地方。服務端Listen成功后如果客戶端在另一臺機器上連不上先關(guān)掉 Windows Defender 防火墻的對應入站規(guī)則測試確認是防火墻問題后再加規(guī)則放行端口。本機127.0.0.1測試不受防火墻影響所以先用回環(huán)驗證代碼邏輯再上局域網(wǎng)。3.3 用 TCP 調(diào)試助手做交叉驗證不要只信自己的客戶端和服務端。常見做法是服務端跑起來后用 TCP 調(diào)試助手網(wǎng)上搜「tcp調(diào)試助手1.17」這類工具當客戶端連上去手動發(fā)幾條數(shù)據(jù)看服務端OnReceive有沒有正確解析。反過來用調(diào)試助手開一個 TCP 服務端讓你的 MFC 客戶端去連驗證Connect和Send是否正常。這樣能把「代碼問題」和「環(huán)境問題」分開。調(diào)試助手發(fā)十六進制和 ASCII 都支持測粘包時特別有用。4. 避坑與排查MFC TCP 通信里最容易翻車的五件事4.1 現(xiàn)象客戶端連不上錯誤碼 10061原因服務端沒Listen成功或者端口被占用或者防火墻攔了。10061是WSAECONNREFUSED意思是目標端口沒有程序在監(jiān)聽。解決先在服務端機器上netstat -ano | findstr 8888看端口有沒有處于LISTENING。如果沒有檢查Create和Listen的返回值別把AfxMessageBox彈窗當裝飾。端口被占用就換一個??鐧C測試先臨時關(guān)防火墻確認。4.2 現(xiàn)象OnReceive 收到的數(shù)據(jù)少一截或亂碼原因TCP 是字節(jié)流沒有消息邊界。你發(fā)兩次對面可能一次Receive全收到也可能分兩次收到。另外 Unicode 工程里CString發(fā)出去是寬字符對面按char收就會亂。解決自定義一個簡單的包頭比如前 4 字節(jié)寫長度后面跟實際數(shù)據(jù)。收的時候先收 4 字節(jié)解析出長度再循環(huán)收夠為止。或者統(tǒng)一用char數(shù)組和sizeof(char)發(fā)送界面顯示時再轉(zhuǎn)CString。粘包處理是 TCP 編程的必修課別指望一次Receive就是一條完整消息。4.3 現(xiàn)象關(guān)閉窗口時程序崩潰或報內(nèi)存泄漏原因CClientSocket對象是new出來的連接關(guān)閉后沒有delete。MFC 的CAsyncSocket析構(gòu)時會關(guān)閉句柄但對象本身要你管理。解決在OnClose里把對應的 socket 對象從列表移除并delete或者用CArray、CList配合智能指針。調(diào)試時看輸出窗口的「Detected memory leaks」提示定位是哪個對象沒釋放。4.4 現(xiàn)象OnReceive 里更新界面沒反應或直接崩原因如果你把 socket 放到工作線程里OnReceive就在工作線程執(zhí)行直接調(diào)SetWindowText或操作控件會跨線程訪問 UI輕則無效重則崩潰。解決用PostMessage把數(shù)據(jù)指針投遞到主窗口在主窗口的自定義消息處理函數(shù)里更新界面。PostMessage是異步的不會阻塞工作線程。數(shù)據(jù)指針要用new分配接收方負責delete。4.5 現(xiàn)象編譯報錯「無法打開包括文件 afxsock.h」原因工程沒有啟用 MFC或者 MFC 組件沒裝全。解決項目屬性 → 常規(guī) → 「使用 MFC」設為「在共享 DLL 中使用 MFC」或「在靜態(tài)庫中使用 MFC」。如果選項是灰的說明 MFC 組件沒裝回安裝器勾上。頭文件包含順序也有講究afxsock.h要放在afxwin.h之后。5. 進階技巧把通信模塊從界面里剝出來寫到這兒程序能跑但代碼全堆在對話框里加個功能就牽一發(fā)動全身。我一般會做一件事把 socket 通信封裝成一個獨立的CNetManager類對話框只負責調(diào)StartServer、ConnectServer、SendData和接收回調(diào)不直接碰CAsyncSocket。這樣換界面、加協(xié)議、上多線程都不用動網(wǎng)絡層。具體做法是定義一個回調(diào)接口// 網(wǎng)絡事件回調(diào)接口 class INetCallback { public: virtual void OnNetRecv(const char* data, int len) 0; virtual void OnNetConnect() 0; virtual void OnNetClose() 0; }; // CNetManager 持有 CAsyncSocket 派生對象把事件轉(zhuǎn)發(fā)給 callback class CNetManager { public: void SetCallback(INetCallback* cb) { m_cb cb; } BOOL StartServer(UINT port); BOOL ConnectTo(LPCTSTR ip, UINT port); int SendData(const char* data, int len); private: INetCallback* m_cb nullptr; CListenSocket m_listen; CClientSocket m_client; };對話框?qū)崿F(xiàn)INetCallback在OnNetRecv里把數(shù)據(jù)轉(zhuǎn)成CString顯示。這樣網(wǎng)絡層和界面層解耦測試的時候可以寫一個控制臺程序?qū)崿F(xiàn)同樣的接口不用開窗口就能壓測收發(fā)。驗證方法上我習慣用兩個指標一是連續(xù)發(fā) 10000 條 1KB 數(shù)據(jù)看有沒有丟包和錯序TCP 本身保證不丟不亂但你的解析邏輯可能出錯二是用任務管理器看句柄數(shù)反復連接斷開 100 次句柄數(shù)應該回到初始值附近如果一直漲說明 socket 對象沒釋放干凈。注意CAsyncSocket在 MFC 里不是線程安全的一個 socket 對象只能在一個線程里用。多線程方案要么每個線程獨立 socket要么用CSocket配合工作線程但CSocket的阻塞特性又和 UI 線程沖突選型時要先想清楚。從那以后我每次寫 MFC 網(wǎng)絡程序都強制先把「監(jiān)聽、連接、收發(fā)、關(guān)閉」四個動作的返回值檢查一遍再談界面。希望幫到你。本文還有配套的精品資源點擊獲取