網路程式設計是現代應用的基石。MFC 在 WinSock API 之上建構了三層封裝體系:CAsyncSocket(薄層非阻塞)、CSocket(阻塞模擬+訊息泵)、CSocketFile(檔案介面適配),並與 CArchive 無縫整合。本文將從 WinSock 基礎、非阻塞機制、MFC 封裝層層遞進,深入剖析其設計哲學與實作細節。
| 類別 | 代表函式 | 特性 |
|---|---|---|
| Berkeley 相容 I/O | send, recv, connect, accept, select | 網路 I/O,可阻塞/非阻塞 |
| 本地端作業 | bind, listen, getsockopt, htonl | 非阻塞,即時返回 |
| 資料庫查詢 | gethostbyname, getservbyname | 阻塞,提供非同步版 WSAAsync* |
| Windows 擴充 | WSAAsyncSelect, WSAIsBlocking | 訊息驅動非同步通知 |
socket() 建立的為阻塞 Socket,I/O 函式直到完成才返回。ioctlsocket(s, FIONBIO, &nonZero) 或 WSAAsyncSelect(s, hWnd, msg, events) 將指定事件轉為非同步通知。FD_READ | FD_WRITE | FD_OOB | FD_ACCEPT | FD_CONNECT | FD_CLOSE。SOCKET m_hSocket; // 底層 Socket 控制代碼
long m_lEvent; // 已註冊的網路事件遮罩
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;
}
_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 繼承 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;
}
m_pbBlocking 防止重入。PeekMessage 循環,處理 WM_TIMER、WM_SOCKET_NOTIFY(目標事件或 FD_CLOSE)、WM_QUIT。m_listSocketNotifications,阻塞結束後重新分發。設計目的:讓 CArchive 能直接在 Socket 上序列化物件。
CSocketFile::CSocketFile(CSocket* pSocket, BOOL bArchiveCompatible)
: m_pSocket(pSocket), m_bArchiveCompatible(bArchiveCompatible)
{
ASSERT(pSocket->GetSockOpt(SO_TYPE) == SOCK_STREAM); // 僅支援流式
}
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 契約。
// 伺服器端
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。CFile 但未實作 Seek/GetLength,呼叫將拋出例外。CString 序列化會因前綴標記不符而失敗。MFC Socket 體系展現了經典的「適配器模式」與「裝飾器模式」結合:
CAsyncSocket 適配 WinSock 非同步模型 → 事件驅動介面。CSocket 裝飾非阻塞 → 模擬同步阻塞 + UI 友善訊息泵。CSocketFile 適配 Socket → CFile 介面 → 無縫接入 CArchive 序列化管線。理解各層職責邊界,能在效能、複雜度、維護性之間做出正確架構決策。