网络编程I/O模型分析
1、阻塞I/O、非阻塞I/O模型
阻塞IO模型
例如
在服务端等待客户端连接时调用会产生阻塞,一直等待客户连接
int accept(int socketfd,struct sockaddr *addr,socklen_t *addrlen);
等待接受内容,客户端或服务端产生阻塞,一直等对方发送数据
int recv(int socketfd,void *buff,size_t buffsize,int flag);
好处是信息同步
坏处就是程序阻塞,不能执行其他的命令。
非阻塞型
在获取内容时,不阻塞,没有获取到直接返回。
坏处:浪费时间,因为可能大多数时候是没有数据的
常用函数
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
fcntl ( fd, F_SETFL, flag | O_NONBLOCK ) ;
2、I/O复用模型
slecet()
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
@nfds:
最大描述文件,也就是文件遍历边界
@readfds:
select监视的可读文件描述符集合,为fd_set结构体指针
@writefds:
select监视的可写文件描述符集合,fd_set结构体指针
@exceptfds:
select监视的错误文件描述符集合,fd_set结构体指针
@timeout:
timeval结构体
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
表明超时的时间
return:
-1:select错误
0:超时
>0:正确三个返回的描述符集中包含的文件描述符的数量(即readfds、writefds、exceptds中设置的位的总数)
例子:
客户端与服务端连接,最大连接数在服务器端的mapSize宏定义。
客户端每1秒发送“PPPPPPPP”到服务端,同时可mapSize个客户端进行连接。
服务端会将客户端的ip、port、数据、连接到服务器的个数打印。
代码几乎每行都进行了注释了。
输出结果为:
connect NUM :6
192.168.1.119 38161 PPPPPPPP
connect NUM :6
192.168.1.119 38162 PPPPPPPP
connect NUM :6
192.168.1.119 38163 PPPPPPPP
connect NUM :6
192.168.1.119 38164 PPPPPPPP
connect NUM :6
192.168.1.119 38165 PPPPPPPP
connect NUM :6
192.168.1.119 38166 PPPPPPPP
client:
#define PORT 5555
#define IP "192.168.1.108"
void bzero(void *s,size_t n);
int main(int argc, char const *argv[])
{
char *buff=malloc(sizeof(char)*MAXLINE);
bzero(buff,MAXLINE);
//将前8位写入‘P’
memset(buff,80,8);
//创建socket描述符
int clientfd;
clientfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
struct sockaddr_in clientAddr;
bzero(&clientAddr,sizeof(clientAddr));
clientAddr.sin_port=htons(PORT);
clientAddr.sin_family=AF_INET;
//写入addr
inet_pton(AF_INET,IP,&clientAddr.sin_addr.s_addr);
//连接
connect(clientfd,(struct sockaddr *)&clientAddr,sizeof(clientAddr));
//创建可写文件描述符集合
fd_set wset;
//清空可写文件描述符集合
FD_ZERO(&wset);
//将连接socket的文件描述符写入wset
FD_SET(clientfd,&wset);
while(1){
//监视文件描述符
int ret=select(clientfd+1,NULL,&wset,NULL,NULL);
//错误
if(ret<0){
perror("select error");
continue;
}
//可写情况 将buff传输到服务端
else if(FD_ISSET(clientfd,&wset)) {
send(clientfd,buff,8,0);
}
sleep(1);
}
free(buff);
close(clientfd);
return 0;
}
服务端:
#define mapSize 20
#define PORT 5555
struct {
int fd;
char ip[16];
int port;
}map[mapSize];
void bzero(void *s, size_t n);
int main(int argc, char const *argv[])
{
//创建收发缓冲区
char *buff=malloc(sizeof(char)*MAXLINE);
//创建监听端口的文件描述符 以及 连接的数量
int listenfd,connectNum=0;
//对监听描述符赋值
listenfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//设置sock选项为收发地址可为同一个地址
int opt=1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
//创建服务器与客户端的sock结构体
struct sockaddr_in serverAddr,clientAddr;
int clientSize=sizeof(clientAddr);
//清空客户表以及结构体
bzero(&serverAddr,sizeof(serverAddr));
bzero(&map,sizeof(map));
bzero(&clientAddr,clientSize);
//服务端设置为IPV4 端口号为5555 IP为任意本机地址
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(PORT);
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定监听
bind(listenfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
//开始监听
listen(listenfd,3);
//创建最大描述符
int maxfd=listenfd;
//创建可读文件描述符集合rset与备用的tmp (以防止第一次创建连接时并没有可读选项 而被读的情况)
fd_set rset,tmp;
//清空rset的文件描述符
FD_ZERO(&rset);
//将监听文件描述符设置进rset
FD_SET(listenfd,&rset);
while(1){
//重新赋值,结构体整体拷贝
tmp=rset;
printf("connect NUM :%d\n",connectNum );
//遍历描述符
int ret=select(maxfd+1,&tmp,NULL,NULL,NULL);
//-1为出错 0为超时
if(ret==-1){
perror("select error");
if(errno==EINTR)continue;
else if(errno == EAGAIN||errno == EWOULDBLOCK)exit(1);
}
//是监听的情况
if(FD_ISSET(listenfd,&tmp)){
//连接
int connfd=accept(listenfd,(struct sockaddr*)&clientAddr,&clientSize);
if(connfd<0){
perror("accept error");
continue;
}
//查询表
for (int i = 0; i < mapSize; ++i)
{
//找到空的表处
if(map[i].fd)continue;
//写入fd ip port
map[i].fd=connfd;
strcpy(map[i].ip,inet_ntoa(clientAddr.sin_addr));
map[i].port=ntohs(clientAddr.sin_port);
printf("accept %s %d\n",map[i].ip,map[i].port );
connectNum++;
if(connectNum==20)FD_CLR(listenfd,&rset);
break;
}
//将连接的客户端描述符放入rset可读文件描述符集合中
FD_SET(connfd,&rset);
//将客户端描述符与最大描述符的较大者写入最大描述符中
maxfd=connfd>maxfd?connfd:maxfd;
}
if(connectNum<20) FD_SET(listenfd,&rset);
//不是监听情况
for (int i = 0; i < mapSize; ++i)
{
int fd=map[i].fd;
//查找到是哪一个文件可读
if(!FD_ISSET(fd,&tmp))continue;
bzero(buff,MAXLINE);
//接受信息
int ret=recv(fd,buff,MAXLINE,0);
//接受出错
if(ret<=0){
printf("no msg\n");
//关闭客户端
close(fd);
//将该客户端移除rset结构体
FD_CLR(fd,&rset);
//将该客户端的信息移除表
bzero(&map[i],sizeof(map[i]));
connectNum--;
continue;
}
//打印从客户端得到的信息
printf("%s %d %s\n",map[i].ip,map[i].port,buff );
}
}
close(listenfd);
return 0;
}
pool()
据说与select相同,本文不再赘述。
epool()
结构体
结构体定义:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; // Epoll events
epoll_data_t data; // User data variable
};
epoll_create()
/****************创建epfp函数*******************/
@size:
不起作用了,大于0就行
@flags:
0:与create相同
不为0的话只支持EPOLL_CLOEXEC:
用于设置该描述符的close-on-exec(FD_CLOEXEC)标志。执行时关闭
return:
0:成功
-1:错误,并写入errno
int epoll_create(int size);
int epoll_create1(int flags);
epoll_ctl()
/**************创建等待队列函数*******************/
@epfd:
epoll专用描述符,由epoll_create创建
@op:
EPOLL_CTL_ADD:在epfd中添加fd
EPOLL_CTL_MOD:更改目标相关联的事件
EPOLL_CTL_DEL:在epfd中删除fd,此时事件可为空
@fd:
需要检测的等待队列文件描述符
@event:
epool_event 结构体指针
结构体中的evnet成员位变量:
EPOLLIN:可写
EPOLLOUT:可读
EPOLLRDHUP:套接字关闭或关闭一半的连接
EPOLLPRI:可读紧急数据
EPOLLERR:发生在相关联文件之间的错误,非必要不使用
EPOLLHUP:文件被挂断时。非必要不使用
EPOLLET:边沿触发
EPOLLONESHOT:fd一次性使用,想要重新使用则使用EPOLL_CTL_MOD
return:
0:成功
-1:错误 并写入errno
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
问题:为什么epoll_ctl中有fd的输入还需要结构体中的data联合体成员的fd变量?
答:函数不知道是否输入为fd,因为该联合体可为指针。
epoll_wait()
/********************等待函数*******************/
@epfd:
epoll专用描述符,由epoll_create创建
@events:
epoll_event 结构体执政
@maxevents:
最大事件总数
@timeout:
超时时间:-1 一直阻塞;0:立即返回
return:
大于0:可使用的文件数量
等于0:超时
-1:错误,并写入errno
int epoll_wait (int epfd, struct epoll_event *events,int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,int maxevents, int timeout,const sigset_t *sigmask);
代码
下列服务端代码使用联合体中的指针存放客户信息。每次客户端发送的数据会带上客户的信息一同打印。(客户端与select方法相同)
#define mapSize 20
#define MAXEVENTS 20
typedef void(*a)(void *p);
typedef struct {
int fd;
char ip[16];
int port;
a func;
}Map;
Map clientMap[mapSize];
void clientPrintData(void *p)
{
Map *map=(Map*)p;
printf("%s:ip:%s port:%d",__FUNCTION__,map->ip,map->port );
}
void bzero(void *s, size_t n);
int main(int argc, char const *argv[])
{
int ret=0;
//创建收发缓冲区
char *buff=malloc(sizeof(char)*MAXLINE);
//创建监听端口的文件描述符 以及 连接的数量
int listenfd,connectNum=0;
//对监听描述符赋值
listenfd=socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
//设置sock选项为收发地址可为同一个地址
int opt=1;
setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(int));
//创建服务器与客户端的sock结构体
struct sockaddr_in serverAddr,clientAddr;
int clientSize=sizeof(clientAddr);
//清空结构体
bzero(&serverAddr,sizeof(serverAddr));
bzero(&clientAddr,clientSize);
bzero(&clientMap,sizeof(clientMap));
//服务端设置为IPV4 端口号为5555 IP为任意本机地址
serverAddr.sin_family=AF_INET;
serverAddr.sin_port=htons(PORT);
serverAddr.sin_addr.s_addr=htonl(INADDR_ANY);
//绑定监听
bind(listenfd,(struct sockaddr*)&serverAddr,sizeof(serverAddr));
//开始监听
listen(listenfd,10);
//创建epoll
int epfd=epoll_create(1);
//创建epoll单个事件结构体 与 结构体数组
struct epoll_event ev,events[MAXEVENTS];
bzero(&ev,sizeof(ev));bzero(events,sizeof(events));
//创建一个表
Map listenMap;
listenMap.fd=listenfd;
//将listenfd设置为输入
ev.events = EPOLLIN;
ev.data.ptr = (void *)&listenMap;
//将监听事件写入epfd
ret=epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
if(ret==-1){
perror("epoll_ctl error");
exit(-1);
}
int running=1,nfds=0;
while(running){
nfds=epoll_wait(epfd,events,MAXEVENTS,-1);
if(nfds==-1){
perror("epoll_wait");
exit(-1);
}
printf("%d\n",connectNum );
for (int i = 0; i < (nfds<mapSize?nfds:mapSize); ++i)
{
//创建结构体指针指向事件的指针
Map *map=(Map*)events[i].data.ptr;
//输入事件
if(events[i].events==EPOLLIN){
//新的连接
if(map->fd==listenfd){
//建立连接
int clientfd=accept(listenfd,(struct sockaddr*)&clientAddr,&clientSize);
bzero(&clientMap[connectNum],sizeof(Map));
//在表中写入fd ip port 函数指针
clientMap[connectNum].fd=clientfd;
clientMap[connectNum].port=ntohs(clientAddr.sin_port);
strcpy(clientMap[connectNum].ip,inet_ntoa(clientAddr.sin_addr));
clientMap[connectNum].func=&clientPrintData;
printf("accept :%s:%d\n",clientMap[connectNum].ip,clientMap[connectNum].port );
//将新的连接挂到epoll树上,检测输入与边沿触发
ev.events=EPOLLIN|EPOLLET;
// 将data的联合体内的指针指向该表
ev.data.ptr=(void *)&clientMap[connectNum];
ret=epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
if(ret==-1){
perror("new epoll_ctl error");
exit(-1);
}
connectNum++;
if(connectNum==mapSize)epoll_ctl(epfd,EPOLL_CTL_DEL,listenfd,NULL);
}//读事件
else {
memset(buff,0,MAXLINE);
int fd=map->fd;
ret=recv(fd,buff,MAXLINE,0);
//错误
if(ret==-1){
perror("recv error");
exit(-1);
}//断开连接
else if(ret==0){
printf("no msg\n");
connectNum--;
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
ev.events=EPOLLIN;
ev.data.ptr=&listenMap;
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
}//正常执行指针指向的函数
else{
map->func(map);
printf("data:%s\n",buff );
}
}
}
}
}
close(listenfd);
return 0;
}
3、信号驱动I/O模型
信号驱动通过signation函数配置信号与函数,当信号来临时,
信息还在内核空间,使用读取函数recvfrom()将数据拷贝到应用缓冲区期间,进程阻塞,因此还是同步模型。
4、异步I/O模型
当信号来临时,信息以及从内核空间拷贝到用户态空间了,因此无阻塞,是效率最高的I/O模型。
常用的是#include <aio.h>