Proxy源代码分析

来源:互联网 发布:五轴数控刀具磨床编程 编辑:程序博客网 时间:2024/05/06 06:47

这段代码虽然只是描述了最简单的 proxy 操作,但它的确是经典,它不仅清晰地描述了客户机/ 服务器系统的概念,而且几乎包括了 Linux 网络编程的方方面面,非常适合 Linux 网络编程的初学者学习。
  这段 Proxy 程序的用法是这样的,我们可以使用这个 proxy 登录其它主机的服务端口。假如编译后生成了名为 Proxy 的可执行文件,那么命令及其参数的描述为:
    ./Proxy <proxy_port> <remote_host> <service_port>
  其中参数 proxy_port 是指由我们指定的代理服务器端口。参数remote_host 是指我们希望连接的远程主机的主机名, IP 地址也同样有效。这个主机名在网络上应该是唯一的,如果您不确定的话,可以在远程主机上使用 uname -n 命令查看一下。参数 service_port 是远程主机可提供的服务名,也可直接键入服务对应的端口号。这个命令的相应操作是将代理服务器的proxy_port 端口绑定到 remote_hostservice_port 端口。然后我们就可以通过代理服务器的proxy_port 端口访问 remote_host 了。例如一台计算机,网络主机名是 legendsIP 地址为 10.10.8 .221 ,如果在我的计算机上执行:
    [root@lee /root]#./proxy 8000 legends telnet
  那么我们就可以通过下面这条命令访问 legendstelnet 端口。
-----------------------------------------------------------------
[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
    23telnet 服务的标准端口号,其它服务的对应端口号我们可以在 /etc/services 中查看。

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

main() 函数
-----------------------------------------------------------------
#include <stdio.h>
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <netdb.h>
#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 Harris94 年编写这段代码时系统还没有定义 sys_errlist[] ,不过现在我们不修改一下的话,编译时系统就会告诉我们sys_errlist 发生了定义冲突。
  另外我添加了一个函数类型定义:
    typedef void Sigfunc(int);
  具体原因我将在后面向大家解释。

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

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

创建通信套接字

  下面就是建立一个服务器的详细过程。服务器程序的第一个操作是创建一个套接字。这是通过调用函数 socket() 来实现的。 socket() 函数的具体描述为:
-----------------------------------------------------------------
   #include <sys/types.h>
   #include <sys/socket.h>
   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 系统还定义了一种通用的套接字地址结构:
-----------------------------------------------------------------
<linux/socket.h>
struct sockaddr
{
   unsigned short sa_family; /* address type */
   char sa_data[14]; /* protocol address */
}
-----------------------------------------------------------------
  其中 sa_family 意指套接字使用的协议族地址类型,对于我们的TCP/IP 网络,其值应该是 AF_INETsa_data 中存储具体的协议地址,不同的协议族有不同的地址格式。这个通用的套接字地址结构一般不用做定义具体的实例,但是常用做套接字地址结构的强制类型转换,如我们经常可以看到这样的用法:
    bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))
  用于 TCP/IP 协议族的套接字地址结构是 sockaddr_in ,其定义为:
-----------------------------------------------------------------
<linux/in.h>
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
  另外需要说明的一点是,在本例中,我们并没有看到在预编译部分中包含有 <linux/socket.h><linux/in.h> 这两个头文件,那是因为这两个头文件已经分别被包含在<sys/types.h><sys/types.h> 中了,而且后面这两个头文件是与平台无关的,所以在网络通信中一般都使用这两个头文件。

服务器公开地址

  如果服务器要接受客户机的连接请求,那么它必须先要在整个网络上公开自己的地址。在设置了服务器的套接字地址结构之后,可以通过调用函数 bind() 绑定服务器的地址和套接字来完成公开地址的操作。函数 bind() 的详细描述为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
   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 <sys/socket.h>
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 <sys/socket.h>
   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 <proxy-port> <host> <service-name|port-number>\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_portstruct sockaddr_in hostaddr 。分别用于传递等待连接请求的proxy 端口和被绑定的主机网络信息。

检验命令行参数

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

传递参数

  接下来就要将命令行的三个参数变换成合适的形式赋值给全局变量 proxy_porthostaddr ,以供其它函数调用。首先传送代理服务器端口 pargs.proxy_port ,在这里程序调用了一个系统函数 isdigit() 检验用户输入的端口号是否有效。 isdigit() 的具体描述为:
