当前位置:文档之家› Proxy源代码分析--谈谈如何学习linux网络编程

Proxy源代码分析--谈谈如何学习linux网络编程

Proxy源代码分析--谈谈如何学习linux网络编程
Proxy源代码分析--谈谈如何学习linux网络编程

Linux是一个可靠性非常高的操作系统,但是所有用过Linux的朋友都会感觉到,Linux和Windows这样的"傻瓜"操作系统(这里丝毫没有贬低Windows的意思,相反这应该是Windows的优点)相比,后者无疑在易操作性上更胜一筹。但是为什么又有那么多的爱好者钟情于Linux呢,当然自由是最吸引人的一点,另外Linux强大的功能也是一个非常重要的原因,尤其是Linux强大的网络功能更是引人注目。放眼今天的WAP业务、银行网络业务和曾经红透半边天的电子商务,都越来越倚重基于Linux的解决方案。因此Linux网络编程是非常重要的,而且当我们一接触到Linux网络编程,我们就会发现这是一件非常有意思的事情,因为以前一些关于网络通信概念似是而非的地方,在这一段段代码面前马上就豁然开朗了。在刚开始学习编程的时候总是让人感觉有点理不清头绪,不过只要多读几段代码,很快我们就能体会到其中的乐趣了。下面我就从一段Proxy源代码开始,谈谈如何进行Linux网络编程。

首先声明,这段源代码不是我编写的,让我们感谢这位名叫Carl Harris的大虾,是他编写了这段代码并将其散播到网上供大家学习讨论。这段代码虽然只是描述了最简单的proxy操作,但它的确是经典,它不仅清晰地描述了客户机/服务器系统的概念,而且几乎包括了Linux网络编程的方方面面,非常适合Linux网络编程的初学者学习。

这段Proxy程序的用法是这样的,我们可以使用这个proxy登录其它主机的服务端口。假如编译后生成了名为Proxy的可执行文件,那么命令及其参数的描述为:

./Proxy

其中参数proxy_port是指由我们指定的代理服务器端口。参数remote_host是指我们希望连接的远程主机的主机名,IP地址也同样有效。这个主机名在网络上应该是唯一的,如果您不确定的话,可以在远程主机上使用uname -n命令查看一下。参数service_port是远程主机可提供的服务名,也可直接键入服务对应的端口号。这个命令的相应操作是将代理服务器的proxy_port端口绑定到remote_host的

service_port端口。然后我们就可以通过代理服务器的proxy_port端口访问remote_host了。例如一台计算机,网络主机名是legends,IP地址为10.10.8.221,如果在我的计算机上执行:

[root@lee /root]#./proxy 8000 legends telnet

那么我们就可以通过下面这条命令访问legends的telnet端口。

-----------------------------------------------------------------

[root@lee /root]#telnet legends 8000

Trying 10.10.8.221...

Connected to legends(10.10.8.221).

Escape character is '^]'

Red Hat Linux release 6.2(Zoot)

Kernel 2.2.14-5.0 on an i686

Login:

-----------------------------------------------------------------

上面的绑定操作也可以使用下面的命令:

[root@lee /root]#./proxy 8000 10.10.8.221 23

23是telnet服务的标准端口号,其它服务的对应端口号我们可以在/etc/services中查看。

下面我就从这段代码出发谈谈我对Linux网络编程的一些粗浅的认识,不对的地方还请各位大虾多多批评指正。

◆main()函数

-----------------------------------------------------------------

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define TCP_PROTO "tcp"

int proxy_port; /* port to listen for proxy connections on */

struct sockaddr_in hostaddr; /* host addr assembled from gethostbyname() */

extern int errno; /* defined by libc.a */

extern char *sys_myerrlist[];

void parse_args (int argc, char **argv);

void daemonize (int servfd);

void do_proxy (int usersockfd);

void reap_status (void);

void errorout (char *msg);

/*This is my modification.

I'll tell you why we must do this later*/

typedef void Signal(int);

