上一篇我们掌握了多线程编程与线程池。当设备需要对外提供网络服务时,网络编程就是绕不开的课题。本篇将从TCP Socket基础开始,逐步深入到I/O多路复用技术(select/poll/epoll),最终实现一个支持高并发的回声服务器。所有代码均交叉编译在ARM开发板上验证通过。

一、引言
嵌入式设备最常见的网络场景:传感器数据上报、远程控制指令接收、Web配置界面。这些都需要网络通信。Linux下网络编程的核心是Socket API,而要应对成千上万个并发连接,就需要掌握I/O多路复用。
本文采用循序渐进的方式:先写一个最简单的TCP回声服务器,再逐渐优化为多进程、多线程版本,最后用epoll实现单线程高并发,让你彻底理解网络编程的演进脉络。
二、网络编程基础
2.1 TCP/IP协议栈简图
应用层 (HTTP, MQTT, 自定义协议)
传输层 (TCP, UDP)
网络层 (IP)
链路层 (Ethernet, Wi-Fi)
我们编写的Socket程序工作在应用层,使用传输层提供的TCP或UDP服务。
2.2 Socket是什么?
Socket是操作系统提供的一个编程接口 ,用于访问网络协议栈。对应用层来说,Socket就是一个文件描述符,可以用read/write(或专用的send/recv)进行数据传输。
2.3 TCP通信流程
服务器端 客户端
socket() socket()
↓ ↓
bind() (可选的bind)
↓ ↓
listen() connect()
↓ ↓
accept() ←--------- 三次握手 ------------→ ↓
↓ ↓
read()/write() ←------ 数据传输 ------→ read()/write()
↓ ↓
close() close()
三、核心Socket API
3.1 socket() ------ 创建套接字
c
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
domain:AF_INET(IPv4)、AF_INET6(IPv6)type:SOCK_STREAM(TCP,字节流)、SOCK_DGRAM(UDP,数据报)protocol:通常设为0,自动选择协议- 返回值:文件描述符,失败返回-1
3.2 bind() ------ 绑定地址
c
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
用于服务器将Socket绑定到特定IP和端口。地址结构体:
c
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址
};
3.3 listen() ------ 开始监听
c
int listen(int sockfd, int backlog);
backlog:未完成连接队列(SYN队列)与已完成连接队列(accept队列)之和的最大值。Linux 5.4+默认4096。
3.4 accept() ------ 接受连接
c
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 默认阻塞,直到有客户端连接
- 返回新的套接字(用于与该客户端通信),失败返回-1
3.5 connect() ------ 客户端连接服务器
c
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
3.6 数据收发函数
c
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);
ssize_t read(int sockfd, void *buf, size_t count); // 也可用
ssize_t write(int sockfd, const void *buf, size_t count);
3.7 字节序转换函数
网络字节序使用大端(Big-Endian),而主机可能是小端(如x86、ARM)。需要转换:
c
#include <arpa/inet.h>
uint32_t htonl(uint32_t hostlong); // Host to Network Long
uint16_t htons(uint16_t hostshort); // Host to Network Short
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
// IP地址转换
int inet_pton(int af, const char *src, void *dst); // 字符串 → 二进制
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 二进制 → 字符串
四、实战案例演进
4.1 最简TCP回声服务器(单连接,阻塞)
只能处理一个客户端,处理完才接受下一个。
c
/**
* tcp_echo_basic.c ------ 最简TCP回声服务器
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o tcp_echo_basic tcp_echo_basic.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main(void)
{
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
ssize_t n;
/* 1. 创建socket */
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
exit(EXIT_FAILURE);
}
/* 设置地址复用,避免重启时TIME_WAIT导致bind失败 */
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* 2. 绑定地址和端口 */
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有网卡
server_addr.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("bind");
close(server_fd);
exit(EXIT_FAILURE);
}
/* 3. 监听 */
if (listen(server_fd, 5) == -1) {
perror("listen");
close(server_fd);
exit(EXIT_FAILURE);
}
printf("服务器启动,监听端口 %d...\n", PORT);
/* 4. 接受连接(循环) */
while (1) {
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd == -1) {
perror("accept");
continue;
}
printf("客户端连接: %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
/* 5. 回声服务:读多少,回多少 */
while ((n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0)) > 0) {
buffer[n] = '\0';
printf("收到: %s", buffer);
send(client_fd, buffer, n, 0);
}
printf("客户端断开\n");
close(client_fd);
}
close(server_fd);
return 0;
}
4.2 多进程版本(每个连接fork一个子进程)
解决了并发问题,但每次fork开销大,进程数有限。
c
/**
* tcp_echo_fork.c ------ 多进程TCP回声服务器
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o tcp_echo_fork tcp_echo_fork.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <signal.h>
#define PORT 8080
#define BUFFER_SIZE 1024
/* SIGCHLD处理:回收僵尸进程 */
void sigchld_handler(int sig) {
(void)sig;
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(void)
{
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
struct sigaction sa;
/* 注册SIGCHLD处理 */
memset(&sa, 0, sizeof(sa));
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sigaction(SIGCHLD, &sa, NULL);
server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, 5);
printf("多进程服务器启动,端口 %d\n", PORT);
while (1) {
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd == -1) {
perror("accept");
continue;
}
pid_t pid = fork();
if (pid == 0) {
/* 子进程 */
close(server_fd); // 不需要监听socket
char buffer[BUFFER_SIZE];
ssize_t n;
while ((n = recv(client_fd, buffer, BUFFER_SIZE - 1, 0)) > 0) {
buffer[n] = '\0';
send(client_fd, buffer, n, 0);
}
close(client_fd);
_exit(0);
} else {
close(client_fd); // 父进程关闭已accept的socket,继续监听
}
}
close(server_fd);
return 0;
}
4.3 I/O多路复用:select版本
select允许一个线程同时监听多个文件描述符的可读、可写、异常事件。
c
/**
* tcp_echo_select.c ------ select实现多客户端回声
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o tcp_echo_select tcp_echo_select.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define MAX_CLIENTS FD_SETSIZE // 通常1024
int main(void)
{
int server_fd, client_fd, max_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
fd_set readfds, master_fds;
char buffer[1024];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, 5);
printf("select服务器启动,端口 %d\n", PORT);
FD_ZERO(&master_fds);
FD_SET(server_fd, &master_fds);
max_fd = server_fd;
while (1) {
readfds = master_fds; // 每次都要复制,因为select会修改
if (select(max_fd + 1, &readfds, NULL, NULL, NULL) == -1) {
perror("select");
break;
}
/* 遍历所有fd */
for (int fd = 0; fd <= max_fd; fd++) {
if (!FD_ISSET(fd, &readfds)) continue;
if (fd == server_fd) {
/* 新连接 */
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd != -1) {
FD_SET(client_fd, &master_fds);
if (client_fd > max_fd) max_fd = client_fd;
printf("新连接 fd=%d\n", client_fd);
}
} else {
/* 客户端数据 */
ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n <= 0) {
if (n == 0) printf("客户端断开 fd=%d\n", fd);
else perror("recv");
close(fd);
FD_CLR(fd, &master_fds);
if (fd == max_fd) {
/* 更新max_fd */
while (max_fd >= 0 && !FD_ISSET(max_fd, &master_fds))
max_fd--;
}
} else {
buffer[n] = '\0';
send(fd, buffer, n, 0);
}
}
}
}
close(server_fd);
return 0;
}
4.4 I/O多路复用:poll版本
poll使用可变长度的pollfd数组,突破了1024限制。
c
/**
* tcp_echo_poll.c ------ poll实现多客户端回声
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o tcp_echo_poll tcp_echo_poll.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <poll.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 8080
#define MAX_CLIENTS 1024
int main(void)
{
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
struct pollfd fds[MAX_CLIENTS + 1]; // +1 for server_fd
int nfds = 0;
char buffer[1024];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, 5);
printf("poll服务器启动,端口 %d\n", PORT);
/* 初始化:添加监听socket */
memset(fds, 0, sizeof(fds));
fds[0].fd = server_fd;
fds[0].events = POLLIN;
nfds = 1;
while (1) {
int ret = poll(fds, nfds, -1);
if (ret == -1) {
perror("poll");
break;
}
/* 遍历所有fd */
for (int i = 0; i < nfds; i++) {
if (!(fds[i].revents & POLLIN)) continue;
if (fds[i].fd == server_fd) {
/* 新连接 */
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd != -1) {
fds[nfds].fd = client_fd;
fds[nfds].events = POLLIN;
nfds++;
printf("新连接 fd=%d\n", client_fd);
}
} else {
/* 客户端数据 */
int fd = fds[i].fd;
ssize_t n = recv(fd, buffer, sizeof(buffer) - 1, 0);
if (n <= 0) {
if (n == 0) printf("客户端断开 fd=%d\n", fd);
else perror("recv");
close(fd);
/* 删除该fd:与最后一个交换后nfds-- */
fds[i] = fds[nfds - 1];
nfds--;
i--; // 重新检查当前位置
} else {
buffer[n] = '\0';
send(fd, buffer, n, 0);
}
}
}
}
close(server_fd);
return 0;
}
4.5 I/O多路复用:epoll版本(高并发核心)
epoll是Linux特有的高性能I/O多路复用机制,复杂度O(1)。
c
/**
* tcp_echo_epoll.c ------ epoll实现高并发回声服务器
* 编译: arm-linux-gnueabihf-gcc -Wall -g -o tcp_echo_epoll tcp_echo_epoll.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <fcntl.h>
#define PORT 8080
#define MAX_EVENTS 1024
#define BUFFER_SIZE 4096
/* 设置非阻塞 */
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
int main(void)
{
int server_fd, client_fd, epoll_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
struct epoll_event ev, events[MAX_EVENTS];
char buffer[BUFFER_SIZE];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
set_nonblocking(server_fd);
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
listen(server_fd, SOMAXCONN);
/* 创建epoll实例 */
epoll_fd = epoll_create1(0);
ev.events = EPOLLIN;
ev.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
printf("epoll服务器启动,端口 %d (ET模式)\n", PORT);
while (1) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
break;
}
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
/* 边缘触发:循环accept直到EAGAIN */
while (1) {
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &addr_len);
if (client_fd == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
break; // 已全部accept
perror("accept");
break;
}
printf("新连接: %s:%d fd=%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), client_fd);
set_nonblocking(client_fd);
ev.events = EPOLLIN | EPOLLET; // 边缘触发
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
}
} else if (events[i].events & EPOLLIN) {
/* 处理客户端数据(边缘触发,需循环读) */
int fd = events[i].data.fd;
while (1) {
ssize_t n = recv(fd, buffer, sizeof(buffer), 0);
if (n > 0) {
/* 回声:原样返回 */
send(fd, buffer, n, 0);
} else if (n == 0) {
printf("客户端断开 fd=%d\n", fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
} else {
if (errno == EAGAIN)
break; // 数据读完
perror("recv");
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
}
}
}
}
close(server_fd);
close(epoll_fd);
return 0;
}
epoll核心优势:
- 只返回就绪事件,不需要遍历所有fd
- 红黑树管理监听的fd,增删改查O(logN)
- 支持ET(边缘触发)和LT(水平触发)两种模式
五、select/poll/epoll对比
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大并发数 | 1024 (FD_SETSIZE) | 无限制 | 无限制 |
| 效率 | O(n)遍历 | O(n)遍历 | O(1)回调 |
| 内存复制 | 每次wait复制整个集合 | 每次poll复制整个数组 | 只返回就绪事件 |
| 触发模式 | 水平触发 | 水平触发 | 水平触发(LT) + 边缘触发(ET) |
| 适用场景 | 少量连接 | 中量连接 | 大量并发连接 |
六、最佳实践与常见陷阱
| 陷阱 | 对策 |
|---|---|
| ET模式下只读一次,导致数据残留 | 循环读取直到EAGAIN |
忘记设置SO_REUSEADDR,重启报错 |
监听socket务必设置 |
send/recv返回值<期望值 |
封装send_all/recv_all |
| 客户端突然断开(拔网线) | 定期心跳检测或设置SO_KEEPALIVE |
| 多线程处理同一fd | 使用EPOLLONESHOT |
| 惊群问题(多进程/线程epoll同一fd) | 内核4.5+使用EPOLLEXCLUSIVE |
七、总结与思考题
本篇从最简TCP回声服务器开始,逐步引入多进程、select/poll/epoll,完整呈现了高并发服务器的演进路径。epoll版本的代码已经可以作为嵌入式网络服务的基础框架。
核心要点:
- Socket就是文件描述符,可用read/write操作
- 网络编程务必处理字节序转换
- select有1024限制和O(n)效率问题,poll突破限制但效率仍为O(n)
- epoll采用回调机制,复杂度O(1),支持ET/LT两种模式
- ET模式下必须循环读写直到EAGAIN
思考题:
- ET模式下,如果一个客户端分两次发送了"Hello"和"World",在服务器端可能一次读到"HelloWorld",这说明了什么问题?接收端应如何处理?
- 如果要在epoll服务器上实现给所有客户端广播消息,应该如何修改代码?
- 结合上一篇文章的线程池,如何设计一个epoll + 线程池的高性能服务器?画出架构简图。
欢迎在评论区留下你的思考。下一篇我们将进入信号处理进阶------sigaction、signalfd与定时器的深度探索。
参考资料
- 《UNIX网络编程 卷1:套接字联网API》
man 7 socket,man 7 epoll- Linux内核源码
fs/eventpoll.c