目录
IO多路复用 select、poll、epoll
案例分析:键盘鼠标事件
同时对键盘和鼠标进行监听,当敲击键盘按下回车,就打印键盘输入的东西,动鼠标就要打印鼠标写入的内容。
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#define N 64
int main(int argc, char const *argv[])
{
int mouse = open("/dev/input/mouse0", O_RDONLY);
if (mouse < 0)
{
perror("open失败");
return -1;
}
char buf[N];
while (1)
{
memset(buf, 0, N);
gets(buf);
printf("buf:%s\n", buf);
int ret = read(mouse, buf, N);
if (ret < 0)
{
perror("read失败");
return -1;
}
else
{
printf("mouse:%s\n", buf);
}
}
}
IO多路复用机制
使用I/O多路复用技术。其基本思想是:
1.先构造一张有关描述符的表,然后调用一个函数。
2.当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。
3.函数返回时告诉进程哪个描述符已就绪,可以进行I/O操作。
一.select
1.函数
#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(超时时长));
一般写:select(int nfds,fd_set *readfds,NULL,NULL,NULL);
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); //将表置零
2.流程
第一步:建表初始化
第二步:填表
第三步:监听表
第四步:判断,操作
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
#define N 64
int main(int argc, char const *argv[])
{
int mouse = open("/dev/input/mouse0", O_RDONLY);
if (mouse < 0)
{
perror("open失败");
return -1;
}
char buf[N];
// 第一步:建表初始化
fd_set readfds, tempfds;
FD_ZERO(&readfds);
// 第二步:填表
FD_SET(0, &readfds);
FD_SET(mouse, &readfds);
// 第三步:监听表
while (1)
{
memset(buf, 0, N);
temphfds = readfds;
int ret = select(4, &tempfds, NULL, NULL, NULL);
// 第四步:判断,操作
if (ret == -1)
{
perror("select失败");
return -1;
}
if (FD_ISSET(0, &tempfds))
{
gets(buf);
printf("buf:%s\n", buf);
}
if (FD_ISSET(mouse, &tempfds))
{
int n = read(mouse, buf, N);
if (n < 0)
{
perror("read失败");
return -1;
}
else
{
printf("mouse:%s\n", buf);
}
}
}
return 0;
}
3.案例使用select创建全双工客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <sys/select.h>
#include <errno.h>
int main(int argc, char const *argv[])
{
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.连接
unsigned short post = 0;
char ip[15];
printf("请输入ip地址");
scanf("%s", ip);
getchar();
printf("请输入端口号");
scanf("%hd", &post);
getchar();
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(post);
saddr.sin_addr.s_addr = inet_addr(ip);
socklen_t addrlen = sizeof(saddr);
if (connect(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
perror("connect失败\n");
return -1;
}
// 3.接收
#define N 64
char buf[N];
while (1)
{
// 第一步:建表初始化
fd_set readfds, tempfds;
FD_ZERO(&readfds);
// 第二步:填表
FD_SET(0, &readfds);
FD_SET(sockfd, &readfds);
// 第三步:监听表
while (1)
{
memset(buf, 0, N);
tempfds = readfds;
int ret = select(4, &tempfds, NULL, NULL, NULL);
// 第四步:判断,操作
if (ret == -1)
{
perror("select失败");
return -1;
}
if (FD_ISSET(0, &tempfds))
{
scanf("%s", buf);
send(sockfd, buf, N, 0);
}
if (FD_ISSET(sockfd, &tempfds))
{
int ret = recv(sockfd, buf, N, 0);
printf("服务端:%s\n", buf);
}
}
}
close(sockfd);
return 0;
}
4.并发服务器
可以同时接收多个客户端的连接
5.案例使用select创建全双工服务端
sockfd只要创建并监听listen,就可以接收连接请求,即只要sockfd的读缓冲区可读,就可以创建连接。
客户端的连接请求会发给sockfd,如果可以建立连接的话,会在sockfd的缓冲区内保存
accept其实就是去sockfd的缓冲区里取连接
select创建并发服务器过程
创建并发服务器
创建连接 accept --> sockfd
发送消息 gets --> 0
接收 recv --> 所有已连接的acceptfd
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/select.h>
#define N 64
char buf[N];
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "line:%d ", __LINE__); \
perror(msg); \
} while (0)
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:<port>\n");
return -1;
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.bind绑定IP和Port端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
// saddr.sin_addr.s_addr = inet_addr("192.168.50.213");
socklen_t addrlen = sizeof(saddr);
#if 0
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#else
saddr.sin_addr.s_addr = INADDR_ANY;
#endif
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
ERR_MSG("bind失败");
return -1;
}
printf("bind成功\n");
// 3.监听listen将主动套接字变为被动套接字
if (listen(sockfd, 7) < 0)
{
ERR_MSG("lisren失败");
return -1;
}
printf("listen成功\n");
// 第一步:建表初始化
fd_set readfds, tempfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
FD_SET(0, &readfds);
int max = sockfd;
while (1)
{
memset(buf, 0, N);
tempfds = readfds;
int ret = select(max + 1, &tempfds, NULL, NULL, NULL);
if (ret == -1)
{
perror("select失败");
return -1;
}
if (FD_ISSET(sockfd, &tempfds))
{
// 4.accept阻塞等待链接
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (acceptfd < 0)
{
ERR_MSG("accept失败\n");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
FD_SET(acceptfd, &readfds);
if (max < acceptfd)
{
max = acceptfd;
}
}
// 5.发送
else if (FD_ISSET(0, &tempfds))
{
scanf("%s", buf);
for (int i = 4; i <= max; i++)
{
if (FD_ISSET(i, &readfds))
{
send(i, buf, N, 0);
}
}
}
// 6.接收
for (int n = 4; n <= max; n++)
{
if (FD_ISSET(n, &tempfds))
{
int ret = recv(n, buf, N, 0);
if (ret < 0)
{
perror("recv失败");
return -1;
}
else if (ret > 0)
{
printf("客户端:%s\n", buf);
}
else
{
printf("客户端acceptfd:%d退出\n", n);
FD_CLR(n, &readfds);
close(n);
while (!FD_ISSET(max, &readfds))
{
max--;
}
}
}
}
}
close(sockfd);
return 0;
}
二.poll
1.函数
#include <poll.h>
int poll(suct trpollfd *fds, nfds_t nfds, int timeout);
参数:
fds:创建的pollfd结构体类型的数组
nfds:数组的大小
timeout:超时检测的时间,一般不用的话设置为-1
返回值:
成功:0
失败:-1
struct pollfd {
int fd; //第一个成员变量 fd是向poll说明要监听哪个文件描述符
short events; //第二个成员变量 events 是向poll说明要对这个文件描述符的哪种事件进行监听,一般设置为POLLIN
short revents; //第三个成员变量 revents 是poll函数自动生成,当fd发生了events事件时,poll函数会将events(POLLIN)写入revents
};
2.流程
3.案例使用poll创建全双工客户端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
int main(int argc, char const *argv[])
{
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.连接
unsigned short post = 0;
char ip[15];
printf("请输入ip地址");
scanf("%s", ip);
getchar();
printf("请输入端口号");
scanf("%hd", &post);
getchar();
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(post);
saddr.sin_addr.s_addr = inet_addr(ip);
socklen_t addrlen = sizeof(saddr);
if (connect(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
perror("connect失败\n");
return -1;
}
// 3.接收
#define N 64
char buf[N];
while (1)
{
// 第一步:建表初始化
struct pollfd fds[2];
// 第二步:填表
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[1].fd = sockfd;
fds[1].events = POLLIN;
// 第三步:监听表
while (1)
{
memset(buf, 0, N);
poll(fds, 2, -1);
// 第四步:判断,操作
// if (ret == -1)
// {
// perror("select失败");
// return -1;
// }
for (int i = 0; i <= 1; i++)
{
if (fds[i].revents == POLLIN)
{
if (i == 0)
{
scanf("%s", buf);
send(sockfd, buf, N, 0);
}
if (i == 1)
{
int ret = recv(sockfd, buf, N, 0);
if (ret < 0)
{
perror("recv失败");
return -1;
}
else
{
printf("服务端:%s\n", buf);
}
}
}
}
}
}
close(sockfd);
return 0;
}
4.案例使用poll创建全双工服务端
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <poll.h>
#define N 64
char buf[N];
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "line:%d ", __LINE__); \
perror(msg); \
} while (0)
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:<port>\n");
return -1;
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.bind绑定IP和Port端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("192.168.50.213");
socklen_t addrlen = sizeof(saddr);
// #if 0
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
// #else
// saddr.sin_addr.s_addr = INADDR_ANY;
// #endif
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
ERR_MSG("bind失败");
close(sockfd);
return -1;
}
printf("bind成功\n");
// 3.监听listen将主动套接字变为被动套接字
if (listen(sockfd, 7) < 0)
{
ERR_MSG("lisren失败");
close(sockfd);
return -1;
}
printf("listen成功\n");
// 第一步:建表初始化
struct pollfd fds[100];
int last = -1;
// 第二步:填表
fds[++last].fd = 0;
fds[last].events = POLLIN;
fds[last].revents = 0;
fds[++last].fd = sockfd;
fds[last].events = POLLIN;
fds[last].revents = 0;
while (1)
{
memset(buf, 0, N);
int po = poll(fds, last + 1, -1);
if (po == -1)
{
perror("select失败");
close(sockfd);
return -1;
}
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == sockfd)
{
// 4.accept阻塞等待链接
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (acceptfd < 0)
{
ERR_MSG("accept失败\n");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
fds[++last].fd = acceptfd;
fds[last].events = POLLIN;
fds[last].revents = 0;
}
else if (fds[i].fd == 0)
{
scanf("%s", buf);
for (int j = 2; j <= last; j++)
{
send(fds[j].fd, buf, N, 0);
}
}
else
{
int ret = recv(fds[i].fd, buf, N, 0);
if (ret < 0)
{
perror("recv失败");
close(sockfd);
return -1;
}
else if (ret > 0)
{
printf("客户端%s:%s\n", inet_ntoa(caddr.sin_addr), buf);
}
else
{
printf("客户端acceptfd:%d退出\n", fds[i].fd);
close(fds[i].fd);
fds[i] = fds[last];
last--;
}
}
}
}
}
close(sockfd);
return 0;
}
三、epoll
1.流程
2.案例使用epoll创建全双工服务端
#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 <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/epoll.h>
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("usage: <port>\n");
return -1;
}
// 1.创建套接字-->tcp 流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sockfd:%d\n", sockfd);
// 2.绑定IP和端口号
// 填充通信结构体
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET; // 选定ipv4协议族
saddr.sin_port = htons(atoi(argv[1])); // 绑定端口
// saddr.sin_addr.s_addr = inet_addr(argv[1]); // 绑定ip地址
#if 1
saddr.sin_addr.s_addr = INADDR_ANY; // 绑定ip地址
#else
saddr.sin_addr.s_addr = inet_addr("0.0.0.0"); // 绑定ip地址
#endif
socklen_t len = sizeof(saddr);
// 绑定
if (bind(sockfd, (struct sockaddr *)&saddr, len) < 0)
{
perror("bind err");
return -1;
}
printf("bind ok\n");
// 3.启动监听 将主动套接字变成被动套接字
if (listen(sockfd, 8) < 0)
{
perror("listen err");
return -1;
}
printf("listen ok\n");
// 1.创建红黑树,拿到根节点---》建表
int epfd = epoll_create(99);
// 2.将关心的文件描述符挂载到树上---》填表
struct epoll_event event;
struct epoll_event events[10];
event.data.fd = sockfd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
// 3.去链表中拿事件
char buf[128];
while (1)
{
int ret = epoll_wait(epfd, events, 10, -1);
if (ret < 0)
{
perror("errr");
return -1;
}
else if (ret == 0)
{
printf("nothing \n");
}
else
{
for (int i = 0; i < ret; i++)
{
if (events[i].data.fd == sockfd)
{
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);
if (acceptfd < 0)
{
perror("accept err");
return -1;
}
printf("%d,login\n", acceptfd);
event.data.fd = acceptfd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &event);
}
else
{
int ret = recv(events[i].data.fd, buf, sizeof(buf), 0);
if (ret < 0)
{
perror("recv err");
return -1;
}
else if (ret > 0)
{
printf("%d:%s\n", events[i].data.fd, buf);
}
else
{
printf("%d exit\n", events[i].data.fd);
close(events[i].data.fd);
event.data.fd = events[i].data.fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &event);
}
}
}
}
}
return 0;
}
select,poll和epoll的特点:
1.select特点
优点:可跨平台,Linux、macos、windows都可用
可以监听多个文件描述符
轻量级缺点:最大监听1024个文件描述符,最大监听1020个客户端连接
每次都要进行轮询,消耗CPU
每次都要拷贝一遍表
2.poll特点
优点:
1、优化了文件描述符的数量,监听的文件描述符数取决于数组的大小,数组大小受内存容量的限制。
2、不需要每次都拷贝一遍表
缺点:
1、需要轮询
2、只能用在UNIX原生系统下,不支持跨平台
3.epoll特点
优点:
1、超高并发,百万级并发
2、不需要轮询,因为有异步通知机制
3、不需要拷贝表
缺点:
只能跑在Linux