io复用
五种io:
io复用
阻塞i/o
非阻塞i/o
信号驱动i/o
异步i/o
前4种是同步io最后一种才是真正的异步io
同步IO和异步IO的区别就在于:数据访问的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!
同步异步在于双方的配合消息的通信机制
阻塞与非阻塞在于一端对待某个调用的态度,等待返回的状态。
io复用
阻塞i/o
非阻塞i/o
信号驱动i/o
异步i/o
前4种是同步io最后一种才是真正的异步io
同步IO和异步IO的区别就在于:数据访问的时候进程是否阻塞!
阻塞IO和非阻塞IO的区别就在于:应用程序的调用是否立即返回!
同步异步在于双方的配合消息的通信机制
阻塞与非阻塞在于一端对待某个调用的态度,等待返回的状态。
1.select
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout)fd_set结构体仅包含一个整形数组
struct timeval struct timeval
{ {
long tv_sec; long sec(秒)
long tv_usec; long usec(微秒)
} }
成功返回注册文件描述符的总数,包括就绪和没有就绪的pollepoll类似。
nfds,是指定的最大套接字+1.
fd_set是一个整形数组总共有1024bit位对应相应描述符
;FD_ZERO(readfds);FD_SET(1,&set);FD_CLR(1,&set);这段代码描述了,如何构造我们对于听感兴趣
的结构体。首先清0之后,把1描述符的加入这个感兴趣的读事件里面,删除这个事件。将构造好的结构体地址传回select
完成设置。如果对于读写异常哪个不感兴趣不用定义结构体直接传NULL指针。
timeout系统等待的时间,超出时间就不在阻塞(阻塞NULL,半阻塞,非阻塞把结构体里面全设成0,填入结构体指针)
select总结:select流程是线程在走到select的时候按照select中timeval停相应的时间陷入内核态,一旦我们感兴趣
的事件发生了内核就会把相应比特位的数改为1.我们可以通过FD_ISSET查看相应的标识符是否有事件(非0则有)。
我们每次调用select都要先清FD_ZERO一次,因为内核改变了这个原有fd_set结构体变量。注意,是轮询到第一个就返回(
这圈要先执行完是马上不是立刻所以只要在这圈内的就绪事件都会不被制成1),
不是说一直轮询到指定时间结束,然后把所有可读写的套接字一起在集合内返回。
//网络编程服务端(select)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>//htons()函数头文件
#include <netinet/in.h>//inet_addr()头文件
#include <fcntl.h>
#include <sys/select.h>
#include <time.h>
#include "pub.h"
#define MAXSOCKET 1024
int main(int arg, char *args[])
{
if (arg < 2)
{
printf("please print one param!\n");
return -1;
}
//create server socket
int listen_st = server_socket(atoi(args[1]));
if (listen_st < 0)
{
return -1;
}
//设置非阻塞文件描述符
setnonblock(listen_st);
int i = 0;
int maxfd = 0; //最大的socket,select函数第一个参数使用
/*
*建立客户端连接池
*/
int client[MAXSOCKET]; //select最大支持1024个socket连接
//将所有的客户端连接池初始化,将每个成员都设置为-1,表示无效
for (; i < MAXSOCKET; i++)
{
client[i] = -1;
}
maxfd = listen_st; //程序刚开始执行时,只有服务端socket,所以服务端socket最大
//定义一个事件数组结构
fd_set allset;
while (1)
{
//初始化一个fd_set对象
FD_ZERO(&allset);
//将服务器端socket放入事件数组allset中(服务端socket需要特殊处理,所以没有放入socket池中)
FD_SET(listen_st, &allset);
//先假设最大的socket就是服务器端socket
maxfd = listen_st;
//遍历socket池 找出值最大的socket
for (i = 0; i < MAXSOCKET; i++)
{
if (client[i] != -1)
{
//将socket池中的所有socket都添加到事件数组allset中
FD_SET(client[i], &allset);
if (client[i] > maxfd)
{
maxfd = client[i]; //maxfd永远是值最大的socket
}
}
}
//开始等待socket发生读事件
int rc = select(maxfd + 1, &allset, NULL, NULL, NULL);
/*
* select函数返回之后,allset数组的数据产生变化,现在allset数组里的数据是发生事件的socket
* select和epoll不同,select每次返回后,
* 会清空select池中的所有socket,所有的socket等select返回后就被清除了
* 所以必须由程序建立一个socket池,每次都将socket池中的socket加入到select池中
* select不会为程序保存socket信息,这与epoll最大的不同,
* epoll添加到events中的socket,如果不是程序员清除,epoll永远保留这些socket
*/
if (rc < 0)
{
//select函数出错,跳出循环
printf("select failed ! error message:%s\n", strerror(errno));
break;
}
//判断是否是服务器socket接收到数据,有客户端连接
if (FD_ISSET(listen_st, &allset))
{
//accept
int client_st = server_accept(listen_st);
if (client_st < 0)
{
//直接跳出select循环
break;
}
//客户端连接成功 设置客户端非阻塞
setnonblock(client_st);
//将客户端socket加入socket池中
for (i = 0; i < MAXSOCKET; i++)
{
if (client[i] == -1)
{
client[i] = client_st;
break;
}
}
if (i == MAXSOCKET)
{
//socket池已满,关闭客户端连接
close_socket(client_st);
}
}
//处理客户端的socket
for (i = 0; i < MAXSOCKET; i++)
{
if (client[i] == -1)
{
//无效socket直接退出
continue;
}
//判断是否是这个socket有事件发生
if (FD_ISSET(client[i], &allset))
{
//接收消息
if (socket_recv(client[i]) < 0)
{
//如果接收消息出错,关闭客户端socket
close_socket(client[i]);
//从socket池中将这个socket清除
client[i] = -1;
}
rc--;
}
//说明所有消息的socket已经处理完成
if (rc < 0)
{
//备注:双循环break只能跳出最近的一重循环
break;
}
}
}
//close server socket
close_socket(listen_st);
return 0;
}
2.poll
int poll(struct pollfd*fds,unsigned long nfds,int timeout)
poll相对select来说支持文件描述符数目多于select,事件类型相对于select更为丰富。
struct pollfd
{
int fd;//描述符
short events;//监听类型(POLLIN普通数据读)
short revents;//真的有事件发生内核将它相应感兴趣的bit位置为1.
}
fds是一个数组,nfds是数组最大值MAX,timeout单位是毫秒填-1就是一直阻塞
程序:
int create_socket();
void fds_init(struct pollfd fds[]);
void fds_add(struct pollfd fds[],int fd);
void fds_del(struct pollfd fds[],int fd);
int main()
{
int sockfd=create_socket();
assert(sockfd!=-1);
struct pollfd fds[MAX];
fds_init(fds);
fds_add(fds,sockfd);
while(1)
{
int n=poll(fds,MAX,5000);
if(n==-1)
{
perror("poll error");
continue;
}
else if(n==0)
{
printf("time out");
continue;
}
else
{
int i=0;
for(;i<MAX;i++)
{
if(fds[i].fd==-1)
{
continue;
}
if(fds[i].revents & POLLIN)
{
if(fds[i].fd==sockfd)
{
struct sockaddr caddr;
int len=sizeof(caddr);
int c=accept(sockfd,(struct sockaddr *)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d\n");
fds_add(fds,c);
}
else
{
char buff[128]={0};
if(recv(fds[i].fd,buff,127,0)<=0)
{
printf("duan kai");
close(fds[i].fd);
fds_del(fds,fds[i].fd);
continue;
}
else
{
printf("read:%s\n",buff);
send(fds[i].fd,"ok",2,0);
}
}
}
int create_socket()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in saddr;
memset(&saddr,0,sizeof(saddr));
saddr.sin_family=AF_INET;
saddr.sin_addr.s_addr=inet_addr("192.168.31.54");
saddr.sin_port=htons(6000);
int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));
assert(res!=-1);
listen(sockfd,5);
return sockfd;
}
void fds_del(struct pollfd fds[],int fd)
{
int i=0;
for(;i<MAX;i++)
{
if(fds[i].fd==fd)
{
fds[i].fd=-1;
fds[i].events=0;
break;
}
}
}
void fds_add(struct pollfd fds[],int fd)
{
int i=0;
for(;i<MAX;i++)
{
if(fds[i].fd==-1)
{
fds[i].fd=fd;
fds[i].events=POLLIN;
}
}
}
void fds_init(struct pollfd fds[])
{
int i=0;
for(;i<MAX;i++)
{
fds[i].fd=-1;
fds[i].events=0;
}
}
总结:poll相对于select事件类型多而且存放描述符也多。poll中的pollfd结构体中就存放fd,他不用
我们另外维护一个数组像select一样,直接可以建立pollfd数组,编程上较简单一些。
3.epoll
int epoll_create(int MAX)
这个函数是用来建立内核事件表,它与select和poll不同不用再用一个数组取维护标识符。返回一个标识符
int epoll_ctl(int epfd,int op,int fd,struct epoll_event event)
这个epfd是内核事件表的标识符,op是增加删除修改fd选项,fd是io标识符
struct epoll_event
{
_unit32_t events;//读写事件与poll相同前面加E如EPOLLIN
epoll_data_t data;//这个结构体如下
}
tpyedef union epoll_data
{
int fd;//io标志符
void *ptr;//用户数据
uint32_t u32
}epoll_data_t;
int epoll_wait(int epfd,epoll_event *event,MAX,int timeout)
第一个是内核事件表标识符,第二个是一个数组指针如果有事件相应就把内核事件表中的epoll_event加入到
里面,timeout与poll一样单位是毫秒。MAX就是最多监听多少个事件。
4.ET和LT模式
LT模式是poll和select的固有模式,但是epoll可以使用ET模式,ET模式较LT模式更为高效。
ET模式返回一次,而LT则会返回多次。
其实ET相对于LT来说,把文件描述符状态跟踪的部分责任由内核空间推到用户空间,内核只关心状态切换即从
(ET>LT)
未就绪到就绪切换时才通知用户,至于保持就绪 状态的,内核不再通知用户,这样在实现非阻塞模型时更方便,
不需要每次操作都先查看文件描述符状态。
(ET必须使用非阻塞模式?)
第一种情况:考虑这种情况:TCP连接被客户端夭折,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚
建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其
他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描
述符都得不到处理。
第二种情况:while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0)
这种情况下由于ET只提醒一次,所以要用while抱住它,这种情况对于recv和send同样道理。我们不停地处理直到
缓存区没有数据或者说队列中没有连接(即最后一次)这个时候我们本想最后一次进入while判断,想象返回值
可以是小于等于0.但是一旦进入阻塞的调用就出不来了,直到有新数据打破在处理,在陷入等待,死循环出不来了。