网络编程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>

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ySh_ppp

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值