首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

MFC Socket 程式設計全景:從 WinSock API 到 CAsyncSocket/CSocket/CSocketFile 封裝體系

     編輯:關於VC++

前言

網路程式設計是現代應用的基石。MFC 在 WinSock API 之上建構了三層封裝體系:CAsyncSocket(薄層非阻塞)、CSocket(阻塞模擬+訊息泵)、CSocketFile(檔案介面適配),並與 CArchive 無縫整合。本文將從 WinSock 基礎、非阻塞機制、MFC 封裝層層遞進,深入剖析其設計哲學與實作細節。

一、WinSock 基礎與阻塞/非阻塞模型

API 分類

類別代表函式特性
Berkeley 相容 I/Osend, recv, connect, accept, select網路 I/O,可阻塞/非阻塞
本地端作業bind, listen, getsockopt, htonl非阻塞,即時返回
資料庫查詢gethostbyname, getservbyname阻塞,提供非同步版 WSAAsync*
Windows 擴充WSAAsyncSelect, WSAIsBlocking訊息驅動非同步通知

阻塞與非阻塞 Socket

  • 預設阻塞:socket() 建立的為阻塞 Socket,I/O 函式直到完成才返回。
  • 非阻塞轉換:ioctlsocket(s, FIONBIO, &nonZero) 或 WSAAsyncSelect(s, hWnd, msg, events) 將指定事件轉為非同步通知。
  • WSAAsyncSelect 事件:FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE。

二、CAsyncSocket:非阻塞薄封裝與訊息分發

核心資料成員

SOCKET m_hSocket;              // 底層 Socket 控制代碼
long   m_lEvent;               // 已註冊的網路事件遮罩

建立流程:Create → Socket → Attach → AsyncSelect → Bind

BOOL CAsyncSocket::Create(UINT nPort, int nType, long lEvent, LPCTSTR lpszAddr)
{
    if (Socket(nType, lEvent)) {           // 1. 建立 SOCKET & 註冊事件
        if (Bind(nPort, lpszAddr)) return TRUE; // 2. 繫結本地端口/IP
        int err = GetLastError(); Close(); WSASetLastError(err);
    }
    return FALSE;
}

BOOL CAsyncSocket::Socket(int nType, long lEvent, ...)
{
    ASSERT(m_hSocket == INVALID_SOCKET);
    m_hSocket = socket(PF_INET, nType, 0); // 建立 WinSock
    if (m_hSocket != INVALID_SOCKET) {
        AttachHandle(m_hSocket, this);     // 映射 SOCKET → this
        return AsyncSelect(lEvent);        // 註冊非同步事件
    }
    return FALSE;
}

AttachHandle:執行緒區域儲存 (TLS) 映射表

  • 模組執行緒狀態 _AFX_SOCK_THREAD_STATE 維護 CMapPtrToPtr m_mapSocketHandle。
  • 建立專用隱藏視窗 m_hSocketWindow 接收 WM_SOCKET_NOTIFY。

非同步事件回調鏈

// 隱藏視窗收到 WM_SOCKET_NOTIFY
// lParam 高字 = 錯誤碼,低字 = 網路事件
switch (WSAGETSELECTEVENT(lParam)) {
    case FD_READ:  pSocket->OnReceive(err);  break;
    case FD_WRITE: pSocket->OnSend(err);     break;
    case FD_ACCEPT: pSocket->OnAccept(err);  break;
    case FD_CONNECT: pSocket->OnConnect(err); break;
    case FD_CLOSE: pSocket->OnClose(err);    break;
    // ...
}

關鍵點:On* 為虛擬函式,使用者覆寫即可處理事件;基類預設為空實作。

三、CSocket:在非阻塞之上模擬阻塞 + 訊息泵

CSocket 繼承 CAsyncSocket,但 不註冊 FD_READ/FD_WRITE/FD_CONNECT,改由自身實作同步等待:

int CSocket::Receive(void* lpBuf, int nBufLen, int nFlags)
{
    if (m_pbBlocking) { WSASetLastError(WSAEINPROGRESS); return SOCKET_ERROR; }
    m_pbBlocking = &m_bBlocking; // 標記進入阻塞

    int nResult;
    while ((nResult = CAsyncSocket::Receive(lpBuf, nBufLen, nFlags)) == SOCKET_ERROR) {
        if (GetLastError() == WSAEWOULDBLOCK) {
            if (!PumpMessages(FD_READ)) return SOCKET_ERROR; // 進入訊息泵等待
        } else return SOCKET_ERROR;
    }
    m_pbBlocking = NULL; return nResult;
}

