一个简易的proxy程序的开发过程(1)
1、引言
很多人都看过Eric Steven Raymond写的<<The Cathedral and the Bazaar>> (大教堂与集市) 这篇文章吧。
这篇文章讲述了传统的开发小组开发方式和基于Internet的分散的开发方式(linux的开发方式,GNU软件的
开发方式)的区别,并且根据自己的一个程序的开发例子来讲述了The Bazaar开发方式的若干条重要原则。
不过,国内很多程序员,工作的时候还是采用的传统的开发方式,很难有机会在工作中体验这些原则。那么,
这个例子就给了大家又一个体验这些原则的过程。
这个例子,主要是运用了一些编程的技术,比如,socket编程,信号,进程,还有一些unix socket编程的较
高级论题。当然,这些都不是主要的,重要的是,体验一下集市的开发方式。
2、开发这个proxy程序的背景
我工作的时候,处在一个比较封闭的网络环境中。我的机器在局域网 (LAN) 之中,与外界的Internet相连采
用了代理的方式,有若干台unix服务器作为代理服务器,运行squid作为http的代理,运行socks作为socks 5
代理。应该说,这样的待遇,还算不错,:-), 要浏览网站,squid够用了;要运行ICQ, OICQ之类的程序,用
socks也够了。但是,我碰到了一个比较麻烦的问题,在这样的网络环境中,我没有办法用Outlook等工具收取
非来自公司邮件服务器的邮件(比方说,@linuxaid.com.cn, @163.net, @sina.com.cn 等等);也没有办法用
Gravity等工具来收取USENET上的讨论。当然,折衷的办法还是有,我可以用linux下的一些支持socks的邮件
客户端软件和新闻组阅读软件。但是,这样势必造成一些麻烦( 实际上我也这样做过 ),当我需要收取邮件
或者阅读新闻组的时候,我必须重新启动机器转换到linux操作系统中去,而当我要办公的时候,我又不得不
重新启动机器再转换到windows操作系统中来 ( 我不得不说,linux作为办公的桌面还是不如windows, 虽然这
句话肯定会惹恼很多linux fan :-) )。作为一个程序员,我当然不能忍受这种麻烦。我必须想办法来解决这
个问题。经过考虑,我有了一个好的想法。
这体现了The Bazaar原则一:
Every good work of software starts by scratching a developer's personal itch.
每一个软件的开发都是带有开发者自己的烙印。
3、初期设计
我需要的是一个程序,他能够做"二传手"的工作。他自身处在能同时连通外界目标服务器和我的机器的位置上。
我的机器把请求发送给他,他接受请求,把请求原封不动的抄下来发送给外界目标服务器;外界目标服务器
响应了请求,把回答发送给他,他再接受回答,把回答原封不动的抄下来发送给我的机器。这样,我的机器
实际上是把他当作了目标服务器( 由于是原封不动的转抄,请求和回答没有被修改 )。而他则是外界目标
服务器的客户端( 由于是原封不动的转抄,请求和回答没有被修改 )。我把这种代理服务程序叫做"二传手"。
原理图如下:
|--------------| |-----------------| |--------------------|
| |------------------>| |---------------->| |
| 我的机器 | | 代理服务程序 | | 目标服务器 |
| |<------------------| |<----------------| |
|--------------| |-----------------| |--------------------|
4、例子重用
The Bazaar原则二:
Good programmers know what to write. Great ones know what to rewrite (and reuse).
好的程序员知道写什么。而伟大的程序员知道重写和重用什么。
基于这个原则,我当然不会从头来写这个程序(其实,这个程序是一个很小的程序,没有必要一定要这么做。
但是,为了给大家,同时也是给我自己一个集市化的开发方式的体验,我还是这么做了,我先是写出来了一个
简单的程序---附在本文最后----然后才想起来去找找有没有类似的程序 :-), 结果浪费了很多时间)。
在网上找了找,花了大概半个小时( 和我写出第一个简单程序所花的时间差不多 :-) ),找到了这个程序。
程序如下:
------------------------------------------------------------------------------------------------
/****************************************************************************
program: proxyd
module: proxyd.c
summary: provides proxy tcp service for a host on an isolated network.
programmer: Carl Harris (ceharris@vt.edu)
date: 22 Feb 94
des cription:
This code implements a daemon process which listens for tcp connec-
tions on a specified port number. When a connection is established,
a child is forked to handle the new client. The child then estab-
lishes a tcp connection to a port on the isolated host. The child
then falls into a loop in which it writes data to the isolated host
for the client and vice-versa. Once a child has been forked, the
parent resumes listening for additional connections.
The name of the isolated host and the port to serve as proxy for,
as well as the port number the server listen on are specified as
command line arguments.
****************************************************************************/
#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 <netinet/in.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_errlist[]; /* defined by libc.a */
void parse_args (int argc, char **argv);
void daemonize (int servfd);
void do_proxy (int usersockfd);
void reap_status (void);
void errorout (char *msg);
/****************************************************************************
function: main
des cription: 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
",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
",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! */
close(newsockfd);
}
}
/****************************************************************************
function: parse_args
des cription: 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>
",
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
",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
",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
",
pargs.service_name);
exit(1);
}
}
/****************************************************************************
function: daemonize
des cription: detach the server process from the current context,
creating a pristine, predictable environment in which it
will execute.
arguments:
servfd file des criptor 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
",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
",stderr);
exit(1);
}
/* lose controlling terminal */
if ((fd = open("/dev/tty",O_RDWR)) >= 0) {
ioctl(fd,TIOCNOTTY,NULL);
close(fd);
}
/* close any open file des criptors */
for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++)
if (fd != servfd)
close(fd);
/* set working directory to / to allow filesystems to be unmounted */
chdir("/");
/* clear the inherited umask */
umask(0);
/* setup zombie prevention */
signal(SIGCLD,reap_status);
}
/****************************************************************************
function: do_proxy
des cription: 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_errlist[errno]);
strcat(buf,"
");
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 */
write(isosockfd,buf,iolen); /* copy to host -- blocking semantics */
}
/* is the host sending data? */
if (FD_ISSET(isosockfd,&rdfdset)) {
if ((iolen = read(isosockfd,buf,sizeof(buf))) <= 0)
break; /* zero length means the host disconnected */
write(usersockfd,buf,iolen); /* copy to client -- blocking semantics */
}
}
/* we're done with the sockets */
close(isosockfd);
close(usersockfd);
}
/****************************************************************************
function: errorout
des cription: 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
",msg);
fclose(console);
exit(1);
}
/****************************************************************************
function: reap_status
des cription: 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 */
}
------------------------------------------------------------------------------------------------
( 这个程序来自水木清华BBS精华版 )
看了这个程序,我细化了我的初步设计: 程序监听服务端口,接受客户端连接,派生出子进程处理连接,
同时连接远程机器的服务端口,然后开始完成"二传手"的工作。
当然,这个小程序也有不足的地方:
a、他只能监听一个服务端口,只能连接一个远程机器的服务端口。
b、他采用了子进程的方式,假如客户端连接很多,就会给服务器造成比较大的压力。
c、他只能监听tcp,而不能作为udp的代理服务器 ( 广大 OICQ 用户都知道,这个程序不能用来做 OICQ
代理)。
d、他只能用命令行的方式读入服务端口,远程服务器地址和端口,不能用配置文件的方式。
所以,我还是决定继续完善我自己的程序,而不是用他。
The Bazaar原则三:
Plan to throw one away; you will, anyhow.
5、第一版的代码
我的小程序,第一版本如下:
------------------------------------------------------------------------------------------------
/***************************************************************
Program: sp.c
Des cription: a smart proxy
Author: Alan Chen (ariesram@may10.ca)
Date: July 10, 2001
***************************************************************/
#include <stdio.h>
#include <string.h>
#includ
e <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <unistd.h>
#include <netinet/in.h>
int do_proxy(int infd);
int max(int i, int j);
void waitchild(int);
int main(void) {
struct sockaddr_in servaddr, clientaddr;
int listenfd, connfd;
int clientlen;
pid_t chpid;
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(7000);
servaddr.sin_addr.s_addr = INADDR_ANY;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if(listenfd < 0) {
printf("socket error
");
exit(-1);
}
if( bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) {
printf("bind error
");
exit(-1);
}
if( listen(listenfd, 5) < 0 ) {
printf("listen error
");
exit(-1);
}
signal(SIGCHLD, waitchild);
for(;;) {
connfd = accept( listenfd, (struct sockaddr *)&clientaddr,
&clientlen );
if( connfd < 0 ) {
printf("accept error
");
exit(-1);
}
if( (chpid = fork()) == -1 ) {
printf("fork error
");
exit(-1);
}
if( chpid == 0 ) {
close(listenfd);
do_proxy(connfd);
exit(0);
}
if( chpid > 0 ) {
close(connfd);
}
}
exit(0);
}
int do_proxy(int infd) {
struct sockaddr_in rout;
int outfd;
int maxfd;
int count = 65535;
int n;
fd_set set;
char buf[count];
bzero(&rout, sizeof(rout));
rout.sin_family = AF_INET;
rout.sin_port = htons(7001);
rout.sin_addr.s_addr = inet_addr("127.0.0.1");
if( (outfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("socket error
");
exit(-1);
}
if( connect(outfd, (struct sockaddr *)&rout, sizeof(rout)) < 0 ) {
printf("connect error
");
exit(-1);
}
while(1) {
FD_ZERO(&set);
FD_SET(infd, &set);
FD_SET(outfd, &set);
maxfd = max(outfd, infd);
if( select(maxfd + 1, &set, NULL, NULL, NULL) < 0 ) {
perror("select error:");
exit(-1);
}
if( FD_ISSET(infd, &set) ) {
n = read(infd, (void *)buf, count);
if( n <= 0)
break;
if( write(outfd, (const void *)buf, n) != n ) {
printf("write error
");
continue;
}
}
if( FD_ISSET(outfd, &set) ) {
n = read(outfd, (void *)buf, count);
if( n <= 0)
break;
if( write(infd, (const void *)buf, n) != n ) {
printf("write error
");
continue;
}
}
}
close(infd);
close(outfd);
}
int max(int i, int j) {
return i>j?i:j;
}
void waitchild(int signo) {
int status;
pid_t childpid;
if( (childpid = waitpid(-1, &status, WNOHANG)) < 0 ) {
printf("wait error
");
exit(1);
}
printf("child %d quitted
", childpid);
return;
}
------------------------------------------------------------------------------------------------
下面简单解释一下程序。对 socket 网络编程比较熟悉的就不要看了。:-)
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(7000);
servaddr.sin_addr.s_addr = INADDR_ANY;
给出一个sockaddr_in结构,定义了服务器的端口号和地址。
listenfd = socket(AF_INET, SOCK_STREAM, 0);
socket()函数返回一个socket类型的描述字,类型为AF_INET ( IPv4 ), SOCK_STREAM ( TCP ) .
if(listenfd < 0) {
printf("socket error
");
exit(-1);
}
假如socket()函数返回值为小于0, 则表示出错。
if( bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) {
printf("bind error
");
exit(-1);
}
绑定描述字和服务器地址端口。假如bind()函数返回值为小于0, 则表示出错。
signal(SIGCHLD, waitchild);
指定SIGCHLD信号的处理函数为waitchild()。当主进程fork()出的子进程结束的时候,主进程会收到
一个SIGCHLD信号,内核发送这个信号的目的是为了让主进程有机会能够检查子进程的退出状态,并
做一些清理工作( 假如必要的话 )。假如主进程不处理SIGCHLD信号,子进程将会变成僵尸进程,
直到主进程退出,被init进程接管,被init进程清理掉。
waitchild() 函数如下:
void waitchild(int signo) {
int status;
pid_t childpid;
if( (childpid = waitpid(-1, &status, WNOHANG)) < 0 ) {
printf("wait error
");
exit(1);
}
printf("child %d quitted
", childpid);
return;
}
注重:signal处理函数必须定义成 void func(int)形式。
waitpid(-1, &status, WNOHANG)等待子进程退出,并且获取子进程的退出状态保存到status里。
printf("child %d quitted
", childpid);打印子进程的进程号。
if( listen(listenfd, 5) < 0 ) {
printf("listen error
");
exit(-1);
}
启动监听,指定等待队列长度为5。假如listen()函数返回值为小于0, 则表示出错。
for(;;) {
connfd = accept( listenfd, (struct sockaddr *)&clientaddr,
&clientlen );
if( connfd < 0 ) {
printf("accept error
");
exit(-1);
}
if( (chpid = fork()) == -1 ) {
printf("fork error
");
exit(-1);
}
if( chpid == 0 ) {
close(listenfd);
do_proxy(connfd);
exit(0);
}
if( chpid > 0 ) {
close(connfd);
}
}
在for(;;){}这个无限循环中,进程阻塞于accept。
accept( listenfd, (struct sockaddr *)&clientaddr,
&clientlen )
等待客户端连接,假如连接成功,则在clientaddr中返回客户端的IP地址以及端口号,协议类型等信息,同时
clientaddr的长度存于clientlen中。accept返回socket连接描述字connfd.假如accept()函数返回
值为小于0,
则表示出错。
连接成功,主进程采用fork()派生子进程。假如FORK()函数返回值为小于0, 则表示出错。
在主进程中( chpid > 0 ),关闭connfd描述字,并继续for(;;){}循环。在子进程中( chpid == 0 ),关闭
listenfd监听socket描述字,并调用do_proxy()函数 ( 稍候介绍,用于完成proxy的工作 )。等待do_proxy
()函数返回,并且退出子进程。
注重:fork() 函数是调用一次,返回两次,一次返回在主进程中,一次返回在子进程中。
下面介绍do_proxy()函数。
bzero(&rout, sizeof(rout));
rout.sin_family = AF_INET;
rout.sin_port = htons(7001);
rout.sin_addr.s_addr = inet_addr("127.0.0.1");
定义连接的远程服务端口。由于这个程序是基于测试目的,为了方便,我把远程服务定义为本机的7001端口
( 也就是说,实际上走的是loopback interface )。
if( (outfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("socket error
");
exit(-1);
}
socket()函数返回一个socket类型的描述字,类型为AF_INET ( IPv4 ), SOCK_STREAM ( TCP ) .
假如socket()函数返回值为小于0, 则表示出错。
if( connect(outfd, (struct sockaddr *)&rout, sizeof(rout)) < 0 ) {
printf("connect error
");
exit(-1);
}
connect()函数连接远程服务地址和端口,假如connect()函数返回值为小于0, 则表示出错。
在while(1) { } 无限循环中:
FD_ZERO(&set);
清空fd_set
FD_SET(infd, &set);
FD_SET(outfd, &set);
把infd ( 是从主程序中传进来的,就是连接描述字connfd ), outfd ( 连接远程服务的描述字 )放进
fd_set。
maxfd = max(outfd, infd);
取两个描述字的最大值。
max() 函数定义如下:
int max(int i, int j) {
return i>j?i:j;
}
很简单,就不用解释了。
if( select(maxfd + 1, &set, NULL, NULL, NULL) < 0 ) {
perror("select error:");
exit(-1);
}
阻塞于 select() 函数, 等待infd, outfd中任意描述字可读。
这里稍微解释一下:maxfd + 1, select函数要求第一个参数是集合中描述字最大值加1 ( 很多人经常忘记了
加上1,结果导致select函数出错 ) 。我把可写,异常两个集合都定义为空,因为我们不必关心这两个集合。
超时设置为NULL, 这表示假如没有描述字不可读,将永远阻塞在select 函数中。( 在以后的版本里面,我将修改
这一函数调用,以增强程序性能。假如select()函数返回值为小于0, 则表示出错。
if( FD_ISSET(infd, &set) ) {
n = read(infd, (void *)buf, count);
if( n <= 0)
break;
if( write(outfd, (const void *)buf, n) != n ) {
printf("write error
");
continue;
}
}
假如select返回值大于0,检测是否infd可读。假如可读,则从infd中读出数据,并写回到outfd中。这里,
假如read返回值小于或者等于0,表示服务器写入了终止符号或者服务器停止服务 ( 这里的情况比较复杂,需要注重。
)假如read出错,则终止循环。假如write写入outfd的字节数不为n则表示write出错 ( 原因可能是客户端终止或者其他异常情况 )。
但是,需要注重的是,当write出错的时候,我们并不退出,而是继续 while(1) { }循环。
if( FD_ISSET(outfd, &set) ) {
n = read(outfd, (void *)buf, count);
if( n <= 0)
break;
if( write(infd, (const void *)buf, n) != n ) {
printf("write error
");
continue;
}
}
假如select返回值大于0,检测是否outfd可读。假如可读,则从outfd中读出数据,并写回到infd中。这里,
假如read返回值小于或者等于0,表示服务器写入了终止符号或者服务器停止服务 ( 这里的情况比较复杂,需要注重。
)假如read出错,则终止循环。假如write写入outfd的字节数不为n则表示write出错 ( 原因可能是客户端终止或者其他异常情况 )。
但是,需要注重的是,当write出错的时候,我们并不退出,而是继续 while(1) { }循环。
这一部分就是初步设计中的思想的实现。就是这两段程序完成了"二传手"的工作。
close(infd);
close(outfd);
当循环因为服务端或者客户端终止或者其他出错退出,则关闭两个描述字,并返回。
5、测试第一版的程序
为了测试我的小程序是否能够按希望的方式运行并且得到正确的结果,我写了另外一个小程序用来辅助测试
的工作。
程序清单如下:
-----------------------------------------------------------------------------------------------
/*******************************************************
Program: echos.c
Des cription: an echo server
Author: Alan Chen (ariesram@may10.ca)
Date: July, 10, 2001
*******************************************************/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
void do_echo(int);
void waitchild(int signo);
int main(void) {
struct sockaddr_in servaddr, clientaddr;
int clientlen;
int listenfd, connfd;
pid_t childpid;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if( listenfd < 0 ) {
printf("socket error
");
exit(1);
}
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(7001);
servaddr.sin_addr.s_addr = INADDR_ANY;
if( bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0 ) {
printf("bind error
");
exit(1);
}
if( listen(listenfd, 5) < 0 ) {
printf("listen error
");
exit(1);
}
signal(SIGCHLD, waitchild);
while(1) {
if( (connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientlen)) < 0 ) {
printf("accept error
");
exit(1);
}
if( (childpid = fork() ) < 0 ) {
printf("fork error
");
exit(1);
}
if( childpid == 0 ) {
close(listenfd);
do_echo(connfd);
exit(0);
}
close(connfd);
}
exit(0);
}
void do_echo(int fd) {
int n;
char buf[1024];
while(1) {
if( (n = read(fd, (void *)&buf, 1024)) <= 0 )
break;
if( (n = write(fd, (void *)&buf, n)) <= 0)
break;
}
}
void waitchild(int signo) {
int status;
pid_t chpid;
if( (chpid = waitpid(-1, &status, WNOHANG)) < 0 ) {
printf("waitpid error
");
exit(-1);
}
printf("child %d quitted.
", chpid);
return;
}
-----------------------------------------------------------------------------------------------
这个程序比较简单,功能是把客户端输入的字符返回给客户端。当客户端终止时,则停止子进程。
程序解释完了,我们来看一下运行结果。
首先,编译这两个程序。
gcc -o sp sp.c
gcc -o echos echos.c
运行.
./sp
./echos
看看程序初始化的时候端口的状态。
[alan@ariesram proxy]$ netstat -na | grep 700
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN
sp, echos分别监听两个端口,7000 和 7001。
启动一个客户端,连接sp的服务端口7000。
[alan@ariesram proxy]$ telnet localhost 7000
Trying 127.0.0.1...
Connected to ariesram.
Escape character is '^]'.
再来看看端口的状态。
[alan@ariesram alan]$ netstat -na | grep 700
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:32769 127.0.0.1:7000 ESTABLISHED
tcp 0 0 127.0.0.1:7001 127.0.0.1:32770 ESTABLISHED
tcp 0 0 127.0.0.1:32770 127.0.0.1:7001 ESTABLISHED
tcp 0 0 127.0.0.1:7000 127.0.0.1:32769 ESTABLISHED
其中,
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN
仍然处在监听状态。
而
tcp 0 0 127.0.0.1:32769 127.0.0.1:7000 ESTABLISHED
是我启动的telnet连接到sp服务端口的连接。
同时,sp发起了一个到目的服务端口7001的连接。
tcp 0 0 127.0.0.1:32770 127.0.0.1:7001 ESTABLISHED
另外,
tcp 0 0 127.0.0.1:7000 127.0.0.1:32769 ESTABLISHED
tcp 0 0 127.0.0.1:7001 127.0.0.1:32770 ESTABLISHED
分别是sp代理服务程序连接客户端和远程目标服务端口连接代理服务程序的连接。假如是remote方式
的话,是看不到这两个连接的。
在telnet客户端输入字符串做测试,看是否能够把输入字符串原样返回。
[alan@ariesram proxy]$ telnet localhost 7000
Trying 127.0.0.1...
Connected to ariesram.
Escape character is '^]'.
asdf
asdf
sadf
sadf
asdfasdfasdfasfd
asdfasdfasdfasfd
结果显示,我们的程序是成功的。:-)
退出telnet客户端,再来看看端口的状态。
[alan@ariesram proxy]$ netstat -na | grep 700
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:32769 127.0.0.1:7000 TIME_WAIT
tcp 0 0 127.0.0.1:32770 127.0.0.1:7001 TIME_WAIT
我们可以看到,由 telnet 客户端发起的连接和代理服务程序sp发起的连接都处于close过程的TIME_WAIT
状态。该状态的持续时间是最长分节生命周期 MSL ( maximum segment lifetime ) 的两倍,有时候称作
2MSL。
存在TIME_WAIT状态的两个理由:
1、实现终止TCP全双工连接的可靠性。
2、答应老的重复分节在网络中消逝。
而其中,
tcp 0 0 0.0.0.0:7000 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:7001 0.0.0.0:* LISTEN
仍然处在监听状态, 直到echos, sp两个程序退出。
6、小结
以上讲述了第一版的开发以及测试过程。我们看到,我的初步设想是能够实现的。接下来需要做的是
将代理服务程序修改成为一个可用的版本。需要做的事情是:
a、修改程序运行方式,使其能从命令行读入 option,设定监听端口和所要连接的远程服务地址以及
端口。
b、使程序能够以后台方式运行 ,而不是前台方式,成为一个真正的服务程序。( 现在的版本当用户
退出控制台的时候会终止运行。)
进一步的工作是:
c、使程序能够监听多个端口,并且连接多个远程服务。
d、使程序能够从配置文件中读取设定监听端口和所要连接的远程服务地址以及端口以满足多种服务并存
的需要。
这些工作我将在下一部分文章中描述。
有什么问题、意见,可以通过电子邮件和我联系。
视频教程列表
文章教程搜索
C语言程序设计推荐教程
C语言程序设计热门教程
|