A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

前面讲了一些Java NIO分析(2): I/O多路复用历史杂谈, 谈到了多路复用的发展历史
以及为什么需要它。今天讲广受各大内核支持的select系统调用,select允许进程
指定内核等待1个或者多个事件的任何一个发生, 并且只在有它们发生之后或者等待一段时间后才唤醒进程。
1. select介绍
使用select, 我们即使应用进程里只有1个线程也能够接受多个连接并且做出处理,
因为应用进程不需要阻塞在socket的read,write或者accept系统调用上, 而是
内核告诉应用有事件到来了, 应用进程遍历fd_set看是哪个fd就绪了,然后再处理
fd_set称为描述符集,通常是一个整数数组,其中每个整数的每一位对应1个描述符的状态.
select的函数签名是:
#include <sys/select.h>#include <sys/time.h>int select(int maxfdpl, fd_set *readset, fd_set *writeset,           fd_set *exceptset, const struct timeval *timeout)
其中,返回值表示fd_set里就绪的元素总个数,包括读,写,异常fd_set
第一个参数表示待测试的fd个数,值一般是待测试的fd总数+1
中间仨代表要监听的读set, 写set和异常set
最后一个参数代表select每个fd经历的时间
操作描述符集的方法是4个宏:
  • FD_ZERO 清除fdset里的所有位
  • FD_SET 开启某fd在fdset里对应的位, 一般就置1
  • FD_CLR 清除某fd在fdset里对应的位, 一般就置0
  • FD_ISSET 判断fdset里是否包含某个fd的位
socket描述符的读就绪条件有:
  • 该socket的读缓冲区的数据字节数 >= 读缓冲区低水位标记的当前大小
  • 连接的读半关闭(FIN)
  • socket是监听socket且已完成的连接数不为0
  • socket有异常需要处理
socket描述符的写就绪条件有:
  • 该socket的发送缓冲区的可用空间字节数 >= socket发送缓冲区低水位的大小
  • 连接的写半关闭
  • 使用非阻塞式的connect的socket已建立连接
  • socket有异常要处理
通俗点说就是, 缓冲区是一个大小为n的仓库,数据是货物。
读缓冲区就类似于,你去取货,首先要货物有一定数量(读缓冲区低水位标记), 不然就拿不到货了
写缓冲区好比,你要把货物入库,仓库得还有剩余空间(写缓冲区低水位标记), 不然仓库放不下
2. select写server实战
下面是一个使用c写的echo server示例,可以直接编译运行
gcc -o select ./select.c./select
主要有5个步骤
  • 新建socket, 用于监听端口
  • socket的fd绑定端口
  • socket监听
  • 初始化fd_set
  • 发起select系统调用获取所有fd_set,寻找和处理事件
#include <sys/select.h>#include <sys/socket.h>#include <stdio.h>#include <string.h>#include <errno.h>#include <netinet/in.h>#include <stdlib.h>#include <unistd.h>int main() {    // 1. 新建socket, 用于监听端口    int listenfd = socket(AF_INET, SOCK_STREAM, 0);    if (listenfd == -1) printf("创建socket失败, error: %s (errno: %d)\n", strerror(errno), errno);    // 2. 绑定端口    unsigned short listenPort = 8090;    struct sockaddr_in servaddr;    memset(&servaddr, 0, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port = htons(listenPort);    int on = 1;    // 设置socket绑定的端口,再程序关闭之后可以重复使用    if ((setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(int))) < 0) {        exit(1);    }    int bindRet = bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));    if (bindRet == -1) {        printf("socket绑定地址失败, error: %s (errno: %d)\n", strerror(errno), errno);        exit(1);    }    // 3. 监听端口    int listenRet = listen(listenfd, 10);    if (listenRet == -1) printf("socket监听端口失败, error: %s (errno: %d)\n", strerror(errno), errno);    printf("socket 监听完毕, 地址: 127.0.0.1:%d", listenPort);    // 4. 初始化fd_set    int client[FD_SETSIZE]; // 保存已连接fd    for (int i = 0; i < FD_SETSIZE; ++i) {        client = -1;    }    int maxfd = listenfd, i, maxi = -1, MAXLEN = 1024;    char buf[MAXLEN];    fd_set rset, allset; // fd_set代表的是描述符集,本质上是一个整数, 根据操作系统的不同有可能是64位或者32位,通过每位的0或者1来判断fd是否就绪    FD_ZERO(&allset); // 清零    FD_SET(listenfd, &allset); // 把listenfd加入到allset    printf("ready");    while (1) {        rset = allset;        // 函数签名int select(int maxfdpl, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)        // 返回值表示fd_set里就绪的元素总个数,包括读,写,异常fd_set        // 第一个参数表示待测试的fd个数,值一般是待测试的fd总数+1        // 中间仨代表要监听的读set, 写set和异常set        // 最后一个参数代表select每个fd经历的时间        // 5. select获取 连接fd,断开连接fd, 有数据可读fd        int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);        if (FD_ISSET(listenfd, &rset)) { // 接受新连接            struct sockaddr_in clientaddr;            int clilen = sizeof(clientaddr);            int connfd = accept(listenfd, (struct sockaddr *) &clientaddr, &clilen);            // 保存            for (i = 0; i < FD_SETSIZE; ++i) {                if (client < 0) {                    client = connfd;                    break;                }            }            if (i == FD_SETSIZE) exit(1);            FD_SET(connfd, &allset);            if (connfd > maxfd) maxfd = connfd;            if (i > maxi) maxi = i;            if (--nready <= 0) continue;        }        for (i = 0; i <= maxi; i++) {            int sockfd = client;            if (sockfd < 0) continue;            if (FD_ISSET(sockfd, &rset)) {                int n = recv(sockfd, buf, MAXLEN, 0);                if (n == 0) { // 读到0表示连接关闭                    close(sockfd);                    FD_CLR(sockfd, &allset);                    client = -1;                } else write(sockfd, buf, n);                if (--nready == 0) break;            }        }    }}
然后使用任何tcp测试工具都能测, 例如好用的nc, 多开几个终端用nc去发消息,程序也能正常处理
nc localhost 80903. select总结
select是早期berkly随着tcp/ip协议栈一起发出来的。有非常良好的兼容性,各个平台都支持,
是早期I/O多路复用比较好的方案
select的fd_size(最大描述符数)只有1024, 是在头文件里用宏写死的,
早期bsd内核最多只能开20个进程,在当时看来1024长度的fd_set已经大到用不完了, 放在今天肯定是不够了
一旦fd_set里任意fd上有事件发生,内核会立刻返回,将数据从内核空间拷贝到用户空间,然后应用进程需要遍历整个
fd_set去寻找哪个fd有事件了。
所以,select的缺点主要有:
  • 最大并发限制: 1024个描述符
  • 内核/用户空间的fd_set数据拷贝
  • 遍历整个fd_set集合效率低,集合越大浪费的时间越多
为了解决这些问题,后人主要有2个改进,分别是poll和epoll, 后面我们会讲到。

6 个回复

倒序浏览
优秀,奈斯
回复 使用道具 举报
回复 使用道具 举报
回复 使用道具 举报
回复 使用道具 举报
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马