/****************************************************************

function: main

description: Main level driver. After daemonizing the process, a socket is opened to listen for connections on the proxy port, connections are accepted and children are spawned to handle each new connection.

arguments: argc,argv you know what those are.

return value: none.

calls: parse_args, do_proxy.

globals: reads proxy_port.

****************************************************************/

main (argc,argv)

int argc;

char **argv;

{

int clilen;

int childpid;

int sockfd, newsockfd;

struct sockaddr_in servaddr, cliaddr;

parse_args(argc,argv);

/* prepare an address struct to listen for connections */

bzero((char *) &servaddr, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

servaddr.sin_port = proxy_port;

/* get a socket... */

if ((sockfd = socket(AF_INET,SOCK_STREAM,0)) < 0) {

fputs("failed to create server socket\r\n",stderr);

exit(1);

}

/* ...and bind our address and port to it */

if (bind(sockfd,(struct sockaddr_in *) &servaddr,sizeof(servaddr)) < 0) { fputs("faild to bind server socket to specified port\r\n",stderr);

exit(1);

}

/* get ready to accept with at most 5 clients waiting to connect */

listen(sockfd,5);

/* turn ourselves into a daemon */

daemonize(sockfd);

/* fall into a loop to accept new connections and spawn children */

while (1) {

/* accept the next connection */

clilen = sizeof(cliaddr);

newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);

if (newsockfd < 0 && errno == EINTR)

continue;

/* a signal might interrupt our accept() call */

else if (newsockfd < 0)

/* something quite amiss -- kill the server */

errorout("failed to accept connection");

/* fork a child to handle this connection */

if ((childpid = fork()) == 0) {

close(sockfd);

do_proxy(newsockfd);

exit(0);

}

/* if fork() failed, the connection is silently dropped -- oops! */

lose(newsockfd);

}

}

-----------------------------------------------------------------

上面就是Proxy源代码的主程序部分,也许您在网上也曾经看到过这段代码,不过细心的您会发现在上面这段代码中我修改了两个地方,都是在预编译部分。一个地方是在定义外部字符型指针数组时,我将原代码中的

extern char *sys_errlist[];

修改为

extern char *sys_myerrlist[];原因是在我的Linux环境下头文件"stdio.h"已经对sys_errlist[]进行了如下定义:

extern __const char *__const sys_errlist[];

也许Carl Harris在94年编写这段代码时系统还没有定义sys_errlist[],不过现在我们不修改一下的话,编译时系统就会告诉我们sys_errlist发生了定义冲突。

另外我添加了一个函数类型定义:

typedef void Sigfunc(int);

具体原因我将在后面向大家解释。

套接字和套接字地址结构定义

这段主程序是一段典型的服务器程序。网络通讯最重要的就是套接字的使用,在程序的一开始就对套接字描述符sockfd和newsockfd进行了定义。接下来定义客户机/服务器的套接字地址结构cliaddr和servaddr,存储客户机/服务器的有关通信信息。然后调用parse_args(argc,argv)函数处理命令参数。关于这个parse_args()函数我们待会儿再做介绍。

创建通信套接字

下面就是建立一个服务器的详细过程。服务器程序的第一个操作是创建一个套接字。这是通过调用函数socket()来实现的。socket()函数的具体描述为:

-----------------------------------------------------------------

#include

#include

int socket(int domain, int type, int protocol);

-----------------------------------------------------------------

参数domain指定套接字使用的协议族,AF_INET表示使用TCP/IP协议族,AF_UNIX表示使用Unix协议族,AF_ISO表示套接字使用ISO协议族。type指定套接字类型,一般的面向连接通信类型(如TCP)设置为SOCK_STREAM,当套接字为数据报类型时,type应设置为SOCK_DGRAM,如果是可以直接访问IP协议的原始套接字则type应设置为SOCK_RAW。参数protocol一般设置为"0",表示使用默认协议。当socket()函数成功执行时,返回一个标志这个套接字的描述符,如果出错则返回"-1",并设置errno为相应的错误类型。

设置服务器套接字地址结构

在通常情况下,首先要将描述服务器信息的套接字地址结构清零,然后在地址结构中填入相应的内容,准备接受客户机送来的连接建立请求。这个清零操作可以用多种字节处理函数来实现,例如bzero()、bcopy()、memset()、memcpy()等,以字母"b"开始的两个函数是和BSD系统兼容的,而后面两个是ANSI C 提供的函数。这段代码中使用的bzero()其描述为:

