提出背景
不管是多线程,或者多进程,以及线程池,进程池。他们都存在一定的效率问题。
1.每个进程或线程只能为一个客户端进行服务,知道该客户端结束。(如果客户端在同一时间的访问数量特别大呢?)
2.当客户端发送来数据后,分配线程或进程为其服务完后,就要等待该客户端的下一次数据。(如果该客户端连接成功仅仅发了一次数据呢?)
他们虽然能够进行与客户的交互服务,但是当在规定的时间内,系统(或者池)分配不出足够的进程或线程,达不到及时响应的要求。
I/O复用
1.决解的问题:一个进程或线程能够同时对多个文件描述符(socket)进行服务。
2.服务器上的进程或线程 如何将多个文件描述符进行同一监听,当任意文件描述符上有事件发生,其都能够及时处理。
列举几种我们要学习的I/O复用技术:1.select ; 2. poll ; 3. epoll(Linux独有的)。
select函数
1.函数原型:
#include<sys/select.h>
int select (int nfds , fd_set *readfds , fd_set *writefds , fd_set *excefds , struct timeval*timeout);
2.fd_set结构体
#include<typesizes.h>
#define _FD_SETSIZE 1024 //决定了fd_set能容纳的文件描述符数量
#include<sys/select.h>
#define FD_SETSIZE _FD_SETSIZE
typedef long_int _fd_mask;
#undef _NFDBITS
#define _NFDBITS { 8 *(int)sizeof(_fd_mask) ) // 8 * 4 = 32
typedef struct
{
#ifdef _USE_XOPEN
_fd_mask fds_bits[_FD_SETSIZE / _NFDBITS]; //位数组 1024 / 32 = 32
#define _FDS_BITS(set) ((set) -> fds_bits)
#else
fd_mask _fds_bits[_FD_SETSIZE / _NFDBITS]; //位数组 1024 / 32 = 32
#define _FDS_BITS(set) ((set) -> fds_bits)
#endif
}fd_set;
其实呢,简而言之就是
typedef struct
{
int fds_bits[32];
}fd_set;
fd_set结构体仅仅包含一个整型数组,该数组的每一个元素的每一位(bit)标记一个文件描述符。int类型,总共32个元素,那么总共可以标记32 * 32 = 1024个文件描述符。
由于位运算操作过于繁琐,提供了一系列的宏来访问fd_set结构体中的位:
头文件: | #include<sys/select.h> |
清除fdset的所有位 | FD_ZERO(fd_set *fdset); |
设置fdset的位fd | FD_SET(int fd, fd_set *fdset); |
清除fdset的位fd | FD_CLR( int fd , fd_set *fdset); |
测试fdset的位fd是否被设置 | int FD_ISSET(int fd , fd_set *fdset); |
3.参数介绍
1.nfd: | 被监听文件描述符总数 + 1 |
2.readfds: | 用户感兴趣的可读事件的文件描述符集合 |
3.writefds: |
可写事件的文件描述符的集合 |
4.exceptfds: | 异常事件的文件描述符集合 |
5.timeout | 设置超时时间,如果timeout设置为NULL,则select一直阻塞,直到某个文件就绪。 |
4.如何将文件描述符分别设置到readfds, writefds , exceptfds里?
通过位运算宏函数FD_SET()。
5.select返回后,如何知道哪些文件描述符已经就绪?
循环探测;通过宏函数int FD_ISSET(int fd , fd_set *fdset);
6.每次调用select之前,需要做什么准备工作?
将所有的readfds , writefds , exceptfds全部都置为空。
逻辑理念
测试代码
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
void Init_fds(int* fds,int len)
{
int i = 0;
for(;i<len;++i)
{
fds[i] = -1;//都是无效的
}
}
void Delete_fds(int* fds,int fd,int len)
{
for(int i = 0;i<len;++i)
{
if(fds[i] == fd)
{
fds[i] = -1;
break;
}
}
}
void Insert_fds(int* fds,int fd,int len)
{
int i =0;
for(;i<len;++i)
{
if(fds[i] == -1)
{
fds[i] = fd;
break;
}
}
}
int main()
{ //完成TCP连接服务
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
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(sockfd, (struct sockaddr*)&ser,sizeof(ser));
assert(res != -1);
listen(sockfd,5);
//fd_set,并将sockfd插入进去
fd_set readfds;
int fds[100];//数组
Init_fds(fds,100);//初始化全部设置为-1,均为无效的文件描述符。
Insert_fds(fds,sockfd,100);//将sockfd添加到fds中
while(1)
{
int maxfd = -1;
FD_ZERO(&readfds);//清空
for(int i = 0;i<100;++i)//fds[100]的循环探测
{
if(fds[i] != -1)//说明数组有了就绪的事件
{
if((fds[i] > maxfd))//对于maxfd的处理
{
maxfd = fds[i];
}
FD_SET(fds[i],&readfds);//设置fd位。
}
}
int n = select(maxfd + 1,&readfds,NULL,NULL,NULL);//读取
if(n<=0)
{
printf("select is fail:\n");
continue;
}
//n>0有文件描述符就绪,如何进行探测呢?
for(int i = 0;i < 100;++i)//循环探测
{
if(fds[i] != -1 && FD_ISSET(fds[i],&readfds))//探测fdset的位fd是否被设置
//就绪
{
if(fds[i] == sockfd)//连接请求
{
int len = sizeof(cli);
int c = accept(sockfd,(struct sockaddr*)&cli,(socklen_t*)&len);
if(c<0)
{
printf("accept is fail:\n");
continue;
}
Insert_fds(fds,c,100);//对这个c加入到 readfds中fds[100]
}
else
{
int fd = fds[i];
char buff[128]={0};//有数据可读取
int n= recv(fd,buff,127,0);//只读取一次
if(n<=0)
{
close(fd);
Delete_fds(fds,fd,100);
continue;
}
else
{
printf("%d: %s\n",fd,buff);
send(fd,"OK",2,0);
}
}
}
}
}
close(sockfd);
}