-----------------------------------------------------------------
#include <ctype.h>
   int isdigit(int c)
-----------------------------------------------------------------
   isdigit() 函数用来检测参数 "c" 是否是数字 1~9 中间的一个,如果答案是肯定的,则返回非"0" 值,反之,返回 "0" 。程序中采用了这样的方法来对用户的输入进行逐位检验:
    if (!isdigit(*(pargs.proxy_port + i)))
    break;
  在将有效端口号传递给全局变量 proxy_port 之前,还要将其转换成为网络字节顺序。这是因为网络中存在着多个公司的不同设备,这些设备表示数据的字节顺序是不同的。例如在内存地址0x1000 处存储一个 16 位的整数 FF11 ,不同公司的机器在内存中的存储方式也不相同,有的将FF 置于内存指针的起始位置 0x100011 置于 0x1001 ,这称为 big-endian 顺序;有的却恰恰相反,即little-endian 顺序。这种基于主机的数据存储顺序就称为主机字节顺序(host byte order )。为了在不同类型的主机之间进行通信,网络协议就规定了一种统一的网络字节顺序,这种顺序被规定为little-endian 顺序。所以数据的网络字节顺序和主机字节顺序有可能是不同的,因此在编写通信程序时一定要注意不同顺序之间的转换。所以,程序中一定要有例程中这样的语句:
    proxy_port = htons(atoi(pargs.proxy_port));
  函数 htons() 的作用就是将主机字节顺序转换为网络字节顺序。它的具体描述为:
-----------------------------------------------------------------
#include <netinet/in.h>
unsigned short int htons(unsigned short int data)
-----------------------------------------------------------------
  与 htons() 相似的函数还有三个,它们分别是htonl()ntohs()ntohl() ,都用于网络与主机字节顺序之间的转换。如果这几个名字比较容易混淆的话,我们可以这样记忆:函数名中的h 代表 hostn 代表 networks 代表 unsigned shortl 代表 unsigned long 。所以 "hton" 即为"host-to-network": 变换主机字节为网络字节。接收数据的就要用到"ntoh""network-to-host" )函数了。
  在我们的例程中,由于端口号一般情况下最多不会超过 4 位数字,所以选用unsigned short 型的 htons() 即可。
注意在例程中 htons() 的参数是另一个函数 atoi() 的返回结果。 atoi() 函数的具体描述为:
-----------------------------------------------------------------
#include <stdlib.h>
int atoi(const char *nptr)
-----------------------------------------------------------------
  它的作用是将字符指针 nptr 指向的字符串转换成相应的整数并将其作为结果返回。这个操作与函数调用strtol(nptr,(char **)NULL,10) 的效果几乎完全相同,唯一的区别是atoi() 没有出错返回信息。之所以要调用这个函数是因为,系统在读取命令行的时候将所有的参数都作为字符串处理,所以我们必须将其转换为整数形式。
接下来 , 例程先将全局变量 hostaddr 的所有成员清零,然后将成员 hostaddr.sin_family 设置为TCP/IP 协议族标志 AF_INET 。下面就可将命令行的另外两个参数 <remote_host><service_port> 传递给全局变量 hostaddr 的两个成员 hostaddr.sin_porthostaddr.sin_addr 了。这里我们用到了两个局部变量struct hostent *hostpstruct 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_INETh_length 是主机的地址长度,一般设置为 4 个字节。h_addr_list 是主机的 IP 地址列表。
  我们要用它来传递我们期望绑定的远程主机名或是 IP 地址。因为命令行中的主机名参数已经被存储进pargs.isolated_host ,所以我们就调用 inet_addr() 函数对主机名或主机的 IP 地址进行二进制和字节顺序转换。inet_addr() 函数的描述为:
-----------------------------------------------------------------
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
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 <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
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 <netdb.h>
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 为服务的正式名称,如ftphttp 等, s_aliases 是服务的别名列表,s_port 是服务的端口号,例如在一般情况下 ftp 的端口号为 21http 服务的端口号为 80 ,注意此端口号应该存储为网络字节顺序,s_proto 是应用协议的类型。
  例程中使用 getservbyname() 函数转换命令行参数中的服务名,此函数的详细描述为:
