Unix网络编程-select模型重写服务器回射函数

Unix网络编程-TCP客户端服务器示例(1)这篇文章给出的tcp服务端程序,把他重写成使用select来处理任意个客户的单进程程序,而不是每个客户派生出一个子进程。在给出具体代码之前,让我们先查看用以跟踪客户的数据结构。下图给出了第一个客户建立连接前服务器的状态。

服务器有单个监听描述符,我们用一个圆点表示。

服务器只维护一个读描述符集,如下所示,假设服务器是在前台启动的,那么描述符0、1、和2将分别被设置为标准输入、标准输出、标准错误输出。可见监听套接字的第一个可用描述符是3。下图展示了一个名为clinet的整型数组,他含有每个客户的已连接套接字描述符。该数组的所有元素都被初始化为-1。

集合中唯一的非0项是表示监听套接字的项,因此select的第一个参数将是4。当第一个客户与服务器建立连接时,监听描述符变为可读,我们的服务器于是调用accept。在本例的假设下,由accept返回的新的已连接描述符将是4。下图展示了从客户到服务器的连接。

从现在起,我们的服务器必须在其client数组中记住每个新的已连接描述符,并把他加到描述符集中去。下面展示了这样更新后的数据结构

稍后,第二个客户与服务器建立连接,下图展示了这种情形

新的已连接描述符假设为5,必须被记住,从而给出如下所示的数据结构。

我们接着假设第一个客户终止他的连接。该客户tcp发送一个FIN,使得服务器中的描述符4变为可读。当服务器读这个已连接套接字时,read将返回0。我们于是关闭该套接字并相应地更新数据结构:把client[0]的值置为-1,把描述符集中描述符4的位设置为0。如下所示,注意maxfd的值没有改变。

总之,当有客户到达时,我们在client数组中的第一个可用项中记录其已经连接套接字的描述符。我们还必须把这个已连接描述符加到读描述符集中。变量maxi是client数组使用的项的最大下标,而变量maxfd(加1之后)是select函数第一个参数的当前值。对于本服务器所能处理的最大客户数目的限制是以下两个值中的较小者:FD_SETSIZE和内核允许本进程打开的最大描述符数。

下面给出重写后的服务端代码

阻塞于select

select等待某个事情发生:或是新客户连接的建立,或是数据、FIN或RST的到达。

accept新的连接

如果监听套接字变为可读,那么已建立了一个新的连接。我们调用accept并相应地更新数据结构,使用client数组中的第一个未用项记录这个已连接描述符。就绪描述符数目减1,若其值变为0,就可以避免进入下一个for循环。

这样做让我们可以使用select的返回值来避免检查未就绪的描述符。

检查现有连接

对于每个现有的客户连接,我们要测试其描述符是否在select返回的描述符中,如果是就该从客户读入一行并回射给它。如果该客户关闭了连接,那么read将返回0,我们于是相应地更新数据结构。

拒绝服务型攻击

不幸的是,我们刚刚给出的服务器程序存在一个问题。考虑一下如果有一个恶意的客户连接到该服务器,发送一个字节的数据(不是换行符)后进入睡眠,将会发生什么。服务器将调用read,它从客户读入这个单字节的数据,然后阻塞于下一个read调用,以等待来自客户的其余数据。服务器于是因为这么一个客户而被阻塞(称它被挂起也许更确切些),不能再为其他任何客户提供服务(不论是接受新的连接还是读取其他现有的客户的数据),直到那个恶意客户端发出一个换行符或者终止为止。

这里有一个基本概念是:当一个服务器在处理多个客户时,他绝对不能阻塞于单个客户相关的某个函数调用。否则可能导致服务器被挂起,拒接为所有其他客户提供服务。这就是所谓的拒绝服务型攻击(denial  of  service)。可以采取的解决办法:

(a) 使用非阻塞式I/O

(b) 让每个客户由单独的控制线程提供服务

(c) 对I/O操作设置一个超时

你可能感兴趣的:(Unix网络编程-select模型重写服务器回射函数)