Linux网络编程-并发服务器与IO模型

一. 并发服务器

单循环服务器: 一次只能处理一个客户端任务的服务器。

**并发服务器:**能够同时处理多个客户端任务的服务器

如何实现并发服务器:

TCP并发服务端:

1. 多进程

安全、资源开销大、并发量小

2. 多线程

相对进程资源开销小,相对并发量大

3. 线程池

提前创建好大量线程管理起来,避免反复创建线程带来时间消耗

生产者--消费者 设计模式

4. IO多路复用

文件 ---> fd

对多个文件描述符的监测,复用一个进程。

1.1 多进程实现并发服务器

cpp 复制代码
#define SER_PORT 50000

int init_tcp_ser()
{
    int lisfd, ret;
    struct sockaddr_in seraddr;
    lisfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lisfd < 0){
        perror("socket()");
        return -1;
    }
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    ret = bind(lisfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0){
        perror("bind()");
        return -1;
    }
    ret = listen(lisfd, 128);
    if(ret < 0){
        perror("listen()");
        return -1;
    }
    return lisfd;
}

void child_catch(int signum)
{
    pid_t wid;
    while((wid = waitpid(0, NULL, WNOHANG)) > 0){
        printf("wait finish:%d\n",wid);
    }
    return;
}

int main()
{
    int lisfd, connfd;
    struct sockaddr_in cliaddr;
    char cli_ip[64] = { 0 };
    char rbuff[128] = { 0 };
    ssize_t size;
    pid_t pid;
    socklen_t cliaddr_len = sizeof(cliaddr);
    
    struct sigaction act;
    act.sa_handler = child_catch;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(SIGCHLD, &act, NULL);


    lisfd = init_tcp_ser();
    if(lisfd == -1){
        exit(1);
    }
    while(1){
        connfd = accept(lisfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        if(connfd < 0){
            if(errno == EINTR){
                continue;
            }
            perror("accept()");
            exit(1);
        }
        printf("[%s : %d] get online!\n",
               inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip,sizeof(cliaddr)),
                         ntohs(cliaddr.sin_port));
        pid = fork();
        if(pid < 0){
            perror("fork()");
            exit(1);
        }
        else if(pid == 0){
            close(lisfd);
            while(1){
                memset(rbuff, 0, sizeof(rbuff));
                size = recv(connfd, rbuff, sizeof(rbuff), 0);
                if(size < 0){
                    perror("recv()");
                    close(connfd);
                    exit(1);
                }
                else if(size == 0){
                    exit(1);
                }
                //strcat(rbuff, "---ok");
                send(connfd, rbuff, strlen(rbuff), 0);
                write(STDOUT_FILENO, rbuff, strlen(rbuff));
            }
        }
    }
    close(lisfd);
    return 0;
}

1.2 多线程实现并发服务器

cpp 复制代码
typedef struct inf{
    int connfd;
    struct sockaddr_in cliaddr;
}Inf_t;

#define SER_PORT 50000

int init_tcp_ser()
{
    int lisfd, ret;
    struct sockaddr_in seraddr;
    lisfd = socket(AF_INET, SOCK_STREAM, 0);
    if(lisfd < 0){
        perror("socket()");
        return -1;
    }
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SER_PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    ret = bind(lisfd, (struct sockaddr *)&seraddr, sizeof(seraddr));
    if(ret < 0){
        perror("bind()");
        return -1;
    }
    ret = listen(lisfd, 128);
    if(ret < 0){
        perror("listen()");
        return -1;
    }
    return lisfd;
}

void *handler(void *arg)
{
    Inf_t *pcli_inf = (Inf_t *)arg;
    ssize_t size;
    char rbuff[128];
    char cli_ip[64] = { 0 };
    while(1){
        memset(rbuff, 0, sizeof(rbuff));
        size = recv(pcli_inf->connfd, rbuff, sizeof(rbuff), 0);
        if(size < 0){
            perror("recv()");
            close(pcli_inf->connfd);
            break;
        }
        else if(size == 0){
            break;
        }
        //write(STDOUT_FILENO, rbuff, size);
        printf("[%s : %d] : %s\n",
               inet_ntop(AF_INET, &(pcli_inf->cliaddr.sin_addr), cli_ip
                         ,sizeof(pcli_inf->cliaddr)),
               ntohs(pcli_inf->cliaddr.sin_port),rbuff);
    }
    pthread_exit(NULL);
}