-----------------------------------------------------------------
#include <netdb.h>
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 <sys/types.h>
#include <unistd.h>
pid_t fork(void);
-----------------------------------------------------------------
  该函数被调用一次,但是返回两次,这两次返回的区别是子进程的返回值为 "0" ,而父进程的返回值为子进程的 ID 。如果出错则返回"-1"

   2 . 保证进程不会获得任何控制终端。通常的做法是调用函数setsid() 创建一个新的会晤期。 setsid() 的详细描述为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
-----------------------------------------------------------------
  第一步的操作已经保证调用此函数的进程不是进程组的组长,那么此函数将创建一个新的会晤,其结果是:首先,此进程变成该会晤期的首进程 (session leader ,系统默认会晤期的首进程是创建该会晤期的进程 ) 。而且,此进程是该会晤期中的唯一进程。然后,此进程将成为一个新的进程组的组长进程,新进程组的组 ID 就是该进程的进程 ID 。最后,保证此进程没有控制终端,即使在调用setsid() 之前此进程拥有控制终端,在创建会晤期后这种联系也将被解除。如果调用该函数的进程为一个进程组的组长,那么函数将返回出错信息"-1"
当然我们还有其他的办法让进程无法获得控制终端,就象例程中所做的那样,
-----------------------------------------------------------------
if ((fd = open("/dev/tty",O_RDWR)) >= 0) {
ioctl(fd,TIOCNOTTY,NULL);
close(fd);
}
-----------------------------------------------------------------
  其中 /dev/tty 是一个流设备,也是我们的终端映射。调用close() 函数将终端关闭。

   3 .信号处理。一般是要忽略掉某些信号。这里就涉及到信号的概念了。信号其实相当于软件中断,Linux/Unix 下的信号机制提供了一种处理异步事件的方法,终端用户键入印发中断的键,或是系统异常发出信号,这都会通过信号处理机制终止一个或多个程序的运行。
  不同情况下引发的信号是不同的。不过所有的信号都有自己的名字,所有的名字都是以 "SIG" 开头的,只是后面有所不同,我们可以通过这些名字了解到系统中到底发生了些什么事。

  当信号出现时,我们可以要求系统进行以下三种操作:
  ◇ 忽略信号。大多数信号都是采取这种方式进行处理的,在例程中我们就可以见到这种用法。但值得注意的是有两个例外,那就是对 SIGKILLSIGSTOP 信号不能做忽略处理。
  ◇ 捕捉信号。这是一种最为灵活的操作方式。这种处理方式的意思就是,当某种信号发生时,我们可以调用一个函数对这种情况进行相应的处理。最常见的情况就是,如果捕捉到SIGCHID 信号,则表示子进程已经终止,然后可在此信号的捕捉函数中调用waitpid() 函数取得该子进程的进程 ID 以及它的终止状态。在我们这段例程中,就有这种用法的一个实例。还有就是如果进程创建了临时文件,那么就要为进程终止信号 SIGTERM 编写一个信号捕捉函数来清除这些临时文件。
  ◇ 执行系统的默认动作。对绝大多数信号而言,系统的默认动作都是终止该进程。
  在 Linux 下,信号有很多种,我在这里就不一一介绍了,如果想详细地对这些信号进行了解,可以查看头文件<sigal.h> ,这些信号都被定义为正整数,也就是它们的信号编号。在对信号进行处理时,必须要用到函数signal() ,此函数的详细描述为:
-----------------------------------------------------------------
#include <signal.h>
void (*signal (int signo, void (*func)(int)))(int);
-----------------------------------------------------------------
  其中参数 signo 为信号名,参数 func 的值根据我们的需要可以是以下几种情况:( 1 )常数SIG_DFL, 表示执行系统的默认动作。( 2 )常数 SIG_IGN ,表示忽略信号。( 3 )收到信号后需要调用的处理函数的地址,此信号捕捉程序应该有一个整型参数但是没有返回值。 signal() 函数返回一个函数指针,而该指针指向的函数应该无返回值( void ),这个指针其实指向以前的信号捕捉程序。
  下面 回到我们的 daemonize() 函数上来。这个函数在创建守护进程时忽略了三个信号:
    signal(SIGTTOU,SIG_IGN);
    signal(SIGTTIN,SIG_IGN);
    signal(SIGTSTP,SIG_IGN);
  这三个信号的含义分别是: SIGTTOU 表示后台进程写控制终端,SIGTTIN 表示后台进程读控制终端, SIGTSTP 表示终端挂起。

   4 .关闭不再需要的文件描述符,并为标准输入、标准输出和标准错误输出打开新的文件描述符(也可以继承父进程的标准输入、标准输出和标准错误输出文件描述符,这个操作是可选的)。在我们这段例程中,因为是代理服务器程序,而且是在执行了listen() 函数之后执行这个 daemonize() 的,所以要保留已经转换成功的倾听套接字,所以我们可以见到这样的语句:
