前言
Linux网络编程的核心是伯克利套接字(Berkeley Socket),它封装了TCP/IP协议栈的底层细节,提供统一的跨平台API。本文基于实战场景,深入讲解Socket编程的核心流程、地址处理、TCP/UDP通信实现、IO多路复用(select/epoll)、非阻塞IO、边缘触发等高级特性,同时解决粘包、TIME_WAIT、连接超时等经典问题,搭配完整代码示例,帮你从入门到精通Linux网络编程。
前置知识:TCP/IP协议基础、Linux文件描述符、C语言基础、进程/线程概念
核心头文件:sys/socket.h、netinet/in.h、arpa/inet.h、sys/epoll.h、fcntl.h
编译说明:所有示例需链接Socket库(gcc 代码.c -o 可执行文件 -lpthread,多线程场景需加-lpthread)
一、Socket编程基础:地址处理与字节序转换
1.1 核心地址结构体
Socket编程中需通过地址结构体描述网络地址,IPv4主要使用struct sockaddr(通用地址)和struct sockaddr_in(IPv4专用地址),两者可强制类型转换。
结构体定义
#include <netinet/in.h>
#include <sys/socket.h>
// IPv4专用地址结构体(推荐使用)
struct sockaddr_in {
sa_family_t sin_family; // 地址族:AF_INET(IPv4)
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IPv4地址结构体
};
// IPv4地址结构体(存储32位网络字节序IP)
struct in_addr {
uint32_t s_addr; // IPv4地址(大端字节序)
};
// 通用地址结构体(接口参数通用类型)
struct sockaddr {
sa_family_t sa_family; // 地址族
char sa_data[14]; // 地址数据(含端口+IP)
};
关键说明
- sin_family必须设为AF_INET(IPv4),否则绑定/连接会失败;
- sin_port和sin_addr.s_addr必须是网络字节序(大端),不能直接使用主机字节序(小端)。
1.2 字节序转换(主机↔网络)
TCP/IP协议规定,网络传输中端口号、IP地址等控制信息必须使用大端字节序(网络字节序),而主机(x86架构)默认是小端字节序,需通过以下函数转换:
| uint16_t htons(uint16_t hostshort) | 主机字节序→网络字节序(16位,端口号) | 绑定/连接时设置端口 |
| uint32_t htonl(uint32_t hostlong) | 主机字节序→网络字节序(32位,IP地址) | 手动设置IP地址(如INADDR_ANY) |
| uint16_t ntohs(uint16_t netshort) | 网络字节序→主机字节序(16位) | 解析接收的端口号 |
| uint32_t ntohl(uint32_t netlong) | 网络字节序→主机字节序(32位) | 解析接收的IP地址 |
示例:端口号字节序转换
#include <arpa/inet.h>
#include <stdio.h>
int main() {
uint16_t host_port = 8080; // 主机字节序(小端)
uint16_t net_port = htons(host_port); // 转换为网络字节序(大端)
printf("主机字节序:%d → 网络字节序:%d\\n", host_port, net_port);
printf("网络字节序→主机字节序:%d\\n", ntohs(net_port)); // 反向转换
return 0;
}
1.3 IP地址转换(点分十进制↔网络字节序)
将用户输入的点分十进制IP(如192.168.1.1)转换为32位网络字节序,或反之,需使用以下函数:
| int inet_aton(const char *cp, struct in_addr *inp) | 点分十进制IP → 网络字节序IP | 是 |
| char *inet_ntoa(struct in_addr in) | 网络字节序IP → 点分十进制IP | 否(静态缓冲区) |
| const char *inet_ntop(int af, const void *src, char *dst, socklen_t size) | 网络字节序→字符串(支持IPv4/IPv6) | 是 |
| int inet_pton(int af, const char *src, void *dst) | 字符串→网络字节序(支持IPv4/IPv6) | 是 |
示例:IP地址转换
#include <arpa/inet.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "用法:%s 点分十进制IP\\n", argv[0]);
return 1;
}
struct sockaddr_in addr;
// 点分十进制 → 网络字节序
int ret = inet_aton(argv[1], &addr.sin_addr);
if (ret == 0) {
fprintf(stderr, "无效IP地址\\n");
return 1;
}
printf("网络字节序IP:%x\\n", addr.sin_addr.s_addr);
// 网络字节序 → 点分十进制(线程不安全)
printf("点分十进制IP(inet_ntoa):%s\\n", inet_ntoa(addr.sin_addr));
// 线程安全版本(inet_ntop)
char ip_buf[16];
inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));
printf("点分十进制IP(inet_ntop):%s\\n", ip_buf);
return 0;
}
1.4 域名解析(gethostbyname)
通过域名(如www.baidu.com)获取对应的IP地址,使用gethostbyname函数(IPv4专用):
函数原型与结构体
#include <netdb.h>
struct hostent *gethostbyname(const char *name);
// 域名解析结果结构体
struct hostent {
char *h_name; // 主机正式名称
char **h_aliases; // 主机别名列表(如www.baidu.com→www.a.shifen.com)
int h_addrtype; // 地址类型(AF_INET=IPv4)
int h_length; // 地址长度(IPv4为4字节)
char **h_addr_list; // IP地址列表(网络字节序)
};
示例:域名解析
#include <netdb.h>
#include <arpa/inet.h>
#include <stdio.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
fprintf(stderr, "用法:%s 域名\\n", argv[0]);
return 1;
}
// 解析域名
struct hostent *host = gethostbyname(argv[1]);
if (host == NULL) {
herror("gethostbyname failed"); // 打印域名解析错误
return 1;
}
// 打印解析结果
printf("正式名称:%s\\n", host->h_name);
printf("别名列表:");
for (int i = 0; host->h_aliases[i] != NULL; i++) {
printf("%s ", host->h_aliases[i]);
}
printf("\\n地址类型:%d(AF_INET=%d)\\n", host->h_addrtype, AF_INET);
printf("IP地址列表:");
char ip_buf[16];
for (int i = 0; host->h_addr_list[i] != NULL; i++) {
inet_ntop(host->h_addrtype, host->h_addr_list[i], ip_buf, sizeof(ip_buf));
printf("%s ", ip_buf);
}
printf("\\n");
return 0;
}
二、TCP Socket编程:连接、通信与粘包处理
TCP是面向连接、可靠的流式协议,编程流程分为服务端和客户端,核心流程:创建Socket→绑定/连接→监听/接受→读写数据→关闭Socket。
2.1 TCP服务端核心流程
完整示例:TCP服务端(基础版)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define PORT 8080
#define BUF_SIZE 1024
// 错误检查宏
#define ERROR_CHECK(ret, msg) { \\
if (ret == –1) { \\
perror(msg); \\
exit(1); \\
} \\
}
int main() {
// 1. 创建TCP Socket(监听套接字)
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(listen_fd, "socket failed");
// 2. 设置地址重用(解决TIME_WAIT端口占用)
int reuse = 1;
int ret = setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
ERROR_CHECK(ret, "setsockopt failed");
// 3. 配置服务端地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网卡IP
serv_addr.sin_port = htons(PORT); // 端口转换为网络字节序
// 4. 绑定地址
ret = bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
ERROR_CHECK(ret, "bind failed");
// 5. 监听连接(backlog=10,全连接队列长度)
ret = listen(listen_fd, 10);
ERROR_CHECK(ret, "listen failed");
printf("服务端启动,监听端口 %d…\\n", PORT);
// 6. 接受客户端连接(阻塞,返回已连接套接字)
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
ERROR_CHECK(conn_fd, "accept failed");
printf("客户端连接:IP=%s, 端口=%d\\n",
inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
// 7. 读写数据(Echo服务:接收后原样返回)
char buf[BUF_SIZE];
while (1) {
memset(buf, 0, sizeof(buf));
// 接收客户端数据(阻塞)
ret = recv(conn_fd, buf, sizeof(buf), 0);
if (ret == 0) {
printf("客户端断开连接\\n");
break;
}
ERROR_CHECK(ret, "recv failed");
printf("收到客户端数据:%s\\n", buf);
// 回显数据给客户端
ret = send(conn_fd, buf, strlen(buf), 0);
ERROR_CHECK(ret, "send failed");
}
// 8. 关闭Socket
close(conn_fd); // 关闭已连接套接字
close(listen_fd); // 关闭监听套接字
return 0;
}
2.2 TCP客户端核心流程
完整示例:TCP客户端(基础版)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUF_SIZE 1024
#define ERROR_CHECK(ret, msg) { \\
if (ret == –1) { \\
perror(msg); \\
exit(1); \\
} \\
}
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "用法:%s 服务端IP 服务端端口\\n", argv[0]);
return 1;
}
// 1. 创建TCP Socket
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(sock_fd, "socket failed");
// 2. 配置服务端地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
// 点分十进制IP → 网络字节序
ret = inet_aton(argv[1], &serv_addr.sin_addr);
ERROR_CHECK(ret, "inet_aton failed");
// 端口号 → 网络字节序
serv_addr.sin_port = htons(atoi(argv[2]));
// 3. 连接服务端(阻塞)
ret = connect(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
ERROR_CHECK(ret, "connect failed");
printf("连接服务端 %s:%d 成功\\n", argv[1], atoi(argv[2]));
// 4. 读写数据
char buf[BUF_SIZE];
while (1) {
printf("请输入发送数据(输入quit退出):");
fgets(buf, sizeof(buf), stdin);
// 去除换行符
buf[strcspn(buf, "\\n")] = '\\0';
if (strcmp(buf, "quit") == 0) {
break;
}
// 发送数据
ret = send(sock_fd, buf, strlen(buf), 0);
ERROR_CHECK(ret, "send failed");
// 接收服务端回显数据
memset(buf, 0, sizeof(buf));
ret = recv(sock_fd, buf, sizeof(buf), 0);
if (ret == 0) {
printf("服务端断开连接\\n");
break;
}
ERROR_CHECK(ret, "recv failed");
printf("服务端回显:%s\\n", buf);
}
// 5. 关闭Socket
close(sock_fd);
return 0;
}
2.3 TCP粘包与半包问题(核心难点)
TCP是流式协议,消息无边界,可能出现“粘包”(多个消息合并接收)或“半包”(一个消息分多次接收),解决方案是自定义消息协议,常用两种方式:
方案1:固定消息长度
约定每个消息的固定长度(如1024字节),接收方每次读取固定长度,不足则补0,超过则截断(需处理剩余数据)。
方案2:消息头+消息体(推荐)
消息分为两部分:
- 消息头:固定4字节,存储消息体长度(网络字节序);
- 消息体:实际数据,长度由消息头指定。
示例:消息头+消息体协议实现(解决粘包)
// 发送消息(添加消息头)
int send_msg(int sock_fd, const char* data) {
int data_len = strlen(data);
// 消息头:4字节,存储消息体长度(网络字节序)
uint32_t len_net = htonl(data_len);
// 先发送消息头
int ret = send(sock_fd, &len_net, sizeof(len_net), 0);
ERROR_CHECK(ret, "send header failed");
// 再发送消息体
ret = send(sock_fd, data, data_len, 0);
ERROR_CHECK(ret, "send body failed");
return 0;
}
// 接收消息(解析消息头+消息体)
int recv_msg(int sock_fd, char* buf, int buf_size) {
// 先接收消息头(4字节)
uint32_t len_net;
int ret = recv(sock_fd, &len_net, sizeof(len_net), 0);
if (ret == 0) return –1; // 客户端断开
ERROR_CHECK(ret, "recv header failed");
// 网络字节序 → 主机字节序
int data_len = ntohl(len_net);
if (data_len > buf_size) {
fprintf(stderr, "消息体过长,缓冲区不足\\n");
return –1;
}
// 再接收消息体
ret = recv(sock_fd, buf, data_len, 0);
ERROR_CHECK(ret, "recv body failed");
buf[data_len] = '\\0'; // 字符串结束符
return 0;
}
2.4 TIME_WAIT状态与地址重用
服务端主动关闭连接后,会进入TIME_WAIT状态(默认60秒),期间端口被占用,无法重新绑定。解决方案是设置SO_REUSEADDR选项:
int reuse = 1;
// 必须在bind之前调用
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
TIME_WAIT的意义:
三、UDP Socket编程:无连接通信
UDP是无连接、不可靠的数据包协议,无需建立连接,直接发送数据,适用于实时性要求高的场景(如游戏、视频通话)。
3.1 UDP核心流程
- 服务端:创建Socket→绑定地址→recvfrom接收数据→sendto发送数据→关闭Socket;
- 客户端:创建Socket→sendto发送数据→recvfrom接收数据→关闭Socket。
3.2 核心函数(sendto/recvfrom)
// 发送UDP数据包
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
// 接收UDP数据包(获取对端地址)
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
关键说明
- UDP无需连接,发送时需指定对端地址(dest_addr);
- 接收时通过src_addr获取对端地址,后续可通过该地址回复;
- UDP保留消息边界,发送方发送n个数据包,接收方最多接收n个(可能丢包,但不会合并)。
3.3 完整示例:UDP服务端与客户端
UDP服务端
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define PORT 8080
#define BUF_SIZE 1024
#define ERROR_CHECK(ret, msg) { \\
if (ret == –1) { \\
perror(msg); \\
exit(1); \\
} \\
}
int main() {
// 1. 创建UDP Socket
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(sock_fd, "socket failed");
// 2. 配置服务端地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(PORT);
// 3. 绑定地址(UDP必须绑定,否则无法接收数据)
int ret = bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
ERROR_CHECK(ret, "bind failed");
printf("UDP服务端启动,监听端口 %d…\\n", PORT);
// 4. 接收并回复数据
char buf[BUF_SIZE];
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
while (1) {
memset(buf, 0, sizeof(buf));
// 接收UDP数据包(阻塞)
ret = recvfrom(sock_fd, buf, sizeof(buf), 0,
(struct sockaddr*)&cli_addr, &cli_len);
ERROR_CHECK(ret, "recvfrom failed");
printf("收到客户端 %s:%d 数据:%s\\n",
inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port), buf);
// 回复数据给客户端
char reply[BUF_SIZE];
snprintf(reply, sizeof(reply), "UDP回复:%s", buf);
ret = sendto(sock_fd, reply, strlen(reply), 0,
(struct sockaddr*)&cli_addr, cli_len);
ERROR_CHECK(ret, "sendto failed");
}
// 5. 关闭Socket(实际不会执行,需手动终止)
close(sock_fd);
return 0;
}
UDP客户端
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUF_SIZE 1024
#define ERROR_CHECK(ret, msg) { \\
if (ret == –1) { \\
perror(msg); \\
exit(1); \\
} \\
}
int main(int argc, char* argv[]) {
if (argc != 3) {
fprintf(stderr, "用法:%s 服务端IP 服务端端口\\n", argv[0]);
return 1;
}
// 1. 创建UDP Socket
int sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
ERROR_CHECK(sock_fd, "socket failed");
// 2. 配置服务端地址
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
ret = inet_aton(argv[1], &serv_addr.sin_addr);
ERROR_CHECK(ret, "inet_aton failed");
serv_addr.sin_port = htons(atoi(argv[2]));
socklen_t serv_len = sizeof(serv_addr);
// 3. 发送并接收数据
char buf[BUF_SIZE];
while (1) {
printf("请输入发送数据(输入quit退出):");
fgets(buf, sizeof(buf), stdin);
buf[strcspn(buf, "\\n")] = '\\0';
if (strcmp(buf, "quit") == 0) {
break;
}
// 发送UDP数据包
ret = sendto(sock_fd, buf, strlen(buf), 0,
(struct sockaddr*)&serv_addr, serv_len);
ERROR_CHECK(ret, "sendto failed");
// 接收服务端回复
memset(buf, 0, sizeof(buf));
ret = recvfrom(sock_fd, buf, sizeof(buf), 0, NULL, NULL);
ERROR_CHECK(ret, "recvfrom failed");
printf("服务端回复:%s\\n", buf);
}
// 4. 关闭Socket
close(sock_fd);
return 0;
}
四、IO多路复用:select与epoll(高并发核心)
单线程/进程同时监听多个文件描述符(如多个Socket、标准输入),需使用IO多路复用机制,避免阻塞在单个描述符上,核心工具是select和epoll(Linux特有,高并发首选)。
4.1 select:基础IO多路复用
select通过位图监听读、写、异常事件,适用于描述符数量较少的场景(默认上限1024)。
核心函数
#include <sys/select.h>
int select(int maxfd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 位图操作宏
void FD_ZERO(fd_set *set); // 清空位图
void FD_SET(int fd, fd_set *set); // 添加描述符到位图
void FD_CLR(int fd, fd_set *set); // 从位图移除描述符
int FD_ISSET(int fd, fd_set *set); // 检查描述符是否就绪
示例:select实现TCP即时聊天(服务端)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#define PORT 8080
#define BUF_SIZE 1024
#define ERROR_CHECK(ret, msg) { \\
if (ret == –1) { \\
perror(msg); \\
exit(1); \\
} \\
}
int main() {
// 1. 创建监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(listen_fd, "socket failed");
// 2. 地址重用+绑定+监听
int reuse = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_in serv_addr = {AF_INET, htons(PORT), htonl(INADDR_ANY)};
bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(listen_fd, 10);
printf("服务端启动,监听端口 %d…\\n", PORT);
// 3. 初始化select监听集合
fd_set read_set, temp_set;
FD_ZERO(&read_set);
FD_SET(listen_fd, &read_set); // 监听监听套接字
FD_SET(STDIN_FILENO, &read_set); // 监听标准输入
int max_fd = listen_fd; // 最大文件描述符
char buf[BUF_SIZE];
int conn_fd = –1;
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
while (1) {
// select会修改位图,每次需拷贝临时集合
temp_set = read_set;
// 阻塞监听(timeout=NULL)
int ready_num = select(max_fd + 1, &temp_set, NULL, NULL, NULL);
ERROR_CHECK(ready_num, "select failed");
// 遍历就绪描述符
for (int fd = 0; fd <= max_fd; fd++) {
if (FD_ISSET(fd, &temp_set)) {
if (fd == listen_fd) {
// 4. 监听套接字就绪:接收新连接
conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
ERROR_CHECK(conn_fd, "accept failed");
printf("客户端连接:%s:%d\\n",
inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
FD_SET(conn_fd, &read_set); // 添加到监听集合
if (conn_fd > max_fd) max_fd = conn_fd; // 更新最大描述符
} else if (fd == STDIN_FILENO) {
// 5. 标准输入就绪:发送数据给客户端
memset(buf, 0, sizeof(buf));
read(fd, buf, sizeof(buf));
buf[strcspn(buf, "\\n")] = '\\0';
if (conn_fd != –1) {
send(conn_fd, buf, strlen(buf), 0);
}
} else if (fd == conn_fd) {
// 6. 已连接套接字就绪:接收客户端数据
memset(buf, 0, sizeof(buf));
int ret = recv(fd, buf, sizeof(buf), 0);
if (ret == 0) {
printf("客户端断开连接\\n");
close(fd);
FD_CLR(fd, &read_set);
conn_fd = –1;
continue;
}
ERROR_CHECK(ret, "recv failed");
printf("客户端数据:%s\\n", buf);
}
}
}
}
close(listen_fd);
return 0;
}
select的缺点
4.2 epoll:高并发首选IO多路复用
epoll是Linux特有机制,解决select的缺点,支持海量描述符(无上限)、边缘触发(ET)、高效事件通知,适用于高并发场景(如Nginx、Redis)。
核心原理
- 内核维护两个数据结构:红黑树(监听事件集合)、就绪事件队列(链表);
- 监听事件通过epoll_ctl添加到红黑树,无需每次拷贝;
- 事件就绪时,内核将描述符加入就绪队列,用户态直接遍历队列,无需轮询。
核心函数
#include <sys/epoll.h>
// 创建epoll实例(返回epoll文件描述符)
int epoll_create(int size); // size已废弃,填>0即可
// 管理监听事件(添加/修改/删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// op:EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DEL(删除)
// event:监听事件(如EPOLLIN=读事件)
// 等待事件就绪(阻塞)
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// events:输出参数,存储就绪事件
// maxevents:最大就绪事件数
// timeout:超时时间(-1=永久阻塞,0=非阻塞)
// 事件结构体
struct epoll_event {
uint32_t events; // 监听事件(EPOLLIN/EPOLLOUT/EPOLLET等)
epoll_data_t data; // 用户数据(通常存储文件描述符)
};
// 用户数据联合体
typedef union epoll_data {
void *ptr;
int fd; // 常用:存储文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
示例:epoll实现TCP高并发服务端(支持多客户端)
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define PORT 8080
#define BUF_SIZE 1024
#define MAX_EVENTS 1024 // 最大就绪事件数
#define ERROR_CHECK(ret, msg) { \\
if (ret == –1) { \\
perror(msg); \\
exit(1); \\
} \\
}
// 设置文件描述符为非阻塞
void set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL);
ERROR_CHECK(flags, "fcntl F_GETFL failed");
flags |= O_NONBLOCK;
int ret = fcntl(fd, F_SETFL, flags);
ERROR_CHECK(ret, "fcntl F_SETFL failed");
}
int main() {
// 1. 创建监听套接字
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
ERROR_CHECK(listen_fd, "socket failed");
// 2. 地址重用+绑定+监听
int reuse = 1;
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
struct sockaddr_in serv_addr = {AF_INET, htons(PORT), htonl(INADDR_ANY)};
bind(listen_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
listen(listen_fd, 1024);
printf("epoll服务端启动,监听端口 %d…\\n", PORT);
// 3. 创建epoll实例
int epfd = epoll_create(1);
ERROR_CHECK(epfd, "epoll_create failed");
// 4. 添加监听套接字到epoll(水平触发LT,默认)
struct epoll_event ev;
ev.events = EPOLLIN; // 监听读事件
ev.data.fd = listen_fd;
int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);
ERROR_CHECK(ret, "epoll_ctl add listen_fd failed");
// 5. 循环等待事件就绪
struct epoll_event events[MAX_EVENTS];
while (1) {
// 阻塞等待事件(timeout=-1)
int ready_num = epoll_wait(epfd, events, MAX_EVENTS, –1);
ERROR_CHECK(ready_num, "epoll_wait failed");
// 遍历就绪事件
for (int i = 0; i < ready_num; i++) {
int fd = events[i].data.fd;
if (fd == listen_fd) {
// 6. 新连接就绪:接收连接
struct sockaddr_in cli_addr;
socklen_t cli_len = sizeof(cli_addr);
int conn_fd = accept(listen_fd, (struct sockaddr*)&cli_addr, &cli_len);
ERROR_CHECK(conn_fd, "accept failed");
printf("客户端连接:%s:%d\\n",
inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
// 设置非阻塞(配合边缘触发ET)
set_nonblock(conn_fd);
// 添加到epoll(边缘触发ET)
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_fd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, conn_fd, &ev);
ERROR_CHECK(ret, "epoll_ctl add conn_fd failed");
} else if (events[i].events & EPOLLIN) {
// 7. 读事件就绪:接收客户端数据
char buf[BUF_SIZE];
memset(buf, 0, sizeof(buf));
while (1) {
// 非阻塞读(边缘触发需循环读完缓冲区)
ret = recv(fd, buf, sizeof(buf), 0);
if (ret > 0) {
// 回显数据
send(fd, buf, ret, 0);
printf("客户端 %d 数据:%s\\n", fd, buf);
} else if (ret == 0) {
// 客户端断开
printf("客户端 %d 断开连接\\n", fd);
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
} else if (ret == –1 && errno == EAGAIN) {
// 缓冲区已读完,退出循环
break;
} else {
// 其他错误
perror("recv failed");
close(fd);
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
break;
}
}
}
}
}
// 8. 关闭资源
close(listen_fd);
close(epfd);
return 0;
}
4.3 水平触发(LT)与边缘触发(ET)
epoll支持两种触发模式,核心区别在于“事件就绪的通知方式”:
| 水平触发(LT,默认) | 只要缓冲区有数据/可写,就持续通知 | 编程简单,无需循环读/写 | 普通场景,无需高并发 |
| 边缘触发(ET) | 仅当缓冲区状态变化时通知(如数据从无到有) | 效率高,仅通知一次,需配合非阻塞IO循环读/写 | 高并发场景,减少通知次数 |
ET模式注意事项:
五、Socket高级特性:非阻塞IO与属性调整
5.1 非阻塞IO
默认Socket操作(accept/recv/send)是阻塞的,设置为非阻塞后,无数据/无法写时会立即返回-1,并设置errno=EAGAIN,需配合循环使用。
设置非阻塞IO(fcntl函数)
void set_nonblock(int fd) {
int flags = fcntl(fd, F_GETFL); // 获取当前状态
flags |= O_NONBLOCK; // 添加非阻塞标志
fcntl(fd, F_SETFL, flags); // 设置新状态
}
示例:非阻塞recv
char buf[BUF_SIZE];
while (1) {
ret = recv(fd, buf, sizeof(buf), 0);
if (ret > 0) {
// 处理数据
} else if (ret == –1 && errno == EAGAIN) {
// 无数据,退出循环
break;
} else {
// 错误或断开
break;
}
}
5.2 Socket属性调整(setsockopt/getsockopt)
通过setsockopt调整Socket属性,常用属性如下:
| SO_REUSEADDR | 地址重用(解决TIME_WAIT) | setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) |
| SO_RCVBUF/SO_SNDBUF | 调整接收/发送缓冲区大小 | int bufsize=8192; setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize)) |
| SO_RCVLOWAT/SO_SNDLOWAT | 接收/发送缓冲区下限(低于则不触发IO) | int lowat=10; setsockopt(fd, SOL_SOCKET, SO_RCVLOWAT, &lowat, sizeof(lowat)) |
| SO_LINGER | 关闭连接时的延迟时间(控制TIME_WAIT) | struct linger l={1, 5}; setsockopt(fd, SOL_SOCKET, SO_LINGER, &l, sizeof(l)) |
5.3 recv/send标志位
recv/send的flags参数支持特殊功能,常用标志:
| MSG_DONTWAIT | 临时非阻塞IO(不修改Socket属性) | recv(fd, buf, len, MSG_DONTWAIT) |
| MSG_PEEK | 预览缓冲区数据(不移除数据) | recv(fd, buf, len, MSG_PEEK)(下次仍可读取该数据) |
| MSG_OOB | 发送/接收紧急数据 | send(fd, buf, len, MSG_OOB) |
示例:MSG_PEEK预览数据
// 预览缓冲区数据(不移除)
recv(fd, buf, sizeof(buf), MSG_PEEK);
printf("预览数据:%s\\n", buf);
// 再次读取(仍能获取相同数据)
recv(fd, buf, sizeof(buf), 0);
printf("实际读取数据:%s\\n", buf);
六、核心总结与进阶方向
6.1 核心知识点总结
6.2 进阶学习方向
标签:Linux网络编程、Socket、TCP/UDP、epoll、IO多路复用、高并发、非阻塞IO
声明:本文基于Linux x86_64平台,所有示例代码在Ubuntu 20.04/22.04测试通过,核心API在所有Linux发行版中通用。当前文件内容过长,豆包只阅读了前 15%。




