- 核心概念:理解 Socket 的基本原理。
- Socket 编程流程:服务器和客户端通信的基本步骤。
- 关键 API 函数:详细介绍每个步骤会用到的核心函数。
- 完整代码示例:提供一个最经典、最完整的 TCP 服务器和客户端代码,并附上详细注释。
- 编译与运行:说明如何在 Linux 和 Windows 上编译和运行这些程序。
- UDP Socket 简介:简要介绍 UDP 编程与 TCP 的区别。
- 总结与注意事项。
核心概念
在开始编码前,我们需要理解几个基本概念:

(图片来源网络,侵删)
- IP 地址:网络中设备的唯一标识,就像家庭住址一样。
168.1.100。 - 端口号:同一台主机上,不同应用程序的标识,就像房间号一样,一个 IP 地址可以提供多个服务(如 Web 服务、FTP 服务),端口号用来区分这些服务,范围是 0-65535,0-1023 是系统保留端口。
- 协议:通信双方必须遵守的规则,Socket 编程主要涉及两种协议:
- TCP (Transmission Control Protocol):面向连接的协议,通信前需要先建立连接(三次握手),数据传输可靠、有序,但开销较大,适用于要求高可靠性的场景,如文件传输、网页浏览。
- UDP (User Datagram Protocol):无连接的协议,直接发送数据,不保证顺序和可靠性,但开销小、速度快,适用于实时性要求高的场景,如视频会议、在线游戏。
- Socket:它是网络编程的 API,可以看作是“通信端点”,应用程序通过操作 Socket,将数据发送到网络或从网络接收数据。
Socket 编程流程
TCP 通信是“客户端-服务器”模式,流程如下:
TCP 服务器端 流程
- 创建 Socket:使用
socket()函数创建一个套接字描述符。 - 绑定地址和端口:使用
bind()函数将 Socket 与一个 IP 地址和端口号绑定,这样客户端才能找到它。 - 监听连接:使用
listen()函数让 Socket 进入被动监听状态,等待客户端的连接请求。 - 接受连接:使用
accept()函数从等待队列中取出一个客户端连接请求,并创建一个新的 Socket 与该客户端进行通信,原 Socket 继续监听新的连接。 - 收发数据:使用
send()和recv()(或read()/write()) 函数与客户端进行数据交换。 - 关闭连接:通信结束后,使用
close()关闭与客户端的 Socket,并最终关闭监听 Socket。
TCP 客户端 流程
- 创建 Socket:同样使用
socket()函数创建一个套接字描述符。 - 连接服务器:使用
connect()函数主动向服务器的 IP 地址和端口号发起连接请求。 - 收发数据:连接建立后,使用
send()和recv()(或read()/write()) 函数与服务器进行数据交换。 - 关闭连接:通信结束后,使用
close()关闭 Socket。
关键 API 函数
这些函数在 <sys/socket.h> (Linux) 或 <winsock2.h> (Windows) 中定义。
| 函数 | 功能 | 服务器/客户端 |
|---|---|---|
socket() |
创建一个 Socket,返回一个描述符。 | 服务器 & 客户端 |
bind() |
将 Socket 与指定的 IP 地址和端口号绑定。 | 服务器 |
listen() |
将 Socket 设为监听状态,等待客户端连接。 | 服务器 |
accept() |
接受客户端的连接请求,返回一个新的通信 Socket。 | 服务器 |
connect() |
主动向服务器发起连接请求。 | 客户端 |
send() / write() |
通过已连接的 Socket 发送数据。 | 服务器 & 客户端 |
recv() / read() |
通过已连接的 Socket 接收数据。 | 服务器 & 客户端 |
close() / closesocket() |
关闭 Socket,释放资源。 | 服务器 & 客户端 |
重要提示 (Windows vs. Linux):
- 头文件:Linux 用
<sys/socket.h>,Windows 用<winsock2.h>。 - 库文件:Linux 默认链接,Windows 需要额外链接
ws2_32.lib库。 - 初始化与清理:Windows 使用 Socket 前,必须先调用
WSAStartup()初始化;程序结束时,必须调用WSACleanup()清理,Linux 不需要。
完整代码示例 (TCP)
下面是一个经典的“回显服务器” (Echo Server) 的完整代码,服务器接收客户端发来的任何消息,然后原样发回。