if (fd != servfd)
close(fd);

   5 .调用函数 chdir("/") 将当前工作目录更改为根目录。这是为了保证我们的进程不使用任何目录。否则我们的守护进程将一直占用某个目录,这可能会造成超级用户不能卸载一个文件系统。

   6 .调用函数 umask(0) 将文件方式创建屏蔽字设置为 "0" 。这是因为由继承得来的文件创建方式屏蔽字可能会禁止某些许可权。例如我们的守护进程需要创建一组可读可写的文件,而此守护进程从父进程那里继承来的文件创建方式屏蔽字却有可能屏蔽掉了这两种许可权,则新创建的一组文件其读或写操作就不能生效。因此要将文件方式创建屏蔽字设置为"0"
  在 daemonize() 函数的最后,我们可以看到这样的信号捕捉处理语句:
    signal(SIGCLD,(Sigfunc *)reap_status);
  这不是创建守护进程过程中必须的一步,它的作用是调用我们自定义的 reap_status() 函数来处理僵死进程。 reap_status() 在例程中的定义为:
-----------------------------------------------------------------
/****************************************************************
function:
    reap_status
description:
  handle a SIGCLD signal by reaping the exit status of the perished child, and            discarding it.
arguments:
   none.
return value:
  none.
calls:
     none.
globals:
    none.
****************************************************************/
void reap_status()
{
   int pid;
   union wait status;
   while ((pid = wait3(&status,WNOHANG,NULL)) > 0)
   ; /* loop while there are more dead children */
}
-----------------------------------------------------------------
  上面信号捕捉语句的原文为:
    signal(SIGCLD, reap_status);
  我们刚才说过, signal() 函数的第二个参数一定要有有一个整型参数但是没有返回值。而reap_status() 是没有参数的,所以原来的语句在编译时无法通过。所以我在预编译部分加入了对Sigfunc() 的类型定义,在这里用做对 reap_status 进行强制类型转换。而且在 BSD 系统中通常都使用SIGCHLD 信号来处理子进程终止的有关信息, SIGCLDSystem V 中定义的一个信号名,如果将SIGCLD 信号的处理方式设定为捕捉,那么内核将马上检查系统中是否存在已经终止等待处理的子进程,如果有,则立即调用信号捕捉处理程序。
  一般在信号捕捉处理程序中都要调用 wait()waitpid()wait3() 或是 wait4() 来返回子进程的终止状态。这些 " 等待" 函数的区别是,当要求函数 " 等待 " 的子进程还没有终止时, wait() 将使其调用者阻塞;而在 waitpid() 的参数中可以设定使调用者不发生阻塞,wait() 函数不被设置为等待哪个具体的子进程,它等待调用者所有子进程中首先终止的那个,而在调用waitpid() 时却必须在参数中设定被等待的子进程 ID 。而 wait3()wait4() 的参数分别比 wait()waitpid() 还要多一个 "rusage" 。例程中的reap_status() 就调用了函数 wait3() ,这个函数是 BSD 系统支持的,我们把它和wait4() 的定义一起列出来:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/time.h>
