📄 276.htm
字号:
connfd = accept( listenfd, (struct sockaddr *)&clientaddr, <br>
&clientlen ); <br>
if( connfd < 0 ) { <br>
printf("accept error <br>
"); <br>
exit(-1); <br>
} <br>
if( (chpid = fork()) == -1 ) { <br>
printf("fork error <br>
"); <br>
exit(-1); <br>
exit(-1); <br>
} <br>
if( chpid == 0 ) { <br>
close(listenfd); <br>
do_proxy(connfd); <br>
exit(0); <br>
} <br>
if( chpid > 0 ) { <br>
close(connfd); <br>
} <br>
} <br>
在for(;;){}这个无限循环中,进程阻塞于accept。 <br>
accept( listenfd, (struct sockaddr *)&clientaddr, <br>
&clientlen ) <br>
等待客户端连接,如果连接成功,则在clientaddr中返回客户端的IP地址以及端口号, <br>
协议类型等信息,同时 <br>
clientaddr的长度存于clientlen中。accept返回socket连接描述字connfd.如果accept <br>
()函数返回值为小于0, <br>
则表示出错。 <br>
连接成功,主进程采用fork()派生子进程。如果FORK()函数返回值为小于0, 则表示出错 <br>
。 <br>
在主进程中( chpid > 0 ),关闭connfd描述字,并继续for(;;){}循环。在子进程中( <br>
chpid == 0 ),关闭 <br>
listenfd监听socket描述字,并调用do_proxy()函数 ( 稍候介绍,用于完成proxy的工 <br>
作 )。等待do_proxy <br>
()函数返回,并且退出子进程。 <br>
注意:fork() 函数是调用一次,返回两次,一次返回在主进程中,一次返回在子进程中 <br>
。 <br>
下面介绍do_proxy()函数。 <br>
bzero(&rout, sizeof(rout)); <br>
rout.sin_family = AF_INET; <br>
rout.sin_port = htons(7001); <br>
rout.sin_addr.s_addr = inet_addr("127.0.0.1"); <br>
定义连接的远程服务端口。由于这个程序是基于测试目的,为了方便,我把远程服务定 <br>
义为本机的7001端口 <br>
( 也就是说,实际上走的是loopback interface )。 <br>
if( (outfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { <br>
printf("socket error <br>
"); <br>
exit(-1); <br>
} <br>
socket()函数返回一个socket类型的描述字,类型为AF_INET ( IPv4 ), SOCK_STREAM <br>
( TCP ) . <br>
如果socket()函数返回值为小于0, 则表示出错。 <br>
if( connect(outfd, (struct sockaddr *)&rout, sizeof(rout)) < 0 ) { <br>
printf("connect error <br>
"); <br>
exit(-1); <br>
} <br>
connect()函数连接远程服务地址和端口,如果connect()函数返回值为小于0, 则表示出 <br>
错。 <br>
在while(1) { } 无限循环中: <br>
FD_ZERO(&set); <br>
清空fd_set <br>
FD_SET(infd, &set); <br>
FD_SET(outfd, &set); <br>
把infd ( 是从主程序中传进来的,就是连接描述字connfd ), outfd ( 连接远程服务的 <br>
描述字 )放进 <br>
fd_set。 <br>
maxfd = max(outfd, infd); <br>
取两个描述字的最大值。 <br>
max() 函数定义如下: <br>
int max(int i, int j) { <br>
return i>j?i:j; <br>
} <br>
很简单,就不用解释了。 <br>
if( select(maxfd + 1, &set, NULL, NULL, NULL) < 0 ) { <br>
perror("select error:"); <br>
exit(-1); <br>
} <br>
阻塞于 select() 函数, 等待infd, outfd中任意描述字可读。 <br>
这里稍微解释一下:maxfd + 1, select函数要求第一个参数是集合中描述字最大值加 <br>
1 ( 很多人常常忘记了 <br>
加上1,结果导致select函数出错 ) 。我把可写,异常两个集合都定义为空,因为我们 <br>
不必关心这两个集合。 <br>
超时设置为NULL, 这表示如果没有描述字不可读,将永远阻塞在select 函数中。( 在以 <br>
后的版本里面,我将修改 <br>
这一函数调用,以增强程序性能。如果select()函数返回值为小于0, 则表示出错。 <br>
if( FD_ISSET(infd, &set) ) { <br>
n = read(infd, (void *)buf, count); <br>
if( n <= 0) <br>
break; <br>
if( write(outfd, (const void *)buf, n) != n ) { <br>
printf("write error <br>
"); <br>
continue; <br>
} <br>
} <br>
如果select返回值大于0,检测是否infd可读。如果可读,则从infd中读出数据,并写回 <br>
到outfd中。这里, <br>
如果read返回值小于或者等于0,表示服务器写入了终止符号或者服务器停止服务 ( 这 <br>
里的情况比较复杂,需要注意。 <br>
)如果read出错,则终止循环。如果write写入outfd的字节数不为n则表示write出错 <br>
( 原因可能是客户端终止或者其他异常情况 )。 <br>
但是,需要注意的是,当write出错的时候,我们并不退出,而是继续 while(1) { }循 <br>
环。 <br>
if( FD_ISSET(outfd, &set) ) { <br>
n = read(outfd, (void *)buf, count); <br>
if( n <= 0) <br>
break; <br>
if( write(infd, (const void *)buf, n) != n ) { <br>
printf("write error <br>
"); <br>
continue; <br>
} <br>
} <br>
如果select返回值大于0,检测是否outfd可读。如果可读,则从outfd中读出数据,并写 <br>
回到infd中。这里, <br>
如果read返回值小于或者等于0,表示服务器写入了终止符号或者服务器停止服务 ( 这 <br>
里的情况比较复杂,需要注意。 <br>
)如果read出错,则终止循环。如果write写入outfd的字节数不为n则表示write出错 <br>
( 原因可能是客户端终止或者其他异常情况 )。 <br>
但是,需要注意的是,当write出错的时候,我们并不退出,而是继续 while(1) { }循 <br>
环。 <br>
这一部分就是初步设计中的思想的实现。就是这两段程序完成了"二传手"的工作。 <br>
close(infd); <br>
close(outfd); <br>
当循环因为服务端或者客户端终止或者其他出错退出,则关闭两个描述字,并返回。 <br>
5、测试第一版的程序 <br>
为了测试我的小程序是否能够按希望的方式运行并且得到正确的结果,我写了另外一个 <br>
小程序用来辅助测试 <br>
的工作。 <br>
程序清单如下: <br>
--------------------------------------------------------------------------- <br>
-------------------- <br>
/******************************************************* <br>
Program: echos.c <br>
Description: an echo server <br>
Author: Alan Chen (ariesram@may10.ca) <br>
Date: July, 10, 2001 <br>
*******************************************************/ <br>
#include <stdio.h> <br>
#include <sys/types.h> <br>
#include <sys/socket.h> <br>
#include <sys/wait.h> <br>
#include <netinet/in.h> <br>
void do_echo(int); <br>
void waitchild(int signo); <br>
int main(void) { <br>
struct sockaddr_in servaddr, clientaddr; <br>
int clientlen; <br>
int listenfd, connfd; <br>
pid_t childpid; <br>
listenfd = socket(AF_INET, SOCK_STREAM, 0); <br>
if( listenfd < 0 ) { <br>
printf("socket error <br>
"); <br>
exit(1); <br>
} <br>
bzero(&servaddr, sizeof(servaddr)); <br>
servaddr.sin_family = AF_INET; <br>
servaddr.sin_port = htons(7001); <br>
servaddr.sin_addr.s_addr = INADDR_ANY; <br>
if( bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) { <br>
printf("bind error <br>
"); <br>
exit(1); <br>
} <br>
if( listen(listenfd, 5) < 0 ) { <br>
printf("listen error <br>
"); <br>
exit(1); <br>
} <br>
signal(SIGCHLD, waitchild); <br>
while(1) { <br>
if( (connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientlen)) <br>
< 0 ) { <br>
printf("accept error <br>
"); <br>
exit(1); <br>
} <br>
if( (childpid = fork() ) < 0 ) { <br>
printf("fork error <br>
"); <br>
exit(1); <br>
} <br>
if( childpid == 0 ) { <br>
close(listenfd); <br>
do_echo(connfd); <br>
exit(0); <br>
} <br>
close(connfd); <br>
} <br>
exit(0); <br>
} <br>
void do_echo(int fd) { <br>
int n; <br>
char buf[1024]; <br>
while(1) { <br>
if( (n = read(fd, (void *)&buf, 1024)) <= 0 ) <br>
break; <br>
if( (n = write(fd, (void *)&buf, n)) <= 0) <br>
break; <br>
} <br>
} <br>
void waitchild(int signo) { <br>
int status; <br>
pid_t chpid; <br>
if( (chpid = waitpid(-1, &status, WNOHANG)) < 0 ) { <br>
printf("waitpid error <br>
"); <br>
exit(-1); <br>
} <br>
printf("child %d quitted. <br>
", chpid); <br>
return; <br>
} <br>
--------------------------------------------------------------------------- <br>
-------------------- <br>
这个程序比较简单,功能是把客户端输入的字符返回给客户端。当客户端终止时,则停 <br>
止子进程。 <br>
程序解释完了,我们来看一下运行结果。 <br>
首先,编译这两个程序。 <br>
gcc -o sp sp.c <br>
gcc -o echos echos.c <br>
运行. <br>
./sp <br>
./echos <br>
看看程序初始化的时候端口的状态。 <br>
[alan@ariesram proxy]$ netstat -na | grep 700 <br>
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN <br>
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN <br>
sp, echos分别监听两个端口,7000 和 7001。 <br>
启动一个客户端,连接sp的服务端口7000。 <br>
[alan@ariesram proxy]$ telnet localhost 7000 <br>
Trying 127.0.0.1... <br>
Connected to ariesram. <br>
Escape character is '^]'. <br>
再来看看端口的状态。 <br>
[alan@ariesram alan]$ netstat -na | grep 700 <br>
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN <br>
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN <br>
tcp 0 0 127.0.0.1:32769 127.0.0.1:7000 ESTABLISHED <br>
tcp 0 0 127.0.0.1:7001 127.0.0.1:32770 ESTABLISHED <br>
tcp 0 0 127.0.0.1:32770 127.0.0.1:7001 ESTABLISHED <br>
tcp 0 0 127.0.0.1:7000 127.0.0.1:32769 ESTABLISHED <br>
其中, <br>
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN <br>
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN <br>
仍然处在监听状态。 <br>
而 <br>
tcp 0 0 127.0.0.1:32769 127.0.0.1:7000 ESTABLISHED <br>
是我启动的telnet连接到sp服务端口的连接。 <br>
同时,sp发起了一个到目的服务端口7001的连接。 <br>
tcp 0 0 127.0.0.1:32770 127.0.0.1:7001 ESTABLISHED <br>
另外, <br>
tcp 0 0 127.0.0.1:7000 127.0.0.1:32769 ESTABLISHED <br>
tcp 0 0 127.0.0.1:7001 127.0.0.1:32770 ESTABLISHED <br>
分别是sp代理服务程序连接客户端和远程目标服务端口连接代理服务程序的连接。如果 <br>
是remote方式 <br>
的话,是看不到这两个连接的。 <br>
在telnet客户端输入字符串做测试,看是否能够把输入字符串原样返回。 <br>
[alan@ariesram proxy]$ telnet localhost 7000 <br>
Trying 127.0.0.1... <br>
Connected to ariesram. <br>
Escape character is '^]'. <br>
asdf <br>
asdf <br>
sadf <br>
sadf <br>
asdfasdfasdfasfd <br>
asdfasdfasdfasfd <br>
结果显示,我们的程序是成功的。:-) <br>
退出telnet客户端,再来看看端口的状态。 <br>
[alan@ariesram proxy]$ netstat -na | grep 700 <br>
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN <br>
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN <br>
tcp 0 0 127.0.0.1:32769 127.0.0.1:7000 TIME_WAIT <br>
tcp 0 0 127.0.0.1:32770 127.0.0.1:7001 TIME_WAIT <br>
我们可以看到,由 telnet 客户端发起的连接和代理服务程序sp发起的连接都处于clos <br>
e过程的TIME_WAIT <br>
状态。该状态的持续时间是最长分节生命周期 MSL ( maximum segment lifetime ) 的 <br>
两倍,有时候称作 <br>
2MSL。 <br>
存在TIME_WAIT状态的两个理由: <br>
1、实现终止TCP全双工连接的可靠性。 <br>
2、允许老的重复分节在网络中消逝。 <br>
而其中, <br>
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN <br>
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN <br>
仍然处在监听状态, 直到echos, sp两个程序退出。 <br>
6、小结 <br>
以上讲述了第一版的开发以及测试过程。我们看到,我的初步设想是能够实现的。接下 <br>
来需要做的是 <br>
将代理服务程序修改成为一个可用的版本。需要做的事情是: <br>
a、修改程序运行方式,使其能从命令行读入 option,设定监听端口和所要连接的远程 <br>
服务地址以及 <br>
端口。 <br>
b、使程序能够以后台方式运行 ,而不是前台方式,成为一个真正的服务程序。( 现在 <br>
的版本当用户 <br>
退出控制台的时候会终止运行。) <br>
进一步的工作是: <br>
c、使程序能够监听多个端口,并且连接多个远程服务。 <br>
d、使程序能够从配置文件中读取设定监听端口和所要连接的远程服务地址以及端口以满 <br>
足多种服务并存 <br>
的需要。 <br>
这些工作我将在下一部分文章中描述。 <br>
有什么问题、意见,可以通过电子邮件和我联系。 <br>
<br>
-- <br>
Target Locked:Guru In Darkness. <br>
我只是一只静静卧着的狮子。。。 <br>
※ 修改:·guru 於 10月03日21:03:40 修改本文·[FROM: 202.114.36.210] <br>
※ 来源:·UNIX编程 www.tiaozhan.com/unixbbs/·[FROM: 202.114.36.210] <br>
</small><hr>
<p align="center">[<a href="index.htm">回到开始</a>][<a href="192.htm">上一层</a>][<a href="277.htm">下一篇</a>]
<p align="center"><a href="http://cterm.163.net">欢迎访问Cterm主页</a></p>
</table>
</body>
</html>
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -