C语言中IO模型实现并发服务器

一、IO模型

1.分类

在UNIX/Linux下主要有4种I/O 模型:

        阻塞I/O:最常用、最简单、效率最低

        非阻塞I/O:可防止进程阻塞在I/O操作上,需要轮询

        I/O 多路复用:允许同时对多个I/O进行控制

        信号驱动I/O:一种异步通信模型------底层驱动专栏中详细讲

2.阻塞IO

以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,进程会被挂起,一直阻塞,直到缓冲区中有内容了,内核会唤醒该进程,读完内容后继续向下执行。

写操作也是会阻塞的,当缓冲区满了,就阻塞了,当缓冲区中有足够的空间接收这次写了就能解除阻塞。一般情况下,对于阻塞的问题,考虑的都是读的阻塞。

示例:以写阻塞为例

//写端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(){
    int fd = open("my_fifo", O_WRONLY);
	if(-1 == fd){
		perror("open error");
		exit(-1);
	}

    int count = 0;
    while(1){
        if(-1 == write(fd, "hello world", 11)){
			perror("write error");
			exit(-1);
		}
        count++;
        printf("count = %d\n", count);
    }

    close(fd);
    return 0;
}

//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(){
    int fd = open("my_fifo", O_RDONLY);
    char buff[11] = {0};
    read(fd, buff, 11);

    while(1);//防止管道破裂
    close(fd);
    return 0;
}

3.非阻塞IO

以读阻塞为例,如果程序执行到阻塞函数时,这时如果缓冲区中有内容,则程序会正常执行,如果缓冲区中没有内容,相当于告诉内核,不要将这个进程挂起,而是立即给我返回一个错误。

一般的带有阻塞属性的函数,默认方式都是阻塞IO。对于recv recvfrom 等函数,是可以通过参数来设置成非阻塞的。如:recv 的 MSG_DONTWAIT,recvfrom 的 MSG_DONTWAIT,waitpid 的 WNOHANG等。但是对于 read 等函数,默认方式就是阻塞,如果想使用read实现非阻塞,需要用到 fcntl() 来修改文件描述符的状态。

fcntl函数说明:

int fcntl(int fd, int cmd, ... /* arg */ );
功能:设置或获取文件描述符的状态

#include <unistd.h>
#include <fcntl.h>
参数:
        @fd: 文件描述符
        @cmd: 要控制的指令
                F_GETFL 获取文件描述符的状态
                F_SETFL 设置文件描述符的状态 O_NONBLOCK 非阻塞
        @arg: 可变参
                具体需不需要取决于第二个参数是什么,
                如果第二个参数是 F_GETFL 就不需要
                如果第二个参数是 F_SETFL 就需要
返回值: F_GETFL 返回的就是文件描述符的状态
               F_SETFL 成功返回0 失败返回-1

示例:使用管道时,注意,写端未打开,读端的open会阻塞,需要设置成非阻塞才能读到。

