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

        在《朴素、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'.  */
#define	FD_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 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券