目录
- 什么是 Socket?
- Socket 编程基本流程
- 核心 API 函数详解
- 一个完整的示例:简单的 Echo 服务器与客户端
- 关键注意事项
- 总结与进阶
什么是 Socket?
你可以把 Socket(套接字) 想象成一个通信的“端点”,它是一个编程接口,允许程序发送或接收数据,就像一个电话插座,你把电话线(网络连接)插进去,就可以进行通话(数据交换)。

(图片来源网络,侵删)
在 C 语言中,Socket 不是一个数据类型,而是一个文件描述符,它是一个整数,操作系统用它来标识一个网络连接,我们可以像读写文件一样对 Socket 进行读写操作。
Socket 主要分为两种类型:
- 流式套接字:使用 TCP (Transmission Control Protocol) 协议,它提供面向连接的、可靠的、有序的、基于字节流的数据传输服务,数据不会丢失、重复或乱序,打电话的比喻很形象:你必须先拨号建立连接,然后才能通话,通话结束后挂断。
- 数据报套接字:使用 UDP (User Datagram Protocol) 协议,它提供无连接的、不可靠的、基于数据报的数据传输服务,数据可能会丢失、重复或乱序,寄明信片的比喻很形象:你写好地址直接投递,不需要提前联系对方,也无法保证明信片能按时、按顺序到达。
本指南将重点介绍更常用的 TCP Socket 编程。
Socket 编程基本流程
TCP 编程分为服务器端和客户端,两者的流程是不同的。