void bzero(void *s, int n);

函数的具体操作是将参数s指定的内存的前n个字节清零。memset()同样也很常用,其描述为:void *memset(void *s, int c, size_t n);

具体操作是将参数s指定的内存区域的前n个字节设置为参数c的内容。

下一步就是在已经清零的服务器套接字地址结构中填入相应的内容。Linux系统的套接字是一个通用的网络编程接口,它应该支持多种网络通信协议,每一种协议都使用专门为自己定义的套接字地址结构(例如TCP/IP网络的套接字地址结构就是struct sockaddr_in)。不过为了保持套接字函数调用参数的一致性,Linux系统还定义了一种通用的套接字地址结构:

-----------------------------------------------------------------

struct sockaddr

{

unsigned short sa_family; /* address type */

char sa_data[14]; /* protocol address */

}

-----------------------------------------------------------------

其中sa_family意指套接字使用的协议族地址类型,对于我们的TCP/IP网络,其值应该是AF_INET,sa_data中存储具体的协议地址,不同的协议族有不同的地址格式。这个通用的套接字地址结构一般不用做定义具体的实例,但是常用做套接字地址结构的强制类型转换,如我们经常可以看到这样的用法:bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))

用于TCP/IP协议族的套接字地址结构是sockaddr_in,其定义为:

-----------------------------------------------------------------

struct in_addr

{

__u32 s_addr;

};

struct sochaddr_in

{

short int sin_family;

unsigned short int sin_port;

struct in_addr sin_addr;

/*This part has not been taken into use yet*/

nsigned char_ _ pad[_ _ SOCK_SIZE__- sizeof(short int) -sizeof(unsigned short int) - sizeof(struct in_addr)];

};

#define sin_zero_ - pad

-----------------------------------------------------------------

其中sin_zero成员并未使用,它是为了和通用套接字地址struct sockaddr兼容而特意引入的。在编程时,一般都通过bzero()或是memset()将其置零。其他成员的设置一般是这样的:

servaddr.sin_family = AF_INET;

表示套接字使用TCP/IP协议族。

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

设置服务器套接字的IP地址为特殊值INADDR_ANY,这表示服务器愿意接收来自任何网络设备接口的客户机连接。htonl()函数的意思是将主机顺序的字节转换成网络顺序的字节。

servaddr.sin_port = htons(PORT);

设置通信端口号,PORT应该是我们已经定义好的。在本例中servaddr.sin_port = proxy_port;这是表示端口号是函数的返回值proxy_port。

另外需要说明的一点是,在本例中,我们并没有看到在预编译部分中包含有

这两个头文件,那是因为这两个头文件已经分别被包含在中了,而且后面这两个头文件是与平台无关的,所以在网络通信中一般都使用这两个头文件。

服务器公开地址

如果服务器要接受客户机的连接请求,那么它必须先要在整个网络上公开自己的地址。在设置了服务器的套接字地址结构之后,可以通过调用函数bind()绑定服务器的地址和套接字来完成公开地址的操作。函数bind()的详细描述为:

-----------------------------------------------------------------

#include

#include

int bind(int sockfd, struct sockaddr *addr, int addrlen);

-----------------------------------------------------------------

参数sockfd是我们通过调用socket()创建的套接字描述符。参数addr是本机地址,参数addrlen是套接字地址结构的长度。函数执行成功时返回"0",否则返回"-1",并设置errno变量为EADDRINUAER。

如果是服务器调用bind()函数,如果设置了套接字的IP地址为某个本地IP地址,那么这表示服务器只接受来自于这个IP地址的特定主机发出的连接请求。不过一般情况下都是将IP地址设置为INADDR_ANY,以便接受所有网络设备接口送来的连接请求。

客户机一般是不会调用bind()函数的,因为客户机在连接时不用指定自己的套接字地址端口号,系统会自动为客户机选择一个未用端口号,并且用本地IP地址自动填充客户机套接字地址结构中的相应项。但是在某些特定的情况下客户机需要使用特定的端口号,例如Linux中的rlogin命令就要求使用保留端口号,

