首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 软件管理 > 软件开发 >

简易的proxy程序的开发过程(一)

2012-07-27 
简易的proxy程序的开发过程(1)摘要:一个简易的proxy程序的开发过程。这个例子,主要是运用了一些编程的技术,

简易的proxy程序的开发过程(1)
摘要:一个简易的proxy程序的开发过程。这个例子,主要是运用了一些编程的技术,比如,socket编程,信号,进程,还有一些unix socket编程的较高级论题。当然,这些都不是主要的,重要的是,体验一下集市的开发方式
  
  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).
  好的程序员知道写什么。而伟大的程序员知道重写和重用什么。
    基于这个原则,我当然不会从头来写这个程序(其实,这个程序是一个很小的程序,没有必要一定要这么做。但是,为了给大家,同时也是给我自己一个集市化的开发方式的体验,我还是这么做了,我先是写出来了一个简单的程序---附在本文最后----然后才想起来去找找有没有类似的程序 :-), 结果浪费了很多时间)。
  
    在网上找了找,花了大概半个小时( 和我写出第一个简单程序所花的时间差不多 :-) ),找到了这个程序。
  
    程序如下:
  
  ( 这个程序来自水木清华BBS精华版 )
  
    看了这个程序,我细化了我的初步设计: 程序监听服务端口,接受客户端连接,派生出子进程处理连接,同时连接远程机器的服务端口,然后开始完成"二传手"的工作。
  
    当然,这个小程序也有不足的地方:
  
  他只能监听一个服务端口,只能连接一个远程机器的服务端口。
  他采用了子进程的方式,如果客户端连接很多,就会给服务器造成比较大的压力。
  他只能监听tcp,而不能作为udp的代理服务器 ( 广大 OICQ 用户都知道,这个程序不能用来做 OICQ代理)。
  他只能用命令行的方式读入服务端口,远程服务器地址和端口,不能用配置文件的方式。
    所以,我还是决定继续完善我自己的程序,而不是用他。
  
    The Bazaar原则三:
  
  Plan to throw one away; you will, anyhow.
  5.第一版的代码
  
    我的小程序,第一版本如下:
  
    下面简单解释一下程序。对 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() 函数是调用一次,返回两次,一次返回在主进程中,一次返回在子进程中。

热点排行