1 非阻塞型IO
让我们的read函数不再阻塞,无论是否读取到消息,立刻返回
1.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属性
#include <myhead.h>
int main(int argc, char const *argv[])
{
// ①获取scanf使用的标准输入流的原本的属性,以返回值的形式返回
int flag = fcntl(STDIN_FILENO, F_GETFL);
// ②为flag叠加O_NONBLOCK属性
flag |= O_NONBLOCK;
// ③将追加完非阻塞属性的flag设置回标准输入流里面
fcntl(STDIN_FILENO, F_SETFL, flag);
char buf[32] = {0};
while (1)
{
bzero(buf, 32);
scanf("%s", buf);
printf("buf = %s\n", buf);
sleep(1);
}
}
2 多路文件IO
能够实现效果:先发生输入事件,再去调用阻塞型的读取函数
例如:
正常情况下,都是先调用scanf函数,阻塞并等待键盘输入事件
如果使用了多路文件IO的话,就能实现:先发生键盘输入事件,事件发生之后,立刻调用scanf函数,直线了高效率且非阻塞
2.1 多路文件IO的工作原理
内核会监视目标套接字的缓存区变化:如果
① 缓存区发生了改变
② 缓存区存在数据
内核就会通知监视者,有描述符是可读的
已下3个模型,都是上述工作方式
2.2 select 模型
原型:int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
调用:select(描述符集合最大值,描述符集合,0,0,0)
功能描述:以阻塞的形式监视 readfds,writefds,exceptfds 这3个描述符集合中,所有描述符,如果有任何描述符激活,则select解除阻塞
参数解析:
参数 nfds:readfds,writefds,exceptfds 这3个集合中的最大值
参数 readfds:监视描述符集合中任意的描述符是否可读,一般我们只用这个
参数 writefds:监视描述符集合中任意的描述符是否可写,一般写NULL
参数 exceptfds:监视描述符集合中任意的描述符是否发生意外,一般写NULL
参数 timeout:是一个结构体,结构如下
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
表示select函数只阻塞传入的时间长度的秒数,超过这个时间自动解除阻塞
传NULL表示:一直阻塞,不受时间影响
返回值:返回激活的描述符的数量
如何操作描述符集合
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 所有描述符,相当于初始化的功能
#include <myhead.h>
int main(int argc, const char *argv[])
{
//监视标准输入流和一个有名管道的读端,谁激活了就调用对应的读取函数读取数据
if (access("./myfifo", F_OK) == -1)
{
mkfifo("./myfifo", 0666);
}
int rp = open("./myfifo", O_RDONLY);
fd_set readfds;
FD_ZERO(&readfds); // 初始化描述符集合
FD_SET(rp, &readfds); // 将管道读端描述符添加进入描述符集合
FD_SET(0, &readfds); // 将标准输入流添加进入描述符集合
//readfds[n] ={rp,0}
while (1)
{
fd_set temp = readfds; // 复制描述符集合
select(FD_SETSIZE, &temp, NULL, NULL, NULL); // 监视readfds
if (FD_ISSET(rp, &temp) == 1)
{ // 如果是管道激活
printf("管道读端激活\n");
char buf[32] = {0};
read(rp, buf, sizeof(buf));
printf("管道读取到的数据为:%s\n", buf);
}
if (FD_ISSET(0, &temp) == 1)
{ // 如果是标准输入流激活
printf("标准输入流激活\n");
char buf[32] = {0};
scanf("%s", buf);
while (getchar() != 10);
printf("终端读取到的数据为:%s\n", buf);
}
}
close(rp);
return 0;
}
select多并发服务器
#include <myhead.h>
void insert_client(int *client_arr, int *len, int client) {
client_arr[*len] = client;
(*len)++;
}
int find_client(int *client_arr, int len, int client) {
for (int i = 0; i < len; i++) {
if (client_arr[i] == client) {
return i;
}
}
return -1;
}
void remove_client(int *client_arr, int *len, int client) {
int tar = find_client(client_arr, *len, client);
if (tar == -1) {
return;
}
int i = -1;
for (i = tar; i < *len - 1; i++) {
client_arr[i] = client_arr[i + 1];
}
(*len)--;
}
int main(int argc, const char *argv[]) {
if (argc != 2) {
printf("请输入正确的端口号\n");
return 1;
}
int port = atoi(argv[1]);
fd_set readfds;
FD_ZERO(&readfds);
int client_arr[100] = {0}; //存放客户端套接字的数组
int client_count = 0; //用来表示客户端套接字数组中有多少个客户端
int server = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr("192.168.2.100");
if (bind(server, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
return 1;
}
listen(server, 100);
FD_SET(server, &readfds);
while (1) {
fd_set temp = readfds;
select(FD_SETSIZE,&temp,0,0,0);
if (FD_ISSET(server, &temp)) {
int client = accept(server, NULL, NULL);
printf("有新客户端连接\n");
FD_SET(client, &readfds); //将新连接的客户端加入监视列表,下一次循环时碰到select就能监视到了
insert_client(client_arr, &client_count, client); //将新连接的客户端,存入客户端数组中进行统一管理
} else {
for (int i = 0; i < client_count; i++) {
int client = client_arr[i];
if (FD_ISSET(client_arr[i], &temp)) {
char buf[128] = {0};
int res = read(client, buf, 128);
if (res == 0) { //客户端断开连接或出错
printf("有客户端断开连接\n");
FD_CLR(client, &readfds); //从监视列表readfds里面删除
remove_client(client_arr, &client_count, client); //从客户端数组client_arr中删除
close(client); //关闭套接字
break;
}
printf("客户端发来消息:%s\n", buf);
}
}
}
}
return 0;
}
select代码模型
fd_set readfds;
FD_ZERO(readfds)
FD_SET(想要监视的描述符,readfds)
while(1){
select()
判断/循环判断哪个描述符激活了{
调用对用的阻塞函数
例如:accept 或者 read
}
}
2.3 poll模型
工作方式和select是一样的,都是用来监视描述符是否激活,只不过操作过程不一样
因为select的不足,才会有poll模型的
① select中 fd_set 类型大小固定,1024个,如果要监视的描述符数量超过1024个,就会有问题
② select监视到激活的描述符成功后,会将激活的把readfds覆盖
poll 解决了上述2个问题
原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
调用:poll(准备监视的struct pollfd 数组,数组的容量/实际长度,-1)
功能描述:监视 fds所指向的描述符数组中所有描述符的情况,最多监视nfds个,一般就是数组的容量
参数解析:
参数 fds:结构体数组,数组中的每一个结构体元素都是一个描述符搭配一些其他数据,结构如下
struct pollfd {
int fd; /* file descriptor */要监视的描述符
short events; /* requested events */可读、可写、意外
因为可读的原因激活:POLLIN,一般我们都写这个
因为可写的原因激活:POLLOUT
short revents; /* returned events */
fd描述符,一旦激活,用来表示具体因为什么原因激活的
};
参数 nfds:想要监视的描述符的数量,一般就是fds这个数组的实际长度
参数 timeout:poll函数阻塞时长,单位为毫秒
传 0 表示不阻塞
传 -1 表示一直阻塞,直到有描述符激活
返回值:成功返回激活的描述符的数量
注意:poll函数监视的直接是一个结构体数组,这个数组我们是可以直接操作的,不需要额外的函数去操作
注意:poll的激活方式为水平触发
水平触发:只要监视的所有套接字中,有任何套接字缓存区存在数据,则poll就会激活
#include <myhead.h>
int main(int argc, const char *argv[])
{
if (argc != 2)
{
printf("请输入正确的端口号\n");
return 1;
}
int port = atoi(argv[1]);
struct pollfd fds[50] = {0};
int server = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr("192.168.0.103");
if (bind(server, (struct sockaddr *)&addr, sizeof(addr)) == -1)
{
perror("bind");
return 1;
}
listen(server, 100);
fds[0].fd = server;
fds[0].events = POLLIN;
int fd_count = 1;
while (1)
{
poll(fds, fd_count, -1);
for (int i = 0; i < fd_count; i++)
{
int fd = fds[i].fd;
short revents = fds[i].revents;
if (fd == server && revents == POLLIN)
{
printf("有新客户端连接\n");
int client = accept(server, NULL, NULL);
fds[fd_count].fd = client;
fds[fd_count].events = POLLIN;
fd_count++;
}
if (fd != server && revents == POLLIN)
{
char buf[128] = {0};
int res = read(fd, buf, 128);
if (res == 0)
{
printf("有客户端断开连接\n");
int j = -1;
for (int j = i; j < fd_count - 1; j++)
{
fds[j] = fds[j + 1];
}
memset(fds + j, 0, sizeof(fds[j]));
fd_count--;
i--;
close(fd);
break;
}
printf("客户端发来消息:%s\n", buf);
}
}
}
return 0;
}
poll的代码模型
struct pollfd fds[n] = {0};
fds[0].fd = 想要监视的第一个描述符
fds[0].events = POLLIN
while(1){
poll(fds,n,-1)
for(遍历 fds){
// 判断 fds中哪个描述符激活了
if(fds[i].revents == POLLIN){
调用对应的阻塞函数
例如 accept 或者 read 或者 scanf等
}
}
}
作业:
运行1个服务器和2个客户端
实现效果:
服务器和2个客户端互相聊天,服务器和客户端都需要使用select模型去实现
服务器要监视2个客户端是否连接,2个客户端是否发来消息以及服务器自己的标准输入流
客户端要监视服务器是否发来消息以及客户端自己的标准输入流
在不开线程的情况下,实现互相聊天