而系统是不能为客户机自动分配保留端口号的,这就需要调用bind()来绑定一个保留端口号了。不过在一些特殊的环境下,这样绑定特定端口号也会带来一些负面影响,如在HTTP服务器进入TIME_WAIT状态后,客户机如果要求再次与服务器建立连接,则服务器会拒绝这一连接请求。如果客户机最后进入TIME_WAIT 状态,则马上再次执行bind()函数时会返回出错信息"-1",原因是系统会认为同时有两次连接绑定同一个端口。

转换Listening套接字

接下来,服务器需要将我们刚才与IP地址和端口号完成绑定的套接字转换成倾听listening套接字。只有服务器程序才需要执行这一步操作。我们通过调用函数listen()实现这一操作。listen()的详细描述为:

-----------------------------------------------------------------

#include

int listen(int sockfd, int backlog);

-----------------------------------------------------------------

参数sockfd指定我们要求转换的套接字描述符,参数backlog设置请求队列的最大长度。函数listen()主要完成以下操作。

首先是将套接字转换成倾听套接字。因为函数socket()创建的套接字都是主动套接字,所以客户机可以通过调用函数connect()来使用这样的套接字主动和服务器建立连接。而服务器的情况恰恰相反,服务器需要通过套接字接收客户机的连接请求,这就需要一个"被动"套接字。listen()就可将一个尚未连接的主动套接字转换成为这样的"被动"套接字,也就是倾听套接字。在执行了listen()函数之后,服务器的TCP 就由CLOSED变成LISTEN状态了。

另外listen()可以设置连接请求队列的最大长度。虽然参数backlog的用法非常简单,只是一个简单的整数。但搞清楚请求队列的含义对理解TCP协议的通信过程建立非常重要。TCP协议为每个倾听套接字实际上维护两个队列,一个是未完成连接队列,这个队列中的成员都是未完成3次握手的连接;另一个是完成连接队列,这个队列中的成员都是虽然已经完成了3次握手,但是还未被服务器调用accept()接收的连接。参数backlog实际上指定的是这个倾听套接字完成连接队列的最大长度。在本例中我们是这样用的:listen(sockfd,5);表示完成连接队列的最大长度为5。

接收连接

接下来我们在主程序中看到通过名为daemonize()的自定义函数创建一个守护进程,关于这个daemonize()以及守护进程的相关概念,我们等一会儿再做详细介绍。然后服务器程序进入一个无条件循环,用于监听接收客户机的连接请求。在此过程中如果有客户机调用connect()请求连接,那么函数accept()可以从倾听套接字的完成连接队列中接受一个连接请求。如果完成连接队列为空,这个进程就睡眠。accept()的详细描述为:

-----------------------------------------------------------------

#include

int accept(int sockfd, struct sockaddr *addr, int *addrlen);

-----------------------------------------------------------------

参数sockfd是我们转换成功的倾听套接字描述符;参数addr是一个指向套接字地址结构的指针,参数addrlen为一个整型指针。当函数成功执行时,返回3个结果,函数返回一个新的套接字描述符,服务器可以通过这个新的套接字描述符和客户机进行通信。参数addr所指向的套接字地址结构中将存放客户机的相关信息,addrlen指针将描述前述套接字地址结构的长度。在通常情况下服务器对这些信息不是很感兴趣,因此我们经常可以看到一些源代码中将accept()函数的后两个参数都设置为NULL。不过在这段proxy 源代码中需要用到有关的客户机信息,因此我们看到通过执行

newsockfd = accept(sockfd, (struct sockaddr_in *) &cliaddr, &clilen);

将客户机的详细信息存放在地址结构cliaddr中。而proxy就通过套接字newsockfd与客户机进行通信。值得注意的是这个返回的套接字描述符与我们转换的倾听套接字是不同的。在一段服务器程序中,可以始终只用一个倾听套接字来接收多个客户机的连接请求;而如果我们要和客户机建立一个实际的连接的话,对每一个请求我们都需要调用accept()返回一个新的套接字。当服务器处理完毕客户机的请求后,一定要将相应的套接字关闭;如果整个服务器程序将要结束,那么一定要将倾听套接字关闭。

