linux socket的IO多路复用简单例子(四)

本文介绍如何使用epoll函数实现高效的socket编程。通过创建epoll实例并管理多个文件描述符,实现实时监控和处理客户端连接及数据收发。文章详细展示了epoll在服务器端的具体实现过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

使用epoll函数编写socket。

原理:使用一个文件描述符来管理多个描述符。epoll只拷贝一次描述符到内核态,当监控的事件发生时,通过函数回调将fd加入到一个就绪表中。接着,检测某个fd是否在就绪表中,存在的话进行对应的读写操作。epoll引入了一个事件结构体。每一个文件描述符对应一个事件结构体。结构体包含文件描述符,需要监控的事件。

server端:

#include <iostream>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <sys/shm.h>
#include <sys/epoll.h>
#include <vector>
using namespace std;


int main()
{
	int server_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_fd == -1)
	{
		perror("create server fd error");
		return 0;
	}
	struct sockaddr_in serverAttr;
	serverAttr.sin_family = AF_INET;
	serverAttr.sin_port = htons(5875);
	serverAttr.sin_addr.s_addr = htonl(INADDR_ANY);

	if (bind(server_fd, (struct sockaddr*)&serverAttr, sizeof(serverAttr)) == -1)
	{
		perror("bind error");
		return 0;
	}
	if (listen(server_fd, 10))
	{
		perror("listen error");
		return 0;
	}
	/*
	Creates an epoll instance.  Returns an fd for the new instance.
	The "size" parameter is a hint specifying the number of file
	descriptors to be associated with the new instance.  The fd
	returned by epoll_create() should be closed with close().
	int epoll_create (int __size) __THROW;
	*/
	//创建一个总的文件描述符
	int epollFd = epoll_create(1024);
	//创建服务器fd所对应的事件结构体
	struct epoll_event event;
	event.events = EPOLLIN;
	event.data.fd = server_fd;

	/*
	 Manipulate an epoll instance "epfd". Returns 0 in case of success,
	 -1 in case of error ( the "errno" variable will contain the
	 specific error code ) The "op" parameter is one of the EPOLL_CTL_*
	 constants defined above. The "fd" parameter is the target of the
	 operation. The "event" parameter describes which events the caller
	 is interested in and any associated user data.
	int epoll_ctl (int __epfd, int __op, int __fd,
		      struct epoll_event *__event) __THROW;
	*/
	//将服务器fd和事件结构体增加到总的fd中
	epoll_ctl(epollFd, EPOLL_CTL_ADD, server_fd, &event);
	//用来保存系统监控发生后,已经改变的fd事件
	struct epoll_event evs[1024];
	while (1)
	{
		/* Wait for events on an epoll instance "epfd". Returns the number of
   		triggered events returned in "events" buffer. Or -1 in case of
   		error with the "errno" variable set to the specific error code. The
   		"events" parameter is a buffer that will contain triggered
   		events. The "maxevents" is the maximum number of events to be
   		returned ( usually size of "events" ). The "timeout" parameter
   		specifies the maximum wait time in milliseconds (-1 == infinite).
	    int epoll_wait (int __epfd, struct epoll_event *__events,
		       int __maxevents, int __timeout);
		 */
		//ret表示发生改变的事件数量
		int ret = epoll_wait(epollFd, evs, 100, -1);
		for (int i = 0; i < ret; i++)
		{
			int fd = evs[i].data.fd;
			//表示接受到新的请求
			if ((fd == server_fd) && (evs[i].events & EPOLLIN))
			{
				struct sockaddr_in clAttr;
				socklen_t len = sizeof(clAttr);
				int clFd = accept(server_fd, (struct sockaddr*)&clAttr, &len);
				if (clFd == -1)
				{
					perror("accept error");
					continue;
				}
				cout << "accept client :" << inet_ntoa(clAttr.sin_addr) << ":" << clAttr.sin_port << endl;
				//将新的客户端fd和对应的事件添加到总的fd中
				struct epoll_event e;
				e.events = EPOLLIN;
				e.data.fd = clFd;
				epoll_ctl(epollFd, EPOLL_CTL_ADD, clFd, &e);
				continue;
			}
			//表示这个fd接受到客户端发送的消息
			if (evs[i].events & EPOLLIN)
			{
				char buf[1024] = {0};
				int nread = read(evs[i].data.fd, buf, 1024);
				if (nread == -1)
				{
					perror("read error");
					close(fd);
					epoll_ctl(epollFd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
					continue;
				}
				if (nread == 0)
				{
					perror("server close");
					close(fd);
					epoll_ctl(epollFd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
					continue;
				}
				cout << "read msg is " << buf << endl;
				evs[i].events = EPOLLOUT;
				epoll_ctl(epollFd, EPOLL_CTL_MOD, evs[i].data.fd, &evs[i]);
				continue;
			}
			//返回信息给对应的客户端
			if (evs[i].events & EPOLLOUT)
			{
				string wrtBuf = "hello,clients[i]ient " + to_string(i);
				int nwrite = write(evs[i].data.fd, wrtBuf.c_str(), 1024);
				if (nwrite == -1)
				{
					perror("write error");
					close(evs[i].data.fd);
					epoll_ctl(epollFd, EPOLL_CTL_DEL, evs[i].data.fd, &evs[i]);
					continue;
				}
				evs[i].events = EPOLLIN;
				epoll_ctl(epollFd, EPOLL_CTL_MOD, evs[i].data.fd, &evs[i]);
			}
		}

	}
	return 0;
}

参考:

http://www.cnblogs.com/Anker/p/3263780.html

<think>IO多路复用是一种允许单个进程或线程同时监视多个文件描述符(通常是网络套接字)的机制,以检查它们是否可读、可写或出现异常。这种机制使得一个进程可以高效地处理多个I/O操作,而无需为每个连接创建一个线程或进程。 ### 为什么需要IO多路复用? 在传统的阻塞I/O模型中,每个连接都需要一个独立的线程或进程来处理。当连接数增加时,系统资源(如内存和CPU时间)会被大量消耗在线程/进程的创建和上下文切换上,导致性能下降。而IO多路复用技术通过一个进程管理多个连接,大大减少了资源开销。 ### 实现IO多路复用的系统调用 在Linux系统中,常见的IO多路复用机制有: 1. **select**: 最早的IO多路复用机制,存在性能瓶颈(文件描述符数量限制和效率问题)。 2. **poll**: 解决了select的文件描述符数量限制,但效率问题依然存在(需要遍历所有文件描述符)。 3. **epoll**: Linux特有的高效机制,解决了select和poll的性能问题,适用于高并发场景。 ### epoll的工作原理 epoll通过三个系统调用实现: 1. `epoll_create()`: 创建一个epoll实例,返回一个文件描述符。 2. `epoll_ctl()`: 向epoll实例注册、修改或删除要监视的文件描述符及其关注的事件。 3. `epoll_wait()`: 等待事件发生,返回就绪的事件列表。 ### 示例:使用epoll的简单服务器 以下是一个使用epoll的简单回显服务器示例: ```c #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 // 设置文件描述符为非阻塞模式 void set_nonblocking(int sockfd) { int flags = fcntl(sockfd, F_GETFL, 0); if (flags == -1) { perror("fcntl F_GETFL"); exit(EXIT_FAILURE); } if (fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) == -1) { perror("fcntl F_SETFL"); exit(EXIT_FAILURE); } } int main() { int server_fd, client_fd; struct sockaddr_in address; int addrlen = sizeof(address); struct epoll_event ev, events[MAX_EVENTS]; int epoll_fd; char buffer[1024] = {0}; // 创建监听socket if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置端口复用 int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) { perror("setsockopt"); exit(EXIT_FAILURE); } // 绑定地址 address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听 if (listen(server_fd, SOMAXCONN) < 0) { perror("listen"); exit(EXIT_FAILURE); } // 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } // 添加监听socket到epoll ev.events = EPOLLIN | EPOLLET; // 边缘触发模式 ev.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev) == -1) { perror("epoll_ctl: server_fd"); exit(EXIT_FAILURE); } // 事件循环 while (1) { int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (int i = 0; i < nfds; i++) { // 处理新连接 if (events[i].data.fd == server_fd) { while (1) { client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen); if (client_fd == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 所有连接已处理完 break; } else { perror("accept"); break; } } // 设置客户端socket为非阻塞 set_nonblocking(client_fd); // 添加客户端socket到epoll ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP; ev.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) == -1) { perror("epoll_ctl: client_fd"); close(client_fd); } } } // 处理客户端数据 else { if (events[i].events & EPOLLRDHUP) { // 客户端断开连接 close(events[i].data.fd); } else if (events[i].events & EPOLLIN) { // 读取客户端数据 ssize_t bytes_read; while ((bytes_read = read(events[i].data.fd, buffer, sizeof(buffer))) { if (bytes_read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 数据已读完 break; } else { perror("read"); close(events[i].data.fd); break; } } else if (bytes_read == 0) { // 客户端关闭连接 close(events[i].data.fd); break; } // 回显数据 write(events[i].data.fd, buffer, bytes_read); } } } } } close(server_fd); return 0; } ``` ### IO多路复用的优点 1. **高并发支持**:单进程可处理成千上万的连接。 2. **资源高效**:避免了多进程/线程的创建和切换开销。 3. **事件驱动**:只在有I/O事件时进行处理,减少空转。 ### 适用场景 - Web服务器(如Nginx) - 实时通信系统 - 数据库服务器 - 任何需要高并发网络I/O的应用 §§相关问题§§: 1. epoll的边缘触发(ET)模式和水平触发(LT)模式有什么区别? 2. 在边缘触发模式下,为什么必须使用非阻塞I/O? 3. 除了epoll,还有哪些IO多路复用机制?它们之间有何不同? 4. 如何设计一个高性能的IO多路复用服务器以支持十万级并发连接?</think># IO多路复用详解 IO多路复用是一种高效的I/O处理技术,允许单个进程/线程同时监视多个文件描述符(如网络套接字),并在其中任何一个或多个可读写时得到通知。这种技术是高并发服务器的核心实现方式。 ## 核心概念 ### 1. 基本思想 - **单线程处理多连接**:一个线程处理多个I/O操作 - **事件驱动**:只在有实际I/O事件时唤醒处理 - **非阻塞I/O**:避免线程在I/O操作上阻塞 ### 2. 与传统模型的对比 | 模型 | 处理方式 | 资源消耗 | 并发能力 | |------|----------|----------|----------| | 阻塞I/O | 每个连接一个线程 | 高(线程开销) | 低(受限于线程数) | | 非阻塞I/O轮询 | 单线程轮询所有连接 | 中(CPU占用高) | 中(受限于轮询效率) | | IO多路复用 | 单线程+事件通知 | 低(仅活动连接) | 高(万级连接) | ## 实现机制(以epoll为例) ### 1. epoll工作流程 ```c // 创建epoll实例 int epoll_fd = epoll_create1(0); // 添加监听socket struct epoll_event event; event.events = EPOLLIN; // 关注可读事件 event.data.fd = listen_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event); // 事件循环 while (1) { int n = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for (int i = 0; i < n; i++) { if (events[i].data.fd == listen_fd) { // 处理新连接 int client_fd = accept(listen_fd, ...); // 添加新客户端到epoll } else { // 处理客户端数据 recv(client_fd, ...); } } } ``` ### 2. epoll核心数据结构 ```c struct epoll_event { uint32_t events; // 关注的事件类型 epoll_data_t data; // 用户数据 }; typedef union epoll_data { void *ptr; int fd; // 最常用:关联的文件描述符 uint32_t u32; uint64_t u64; } epoll_data_t; ``` ### 3. epoll事件类型 | 事件标志 | 描述 | 触发条件 | |----------|------|----------| | EPOLLIN | 可读 | 有数据可读或新连接 | | EPOLLOUT | 可写 | 发送缓冲区可用 | | EPOLLRDHUP | 对端关闭 | TCP连接半关闭 | | EPOLLET | 边缘触发 | 状态变化时通知 | | EPOLLERR | 错误 | 发生错误 | | EPOLLHUP | 挂起 | 连接完全关闭 | ## 触发模式详解 ### 1. 水平触发(LT,默认模式) - **特点**:只要文件描述符就绪,就会持续通知 - **优点**:编程简单,不易遗漏事件 - **缺点**:可能产生多余通知,效率较低 ```c // 水平触发示例 event.events = EPOLLIN; // 默认水平触发 ``` ### 2. 边缘触发(ET) - **特点**:仅在状态变化时通知一次 - **优点**:减少系统调用,提高效率 - **要求**:必须使用非阻塞I/O,必须完全处理所有数据 ```c // 边缘触发设置 event.events = EPOLLIN | EPOLLET; // 边缘触发模式 ``` ### 3. ET模式处理示例 ```c void handle_client(int client_fd) { char buffer[4096]; ssize_t n; // 必须循环读取直到EAGAIN while ((n = recv(client_fd, buffer, sizeof(buffer), 0)) > 0) { // 处理数据... } if (n == 0) { // 连接关闭 close(client_fd); } else if (n < 0) { if (errno != EAGAIN && errno != EWOULDBLOCK) { perror("recv error"); close(client_fd); } // EAGAIN表示数据已读完 } } ``` ## 与其他多路复用技术的对比 ### 1. select/poll/epoll对比 | 特性 | select | poll | epoll | |------|--------|------|-------| | 时间复杂度 | O(n) | O(n) | O(1) | | 最大文件描述符 | FD_SETSIZE(1024) | 无限制 | 系统限制 | | 效率 | 低(线性扫描) | 中(线性扫描) | 高(事件通知) | | 触发模式 | LT | LT | LT/ET | | 内存拷贝 | 每次调用都拷贝 | 每次调用都拷贝 | 内核共享内存 | | 适用场景 | 低并发 | 中并发 | 高并发 | ### 2. 性能对比图 ``` 并发连接数 ^ | epoll | / | / | / | / | / poll | / | / | / select +-----------------> 处理能力 ``` ## 实际应用场景 ### 1. Web服务器架构 ```mermaid graph LR A[客户端1] --> B[监听端口] C[客户端2] --> B D[客户端N] --> B B --> E[epoll_wait] E --> F{事件类型?} F -->|新连接| G[accept] F -->|数据可读| H[recv+处理] F -->|可写| I[send] ``` ### 2. 高性能服务器设计要点 1. **非阻塞I/O**:所有socket必须设置为非阻塞 2. **边缘触发**:使用ET模式提高性能 3. **事件驱动**:基于事件回调处理 4. **缓冲区管理**:每个连接维护读写缓冲区 5. **定时器管理**:处理超时连接 ## 完整示例:epoll回显服务器 ```c #include <sys/epoll.h> #include <sys/socket.h> #include <netinet/in.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #define MAX_EVENTS 10 #define PORT 8080 #define BUFFER_SIZE 1024 void set_nonblocking(int fd) { int flags = fcntl(fd, F_GETFL, 0); fcntl(fd, F_SETFL, flags | O_NONBLOCK); } int main() { int server_fd, epoll_fd; struct sockaddr_in address; struct epoll_event ev, events[MAX_EVENTS]; // 创建服务器socket if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) { perror("socket failed"); exit(EXIT_FAILURE); } // 设置端口复用 int opt = 1; setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 绑定地址 address.sin_family = AF_INET; address.sin_addr.s_addr = INADDR_ANY; address.sin_port = htons(PORT); bind(server_fd, (struct sockaddr*)&address, sizeof(address)); // 监听 listen(server_fd, SOMAXCONN); // 创建epoll实例 epoll_fd = epoll_create1(0); // 添加服务器socket到epoll ev.events = EPOLLIN | EPOLLET; ev.data.fd = server_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev); printf("Epoll服务器运行在端口 %d...\n", PORT); while (1) { int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); for (int i = 0; i < nfds; i++) { // 处理新连接 if (events[i].data.fd == server_fd) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd; // 接受所有新连接 while ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len)) > 0) { set_nonblocking(client_fd); // 添加新客户端到epoll ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP; ev.data.fd = client_fd; epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev); printf("新客户端连接: %d\n", client_fd); } if (client_fd == -1 && errno != EAGAIN) { perror("accept error"); } } // 处理客户端数据 else { int client_fd = events[i].data.fd; // 连接关闭 if (events[i].events & (EPOLLRDHUP | EPOLLHUP)) { printf("客户端断开: %d\n", client_fd); close(client_fd); continue; } // 数据可读 if (events[i].events & EPOLLIN) { char buffer[BUFFER_SIZE]; ssize_t bytes_read; // 边缘触发模式必须完全读取数据 while ((bytes_read = read(client_fd, buffer, BUFFER_SIZE)) > 0) { // 回显数据 write(client_fd, buffer, bytes_read); } // 处理读取错误 if (bytes_read == 0) { printf("客户端关闭连接: %d\n", client_fd); close(client_fd); } else if (bytes_read < 0 && errno != EAGAIN) { perror("read error"); close(client_fd); } } } } } close(server_fd); return 0; } ``` ## 性能优化技巧 1. **连接状态管理** ```c struct connection { int fd; char read_buf[4096]; char write_buf[4096]; size_t read_pos; size_t write_pos; }; // 在epoll_data中存储连接结构 struct connection *conn = malloc(sizeof(struct connection)); conn->fd = client_fd; ev.data.ptr = conn; ``` 2. **批量写操作** ```c // 当EPOLLOUT事件触发时 while (conn->write_pos > 0) { ssize_t n = write(conn->fd, conn->write_buf, conn->write_pos); if (n > 0) { // 移动剩余数据 memmove(conn->write_buf, conn->write_buf + n, conn->write_pos - n); conn->write_pos -= n; } else if (n == -1 && errno != EAGAIN) { // 处理错误 break; } else { break; // EAGAIN或部分写入 } } ``` 3. **定时器管理** ```c // 使用时间轮管理超时连接 void check_timeouts() { time_t now = time(NULL); for (each connection) { if (now - conn->last_active > TIMEOUT) { close(conn->fd); free(conn); } } } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值