(图片来源网络,侵删)
服务器端 流程
- 创建套接字:使用
socket()函数创建一个通信端点。 - 绑定地址和端口:使用
bind()函数将套接字与一个特定的 IP 地址和端口号绑定,这样客户端才知道连接到哪里。 - 监听连接:使用
listen()函数将套接字设置为被动模式,准备接受客户端的连接请求。 - 接受连接:使用
accept()函数阻塞等待客户端的连接请求,当有客户端连接时,accept()返回一个新的套接字,专门用于与这个客户端通信。 - 收发数据:使用
send()和recv()(或read()和write()) 函数通过新的套接字与客户端进行数据交换。 - 关闭套接字:通信结束后,使用
close()关闭用于通信的套接字,最后再关闭监听套接字。
客户端 流程
- 创建套接字:同样使用
socket()函数创建一个通信端点。 - 连接服务器:使用
connect()函数主动向服务器的 IP 地址和端口号发起连接请求。 - 收发数据:连接成功后,使用
send()和recv()(或read()和write()) 函数与服务器进行数据交换。 - 关闭套接字:通信结束后,使用
close()关闭套接字。
核心 API 函数详解
在使用这些函数前,需要包含必要的头文件:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> // 用于 read, write, close #include <sys/socket.h> // 核心 socket 函数 #include <netinet/in.h> // sockaddr_in 结构体 #include <arpa/inet.h> // inet_addr 函数
socket()
int socket(int domain, int type, int protocol);
- 功能: 创建一个套接字。
- 参数:
domain: 地址族,常用AF_INET(IPv4) 或AF_INET6(IPv6)。type: 套接字类型,常用SOCK_STREAM(TCP) 或SOCK_DGRAM(UDP)。protocol: 协议,通常设为0,系统会自动根据domain和type选择。
- 返回值: 成功返回套接字描述符(一个整数),失败返回
-1。
bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 将套接字与一个 IP 地址和端口号绑定。
- 参数:
sockfd:socket()返回的套接字描述符。addr: 指向sockaddr结构体的指针,实际使用时,我们通常用sockaddr_in结构体并强制转换。addrlen:addr结构体的长度。
- 返回值: 成功返回
0,失败返回-1。
listen()
int listen(int sockfd, int backlog);
- 功能: 将套接字设置为被动监听模式,等待客户端连接。
- 参数:
sockfd: 已经绑定地址的套接字描述符。backlog: 请求队列的最大长度,表示最多有多少个连接可以等待被accept()。
- 返回值: 成功返回
0,失败返回-1。
accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 功能: 从监听队列中接受一个连接请求,这是一个阻塞函数,直到有客户端连接为止。
- 参数:
sockfd: 处于监听状态的套接字描述符。addr: 用于保存客户端的地址信息(可选,可设为NULL)。addrlen: 指向addr结构体长度的指针(可选,可设为NULL)。
- 返回值: 成功返回一个新的套接字描述符(用于与客户端通信),失败返回
-1。
connect()
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 功能: 客户端主动向服务器发起连接请求。
- 参数:
sockfd: 客户端创建的套接字描述符。addr: 指向服务器地址结构体的指针。addrlen: 服务器地址结构体的长度。
- 返回值: 成功返回
0,失败返回-1。
send() / recv()
ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 功能: 在已连接的套接字上发送和接收数据。
- 参数:
sockfd: 已连接的套接字描述符。buf: 发送/接收数据的缓冲区。len: 要发送/接收的数据长度。flags: 通常设为0。
- 返回值: 成功返回实际发送/接收的字节数(可能小于
len),连接断开时返回0,失败返回-1。
close()
int close(int fd);
- 功能: 关闭一个套接字(或文件)描述符。
- 返回值: 成功返回
0,失败返回-1。
一个完整的示例:简单的 Echo 服务器与客户端
这个例子中,客户端发送一条消息,服务器收到后将原消息返回给客户端。
sockaddr_in 结构体
这是 sockaddr 的 IPv4 版本,我们在编程中主要使用它:
struct sockaddr_in {
short sin_family; // 地址族,必须为 AF_INET
unsigned short sin_port; // 端口号,需要用 htons() 转换
struct in_addr sin_addr; // IP 地址,需要用 inet_addr() 或 inet_pton() 转换
char sin_zero[8]; // 填充,不用管
};
重要: sin_port 和 sin_addr 的值在赋值前必须进行网络字节序转换。
htons(): Host to Network Short (16位)htonl(): Host to Network Long (32位)inet_addr(): 将点分十进制的 IP 字符串转换为网络字节序的in_addr结构。
服务器端代码 (server.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 server_fd, new_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 1. 创建套接字
// AF_INET: IPv4, SOCK_STREAM: TCP, 0: 自动选择协议
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置套接字选项,允许地址重用(避免服务器重启时端口被占用)
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET; // IPv4
address.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
address.sin_port = htons(PORT); // 将端口号转换为网络字节序
// 2. 绑定地址和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 3. 监听连接
if (listen(server_fd, 3) < 0) {
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("Client connected: %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: %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");
}
// 6. 关闭套接字
close(new_socket);
close(server_fd);
return 0;
}
客户端代码 (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. 创建套接字
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 echoed: %s\n", buffer);
// 5. 关闭套接字
close(sock);
return 0;
}
关键注意事项
- 错误处理: 所有的 Socket API 调用都可能失败,必须检查它们的返回值,失败时,通常使用
perror()打印错误信息,并用exit()退出程序。 - 字节序: 网络字节序是大端序,在设置端口号和 IP 地址时,必须使用
htons()、htonl()、inet_addr()等函数进行转换,以确保在不同架构的机器上都能正确通信。 - 地址重用: 服务器端调用
setsockopt()设置SO_REUSEADDR是一个好习惯,它可以让你在服务器快速重启时,不必等待 TIME_WAIT 状态结束就能重新绑定端口。 - 阻塞与非阻塞:
accept()和recv()等函数默认是阻塞的,即如果没有数据到达,程序会一直等待,对于高性能服务器,需要使用select(),poll(), 或epoll(Linux) 等I/O多路复用技术来实现非阻塞I/O。 - 跨平台性: 本指南中的代码主要基于 Linux/Unix 系统(使用了
unistd.h),在 Windows 上,需要包含<winsock2.h>并在使用前调用WSAStartup()初始化,使用后调用WSACleanup()清理。
总结与进阶
- 你已经了解了 C 语言 TCP Socket 编程的基本流程和核心 API,并成功实现了一个简单的 Echo 服务器和客户端,这是网络编程的“Hello, World”。
- 进阶方向:
- 并发服务器: 上述服务器一次只能处理一个客户端,要同时处理多个客户端,你需要使用多线程 (
pthread) 或多进程 (fork) 为每个客户端的连接创建一个新的处理单元。 - I/O 多路复用: 学习
select(),poll(),epoll(Linux) 等技术,这是构建高性能服务器的关键,它们允许你在一个线程中监控多个套接字,当某个套接字准备好读写时,程序才会去处理它,避免了线程/进程创建和切换的开销。 - UDP 编程: 尝试使用
SOCK_DGRAM实现 UDP 服务器和客户端,你会发现流程比 TCP 简单得多,因为不需要listen()和accept(),直接sendto()和recvfrom()即可。 - 更复杂的应用: 尝试实现一个简单的聊天室、文件传输协议或 HTTP 服务器。
- 并发服务器: 上述服务器一次只能处理一个客户端,要同时处理多个客户端,你需要使用多线程 (
希望这份指南对你有帮助!动手编译和运行上面的代码是理解 Socket 编程最好的方式。