如果accept()函数执行失败,则返回"-1",如果accept()函数阻塞等待客户机调用connect()建立连接,进程在此时恰好捕捉到信号,那么函数在返回"-1"的同时将变量errno的值设置为EINTR。这和accept()函数执行失败是有区别的。因此我们在代码中可以看到这样的语句:

-----------------------------------------------------------------

if (newsockfd < 0 && errno == EINTR)

continue;

/* a signal might interrupt our accept() call */

else if (newsockfd < 0)

/* something quite amiss -- kill the server */

errorout("failed to accept connection");

-----------------------------------------------------------------

可以看出程序在处理这两种情况时操作是完全不同的,同样是accept()返回"-1",如果有errno == EINTR,那么系统将再次调用accept()接受连接请求,否则服务器进程将直接结束。

处理客户机请求

当服务器与客户机建立连接以后,就可以处理客户机的请求了。一般情况下服务器程序都要创建一个子进程用于处理客户机请求;而父进程则继续监听,时刻准备接受其它客户机的连接请求。我们这段proxy 程序也不例外。它通过调用fork()创建处理客户机请求的子进程。我想在linux/Unix编程中,fork()的重要性不用我再多说什么了,在大型的服务器程序中,一般都要在子进程里,根据客户机请求的不同而通过exec()系列函数调用不同的处理程序,这也是在学习linux/Unix编程中一个非常重要的地方。不过我们这个proxy程序旨在讲述一些linux网络编程的基本概念,因此在子程序部分就直接调用了一个完成proxy功能的函数do_proxy(),其实际参数newsockfd就是accept()返回的套接字描述符。另外值得注意的一点就是,因为子进程继承了所有父进程中可用的文件描述符,所以我们必须在子进程中关闭倾听套接字(代码中子进程部分的close(sockfd);),同时在父进程中关闭accept()返回的套接字描述符(例如代码中父进程部分的close(newsockfd);)。

◆函数parse_args()

此函数的定义是:void parse_args (int argc, char **argv);

-----------------------------------------------------------------

/****************************************************************

function: parse_args

description: parse the command line args.

arguments: argc,argv you know what these are.

return value: none.

calls: none.

globals: writes proxy_port, writes hostaddr.

****************************************************************/

void parse_args (argc,argv)

int argc;

char **argv;

{

int i;

struct hostent *hostp;

struct servent *servp;

unsigned long inaddr;

struct {

char proxy_port [16];

char isolated_host [64];

char service_name [32];

} pargs;

if (argc < 4) {

printf("usage: %s \r\n", argv[0]);

exit(1);

}

strcpy(pargs.proxy_port,argv[1]);

strcpy(pargs.isolated_host,argv[2]);

strcpy(pargs.service_name,argv[3]);

for (i = 0; i < strlen(pargs.proxy_port); i++)

if (!isdigit(*(pargs.proxy_port + i)))

break;

if (i == strlen(pargs.proxy_port))

proxy_port = htons(atoi(pargs.proxy_port));

else {

printf("%s: invalid proxy port\r\n",pargs.proxy_port);

exit(0);

}

bzero(&hostaddr,sizeof(hostaddr));

hostaddr.sin_family = AF_INET;

if ((inaddr = inet_addr(pargs.isolated_host)) != INADDR_NONE)

bcopy(&inaddr,&hostaddr.sin_addr,sizeof(inaddr));

else if ((hostp = gethostbyname(pargs.isolated_host)) != NULL)

bcopy(hostp->h_addr,&hostaddr.sin_addr,hostp->h_length);

else {

printf("%s: unknown host\r\n",pargs.isolated_host);

exit(1);

}

if ((servp = getservbyname(pargs.service_name,TCP_PROTO)) != NULL)

hostaddr.sin_port = servp->s_port;

else if (atoi(pargs.service_name) > 0)

hostaddr.sin_port = htons(atoi(pargs.service_name));

else {

printf("%s: invalid/unknown service name or port number\r\n", pargs.service_name);

exit(1);

}

}

