1. 网络编程概述
网络编程是指编写应用程序,使其能够通过网络与其他计算机上的进程进行通信。在 Windows 平台上,最核心的网络编程接口是 Windows Sockets (Winsock),它基于 Berkeley Sockets 规范,并扩展了 Windows 特有的异步模型。
网络编程通常涉及两种主要的传输层协议:
-
TCP (Transmission Control Protocol):面向连接、可靠、基于字节流的协议。适用于需要可靠数据传输的场景,如 HTTP、FTP、邮件等。
-
UDP (User Datagram Protocol):无连接、不可靠、基于数据报的协议。适用于实时性要求高、可容忍丢包的场景,如视频通话、在线游戏、DNS 查询等。
2. Winsock 基础
Winsock 提供了一套 API,用于创建和管理网络连接。使用 Winsock 编程通常需要以下步骤:
2.1 初始化 Winsock
任何使用 Winsock 的程序都必须先调用 WSAStartup 初始化库,指定所需的 Winsock 版本(通常为 2.2)。程序结束时调用 WSACleanup 清理。
#include <winsock2.h>
#include <ws2tcpip.h> // 包含 getaddrinfo 等现代函数
#pragma comment(lib, "ws2_32.lib") // 链接 Winsock 库
int main() {
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\\n", iResult);
return 1;
}
// … 网络操作 …
WSACleanup();
return 0;
}
2.2 套接字创建
socket 函数创建一个套接字描述符。
SOCKET sock = socket(AF_INET, // 地址族:IPv4
SOCK_STREAM, // 类型:TCP流式套接字
0); // 协议:自动选择
if (sock == INVALID_SOCKET) {
printf("socket failed: %d\\n", WSAGetLastError());
}
-
AF_INET:IPv4;AF_INET6:IPv6。
-
SOCK_STREAM:TCP;SOCK_DGRAM:UDP。
-
对于 TCP,第三个参数可设为 0,系统自动选择 IPPROTO_TCP。
2.3 地址结构
在 IPv4 中,通常使用 sockaddr_in 结构指定地址和端口。现代编程推荐使用 getaddrinfo 函数,它独立于协议,能正确处理 IPv4/IPv6。
struct addrinfo hints, *result;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET; // 或 AF_UNSPEC 表示任意
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_protocol = IPPROTO_TCP;
int iResult = getaddrinfo("127.0.0.1", "8080", &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed: %d\\n", iResult);
WSACleanup();
return 1;
}
// 使用 result 中的地址信息…
freeaddrinfo(result);
2.4 错误处理
Winsock 函数失败时通常返回 SOCKET_ERROR 或 INVALID_SOCKET,具体错误码通过 WSAGetLastError() 获取。常见错误码如 WSAEWOULDBLOCK(操作会阻塞但套接字为非阻塞模式)、WSAECONNRESET(连接被重置)等。
3. TCP 编程模型
3.1 TCP 服务器端流程
创建套接字(socket)。
绑定地址和端口(bind)。
监听连接(listen)。
接受客户端连接(accept)。
与客户端收发数据(send / recv)。
关闭套接字(closesocket)。
示例:简单的 TCP 服务器
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
// 创建套接字
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (listenSock == INVALID_SOCKET) {
printf("socket failed: %d\\n", WSAGetLastError());
return 1;
}
// 绑定地址
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有接口
serverAddr.sin_port = htons(8888); // 端口
if (bind(listenSock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("bind failed: %d\\n", WSAGetLastError());
closesocket(listenSock);
return 1;
}
// 监听
if (listen(listenSock, SOMAXCONN) == SOCKET_ERROR) {
printf("listen failed: %d\\n", WSAGetLastError());
closesocket(listenSock);
return 1;
}
printf("Server listening on port 8888…\\n");
// 接受连接
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
SOCKET clientSock = accept(listenSock, (struct sockaddr*)&clientAddr, &clientAddrLen);
if (clientSock == INVALID_SOCKET) {
printf("accept failed: %d\\n", WSAGetLastError());
closesocket(listenSock);
return 1;
}
printf("Client connected!\\n");
// 接收数据
char recvbuf[512];
int recvResult = recv(clientSock, recvbuf, sizeof(recvbuf) – 1, 0);
if (recvResult > 0) {
recvbuf[recvResult] = '\\0';
printf("Received: %s\\n", recvbuf);
// 发送响应
const char* response = "Hello from server!";
send(clientSock, response, strlen(response), 0);
}
// 清理
closesocket(clientSock);
closesocket(listenSock);
WSACleanup();
return 0;
}
3.2 TCP 客户端流程
创建套接字。
连接服务器(connect)。
收发数据。
关闭套接字。
示例:简单的 TCP 客户端
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")
int main() {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET clientSock = socket(AF_INET, SOCK_STREAM, 0);
if (clientSock == INVALID_SOCKET) {
printf("socket failed: %d\\n", WSAGetLastError());
return 1;
}
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
serverAddr.sin_port = htons(8888);
if (connect(clientSock, (struct sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("connect failed: %d\\n", WSAGetLastError());
closesocket(clientSock);
return 1;
}
printf("Connected to server!\\n");
// 发送数据
const char* message = "Hello from client!";
send(clientSock, message, strlen(message), 0);
// 接收响应
char recvbuf[512];
int recvResult = recv(clientSock, recvbuf, sizeof(recvbuf) – 1, 0);
if (recvResult > 0) {
recvbuf[recvResult] = '\\0';
printf("Server response: %s\\n", recvbuf);
}
closesocket(clientSock);
WSACleanup();
return 0;
}
4. UDP 编程模型
UDP 无需连接,直接通过 sendto 和 recvfrom 发送/接收数据报。
4.1 UDP 服务器
SOCKET udpSock = socket(AF_INET, SOCK_DGRAM, 0);
// 绑定地址(同 TCP)
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(8888);
bind(udpSock, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
char recvbuf[512];
struct sockaddr_in clientAddr;
int clientAddrLen = sizeof(clientAddr);
int recvLen = recvfrom(udpSock, recvbuf, sizeof(recvbuf), 0,
(struct sockaddr*)&clientAddr, &clientAddrLen);
if (recvLen > 0) {
recvbuf[recvLen] = '\\0';
printf("Received: %s\\n", recvbuf);
// 回复
sendto(udpSock, "ACK", 3, 0, (struct sockaddr*)&clientAddr, clientAddrLen);
}
4.2 UDP 客户端
SOCKET udpSock = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
serverAddr.sin_port = htons(8888);
sendto(udpSock, "Hello", 5, 0, (struct sockaddr*)&serverAddr, sizeof(serverAddr));
char recvbuf[512];
int fromLen = sizeof(serverAddr);
int recvLen = recvfrom(udpSock, recvbuf, sizeof(recvbuf), 0,
(struct sockaddr*)&serverAddr, &fromLen);
5. 阻塞与非阻塞模式
默认创建的套接字是阻塞模式:recv、send、accept、connect 等操作会阻塞线程直到完成。在需要并发处理多个连接时,通常需要:
-
多线程:每个客户端一个线程,简单但资源消耗大。
-
非阻塞模式 + select/poll:将套接字设为非阻塞,使用 select 或 WSAPoll 监视多个套接字状态。
-
异步 I/O:使用 Winsock 的异步事件或重叠 I/O。
5.1 设置非阻塞模式
u_long mode = 1; // 1 表示非阻塞,0 表示阻塞
ioctlsocket(sock, FIONBIO, &mode);
5.2 使用 select 管理多个套接字
select 可以监视一组套接字是否可读、可写或发生异常。适用于中等数量的连接。
fd_set readSet;
FD_ZERO(&readSet);
FD_SET(listenSock, &readSet);
FD_SET(clientSock1, &readSet);
// …
struct timeval tv;
tv.tv_sec = 5;
tv.tv_usec = 0;
int result = select(0, &readSet, NULL, NULL, &tv); // 第一个参数被忽略,传入0即可
if (result > 0) {
if (FD_ISSET(listenSock, &readSet)) {
// 有新连接
SOCKET newClient = accept(listenSock, NULL, NULL);
// 添加到集合…
}
if (FD_ISSET(clientSock1, &readSet)) {
// 可读,调用 recv
}
}
6. Winsock 异步 I/O 模型
Windows 提供了多种异步通知机制,可以将网络 I/O 与 GUI 消息循环或高性能服务器结合。
6.1 异步选择 (WSAAsyncSelect)
允许窗口接收套接字事件消息(如 FD_READ、FD_WRITE、FD_ACCEPT)。常用于 Windows GUI 程序。
WSAAsyncSelect(sock, hwnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
// 在窗口过程处理 WM_SOCKET 消息
6.2 基于事件的异步 (WSAEventSelect)
将套接字事件与事件对象关联,通过 WaitForMultipleObjects 等待。
WSAEVENT hEvent = WSACreateEvent();
WSAEventSelect(sock, hEvent, FD_READ | FD_WRITE | FD_CLOSE);
// 等待事件
DWORD dwIndex = WSAWaitForMultipleEvents(1, &hEvent, FALSE, WSA_INFINITE, FALSE);
// 枚举事件
WSAResetEvent(hEvent);
// 调用 WSAGetOverlappedResult 或使用 WSAEnumNetworkEvents 获取具体事件
6.3 重叠 I/O (Overlapped I/O)
使用支持重叠操作的 Winsock 函数(WSASend、WSARecv、AcceptEx 等),配合事件通知或完成例程。与文件异步 I/O 类似,但网络 I/O 更复杂。
-
事件通知:在 WSAOVERLAPPED 结构中设置 hEvent,然后等待事件。
-
完成例程:使用 WSARecv 等函数的 lpCompletionRoutine 参数,线程需处于可告警等待。
示例:使用 WSARecv 异步接收数据,通过事件等待完成。
WSAOVERLAPPED overlapped;
ZeroMemory(&overlapped, sizeof(overlapped));
overlapped.hEvent = WSACreateEvent();
char buffer[1024];
DWORD flags = 0;
WSABUF dataBuf;
dataBuf.buf = buffer;
dataBuf.len = sizeof(buffer);
int ret = WSARecv(sock, &dataBuf, 1, NULL, &flags, &overlapped, NULL);
if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING) {
// 错误
}
// 等待事件
WSAWaitForMultipleEvents(1, &overlapped.hEvent, FALSE, WSA_INFINITE, FALSE);
DWORD bytesTransferred;
// 获取结果
WSAGetOverlappedResult(sock, &overlapped, &bytesTransferred, FALSE, &flags);
6.4 I/O 完成端口 (IOCP) 在网络编程中的应用
IOCP 是 Windows 上最高效的异步模型,尤其适合高并发网络服务器。基本步骤:
创建完成端口。
创建监听套接字,绑定并监听。
将监听套接字与完成端口关联。
创建多个工作线程,调用 GetQueuedCompletionStatus 等待完成包。
使用 AcceptEx 异步接受连接,并传入与客户端套接字关联的完成端口。
对每个客户端套接字,发起异步 WSARecv/WSASend 操作,并在完成包中处理数据。
IOCP 能高效利用少量线程处理大量并发连接,是高并发服务器的基石。
IOCP 网络服务器简化示例(核心部分)
// 1. 创建完成端口
HANDLE hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 2. 创建监听套接字并关联完成端口
SOCKET listenSock = socket(AF_INET, SOCK_STREAM, 0);
// … bind, listen …
CreateIoCompletionPort((HANDLE)listenSock, hIOCP, (ULONG_PTR)listenSock, 0);
// 3. 创建工作线程
for (int i = 0; i < threadCount; i++) {
CreateThread(NULL, 0, WorkerThread, hIOCP, 0, NULL);
}
// 4. 主线程循环接受连接(或使用 AcceptEx 异步接受)
while (true) {
SOCKET clientSock = accept(listenSock, NULL, NULL);
// 将客户端套接字关联到完成端口
CreateIoCompletionPort((HANDLE)clientSock, hIOCP, (ULONG_PTR)clientSock, 0);
// 为客户端发起一个异步接收
PerIoContext *ctx = new PerIoContext();
ctx->sock = clientSock;
WSABUF buf;
buf.buf = ctx->buffer;
buf.len = sizeof(ctx->buffer);
DWORD flags = 0;
WSARecv(clientSock, &buf, 1, NULL, &flags, &ctx->overlapped, NULL);
// 若返回 WSA_IO_PENDING 则正常
}
// 工作线程函数
DWORD WINAPI WorkerThread(LPVOID lpParam) {
HANDLE hIOCP = (HANDLE)lpParam;
DWORD bytesTransferred;
ULONG_PTR completionKey;
LPOVERLAPPED lpOverlapped;
while (GetQueuedCompletionStatus(hIOCP, &bytesTransferred, &completionKey, &lpOverlapped, INFINITE)) {
if (completionKey == (ULONG_PTR)listenSock) {
// 有新的客户端连接(如果用 AcceptEx)
} else {
SOCKET clientSock = (SOCKET)completionKey;
PerIoContext *ctx = CONTAINING_RECORD(lpOverlapped, PerIoContext, overlapped);
if (bytesTransferred == 0) {
// 客户端关闭
closesocket(clientSock);
delete ctx;
} else {
// 处理接收到的数据,然后再次发起接收
// …
WSARecv(clientSock, …);
}
}
}
return 0;
}
7. 注意事项
字节序:网络字节序为大端(big-endian),使用 htons、htonl、ntohs、ntohl 转换端口和 IP 地址。
错误处理:始终检查返回值,并使用 WSAGetLastError 获取详细错误。
资源释放:确保关闭所有套接字,调用 WSACleanup。
线程安全:Winsock 函数在大多数情况下是线程安全的,但需注意共享数据的同步。
性能优化:
-
使用非阻塞 I/O 或 IOCP 提高并发能力。
-
避免在每个操作中分配内存,使用对象池。
-
调整套接字缓冲区大小(SO_RCVBUF、SO_SNDBUF)。
-
启用 Nagle 算法(TCP_NODELAY)控制小包延迟。





