一、IO模型介绍
1、阻塞IO,常用的scanf、printf、read、write、cout、cin
2、非阻塞IO,读取不到数据也立即返回
recv、send和flags设置为MSG_DONTWAIT
open打开文件时带O_NONBLOCK标志,read、write就是非阻塞的
3、信号驱动IO(Qt的信号和槽机制)
4、异步IO(不能立即得到结果,但可以先去做其他事情,能完成后再通知你)
5、多路复用IO
在不创建新的进程和线程的情况下监控多个文件描述符,多应用于网络编程时一个服务端程序为多个客户端程序提供服务,多用于在业务逻辑简单,客户端需要的服务时间短,响应时间无太高要求的场景。
二、使用select函数实现多路复用IO
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
功能:监控多个文件描述符的 读、写、异常 操作
nfds:最大文件描述符+1
readfds:监控读操作文件描述符集合
writefds:监控写操作文件描述符集合
exceptfds:监控异常操作文件描述符集合
timeout:设置超时时间
返回值:监控到文件描述符的个数,超时返回0,出错返回-1
void FD_CLR(int fd, fd_set *set);
功能:从集合吕删除文件描述符
int FD_ISSET(int fd, fd_set *set);
功能:测试集合中是否有文件描述符存在
void FD_SET(int fd, fd_set *set);
功能:向集合中添加文件描述符
void FD_ZERO(fd_set *set);
功能:清空文件描述符集合
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in addr = {
};
addr.sin_family = AF_INET;
addr.sin_port = htons(5678);
addr.sin_addr.s_addr = inet_addr("192.168.0.111");
socklen_t addrlen = sizeof(addr);
if (bind(sockfd, (struct sockaddr *)&addr, addrlen))
{
perror("bind");
return -1;
}
if (listen(sockfd, 10))
{
perror("listen");
return -1;
}
// 创建两个文件描述符集合
fd_set readfd, readcp;
// 清空两个文件描述符集合
FD_ZERO(&readfd);
FD_ZERO(&readcp);
// 把sockfd加到readfd中
FD_SET(sockfd, &readfd);
int maxfd = sockfd + 1;
struct timeval timeout = {
5, 5000000};
char buf[4096] = {
};
for (;;)
{
// 每次都要重新向select传递所监控的对象
readcp = readfd;
// 监控集合中的文件描述符
int ret = select(maxfd, &readcp, NULL, NULL, &timeout);
// 返回值小于零出错,等于零超时
if (ret <= 0)
continue;
for (int i = 3; i < maxfd; i++)
{
// 检查sockfd 是否有新客户端连接
if (i == sockfd && FD_ISSET(i, &readcp))
{
int clifd = accept(sockfd, (struct sockaddr *)&addr, &addrlen);
if (clifd < 0)
continue;
FD_SET(clifd, &readfd);
if (clifd + 1 >= maxfd)
{
maxfd = clifd + 1;
}
}
else if (FD_ISSET(i, &readcp))
{
int ret = recv(i, buf, sizeof(buf), 0);
if (ret <= 0)
{
FD_CLR(i, &readfd);
continue;
}
printf("fd:%d recv:%s\n", i, buf);
send(i, buf, strlen(buf) + 1, 1);
}
}
}
}
select设计不合理的地方:
1、所有被监视的文件描述符都需要检查(效率不高)。
2、每次调用select都需要向它传递新的监视对象信息。
select的优点是:
程序的兼容性高,所有操作系统都支持。
扫描二维码关注公众号,回复:
15401239 查看本文章
三、使用pselect函数实现多路复用IO
int pselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timespec *timeout, const sigset_t *sigmask);
功能:与select的功能大致类似
区别:
1、select函数用的timeout参数,是一个timeval的结构体(包含秒和微秒),然而pselect用的是一个timespec结构体(包含秒和纳秒)
2、select函数可能会为了指示还剩多长时间而更新timeout参数,然而pselect不会改变timeout参数
3、select函数没有sigmask参数,当pselect的sigmask参数为null时,两者行为时一致的。有sigmask的时候,pselect相当于如下的select()函数,在进入select()函数之前手动将信号的掩码改变,并保存之前的掩码值;select()函数执行之后,再恢复为之前的信号掩码值。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in addr = {
};
addr.sin_family = AF_INET;
addr.sin_port = htons(5678);
addr.sin_addr.s_addr = inet_addr("192.168.0.111");
socklen_t addrlen = sizeof(addr);
if (bind(sockfd, (struct sockaddr *)&addr, addrlen))
{
perror("bind");
return -1;
}
if (listen(sockfd, 10))
{
perror("listen");
return -1;
}
// 创建两个文件描述符集合
fd_set readfd, readcp;
// 清空两个文件描述符集合
FD_ZERO(&readfd);
FD_ZERO(&readcp);
// 把sockfd加到readfd中
FD_SET(sockfd, &readfd);
int maxfd = sockfd + 1;
// struct timeval timeout = {5,5000000};
struct timespec timeout = {
5, 500000000};
char buf[4096] = {
};
for (;;)
{
// 每次都要重新向select传递所监控的对象
readcp = readfd;
// 监控集合中的文件描述符
// pselect 不会屏蔽信号,需要把已经屏蔽的信号集经pselect函数,pselect会把屏蔽的信号取消,等执行结束后再还原屏蔽的信号
int ret = pselect(maxfd, &readcp, NULL, NULL, &timeout, NULL);
// 返回值小于零出错,等于零超时
if (ret <= 0)
continue;
for (int i = 3; i < maxfd; i++)
{
// 检查sockfd 是否有新客户端连接
if (i == sockfd && FD_ISSET(i, &readcp))
{
int clifd = accept(sockfd, (struct sockaddr *)&addr, &addrlen);
if (clifd < 0)
continue;
FD_SET(clifd, &readfd);
if (clifd + 1 >= maxfd)
{
maxfd = clifd + 1;
}
}
else if (FD_ISSET(i, &readcp))
{
int ret = recv(i, buf, sizeof(buf), 0);
if (ret <= 0)
{
FD_CLR(i, &readfd);
continue;
}
printf("fd:%d recv:%s\n", i, buf);
send(i, buf, strlen(buf) + 1, 1);
}
}
}
}
四、使用poll函数实现多路复用IO
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds:所有被监控的文件描述符结构体数组
nfds:数组的长度
timeout:超时时间,毫秒单位
struct pollfd
{
int fd; //被监控文件描述符
short events; // 等待的需要监控事件
short revents; // 实际发生了的事件,也就是返回结果
};
events:
POLLIN 普通或优先级带数据可读
POLLPRI 高优先级数据可读
POLLOUT 普通数据可写
POLLRDHUP 对方socket关闭
POLLERR 发生错误
POLLHUP 发生挂起
POLLNVAL 描述字不是一个打开的文件
poll特点:
1.文件描述符没有最大限制 -数据结构:链表
2.每次调用都需要将fd集合从用户态拷贝到内核态
3.内核需要遍历所有fd,效率低
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <poll.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in addr = {
};
addr.sin_family = AF_INET;
addr.sin_port = htons(5678);
addr.sin_addr.s_addr = inet_addr("192.168.0.111");
socklen_t addrlen = sizeof(addr);
if (bind(sockfd, (struct sockaddr *)&addr, addrlen))
{
perror("bind");
return -1;
}
if (listen(sockfd, 10))
{
perror("listen");
return -1;
}
nfds_t nfds = 10, maxi = 1;
struct pollfd *fds = calloc(sizeof(struct pollfd), 10);
fds[0].fd = sockfd;
fds[0].events = POLLIN;
for (;;)
{
int ret = poll(fds, nfds, 100000);
if (fds[0].revents & POLLIN)
{
int clifd = accept(sockfd, (struct sockaddr *)&addr, &addrlen);
if (clifd < 0)
continue;
for (int i = 0; i < nfds; i++)
{
if (0 == fds[i].fd)
{
fds[i].fd = clifd;
fds[i].events = POLLIN;
maxi = i + 1;
break;
}
}
}
for (int i = 1; i < maxi; i++)
{
if (fds[i].revents & (POLLIN))
{
char buf[4096] = {
};
int ret = recv(fds[i].fd, buf, sizeof(buf), 0);
if (ret <= 0)
{
fds[i].fd = 0;
continue;
}
printf("clifd:%d recv:%s\n", fds[i].fd, buf);
send(fds[i].fd, buf, strlen(buf) + 1, 1);
}
}
}
}
五、使用epoll函数实现多路复IO
int epoll_create(int size);
功能:创建用于保存被监控文件描述符的空间
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:向文件中添加、删除,文件描述符
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
功能:监控文件描述符
特点:
1.文件描述符没有最大限制 --数据结构:红黑树
2.只需拷贝一次fd到内核态
3.内核只需判断就绪链表是否为空,不需要遍历所有fd,效率高,并把就绪fd拷贝到用户空间
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
int main()
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (0 > sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in addr = {
};
addr.sin_family = AF_INET;
addr.sin_port = htons(6666);
addr.sin_addr.s_addr = inet_addr("192.168.0.111");
socklen_t addrlen = sizeof(addr);
if (bind(sockfd, (struct sockaddr *)&addr, addrlen))
{
perror("bind");
return -1;
}
if (listen(sockfd, 10))
{
perror("listen");
return -1;
}
int epfd = epoll_create(10);
struct epoll_event event;
event.data.fd = sockfd;
event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
struct epoll_event *events = calloc(sizeof(struct epoll_event), 10);
for (;;)
{
int cnt = epoll_wait(epfd, events, 10, 100000);
if (cnt <= 0)
continue;
for (int i = 0; i < cnt; i++)
{
printf("cnt=%d fd=%d sockfd:%d\n", cnt, events[0].data.fd, sockfd);
if (events[i].data.fd == sockfd)
{
int clifd = accept(sockfd, (struct sockaddr *)&addr, &addrlen);
if (0 > clifd)
continue;
event.data.fd = clifd;
event.events = EPOLLIN;
epoll_ctl(epfd, EPOLL_CTL_ADD, clifd, &event);
}
else
{
char buf[4096] = {
};
int ret = recv(events[i].data.fd, buf, sizeof(buf), 0);
if (ret <= 0)
{
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, events + i);
continue;
}
printf("clifd:%d recv:%s\n", events[i].data.fd, buf);
send(events[i].data.fd, buf, strlen(buf) + 1, 1);
}
}
}
}