服务器编程模型
本文主要讲述常用的5种IO模型,以及两种事件处理模式
IO模型
IO模型有同步与异步IO之分
同步IO会导致请求进程阻塞,直到IO操作完成
异步IO不会导致请求进程阻塞
简单来讲两者区别就是:内核通知可以启动IO操作,为同步IO;内核通知IO操作已经完成,为异步IO
同步IO
1.阻塞式IO
进程调用系统调用(如recvfrom函数)后,要等待数据到达缓冲区或发生某种错误(如被信号中断)才返回
2.非阻塞式IO
可以将套接字设置成非阻塞,这样即使没有数据到来也会立即返回一个错误(通常为EWOULDBLOCK或EAGAIN)
3.IO复用
使用IO复用,一个进程就可以管理多个套接字
Linux上IO复用方式有三种:select、poll、epoll
使用IO复用就可以阻塞在系统调用中上(如epoll的epoll_wait函数),而不是阻塞在IO系统调用上
4.信号驱动式IO
描述符就绪时(有数据到来),内核发送SIGIO信号通知进程处理此描述符
优点就是等待数据到达期间进程不会阻塞
异步IO
5.可以调用aio_read等异步函数,内核会在整个操作完成时通知进程,在等待IO完成期间,进程不会阻塞
与信号驱动式IO相比,信号是在数据准备好(数据已在应用缓冲区中)才产生
事件处理模式(多线程)
Reactor模式:主线程负责监听socket连接,工作线程负责读写数据以及逻辑处理。一般用于同步IO
Proactor模式:主线程和内核负责监听socket连接以及所有IO操作,工作线程用于逻辑处理。一般用于异步IO
epoll应用相关代码
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <string.h>
#include <assert.h>
int main(){
//创建socket,设置socketfd属性:SOCK_NONBLOCK,非阻塞;SOCK_CLOEXEC,进程被替换时会关闭打开的文件描述符
int listenfd=socket(AF_INET,SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC,0);
assert(listenfd>=0);
//设置监听fd地址
struct sockaddr_in address;
bzero(&address,sizeof(address));
address.sin_family=AF_INET;
address.sin_addr.s_addr=htonl(INADDR_ANY);
address.sin_port=htons(1234);
//绑定地址
int ret=bind(listenfd,(struct sockaddr*)(&address),sizeof(address));
assert(ret!=-1);
//设置监听队列
assert(listen(listenfd,SOMAXCONN)!=-1);
//创建epollfd,EPOLL_CLOEXEC:进程被替换时会关闭打开的文件描述符
int epollfd=epoll_create1(EPOLL_CLOEXEC);
assert(epollfd!=-1);
//添加fd以及感兴趣事件(EPOLLIN,读事件;EPOLLOUT,写事件)
{
epoll_event event;
event.data.fd=listenfd;
event.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event);
}
epoll_event events[100];
char read_buf[1024];
char write_buf[2048];
while(1)
{
//阻塞进程,等待IO事件到来
int num=epoll_wait(epollfd,events,100,-1);
assert(num>=0);
//遍历事件数组
for(int i=0;i<num;i++)
{
int sockfd=events[i].data.fd;
if(sockfd==listenfd)
{
//设置连接fd地址
struct sockaddr_in client_addr;
bzero(&client_addr,sizeof(client_addr));
socklen_t client_len=sizeof(client_addr);
//接受新连接
int connfd=accept4(listenfd,(struct sockaddr*)&client_addr,&client_len,SOCK_NONBLOCK|SOCK_CLOEXEC);
assert(connfd>0);
printf("new conn fd:%d\n",connfd);
//添加事件
epoll_event event;
event.data.fd=connfd;
event.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,connfd,&event);
}
else if(events[i].events&EPOLLIN)
{
memset(read_buf,'\0',sizeof(read_buf));
int bytes_read=recv(sockfd,read_buf,sizeof(read_buf)-1,0);
if(bytes_read==0)
{
printf("client %d closed\n",sockfd);
close(sockfd);
}
else
{
memset(write_buf,'\0',sizeof(write_buf));
strcpy(write_buf,"server receive data:");
strcat(write_buf,read_buf);
send(sockfd,write_buf,sizeof(write_buf)-1,0);
}
}
}
}
close(listenfd);
return 0;
}
结果:
EPOLL有两种工作模式:
LT模式(默认):LT会去遍历在epoll事件表中每个文件描述符,只要文件描述符还有感兴趣的事件,每次epoll_wait都会返回。
ET模式(需要设置,EPOLLET):ET在发现有我们感兴趣的事件发生后,立即返回,并且sleep这一事件的epoll_wait,不管该事件有没有结束。例如读事件,有数据要读到空,errno返回EAGAIN或EWOULDBLOCK为止,否则下次epoll_wait返回不会有上一次IO事件,会导致数据不完整
ET需要相应文件描述符非阻塞,否则最后的读写会阻塞进程
通常情况下只需要注册读事件,毕竟写一般都是主动发送就可以了,除非遇到某种情况发送失败或没有发送完才需要注册写事件
#include <sys/epoll.h>
int epoll_create(int size);
创建epoll文件描述符
int epoll_ctl(int epfd,int op,int fd,struct epoll_event event);
op: EPOLL_CTL_ADD 向事件表中注册fd的事件
EPOLL_CTL_MOD 修改fd上的注册事件
EPOLL_CTL_DEL 删除fd上的注册事件
event:指定事件,是epoll_event结构体指针类型
data可以用于存储用户数据,但注意是联合体类型,因此只能使用其中一个
int epoll_wait(int epfd,struct epoll_event events,int maxevents,int timeout);
一段时间内等待一组文件描述符上的事件,返回文件描述符的个数,并将所有就绪事件复制到第二个参数events中
参考
UNIX网络编程 卷1:套接字联网API 第3版
Linux高性能服务器编程——游双