学习记录——day34 IO多路复用 fcntl select poll select实现聊天室
目录
一、IO多路复用引入
二、非阻塞型IO
1、 fcntl
三、多路文件IO
1、原理:内核监视对象套接字的缓冲区变化
2、select模型
select注意事项
描述符操作函数
3、 select实现并发服务器
1)服务器端
2)客服端
4、 select实现聊天室(无姓名)
1)服务器端
2)客服端
5、poll模型
poll函数注意事项
poll实现并发服务器
一、IO多路复用引入
对于用多进程/多线程实现的并发服务器而言:
1)可连接的客服端数量搜最大进程/线程数限制
2)进程阻塞占用时间片会影响效率
而对于IO多路复用实现的并发服务器:
1)连接数量没有限制(select 受文件描述符最大数量限制-》1024)
2)由一个进程实现,不会出现时间片资源占用
3)事件触发才执行,执行效率和资源利用率较高
二、非阻塞型IO
1、 fcntl
原型:int fcntl(int fd, int cmd, ... /* arg */ );
调用:int flag = fcntl(描述符,F_GETFL)
fcntl(描述符,F_SETFL,flag)
功能描述:设置或者获取文件的各项属性,到底如何操作由cmd决定,一般我们都会用来设置阻塞或者非阻塞IO
参数解析:
参数 fd:准备设置属性的文件的描述符
参数 cmd:文件到底设置什么属性又cmd决定
参数 ...:
F_SETFL:设置文件的flag属性
F_GETFL:获取当前文件的flag属性
三、多路文件IO
先发送输入事件,再调取阻塞型读取函数
1、原理:内核监视对象套接字的缓冲区变化
1)边缘触发:缓冲区改变
2)水平触发:缓冲区存在数据
内核会通知监视者,有描述符可读
2、select模型
原型:int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);功能描述:以阻塞的形式监视 readfds,writefds,exceptfds 这3个描述符集合中,所有描述符,如果有任何描述符激活,则select解除阻塞
参数解析:
参数 nfds:readfds,writefds,exceptfds 这3个集合中的最大值
参数 readfds:监视描述符集合中任意的描述符是否可读,一般我们只用这个
参数 writefds:监视描述符集合中任意的描述符是否可写,一般写NULL
参数 exceptfds:监视描述符集合中任意的描述符是否发生意外,一般写NULL
注意:只要select监视到了有描述符激活,就会将激活的描述符,以覆盖的形式写入到上述3个fds里面去
参数 timeout:是一个结构体,结构如下
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
表示select函数只阻塞传入的时间长度的秒数,超过这个时间自动解除阻塞
传NULL表示:一直阻塞,不受时间影响
返回值:返回激活的描述符的数量
select注意事项
select 为水平触发
select fd_set 最大为 1024(文件描述符最大数量)
select监视到激活的描述符成功后,会覆盖原队列
描述符操作函数
void FD_CLR(int fd, fd_set *set);
功能描述: 从 set 中删除描述符 fd
int FD_ISSET(int fd, fd_set *set);
功能描述:判断 set 中是否存在描述符 fd
返回值:如果存在返回1,不存在返回0
void FD_SET(int fd, fd_set *set);
功能描述:将描述符 fd 添加到 set 里面去
void FD_ZERO(fd_set *set);
功能描述:清空 set 所有描述符,相当于初始化的功能
3、 select实现并发服务器
1)服务器端
#include <myhead.h>
#define SER_PORT 6666
#define SER_IP "192.168.2.106"
int main(int argc, char const *argv[])
{// 1、创建套接字int sfd = socket(AF_INET, SOCK_STREAM, 0);if (sfd == -1){perror("socket error");return -1;}printf("socket success, sfd = %d\n", sfd); // 3// 2、为套接字绑定ip地址和端口号// 2.1 填充地址信息结构体struct sockaddr_in sin;sin.sin_family = AF_INET; // 通信域sin.sin_port = htons(SER_PORT); // 端口号sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址// 3、绑定if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1){perror("bind error");return -1;}// 4、设置socket功能 -> 监听if (listen(sfd, 128) == -1){perror("listen error");return -1;}printf("listen on\n");// 5、创建接收客服端的结构体(后面没用,可无)//如果要实现有姓名的聊天室必须定义信息结构体数组或使用链表存储对端地址信息struct sockaddr_in cin;socklen_t addrlen;// 6、select监听套接字变化fd_set readfds;FD_ZERO(&readfds);FD_SET(sfd, &readfds);int newsdf_arr[100] = {0}; // 存放客服端套接字int nwesdf_count = 0; // 记录客服端套接字数组下标while (1){//6.1、启动select监听套接字变化//定义中间变量避免 readfds 被覆盖fd_set temp = readfds;select(FD_SETSIZE, &temp, 0, 0, 0);printf("select on\n");if (FD_ISSET(sfd, &temp)){int newsdf = accept(sfd, (struct sockaddr *)&cin, &addrlen);//后两个参数后面用不上,可以填0//无客服端连接时,accept 会清空缓冲区if (newsdf == -1){perror("accept error");return -1;}printf("客服端连接成功\n");FD_SET(newsdf, &readfds); // 将新连接的套接字加入监听列表newsdf_arr[nwesdf_count] = newsdf;nwesdf_count++;}//6.2、循环监听客服端套接字变化for (int i = 0; i < nwesdf_count; i++){int newsfd_temp = newsdf_arr[i];//定义变量接收操作对象信息if (FD_ISSET(newsfd_temp, &temp)){char buf[128] = {0};int res = read(newsfd_temp, buf, 128);//read 函数在阻塞状态下 客服端断开连接 返回 0,非阻塞状态下,返回 -1if (res == 0){printf("客服端断开连接\n");// 客服端断开连接// 将断开的客服端从监视列表 readfds 删除FD_CLR(newsfd_temp, &readfds);// 将断开的客服端从客服端列表 newsfd_arr 删除for (int j = i; j < nwesdf_count - 1; j++){newsdf_arr[j] = newsdf_arr[j + 1];}nwesdf_count--;// 关闭套接字close(newsfd_temp);}else{printf("接收到:%s\n", buf);}}}}return 0;
}
2)客服端
#include <myhead.h>
#define SER_PORT 6666
#define SER_IP "192.168.2.106"
int main(int argc, char const *argv[])
{int cfd = socket(AF_INET, SOCK_STREAM, 0);if (cfd == -1){perror("socket error");return -1;}printf("socket success, cfd = %d\n", cfd);struct sockaddr_in sin;sin.sin_family = AF_INET; // 通信域sin.sin_port = htons(SER_PORT); // 端口号sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址connect(cfd,(struct sockaddr*)&sin,sizeof(sin));while (1){char buf[128] = {0};fgets(buf,sizeof(buf),stdin);buf[strlen(buf)-1] = 0;if (strcmp(buf,"quit") == 0){break;}sendto(cfd,buf,sizeof(buf),0,(struct sockaddr*)&sin,sizeof(sin)); }close(cfd);return 0;
}
4、 select实现聊天室(无姓名)
1)服务器端
#include <myhead.h>
#define SER_PORT 6666
#define SER_IP "192.168.2.106"
int main(int argc, char const *argv[])
{// 1、创建套接字int sfd = socket(AF_INET, SOCK_STREAM, 0);if (sfd == -1){perror("socket error");return -1;}printf("socket success, sfd = %d\n", sfd); // 3// 2、为套接字绑定ip地址和端口号// 2.1 填充地址信息结构体struct sockaddr_in sin;sin.sin_family = AF_INET; // 通信域sin.sin_port = htons(SER_PORT); // 端口号sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址// 3、绑定if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1){perror("bind error");return -1;}// 4、设置socket功能 -> 监听if (listen(sfd, 128) == -1){perror("listen error");return -1;}printf("listen on\n");// 5、创建接收客服端的结构体(用不上,可以没有)struct sockaddr_in cin;socklen_t addrlen;// 6、select监听套接字变化fd_set readfds;FD_ZERO(&readfds);FD_SET(sfd, &readfds); // 监听连接用的套接字FD_SET(0, &readfds); // 监听标准输入流int newsdf_arr[100] = {0}; // 存放客服端套接字int nwesdf_count = 0; // 记录客服端套接字数组下标while (1){// 6.1、启动select监听套接字变化// 定义中间变量避免 readfds 被覆盖fd_set temp = readfds;select(FD_SETSIZE, &temp, 0, 0, 0);//printf("select on\n");// 调试用// 监听客服端连接if (FD_ISSET(sfd, &temp)){int newsdf = accept(sfd, (struct sockaddr *)&cin, &addrlen); // 后两个参数后面用不上,可以填0if (newsdf == -1){perror("accept error");return -1;}printf("客服端连接成功\n");FD_SET(newsdf, &readfds); // 将新连接的套接字加入监听列表newsdf_arr[nwesdf_count] = newsdf;nwesdf_count++;}// 6.2、循环监听客服端套接字变化for (int i = 0; i < nwesdf_count; i++){int newsfd_temp = newsdf_arr[i]; // 定义变量接收操作对象信息if (FD_ISSET(newsfd_temp, &temp)){char buf[128] = {0};int res = read(newsfd_temp, buf, 128);// read 函数在阻塞状态下 客服端断开连接 返回 0,非阻塞状态下,返回 -1if (res == 0){printf("客服端断开连接\n");// 客服端断开连接// 将断开的客服端从监视列表 readfds 删除FD_CLR(newsfd_temp, &readfds);// 将断开的客服端从客服端列表 newsfd_arr 删除for (int j = i; j < nwesdf_count - 1; j++){newsdf_arr[j] = newsdf_arr[j + 1];}nwesdf_count--;i--;// 关闭套接字close(newsfd_temp);}else{printf("接收到:%s\n", buf);// 将从客服端接收到信息发个其他客服端for (int k = 0; k < nwesdf_count; k++){if (newsfd_temp != newsdf_arr[k])//避免将信息发回发送者{send(newsdf_arr[k], buf, sizeof(buf), 0);}}}}}// 监听服务器标准输入流if (FD_ISSET(0, &temp)){// 服务器输入char ser_buf[128];fgets(ser_buf, 128, stdin);ser_buf[strlen(ser_buf) - 1] = 0;for (int i = 0; i < nwesdf_count; i++){send(newsdf_arr[i], ser_buf, sizeof(ser_buf), 0);}}}return 0;
}
2)客服端
#include <myhead.h>
#define SER_PORT 6666
#define SER_IP "192.168.2.106"
int main(int argc, char const *argv[])
{int cfd = socket(AF_INET, SOCK_STREAM, 0);if (cfd == -1){perror("socket error");return -1;}printf("socket success, cfd = %d\n", cfd);struct sockaddr_in sin;sin.sin_family = AF_INET; // 通信域sin.sin_port = htons(SER_PORT); // 端口号sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址socklen_t addrlen = sizeof(sin);connect(cfd, (struct sockaddr *)&sin, sizeof(sin));// 创建select监听套接字,stdin// 创建监听对象信息结构体fd_set readfds;FD_ZERO(&readfds);FD_SET(cfd, &readfds);FD_SET(0, &readfds);while (1){fd_set temp = readfds;select(FD_SETSIZE, &temp, 0, 0, 0); // 最后一个0表示阻塞printf("select on\n");if (FD_ISSET(cfd, &temp)){char cli_buf[128];recvfrom(cfd, cli_buf, sizeof(cli_buf), 0, (struct sockaddr *)&sin, &addrlen);printf("接收到:%s\n", cli_buf);}if (FD_ISSET(0, &temp)){char buf[128] = {0};fgets(buf, sizeof(buf), stdin);buf[strlen(buf) - 1] = 0;if (strcmp(buf, "quit") == 0){break;}sendto(cfd, buf, strlen(buf), 0, (struct sockaddr *)&sin, sizeof(sin));}}close(cfd);return 0;
}
5、poll模型
poll解决了上述两个问题
原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
调用:
功能描述:监视 fds所指向的描述符数组中所有描述符的情况,最多监视nfds个,一般就是数组的容量
参数解析:
参数 fds:结构体数组,数组中的每一个结构体元素都是一个描述符搭配一些其他数据,结构如下
struct pollfd {
int fd; 监视对象
short events; 监视对象激活条件:可读、可写、意外
因为可读的原因激活:POLLIN,常用因为可写的原因激活:POLLOUT
short revents; 监视对象激活后 events 会覆盖到 revents
};
参数 nfds:想要监视的描述符的数量,一般就是fds这个数组的实际长度
参数 timeout:poll函数阻塞时长,单位为毫秒
0 表示不阻塞
-1 表示阻塞,直到有描述符激活
返回值:成功返回激活的描述符的数量注意:
poll函数注意事项
poll函数监视的直接是一个结构体数组,这个数组我们是可以直接操作的,不需要额外的函数去操作
poll函数的激活方式为水平激活
poll实现并发服务器
#include <myhead.h>
#define SER_PORT 6666
#define SER_IP "192.168.2.106"int main(int argc, char const *argv[])
{// 1、创建套接字int sfd = socket(AF_INET, SOCK_STREAM, 0);if (sfd == -1){perror("socket error");return -1;}printf("socket success, sfd = %d\n", sfd); // 3// 2、为套接字绑定ip地址和端口号// 2.1 填充地址信息结构体struct sockaddr_in sin;sin.sin_family = AF_INET; // 通信域sin.sin_port = htons(SER_PORT); // 端口号sin.sin_addr.s_addr = inet_addr(SER_IP); // ip地址// 3、绑定if (bind(sfd, (struct sockaddr *)&sin, sizeof(sin)) == -1){perror("bind error");return -1;}// 4、设置socket功能 -> 监听if (listen(sfd, 128) == -1){perror("listen error");return -1;}printf("listen on\n");// 5、创建接收客服端的结构体(后面用不到可以没有)struct sockaddr_in cin;socklen_t addrlen;// 6、poll监听// 6.1、将服务器加入监视列表struct pollfd fds[50] = {0};fds[0].fd = sfd; // 监视对象 用于连接的套接字fds[0].events = POLL_IN; // 对象激活条件 可读-》客服端申请连接int fd_count = 1; // 记录监视列表下标while (1){// 设置监听对象,实际监听数量,阻塞时间poll(fds, fd_count, -1); //-1 表示阻塞for (int i = 0; i < fd_count; i++){// 提出数据,以减少后续需要输入的内容int fd = fds[i].fd;short revents = fds[i].revents;// 判断 监视对象:服务器 是否激活if (fd == sfd && revents == fds[i].events){// 接收客服端连接int cfd = accept(sfd, 0, 0);if (cfd == -1){perror("accept error");return -1;}printf("accpet success\n");// 将客服端加入监视列表fds[fd_count].fd = cfd;fds[fd_count].events = POLL_IN;fd_count++;}// 判断其他监视对象是否激活if (fd != sfd && revents == fds[i].events){char buf[128] = {0};int res = read(fd, buf, 128);if (res == 0){printf("客服端断开连接\n");// 将断开连接的客服端从监视列表删除for (int j = i; j < fd_count; j++){fds[j] = fds[j + 1];}fd_count--;i--; // 防止跳过监视列表成员close(fd);break;}else{printf("接收到:%s\n", buf);}}}}return 0;
}