#include <sys/resource.h>
pid_t wait3(int *statloc, int options, struct rusage *rusage);
pid_t wait4(pid_t pid, int *statloc, int options, struct rusage *rusage);
-----------------------------------------------------------------
  其中指针 statloc 如果不为 "NULL" ,那么它将指向返回的子进程终止状态。参数 pid 是我们指定的被等待的子进程的进程ID 。参数 options 是我们的控制选择项,一般为 WNOHANG 或是WUNTRACED 。例程中使用了选项 WNOHANG ,意即如果不能立即返回子进程的终止状态(譬如由于子进程还未结束),那么等待函数不阻塞,此时返回 "0" 。       WUNTRACED 选项的意思是如果系统支持作业控制,如果要等待的子进程的状态已经暂停,而且其状态自从暂停以来还从未报告过,则返回其状态。参数rusage 如果不为 "NULL" ,则它将指向内核返回的由终止进程及其所有子进程使用的资源摘要,该摘要包括用户 CPU 时间总量、缺页次数、接收到信号的次数等。

◆ 代理服务程序 do_proxy()

在例程 main() 函数快要结束时,我们看到,在服务器接受了客户机的连接请求后,将为其创建子进程,并在子进程中执行代理服务程序do_proxy()
-----------------------------------------------------------------/****************************************************************
function:
   do_proxy
description:
  does the actual work of virtually connecting a client to the telnet service on the         isolated host.
arguments:
   usersockfd socket to which the client is connected. return value: none.
calls:
     none.
globals:
     reads hostaddr.
****************************************************************/
void do_proxy (usersockfd)
int usersockfd;
{
   int isosockfd;
   fd_set rdfdset;
   int connstat;
   int iolen;
   char buf[2048];
   /* open a socket to connect to the isolated host */
   if ((isosockfd = socket(AF_INET,SOCK_STREAM,0)) < 0)
   errorout("failed to create socket to host");
   /* attempt a connection */
   connstat = connect(isosockfd,(struct sockaddr *) &hostaddr, sizeof(hostaddr));
   switch (connstat) {
   case 0:
   break;
   case ETIMEDOUT:
   case ECONNREFUSED:
   case ENETUNREACH:
   strcpy(buf,sys_myerrlist[errno]);
   strcat(buf,"\r\n");
   write(usersockfd,buf,strlen(buf));
   close(usersockfd);
   exit(1);
   /* die peacefully if we can't establish a connection */
   break;
   default:
   errorout("failed to connect to host");
   }
   /* now we're connected, serve fall into the data echo loop */
   while (1) {
     /* Select for readability on either of our two sockets */
     FD_ZERO(&rdfdset);
     FD_SET(usersockfd,&rdfdset);
     FD_SET(isosockfd,&rdfdset);
   if (select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL) < 0)
    errorout("select failed");
    /* is the client sending data? */
   if (FD_ISSET(usersockfd,&rdfdset)) {
     if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0)
      break; /* zero length means the client disconnected */
      rite(isosockfd,buf,iolen);
      /* copy to host -- blocking semantics */
    }
   /* is the host sending data? */
   if (FD_ISSET(isosockfd,&rdfdset)) {
     f ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
       break; /* zero length means the host disconnected */
       rite(usersockfd,buf,iolen);
       /* copy to client -- blocking semantics */
     }
    }
    /* we're done with the sockets */
    close(isosockfd);
    lose(usersockfd);
   }
-----------------------------------------------------------------
  在我们这段代理服务器例程中,真正连接用户主机和远端主机的一段操作,就是由这个 do_proxy() 函数来完成的。回想一下我们一开始对这段 proxy 程序用法的介绍。先将我们的proxy 与远端主机绑定,然后用户通过 proxy 的绑定端口与远端主机建立连接。而在 main() 函数中,我们的proxy 由一段服务器程序与用户主机建立了连接,而在这个 do_proxy() 函数中, proxy 将与远端主机的相应服务端口(由用户在命令行参数中指定)建立连接,并负责传递用户主机和远端主机之间交换的数据。
  由于要和远端主机建立连接,所以我们看到 do_proxy() 函数的前半部分实际上相当于一段标准的客户机程序。首先创建一个新的套接字描述符isosockfd ,然后调用函数 connect() 与远端主机之间建立连接。函数 connect() 的定义为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, struct sockaddr *servaddr, int addrlen);