PumpMessages 訊息泵核心

  • 設定 m_pbBlocking 防止重入。
  • PeekMessage 循環,處理 WM_TIMER、WM_SOCKET_NOTIFY(目標事件或 FD_CLOSE)、WM_QUIT。
  • 將其他 Socket 的通知暫存至 m_listSocketNotifications,阻塞結束後重新分發。
  • 此機制確保 UI 執行緒不凍結,仍能處理繪圖、使用者輸入等訊息。

四、CSocketFile:將 Socket 包裝為 CFile 介面

設計目的:讓 CArchive 能直接在 Socket 上序列化物件。

建構與關聯

CSocketFile::CSocketFile(CSocket* pSocket, BOOL bArchiveCompatible)
    : m_pSocket(pSocket), m_bArchiveCompatible(bArchiveCompatible)
{
    ASSERT(pSocket->GetSockOpt(SO_TYPE) == SOCK_STREAM); // 僅支援流式
}

Read/Write 實作差異

UINT CSocketFile::Read(void* lpBuf, UINT nCount)
{
    if (!m_bArchiveCompatible) { // 獨立使用:必須讀滿 nCount
        while (nLeft > 0) {
            nRead = m_pSocket->Receive(pBuf, nLeft); // 阻塞直到滿足
            if (nRead <= 0) throw ...;
            nLeft -= nRead; pBuf += nRead;
        }
        return nCount;
    } else { // 搭配 CArchive:讀到多少算多少
        return m_pSocket->Receive(lpBuf, nCount, 0);
    }
}

void CSocketFile::Write(const void* lpBuf, UINT nCount)
{
    int nWritten = m_pSocket->Send(lpBuf, nCount); // 阻塞發送
    if (nWritten == SOCKET_ERROR) throw ...;
}

設計哲學:bArchiveCompatible=TRUE 時,Read 返回實際讀取位元組數,配合 CArchive 內部緩衝區管理;獨立使用時保證讀滿指定長度,符合 CFile 契約。

五、典型使用模式:CArchive over CSocketFile over CSocket

// 伺服器端
CSocket sockSrv; sockSrv.Create(8080); sockSrv.Listen();
CSocket sockClient; sockSrv.Accept(sockClient);

CSocketFile file(&sockClient);          // 包裝為檔案
CArchive arIn(&file, CArchive::load);   // 讀取封存
CArchive arOut(&file, CArchive::store); // 寫入封存

// 雙向序列化物件
arOut << pMyObject; arOut.Flush();
arIn >> pReceivedObject;

此模式完整展現 MFC 分層設計優勢:應用層只操作 CArchive,完全屏蔽網路細節。

六、常見陷阱與最佳實務

  • 阻塞操作不可巢狀:m_pbBlocking 防護,嘗試巢狀會返回 WSAEINPROGRESS。
  • 非阻塞與阻塞混用:若需同時支援,建議使用 CAsyncSocket 並自行管理事件循環,或改用執行緒池 + 阻塞 Socket。
  • CSocketFile 不支援 Seek:繼承自 CFile 但未實作 Seek/GetLength,呼叫將拋出例外。
  • Unicode/ANSI 一致性:雙端編譯選項必須一致,否則 CString 序列化會因前綴標記不符而失敗。
  • 防火牆/NAT:MFC Socket 類別不處理穿透,需應用層自行實作 STUN/UPnP 或使用中繼伺服器。

總結

MFC Socket 體系展現了經典的「適配器模式」與「裝飾器模式」結合:

  1. CAsyncSocket 適配 WinSock 非同步模型 → 事件驅動介面。
  2. CSocket 裝飾非阻塞 → 模擬同步阻塞 + UI 友善訊息泵。
  3. CSocketFile 適配 Socket → CFile 介面 → 無縫接入 CArchive 序列化管線。

理解各層職責邊界,能在效能、複雜度、維護性之間做出正確架構決策。

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