io复用之select模型

通常,网络程序服务端要能同时接收来自多个客户端的连接,处理请求并做出响应,可以针对每个连接创建子进程专门处理,这种做法的缺点是相耗资源较多,另外如果要牵涉到进程间通信,编程难度会大大增加。

IO复用技术在一定程度上可避免此类问题,无论连接的客户端有多少个,提供服务的进程只有一个。常用的IO复用模型有select/poll/epoll,这里介绍select模型的用法。

select模型中最重要的要属select函数和相应的宏操作了,声明如下:

#include <sys/select.h>
#include <sys/time.h>
struct timeval {
    long tv_sec;
    long tv_usec;
};
int select(int maxfd1, fd_set *rset, fd_set *wset, fd_set *eset, struct timeval *timeout);
FD_ZERO(fd_set *fds);
FD_SET(int fd, fd_set *fds);
FD_CLR(int fd, fd_set *fds);
FD_ISSET(int fd, fd_set *fds);

其中:

以下是基于select的回射服务器例子。

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

int ListenAt(short port) {
    struct sockaddr_in ser;
    bzero(&ser, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_addr.s_addr = htonl(INADDR_ANY);
    ser.sin_port = htons(port);

    int fd = socket(AF_INET, SOCK_STREAM, 0);
    bind(fd, (struct sockaddr*)&ser, sizeof(struct sockaddr));
    listen(fd, 10);
    return fd;
}

int Accept(int serfd) {
    struct sockaddr_in cli;
    socklen_t len = sizeof(cli);
    int clifd = accept(serfd, (struct sockaddr*)&cli, &len);
    return clifd;
}

int main(int argc, char *argv[]) {
    int i, serfd, clifd, maxfd, ret;
    fd_set fds, fds1;
    struct timeval timeout;
    char buf[128];

    maxfd = serfd = ListenAt(atoi(argv[1]));
    FD_ZERO(&fds);
    FD_SET(serfd, &fds);
    for (;;) {
        timeout.tv_sec = 3; timeout.tv_usec = 0;
        fds1 = fds;
        ret = select(maxfd + 1, &fds1, NULL, NULL, &timeout);
        if (ret == -1) break;
        if (ret == 0) continue;
        for (i = 0; i <= maxfd; i++) {
            if (FD_ISSET(i, &fds1)) {
                if (i == serfd) {
                    clifd = Accept(serfd);
                    FD_SET(clifd, &fds);
                    if (maxfd < clifd) maxfd = clifd;
                } else {
                    int msglen = read(i, buf, sizeof(buf));
                    if (msglen == 0) {
                        FD_CLR(i, &fds);
                        close(i);
                    } else {
                        write(i, buf, msglen);
                    }
                }
            }
        }
    }
    close(serfd);
    return 0;
}

上述示例是在Linux环境下的,Windows下也有相应的接口,但稍有区别。

#include <winsock2.h>
int select(int nfds, fd_set *rset, fd_set *wset, fd_set *eset, const struct timeval *timeout);

由于Linux的文件描述符从0开始递增,因此可以找出当前文件描述符数量和最后生成的文件描述符之间的关系,fd_set结构用一个数组就够。而Windows套接字的句柄并非从0开始,且句柄之间无规律可循,因此fd_set除数组外还用了个记录句柄数的变量,从而select函数中第一个参数无意义,只为保证兼容性。

处理fd_set的四个宏的名称、功能和用法与Linux完全一样。 下面是Windows版本示例代码。

#include <winsock.h>
#include <windows.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32.lib")
SOCKET ListenAt(short port) {
    WSADATA wsaData;
    SOCKADDR_IN ser;

    WSAStartup(MAKEWORD(2, 2), &wsaData);
    SOCKET sock = socket(AF_INET, SOCK_STREAM, 0);
    memset(&ser, 0, sizeof(ser));
    ser.sin_family = AF_INET;
    ser.sin_addr.s_addr = htonl(INADDR_ANY);
    ser.sin_port = htons(port);
    bind(sock, (SOCKADDR*)&ser, sizeof(ser));
    listen(sock, 5);
    return sock;
}
SOCKET Accept(SOCKET listenSock) {
    SOCKADDR_IN cli;
    int size = sizeof(cli);
    SOCKET cliSock = accept(listenSock, (SOCKADDR*)&cli, &size);
    return cliSock;
}
int main(int argc, char *argv[]) {
    fd_set fds, fds1;
    struct timeval timeout;

    SOCKET sock = ListenAt(atoi(argv[1]));
    FD_ZERO(&fds);
    FD_SET(sock, &fds);
    for (;;) {
        timeout.tv_sec = 3; timeout.tv_usec = 0;
        fds1 = fds;
        int ret = select(0, &fds1, NULL, NULL, &timeout);
        if (ret == SOCKET_ERROR) break;
        if (ret == 0) continue;
        for (size_t i = 0; i < fds.fd_count; i++) {
            if (FD_ISSET(fds.fd_array[i], &fds1)) {
                if (fds.fd_array[i] == sock) {
                    SOCKET clisock = Accept(sock);
                    FD_SET(clisock, &fds);
                } else {
                    char buf[128];
                    int len = recv(fds.fd_array[i], buf, sizeof(buf), 0);
                    if (len == 0) {
                        FD_CLR(fds.fd_array[i], &fds);
                        closesocket(fds.fd_array[i]);
                    } else {
                        send(fds.fd_array[i], buf, len, 0);
                    }
                }
            }
        }
    }
    closesocket(sock);
    return 0;
}

其他说明:

Table of Contents