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

本文介绍了一种利用poll函数实现socket服务器的方法,通过监听多个客户端连接并进行数据交互。poll函数可以监控任意数量的文件描述符,适用于高并发场景。

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

通过poll函数编写socket。

原理:poll与select的过程差不多。不同的是select的fd集合有限制,而poll没有。poll使用pollfd结构来存储文件描述符。每个pollfd表示一个被监视的文件描述符,里面包含等待事件、文件描述符、实际发生的事件。等待事件需用户设置,表示期望系统检测文件描述符所发生的事件。实际发生事件是指系统监控后,在回调返回时设置。

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 <poll.h>
#include <vector>
using namespace std;

int main()
{
	int server_fd = socket(AF_INET, SOCK_STREAM, 0);
	if (server_fd == -1)
	{
		cout << "create server_fd error" << endl;
		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) == -1)
	{
		perror("listen error");
		return 0;
	}
	int clientCnt = 0;
	struct sockaddr_in clientAttr;
	//使用数组存储pollfd,每一个pollfd结构体表示一个客户端的文件描述符
	/*
	struct pollfd
	{
		int fd;			 File descriptor to poll.
		short int events;		 Types of events poller cares about.
		short int revents;		 Types of events that actually occurred.
	};
	*/
	struct pollfd clients[1024];
	clients[0].fd = server_fd;
	//设置监控读操作
	clients[0].events = POLLIN;
	int clNum = 0;
	//初始化1024个文件描述符
	for (int i = 1; i < 1024; i++)
	{
		clients[i].fd = -1;
	}
	while (1)
	{
		/*
		int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
		Poll the file descriptors described by the NFDS structures starting at
		FDS.  If TIMEOUT is nonzero and not -1, allow TIMEOUT milliseconds for
		an event to occur; if TIMEOUT is -1, block until an event occurs.
		Returns the number of file descriptors with events, zero if timed out,
		or -1 for errors.
		*/
		if (poll(clients, clNum + 1, -1) == -1)
		{
			perror("poll error");
			return 0;
		}
		//第一个是服务器的文件描述符,用来监控是有请求进来
		if (clients[0].revents & POLLIN)
		{
			socklen_t len = sizeof(clientAttr);
			int conn = accept(server_fd, (struct  sockaddr*)&clientAttr, &len);
			if (conn == -1)
			{
				perror("accept error");
				continue;
			}
			cout << "accept client :" << inet_ntoa(clientAttr.sin_addr) << ":" << clientAttr.sin_port << endl;
			//将新的请求文件描述符加到客户端数组中
			int i;
			for (i = 1; i < 1024; i++)
			{
				if (clients[i].fd == -1)
				{
					clients[i].fd = conn;
					clients[i].events = POLLIN;
					break;
				}
			}
			clNum = clNum > i ? clNum : i;
		}
		//轮询客户端fd数组
		char readBuf[1024] = {0};
		for (int i = 1; i <= clNum; i++)
		{
			memset(readBuf, 0, sizeof(readBuf));
			if (clients[i].revents & POLLIN)
			{
				//读取改客户端连接的信息
				int n = read(clients[i].fd, readBuf, 1024);
				cout << "read msg is " << readBuf << ",n=" << n << endl;
				//回复客户端
				string wrtBuf = "hello,clients[i]ient " + to_string(i);
				write(clients[i].fd, wrtBuf.c_str(), wrtBuf.length() + 1);
			}
		}
	}
	return 0;
}

参考:

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