-----------------------------------------------------------------
  参数 sockfd 是调用函数 socket() 返回的套接字描述符,参数 servaddr 指向远程服务器的套接字地址结构,参数addrlen 指定这个套接字地址结构的长度。函数 connect() 执行成功时返回 "0" ,如果执行失败则返回"-1" ,并将全局变量 errno 设置为相应的错误类型。在例程中的 switch() 函数调用中对以下三种出错类型进行了处理:ETIMEDOUTECONNREFUSEDENETUNREACH 。这三个出错类型的意思分别为:ETIMEDOUT 代表超时,产生这种情况的原因有很多,最常见的是服务器忙,无法应答客户机的连接请求;ECONNREFUSED 代表连接拒绝,即服务器端没有准备好的倾听套接字,或是没有对倾听套接字的状态进行监听;ENETUNREACH 表示网络不可达。
  在本例中, connect() 函数的第二个参数 servaddr 是全局变量 hostaddr ,其中存储着函数parse_args() 转换好的命令行参数。如果连接建立失败,在例程中就调用我们自定义的函数errorout() 输出信息 "failed to connect to host"errorout() 函数的定义为:
-----------------------------------------------------------------
/****************************************************************
function:
   errorout
description: displays an error message on the console and kills the current process.
arguments:
  msg -- message to be displayed.
return value: none -- does not return.
calls:
    none.
globals:
   none.
****************************************************************/
void errorout (msg)
char *msg;
{
   FILE *console;
   console = fopen("/dev/console","a");
   fprintf(console,"proxyd: %s\r\n",msg);
   fclose(console);
   exit(1);
}
-----------------------------------------------------------------
   do_proxy() 函数的后半部分是通过 proxy 建立用户主机与远端主机之间的连接。我们既有 proxy 与用户主机连接的套接字(do_proxy() 函数的参数 usersockfd ),又有 proxy 与远端主机连接的套接字isosockfd ,那么最简单直接的通信建立方式就是从一个套接字读,然后直接写到另一个套接字去。如:
-----------------------------------------------------------------
int n;
char buf[2048];
while((n=read(usersockfd, buf, sizeof(buf))>0)
if(write(isosockfd, buf, n)!=n)
err_sys("write wrror\n");
-----------------------------------------------------------------
  这种形式的阻塞 I/O 在单向数据传递的时候是非常有效的,但是在我们的proxy 操作中是要求用户主机和远端主机双向通信的,这样就要求我们对两个套接字描述符既能够读由能够写。如果还是采用这种方式的阻塞I/O 的话,很有可能长时间阻塞在一个描述符上。因此例程在处理这个问题的时候调用了select() 函数,这个函数允许我们执行 I/O 多路转接。其具体含义就是 select() 函数可以构造一个表,在这个表中包含了我们所有要用到的文件描述符。然后我们可以调用一个函数,这个函数可以检测这些文件描述符的状态,当某个(我们指定的)文件描述符准备好进行I/O 操作时,此函数就返回,告知进程哪个文件描述符已经可以执行I/O 操作了。这样就避免了长时间的阻塞。
  还有一个函数 poll() 可以实现 I/O 多路转接,由于在例程中调用的是 select() ,我们就只对select() 进行一下比较详细的介绍。 select() 系列函数的详细描述为:
-----------------------------------------------------------------
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int n, fd_set *readfds, fd_set *writefds, fd_est *exceptfds, struct timeval *timeout);
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
-----------------------------------------------------------------
   select() 函数将创建一个我们所关心的文件描述符表,它的参数将在内核中为这些文件描述符设置我们所关心的条件,例如是否是可读、是否可写以及是否异常,而且在参数中还可以设置我们希望等待的最大时间。在select() 成功执行时,它将返回目前已经准备好的描述符数量,同时内核可以告诉我们各个描述符的状态信息。如果超时,则返回"0" ,如果出错,则函数返回 "-1" ,并同时设置 errno 为相应的值。
   select() 的最后一个参数 timeout 将设置等待时间。其中结构 timeval 是在文件<bits/time.h> 中定义的。