(图片来源网络,侵删)
1 服务器端代码 (server.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 read, write, close
#include <sys/socket.h>
#include <netinet/in.h> // 用于 sockaddr_in 结构体
#include <arpa/inet.h> // 用于 inet_addr 函数
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建 Socket (AF_INET for IPv4, SOCK_STREAM for TCP)
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置 Socket 选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
// 2. 绑定 地址和端口
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
address.sin_port = htons(PORT); // 将端口号从主机字节序转换为网络字节序
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 监听 连接
if (listen(server_fd, 3) < 0) { // 3 是最大等待连接数
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
// 4. 接受 连接
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
printf("Connection accepted from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 5. 收发数据
int valread;
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
printf("Received from client: %s", buffer);
// 将收到的数据回显给客户端
send(new_socket, buffer, valread, 0);
memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
}
if (valread == 0) {
printf("Client disconnected\n");
} else if (valread < 0) {
perror("read error");
}
// 6. 关闭连接
close(new_socket);
close(server_fd);
return 0;
}
2 客户端代码 (client.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char *hello = "Hello from client";
char buffer[BUFFER_SIZE] = {0};
// 1. 创建 Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
// 将 IPv4 地址从文本转换为二进制形式
if (inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 2. 连接 服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
printf("Connected to server.\n");
// 3. 发送数据
send(sock, hello, strlen(hello), 0);
printf("Hello message sent\n");
// 4. 接收数据
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server response: %s\n", buffer);
// 5. 关闭连接
close(sock);
return 0;
}
编译与运行
在 Linux/macOS 上
-
编译: 打开两个终端,分别编译服务器和客户端。
# 终端 1: 编译服务器 gcc server.c -o server # 终端 2: 编译客户端 gcc client.c -o client
-
运行:
- 在一个终端中运行服务器:
./server
你会看到
Server listening on port 8080... - 在另一个终端中运行客户端:
./client
客户端会输出:
(图片来源网络,侵删)Connected to server. Hello message sent Server response: Hello from client - 服务器终端会显示:
Server listening on port 8080... Connection accepted from 127.0.0.1:54321 (端口号可能不同) Received from client: Hello from client Client disconnected
- 在一个终端中运行服务器:
在 Windows 上 (使用 MinGW/GCC)
-
编译: 打开两个命令提示符 (CMD) 或 PowerShell 窗口。
# 窗口 1: 编译服务器 (需要链接 ws2_32.lib) gcc server.c -o server.exe -lws2_32 # 窗口 2: 编译客户端 gcc client.c -o client.exe -lws2_32
-
运行:
- 在一个窗口中运行服务器:
server.exe
- 在另一个窗口中运行客户端:
client.exe
输出结果与 Linux 类似。
- 在一个窗口中运行服务器:
UDP Socket 简介
UDP 编程比 TCP 简单,因为它没有连接的概念。
服务器端流程 (UDP)
socket(): 创建 Socket (类型为SOCK_DGRAM)。bind(): 绑定 IP 和端口。recvfrom(): 接收数据,并获取客户端的地址信息。sendto(): 使用sendto()将数据发送回给客户端(需要客户端的地址)。close(): 关闭 Socket。
客户端流程 (UDP)
socket(): 创建 Socket。sendto(): 直接使用sendto()发送数据给服务器的 IP 和端口。recvfrom(): 接收服务器的响应。close(): 关闭 Socket。
核心区别:TCP 是 read/write,而 UDP 是 recvfrom/sendto,因为 UDP 需要知道数据包从哪里来,要发到哪里去。
总结与注意事项
- 字节序:网络字节序是大端序,在调用
bind(),connect()等函数前,必须使用htons(),htonl()等函数将主机字节序的端口号和地址转换为网络字节序。ntohs(),ntohl()则用于转换回来。 - 错误处理:Socket 函数很多情况下都会返回错误(如返回 -1),务必检查每个函数的返回值,并根据
perror()或strerror(errno)打印错误信息,这是调试的关键。 - 阻塞与非阻塞:默认情况下,
accept(),read(),recv()等函数是阻塞的,即如果没有数据到达,程序会一直等待在这里,更高级的程序会使用select(),poll(),epoll(Linux) 或I/O 完成端口(Windows) 来实现 I/O 多路复用,以处理多个连接。 - 资源释放:程序退出前,一定要调用
close()关闭所有打开的 Socket,否则会造成文件描述符泄漏。
希望这份详细的教程能帮助你理解 C 语言的 Socket 编程!从 TCP 开始是最好的起点,因为它更稳定和可靠。
