问题:使用 select() 函数可以扩展服务端功能吗? 如果可以,具体怎么实现?


目前服务端的瓶颈分析

服务端大多数时候处于等待状态,无法发挥主机(设备)的最大性能

while (1) {
    // 阻塞,等待客户端连接
    client = accept(server, (struct sockaddr*)&caddr, &asize);

    printf("client: %d\n", client);

    do {
        // 阻塞,等待客户端数据
        r = recv(client, buf, sizeof(buf), 0);
        if (r > 0) {
            printf("Receive: %s\n", buf);

            if (strcmp(buf, "quit") != 0) {
                len = send(client, buf, r, 0);
            }
            else {
                break;
            }
        } 
    } while (r > 0);

    close(client);
}

解决方案:阻塞变轮询

  • 通过 select() 函数首先监听服务端 server_fd, 目标事件为 “连接”(读)
  • 当事件发生(客户端连接),则调用 accept() 接受连接
  • 将 client_fd 加入监听范围,目标事件为“数据接收”(读)
  • 循环查看各个被监听的文件描述符是否有事件发生
实现方式

image.png

实现逻辑

while (1) {
    rset = reads;

    num = select(max + 1, &rset, 0, 0, &timeout);

    if (num > 0) {
        int i = 0;
        for (i=1; i<=max; ++i) {  // 注意, 0 被命令行占用,下标从 1 开始遍历
            if (FD_ISSET(i, &rset)) {
                if (i == server) {
                    // accept and add client to fd_set
                } else {
                    // read data from client by i (fd)
                }
            }
        }
    }
}

实现关键

  • 动态调整需要监视的文件描述符

    • 当接收到客户端连接时,将客户端文件描述符加入监听变量 (fd_set) 中
    • 当发现客户端断开时,在监听变量 (fd_set) 剔除客户端文件描述符
// 添加监听
if (client > -1) {
    FD_SET(client, &reads);
    max = (client > max) ? client : max;
    printf("client: %d\n", client);
}

// 剔除监听
if (r == -1) {
    FD_CLR(i, &reads);
    close(i);
}
  • 动态调整需要监视的文件描述符数量
  • 保证每个需要监视的文件描述符能够被轮询
  • max = (client > max) ? client : max

编程实验:改进后的服务端

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int server_handler (int server)
{
    struct sockaddr_in addr = {0};
    socklen_t asize = sizeof(addr);

    return accept(server, (struct sockaddr*)&addr, &asize);
}

int client_handler(int client)
{
    char buf[32] = {0};
    int ret = read(client, buf, sizeof(buf) - 1);

    if (ret > 0) {
        buf[ret] = 0;

        printf("Receive: %s\n", buf);

        if (strcmp(buf, "quit") != 0) {
            ret = write(client, buf, ret);
        } else {
            return -1;
        }
    }

    return ret;
}

int main()
{
    int server = 0;
    struct sockaddr_in saddr = {0};
    int max = 0;
    int num = 0;
    fd_set reads = {0};
    fd_set temps = {0};
    struct timeval timeout = {0};

    server = socket(PF_INET, SOCK_STREAM, 0);

    if (server == -1) {
        printf("server socket error\n");
        return -1;
    }

    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(8888);

    if (bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1) {
        printf("server bind error\n");
        return -1;
    }

    if (listen(server, 1) == -1) {
        printf("server listen error\n");
        return -1;
    }

    printf("server start success\n");

    FD_ZERO(&reads);
    FD_SET(server, &reads);

    max = server;

    while (1) {
        temps = reads;

        timeout.tv_sec = 0;
        timeout.tv_usec = 10000;

        num = select(max+1, &temps, 0, 0, &timeout);

        if (num > 0) {
            int i = 0;

            for (i=1; i<=max; ++i) {
                if (FD_ISSET(i, &temps)) {
                    if (i == server) {
                        int client = server_handler(server);

                        if (client > -1) {
                            FD_SET(client, &reads);

                            max = (client > max) ? client : max;

                            printf("accept client: %d\n", client);
                        }
                    }
                    else {
                        int r = client_handler(i);

                        if (r == -1) {
                            FD_CLR(i, &reads);

                            close(i);
                        }
                    }
                }
            }
        }
    }

    return 0;
}

思考:改进后的服务端是否还有优化的空间? select() 是 Linux() 系统特有的吗?

TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