Windows 下的 Socket 编程与 Linux/Unix 有一个关键区别:需要额外的初始化和清理工作,下面我将从基础概念、完整代码、步骤分解、关键函数和常见问题等多个方面进行阐述。

(图片来源网络,侵删)
核心概念与准备工作
在 Windows 中,Socket API 并不是操作系统内核的一部分,而是由一个名为 Winsock 的 DLL(动态链接库)提供的,你必须显式地告诉程序加载并使用这个库。
关键区别:Winsock 初始化与清理
- Linux/Unix: Socket 功能是系统调用的一部分,直接包含在
<sys/socket.h>中,无需额外初始化。 - Windows: 必须在创建任何 Socket 之前调用
WSAStartup(),并在程序结束时调用WSACleanup()。
一个完整的 TCP 服务器示例
下面是一个完整的、可运行的 TCP 服务器代码,它会在本地 8888 端口监听,等待客户端连接,接收到客户端消息后,将其转换为大写并发送回去。
#define WIN32_LEAN_AND_MEAN // 减少Windows头文件包含,提高编译速度
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
// 链接 Winsock 库
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_PORT "8888"
#define DEFAULT_BUFLEN 512
int main() {
WSADATA wsaData;
int iResult;
// 1. 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
// 2. 创建监听 Socket
struct addrinfo* result = NULL;
struct addrinfo hints;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET; // IPv4
hints.ai_socktype = SOCK_STREAM; // TCP Socket
hints.ai_protocol = IPPROTO_TCP; // TCP 协议
hints.ai_flags = AI_PASSIVE; // 用于绑定到本地地址
// 解析本地地址和端口
iResult = getaddrinfo(NULL, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
SOCKET ListenSocket = INVALID_SOCKET;
ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
if (ListenSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
freeaddrinfo(result);
WSACleanup();
return 1;
}
// 3. 绑定 Socket 到本地地址和端口
iResult = bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
if (iResult == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
freeaddrinfo(result);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// 4. 开始监听
iResult = listen(ListenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("listen failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Server is listening on port %s...\n", DEFAULT_PORT);
// 5. 接受客户端连接
struct addrinfo clientAddr;
int clientAddrLen = sizeof(clientAddr);
SOCKET ClientSocket = INVALID_SOCKET;
ClientSocket = accept(ListenSocket, (struct sockaddr*)&clientAddr, &clientAddrLen);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed with error: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Client connected!\n");
// 6. 与客户端通信
char recvbuf[DEFAULT_BUFLEN];
int recvbuflen = DEFAULT_BUFLEN;
iResult = recv(ClientSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
printf("Message from client: %s\n", recvbuf);
// 将消息转换为大写并发送回去
for (int i = 0; i < iResult; i++) {
recvbuf[i] = toupper(recvbuf[i]);
}
iResult = send(ClientSocket, recvbuf, iResult, 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %d\n", iResult);
} else if (iResult == 0) {
printf("Connection closing...\n");
} else {
printf("recv failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// 7. 关闭连接
iResult = shutdown(ClientSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
closesocket(ClientSocket);
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// 清理资源
closesocket(ClientSocket);
closesocket(ListenSocket);
freeaddrinfo(result);
WSACleanup();
printf("Server shutdown successfully.\n");
return 0;
}
代码步骤详解
步骤 1: 包含头文件并链接库
#define WIN32_LEAN_AND_MEAN #include <windows.h> #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> // 告诉链接器在编译时链接 ws2_32.lib 这个库 #pragma comment(lib, "ws2_32.lib")
winsock2.h: 核心 Winsock 头文件。ws2tcpip.h: 提供了更新的地址转换函数,如inet_pton,比旧的inet_addr更安全。#pragma comment(lib, "ws2_32.lib"): 这是一种方便的方式,让你不需要在 Visual Studio 的项目设置里手动添加库依赖。
步骤 2: 初始化 Winsock (WSAStartup)
WSADATA wsaData; int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
MAKEWORD(2, 2): 请求 Winsock 2.2 版本,这是一个标准的做法。WSADATA: 一个结构体,用于返回 Winsock 的详细实现信息(如最高版本、当前版本等)。- 如果返回值不是
0,则初始化失败。
步骤 3: 创建 Socket (socket)
struct addrinfo hints, *result; ZeroMemory(&hints, sizeof(hints)); hints.ai_family = AF_INET; // IPv4 hints.ai_socktype = SOCK_STREAM; // TCP hints.ai_protocol = IPPROTO_TCP; // TCP hints.ai_flags = AI_PASSIVE; // 表示这个Socket将用于绑定 getaddrinfo(NULL, DEFAULT_PORT, &hints, &result); SOCKET ListenSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol);
- 现代 Windows 编程推荐使用
getaddrinfo来解析地址和端口,它会返回一个addrinfo链表,你只需要取第一个即可。 AF_INET: 使用 IPv4 地址族。SOCK_STREAM: 创建一个流式套接字,即 TCP Socket。IPPROTO_TCP: 明确指定使用 TCP 协议。AI_PASSIVE: 告诉getaddrinfo返回的地址适合用于bind操作(即使用INADDR_ANY)。
步骤 4: 绑定地址和端口 (bind)
bind(ListenSocket, result->ai_addr, (int)result->ai_addrlen);
- 将创建的
ListenSocket与一个特定的 IP 地址和端口号关联起来。 result->ai_addr是getaddrinfo返回的地址信息。result->ai_addrlen是地址的长度。
步骤 5: 开始监听 (listen)
listen(ListenSocket, SOMAXCONN);
- 将 Socket 转入被动监听模式,准备接受客户端的连接请求。
SOMAXCONN是一个系统常量,表示允许的最大连接挂起队列长度。
步骤 6: 接受连接 (accept)
SOCKET ClientSocket = accept(ListenSocket, ...);
- 服务器调用
accept会阻塞程序,直到有一个客户端连接上来。 - 当连接成功时,
accept会返回一个新的ClientSocket。后续的所有通信(收发数据)都通过这个新的ClientSocket进行,而原来的ListenSocket继续监听新的连接。
步骤 7: 数据收发 (send/recv)
// 接收数据 int iResult = recv(ClientSocket, recvbuf, recvbuflen, 0); // 发送数据 iResult = send(ClientSocket, sendbuf, sendbuflen, 0);
recv: 从ClientSocket读取数据到缓冲区recvbuf。send: 将缓冲区sendbuf中的数据通过ClientSocket发送给客户端。- 第四个参数通常为
0,表示使用默认行为。
步骤 8: 关闭和清理 (shutdown, closesocket, WSACleanup)
// 关闭 Socket 的发送功能,告诉对方我们不会再发送数据了 shutdown(ClientSocket, SD_SEND); // 关闭 Socket closesocket(ClientSocket); closesocket(ListenSocket); // 释放 getaddrinfo 分配的内存 freeaddrinfo(result); // 卸载 Winsock DLL WSACleanup();
shutdown: 优雅地关闭连接,可以选择关闭发送、接收或双向。closesocket: 彻底关闭 Socket,释放资源。freeaddrinfo: 释放getaddrinfo分配的内存。WSACleanup: 非常重要! 减少对 Winsock DLL 的引用计数,当计数为 0 时,DLL 会被卸载。必须在程序结束前调用。
一个完整的 TCP 客户端示例
客户端的逻辑相对简单:连接服务器 -> 收发数据 -> 关闭。
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
#define DEFAULT_PORT "8888"
#define DEFAULT_BUFLEN 512
#define SERVER_IP "127.0.0.1" // 本地服务器地址
int main() {
WSADATA wsaData;
int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed with error: %d\n", iResult);
return 1;
}
// 创建用于连接的 Socket
struct addrinfo* result = NULL,
* ptr = NULL,
hints;
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// 解析服务器地址
iResult = getaddrinfo(SERVER_IP, DEFAULT_PORT, &hints, &result);
if (iResult != 0) {
printf("getaddrinfo failed with error: %d\n", iResult);
WSACleanup();
return 1;
}
SOCKET ConnectSocket = INVALID_SOCKET;
// 尝试连接到第一个找到的地址
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype, ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("socket failed with error: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 连接到服务器
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
continue; // 尝试下一个地址
}
break; // 连接成功,跳出循环
}
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
printf("Unable to connect to server!\n");
WSACleanup();
return 1;
}
printf("Connected to server!\n");
// 发送数据
char sendbuf[] = "Hello from client!";
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
printf("Bytes sent: %ld\n", iResult);
// 接收数据
char recvbuf[DEFAULT_BUFLEN];
iResult = recv(ConnectSocket, recvbuf, DEFAULT_BUFLEN - 1, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
recvbuf[iResult] = '\0'; // 确保字符串以 null
printf("Message from server: %s\n", recvbuf);
} else if (iResult == 0) {
printf("Connection closed by server\n");
} else {
printf("recv failed with error: %d\n", WSAGetLastError());
}
// 关闭连接
iResult = shutdown(ConnectSocket, SD_SEND);
if (iResult == SOCKET_ERROR) {
printf("shutdown failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
closesocket(ConnectSocket);
WSACleanup();
return 0;
}
常见问题与注意事项
-
编译错误
(图片来源网络,侵删)- LNK2025: 找不到外部符号,这通常是因为你没有链接
ws2_32.lib库,请确保#pragma comment(lib, "ws2_32.lib")存在或在你的项目设置中添加了该库。 - C2065: “WSAStartup”: 未声明的标识符,这通常是因为你忘记包含
winsock2.h头文件。
- LNK2025: 找不到外部符号,这通常是因为你没有链接
-
运行时错误
- WSAGetLastError(): 当一个 Socket 函数失败时,它会返回一个错误码(通常是
SOCKET_ERROR),调用WSAGetLastError()可以获取具体的错误原因,这对于调试至关重要。bind失败可能是端口被占用。 - 端口被占用: 如果服务器无法
bind到指定端口,请检查是否有其他程序正在使用该端口,可以使用netstat -ano | findstr "8888"(CMD) 或netstat -an | grep 8888(PowerShell) 来查看。 - 防火墙: Windows 防火墙可能会阻止你的程序监听端口或进行网络连接,你可能需要为你的程序创建一个防火墙规则,允许其入站连接。
- WSAGetLastError(): 当一个 Socket 函数失败时,它会返回一个错误码(通常是
-
阻塞与非阻塞模式
- 默认情况下,
accept,recv,connect等函数是阻塞的,即函数会一直等待,直到操作完成或发生错误。 - 对于高性能服务器,通常会使用非阻塞模式或I/O 多路复用(如
select)来同时处理多个连接,避免一个连接阻塞整个程序,Windows 也支持更高效的WSAAsyncSelect和WSAEventSelect模型,以及跨平台的epoll(Linux) /kqueue(BSD) 的 Windows 版本——IOCP (I/O Completion Ports),但对于初学者,阻塞模式是最容易理解的。
- 默认情况下,

(图片来源网络,侵删)
