linux网络编程以及epoll IO多路复用
常见库
头文件 | 定位 | 主要功能 |
---|---|---|
<sys/socket.h> | 网络io | 提供套接字相关的函数和常量,如 socket() 、bind() 、listen() 、accept() 等。 |
<sys/epoll.h> | io复用 | 提供 epoll 相关的函数和结构体,用于高效监控多个文件描述符的 I/O 事件。 |
<netinet/in.h> | 数据结构 | 提供网络编程相关的结构体和常量,如 sockaddr_in 、htons() 、INADDR_ANY 等。 |
<arpa/inet.h> | 转换算法 | 提供 IP 地址转换函数,如 inet_pton() 、inet_ntop() 等。 |
<fcntl.h> | 文件io | 提供文件控制相关的函数和常量,如 fcntl() 、O_NONBLOCK 等。 |
Socket
简介
- TCP 是面向连接的协议,适用于需要可靠数据传输的场景。
- UDP 是无连接的协议,适用于对实时性要求高、允许丢包的场景。
- 多进程可以用于处理多个客户端的并发连接,但需要注意资源管理和进程间通信的问题。
地址
在绑定时需要选择类型,其中AF_
前缀通常表示地址族,而 PF_
前缀表示协议族(Protocol Family)。两者在实际使用中通常等价。
比如常用的AF_INET
是 Socket 编程中的一个常量,表示使用 IPv4 地址和 TCP/UDP 协议进行网络通信。
TCP
基本流程
TCP(传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP Socket 的基本流程如下:
客户端
-
socket() 使用socket()函数创建一个套接字,指定协议族、套接字类型和协议。
-
connect() 使用connect()函数连接到服务器的指定IP地址和端口。
-
send()/recv() 使用send()函数发送数据,使用recv()函数接收数据。
-
close() 使用close()函数关闭套接字连接。
服务器
-
socket() 使用socket()函数创建一个套接字。
-
bind() 使用bind()函数将套接字绑定到本地地址和端口。
-
listen() 使用listen()函数使套接字进入监听状态,等待客户端连接。
-
accept() 使用accept()函数接受客户端连接,返回新的套接字用于通信。
-
send()/recv() 使用send()函数发送数据,使用recv()函数接收数据。
-
close() 使用close()函数关闭套接字连接。
示例
下面是一个使用多进程实现的 TCP 服务器示例代码,它允许多个客户端同时连接并处理数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
#define PORT 8080
#define BUFFER_SIZE 1024
void handle_client(int client_socket) {
char buffer[BUFFER_SIZE];
int bytes_received;
while ((bytes_received = recv(client_socket, buffer, BUFFER_SIZE, 0)) > 0) {
buffer[bytes_received] = '\0';
printf("Received: %s\n", buffer);
// Echo back the received data
send(client_socket, buffer, bytes_received, 0);
}
close(client_socket);
printf("Client disconnected.\n");
exit(0);
}
int main() {
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
pid_t pid;
// Create socket
if ((server_socket = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// Bind socket
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
close(server_socket);
exit(EXIT_FAILURE);
}
// Listen for connections
if (listen(server_socket, 5) == -1) {
perror("Listen failed");
close(server_socket);
exit(EXIT_FAILURE);
}
printf("Server listening on port %d...\n", PORT);
while (1) {
// Accept a new connection
if ((client_socket = accept(server_socket, (struct sockaddr *)&client_addr, &client_addr_len)) == -1) {
perror("Accept failed");
continue;
}
printf("New client connected: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// Fork a new process to handle the client
pid = fork();
if (pid < 0) {
perror("Fork failed");
close(client_socket);
} else if (pid == 0) {
// Child process
close(server_socket); // Close the server socket in the child process
handle_client(client_socket);
} else {
// Parent process
close(client_socket); // Close the client socket in the parent process
}
}
close(server_socket);
return 0;
}
客户端测试:
可以使用 telnet
或 nc
工具作为客户端连接到服务器:
telnet 127.0.0.1 8080
UDP
基本流程
UDP(用户数据报协议)是一种无连接的、不可靠的、基于数据报的传输层通信协议。UDP Socket 的基本流程如下:
客户端
-
socket() 使用socket()函数创建一个UDP套接字,指定协议族、套接字类型和协议。
-
sendto()/recvfrom() 使用sendto()函数发送数据到服务器,使用recvfrom()函数接收服务器的数据。
-
close() 使用close()函数关闭套接字连接。
服务器
-
socket() 使用socket()函数创建一个UDP套接字。
-
bind() 使用bind()函数将套接字绑定到本地地址和端口。
-
recvfrom()/sendto() 使用recvfrom()函数接收客户端的数据,使用sendto()函数发送数据到客户端。
-
close() 使用close()函数关闭套接字连接。
示例
UDP 的实现相对简单,以下是一个 UDP 服务器示例代码:
#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() {
int server_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t client_addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// Create socket
if ((server_socket = socket(AF_INET, SOCK_DGRAM, 0)) == -1) {
perror("Socket creation failed");
exit(EXIT_FAILURE);
}
// Bind socket
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
perror("Bind failed");
close(server_socket);
exit(EXIT_FAILURE);
}
printf("UDP Server listening on port %d...\n", PORT);
while (1) {
// Receive data from client
int bytes_received = recvfrom(server_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr *)&client_addr, &client_addr_len);
if (bytes_received < 0) {
perror("Recvfrom failed");
continue;
}
buffer[bytes_received] = '\0';
printf("Received from %s:%d: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);
// Echo back the received data
sendto(server_socket, buffer, bytes_received, 0, (struct sockaddr *)&client_addr, client_addr_len);
}
close(server_socket);
return 0;
}
客户端测试:
可以使用 nc
工具作为客户端连接到服务器:
nc -u 127.0.0.1 8080
IO复用
select和poll
无论是 select
还是 poll
,它们的工作原理都是:把需要监听的文件描述符(fd)集中在一起,然后轮询检查这些 fd 的状态。当某个 fd 的状态发生变化(比如变得可读、可写或发生异常)时,它们会通知程序,让程序去处理。
select
工作原理
select
使用一个叫fd_set
的东西来存放需要监听的文件描述符。fd_set
实际上是一个位图(bitmap),每个 bit 代表一个文件描述符。- 程序把需要监听的 fd 加入
fd_set
,然后调用select
函数。select
会阻塞(等待),直到有 fd 的状态发生变化。 - 当某个 fd 的状态变化时,
select
会返回,并告诉程序哪些 fd 已经准备好了(比如可读或可写)。 select
能监听的文件描述符数量是有限的,受限于FD_SETSIZE
,通常是 1024。也就是说,select
最多只能同时监听 1024 个 fd。
poll
工作原理
poll
使用一个叫struct pollfd
的数组来存放需要监听的文件描述符。每个struct pollfd
包含一个 fd 和需要监听的事件(比如可读、可写)。- 程序把需要监听的 fd 加入
struct pollfd
数组,然后调用poll
函数。poll
会阻塞(等待),直到有 fd 的状态发生变化。 - 当某个 fd 的状态变化时,
poll
会返回,并告诉程序哪些 fd 已经准备好了。 poll
没有固定的文件描述符数量限制,理论上可以监听任意数量的 fd,只要系统资源允许。
优缺点
- 效率低 : 把整个文件描述符集合从用户空间复制到内核空间,并且在返回时需要遍历所有文件描述符来检查状态。
- 兼容性好 : select几乎兼容所有操作系统, poll兼容unix
epoll
epoll
的核心思想是:事件驱动,即只在文件描述符状态发生变化时通知程序,而不是每次都遍历所有文件描述符。
实现原理
- 高效:
epoll
是 Linux 特有的 I/O 多路复用机制,它通过事件驱动的方式工作,只有在文件描述符状态发生变化时才会通知用户程序,避免了不必要的遍历。 - 文件描述符限制:
epoll
能够监控的文件描述符数量没有固定限制,受限于系统资源。 - 内存使用:
epoll
使用红黑树和链表来管理文件描述符,内存使用效率较高。 - 边缘触发(ET)和水平触发(LT):
epoll
支持两种触发模式,边缘触发(Edge Triggered, ET)和水平触发(Level Triggered, LT)。边缘触发只在状态变化时通知一次,而水平触发在状态持续时会不断通知。
工作流程
- 创建
epoll
实例:使用epoll_create
或epoll_create1
创建一个epoll
实例,它会返回一个文件描述符,用于后续操作。 - 注册需要监听的文件描述符:使用
epoll_ctl
将需要监听的文件描述符添加到epoll
实例中,并指定需要监听的事件(比如可读、可写)。 - 等待事件发生:使用
epoll_wait
等待文件描述符的状态变化。当某个文件描述符的状态发生变化时,epoll_wait
会返回,并告诉程序哪些文件描述符已经准备好了。
常用API
epoll
是 Linux 提供的一种高效的多路复用 I/O 机制,用于监控多个文件描述符(通常是套接字)的状态变化。以下是 epoll
相关的函数及其解释:
epoll_create1
函数原型:
int epoll_create1(int flags);
参数:
• flags
: 控制 epoll
实例的创建行为。常用的标志有:
• 0
: 默认行为。
• EPOLL_CLOEXEC
: 在执行 exec
时关闭文件描述符。
返回值:
• 成功时返回一个新的 epoll
文件描述符。
• 失败时返回 -1
,并设置 errno
。
功能:
创建一个新的 epoll
实例,并返回与之关联的文件描述符。
是epoll_create的增强版, 支持通过额外标志控制行为. 而原本epoll_create的size参数在现代内核中已被忽略,但仍然需要传递一个大于 0 的值。所以最好直接用新版这个
epoll_ctl
函数原型:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
• epfd
: epoll
实例的文件描述符。
• op
: 操作类型,可以是以下之一:
• EPOLL_CTL_ADD
: 添加一个文件描述符到 epoll
实例。
• EPOLL_CTL_MOD
: 修改已注册的文件描述符的事件。
• EPOLL_CTL_DEL
: 从 epoll
实例中删除一个文件描述符。
• fd
: 要操作的文件描述符。
• event
: 指向 struct epoll_event
的指针,用于指定事件类型和相关数据。
返回值:
• 成功时返回 0
。
• 失败时返回 -1
,并设置 errno
。
功能:
用于控制 epoll
实例中的文件描述符,可以添加、修改或删除文件描述符及其关联的事件。
epoll_wait
函数原型:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数:
• epfd
: epoll
实例的文件描述符。
• events
: 指向 struct epoll_event
数组的指针,用于存储发生的事件。
• maxevents
: events
数组的大小,即最多可以存储多少个事件。
• timeout
: 等待事件的超时时间(毫秒)。-1
表示无限等待,0
表示立即返回。
返回值:
• 成功时返回就绪的文件描述符的数量。
• 失败时返回 -1
,并设置 errno
。
功能:
等待 epoll
实例中的文件描述符上发生的事件,并将这些事件存储在 events
数组中。
常用数据结构
epoll_event
结构体定义:
struct epoll_event {
uint32_t events; // Epoll events
epoll_data_t data; // User data variable
};
成员:
• events
: 事件类型,可以是以下宏的组合:
• EPOLLIN
: 文件描述符可读。
• EPOLLOUT
: 文件描述符可写。
• EPOLLRDHUP
: 对端关闭连接或半关闭。
• EPOLLPRI
: 有紧急数据可读。
• EPOLLERR
: 文件描述符发生错误。
• EPOLLHUP
: 文件描述符被挂起。
• EPOLLET
: 边缘触发模式(默认是水平触发)。
• EPOLLONESHOT
: 一次性事件,事件发生后会自动从 epoll
实例中移除。
• data
: 用户数据,通常用于存储文件描述符或指向用户自定义数据的指针。
功能: 一般创建两个该类型的分别为ev和event
ev
用于在调用epoll_ctl
时,指定要添加、修改或删除的文件描述符的事件和用户数据。events
是一个epoll_event
类型的数组,用于存储epoll_wait
返回的就绪事件。
epoll_data_t
联合体定义:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
成员:
• ptr
: 指向用户自定义数据的指针。
• fd
: 文件描述符。
• u32
: 32 位无符号整数。
• u64
: 64 位无符号整数。
示例代码
set_nonblocking
: 将文件描述符设置为非阻塞模式。handle_client
: 处理客户端的数据读取和写入。main
- 创建并绑定服务器套接字。
- 创建
epoll
实例并将服务器套接字添加到epoll
中。 - 使用
epoll_wait
等待事件发生。 - 当有新的连接时,接受连接并将客户端套接字添加到
epoll
中。 - 当客户端套接字有数据可读时,调用
handle_client
处理数据。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define MAX_EVENTS 10
#define PORT 8080
#define BUFFER_SIZE 1024
int set_nonblocking(int fd) {
int flags = fcntl(fd, F_GETFL, 0);
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
void handle_client(int client_fd) {
char buffer[BUFFER_SIZE];
ssize_t bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1);
if (bytes_read > 0) {
buffer[bytes_read] = '\0';
printf("Received: %s\n", buffer);
write(client_fd, buffer, bytes_read);
} else if (bytes_read == 0) {
printf("Client disconnected\n");
close(client_fd);
}
}
int main() {
int server_fd, client_fd, epoll_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len = sizeof(client_addr);
struct epoll_event ev, events[MAX_EVENTS];
int nfds;
// Create socket
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// Set socket options
int opt = 1;
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
// Bind socket
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 for connections
listen(server_fd, SOMAXCONN);
// Create epoll instance
epoll_fd = epoll_create1(0);
// Add server socket to epoll
ev.events = EPOLLIN;
ev.data.fd = server_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev);
printf("Server is running on port %d\n", PORT);
while (1) {
nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
// Accept new connection
client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
// Set client socket to non-blocking
set_nonblocking(client_fd);
// Add client socket to epoll
ev.events = EPOLLIN | EPOLLET; // Edge-triggered mode
ev.data.fd = client_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev);
printf("New client connected\n");
} else {
// Handle client data
handle_client(events[i].data.fd);
}
}
}
close(server_fd);
close(epoll_fd);
return 0;
}
可以使用telnet连接
telnet 127.0.0.1 8080