-----------------------------------------------------------------

这个函数的作用是传递命令行参数。参数的传递是通过两个全局变量来实现的,这两个变量是int proxy_port和struct sockaddr_in hostaddr。分别用于传递等待连接请求的proxy端口和被绑定的主机网络信息。

检验命令行参数

在进行了局部变量定义以后,函数首先要检测命令行参数是否符合程序的要求,即在命令后紧跟代理服务器端口、远程主机名和服务端口号,如果不满足上述要求,则代理服务器程序结束。如果满足上述要求,则将命令行的这三个参数存储进我们自定义的pargs结构之中。注意pargs结构的三个成员都是以字符形式存放命令行参数信息的,后面我们需要调用函数将这些参数信息都转换成为数字形式的。

传递参数

接下来就要将命令行的三个参数变换成合适的形式赋值给全局变量proxy_port和hostaddr,以供其它函数调用。首先传送代理服务器端口pargs.proxy_port,在这里程序调用了一个系统函数isdigit()检验用户输入的端口号是否有效。isdigit()的具体描述为:

-----------------------------------------------------------------

#include

int isdigit(int c)

-----------------------------------------------------------------

isdigit()函数用来检测参数"c"是否是数字1~9中间的一个,如果答案是肯定的,则返回非"0"值,反之,返回"0"。程序中采用了这样的方法来对用户的输入进行逐位检验:

if (!isdigit(*(pargs.proxy_port + i)))

break;

在将有效端口号传递给全局变量proxy_port之前,还要将其转换成为网络字节顺序。这是因为网络中存在着多个公司的不同设备,这些设备表示数据的字节顺序是不同的。例如在内存地址0x1000处存储一个16位的整数FF11,不同公司的机器在内存中的存储方式也不相同,有的将FF置于内存指针的起始位置

0x1000,11置于0x1001,这称为big-endian顺序;有的却恰恰相反,即little-endian顺序。这种基于主机的数据存储顺序就称为主机字节顺序(host byte order)。为了在不同类型的主机之间进行通信,网络协议就规定了一种统一的网络字节顺序,这种顺序被规定为little-endian顺序。所以数据的网络字节顺序和主机字节顺序有可能是不同的,因此在编写通信程序时一定要注意不同顺序之间的转换。所以,程序中一定要有例程中这样的语句:

proxy_port = htons(atoi(pargs.proxy_port));

函数htons()的作用就是将主机字节顺序转换为网络字节顺序。它的具体描述为:

-----------------------------------------------------------------

#include

unsigned short int htons(unsigned short int data)

-----------------------------------------------------------------

与htons()相似的函数还有三个,它们分别是htonl()、ntohs()和ntohl(),都用于网络与主机字节顺序之间的转换。如果这几个名字比较容易混淆的话,我们可以这样记忆:函数名中的h代表host,n代表network,s代表unsigned short,l代表unsigned long。所以"hton"即为"host-to-network":变换主

机字节为网络字节。接收数据的就要用到"ntoh"("network-to-host")函数了。

在我们的例程中,由于端口号一般情况下最多不会超过4位数字,所以选用unsigned short型的htons()即可。

注意在例程中htons()的参数是另一个函数atoi()的返回结果。atoi()函数的具体描述为:

-----------------------------------------------------------------

#include

int atoi(const char *nptr)

-----------------------------------------------------------------

它的作用是将字符指针nptr指向的字符串转换成相应的整数并将其作为结果返回。这个操作与函数调用strtol(nptr,(char **)NULL,10)的效果几乎完全相同,唯一的区别是atoi()没有出错返回信息。之所以要调用这个函数是因为,系统在读取命令行的时候将所有的参数都作为字符串处理,所以我们必须将其转换为整数形式。

接下来,例程先将全局变量hostaddr的所有成员清零,然后将成员hostaddr.sin_family设置为TCP/IP协议族标志AF_INET。下面就可将命令行的另外两个参数传递给全局变量hostaddr的两个成员hostaddr.sin_port和hostaddr.sin_addr了。这里我们用到了两个局部变量struct hostent *hostp和struct servent *servp来传递参数信息。struct hostent的详细描述为:

