目录
一、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,非阻塞状态下,返回 -1
if (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); // 后两个参数后面用不上,可以填0
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,非阻塞状态下,返回 -1
if (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;
}