这里介绍了如果构造一个单线程服务器,使用异步IO以便在多个连接上提供表面上的并发性。本文将扩展这个概念,展示一个单线程服务器如何可以适用于多个传输协议。
多协议服务器的动机
在大多数情况下,一个给定的服务器处理针对一个服务的请求,这些请求是通过一个传输协议发来的。比如,一个提供DAYTIME服务的计算机系统往往是要运行两个服务器,一个服务器处理来自UDP的请求,而另一个服务器则处理来自TCP的请求。
为每个协议使用一组单独服务器的主要优点是便于控制:系统管理员可以通过控制系统所运行的服务器来很容易的控制计算机所提供的协议。
每种协议使用一个服务器的主要缺点:
- 重复,而且如果出错或者版本升级一次就要该两遍。
- 资源使用:多个服务器进程/线程不必要的消耗了进程表的很多项目以及其他的系统资源。
多协议服务器的设计
一个多协议服务器由一个单线程构成,这个线程即可以在TCP也可以在UDP之上使用异步IO来进行通信。
- 服务器最初打开两个套接字:一个使用无连接的传输(UDP),一个使用面向连接的传输(TCP)。
- 接着,服务器使用异步IO等待两个套接字之一就绪。
- 如果TCP套接字就绪,就说明客户请求了一个TCP连接,服务器就使用accept获得新的连接,并在这个连接上与客户通信
- 如果UDP套接字就绪,就说明客户以UDP数据报的形式发来一个请求。服务器就用recvfrom读取这个请求,并记录此发送点的地址。当服务器计算出响应后,服务器用sendto将响应发回给客户
如下图表示了一个循环的、多协议服务器的进程结构。一个单执行线程接收来自多种传输协议的请求。
在任何时候,一个循环的、多协议的服务器最多打开三个套接字。最初,服务器打开一个UDP套接字以接收UDP传入数据报,第二个套接字接收传入的TCP连接请求。
- 当一个数据报到达UDP套接字后,服务器计算出响应,并通过同一个套接字将其发回给客户。
- 当一个TCP连接请求到达时,服务器使用accept获得这个新的连接。accept为这个新连接创建第三个套接字,服务器使用这个新套接字与客户通信。一旦交互结束,服务器将关闭第三个套接字,并等待另两个套接字被激活
多协议DAYTIME
服务器的设计实现
下面程序中有一个线程构成,这个线程可以同时为UDP和TCP提供DAYTIME服务
/* daytimed.c - main */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <time.h>
#include <poll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <error.h>
int daytime(char buf[]);
int errexit(const char *format, ...);
int passiveTCP(const char *service, int qlen);
int passiveUDP(const char *service);
#define MAX(x, y) ((x) > (y) ? (x) : (y))
#define QLEN 32
#define LINELEN 128
/*------------------------------------------------------------------------
* main - Iterative server for DAYTIME service
*------------------------------------------------------------------------
*/
int main(int argc, char *argv[])
{
char *service = "daytime"; /* service name or port number */
char buf[LINELEN+1]; /* buffer for one line of text */
struct sockaddr_in fsin; /* the request from address */
unsigned int alen; /* from-address length */
int tsock; /* TCP master socket */
int usock; /* UDP socket */
int nfds;
fd_set rfds; /* readable file descriptors */
switch (argc) {
case 1:
break;
case 2:
service = argv[1];
break;
default:
errexit("usage: daytimed [port]\n");
}
tsock = passiveTCP(service, QLEN);
usock = passiveUDP(service);
nfds = MAX(tsock, usock) + 1; /* bit number of max fd */
FD_ZERO(&rfds);
while (1) {
FD_SET(tsock, &rfds);
FD_SET(usock, &rfds);
if (select(nfds, &rfds, (fd_set *)0, (fd_set *)0,
(struct timeval *)0) < 0)
errexit("select error: %s\n", strerror(errno));
if (FD_ISSET(tsock, &rfds)) {
int ssock; /* TCP slave socket */
alen = sizeof(fsin);
ssock = accept(tsock, (struct sockaddr *)&fsin,
&alen);
if (ssock < 0)
errexit("accept failed: %s\n",
strerror(errno));
daytime(buf);
(void) write(ssock, buf, strlen(buf));
(void) close(ssock);
}
if (FD_ISSET(usock, &rfds)) {
alen = sizeof(fsin);
if (recvfrom(usock, buf, sizeof(buf), 0,
(struct sockaddr *)&fsin, &alen) < 0)
errexit("recvfrom: %s\n",
strerror(errno));
daytime(buf);
(void) sendto(usock, buf, strlen(buf), 0,
(struct sockaddr *)&fsin, sizeof(fsin));
}
}
}
/*------------------------------------------------------------------------
* daytime - fill the given buffer with the time of day
*------------------------------------------------------------------------
*/
int daytime(char buf[])
{
time_t now;
(void) time(&now);
sprintf(buf, "%s", ctime(&now));
}
daytimed有一个可选的参数,它允许用户指明服务名或者协议端口号。如果用户没有提供这个参数,daytimed使用服务daytime的端口号
在分析参数之后,daytimed调用passiveTCP和passiveUDP创建两个被动套接字,它们分别使用UDP和TCP。这两个套接字使用相同的服务,并且对大多数服务来说,它们都使用相同的协议端口。可以认为这两个是主套接字。服务器一直使它们打开着,所有来自客户的最初的联系都要通过这两者之一来进行。对passiveTCP的调用要指明系统必须使连接请求排队的长度能够达到QLEN。
服务器创建主套接字之后,就准备使用select,以便等待其中之一或两者同时IO准确就绪。首先,它把变量nfds设置为两个套接字中较大的那个,以此作为描述符比特屏蔽码中的索引,它还把比特屏蔽码(rfds)清零。接着,服务器进入到一个无限循环中。在每次循环中,它使用宏FD_SET构造比特屏蔽码,其置1的比特对应于两个主套接字的描述符。接着便使用select等待着二者之一的输入激活
当select调用返回时,就说明主套接字之一或者两者都就绪了。服务器使用宏FD_ISSET检测TCP套接字和UDP套接字。服务器必须对这两个套接字都进行检查,因为如果UDP数据报恰巧与TCP连接同时到达,这两个套接字都将就绪
如TCP套接字就绪,就意味着某个客户发起了一个连接请求。服务器使用accept建立这个连接。accept返回一个新的、临时的、只用于新连接的套接字描述符。服务器调用daytime计算响应,使用write将这个响应通过新连接发送出去,之后,使用close终止连接并释放资源。
如UDP准备就绪,就意味着某个客户发送了一个数据报来获取DAYTIME响应。服务器调用recvfrom读取传入数据报,并记录下客户的端点地址。它也使用过程daytime计算响应,之后便调用sendto将响应发回给客户。因为对所有的通信,服务器都使用主UDP套接字,所以它在发送完UDP响应后并不调用close
并发多协议服务器
这里以及本文的多协议DAYTIME服务器都是使用一种循环的方法来处理请求。之所以采用这种循环的方案,是因为:对于每个请求,DAYTIME服务所执行的计算是很少的,这样,一种循环的服务器就足够了。
如果每个请求要求更多的计算,这时就不能采用循环实现了。此时,这种多协议设计可以扩展为并发的处理请求。在最简单的情况下,一个多协议的服务器可以创建一个新的线程或者进程,以便并发的处理每个TCP连接,同时,它还循环的处理UDP的请求。当然,多协议设计也可以扩展成这里的实现方法,这种实现为各个请求提供表面上的并发性,这些请求来自TCP连接或者UDP数据报
总结
多协议服务器运行设计者将某个给定服务的所有代码封装到一个程序里,这样就消除了重复,并且也更容易协调各种变化。这种多协议服务器由一个单线程过程,这个线程为UDP和TCP打开主套接字,并且使用select等待二者之一或者两个套接字就绪。如果TCP套接字就绪,服务器就接受这个新的连接并处理使用此连接的请求。如果UDP套接字就绪,服务器就读取请求并响应
另外,多协议服务器可以扩展为允许并发处理TCP连接,更重要的是,它可以扩展成(表面上)并发的处理请求。
多协议服务器使用一个单线程来计算服务的响应,消除了代码的重复,而且多协议服务器对系统资源的要求比多个单独的服务器要少