摘自:http://fanqiang.chinaunix.net/a4/b7/20010810/1200001101.html
在例程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()函数调用中对以下三种出错类型进行了处理:
ETIMEDOUT、ECONNREFUSED和ENETUNREACH。这三个出错类型的意思分别为: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,
writefds和exceptfds则分别是指向文件描述符集的指针,他们分别描述了我们所关心的可读、可写以及状态异常的各个文件描述符。之所以我们
称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网络编程中一些最基础的知识,如果有不对的地方,还请各位大虾批评指正。