我们有一台SUN服务器,因为工作需要,要求服务器上运行按自己需求实现的网络服务程序,但又不希望服务程序一直运行着,占用太多的系统资源。为了设计这个服务程序,我们试用了几种方法,最后发现利用UNIX提供的inetd的守护进程设计服务程序,程序最简单,占用资源最少,运行最可靠。当然,在设计这种服务程序的时候,也需要一些小技巧。下面我们将给出一个简单的例子,把设计这种服务程序的方法介绍给大家。这种方法同样适用于在Linux系统中实现由inetd启动的服务程序。
一、Inetd的工作原理
在设计服务程序之前,我们首先得弄清楚inetd守护进程到底是做什么的,又是怎么工作的。Inetd是一个特殊的守护进程,它监听各种网络请求,当请求到来时启动实际的服务进程,也就是说,它完成任何服务进程都需要做的一些公共的工作,如监听、网络连接、生成对应某个连接的实际服务进程等,使得实际服务进程只做同特定连接有关的具体工作。
Inetd工作过程如下:
1.inetd 启动时,从配置文件/etc/inetd.conf中读入一行,取得服务名及其相关信息,并根据服务所指定使用的协议调用socket()函数生成相应的套接字;
2.从文件/etc/services查找服务所使用的端口号,调用函数bind()将套接字绑定到所获得的端口;
3.若服务使用TCP协议,调用listen()函数,否则直接跳到步骤4;
4.是否为文件/etc/inetd.conf中所定义的服务都生成了套接字,若没有转步骤1;否则往下执行步骤5;
5.调用select()监听,一旦TCP类型的套接字收到连接请求或是UDP类型的套接字有数据到达,select()函数返回;对于TCP套接字,函数accept()返回一个新的套接字和客户端建立连接,而UDP类型的套接字则直接使用一开始生成的套接字;
6.调用fork()函数生成子进程;
7.若为服务使用TCP协议,其服务标志是“nowait”,父进程在关闭连接套接字后直接返回步骤5继续监听;若使用UDP 协议,则其服务标志为“wait”,父进程需要等待子进程结束后才能返回。在父进程运行的同时,子进程生成后关闭除连接套接字外所有的文件;
8.子进程调用三次dup2()函数分别把套接字复制到文件描述符为0(标准输入)、1(标准输出)、2(标准错误)的文件,然后关闭套接字。这样,子进程只打开三个文件:0、1、2;
9.若用户名不是root,需设置用户组号和用户号,使子进程在一定的用户权限下运行;
10.调用exec()函数用相应的服务程序取代子进程,这样服务程序继承了子进程的运行环境。
上述步骤中,1~4完成inetd的初始化和设置,5实现监听,6~9完成实际服务进程启动前的准备工作,10在准备好的运行环境中启动实际服务进程。
二、程序设计
了解inetd的工作原理后,就可以着手进行程序设计了。
服务程序和客户程序是通过一系列约定进行通讯的,在设计服务程序之前,必须首先弄清楚:服务程序与客户端通过什么协议哪个端口进行通讯;服务程序为客户端完成什么功能;客户端需要传递给服务端什么数据;服务端回送客户端什么数据;数据以什么格式进行交换;出现异常如何处理;服务程序何时停止服务等。
在我们即将给出的例子中,客户程序同服务程序之间使用TCP协议6234端口(选择端口号时注意不要同已有的服务冲突)进行数据交换;服务程序为客户程序计算两个数相加的结果;客户端以字符串方式把两个使用制表符分隔的整数传递给服务端;服务端把计算结果以字符串方式返回;如果出现数据或传输错误,服务端立即终止服务;正常情况下,当客户端终止时,服务端停止服务。
2.1 客户程序设计
客户端例子程序client_of.c(见程序清单)与一个一般的socket客户程序没有什么两样,就不再多做解释。这个程序根据《UNIX网络实用编程技术》一书中例子mulprocli.c改动后得到,它的主要流程是:
1. 参数判定,正确则继续,否则给出用法提示后终止;
2. 套接字生成,正确则继续,否则终止;
3. 查找服务所在主机是否存在,存在则继续,否则终止;
4. 连接指定的服务端端口,成功则继续,否则终止;
5. 提示用户输入数据;
6. 准备数据,并发送给服务端;
7. 等待返回数据;
8. 返回数据出错则终止,否则继续;
9. 打印返回结果;
10. 提示用户是否继续,是则转5,否则终止。
一般说来,客户端程序必须进行前4步的工作,后6步的工作也基本上大同小异。数据传输一般使用字符ASCII方式,这种方式可以避免网络上不同类型的计算机取数据时产生歧义。
程序编写完毕,使用SUN的编译命令:cc client_of.c -lsocket -lnsl -o client_of就可以将程序源代码编译为可执行程序。
2.2 服务程序设计
下面我们开始设计为上述客户程序提供服务的服务程序mulsrvd.c(见程序清单)。作为后台运行的服务程序,需要注意的问题是必须同终端脱离,不能向终端输出任何消息。
下面,让我们来看看这个服务程序的具体流程:
1. 使用openlog(argv[0], LOG_PID, LOG_DAEMON)调用打开日志文件;
这条语句中的第一个参数是个字符串,这个字符串将加到每条登记消息的前面,此处是本服务程序的名字;第二个参数是一些选项的组合,用以指明登记消息的方式,LOG_PID选项指明登记每条消息的进程PID号;第三个参数指明发送消息进程的类型,LOG_DAEMON选项指明是系统守护进程。
那么,为什么要使用日志呢?这是由服务程序的特殊性决定的,服务程序在后台运行,没有终端显示,没有人机界面,不能把数据和错误消息输出到标准输出,所以必须使用日志文件来记录一些必要的信息。
2. 从0号端口读入数据,若有数据且正确则继续,否则转7;
3. 从接收到的字符串中取出两个整数,计算结果;
4. 将计算过程写入日志;
5. 把计算结果转为字符串,并写入1号端口;
6. 转2;
7. 关闭日志和端口;
8. 若客户端终止则正常终止,否则异常终止。
使用SUN编译命令:cc mulsrvd.c -lsocket -lnsl -o mulsrvd即可将服务程序源代码编译为可执行程序。
读者可能很奇怪,因为从这个服务程序里看不到任何有关网络的语句,只有从标准输入0读和向标准输出1写的语句,很象一个单机运行程序。的确如此,读者甚至可以在终端直接运行这个程序:
$ mulsrvd
23 56
7912 45
57^C$
输入以制表符分隔的两个整数23和56,则终端输出79然后等待输入,再输入以制表符分隔的两个整数12和45,则终端输出57,由于该程序不能自行终止,只好用Ctrl-C终止。一切都很象一个单机运行的程序。
这就是使用inetd设计服务程序的巧妙之处,在第1节中inted工作步骤8就是关键,通过这一步骤,由inetd启动的服务进程中所有有关标准输入、标准输出和标准错误的操作都被转移到已连接的套接字上,也就是说从标准输入读数据实际是从套接字读数据,向标准输出写数据实际是向套接字写数据。这样,当实际服务进程从标准输入0读数据时,实际上是从套接字读取数据;把数据输出到标准输出1时,实际是向套接字写入数据。
通过上述方法,inetd解决了不同协议不同端口号的服务进程统一实现的问题,实际服务程序的设计只要解决标准输入、标准输出就可以实现数据的网络传输,而客户进程也通过已连接的套接字实现了数据的正确发送、服务结果的正确接收。
这种解决方法带来的另一个好处是服务程序的设计和调试变得十分简单,只要按照服务约定,从标准输入、输出读写数据就可以对程序进行单机调试了。这是因为当服务程序由用户从终端直接启动时,有关标准输入、标准输出和标准错误的操作并没有事先被转移,依旧从标准输入、标准输出读写数据。
客户程序和服务程序都已设计编译好了,下一步的工作就是通知inetd守护程序,以便在客户端提出请求时启动服务程序。
三、配置
inetd通过两个配置文件获取服务信息,为使服务能够正确启动,我们还需要完成一些配置工作。
以root登录, 修改文件/etc/inetd.conf,在文件尾加入下面一行,项与项之间用制表符分隔:
mulsrvd stream tcp nowait root /home/test/mulsrvd mulsrvd
这一行指定了启动服务所需要的全部参数。第一项含义为服务名称,根据这个名称到/etc/services文件中查找服务端口;第二项指明服务使用的套接字类型;第三项指明服务所使用的协议,此处为TCP协议;第四项指明服务进程的父进程是否等待服务结束才可返回,此处为不必等待;第五项指明以哪个用户启动服务进程;第六项指明服务程序所在的完整路径;第七项指明运行服务的命令行参数。
接下来修改文件/etc/services,在文件尾加入下面一行,项与项之间用制表符分隔:
mulsrvd 6234/tcp # Test Daemon
这一行指定服务使用的协议和端口号。第一项是服务名称,这个名称同文件/etc/inetd.conf中相应的服务名称一致;第二项表明该服务程序使用的协议和端口号,此处使用TCP协议,端口为6234;以#号开始的字符串是注释语句。
到此为止,配置完毕,下一步需要使配置生效。有两种方法,一种是向inetd发SIGHUP信号通知它重新读入配置文件,具体命令是“kill –s SIGHUP <inetd的进程号>”;另一种是重启动。
四、运行
下面让我们来看看运行结果。把服务程序放在SUN服务器t1234上,配置使之生效,然后在另一台SUN工作站上运行客户端,得到下述结果:
$ client_of t1234
Connected to host t1234
Enter an integer:23
Enter another integer:45
23+45=68
Would you like to continue(1:continue, 0:end):1
Enter an integer:2222
Enter another integer:33
2222+33=2255
Would you like to continue(1:continue, 0:end):0
$
查看服务器运行日志/var/adm/messages可以找到下述内容:
Aug 15 15:12:16 t1234 mulsrvd[5902]:
Aug 15 15:12:16 t1234 23+45=68
Aug 15 15:12:22 t1234 mulsrvd[5902]:
Aug 15 15:12:22 t1234 2222+33=2255
如果读者想设计自己的服务程序,只要把客户程序和服务程序中的具体处理过程做一下改动就可以了。
另外,读者还可以把同样的源程序搬到Linux系统上,将源程序用“gcc mulsrvd.c –o mulsrvd”和“gcc client_of –o client_of”命令分别编译,并按本文步骤依次操作,就可以得到一个运行于Linux上的由inetd启动的服务程序了。
大家不妨试一试。
五、程序清单
客户端程序client_of.c:
*******************************************************************************
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#define BUFLEN 255 /* 缓冲区长度 */
#define SOCKADDR struct sockaddr
#define SERVER_PORT 6234 /* 服务所在的端口 */
int main(int argc, char** argv)
{
struct sockaddr_in servaddr;
int sockfd, n, flag;
int num1, num2;
char buffer[BUFLEN];
char errmsg[] = "Server does not function. /n";
struct hostent *hp;
if( argc != 2 ) /* 检查命令行参数 */
{
printf("Usage: %s IP_ADDRESS/n", argv[0]);
exit(0);
};
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) /* 生成套接字 */
{
printf("socket creating error!/n");
exit(1);
};
memset(&servaddr, 0, sizeof(struct sockaddr_in));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERVER_PORT);
hp = gethostbyname(argv[1]); /* 获取服务所在主机信息 */
if (hp == NULL) {
(void) printf("host information for %s not found/n", argv[1]);
exit (3);
};
bcopy(hp->h_addr, &servaddr.sin_addr, hp->h_length);
/* 连接到服务端口 */
if(connect(sockfd, (struct sockaddr *)(&servaddr), sizeof(struct sockaddr_in)) <0 )
{
printf("Connection Failure!/n");
exit(3);
};
printf("Connected to host %s/n", argv[1]); /*打印连接信息*/
flag = 1; /* 设置程序结束标志 */
while(flag)
{
/* 提示并获取用户输入的两个整数 */
printf("Enter an integer:");
scanf("%d", &num1);
printf("Enter another integer:");
scanf("%d", &num2);
sprintf(buffer, "%d/t%d", num1, num2); /*两个整数以制表符分隔写入字符串*/
write(sockfd, buffer, strlen(buffer)); /* 通过套接字将字符串发送给服务端 */
if((n = read(sockfd, buffer, BUFLEN)) <0) /* 通过套接字从服务端接收字符串 */
{
perror("read error /n");
exit(3);
};
buffer[n] = '/0';
printf("/n%d+%d=%d", num1, num2, atoi(buffer)); /*打印结果*/
printf("/n Would you like to continue(1:continue, 0:end):"); /*提问是否继续*/
scanf("%d",&flag); /* 获取用户是否继续的回答 */
};
return 0;
}
*******************************************************************************
服务端程序mulsrvd.c:
*******************************************************************************
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <string.h>
#include <syslog.h>
#define BUFLEN 255 /* 同客户端一致的缓冲区长度 */
int main(int argc, char** argv)
{
char buffer[BUFLEN], *ptr;
int n;
int num1, num2, result;
struct sockaddr cliaddr;
socklen_t len;
openlog(argv[0], LOG_PID, LOG_DAEMON); /*打开日志*/
while( (n = read(0, buffer, BUFLEN)) > 1 ) /* 接收客户端输入 */
{
/* 分析客户端数据,提取两个整数,并计算 */
buffer[n] = '/0';
ptr = strchr(buffer, '/t'); /* 定位两个整数之间的制表符 */
*ptr = '/0';
num1 = atoi(buffer); /* 取第一个整数 */
ptr++;
num2 = atoi(ptr); /* 取第二个整数 */
result = num1 + num2;
/*将计算过程记入日志*/
syslog(LOG_ERR|LOG_USER, "/n%d+%d=%d", num1, num2, result);
sprintf(buffer, "%d", result); /*将计算结果写入字符串*/
write(1, buffer, strlen(buffer)); /* 把结果字符串返回客户端 */
};
closelog(); /*关闭日志*/
/*关闭各端口*/
close(0);
close(1);
close(2);
/*根据客户端情况以不同方式终止本程序*/
if(n==0)
exit(0);
else
exit(4);
}