多进程、 多线程、 进程池、 线程池每一个执行序列在同一时刻只能处理一个socket(监听、 链接)。 以线程池为例: 如果创建N个线程, 同一时刻只能处理一个N的客户连接。
I/O复用: 在一个进程或者一个线程中, 同时监听多个socket。 当有socket上有事件发生时, 程序才会接受数据。
也就是服务器并不会阻塞在recv,当客户端发送数据之后,有数据的文件描述符上就会有一个就绪事件,服务器只要处理就绪事件就可以了。
举个生活中的例子:
QQ上有人给我发消息,QQ通知我,我才会去看消息,不消耗单独的线程或者进程接受或发送数据,用一个进程处理多数据,不是时时刻刻监听一个文件描述符,而是哪个文件描述符上有事件发生就处理哪个。
一:select
select : 在一定时间内,监听用户感兴趣的文件描述符的可读、可写和异常等事件
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timval* timeout)
#include<sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timval* timeout)
nfds表示最大文件描述符的个数+1;
readfds, writefds ,exceptfds 可读,可写和异常 事件,类型为fd_set;
FD_SET(int fd,fd_set *fdset) 设置fdset的位fd
FD_ZERO(fd_set *fdset) 清除fdset的所有位
FD_ISSET(int fd,fd_set *fdset) 判断是哪个文件描述符上有事件发生
那么
FD_ISSET(int fd,fd_set *fdset)
,是怎么判断,内核是怎样处理文件描述符上的就绪事件?
select返回值:>0 返回整个用户注册的事件集合(包括就绪的和未就绪的)
=0 文件描述符上没有事件发生
-1 失败,设置error
select编程流程:
socket;
bind;
listen;
socket;
bind;
listen;
fd_set read;
int fds[128]={0};
int n=select(&read);
if(n<0);
int n=select(&read);
if(n<0);
if(n==0);
while(1)
while(1)
{
FD_ZERO(&read); //经过内核修改的文件描述符不可用,必须清除所有的位
for(i=0;i<128;i++)//轮询的方式
{
if(fds[i]==-1)
continue;
for(i=0;i<128;i++)//轮询的方式
{
if(fds[i]==-1)
continue;
if(fds[i])
{
if(FD_ISSET(fds[i]),&read)//判断文件描述符是否发生事件
if(FD_ISSET(fds[i]),&read)//判断文件描述符是否发生事件
{
if(socketfd==fds[i])
{
c=accept();
}
else
{
recv/send;
}
if(socketfd==fds[i])
{
c=accept();
}
else
{
recv/send;
}
}
}
}
}
}
代码实现:
#include<stdio.h>
#include<assert.h>
#include<pthread.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/socket.h>
#include<stdlib.h>
#include<string.h>
#include<sys/select.h>
#include<arpa/inet.h>
void main()
{
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(6500);
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 read;
int fds[128];
int i=0;
for(;i<128;i++)
{
fds[i]=-1;
}
fds[0]=sockfd;
int max = 0;
while(1)
{ FD_ZERO(&read);//qingkong
int j=0;
for(;j<128;j++)
{
if(fds[j]!=-1)
{
if(max < fds[j]);max = fds[j];
FD_SET(fds[j],&read);
}
}
int n=select(max+1,&read,NULL,NULL,NULL);
if(n<0)
{
printf("error\n");
exit(0);
}
if(n==0)
{
printf("time out\n");
continue;
}
int i=0;
for(;i<128;i++)
{
if(fds[i]==-1) continue;
if(FD_ISSET(fds[i],&read))//
{
if(fds[i]==sockfd)
{
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
assert(c!=-1);
int i=0;
for(;i<128;i++)
{
if(fds[i]==-1)
{
fds[i]=c;
break;
}
}
}
else
{
char buff[128]={0};
int n=recv(fds[i],buff,127,0);
if(n<=0)
{
close(fds[i]);
fds[i]=-1;
continue;
}
printf("%s\n",buff);
send(fds[i],"OK",2,0);
}
}
}
}
}
二、poll
poll 也是在指定时间内轮询一定数量的文件描述符,以测试是否有就绪事件发生的文件描述符
那么它和select 的区别是什么呢?
select的缺点是 它有最大文件描述符数量的限制,只能处理1024个文件描述符,poll利用参数nfds让用户指定文件描述符集合的大小。但用户给的nfds的数不能超过一个struct file文件结构支持的最大fd数,默认是256。
此外poll解决了select内核必须对fd_set集合的在线修改,它利用结构体
那么它和select 的区别是什么呢?
select的缺点是 它有最大文件描述符数量的限制,只能处理1024个文件描述符,poll利用参数nfds让用户指定文件描述符集合的大小。但用户给的nfds的数不能超过一个struct file文件结构支持的最大fd数,默认是256。
此外poll解决了select内核必须对fd_set集合的在线修改,它利用结构体
struct pollfd
{int fd
short events
short revents}
数组,把fd和event,revents分开表示,内核修改描述符自己去修改,不用每次去清空被内核处理后的文件描述符,重新设置一次。
{int fd
short events
short revents}
数组,把fd和event,revents分开表示,内核修改描述符自己去修改,不用每次去清空被内核处理后的文件描述符,重新设置一次。
#include<poll.h>
int poll(struct pollfd*fds, int maxsize,int timeout)
poll返回值和select的返回值代表的意义一样
代码实现:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <poll.h>
#define MAX 128
void Init(struct pollfd *fds, int len)
{
int i = 0;
for(; i < len; ++i)
{
fds[i].fd = -1;
fds[i].events = 0;
}
}
void AddFd(struct pollfd *fds, int len, int fd)
{
int i = 0;
for(; i < len; ++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = fd;
fds[i].events = POLLIN;
break;
}
}
}
void main()
{
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);
struct sockaddr_in ser, cli;
memset(&ser, 0, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(6500);
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);
struct pollfd fds[MAX];
Init(fds, MAX);
AddFd(fds, MAX, listenfd);
while(1)
{
int n = poll(fds, MAX, -1);
assert(n != -1);
if(n == 0)
{
printf("time out\n");
continue;
}
int i = 0;
for(; i < MAX; ++i)
{
if(fds[i].fd == -1)
{
continue;
}
if(fds[i].revents & POLLIN)
{
int fd = fds[i].fd;
if(fd == listenfd)
{
int len = sizeof(cli);
int c = accept(fd, (struct sockaddr *)&cli, &len);
assert(c != -1);
printf("one client link\n");
AddFd(fds, MAX, c);
}
else
{
char buff[128] = {0};
int n = recv(fd, buff, 127, 0);
if(n <= 0)
{
printf("client unlink\n");
close(fd);
fds[i].fd = -1;
fds[i].events = 0;
continue;
}
printf("%d : %s\n", fd, buff);
send(fd, "OK", 2, 0);
}
}
}
}
}
三、epoll
epoll是特有的I/O复用函数
select和poll都是返回发生就绪事件的文件描述符的个数,都是通过轮询的方式去查找到底是哪个文件描述符上发生了就绪事件,epoll把用户关心的文件描述符的事件放在内核的事件表中,不用轮询的方式去一一查找文件描述符集或者事件集。
epoll是特有的I/O复用函数
select和poll都是返回发生就绪事件的文件描述符的个数,都是通过轮询的方式去查找到底是哪个文件描述符上发生了就绪事件,epoll把用户关心的文件描述符的事件放在内核的事件表中,不用轮询的方式去一一查找文件描述符集或者事件集。
epoll使用了一组函数
1,标识内核中的这个事件表,这个文件描述符使用epoll_create(int size)函数创建;
2、操作epoll的内核事件表
int epoll_ctl(int epfd,int op,struct epoll_event *event)
操作类型:
EPOLL_CTL_ADD 注册
EPOLL_CTL_MOD 修改
EPOLL_CTL_DEL 删除
成功返回0,表示成功注册,修改,删除事件表上的事件。
3、epoll系统调用的主要接口:epoll_wait (int epfd,struct epoll_event *events,int maxevents,int timeout)
它是一段超时时间内等待一组文件描述符上的事件。
epoll的返回值:
>0,成功返回就绪的文件描述符的个数。,直接把文件描述符就绪的事件复制到第二个参数数组中
=0,超时,没有事件发生
-1 失败返回-1
>0,成功返回就绪的文件描述符的个数。,直接把文件描述符就绪的事件复制到第二个参数数组中
=0,超时,没有事件发生
-1 失败返回-1
代码实现:
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<unistd.h>
void addfd(int epfd,int fd,int enable_et)
{
struct epoll_event event;
event.data.fd=fd;
event.events=EPOLLIN;
if(enable_et)
{
event.events|=EPOLLET;
}
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&event);
}
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in cli,ser;
ser.sin_family=AF_INET;
ser.sin_port=htons(6500);
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);
int epfd=epoll_create(5);
struct epoll_event revent[128];
addfd(epfd,sockfd,1);
while(1)
{
int ret=epoll_wait(epfd,revent,128,-1);
if(ret==0)
{
printf("time out\n");
}
int i=0;
for(;i<ret;i++)
{
if(sockfd==revent[i].data.fd)
{
int len=sizeof(cli);
int c=accept(revent[i].data.fd,(struct sockaddr*)&cli,&len);
assert(c!=-1);
addfd(epfd,c,0);
}
else if(revent[i].events & EPOLLIN)
{
char buff[128]={0};
int n=recv(revent[i].data.fd,buff,1,0);
if(n<=0)
{
close(revent[i].data.fd);
continue;
}
printf("%s\n",buff);
send(revent[i].data.fd,"OK",2,0);
}
}
}
}
四、select 、poll、epoll的区别:
select
1、返回整个用户注册的事件集合(包括就绪的和未就绪的)
2、限定了文件描述符的数量,1024 个, 0~1023,虽然用户可以修改这个限制,但会导致不可预期的后果。
3、内核会修改文件描述符的位,每次select之前必须对文件描述符集重新设置
4、采用轮询的方式,其时间复杂度为0(n)
poll
1、返回整个用户注册的事件集合(包括就绪的和未就绪的)
2、用户可以自己设置文件描述符的数量
3、统一处理所有事件类型,只需一个事件集参数。利用struct pollfd 结构体,内核通过修改其成员变量revents 反馈就绪的事件
4、采用轮询的方式,其时间复杂度为0(n)
epoll
1、返回就绪文件描述符的个数
2、epoll_wait采用回调的方式,当检测到有就绪的文件描述符时,就回调函数就将该文件描述符上对应的事件插入内核就绪事件队列(内核事件表)中,返回给用户空间是拷贝到struct epoll_event *event数组中去。
1、返回整个用户注册的事件集合(包括就绪的和未就绪的)
2、限定了文件描述符的数量,1024 个, 0~1023,虽然用户可以修改这个限制,但会导致不可预期的后果。
3、内核会修改文件描述符的位,每次select之前必须对文件描述符集重新设置
4、采用轮询的方式,其时间复杂度为0(n)
poll
1、返回整个用户注册的事件集合(包括就绪的和未就绪的)
2、用户可以自己设置文件描述符的数量
3、统一处理所有事件类型,只需一个事件集参数。利用struct pollfd 结构体,内核通过修改其成员变量revents 反馈就绪的事件
4、采用轮询的方式,其时间复杂度为0(n)
epoll
1、返回就绪文件描述符的个数
2、epoll_wait采用回调的方式,当检测到有就绪的文件描述符时,就回调函数就将该文件描述符上对应的事件插入内核就绪事件队列(内核事件表)中,返回给用户空间是拷贝到struct epoll_event *event数组中去。
3、采用ET高效模式
4、采用回调方式检测文件描述符,其时间复杂度为O(1)
4、采用回调方式检测文件描述符,其时间复杂度为O(1)