在之前的进程池、线程池中,一个进程或线程同一时刻只能处理一个客户端,也就是说进程或者线程经常阻塞在recv,是对进程或者线程的浪费,因此有了I/O复用
一、概念
I/O复用:将N个文件描述法统一监视,当其中某些文件描述符上有事件发生,则程序只处理有事件发生的文件描述符
有三种:select、poll、epoll(linux独有)
二、select
头文件:#include<sys/select.h>
int select (int maxfd,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
(1)maxfd: 监听的文件描述符中 最大的文件描述符+1
(2)readfds writefds exceptfds 分别记录关注的可读、可写、异常三种事件的文件描述符,
在调用select时,三个结构体变量向内核传递传递用户关注的文件描述符
select返回时,三个结构图变量向用户传递有事件发生的文件描述符
对于readfds、writefds、exceptfds 内核会在线修改三个结构体,每次调用select,都必须重新设置
(3)fd_set结构
typedef struct
{
long int fds_bits[32];
}fd_set;
由此定义可见,fd_set仅包含一个长整型的数组,该数组的每一个元素的每一位标记一个文件描述符,fd_set所能容纳的文件描述符数量是1024个
(4)操作fd_set结构的四个宏函数
FD_ZERO(fd_set *fds); 清空fds的所有位
FD_SET(int fd,fd_set *fds); 将fd 设置到 fds上
FD_CLR(int fd,fd_set *fds); 清除fds上的 fd
FD_ISSET(int fd, fd_set *fds); 判断fds上的fd文件描述符是否有事件发生
(5) timeout:设置select一次监听的时间,当时间到达,如果没有文件描述符就绪,则为超时,select 返回 0
(6)返回值 >0 就绪的文件描述符个数
== -1 select调用出错
三、使用----下面是使用select的服务器端代码
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/select.h>
#define FDMAX 100
typedef struct ClientInfo
{
int fd;
struct sockaddr_in addrInfo;
}ClientInfo;
ClientInfo fdall[FDMAX];
int maxfd = -1;
void InitFd()//初始化
{
int i = 0;
for(; i < FDMAX; ++i)
{
fdall[i].fd = -1;
}
}
void AddFd(int fd, struct sockaddr_in cli)//插入文件描述符及客户端的信息
{
int i = 0;
for(; i < FDMAX; ++i)
{
if(fdall[i].fd == -1)
{
fdall[i].fd = fd;
fdall[i].addrInfo = cli;
break;
}
}
}
void DelFd(int fd)
{
int i = 0;
for(; i < FDMAX; ++i)
{
if(fdall[i].fd == fd)
{
fdall[i].fd = -1;
break;
}
}
}
void SetFd(fd_set *fds)//将数组中的文件描述符设置到fd_set结构上上
{
int i = 0;
for(; i < FDMAX; ++i)
{
if(fdall[i].fd != -1)
{
if(fdall[i].fd > maxfd)
maxfd = fdall[i].fd;
FD_SET(fdall[i].fd, fds);
}
}
}
void GetClientLink(int fd)//处理客户端链接
{
struct sockaddr_in cli;
int len = sizeof(cli);
int c = accept(fd, (struct sockaddr*)&cli, &len);
if(c == -1)
{
return;
}
AddFd(c, cli);
}
void DealClientData(int fd, struct sockaddr_in cli)//处理客户端数据
{
char buff[128] = {0};
int n = recv(fd, buff, 127, 0);
if(n <= 0)
{
DelFd(fd);
return;
}
printf("%s:%d %s\n", inet_ntoa(cli.sin_addr), ntohs(cli.sin_port),
buff);
send(fd, "OK", 2, 0);
}
void DealFinshEvent(int listenfd, fd_set *fds)//处理就绪事件
{
int i = 0;
for(; i < FDMAX; ++i)
{
if(fdall[i].fd != -1)
{
int fd = fdall[i].fd;
if(FD_ISSET(fd, fds))//fd是否被设置,则是否有事件发生
{
// listenfd c
if(fd == listenfd)
{
GetClientLink(fd);
}
else
{
DealClientData(fd, fdall[i].addrInfo);
}
}
}
}
}
int main()
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);
struct sockaddr_in ser;
memset(&ser, 0, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
int res = bind(listenfd, (struct sockaddr*)&ser, sizeof(ser));
assert(res != -1);
listen(listenfd, 5);
InitFd();
AddFd(listenfd, ser);
fd_set read;
while(1)
{
SetFd(&read);//
int n = select(maxfd + 1, &read, NULL, NULL, NULL);
if(n <= 0)
{
exit(0);
}
DealFinshEvent(listenfd, &read);
}
}
四、缺点
1、只能关注三种事件类型
2、select 的三个 fd_set 参数是在线修改,每次都必须重新设置
3、fd_set 最多纪录1024个文件描述符 最大值是1023
4、仅仅返回了就绪文件描述符个数,用户探测就绪的文件描述符的事件复杂度O(n)
5、内核使用轮询方式检测文件描述符,内核时间复杂度为O(n)
本文深入探讨I/O复用的概念及其在Linux系统中的实现,包括select、poll和epoll等机制。通过对比分析,解释了如何利用这些机制提高网络编程的效率,特别是在处理大量并发连接时的优势。
755

被折叠的 条评论
为什么被折叠?



