朴素、Select、Poll和Epoll网络编程模型实现和分析——Select模型

来源:互联网 发布:javascript数组排序 编辑:程序博客网 时间:2024/05/19 22:44

        在《朴素、Select、Poll和Epoll网络编程模型实现和分析——朴素模型》中我们分析了朴素模型的一个缺陷——一次只能处理一个连接。本文介绍的Select模型则可以解决这个问题。(转载请指明出于breaksoftware的csdn博客)

        和朴素模型一样,我们首先要创建一个监听socket,然后调用listen去监听服务器端口。不同的是,我们要对make_socket方法传递1,因为我们要创建一个异步的socket。

listen_sock = make_socket(1);if (listen(listen_sock, SOMAXCONN) < 0) {perror("listen error");exit(EXIT_FAILURE);}
        下一步我们需要清空Select模型使用的fd_set结构体对象,并把监听socket设置进去

FD_ZERO(&active_fd_set);FD_SET(listen_sock, &active_fd_set);
        active_fd_set用于记录活动的socket信息。之后我们使用到的read_fd_set则是其一个拷贝,因为我们只关心读行为

fd_set active_fd_set, read_fd_set;
        和朴素模型类似,我们也需要使用一个死循环让服务器不要停止

/* Initialize the set of active sockets. */while (1) {timeout.tv_sec = 0;timeout.tv_usec = 500;/* Service all the sockets with input pending. */read_fd_set = active_fd_set;switch(select(FD_SETSIZE, &read_fd_set, NULL, NULL, &timeout)) {case -1 : {perror("select error\n");exit(EXIT_FAILURE);}break;case 0 : {//perror("select timeout\n");}break;default: {
        select函数第一个参数我们传递了FD_SETSIZE,它在我的系统上是1024,它代表需要关注的socket的最大个数。第二参数是用于记录需要关注的发生读事件的fd_set对象。我们让select函数按异步方式执行,故最后一个参数设置为500微秒的超时时间。整个select函数意思是我们需要等待socket发生可读事件,如果等待时间超过超时设置,则函数返回0,如果出错则返回-1,如果等待到事件则返回其他值。

default: {for (index = 0; index < FD_SETSIZE; ++index) {if (FD_ISSET(index, &read_fd_set)) {if (listen_sock == index) {/* Connection request on original socket. */int new_sock;new_sock = accept(listen_sock, NULL, NULL);if (new_sock < 0) {perror("accept error");exit(EXIT_FAILURE);}request_add(1);//set_block_filedes_timeout(new_sock);FD_SET(new_sock, &active_fd_set);} else {if (0 == server_read(index)) {server_write(index);}close(index);FD_CLR(index, &active_fd_set);}}}}}}return 0;}
        default中才是我们程序的重点。

        我们使用一个for循环遍历每个socket。如果该socket通过FD_ISSET宏判断不处于我们关注的可读事件fd_set中,则忽略它。

        如果处在可读fd_set中,则看看其是否是监听socket。
        如果是监听socket,则使用accpet方法获取接入的socket。并使用request_add让请求数量加一。还要使用FD_SET宏将该socket加入到活动状态的fd_set中。之后该活动状态的fd_set将被赋值给需要关注可读事件的fd_set中。

        如果不是监听socket,则是接入的socket。于是我们调用《朴素、Select、Poll和Epoll网络编程模型实现和分析——朴素模型》一文中介绍的server_read和server_write方法读取内容并回包。最后我们还要关闭socket,并使用FD_CLR宏将该socket从活动状态的fd_set中去掉。之后的select函数将不会在关注该socket了。

        整个过程非常简单。但是这其中却包含了很多值得思考的问题。

        首先我抛出一个问题,我在default中使用了一个从0到FD_SETSIZE的遍历行为。并且将遍历的游标——index作为socket去操作——使用server_read和server_write去读取。于是问题就来了,使用make_socket创建的socket值和使用accept接收到的socket的值怎么和游标产生关联?代码中似乎没有任何让它们产生关联的逻辑,而且它们的关系是严格的“相等”的关系!那么只有一个假设,就是make_socket和accept返回的socket值在FD_SETSIZE和0之间。但是目前我没有找到文档对这个问题进行说明,而我也没深入研究这两个函数考证到其值就是在这个范围之内,那么为什么我还要这么去用呢?

        我们先记下这个问题,深入到linux的源码中取解释这个使用的正确性。

        我们先看下fd_set的定义

/* The fd_set member is required to be an array of longs.  */typedef long int __fd_mask;/* Some versions of <linux/posix_types.h> define this macros.  */#undef__NFDBITS/* It's easier to assume 8-bit bytes than to get CHAR_BIT.  */#define __NFDBITS(8 * (int) sizeof (__fd_mask))#define__FD_ELT(d)((d) / __NFDBITS)#define__FD_MASK(d)((__fd_mask) 1 << ((d) % __NFDBITS))/* fd_set for select and pselect.  */typedef struct  {    /* XPG4.2 requires this member name.  Otherwise avoid the name       from the global namespace.  */#ifdef __USE_XOPEN    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];# define __FDS_BITS(set) ((set)->fds_bits)#else    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];# define __FDS_BITS(set) ((set)->__fds_bits)#endif  } fd_set;/* Maximum number of file descriptors in `fd_set'.  */#defineFD_SETSIZE__FD_SETSIZE

        以上我是在我ubuntu系统的/usr/include/x86_64-linux-gnu/sys/select.h文件中找到的定义。我们看到fd_set的主体就是一个long int型数组__fds_bits。该数组的个数是两个数的商。被除数__FD_SETSIZE就是我们程序中使用的FD_SETSIZE,也就是1024。除数__NFDBITS是64。于是fd_set中数组元素的个数是1024/64=16。注意一下这个值是16,而我们程序中关注的socket的最大个数是FD_SETSIZE——1024,这是为什么?其实这就是该结构设计的一个精妙之处。fd_set的__fds_bits是一个16个元素的long int型数组,其总长度就是16*64=1024位。于是可以使用每一位表示一个socket。

        我们到/usr/include/x86_64-linux-gnu/bits/select.h 文件中看看linux是如何让socket和这个空间中每一位进行对应的。我们查看FD_SET宏

#define __FD_SET(d, set) \  ((void) (__FDS_BITS (set)[__FD_ELT (d)] |= __FD_MASK (d)))
        __FDS_BITS宏定义在fd_set定义中。它就是返回fd_set的__fds_bits数组首地址。数组的游标是通过__FD_ELT对socket进行处理的结果。__FD_ELT在上面我们已经见过,它是对socket值除以__NFBITS——64的值。通过游标我们取到数组元素值之后,我们再用其与__FD_MASK对socket进行操作的值进行或操作。__FD_MASK的定义也在上面给出,它是将socket的值与__NFBITS——64相除,取得余数,然后让1左移该余数次。这样我们就将该socket映射到fd_set内存的一位中。我们知道,只要在知道除数、商和余数的情况下,可以很方便的推算出被除数是多少。可以说linux内核对这块的设计真是做到了极致,不浪费一点点空间。

        有了上面的认识,我们就知道select模型最大只能支持FD_SETSIZE个数的socket,而且socket的值也只能在FD_SETSIZE之内。如果socket()或accept()函数返回的socket值大于FD_SETSIZE,则select模型将出现错误——上面的计算将溢出。基于这种反向推理,我们可以放心大胆的使用0到FD_SETSIZE的值去当socket的值去计算。我看网上有很多select例子需要使用一个数组去维护接入的socket,如果在不考虑效率的前提下是不必要的。但是如果你追求极致的Select模型性能,还是建议使用一个数组去维护Socket,这样不至于出现大量的浪费操作。这块分析我们将在后文中给出。

        这儿再多言一句,正是因为这种位操作,我们才需要在使用fd_set之前调用FD_ZERO去清空所有空间

# if __WORDSIZE == 64#  define __FD_ZERO_STOS "stosq"# else#  define __FD_ZERO_STOS "stosl"# endif# define __FD_ZERO(fdsp) \  do {      \    int __d0, __d1;      \    __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS      \  : "=c" (__d0), "=D" (__d1)      \  : "a" (0), "0" (sizeof (fd_set)      \  / sizeof (__fd_mask)),      \    "1" (&__FDS_BITS (fdsp)[0])      \  : "memory");      \  } while (0)
        如果socket不再需要监测,则我们使用__FD_CLR在fd_set中去除其对应的位

#define __FD_CLR(d, set) \  ((void) (__FDS_BITS (set)[__FD_ELT (d)] &= ~__FD_MASK (d)))
        我们再来看下Select模型的处理能力。我们采用和《朴素、Select、Poll和Epoll网络编程模型实现和分析——朴素模型》一文中相同的环境和压力,看下服务器的数据输出

        再看下客户端的输出


        可见当前环境下,select模型的处理能力大概是每秒7000多连接。(和下一章介绍的Poll模型差距不大,而且如果使用数组维护Socket还可以提高性能)

0 0
原创粉丝点击