网络编程:基于 I/O 复用的服务器端

44 阅读3分钟

image.png

select函数

使用该函数可以将多个文件描述符集中到一起监视,监视项如下

  • 是否存在套接字接收数据
  • 无需阻塞传输数据的套接字有哪些
  • 哪些套接字发生了异常

select 函数的调用过程如下:

image.png

1. 设置文件描述符

监视文件描述符可以视为监视套接字,使用fd_set数组变量执行此操作,数组是存有 0 和 1 的位数组。

image.png

最左边的位表示文件描述符 0 所在位置,若为 1,表示该文件描述符是监视对象。 在fd_set变量中注册或更改值的操作由下列宏完成:

FD_ZERO(fd_set * fdset) 
将 fd_set 变量的所有位初始化位0

FD_SET(int fd, fd_set * fdset)
在参数 fd_set 指向的变量中注册文件描述符 fd 的信息

FD_CLR(int fd, fd_set * fdset)
从参数 fdset 指向的变量中清除文件描述符 fd 的信息

FD_ISSET(int fd, fd_set * fdset) 
若参数 fdset  指向的变量中包含文件描述符 fd 的信息,则返回真

image.png

2. 设置检查(监视)范围及超时

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfd, fd_set * readset, fd_set * writeset, fd_set * exceptset, const struct timeval * timeout);

成功时返回大于0的值, 该值是发生事件的文件描述符的数目, 失败时返回 -1,超时返回 0

参数解释:
maxfd: 监视对象文件描述符的数量
readset: 将所有关注“是否存在待读取数据”的文件描述符注册到fd_set变量,并传递其地址值
writeset: 将所有关注“是否可传输无阻塞数据”的文件描述符注册到fd_set变量,并传递其地址值 
exceptset: 将所有关注“是否发生异常”的文件描述符注册到fd_set变量,并传递其地址值
timeout: 调用 select 后,为防止陷入无限阻塞的状态,传递超时信息

文件描述符的监视范围

与第一个参数有关,该参数要求传递监视对象文件描述符数量,需要得到注册在fd_set中的文件描述符数,只需将最大的文件描述符值+1 传递到 select 函数即可,加 1 是因为文件描述符的值从 0 开始 超时时间与最后一个参数有关,timeval结构体如下:

struct timeval 
{
    long tv_sec;  //seconds
    long tv_usec; //microseconds
}

设置超时时间是为了防止监视的文件描述符一直未发生变化,造成一直阻塞。

3. 调用 select 后查看结果

image.png select调用完成后,向其传递的fd_set变量中将发生变化。原来为 1 的所有位都变为 0,但发生变化的文件描述符对应位除外,因此可以认为值仍为 1 的位置上的文件描述符发生了变化。

#define BUF_SIZE 30
int main(int argc, char *argv[]) {
    fd_set reads, temps;
    int result, str_len;
    char buf[BUF_SIZE];
    struct timeval timeout;
    
    FD_ZERO(&reads);
    FD_SET(0, &reads);
    
    while (1) {
        temps = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 0;
        result = select(1, &temps, 0, 0, &timeout);
        printf("result = %d", result);
        if (result == -1) {
            puts("select error!!!");
            break;
        } else if (result == 0) {
            puts("time out");
        } else {
            if (FD_ISSET(0, &temps)) {
                str_len = read(0, buf, BUF_SIZE);
                buf[str_len] = 0;
                printf("message from console: %s", buf);
            }
        }
    }
    return 0;
}

4.I/O复用服务器端

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>

#define BUF_SIZE 30
void error_handling(char *message);

int main(int argc, char *argv[]) {
    int serv_sock, cln_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;
    socklen_t adr_sz;
    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE];
    if(argc != 2) {
        printf("usage: %s <port>\n", argv[0]);
        exit(1);
    }
    serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(atoi(argv[1]));
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1) {
        error_handling("bind error !!!");
    }
    if (listen(serv_sock, 5) == -1) {
        error_handling("listen error !!!");
    }

    FD_ZERO(&reads);
    FD_SET(serv_sock, &reads);
    
    fd_max = serv_sock;
    printf("serv_sock is %d \n", fd_max);
    
    while (1) {
        cpy_reads = reads;
        timeout.tv_sec = 5;
        timeout.tv_usec = 5000;
        if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1) {
            break;
        }
        if (fd_num == 0) {
            continue;
        }
        for (i = 0; i < fd_max + 1; i++) {
            if (FD_ISSET(i, &cpy_reads)) {
                if (i == serv_sock) {   // connection request
                    adr_sz = sizeof(clnt_adr);
                    cln_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
                    FD_SET(cln_sock, &reads);
                    if (fd_max < cln_sock) { //保证fd_max比新注册的文件描述符的值大
                        fd_max = cln_sock;
                    }
                    printf("connected clinent: %d \n", cln_sock);
                } else { // read message
                    str_len = read(i, buf, BUF_SIZE);
                    if (str_len == 0) {
                        FD_CLR(i, &reads);
                        close(i);
                        printf("close client: %d \n", i);
                    } else {
                        write(i, buf, str_len); // echo
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}