-----------------------------------------------------------------
struct timeval
{
   __time_t tv_sec; /* Seconds */
   __time_t tv_usec; /* Microseconds */
};
-----------------------------------------------------------------
  参数 timeout 的设置有三种情况。象例程中这样timeout==NULL 时,这表示用户希望永远等待,直到我们指定的文件描述符中的一个已准备好,或者是捕捉到一个信号。如果是由于捕捉到信号而中断了这个无限期的等待过程的话,select() 将返回 "-1" ,同时设置 errno 的值为 EINTR
  如果 timeout->tv_sec==0&&timeout->tv_usec==0 ,那么这表示完全不等待。Select() 测试了所有指定文件描述符后立即返回。这是得到多个描述符状态而不阻塞select() 函数的轮询方法。
  如果 timeout->tv_sec=0||timeout->tv_usec=0 ,那么这两个参数的值即为我们希望函数等待的时间。其中tv_sec 设置时间单位为秒, tv_usec 设置时间单位为微秒。如果在超时的时候,在我们指定的所有文件描述符里面仍然没有任何一个准备好的话,则 select() 将返回 "0"
  中间三个参数的数据类型是 fd_set ,它的意思是文件描述符集,而readfds, writefdsexceptfds 则分别是指向文件描述符集的指针,他们分别描述了我们所关心的可读、可写以及状态异常的各个文件描述符。之所以我们称 select() 可以创建一个文件描述符 "" ,那个所谓的表就是由这三个参数指向的数据结构组成的。其具体结构如图 1 所示。其中在每个 set_fd 数据类型中都为我们关心的所有文件描述符保留了一位。所以在监测文件描述符状态的时候,就在这些set_fd 数据结构中查询相关的位。
  第一个参数 n 用来说明到底需要遍历多少个描述符位。 n 的值一般是这样设置的,从我们关心的所有文件描述符中选出最大值再加 1 。例如我们设置的所有文件描述符中最大的为 6 ,那么将n 设置为 7 ,则系统在检测描述符状态的时候,就只用遍历前 7 位(fd0~fd6 )的状态。不过如果不想这样麻烦的话,我们可以象例程中那样将n 的值直接设置为 FD_SETSIZE 。这是系统中设定的最大文件描述符个数,不同的系统这个值也不相同,一般是 256 或是 1024 。这样在检测描述符状态的时候,函数将遍历所有的描述符位。
  在调用 select() 函数实现多路 I/O 转接时,首先我们要声明一个新的文件描述符集,就象例程中这样:
    fd_set rdfdset;
  然后调用 FD_ZERO() 清空此文件描述符集的所有位,以免下面检测描述符位的时候返回错误结果:
    FD_ZERO(&rdfdset);
  然后调用 FD_SET() 在文件描述符集中设置我们关心的位。在本例中,我们关心的就是分别与用户主机和远端主机连接的两个套接字描述符,所以执行这样的语句:
    FD_SET(usersockfd,&rdfdset);
    FD_SET(isosockfd,&rdfdset);
  然后调用 select() 返回描述符状态,此时描述符状态被存储进描述符集,也就是set_fd 数据结构中。在图 1 中我们看到所有的描述符位状态都是 "0" ,在select() 返回后,例如 fd0 可读,则在 readfds 描述符集中 fd0 对应的位上将状态标志设置为 "1" ,如果fd1 可写,则 writefds 描述符集中 fd1 对应的位上将状态标志设置为"1" ,状态异常的情况也也与此相同。在本例中,我们只关心两个套接字描述符是否可写,因此执行这样的select() 函数:
    select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL)
  那么在 select() 返回后怎样检测 set_fd 数据结构中描述符位的状态呢?这就要调用函数 FD_ISSET() ,如果对应文件描述符的状态为" 已准备好 " (即描述符位为 "1" ),则 FD_ISSET() 返回 "1" ,否则返回 "0"
-----------------------------------------------------------------
if (FD_ISSET(usersockfd,&rdfdset)) {
   if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0)
   break; /* zero length means the host disconnected */
   write(isosockfd,buf,iolen);
-----------------------------------------------------------------
  这一段代码就实现从套接字 usersockfd (用户主机)到套接字isosockfd (远端主机)的无阻塞传输。而下一段代码实现反方向的无阻塞传输:
-----------------------------------------------------------------
if (FD_ISSET(isosockfd,&rdfdset)) {
   if ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
    break; /* zero length means the host disconnected */
    write(usersockfd,buf,iolen);
-----------------------------------------------------------------
  这样就通过 proxy 实现了用户主机与远端主机之间的通信。
  对这段 proxy 代码我只是写了一些自己的理解,大多数是一些函数的用法,这些都是linux 网络编程中一些最基础的知识,如果有不对的地方,还请各位大虾批评指正。

原创粉丝点击