一.select 实现
(1)select流程
基本流程是:
1. 先构造一张有关文件描述符的表; fd_set readfds
2. 清空表 FD_ZERO()
3. 将你关心的文件描述符加入到这个表中; FD_SET()
4. 调用select函数。 selset()
5. 判断是哪一个或哪些文件描述符产生了事件(IO操作); FD_ISSET()
6. 做对应的逻辑处理;
(2)selset函数
头文件: #include<sys/select.h> #include<sys/time.h>
#include<sys/types.h> #include<unistd.h>
声明: int select(int nfds, fd_set *readfds, fd_set *writefds,\
fd_set *exceptfds, struct timeval *timeout);
功能:监测是哪些文件描述符产生事件,阻塞等待产生.
参数:nfds: 监测的最大文件描述个数(文件描述符从0开始,这里是个数,记得+1)
readfds: 读事件集合; // 键盘鼠标的输入,客户端连接都是读事件
writefds: 写事件集合; //NULL表示不关心
exceptfds:异常事件集合; //NULL 表示不关心
timeout: 设为NULL,等待直到某个文件描述符发生变化;
设为大于0的值,有描述符变化或超时时间到才返回。
超时时间检测:如果规定时间内未完成函数功能,返回一个超时的信息,我们可以根据该信息设定相应需求;
返回值: <0 出错 >0 表示有事件产生;
如果设置了超时检测时间:&tv
<0 出错 >0 表示有事件产生; ==0 表示超时时间已到;
结构体如下:
struct timeval {
long tv_sec; 以秒为单位,指定等待时间
long tv_usec; 以毫秒为单位,指定等待时间
};
void FD_CLR(int fd, fd_set *set); //将set集合中的fd清除掉
int FD_ISSET(int fd, fd_set *set); //判断fd是否在set集合中产生了事件
void FD_SET(int fd, fd_set *set); //将fd加入到集合中
void FD_ZERO(fd_set *set); //清空集合
(3)Select特点:
Select特点:
1. 一个进程最多只能监听1024个文件描述符 (32位) [64位为 2048]
2. select被唤醒之后要重新轮询(0-1023)一遍驱动,效率低(消耗CPU资源)
3. select每次会清空未响应的文件描述符,每次都需要拷贝用户空间的表到内核空间,效率低,开销较大
(0~3G是用户态,3G~4G是内核态,两个状态来回切换 拷贝是非常耗时,耗资源的)
(4)select机制:
1. 头文件检测1024个文件描述符 0-1023
2. 在select中0~2存储标准输入、标准输出、标准出错
3. 监测的最大文件描述个数为fd+1(如果fd = 3,则最大为 4) : //因为从0开始的
4. select只对置1的文件描述符感兴趣 ,假如事件产生,select检测时 , 产生的文件描述符会保持1,未产生事件的会置0;
5. select每次轮询都会清空表(置零的清空) //需要在select前备份临时表
练习1:
如何通过select实现 响应鼠标事件同时响应键盘事件?
代码:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
int main(int argc, char const *argv[])
{
int fd = open("/dev/input/mouse0", O_RDONLY);
if (fd < 0)
{
perror("open is err:");
return -1;
}
//1.创建表
fd_set readfds;
//2/清空表
FD_ZERO(&readfds);
//3.设置表
FD_SET(0, &readfds);
FD_SET(fd, &readfds);
fd_set readfdcp = readfds;
int maxfd = fd;
char buf[128] = {0};
while (1)
{
//4.检测是否有相应
select(maxfd + 1, &readfds, NULL, NULL, NULL);
//5.检测哪一个文件描述符
if (FD_ISSET(0, &readfds))
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
printf("key: %s\n", buf);
}
if (FD_ISSET(fd, &readfds))
{
int ret = read(fd, buf, sizeof(buf));
buf[ret] = '\0';
printf("mouse: %s\n", buf);
}
readfds = readfdcp;
}
return 0;
}
练习2:
select是文件描述符和下标一一对应,0只能对应0号文件描述符。因此只有最大的文件描述符关闭时,才--len。注意增加删除时是针对实际表,不是临时表。
使用select实现server的全双工
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <sys/select.h>
int acceptfp;
int main(int argc, char const *argv[])
{
char buf[128] = {0};
//1.创建套接字,返回建立链接的文件描述符
int sockfp = socket(AF_INET, SOCK_STREAM, 0);
if (sockfp == -1)
{
perror("socket is err");
exit(0);
}
printf("%d\n", sockfp);
//2.绑定ip和端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(struct sockaddr_in);
if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind is err");
exit(0);
}
//3.listen监听
if (listen(sockfp, 5))
{
perror("liste err");
exit(0);
}
printf("listen ok\n");
//1.创建表
fd_set readfds;
//2/清空表
FD_ZERO(&readfds);
//3.设置表
FD_SET(0, &readfds);
FD_SET(sockfp, &readfds);
fd_set readfdcp = readfds;
int maxfd = sockfp;
struct timeval st;
while (1)
{
readfds = readfdcp;
//4.检测是否有响应
st.tv_sec = 5;
st.tv_usec = 0;
int ret = select(maxfd + 1, &readfds, NULL, NULL, &st);
if (ret < 0)
{
perror("select err");
return -1;
}
else if (ret == 0)
{
printf("无响应\n");
}
//0响应,证明服务器要发送消息
if (FD_ISSET(0, &readfds))
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
for (int i = 4; i <= maxfd; ++i)
{
send(i, buf, sizeof(buf), 0);
}
}
//sockfp,监听套接字响应证明,有客户端要链接
if (FD_ISSET(sockfp, &readfds))
{
acceptfp = accept(sockfp, (struct sockaddr *)&caddr, &len);
if (acceptfp < 0)
{
perror("acceptfp");
exit(0);
}
printf("port:%d ip: %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
FD_SET(acceptfp, &readfdcp);
if (acceptfp > maxfd)
maxfd = acceptfp;
}
//检测客户端,检查是哪一个客户端发送的消息
for (int i = 4; i <= maxfd; ++i)
{
if (FD_ISSET(i, &readfds))
{
int recvbyte = recv(i, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err");
return -1;
}
else if (recvbyte == 0)
{
printf("%d client is exit\n", i);
close(i);
FD_CLR(i, &readfdcp);
if (i == maxfd)
--maxfd;
}
else
{
printf("%d : %s\n", i, buf);
}
}
}
}
return 0;
}
练习3:
使用select实现client的全双工
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
int main(int argc, const char *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socker is err:");
return -1;
}
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr(argv[2]);
if(connect(sockfd,(struct sockaddr *)&saddr,sizeof(saddr)) < 0)
{
perror("connect is err:");
return -1;
}
//1.创建表
fd_set readfds,tempfds;
//2.清空表
FD_ZERO(&readfds);
FD_ZERO(&tempfds);
//3.添加文件描述符
FD_SET(0,&readfds);
FD_SET(sockfd,&readfds);
int maxfd = sockfd;
int ret;
char buf[128];
while(1)
{
tempfds = readfds;
//4.调select检测
ret = select(maxfd+1,&tempfds,NULL,NULL,NULL);
if(ret < 0)
{
perror("select is err:");
return -1;
}
if(FD_ISSET(0,&tempfds))
{
fgets(buf,sizeof(buf),stdin);
if(buf[strlen(buf)-1] == '\n')
buf[strlen(buf)-1] = '\0';
send(sockfd,buf,sizeof(buf),0);
}
if(FD_ISSET(sockfd,&tempfds))
{
int recvbyte = recv(sockfd,buf,sizeof(buf),0);
if(recvbyte < 0)
{
perror("recv is err:");
return -1;
}
printf("%s\n",buf);
}
}
close(sockfd);
return 0;
}
(5)select的超时时间检测:
超时检测的必要性:
1. 避免进程在没有数据时无限制的阻塞;
2. 规定时间未完成语句应有的功能,则会执行相关功能;
结构体如下:
struct timeval {
long tv_sec; 以秒为单位,指定等待时间
long tv_usec; 以毫秒为单位,指定等待时间
};
二.poll实现
(1)poll流程
使用: 1.先创建结构体数组 struct pollfd fds[100];
2.添加结构体成员的文件描述符以及触发方式 fds[0].fd = ?;fds[0].events = POLLIN
3.保存数组内最后一个有效元素的下标
4. 调用函数poll ret = poll(fds,nfds+1,-1);
5.判断结构体内文件描述符是否触发事件 fds[i].revents == POLLIN
6.根据不同的文件描述符触发不同事件
(2)poll函数
声明:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
头文件: #include<poll.h>
功能: 监视并等待多个文件描述符的属性变化
参数:
1.struct pollfd *fds: 关心的文件描述符数组,大小自己定义
若想检测的文件描述符较多,则建 立结构体数组struct pollfd fds[N];
struct pollfd
{
int fd; //文件描述符
short events; //等待的事件触发条件----POLLIN读时间触发(大多数)
short revents; //实际发生的事件(未产生事件: 0 ))
}
2. nfds: 最大文件描述符个数
3. timeout: 超时检测 (毫秒级):1000 == 1s
如果-1,阻塞 如果0,不阻塞
返回值: <0 出错 >0 表示有事件产生;
如果设置了超时检测时间:&tv
<0 出错 >0 表示有事件产生; ==0 表示超时时间已到;
(3)poll特点
1. 优化文件描述符个数的限制;
(根据poll函数第一个函数的参数来定,如果监听的事件为1个,则结构体数组容量为1,如果想监听100个,那么这个结构体数组的容量就为100,多少文件描述符由程序员自己来决定)
2. poll被唤醒之后需要重新轮询一遍驱动,效率比较低(消耗CPU)
3. poll不需重新构造文件描述符表(也不需清空表),只需要从用户空间向内核空间拷贝一次数据(效率相对比较高)
练习:
使用poll实现server的全双工
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <sys/select.h>
#include <poll.h>
int acceptfp;
int main(int argc, char const *argv[])
{
char buf[128] = {0};
//1.创建套接字,返回建立链接的文件描述符
int sockfp = socket(AF_INET, SOCK_STREAM, 0);
if (sockfp == -1)
{
perror("socket is err");
exit(0);
}
printf("%d\n", sockfp);
//2.绑定ip和端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(struct sockaddr_in);
if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind is err");
exit(0);
}
//3.listen监听
if (listen(sockfp, 5))
{
perror("liste err");
exit(0);
}
printf("listen ok\n");
//1.创建结构体数组
struct pollfd fds[100];
//2.添加文件描述符和触发方式
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[1].fd = sockfp;
fds[1].events = POLLIN;
int nfds = 1;
int ret;
while (1)
{
//3.poll轮循检测
ret = poll(fds, nfds + 1, 2000);
if (ret < 0)
{
perror("poll is err");
return -1;
}
else if (ret == 0)
{
printf("qeqweqe\n");
continue;
}
//4. 判断哪一个文件描述符产生响应,并发布任务
for (int i = 0; i <= nfds; ++i)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == 0)
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
//printf("发送信息:\n");
for (int j = 2; j <= nfds; ++j)
{
send(fds[j].fd, buf, sizeof(buf), 0);
}
}
else if (fds[i].fd == sockfp)
{
acceptfp = accept(sockfp, (struct sockaddr *)&caddr, &len);
if (acceptfp < 0)
{
perror("acceptfp");
exit(0);
}
printf("port:%d ip: %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
fds[++nfds].fd = acceptfp;
fds[nfds].events = POLLIN;
}
else
{
int recvbyte = recv(fds[i].fd, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err");
return -1;
}
else if (recvbyte == 0)
{
printf("%d client is exit\n", i);
close(fds[i].fd);
//覆盖
fds[i] = fds[nfds];
//--i,--nfds后,最后一个循环不到
--nfds, --i;
}
else
{
printf("%d : %s\n", i, buf);
}
}
}
}
}
return 0;
}
(4)poll超时时间检测
timeout: 超时检测 (毫秒级):1000 == 1s
如果-1,阻塞 如果0,不阻塞
三.epoll实现
(1)epoll流程:
Epoll的使用:
1.创建红黑树 和 就绪链表 int epfd = epoll_create(1);
2.添加文件描述符和事件信息到树上
event.events = EPOLLIN|EPOLLET;
event.data.fd = 0;
epoll_ctl(epfd,EPOLL_CTL_ADD,0,&event
3.阻塞等待事件的产生,一旦产生事件,则进行处理
int ret = epoll_wait(epfd,events,32,-1);
4.根据链中准备处理的文件描述符 进行处理
(2)epoll函数族
epoll 要使用一组函数: epoll_create 创建红黑树 和 就序链表
epoll_ctl 添加文件描述符和事件到树上 / 从树上删除
epoll_wait 等待事件产生
epoll_create
创建红黑树以及链表
头文件:#include <sys/epoll.h>
声明:int epoll_create(int size);
功能:创建红黑树根节点(创建epoll实例) , 同时也会创建就绪链表
返回值:成功时返回一个实例(二叉树句柄),失败时返回-1。
epoll_ctl
控制epoll属性
声明: int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:控制epoll属性,比如给红黑树添加节点
参数: 1. epfd: epoll_create函数的返回句柄。//一个标识符
2. op:表示动作类型,有三个宏:
EPOLL_CTL_ADD:注册新的fd到epfd中
EPOLL_CTL_MOD:修改已注册fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd
3. 要操作的文件描述符
4. 结构体信息:
typedef union epoll_data {
int fd; //要添加的文件描述符,只用这个
uint32_t u32; typedef unsigned int
uint64_t u64; typedef unsigned long int
} epoll_data_t;
struct epoll_event {
uint32_t events; 事件
epoll_data_t data; //共用体(看上面)
};
关于events事件:
EPOLLIN: 表示对应文件描述符可读
EPOLLOUT: 可写
EPOLLPRI:有紧急数据可读;
EPOLLERR:错误;
EPOLLHUP:被挂断;
EPOLLET:触发方式,边缘触发;(默认使用边缘触发)
ET模式:表示状态的变化;
NULL: 删除一个文件描述符使用,无事件
返回值:成功:0, 失败:-1
epoll_wait
等待事件产生
声明: int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:等待事件产生
内核会查找红黑树中有事件响应的文件描述符, 并将这些文件描述符放入就绪链表
就绪链表中的内容, 执行epoll_wait会同时复制到第二个参数events
参数: epfd:句柄;
events:用来保存从就绪链表中响应事件的集合;(传出参数,定义结构体数组)
maxevents: 表示每次在链表中拿取响应事件的个数;
timeout:超时时间,毫秒,0立即返回 ,-1阻塞
返回值: 成功: 实际从链表中拿出的数目 失败时返回-1
(4)epoll特点
1.监听的最大的文件描述符没有个数限制(取决与你自己的系统 1GB - 10万个左右)
2.异步I/O,epoll当有事件产生被唤醒之后,文件描述符主动调用callback(回调函数)函数直接拿到唤醒的文件描述符,不需要轮询,效率高
3.epoll不需要重新构造文件描述符表,只需要从用户空间向内核空间拷贝一次数据即可.
(5)epoll机制
select,poll都属于 同步IO机制(轮询)
epoll属于异步IO机制(不轮询):
Epoll处理高并发,百万级
- 红黑树: 是特殊的二叉树(每个节点带有属性),Epoll怎样能监听很多个呢?首先创建树的根节点,每个节点都是一个fd以结构体的形式存储(节点里面包含了一些属性,callback函数)
- 就绪链表: 当某一个文件描述符产生事件后,会自动调用callback函数,通过回调callback函数来找到链表对应的事件(读时间还是写事件)。
练习:
epoll实现server
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <signal.h>
#include <sys/select.h>
#include <poll.h>
#include <sys/epoll.h>
int acceptfp;
int main(int argc, char const *argv[])
{
char buf[128] = {0};
//1.创建套接字,返回建立链接的文件描述符
int sockfp = socket(AF_INET, SOCK_STREAM, 0);
if (sockfp == -1)
{
perror("socket is err");
exit(0);
}
printf("%d\n", sockfp);
//2.绑定ip和端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(struct sockaddr_in);
if (bind(sockfp, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind is err");
exit(0);
}
//3.listen监听
if (listen(sockfp, 5))
{
perror("liste err");
exit(0);
}
printf("listen ok\n");
//1.创建红黑树以及链表
//树的跟节点/树的句柄
int epfd = epoll_create(1);
//2.上树
struct epoll_event event;
struct epoll_event events[32] ;
event.events = EPOLLET | EPOLLIN;
event.data.fd = 0;
epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
event.data.fd = sockfp;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfp, &event);
while (1)
{
//3.阻塞等待文件描述符产生事件
int ret = epoll_wait(epfd, events, 32, -1);
printf("asdsdfgdsf\n");
if (ret < 0)
{
perror("epoll err");
return -1;
}
//4.根据文件描述符号,进行处理
for (int i = 0; i < ret; ++i)
{
if (events[i].data.fd == 0)
{
fgets(buf, sizeof(buf), stdin);
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = '\0';
printf("发送信息:\n");
//send(fds[j].fd, buf, sizeof(buf), 0);
}
else if (events[i].data.fd == sockfp)
{
acceptfp = accept(sockfp, (struct sockaddr *)&caddr, &len);
if (acceptfp < 0)
{
perror("acceptfp");
exit(0);
}
printf("port:%d ip: %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
//上树
event.data.fd = acceptfp;
epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfp, &event);
}
else
{
int recvbyte = recv(events[i].data.fd, buf, sizeof(buf), 0);
if (recvbyte < 0)
{
perror("recv err");
return -1;
}
else if (recvbyte == 0)
{
printf("%d client is exit\n", events[i].data.fd);
close(events[i].data.fd);
//下树
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
}
else
{
printf("%d : %s\n", events[i].data.fd, buf);
}
}
}
}
return 0;
}