-----------------------------------------------------------------

struct hostent {

char *h_name;

char **h_aliases;

int h_addrtype;

int h_length;

char **h_addr_list;

};

#define h_addr h_addrlist[0]

-----------------------------------------------------------------

hostent成员的含义是h_name代表主机在网络上的的正式名称,h_aliases是所有主机别名的列表,h_addrtype是指主机的地址类型,一般设置为TCP/IP协议族AF_INET,h_length是主机的地址长度,一般设置为4个字节。h_addr_list是主机的IP地址列表。

我们要用它来传递我们期望绑定的远程主机名或是IP地址。因为命令行中的主机名参数已经被存储进

pargs.isolated_host,所以我们就调用inet_addr()函数对主机名或主机的IP地址进行二进制和字节顺序转换。inet_addr()函数的描述为:

-----------------------------------------------------------------

#include

#include

#include

unsigned long int inet_addr(const char *cp)

-----------------------------------------------------------------

inet_addr()的作用就是将参数cp指向的Internet主机地址从数字/点的形式转换成二进制形式并同时转换为网络字节顺序,并将转换结果直接返回。如果cp指向的IP地址不可用,则函数返回INADDR_NONE 或"-1"。

虽然Carl Harris在编写这段程序时使用了这个inet_addr()函数,但是我还是建议大家在编写自己的程序时使用另外一个函数inet_aton()来完成这些功能。原因是inet_addr()在IP地址不可用时返回"-1",但我们想想,IP地址255.255.255.255绝对是一个有效地址,那么其二进制返回值也将是"-1",因此inet_addr()无法对这个IP地址进行处理。而函数inet_aton()则采用了一种更好的方法来返回出错信息,它的具体描述为:

-----------------------------------------------------------------

#include

#include

#include

int inet_aton(const char *cp, struct in_addr *inp)

-----------------------------------------------------------------

函数执行成功时返回非零,转换结果存入指针inp指向的in_addr结构。这个结构定义我们在前面的文章里已经介绍过了。如果参数cp指向的IP地址不可用,则返回"0"。这就避免发生inet_addr()那样的问题。

如果说用户在命令行中键入的是远程主机的IP地址,那么只用inet_addr()就算完成任务了,但如果用户键入的是主机域名那该怎么办呢?所以我们在例程中可以看到这样的语句:

-----------------------------------------------------------------

if ((inaddr = inet_addr(pargs.isolated_host)) != INADDR_NONE)

bcopy(&inaddr,&hostaddr.sin_addr,sizeof(inaddr));

else if ((hostp = gethostbyname(pargs.isolated_host)) != NULL)

bcopy(hostp->h_addr,&hostaddr.sin_addr,hostp->h_length);

else {

printf("%s: unknown host\r\n",pargs.isolated_host);

exit(1);

}

-----------------------------------------------------------------

其中gethostbyname()函数就是用来转换主机域名的。它的具体描述为:

-----------------------------------------------------------------

#include

struct hostent *gethostbyname(const char *hostname);

-----------------------------------------------------------------

参数hostname指向我们需要转换的域名地址,函数直接返回转换结果,如果函数执行成功,则结果直接返回到一个指向hostent结构的指针中,否则返回空指针NULL。

例程就是这样调用inet_addr()和gethostbyname()将命令行参数中的主机域名或是主机IP地址传递给全局变量hostaddr的成员sin_addr以便代理执行函数do_proxy()调用。

下面是传递服务名或是服务端口号。这里要用到结构servent做传递中介,struct servent的详细描述为:

-----------------------------------------------------------------

struct servent {

char *s_name;

char **s_aliases;

int s_port;

char *s_proto;

};

-----------------------------------------------------------------

其各成员的含义是s_name为服务的正式名称,如ftp、http等,s_aliases是服务的别名列表,s_port 是服务的端口号,例如在一般情况下ftp的端口号为21,http服务的端口号为80,注意此端口号应该存储为网络字节顺序,s_proto是应用协议的类型。