//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(){
    int fd1 = open("fifo1", O_RDONLY);
        if(-1 == fd1){
                perror("open error");
                exit(-1);
        }
    int fd2 = open("fifo2", O_RDONLY);
        if(-1 == fd2){
                perror("open error");
                exit(-1);
        }
    int fd3 = open("fifo3", O_RDONLY);
        if(-1 == fd3){
                perror("open error");
                exit(-1);
        }
    //将文件描述符 fd1 fd2 fd3 设置成非阻塞
    int flag = fcntl(fd1, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(fd1, F_SETFL, flag);

    flag = fcntl(fd2, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(fd2, F_SETFL, flag);

    flag = fcntl(fd3, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(fd3, F_SETFL, flag);

    char buff1[128] = {0};
    char buff2[128] = {0};
    char buff3[128] = {0};
    while(1){
        read(fd1, buff1, 128);
        printf("buff1 = %s\n", buff1);
        memset(buff1, 0, 128);

        read(fd2, buff2, 128);
        printf("buff2 = %s\n", buff2);
        memset(buff2, 0, 128);

        read(fd3, buff3, 128);
        printf("buff3 = %s\n", buff3);
        memset(buff3, 0, 128);
        //sleep(1);//为了演示现象用的 防止刷屏
    }
    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}
//写端(三个写端一样的)
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(){
    int fd = open("fifo1", O_WRONLY);
        if(-1 == fd){
                perror("open error");
                exit(-1);
        }

    char buff[128] = {0};
    while(1){
        fgets(buff, 128, stdin);
        buff[strlen(buff)-1] = '\0';
        if(-1 == write(fd, buff, 128)){
            perror("write error");
            exit(-1);
        }
        memset(buff, 0, 128);
    }
    close(fd);
    return 0;
}

4.IO多路复用

使用阻塞的方式处理多个阻塞函数,相互之间会有影响,有时不可取。如果使用非阻塞,有需要写一个循环轮询每个函数,十分占用CPU,也不可取。使用多进程、多线程也可以解决这个问题,但是要考虑资源的回收及安全问题,比较麻烦。比较好的一种方式,是使用 IO 多路复用。

IO多路复用的基本思想:

先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。

select函数说明:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
功能:IO多路复用

#include <sys/select.h>
参数:
        @nfds: 最大的文件描述符+1
        @readfds: 要监控的读文件描述符集合 我们一般考虑读
        @writefds: 要监控的写文件描述符集合
        @exceptfds: 要监控的异常文件描述符集合
        @timeout: 超时时间
                有值:阻塞的时间,超时后 select会立即返回
                0:     非阻塞
                NULL:永久阻塞
返回值: 成功返回已经就绪的文件描述符的个数,超时返回0,失败返回-1

注:FD_SETSIZE:select 能监视的最大的文件描述符个数是1024


文件描述符相关函数:

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);            //将集合清空

示例:

//读端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>

int main(){
    int fd1 = open("fifo1", O_RDONLY);
    int fd2 = open("fifo2", O_RDONLY);
    int fd3 = open("fifo3", O_RDONLY);

    int max_fd = 0;//保存最大的文件描述符
    //构建要监视的文件描述符集合
    fd_set readfds;//保存初始的
    fd_set readfds_temp;//给select用的
    FD_ZERO(&readfds);//清空
    FD_ZERO(&readfds_temp);//清空
    //将要监视的文件描述符添加进集合
    FD_SET(fd1, &readfds);
    max_fd = (max_fd>fd1?max_fd:fd1);
    FD_SET(fd2, &readfds);
    max_fd = (max_fd>fd2?max_fd:fd2);
    FD_SET(fd3, &readfds);
    max_fd = (max_fd>fd3?max_fd:fd3);


    char buff1[128] = {0};
    char buff2[128] = {0};
    char buff3[128] = {0};

    while(1){
        //注意:每次select返回都会将没有准备好的文件描述符在表中擦除
        //所以每次要重新将文件描述符添加到集合中
        readfds_temp = readfds;
        if(-1 == select(max_fd+1, &readfds_temp, NULL, NULL, NULL)){
            perror("select error");
            exit(-1);
        }
        if(FD_ISSET(fd1, &readfds_temp)){
            read(fd1, buff1, 128);
            printf("buff1 = [%s]\n", buff1);
            memset(buff1, 0, 128);
        }
        if(FD_ISSET(fd2, &readfds_temp)){
            read(fd2, buff2, 128);
            printf("buff2 = [%s]\n", buff2);
            memset(buff2, 0, 128);
        }
        if(FD_ISSET(fd3, &readfds_temp)){
            read(fd3, buff3, 128);
            printf("buff3 = [%s]\n", buff3);
            memset(buff3, 0, 128);
        }
    }
    close(fd1);
    close(fd2);
    close(fd3);
    return 0;
}
//写端
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(){
    int fd = open("fifo1", O_WRONLY);
        if(-1 == fd){
                perror("open error");
                exit(-1);
        }

    char buff[128] = {0};
    while(1){
        fgets(buff, 128, stdin);
        buff[strlen(buff)-1] = '\0';
        if(-1 == write(fd, buff, 128)){
            perror("write error");
            exit(-1);
        }
        memset(buff, 0, 128);
    }
    close(fd);
    return 0;
}

二、服务器模型

1.概念

服务器模型主要有两种:

        循环服

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值