📄 51.htm
字号:
----------------------------------------------------------------- <br>
在我们这段代理服务器例程中,真正连接用户主机和远端主机的一段操作,就是由 <br>
这个 <br>
do_proxy()函数来完成的。回想一下我们一开始对这段proxy程序用法的介绍。先将我们 <br>
的p <br>
roxy与远端主机绑定,然后用户通过proxy的绑定端口与远端主机建立连接。而在main( <br>
)函 <br>
数 <br>
中,我们的proxy由一段服务器程序与用户主机建立了连接,而在这个do_proxy <br>
()函数中,p <br>
roxy将与远端主机的相应服务端口(由用户在命令行参数中指定)建立连接,并负责传 <br>
递用 <br>
户主机和远端主机之间交换的数据。 <br>
由于要和远端主机建立连接,所以我们看到do_proxy()函数的前半部分实际上相当 <br>
于一 <br>
段标准的客户机程序。首先创建一个新的套接字描述符isosockfd,然后调用函数conne <br>
ct() <br>
与远端主机之间建立连接。函数connect()的定义为: <br>
----------------------------------------------------------------- <br>
#include <sys/types.h> <br>
#include <sys/socket.h> <br>
int connect(int sockfd, struct sockaddr *servaddr, int addrlen); <br>
----------------------------------------------------------------- <br>
参数sockfd是调用函数socket()返回的套接字描述符,参数servaddr指向远程服务 <br>
器的 <br>
套接字地址结构,参数addrlen指定这个套接字地址结构的长度。函数connect()执行成 <br>
功时 <br>
返回"0",如果执行失败则返回"-1",并将全局变量errno设置为相应的错误类型。在例 <br>
程中 <br>
的switch()函数调用中对以下三种出错类型进行了处理: ETIMEDOUT、ECONNREFUSED和 <br>
ENET <br>
UNREACH。这三个出错类型的意思分别为:ETIMEDOUT代表超时,产生这种情况的原因有 <br>
很多 <br>
,最常见的是服务器忙,无法应答客户机的连接请求;ECONNREFUSED代表连接拒绝,即 <br>
服务 <br>
器端没有准备好的倾听套接字,或是没有对倾听套接字的状态进行监听;ENETUNREACH表 <br>
示 <br>
网 <br>
络不可达。 <br>
在本例中,connect()函数的第二个参数servaddr是全局变量hostaddr,其中存储着 <br>
函 <br>
函 <br>
数 <br>
parse_args()转换好的命令行参数。如果连接建立失败,在例程中就调用我们自 <br>
定义的函数 <br>
errorout()输出信息"failed to connect to host"。errorout()函数的定义为: <br>
----------------------------------------------------------------- <br>
/**************************************************************** <br>
function: errorout <br>
description: displays an error message on the console and kills the current <br>
proc <br>
ess. <br>
arguments: msg -- message to be displayed. <br>
return value: none -- does not return. <br>
calls: none. <br>
globals: none. <br>
****************************************************************/ <br>
void errorout (msg) <br>
char *msg; <br>
{ <br>
FILE *console; <br>
console = fopen("/dev/console","a"); <br>
fprintf(console,"proxyd: %s\r\n",msg); <br>
fclose(console); <br>
exit(1); <br>
} <br>
----------------------------------------------------------------- <br>
do_proxy()函数的后半部分是通过proxy建立用户主机与远端主机之间的连接。我们 <br>
既 <br>
有 <br>
proxy与用户主机连接的套接字(do_proxy()函数的参数usersockfd),又有pro <br>
xy与远端主 <br>
机连接的套接字isosockfd,那么最简单直接的通信建立方式就是从一个套接字读,然后 <br>
直 <br>
接 <br>
写到另一个套接字去。如: <br>
----------------------------------------------------------------- <br>
int n; <br>
char buf[2048]; <br>
while((n=read(usersockfd, buf, sizeof(buf))>0) <br>
if(write(isosockfd, buf, n)!=n) <br>
err_sys("write wrror\n"); <br>
----------------------------------------------------------------- <br>
这种形式的阻塞I/O在单向数据传递的时候是非常有效的,但是在我们的proxy操作 <br>
中是 <br>
要求用户主机和远端主机双向通信的,这样就要求我们对两个套接字描述符既能够读由 <br>
能够 <br>
写。如果还是采用这种方式的阻塞I/O的话,很有可能长时间阻塞在一个描述符上。因此 <br>
例 <br>
程 <br>
在处理这个问题的时候调用了select()函数,这个函数允许我们执行I/O多路转接 <br>
。其具体 <br>
含 <br>
义就是select()函数可以构造一个表,在这个表中包含了我们所有要用到 <br>
的文件描述符。然 <br>
后我们可以调用一个函数,这个函数可以检测这些文件描述符的状态,当某个(我们指 <br>
定的 <br>
)文件描述符准备好进行I/O操作时,此函数就返回,告知进程哪个文件描述符已经可以 <br>
执 <br>
行 <br>
I/O操作了。这样就避免了长时间的阻塞。 <br>
还有一个函数poll()可以实现I/O多路转接,由于在例程中调用的是select(),我们 <br>
就 <br>
只 <br>
对select()进行一下比较详细的介绍。select()系列函数的详细描述为: <br>
----------------------------------------------------------------- <br>
#include <sys/time.h> <br>
#include <sys/types.h> <br>
#include <unistd.h> <br>
int select(int n, fd_set *readfds, fd_set *writefds, fd_est *exceptfds, stru <br>
ct t <br>
imeval *timeout); <br>
FD_CLR(int fd, fd_set *set); <br>
FD_ISSET(int fd, fd_set *set); <br>
FD_SET(int fd, fd_set *set); <br>
FD_ZERO(fd_set *set); <br>
----------------------------------------------------------------- <br>
select()函数将创建一个我们所关心的文件描述符表,它的参数将在内核中为这些 <br>
文件 <br>
描述符设置我们所关心的条件,例如是否是可读、是否可写以及是否异常,而且在参数 <br>
中还 <br>
可以设置我们希望等待的最大时间。在select()成功执行时,它将返回目前已经准备好 <br>
的描 <br>
述符数量,同时内核可以告诉我们各个描述符的状态信息。如果超时,则返回"0",如果 <br>
出 <br>
错 <br>
,则函数返回"-1",并同时设置errno为相应的值。 <br>
select()的最后一个参数timeout将设置等待时间。其中结构timeval是在文件<bit <br>
s/ti <br>
me.h>中定义的。 <br>
----------------------------------------------------------------- <br>
struct timeval <br>
{ <br>
__time_t tv_sec; /* Seconds */ <br>
__time_t tv_usec; /* Microseconds */ <br>
}; <br>
----------------------------------------------------------------- <br>
参数timeout的设置有三种情况。象例程中这样timeout==NULL时,这表示用户希望 <br>
永远 <br>
等待,直到我们指定的文件描述符中的一个已准备好,或者是捕捉到一个信号。如果是 <br>
由于 <br>
捕捉到信号而中断了这个无限期的等待过程的话,select()将返回"-1",同时设置errn <br>
o的 <br>
值 <br>
为EINTR。 <br>
如果timeout->tv_sec==0&&timeout->tv_usec==0,那么这表示完全不等待。Selec <br>
t() <br>
测 <br>
试了所有指定文件描述符后立即返回。这是得到多个描述符状态而不阻塞selec <br>
t()函数的轮 <br>
询方法。 <br>
如果timeout->tv_sec!=0||timeout->tv_usec!=0,那么这两个参数的值即为我们 <br>
希 <br>
望 <br>
函数等待的时间。其中tv_sec设置时间单位为秒,tv_usec设置时间单位为微秒。 <br>
如果在超 <br>
时 <br>
的时候,在我们指定的所有文件描述符里面仍然没有任何一个准备好的话 <br>
,则select()将返 <br>
回"0"。 <br>
中间三个参数的数据类型是fd_set,它的意思是文件描述符集,而readfds, write <br>
fds <br>
和 <br>
exceptfds则分别是指向文件描述符集的指针,他们分别描述了我们所关心的可 <br>
读、可写以 <br>
及 <br>
状态异常的各个文件描述符。之所以我们称select()可以创建一个文件 <br>
描述符"表",那个所 <br>
谓的表就是由这三个参数指向的数据结构组成的。其具体结构如图1所示。其中在每个s <br>
et_f <br>
d数据类型中都为我们关心的所有文件描述符保留了一位。所以在监测文件描述符状态的 <br>
时 <br>
候 <br>
,就在这些set_fd数据结构中查询相关的位。 <br>
第一个参数n用来说明到底需要遍历多少个描述符位。n的值一般是这样设置的,从 <br>
我们 <br>
关心的所有文件描述符中选出最大值再加1。例如我们设置的所有文件描述符中最大的为 <br>
6, <br>
那么将n设置为7,则系统在检测描述符状态的时候,就只用遍历前7位(fd0~fd6)的状 <br>
态。 <br>
不过如果不想这样麻烦的话,我们可以象例程中那样将n的值直接设置为FD_SETSIZE。这 <br>
是 <br>
系 <br>
统中设定的最大文件描述符个数,不同的系统这个值也不相同,一般是256或是1 <br>
024。这样 <br>
在 <br>
检测描述符状态的时候,函数将遍历所有的描述符位。 <br>
在调用select()函数实现多路I/O转接时,首先我们要声明一个新的文件描述符集, <br>
就 <br>
象 <br>
例程中这样: <br>
fd_set rdfdset; <br>
然后调用FD_ZERO()清空此文件描述符集的所有位,以免下面检测描述符位的时候返 <br>
回 <br>
错 <br>
误结果: <br>
误结果: <br>
FD_ZERO(&rdfdset); <br>
然后调用FD_SET()在文件描述符集中设置我们关心的位。在本例中,我们关心的就 <br>
是分 <br>
别与用户主机和远端主机连接的两个套接字描述符,所以执行这样的语句: <br>
FD_SET(usersockfd,&rdfdset); <br>
FD_SET(isosockfd,&rdfdset); <br>
然后调用select()返回描述符状态,此时描述符状态被存储进描述符集,也就是se <br>
t_fd <br>
数据结构中。在图1中我们看到所有的描述符位状态都是"0",在select()返回后,例如 <br>
fd0 <br>
可 <br>
读,则在readfds描述符集中fd0对应的位上将状态标志设置为"1",如果fd1可写 <br>
,则writef <br>
ds描述符集中fd1对应的位上将状态标志设置为"1",状态异常的情况也也与此相同。在 <br>
本例 <br>
中,我们只关心两个套接字描述符是否可写,因此执行这样的select()函数: <br>
select(FD_SETSIZE,&rdfdset,NULL,NULL,NULL) <br>
那么在select()返回后怎样检测set_fd数据结构中描述符位的状态呢?这就要调用 <br>
函数 <br>
FD_ISSET(),如果对应文件描述符的状态为"已准备好"(即描述符位为"1"),则FD_IS <br>
SET( <br>
)返回"1",否则返回"0"。 <br>
----------------------------------------------------------------- <br>
if (FD_ISSET(usersockfd,&rdfdset)) { <br>
if ((iolen = read(usersockfd,buf,sizeof(buf))) <= 0) <br>
break; /* zero length means the host disconnected */ <br>
write(isosockfd,buf,iolen); <br>
----------------------------------------------------------------- <br>
这一段代码就实现从套接字usersockfd(用户主机)到套接字isosockfd(远端主机 <br>
) <br>
的 <br>
无阻塞传输。而下一段代码实现反方向的无阻塞传输: <br>
----------------------------------------------------------------- <br>
if (FD_ISSET(isosockfd,&rdfdset)) { <br>
if ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0) <br>
break; /* zero length means the host disconnected */ <br>
write(usersockfd,buf,iolen); <br>
----------------------------------------------------------------- <br>
这样就通过proxy实现了用户主机与远端主机之间的通信。 <br>
对这段proxy代码我只是写了一些自己的理解,大多数是一些函数的用法,这些都是 <br>
lin <br>
ux网络编程中一些最基础的知识,如果有不对的地方,还请各位大 号
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -