Linux网络编程(多路IO复用select函数使用)
@TOC
前言
本篇文章带大家来学习一下多路IO复用select函数的使用。
一、什么是多路IO复用
1.多路I/O复用(Multiplexing I/O)是一种用于同时监视和处理多个输入/输出(I/O)源的技术。它允许一个进程可以同时监听和处理多个文件描述符(sockets、文件、管道等),从而实现高效的事件驱动的编程模型。
2.在传统的I/O模型中,通常采用阻塞I/O或非阻塞I/O方式进行读写操作,为每个I/O源(例如一个socket连接)都创建一个线程或进程来处理。这种方式在高并发场景下,会导致线程或进程数目的剧增,系统资源浪费,且上下文切换开销大。
3.而多路I/O复用通过使用操作系统提供的机制,如select、poll、epoll(在Linux中),允许程序同时监听多个I/O源,实现了异步I/O操作。应用程序可以将多个I/O源注册到多路复用器中,并在多路复用器上等待事件的发生,一旦有事件就绪(如读写准备就绪),程序就可以针对这些就绪的事件进行相应的操作。
多路I/O复用的主要优点如下:
1.高效:通过事件驱动的方式,避免了线程和进程的频繁切换,减小了系统开销。
2.节省资源:使用较少的线程或进程来处理多个I/O源,节省了系统资源,并能够处理大量的并发连接。
3.简化编程模型:与传统的多线程或多进程编程方式相比,使用多路I/O复用可以简化编程模型,使得代码更加清晰和易于维护。
需要注意的是,多路I/O复用并非适用于所有情况,特别是在处理高延迟和大数据量的场景下可能不太适合。此外,不同的操作系统上多路复用器的机制和性能表现也有所不同。
二、select函数讲解
select()函数是一个用于多路复用的I/O模型,可以同时监视多个文件描述符,并在其中任何一个文件描述符就绪时进行相应的处理。它被广泛用于实现高效的事件驱动编程。
select()函数的原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数说明:
nfds:监视的文件描述符的最大值加1。
readfds:指向可读文件描述符集合的指针。
writefds:指向可写文件描述符集合的指针。
exceptfds:指向异常文件描述符集合的指针。
timeout:指定超时时间,选择阻塞的时长。
fd_set是一个文件描述符集合类型,通过宏定义和操作函数进行操作。
使用select()函数的步骤如下:
创建并初始化文件描述符集合。
将需要监视的文件描述符添加到对应的文件描述符集合中。
调用select()函数并传递文件描述符集合。
检查返回值,若为负数则表示出错,若为0则表示超时,若大于0则表示有文件描述符就绪。
使用FD_ISSET()宏检查具体哪些文件描述符就绪,并进行相应的处理。
下面是一个简单的示例代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
fd_set readfds;
int max_fd, ret;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds); // 监视标准输入
max_fd = STDIN_FILENO + 1;
struct timeval timeout;
timeout.tv_sec = 5; // 设置超时时间为5秒
timeout.tv_usec = 0;
ret = select(max_fd, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select");
exit(EXIT_FAILURE);
} else if (ret == 0) {
printf("Timeout\n");
} else {
if (FD_ISSET(STDIN_FILENO, &readfds)) {
printf("Stdin is ready for reading\n");
// 读取标准输入数据
char buffer[100];
fgets(buffer, sizeof(buffer), stdin);
printf("Received: %s", buffer);
}
}
return 0;
}
三、使用select编程并发服务器
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>
int main()
{
int server = 0;
struct sockaddr_in saddr = {0};
int client = 0;
struct sockaddr_in caddr = {0};
socklen_t asize = 0;
int len = 0;
char buf[32] = {0};
int maxfd;//最大文件描述符
int ret = 0;
int i = 0;
server = socket(PF_INET, SOCK_STREAM, 0);
if( server == -1 )
{
printf("server socket error\n");
return -1;
}
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = htonl(INADDR_ANY);
saddr.sin_port = htons(8888);
if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 )
{
printf("server bind error\n");
return -1;
}
if( listen(server, 128) == -1 )
{
printf("server listen error\n");
return -1;
}
printf("server start success\n");
maxfd = server;
fd_set rets;
fd_set allrets;
FD_ZERO(&allrets);
FD_SET(server, &allrets);
while( 1 )
{
rets = allrets;
ret = select(maxfd + 1, &rets, NULL, NULL, NULL);//使用select函数进行监听
if(ret > 0)
{
if(FD_ISSET(server, &rets))
{
/*有客户端连接上来了*/
asize = sizeof(caddr);
client = accept(server, (struct sockaddr*)&caddr, &asize);
FD_SET(client, &allrets);//将连接上来的客户端设置进去
if(maxfd < client)
{
/*更新最大文件描述符*/
maxfd = client;
}
}
else if(ret == -1)
{
for(i = server + 1; i <= maxfd; i++)
{
if(FD_ISSET(i, &rets))//判断是哪一个客户端有信息
{
len = read(i, buf, 1024);
if(len == 0)//客户端断开了连接
{
FD_CLR(i, &allrets);//将断开连接的客户端清除出去
close(i);//关闭客户端
}
else
{
printf("read len : %d read buf : %s\n", len, buf);
write(i, buf, len);
}
}
}
}
}
else
{
printf("select is err\n");
}
}
close(server);
return 0;
}
注意点:
使用select()函数时,传入和传出的readfds等参数可能是不一样的。这是因为select()函数会修改传入的文件描述符集合,以指示哪些文件描述符已经就绪。
所以在使用select时需要先将readfds做一个备份,以免丢失一些文件描述符。
四、select函数的缺点
1.低效性:select()函数在功能上存在一些限制。它使用线性扫描的方式来遍历监视的文件描述符集合,因此在大量文件描述符的情况下效率较低。每次调用select()都需要将整个文件描述符集合传递给内核,并在返回时重新检查整个集合以确定哪些文件描述符已就绪。这种线性扫描的开销随着监视的文件描述符数量的增加而增加。
2.文件描述符数量限制:select()函数所能监视的文件描述符数量存在限制,在某些系统上,这个限制可能是比较小的,例如1024。因此,如果要监视的文件描述符数量超过了限制,就需要采用其他方法来解决。
3.阻塞模式:select()函数是一种阻塞式调用,即当没有任何文件描述符就绪时,它会一直阻塞等待。这会导致程序无法进行其他操作。虽然可以通过设置超时时间来解决这个问题,但超时时间过短可能导致错误的超时,而超时时间过长则会影响程序的响应性。
4.受限的事件类型:select()函数只能监视文件描述符的可读性、可写性和异常情况。如果需要监视其他类型的事件,例如定时器事件或信号事件,就无法使用select()函数。
5.每次调用重新初始化:每次调用select()函数之前,需要重新初始化文件描述符集合,将需要监视的文件描述符重新添加到集合中。这样的初始化过程比较繁琐,尤其当文件描述符集合发生变化时,需要手动更新集合。
总结
本篇文章主要讲解了使用select函数进行多路IO服用实现了并发服务器程序的编写。
- 点赞
- 收藏
- 关注作者
评论(0)