这篇指南将从基础概念开始,逐步深入,提供一个完整的客户端/服务器示例,并解释关键的 API 和注意事项。

目录
- 核心概念
- 什么是 Socket?
- 网络字节序
- Winsock 库
- 服务器端编程步骤
- 第 1 步:初始化 Winsock (
WSAStartup) - 第 2 步:创建套接字 (
socket) - 第 3 步:绑定地址和端口 (
bind) - 第 4 步:监听连接 (
listen) - 第 5 步:接受连接 (
accept) - 第 6 步:收发数据 (
send/recv) - 第 7 步:关闭和清理 (
closesocket,WSACleanup)
- 第 1 步:初始化 Winsock (
- 客户端编程步骤
- 第 1 步:初始化 Winsock (
WSAStartup) - 第 2 步:创建套接字 (
socket) - 第 3 步:连接服务器 (
connect) - 第 4 步:收发数据 (
send/recv) - 第 5 步:关闭和清理 (
closesocket,WSACleanup)
- 第 1 步:初始化 Winsock (
- 完整代码示例
- 服务器端代码 (
server.c) - 客户端代码 (
client.c) - 如何编译和运行
- 服务器端代码 (
- 关键 API 函数详解
- 常见错误与调试技巧
- 进阶话题
核心概念
什么是 Socket?
Socket(套接字)是操作系统提供的一种用于网络通信的 API,它就像一个“电话插座”,应用程序可以通过它来发送和接收数据,在 C 语言中,我们使用一组特定的函数来操作这个“插座”。
网络字节序
计算机在内存中存储多字节数据(如端口号 8080 或 IP 地址)时有两种方式:
- 大端序:高位字节存储在低地址。
- 小端序:低位字节存储在低地址。
不同的 CPU 架构可能使用不同的字节序,为了确保网络通信中数据的一致性,网络协议规定使用大端序(也称为网络字节序)。
在将端口号或 IP 地址地址结构体赋值之前,必须使用 htons() (host to network short) 和 htonl() (host to network long) 函数将主机字节序转换为网络字节序,接收数据时,则使用 ntohs() 和 ntohl() 进行反向转换。

Winsock 库
Windows 和 Linux/Unix 的 Socket API 有细微差别,在 Windows 上,Socket 功能并不是操作系统内核的一部分,而是由一个名为 Winsock 的动态链接库(ws2_32.dll)提供的。
在 Windows 上进行 Socket 编程,必须:
- 链接库:在编译时链接
ws2_32.lib。 - 包含头文件:在代码中包含
<winsock2.h>。 - 初始化与清理:程序开始时调用
WSAStartup初始化,结束时调用WSACleanup清理。
服务器端编程步骤
服务器端的工作流程是:创建 -> 绑定 -> 监听 -> 接受 -> 通信 -> 关闭。
第 1 步:初始化 Winsock (WSAStartup)
在使用任何 Winsock 函数之前,必须先调用 WSAStartup 来告诉操作系统:“我要用 Winsock 了,请加载相关库”。

#include <winsock2.h>
#include <ws2tcpip.h>
// #pragma comment(lib, "ws2_32.lib") // 在 Visual Studio 中可以直接这样链接
int main() {
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 请求 Winsock 2.2 版本
if (result != 0) {
printf("WSAStartup failed: %d\n", result);
return 1;
}
// ... 其他代码 ...
WSACleanup(); // 程序结束时调用
return 0;
}
第 2 步:创建套接字 (socket)
创建一个“通信端点”,即 Socket。
SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
AF_INET: 使用 IPv4 地址族。SOCK_STREAM: 提供面向连接的、可靠的数据流服务(即 TCP)。IPPROTO_TCP: 明确指定使用 TCP 协议。
第 3 步:绑定地址和端口 (bind)
将创建的 Socket 与本机的 IP 地址和端口号关联起来,这样客户端才知道要连接到哪里。
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888); // 将端口号 8888 转换为网络字节序
serverAddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
if (bind(ListenSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("bind failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
sockaddr_in: 用于 IPv4 的地址结构体。INADDR_ANY: 一个特殊的常量,表示绑定到本机所有的网络接口(包括0.0.1和局域网 IP)。
第 4 步:监听连接 (listen)
将 Socket 设置为“监听”模式,等待客户端的连接请求。
if (listen(ListenSocket, SOMAXCONN) == SOCKET_ERROR) {
printf("listen failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
SOMAXCONN: 表示允许的最大连接请求数,系统会自动选择一个合适的值。
第 5 步:接受连接 (accept)
服务器进入阻塞状态,等待客户端发起连接,一旦有客户端连接,accept 会返回一个新的、专门用于与该客户端通信的 Socket。
sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
printf("Waiting for a client to connect...\n");
SOCKET ClientSocket = accept(ListenSocket, (sockaddr*)&clientAddr, &clientAddrSize);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Client connected from %s:%d\n",
inet_ntoa(clientAddr.sin_addr),
ntohs(clientAddr.sin_port));
ListenSocket仍然保持监听状态,可以继续接受其他客户端的连接。ClientSocket是一个全新的 Socket,后续的通信都通过它进行。
第 6 步:收发数据 (send / recv)
使用 ClientSocket 与已连接的客户端进行数据交换。
const char* sendMsg = "Hello from server!";
int sendResult = send(ClientSocket, sendMsg, strlen(sendMsg), 0);
if (sendResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
}
char recvbuf[512];
int recvResult = recv(ClientSocket, recvbuf, 512, 0);
if (recvResult > 0) {
printf("Bytes received: %d\n", recvResult);
recvbuf[recvResult] = '\0'; // 确保字符串以 null
printf("Client says: %s\n", recvbuf);
} else if (recvResult == 0) {
printf("Connection closing...\n");
} else {
printf("recv failed: %d\n", WSAGetLastError());
}
第 7 步:关闭和清理 (closesocket, WSACleanup)
通信结束后,关闭所有打开的 Socket,并调用 WSACleanup 释放 Winsock 资源。
closesocket(ClientSocket); closesocket(ListenSocket); WSACleanup();
客户端编程步骤
客户端的工作流程是:创建 -> 连接 -> 通信 -> 关闭。
第 1 步:初始化 Winsock (WSAStartup)
与服务器端完全相同。
第 2 步:创建套接字 (socket)
与服务器端完全相同。
第 3 步:连接服务器 (connect)
客户端主动向服务器的 IP 地址和端口号发起连接请求。
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8888); // 服务器的端口号
// 将 IP 地址字符串转换为网络格式
inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
if (connect(ClientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
printf("connect failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
inet_pton: 将 "点分十进制" 的 IP 地址字符串(如 "127.0.0.1")转换为sockaddr_in结构体需要的格式。
第 4 步:收发数据 (send / recv)
连接成功后,就可以使用 send 和 recv 与服务器自由通信了。
第 5 步:关闭和清理 (closesocket, WSACleanup)
与服务器端类似,关闭 Socket 并清理 Winsock。
完整代码示例
服务器端代码 (server.c)
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#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: %d\n", iResult);
return 1;
}
// 2. 创建监听 Socket
SOCKET ListenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ListenSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 3. 绑定 Socket 到地址和端口
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(atoi(DEFAULT_PORT));
iResult = bind(ListenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
if (iResult == SOCKET_ERROR) {
printf("bind failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
// 4. 开始监听
iResult = listen(ListenSocket, SOMAXCONN);
if (iResult == SOCKET_ERROR) {
printf("listen failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Server is listening on port %s...\n", DEFAULT_PORT);
// 5. 接受客户端连接
sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
SOCKET ClientSocket = accept(ListenSocket, (SOCKADDR*)&clientAddr, &clientAddrSize);
if (ClientSocket == INVALID_SOCKET) {
printf("accept failed: %d\n", WSAGetLastError());
closesocket(ListenSocket);
WSACleanup();
return 1;
}
printf("Client connected from %s:%d\n",
inet_ntoa(clientAddr.sin_addr),
ntohs(clientAddr.sin_port));
// 6. 收发数据
const char* sendMsg = "Hello from server!";
iResult = send(ClientSocket, sendMsg, strlen(sendMsg), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
printf("Sent %ld bytes.\n", iResult);
char recvbuf[DEFAULT_BUFLEN];
iResult = recv(ClientSocket, recvbuf, DEFAULT_BUFLEN, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
recvbuf[iResult] = '\0';
printf("Client says: %s\n", recvbuf);
} else if (iResult == 0) {
printf("Connection closing...\n");
} else {
printf("recv failed: %d\n", WSAGetLastError());
}
// 7. 关闭和清理
closesocket(ClientSocket);
closesocket(ListenSocket);
WSACleanup();
return 0;
}
客户端代码 (client.c)
#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;
// 1. 初始化 Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("WSAStartup failed: %d\n", iResult);
return 1;
}
// 2. 创建客户端 Socket
SOCKET ClientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (ClientSocket == INVALID_SOCKET) {
printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// 3. 连接服务器
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(atoi(DEFAULT_PORT));
inet_pton(AF_INET, SERVER_IP, &serverAddr.sin_addr);
iResult = connect(ClientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
if (iResult == SOCKET_ERROR) {
printf("connect failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
printf("Connected to server at %s:%s\n", SERVER_IP, DEFAULT_PORT);
// 4. 收发数据
char recvbuf[DEFAULT_BUFLEN];
iResult = recv(ClientSocket, recvbuf, DEFAULT_BUFLEN, 0);
if (iResult > 0) {
printf("Bytes received: %d\n", iResult);
recvbuf[iResult] = '\0';
printf("Server says: %s\n", recvbuf);
} else if (iResult == 0) {
printf("Connection closing...\n");
} else {
printf("recv failed: %d\n", WSAGetLastError());
}
const char* sendMsg = "Hello from client!";
iResult = send(ClientSocket, sendMsg, strlen(sendMsg), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed: %d\n", WSAGetLastError());
closesocket(ClientSocket);
WSACleanup();
return 1;
}
printf("Sent %ld bytes.\n", iResult);
// 5. 关闭和清理
closesocket(ClientSocket);
WSACleanup();
return 0;
}
如何编译和运行
-
使用 Visual Studio (推荐)
- 创建一个新的 "空项目" (Empty Project)。
- 将上述两个代码分别保存为
server.c和client.c,并添加到项目中。 - 在项目属性中,确保 "配置属性" -> "链接器" -> "输入" -> "附加依赖项" 中包含
ws2_32.lib,或者直接在每个.c文件开头加上#pragma comment(lib, "ws2_32.lib")。 - 分别将
server.c和client.c设为“启动项”,然后生成并运行。
-
使用 GCC (MinGW) 命令行
-
打开命令提示符或 PowerShell。
-
使用以下命令编译:
# 编译服务器 gcc server.c -o server.exe -lws2_32 # 编译客户端 gcc client.c -o client.exe -lws2_32
-
先运行服务器:
server.exe
(会看到 "Server is listening on port 8888...")
-
再打开一个新的 命令提示符窗口,运行客户端:
client.exe
-
关键 API 函数详解
| 函数 | 描述 | 参数 | 返回值 |
|---|---|---|---|
WSAStartup |
初始化 Winsock 库。 | wVersionRequested: 请求的 Winsock 版本 (如 MAKEWORD(2,2))。lpWSAData: 指向 WSADATA 结构体的指针。 |
成功返回 0,失败返回错误码。 |
WSACleanup |
卸载 Winsock 库,释放资源。 | 无 | 无返回值。 |
socket |
创建一个 Socket。 | af: 地址族 (如 AF_INET)。type: Socket 类型 (如 SOCK_STREAM)。protocol: 使用的协议 (如 IPPROTO_TCP)。 |
成功返回 SOCKET 句柄,失败返回 INVALID_SOCKET。 |
bind |
将 Socket 与本地 IP 地址和端口绑定。 | s: Socket 句柄。name: 指向 sockaddr 结构体的指针。namelen: sockaddr 结构体的大小。 |
成功返回 0,失败返回 SOCKET_ERROR。 |
listen |
将 Socket 设为监听模式,准备接受连接。 | s: 已绑定的 Socket 句柄。backlog: 请求队列的最大长度。 |
成功返回 0,失败返回 SOCKET_ERROR。 |
accept |
接受一个客户端的连接请求,并返回一个新的通信 Socket。 | s: 处于监听状态的 Socket 句柄。addr: (可选) 指向 sockaddr 结构体的指针,用于存储客户端地址。addrlen: 指向 addr 大小的指针。 |
成功返回新的 SOCKET 句柄,失败返回 INVALID_SOCKET。 |
connect |
客户端主动连接服务器。 | s: 客户端 Socket 句柄。name: 指向服务器 sockaddr 结构体的指针。namelen: sockaddr 结构体的大小。 |
成功返回 0,失败返回 SOCKET_ERROR。 |
send |
通过已连接的 Socket 发送数据。 | s: 已连接的 Socket 句柄。buf: 指向要发送数据的缓冲区。len: 要发送数据的字节数。flags: 选项 (通常为 0)。 |
成功返回实际发送的字节数,失败返回 SOCKET_ERROR,连接断开返回 0。 |
recv |
通过已连接的 Socket 接收数据。 | s: 已连接的 Socket 句柄。buf: 指向接收数据的缓冲区。len: 缓冲区大小。flags: 选项 (通常为 0)。 |
成功返回接收到的字节数,失败返回 SOCKET_ERROR,连接断开返回 0。 |
closesocket |
关闭一个 Socket。 | s: 要关闭的 Socket 句柄。 |
成功返回 0,失败返回 SOCKET_ERROR。 |
htons / htonl |
主机字节序转网络字节序 (short / long)。 | hostshort / hostlong: 主机字节序的值。 |
网络字节序的值。 |
ntohs / ntohl |
网络字节序转主机字节序 (short / long)。 | netshort / netlong: 网络字节序的值。 |
主机字节序的值。 |
inet_addr / inet_pton |
将 IP 地址字符串 (如 "192.168.1.1") 转换为 32 位网络字节序的二进制格式。 | cp / str: IP 地址字符串。addr: 指向存储结果的 in_addr 结构体的指针。 |
inet_addr 返回 in_addr,失败返回 INADDR_NONE。inet_pton 成功返回 1,失败返回 0 或 -1。 |
inet_ntoa / inet_ntop |
将 32 位网络字节序的二进制 IP 地址格式转换为点分十进制字符串。 | in: 指向 in_addr 结构体的指针。str: 指向存储结果的字符串缓冲区。 |
inet_ntoa 返回指向字符串的指针。inet_ntop 成功返回 0,失败返回 NULL。 |
常见错误与调试技巧
-
10048 (WSAEADDRINUSE)
- 错误: 地址在使用中。
- 原因: 端口
8888已经被另一个程序占用,可能是你上次运行的程序没有正常关闭。 - 解决: 修改代码中的端口号,或者找到并关闭占用该端口的程序(在命令行使用
netstat -ano | findstr "8888"查找 PID,然后用taskkill /PID <PID> /F强制结束)。
-
10061 (WSAECONNREFUSED)
- 错误: 连接被拒绝。
- 原因: 客户端尝试连接,但服务器没有在监听指定的端口,或者防火墙阻止了连接。
- 解决: 确保服务器已经成功启动并正在监听,检查防火墙设置。
-
10013 (WSAEACCES)
- 错误: 权限被拒绝。
- 原因: 程序没有权限绑定到小于 1024 的端口(这些是特权端口),或者防火墙阻止了绑定。
- 解决: 使用大于 1024 的端口号。
-
如何调试?
- 打印错误信息: 几乎所有 Winsock 函数失败时,都可以用
WSAGetLastError()获取具体的错误码,并用printf打印出来,这是最直接的调试方法。 - 使用网络分析工具: Wireshark 是一个强大的网络抓包工具,你可以用它来查看你的程序是否真的在发送和接收数据,以及数据包的格式是否正确。
- 检查返回值: 每一步关键操作(
socket,bind,listen,accept,connect,send,recv)后,都应检查其返回值,判断是否成功。
- 打印错误信息: 几乎所有 Winsock 函数失败时,都可以用
进阶话题
- 多线程/多进程模型: 上面的例子是单线程的,一次只能处理一个客户端,在实际应用中,你需要使用多线程(一个主线程负责
accept,每个客户端连接一个新线程)或 I/O 多路复用模型(如select)来同时处理多个客户端。 - 非阻塞模式: 默认情况下,
accept和recv是阻塞的,即如果没有事件发生,程序会卡住,可以设置 Socket 为非阻塞模式,这样函数会立即返回,然后你可以通过轮询或事件通知的方式来处理 I/O。 - UDP 编程: UDP 是无连接的协议,编程模型更简单:
socket->bind->sendto/recvfrom->closesocket,不需要listen和accept。 - 异步 I/O (IOCP): 对于高性能服务器,Windows 提供了 I/O 完成端口机制,这是最复杂但也最高效的并发模型。
