Windows Socket API (Winsock) 是 C++ 網絡編程基石。本文系統總結 TCP 流式套接字開發的標準流程、核心 API 調用序列、錯誤處理要點,並提供可編譯運行的完整服務端/客戶端參考代碼,適合快速上手與面試複習。
| 類型 | 宏 | 協議 | 特點 |
|---|---|---|---|
| 流式套接字 | SOCK_STREAM | TCP | 面向連接、可靠、有序、字節流、無邊界 |
| 數據報套接字 | SOCK_DGRAM | UDP | 無連接、不可靠、無序、有邊界、支援廣播/多播 |
| 原始套接字 | SOCK_RAW | IP/ICMP/自定義 | 直接訪問網絡層,需管理員權限 |
本文專注 SOCK_STREAM (TCP)。
WSAStartup() → socket(AF_INET, SOCK_STREAM, 0)bind(sock, (sockaddr*)&addr, sizeof(addr))listen(sock, backlog)(backlog 通常 5~1024)accept(sock, &clientAddr, &len) → 返回**新套接字**用於通信send(newSock, ...) / recv(newSock, ...)closesocket() → WSACleanup()connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr))send/recv
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) { /* 錯誤處理 */ }
// ...
WSACleanup();
sockaddr_in addr;
ZeroMemory(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 主機序轉網絡序
addr.sin_addr.s_addr = htonl(INADDR_ANY); // 服務端綁定所有網卡
// 客戶端連接時:addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// 發送:返回實際發送字節數,可能 < 請求長度(需循環發送)
int send(SOCKET s, const char* buf, int len, int flags);
// 接收:返回實際接收字節數,0 表示對端關閉,SOCKET_ERROR 表示錯誤
int recv(SOCKET s, char* buf, int len, int flags);
關鍵:TCP 是字節流,**無消息邊界**,應用層必須自行實現分幀(長度前綴、分隔符、固定長度等)。
Server.cpp)
#include
#include
#pragma comment(lib, "ws2_32.lib")
int main() {
// 1. 初始化
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) {
printf("WSAStartup failed\n"); return -1;
}
// 2. 創建監聽套接字
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (listenSock == INVALID_SOCKET) { /* 錯誤 */ }
// 3. 綁定
sockaddr_in serverAddr;
ZeroMemory(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(6000);
serverAddr.sin_addr.s_addr = inet_addr("192.168.245.221"); // 或 INADDR_ANY
if (bind(listenSock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { /* 錯誤 */ }
// 4. 監聽
if (listen(listenSock, 5) == SOCKET_ERROR) { /* 錯誤 */ }
printf("Server listening on port 6000...\n");
// 5. 循環接受
while (true) {
sockaddr_in clientAddr;
int addrLen = sizeof(clientAddr);
SOCKET clientSock = accept(listenSock, (sockaddr*)&clientAddr, &addrLen);
if (clientSock == INVALID_SOCKET) continue;
printf("Client connected: %s\n", inet_ntoa(clientAddr.sin_addr));
// 發送歡迎消息
char sendBuf[100];
sprintf(sendBuf, "Welcome %s to server", inet_ntoa(clientAddr.sin_addr));
send(clientSock, sendBuf, strlen(sendBuf)+1, 0);
// 接收回覆
char recvBuf[100];
int ret = recv(clientSock, recvBuf, sizeof(recvBuf)-1, 0);
if (ret > 0) {
recvBuf[ret] = '\0';
printf("Received: %s\n", recvBuf);
}
closesocket(clientSock); // 關閉客戶端套接字
}
closesocket(listenSock);
WSACleanup();
return 0;
}
Client.cpp)
#include
#include
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2,2), &wsaData) != 0) return -1;
SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serverAddr;
ZeroMemory(&serverAddr, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(6000);
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服務端 IP
if (connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("Connect failed\n");
closesocket(sock);
WSACleanup();
return -1;
}
// 接收歡迎消息
char recvBuf[100];
int ret = recv(sock, recvBuf, sizeof(recvBuf)-1, 0);
if (ret > 0) {
recvBuf[ret] = '\0';
printf("Server: %s\n", recvBuf);
}
// 發送回覆
const char* msg = "Hello, this is client";
send(sock, msg, strlen(msg)+1, 0);
closesocket(sock);
WSACleanup();
return 0;
}
select/WSAAsyncSelect/IOCP (完成端口),避免線程阻塞。WSAGetLastError() 獲取具體錯誤(如 WSAECONNRESET 連接重置)。setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, ...) 避免 TIME_WAIT 導致重啟失敗。htons/htonl (主機→網絡),ntohs/ntohl (網絡→主機)。closesocket 和 WSACleanup,推薦 RAII 封裝。connect 超時或 accept 無反應。<sys/socket.h>、<netinet/in.h>、<arpa/inet.h>、<unistd.h> (close)。SOCKET 類型 (Windows 為 SOCKET_PTR,Linux 為 int)、INVALID_SOCKET (-1)、closesocket → close、WSAGetLastError → errno。