例程中使用getservbyname()函数转换命令行参数中的服务名,此函数的详细描述为:

-----------------------------------------------------------------

#include

struct servent * getservbyname(const char *servname, const char *protoname);

-----------------------------------------------------------------

它的作用就是转换指针servname指向的服务名为相应的整数表示的端口号,参数protoname表示服务使用的协议,例程中protoname 参数的值为TCP_PROTO,这表示使用TCP协议。函数成功时就返回一个struct servent型的指针,其中的s_port成员就是我们关心的服务端口号。如果用户在命令中键入的是端口号而不是服务名,那么和处理代理端口信息一样,使用下面的语句进行处理:

hostaddr.sin_port = htons(atoi(pargs.service_name));

到这里,命令行的参数已经全部被转换成为网络通信所要求的字节顺序和数字类型,并且存储在三个全局变量中,就等着do_proxy()函数来调用了。

◆daemonize()函数创建守护进程

在对main()函数进行介绍的时候我就提到过,一般服务器程序在接收客户机连接请求之前,都要创建一个守护进程。守护进程是linux/Unix编程中一个非常重要的概念,因为在创建一个守护进程的时候,我们要接触到子进程、进程组、会晤期、信号机制以及文件、目录、控制终端等多个概念,因此详细地讨论一下守护进程,对初学者学习进程间关系是非常有帮助的。下面就是例程中的daemonize()函数:

-----------------------------------------------------------------

/****************************************************************

function: daemonize

description: detach the server process from the current context, creating a pristine, predictable environment in which it will execute.

arguments: servfd file descriptor in use by server.

return value: none.

calls: none.

globals: none.

****************************************************************/

void daemonize (servfd)

int servfd;

{

int childpid, fd, fdtablesize;

/* ignore terminal I/O, stop signals */

signal(SIGTTOU,SIG_IGN);

signal(SIGTTIN,SIG_IGN);

signal(SIGTSTP,SIG_IGN);

/* fork to put us in the background (whether or not the user

specified '&' on the command line */

if ((childpid = fork()) < 0) {

fputs("failed to fork first child\r\n",stderr);

exit(1);

}

else if (childpid > 0)

exit(0); /* terminate parent, continue in child */

/* dissociate from process group */

if (setpgrp(0,getpid())<0) {

fputs("failed to become process group leader\r\n",stderr);

exit(1);

}

/* lose controlling terminal */

if ((fd = open("/dev/tty",O_RDWR)) >= 0) {

ioctl(fd,TIOCNOTTY,NULL);

close(fd);

}

/* close any open file descriptors */

for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) if (fd != servfd)

close(fd);

/* set working directory to allow filesystems to be unmounted */ chdir("/");

/* clear the inherited umask */

umask(0);

/* setup zombie prevention */

signal(SIGCLD,(Sigfunc *)reap_status);

}

-----------------------------------------------------------------

此函数的作用就是创建一个守护进程。在Linux系统中,如果要将一个普通进程转换成为守护进程,必须要执行下面的步骤:

1.调用函数fork()创建子进程,然后父进程终止,保留子进程继续运行。之所以要让父进程终止是因为,当一个进程是以前台进程方式由shell启动时,在父进程终止之后子进程自动转为后台进程。另外,我们在下一步要创建一个新的会晤期,这就要求创建会晤期的进程不是一个进程组的组长进程。当父进程终止,子进程运行,这就保证了进程组的组ID与子进程的进程ID不会相等。

函数fork()的定义为:

-----------------------------------------------------------------

#include

#include

pid_t fork(void);

-----------------------------------------------------------------

该函数被调用一次,但是返回两次,这两次返回的区别是子进程的返回值为"0",而父进程的返回值为子进程的ID。如果出错则返回"-1"。

2.保证进程不会获得任何控制终端。通常的做法是调用函数setsid()创建一个新的会晤期。setsid()的详细描述为:

-----------------------------------------------------------------

#include

#include

pid_t setsid(void);

-----------------------------------------------------------------

第一步的操作已经保证调用此函数的进程不是进程组的组长,那么此函数将创建一个新的会晤,其结

相关主题
文本预览
相关文档 最新文档