int main()
{
    int lisfd, connfd, ret, i = 0;
    struct sockaddr_in cliaddr;
    char cli_ip[64] = { 0 };
    char rbuff[128] = { 0 };
    ssize_t size;
    Inf_t inf[256] = { 0 };
    pthread_t tid;

    pid_t pid;
    socklen_t cliaddr_len = sizeof(cliaddr);    

    lisfd = init_tcp_ser();
    if(lisfd == -1){
        exit(1);
    }
    while(1){
        connfd = accept(lisfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
        if(connfd < 0){
            perror("accept()");
            exit(1);
        }
        printf("[%s : %d] get online!\n",
               inet_ntop(AF_INET, &cliaddr.sin_addr, cli_ip,sizeof(cliaddr)),
                         ntohs(cliaddr.sin_port));
        inf[i].connfd = connfd;
        inf[i].cliaddr = cliaddr;

        //ret = pthread_create(&tid, NULL, handler, (void *)&connfd);
        ret = pthread_create(&tid, NULL, handler, (void *)&inf[i]);
        if(ret < 0){
            fprintf(stderr, "pthread_create error:%s\n",strerror(ret));
            continue;
        }
        pthread_detach(tid); 
        i++;
    }
    return 0;
}

二. IO模型

Linux下常见的IO模型:阻塞IO,非阻塞IO,信号驱动IO,多路IO复用

2.1 阻塞IO

类似fgets,read,fread,recv 这些函数,内核层阻塞,应用层也阻塞等待

特点:

多个IO具有先后顺序 ,同步

当发生阻塞时,CPU完成切换,故节省CPU资源

代码示例:

读端

cpp 复制代码
int main()
{
    int fd;
    char buff[128] = { 0 };
    mkfifo("myfifo", 0664);
    fd = open("myfifo", O_RDONLY);
    if(fd < 0){
        perror("open()");
        exit(1);
    }
    while(1){
        memset(buff, 0, sizeof(buff));
        fgets(buff ,sizeof(buff), stdin);
        buff[strlen(buff) - 1] = '\0';
        printf("STDIN :%s\n",buff);

        memset(buff, 0, sizeof(buff));
        read(fd, buff, sizeof(buff));
        printf("FIFO :%s\n",buff);
    }
    close(fd);
    return 0;
}

写端:

cpp 复制代码
int main()
{
    int fd;
    mkfifo("myfifo", 0664);
    fd = open("myfifo", O_WRONLY);
    if(fd < 0){
        perror("open()");
        exit(1);
    }

    while(1){
        write(fd, "hello_world", 11);
        sleep(1);
    }
    close(fd);
    return 0;
}

2.2 非阻塞IO

一般搭配轮询,比较浪费CPU资源

特点:

搭配轮询方式实现

2.2.1 fcntl函数

作用:对文件描述符 fd 执行各类控制操作,是 Linux IO 核心系统调用之一。

参数:

  • fd:要操作的文件描述符(普通文件、socket、管道、FIFO 都支持)
  • cmd:控制命令(决定第三个参数是否存在、类型)
  • arg:可选参数,随 cmd 变化(整数 / 结构体指针)

返回值:

失败-1且errno

成功:不同的cmd返回值不一样

cpp 复制代码
int main()
{
    int fd;
    char buff[128] = { 0 };
    mkfifo("myfifo", 0664);
    fd = open("myfifo", O_RDONLY);
    if(fd < 0){
        perror("open()");
        exit(1);
    }
    //设置终端为非阻塞模式
    int flag = fcntl(0, F_GETFD);
    flag |= O_NONBLOCK;
    fcntl(0, F_SETFL, flag);
    //修改回阻塞模式
    flag &= ~O_NONBLOCK;
    fcntl(0, F_SETFL, flag);
    while(1){
        memset(buff, 0, sizeof(buff));
        fgets(buff ,sizeof(buff), stdin);
        buff[strlen(buff) - 1] = '\0';
        printf("STDIN :%s\n",buff);

        memset(buff, 0, sizeof(buff));
        read(fd, buff, sizeof(buff));
        printf("FIFO :%s\n",buff);
    }
    close(fd);
    return 0;
}

2.3 信号驱动IO

原理: 有信号通知时再去处理IO事件,比如读、写等等

当有信号时,内核通知应用层去处理IO事件

可使用的信号:SIGIO 29号 SIGURG 23号

优缺点: 效率高,但是由于驱动信号个数有限,应用层无法区分多个IO事件

示例代码:

cpp 复制代码
void handler(int signum)
{
    char buff[128];
    memset(buff, 0, sizeof(buff));
    fgets(buff ,sizeof(buff), stdin);
    buff[strlen(buff) - 1] = '\0';
    printf("STDIN :%s\n",buff);
    return;
}

int main()
{
    int fd;
    char buff[128] = { 0 };
    mkfifo("myfifo", 0664);
    fd = open("myfifo", O_RDONLY);
    if(fd < 0){
        perror("open()");
        exit(1);
    }
    signal(SIGIO, handler);


    //设置终端为非阻塞模式
    int flag = fcntl(0, F_GETFL);
    flag |= O_ASYNC;
    fcntl(0, F_SETFL, flag);
    //关联当前进程,接收sigio信号
    fcntl(0, F_SETOWN, getpid());

    while(1){

        memset(buff, 0, sizeof(buff));
        read(fd, buff, sizeof(buff));
        printf("FIFO :%s\n",buff);
    }
    close(fd);
    return 0;
}

2.4 多路复用IO

在一个进程里面,同时监测多个IO

又称IO多路转接

基本流程:

1、先创建文件描述符集合

2、将关注的文件描述符加入到集合

3、通知内核开始监测 select/poll/epoll

4、当有IO事件到达时,监测返回结果

2.4.1 select函数

功能: 通知内核监测文件描述符中的事件

参数:

nfds :集合中最大的文件描述符+1,内核用于遍历,提升效率

readfds:读事件的文件描述符集合,传入传出参数

writefds:写事件的文件描述符集合

exceptfds:其他

timeout:超时时间

NULL :不设置超时时间(阻塞)

返回值:

成功:返回到达事件的个数,有事件满足的个数

失败:-1且errno

void FD_CLR(int fd, fd_set *set);

**功能:**将fd对应文件描述符清零

int FD_ISSET(int fd, fd_set *set);

**功能:**判断fd对应的文件描述符是否被置位

void FD_SET(int fd, fd_set *set);

**功能:**将集合表中的fd对用的文件描述符置位

void FD_ZERO(fd_set *set);

**功能:**将集合表全部清零

示例代码:

cpp 复制代码
int main()
{
    int fd;
    char buff[128] = { 0 };
    mkfifo("myfifo", 0664);
    fd = open("myfifo", O_RDONLY);
    if(fd < 0){
        perror("open()");
        exit(1);
    }

    fd_set rdfds, tmprdfds;
    int maxfd;
    int ret;
    FD_ZERO(&rdfds);
    FD_SET(0, &rdfds);
    maxfd = 0;
    FD_SET(fd, &rdfds);
    maxfd = maxfd > fd ? maxfd : fd; 

    while(1){
        tmprdfds = rdfds;
        ret = select(maxfd + 1, &tmprdfds, NULL, NULL, NULL);
        if(ret < 0){
            perror("select()");
            exit(1);
        }
        if(FD_ISSET(0, &tmprdfds)){
            memset(buff, 0, sizeof(buff));
            fgets(buff ,sizeof(buff), stdin);
            buff[strlen(buff) - 1] = '\0';
            printf("STDIN :%s\n",buff);
        }
        if(FD_ISSET(fd, &tmprdfds)){
            memset(buff, 0, sizeof(buff));
            read(fd, buff, sizeof(buff));
            printf("FIFO :%s\n",buff);
        }
    }
    close(fd);
    return 0;
}