欢迎光临
我们一直在努力

Linux网络编程实战:Socket、IO多路复用与高级特性全指南

前言

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服务端核心流程

  • 创建Socket:socket(AF_INET, SOCK_STREAM, 0);
  • 设置地址重用:解决TIME_WAIT状态下端口占用问题;
  • 绑定地址:bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
  • 监听连接:listen(sockfd, backlog)(设置全连接队列长度);
  • 接受连接:accept(sockfd, NULL, NULL)(阻塞,返回已连接Socket);
  • 读写数据:send/recv或read/write;
  • 关闭Socket:close(已连接Socket)→close(监听Socket)。
  • 完整示例: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客户端核心流程

  • 创建Socket:socket(AF_INET, SOCK_STREAM, 0);
  • 配置服务端地址:设置服务端IP和端口;
  • 连接服务端:connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
  • 读写数据:send/recv;
  • 关闭Socket:close(sockfd)。
  • 完整示例: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的意义:

  • 确保最后一个ACK被服务端接收(避免客户端重传FIN);
  • 等待网络中残留的旧报文消逝(避免新连接收到旧报文)。
  • 三、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的缺点
  • 描述符上限(默认1024);
  • 每次调用需拷贝位图到内核态,效率低;
  • 就绪后需遍历所有描述符,浪费CPU。
  • 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设为非阻塞,否则循环读/写会阻塞;
  • 必须循环读/写直到返回EAGAIN(缓冲区为空/满),否则会丢失数据。
  • 五、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 核心知识点总结

  • Socket基础:地址结构体(sockaddr_in)、字节序转换(htons/ntohs)、IP转换(inet_aton/inet_ntoa)是必备基础;
  • TCP编程:面向连接,需处理粘包/半包(自定义协议)、TIME_WAIT(地址重用);
  • UDP编程:无连接,保留消息边界,无需连接流程,适用于实时性场景;
  • IO多路复用:select适用于少量描述符,epoll(ET模式+非阻塞IO)是高并发首选;
  • 高级特性:非阻塞IO、Socket属性调整、标志位功能,可优化程序性能和稳定性。
  • 6.2 进阶学习方向

  • 并发模型:多线程/多进程+epoll、线程池+epoll(处理高并发连接);
  • 网络安全:SSL/TLS加密(OpenSSL库)、HTTPS协议实现;
  • 高级协议:HTTP/2、WebSocket、RESTful API设计;
  • 性能优化:TCP参数调优(如拥塞控制算法、缓冲区大小)、网络延迟优化;
  • 分布式网络:Socket集群、负载均衡、微服务通信(gRPC/Thrift)。

  • 标签:Linux网络编程、Socket、TCP/UDP、epoll、IO多路复用、高并发、非阻塞IO
    声明:本文基于Linux x86_64平台,所有示例代码在Ubuntu 20.04/22.04测试通过,核心API在所有Linux发行版中通用。当前文件内容过长,豆包只阅读了前 15%。

    赞(0)
    未经允许不得转载:171主机测评 » Linux网络编程实战:Socket、IO多路复用与高级特性全指南
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址