<think>好的,用户之前已经询问过IO多路复用的概念,现在需要我用C语言给出详细的例子来讲解。我需要先回顾一下之前的回答,确保这次的内容不会重复但能深入。用户可能希望了解具体的实现步骤、代码结构以及不同函数的使用方法。 首先,用户可能对C语言的IO多路复用不太熟悉,所以需要从基础开始。可能需要解释select和epoll的区别,以及为什么在例子中选择其中一个。考虑到用户可能是在学习网络编程或者系统编程,例子应该贴近实际应用,比如一个简单的服务器程序。 接下来,我需要决定使用select还是epoll作为例子。虽然epoll更高效,但select是跨平台的,适合初学者理解基本概念。不过用户可能更想了解现代的高效方法,所以应该同时给出两种例子,但需要明确说明各自的优缺点。 在代码示例中,要确保步骤清晰,比如创建socket、设置非阻塞、绑定地址、监听、创建epoll实例、添加事件、循环处理事件等。每个步骤都要有注释,解释其作用。同时,要注意错误处理,比如检查系统调用的返回值,这在网络编程中非常重要。 另外,用户可能想知道如何处理不同的IO事件,比如读、写、错误等情况,以及如何管理多个客户端连接。需要展示如何在一个循环中处理多个文件描述符,以及如何动态添加或移除监控的事件。 还要注意对比select和epoll的性能和适用场景,帮助用户理解在不同情况下如何选择。例如,select适合描述符数量少的情况,而epoll适合高并发。 最后,可能需要总结两种方法的优缺点,并给出使用建议,比如在Linux下使用epoll,而在跨平台时使用select。同时,提醒用户注意常见的陷阱,比如文件描述符设置为非阻塞模式,避免阻塞整个应用。 确保代码示例正确无误,符合C语言标准,并且编译运行步骤清晰。如果有潜在的问题,比如资源泄漏或者错误处理不足,也要在注释或解释中指出,帮助用户避免常见错误。</think>好的!以下通过一个完整的C语言示例,结合**`select`**和**`epoll`**两种实现方式,详细讲解IO多路复用的具体应用。我们将构建一个**简易的TCP回显服务器**,能够同时处理多个客户端的连接和消息。 --- ### 一、使用 `select` 实现IO多路复用 #### 1. 核心代码结构 ```c #include <stdio.h> #include <stdlib.h> #include <sys/select.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #define MAX_CLIENTS 10 #define BUFFER_SIZE 1024 int main() { int server_fd, max_fd; struct sockaddr_in addr; fd_set read_fds; // 用于select监控的文件描述符集合 // 创建TCP套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 绑定地址和端口 addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(8080); if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_fd, MAX_CLIENTS) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Server listening on port 8080...\n"); // 初始化客户端数组 int client_sockets[MAX_CLIENTS] = {0}; while (1) { FD_ZERO(&read_fds); // 清空集合 FD_SET(server_fd, &read_fds); // 加入服务器套接字 max_fd = server_fd; // 添加所有活跃客户端套接字到监控集合 for (int i = 0; i < MAX_CLIENTS; i++) { if (client_sockets[i] > 0) { FD_SET(client_sockets[i], &read_fds); if (client_sockets[i] > max_fd) { max_fd = client_sockets[i]; } } } // 调用select等待事件 int activity = select(max_fd + 1, &read_fds, NULL, NULL, NULL); if (activity < 0) { perror("select error"); } // 处理服务器套接字的新连接 if (FD_ISSET(server_fd, &read_fds)) { int new_socket = accept(server_fd, NULL, NULL); if (new_socket < 0) { perror("accept failed"); continue; } // 将新客户端加入数组 for (int i = 0; i < MAX_CLIENTS; i++) { if (client_sockets[i] == 0) { client_sockets[i] = new_socket; printf("New client connected, socket fd: %d\n", new_socket); break; } } } // 处理客户端数据 for (int i = 0; i < MAX_CLIENTS; i++) { int client_fd = client_sockets[i]; if (client_fd > 0 && FD_ISSET(client_fd, &read_fds)) { char buffer[BUFFER_SIZE] = {0}; int valread = read(client_fd, buffer, BUFFER_SIZE); if (valread == 0) { // 客户端断开连接 printf("Client %d disconnected\n", client_fd); close(client_fd); client_sockets[i] = 0; } else { // 回显数据 write(client_fd, buffer, strlen(buffer)); printf("Echoed to client %d: %s\n", client_fd, buffer); } } } } return 0; } ``` #### 2. 关键点解析 - **`select` 的局限性**:每次调用需遍历所有客户端描述符(时间复杂度$O(n)$) - **文件描述符上限**:`FD_SETSIZE`通常为1024 - **效率问题**:每次调用需重置监控集合 --- ### 二、使用 `epoll` 实现(Linux特有) #### 1. 高效实现代码 ```c #include <stdio.h> #include <stdlib.h> #include <sys/epoll.h> #include <sys/socket.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #define MAX_EVENTS 10 #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 addr; struct epoll_event event, events[MAX_EVENTS]; // 创建TCP套接字 server_fd = socket(AF_INET, SOCK_STREAM, 0); if (server_fd == -1) { perror("socket creation failed"); exit(EXIT_FAILURE); } // 绑定地址和端口 addr.sin_family = AF_INET; addr.sin_addr.s_addr = INADDR_ANY; addr.sin_port = htons(8080); if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind failed"); exit(EXIT_FAILURE); } // 监听连接 if (listen(server_fd, SOMAXCONN) < 0) { perror("listen failed"); exit(EXIT_FAILURE); } printf("Server listening on port 8080...\n"); // 创建epoll实例 epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1 failed"); exit(EXIT_FAILURE); } // 注册服务器套接字到epoll event.events = EPOLLIN | EPOLLET; // 边缘触发模式 event.data.fd = server_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) { perror("epoll_ctl: server_fd"); exit(EXIT_FAILURE); } // 主事件循环 while (1) { int n_ready = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (n_ready == -1) { perror("epoll_wait"); continue; } for (int i = 0; i < n_ready; i++) { // 处理新连接 if (events[i].data.fd == server_fd) { while (1) { // 边缘触发需要循环accept int client_fd = accept(server_fd, NULL, NULL); if (client_fd == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; // 已无新连接 } else { perror("accept"); break; } } set_nonblocking(client_fd); // 设置为非阻塞 event.events = EPOLLIN | EPOLLET; event.data.fd = client_fd; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) { perror("epoll_ctl: client_fd"); close(client_fd); } printf("New client connected: %d\n", client_fd); } } else { // 处理客户端数据 int client_fd = events[i].data.fd; char buffer[BUFFER_SIZE]; ssize_t bytes_read; while (1) { // 边缘触发需读取所有数据 bytes_read = read(client_fd, buffer, BUFFER_SIZE - 1); if (bytes_read == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { break; // 数据已读完 } else { perror("read"); close(client_fd); break; } } else if (bytes_read == 0) { // 客户端断开 printf("Client %d disconnected\n", client_fd); close(client_fd); break; } else { // 回显数据 buffer[bytes_read] = '\0'; write(client_fd, buffer, bytes_read); printf("Echoed to client %d: %s\n", client_fd, buffer); } } } } } close(server_fd); return 0; } ``` #### 2. 关键改进点 - **边缘触发(Edge Triggered)模式**:需循环读取数据直到`EAGAIN` - **非阻塞IO**:必须配合`fcntl`设置`O_NONBLOCK` - **高效事件通知**:仅返回活跃的描述符(时间复杂度$O(1)$) - **支持大规模并发**:理论上可处理数十万连接 --- ### 、对比分析 | 特性 | select | epoll | |---------------------|---------------------------------|---------------------------------| | 时间复杂度 | $O(n)$ 线性扫描 | $O(1)$ 事件驱动 | | 最大连接数 | 1024 (FD_SETSIZE限制) | 仅受系统内存限制 | | 内存拷贝 | 每次调用需全量拷贝fd集合 | 内核与用户空间共享内存 | | 触发模式 | 仅水平触发(LT) | 支持LT和边缘触发(ET) | | 适用场景 | 跨平台、小规模连接 | Linux系统、高并发场景 | --- ### 四、运行测试 1. **编译服务器**: ```bash gcc server.c -o server # select版本 gcc epoll_server.c -o epoll_server # epoll版本 ``` 2. **启动服务器**: ```bash ./server # select版本 ./epoll_server # epoll版本 ``` 3. **客户端测试**(使用`telnet`或`nc`): ```bash telnet localhost 8080 ``` --- ### 五、深入理解 1. **水平触发(LT)**:只要缓冲区有数据就会持续通知 2. **边缘触发(ET)**:仅在状态变化时通知一次(必须处理完所有数据) 3. **非阻塞IO的必要性**:防止单次`read/write`阻塞整个事件循环 通过这两个实例,可以清晰看到IO多路复用如何用**单线程高效管理多个连接**。实际开发中,epoll是Linux下高性能服务器的首选方案,而select则更适合跨平台的小规模应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值