proxy_epoll源代码分析 linux网络编程入门的源码分析资料,附源码

代理服务器源代码分析--谈谈如何学习linux网络编程

Linux是一个可靠性非常高的操作系统.Linux发行版一直被用来作为服务器的操作系统,并且已经在该领域中占据重要地位。根据2006年9月en:Netcraft的报告显示,十个最大型的网络托管公司有八个公司在其Web服务器运行Linux发行版。Linux强大的功能也是一个非常重要的原因,尤其是Linux强大的网络功能更是引人注目,网络功能在Linux下占有核心的地位。放眼今天的银行网络业务和曾经红透半边天的电子商务,到今天的云计算Hadoop、海量数据处理等 无不都基于Linux的解决方案。因此Linux网络编程是非常重要的,而且Linux网络编程本身就是一件有趣的事情。在刚开始学习linux网络编程时往往摸不着头脑刚开始时我们往往不知到如何编写linux上的网络程序,这是我们要做的不是刚开始就在电脑面前 纠结怎么去写自己的所谓高效的程序,我们应该在读了一些代码的基础上 来形成我们自己的编程思路(为何要读代码我们在看这些代码的基础之上然后写出自己的代码,可能会更好些,并且你将会体会其中的乐趣。下面我就从一段proxy_epoll源代码开始,谈谈Linux网络编程的一些基础知识,带领大家进入linux网络编程的世界

proxy_epoll 程序是在CarlHarris的程序proxy(下载地址)的基础之上形成的,这位大虾编写了这段代码并将其散播到网上供大家学习讨论,然后我在学习这段代码之后,又在读了写关于epoll的书籍之后突发奇想的把它改造成具有epoll模型的简单代理服务器。代码虽然简单的描述了proxy操作,但它不仅清晰地描述了客户机/服务器系统的概念描述了Linux网络编程的其他很多有用的知识对于Linux网络编程的初学者是个不错的参考资料
  关于程序proxy_epoll的用法,我们可以使用这个代理登录其它主机的服务端口。
程序proxy_epoll的用法
   ./proxy_epoll <proxy_port> <remote_host><service_port>
  其中参数proxy_port是指由我们指定的代理服务器端口。参数remote_host是指我们希望连接的远程主机的主机名,IP地址也同样有效。这个主机名在网络上应该是唯一的,如果您不确定的话,可以在远程主机上使用uname-n命令查看一下。参数service_port
是远程主机可提供的服务名,也可直接键入服务对应的端口号。这个命令的相应操作是将代理服务器的proxy_port端口绑定到remote_hostservice_port端口。然后我们就可以通过代理服务器的proxy_port端口访问remote_host

例如一台计算机,网络主机名是dane@ubuntuIP地址为172.16.0.12(内网IP)这个机器是我们指定的remote_host远程主机

网络配置如下:

proxy_epoll源代码分析 linux网络编程入门的源码分析资料,附源码_第1张图片

wang@wang-OptiPlex-320 网络配置如下:

     proxy_epoll源代码分析 linux网络编程入门的源码分析资料,附源码_第2张图片

如果在我的计算机wang@wang-OptiPlex-320(ip为172.16.0.11执行命令
  wang@wang-OptiPlex-320:~/desk$sudo ./proxy_epoll 5000 172.16.0.12 telnet

 

此时我们在机器wang@wang-OptiPlex-320 回车后效果如下(此时代理服务器变为守护进程)

wang@wang-OptiPlex-320:~/desk$

此时可知机器 wang@wang-OptiPlex-320 的端口5000已经成为代理服务器的监听端口。

  查看 5000端口被打开命令:

  netstat -anpl |grep 5000

(a代表全部(all),这个所谓的全部就是指包括正在监听的端口,p:显示占用该端口号的进程,n:直接显示端口号,l:显示正在被监听的端口)

wang@wang-OptiPlex-320:~$sudo netstat -anpl | grep 5000

结果: tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      3304/proxy

可知代理服务启动正常,且进程的ID 为3304 (最后不需要代理服务时可以通过sudo kill 3304,关闭它)。

此外也可以通过命令: sudo netstat  -tnpl  来查看所有正在监听的被打开的进程ID及端口号。

 那么我们就可以通过下面这条命令访问172.16.0.12telnet端口(注意此时,wang-OptiPlex-320 telnet的服务已被绑定到代理服务器的5000端口)

 

此时我们在另一台机器上(操作系统为:win7,IP为 172.16.0.139)的cmd上键入命令:telnet 172.16.0.11 5000

(即客户主机win7,172.16.0.139,通过代理服务器(wang@wang-OptiPlex-320,172.16.0.11 端口5000)访问目的主(dane@ubuntu,172.16.0.12 telnet 23端口))
-----------------------------------------------------------------
    C:\Users\Administrator>telnet 172.16.0.11 5000

回车后效果如下:

       proxy_epoll源代码分析 linux网络编程入门的源码分析资料,附源码_第3张图片

-----------------------------------------------------------------
  上面的绑定操作也可以使用下面的命令:
   wang@wang-OptiPlex-320:~/desk$./proxy_epoll 5000 172.16.0.11 23
   23telnet服务的标准端口号,其它服务的对应端口号我们可以在/etc/services中查看。

实验平台:

实验中主要利用两台机器还有一个虚拟机。关于如何让电脑中的虚拟机就像真实网络中的局域网中的硬件电脑。我们需要利用桥接技术,具体参见资源VMware虚拟机配置Ubuntu上网(Bridged)的配置方式。


利用这个程序我们可以很容易的实现从外网访问我们内网的计算机,方法如下:

首先在我们内网的路由器上把某个端口绑定到我的计算机上(假设你的计算机配置的是静态IP比如192.168.16.22,你可以把路由器上端口22222绑定到你       的IP上)这样这要在你的linux系统上运行程序 ./proxy_epoll 2222 192.168.16.22  ssh 。 然后你在外网的计算机通过ssh就可以通过内网的路由器外     网 IP + 我们绑定的端口 2222 访问我们的内网的计算机了(想想其中的原理,是不是很有趣)。


proxy_epoll.h

#ifndef PROXY_EPOLL
#define PROXY_EPOLL
/*
* 功能:将普通进程改造成守护进程
* 参数:listenfd为服务器监听套接子 
*/
void daemonize(int listenfd);

/*
* 功能:设置描述符为非阻塞
* 参数:fd即为所要设置描述符
*/
int set_socket_noblock(int fd);

/*
* 功能:连接远程主机
* 参数:usersock是需要代理的用户socket,argv是main传进的参数
*/
int connect_isolate(int usersockfd, char *argv[]);

/*
* 功能: 代理服务器主题部分,创建代理服务器,并且利用epoll模型进行相应的事件处理。
* 参数: proxy_port:代理服务端口 argv:main函数传进来关于
                远程主机地址及其服务端口信息
*/
void proxy(int proxy_port, char *argv[]);

/*
* 功能:重定向标准输入输出
* 原因:因为守护进程脱离控制终端,不能简单把出错信息写的标准输入输出,
* 此外可以调用syslogd守护进程,用户调用syslog().但有时syslog()系统调用
* 会不起作用,此时无法用syslog()记录出错信息。那么此时只能重定向标准输
* 入输出了。
* 参数:infile为重定向的标准输入文件名,outfile为重定向的标准输出文件名
* errfile为重定向的标准错误输出文件名。
*/
void redirect_stdIO(char *inFile, char *outFile, char *errFile);

/*
* 功能:输出错误信息,在文件/tmp/daemon.log
* 参数:msg为输出的错误信息
*/
void errout(char *msg);

#endif


proxy_epoll.c

#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#include <syslog.h>
#include <sys/types.h>/* syslog定义*/
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <errno.h>/* 错误号定义*/
#include <fcntl.h>/* 文件控制定义*/
#include <unistd.h> /* Unix 标准函数定义*/
#include <sys/ioctl.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <map> /* C++ STL map容器*/
#include "proxy_epoll.h"

using namespace std;

#define TCP_PROTO "tcp"
#define LOG_FILE "/tmp/daemon.log"
#define MAXEVENTS 64

// 开启daemon, stdout, stderr将被输出到/tmp/daemon.log
int main(int argc, char *argv[])
{
	int i, len, proxy_port;
	if(argc < 4){
		printf("Usage:%s<proxy-port> <host | ip> <service-name | port-number>\n", argv[0]);
		exit(1);
	}
	char buf[32];
	strcpy(buf, argv[1]);
	len = strlen(buf);
    for(i=0 ; i < len; i++)
        if(!isdigit(buf[i]))
            break;
    if(len != i){
        printf("Invalid proxy port %s\n", proxy_port);
        exit(1);
	}
    proxy_port = atoi(argv[1]);
    proxy(proxy_port,argv);
	return 0;
}
详细源码见附件
......

proxy_epoll.c就是proxy_epoll源代码的主程序部分.


下面详细的介绍下这段代码:


下面我们来介绍一些程序中实现的函数。

@ main 函数

// 开启daemon, stdout, stderr将被输出到/tmp/daemon.log
int main(int argc, char *argv[])
{
    int i, len, proxy_port;
    if(argc < 4){
        printf("Usage:%s<proxy-port> <host | ip> <service-name | port-number>\n", argv[0]);
        exit(1);
    }
    char buf[32];
    strcpy(buf, argv[1]);
    len = strlen(buf);
    for(i=0 ; i < len; i++)
        if(!isdigit(buf[i]))
            break;
    if(len != i){
        printf("Invalid proxy port %s\n", proxy_port);
        exit(1);
    }
    proxy_port = atoi(argv[1]);
   proxy(proxy_port,argv);
    return 0;
}


mian()函数顾名思义是我们的主函数。他的的作用就是检查用户输入的命令行参数是否正确,若果正确,然后调用proxy()进行代理服务。
检验命令行参数:
  函数首先要检测命令行参数是否符合程序的要求,即在命令后紧跟代理服务器端口、远程主机名和服务端口号,如果不满足上述要求,则代理服务器程序结束。
  首先传送代理服务器端口proxy_port,在这里程序调用了一个系统函数isdigit()检验用户输入的端口号是否有效。isdigit()的具 体描述为:

-----------------------------------------------------------------
#include <ctype.h>
  int isdigit(int c)
-----------------------------------------------------------------
  isdigit()函数用来检测参数"c"是否是数字1~9中间的一个,如果答案是肯定的,则返回非"0"值,反之,返回"0"。程序中采用了这样的方法来对用户的输入进行逐位检验
   if(!isdigit(buf[i]))
   break;

  在将有效端口号传递给函数proxy之后,还要将其转换成为网络字节顺序。这是因为网络中存在着多个公司的不同设备,这些设备表示数据的字节顺序是不同的。例如在内存地址0x1000处存储一个16位的整数FF11,不同公司的机器在内存中的存储方式也不相同,有的将FF置于内存指针的起始位置0x100011置于0x1001,这称为big-endian顺序;有的却恰恰相反,即little-endian顺序。这种基于主机的数据存储顺序就称为主机字节顺序(host byte order)。为了在不同类型的主机之间进行通信,TCP/IP网络协议就规定了一种统一的网络字节顺序,这种顺序被规定为big-endian顺序。所以数据的网络字节顺序和主机字节顺序有可能是不同的,因此在编写通信程序时一定要注意不同顺序之间的转换。所以,程序中一定要有例程中这样的语句:   

          proxy_port =atoi(argv[1]);

         在proxy函数中 需要调用htons 进行字节序转换。

  函数htons()的作用就是将主机字节顺序转换为网络字节顺序。它的具体描述为:

-----------------------------------------------------------------
#include <netinet/in.h>
unsigned short int htons(unsigned short int data)
-----------------------------------------------------------------
  与htons()相似的函数还有三个,它们分别是htonl()ntohs()ntohl(),都用于网络与主机字节顺序之间的转换。如果这几个名字比较容易混淆的话,我们可以这样记忆:函数名中的h代表hostn代表networks代表unsigned shortl代表unsigned long所以"hton"即为"host-to-network":变换主机字节为网络字节。接收数据的就要用到"ntoh""network-to-host")函数了。
  在我们的例程中,由于端口号一般情况下最多不会超过4位数字,所以选用unsigned short型的htons()即可。
注意在例程中htons()的参数是另一个函数atoi()的返回结果atoi()函数的具体描述为:
-----------------------------------------------------------------
#include <stdlib.h>
int atoi(const char *nptr)
-----------------------------------------------------------------

  它的作用是将字符指针nptr指向的字符串转换成相应的整数并将其作为结果返回。这个操作与函数调用strtol(nptr,(char **)NULL,10)的效果几乎完全相同,唯一的区别是atoi()没有出错返回信息。之所以要调用这个函数是因为,系统在读取命令行的时候将所有的参数都作为字符串处理,所以我们必须将其转换为整数形式    

@ errout 函数。
   

// 写错误日志
void errout(char *msg)
{
    if(msg)// 开启daemon错误将被输出到/tmp/daemon.log
        printf("%s\n", msg);
    exit(1);
}


此函数的作用就是吧把我们的程序出现的错误重定向输出到文件/tmp/daemon.log 中。
下面介绍怎么实现的错误重定向的。

@redirect_stdIO 函数。
   

// 重定向标准输入输出
void redirect_stdIO(char *szInFile, char *szOutFile, char *szErrFile)
{
    int fd;
    openlog("proxy_epoll_log", LOG_CONS | LOG_PID, 0);
    if (NULL!= szInFile) {
        fd = open(szInFile, O_RDONLY| O_CREAT, 0666);
        if (fd> 0) {
            // 标准输入重定向
            if (dup2(fd, STDIN_FILENO)< 0) {
                syslog(LOG_ERR, "redirect_stdIO dup2 in");
                exit(1);
            }
            
            close(fd);
        }
        else
            syslog(LOG_ERR, "redirect_stdIO open %s: %s\n", szInFile, strerror(errno));
    }
    
    if (NULL != szOutFile) {
        fd = open(szOutFile, O_WRONLY| O_CREAT | O_APPEND/*| O_TRUNC*/, 0666);
        if (fd> 0) {
            // 标准输出重定向
            if (dup2(fd, STDOUT_FILENO)< 0) {
                syslog(LOG_ERR, "redirect_stdIO dup2 out");
                exit(1);
            }
            
            close(fd);
        }
        else
            syslog(LOG_ERR, "redirect_stdIO open %s: %s\n", szOutFile, strerror(errno));
    }
    
    if (NULL!= szErrFile) {
        fd = open(szErrFile, O_WRONLY| O_CREAT | O_APPEND/*| O_TRUNC*/, 0666);
        if (fd> 0) {
            // 标准错误重定向
            if (dup2(fd, STDERR_FILENO)< 0){
                syslog(LOG_ERR, "RedirectIO dup2 error\n");
                exit(1);
            }
            
            close(fd);
        }
        else
            syslog(LOG_ERR, "redirect_stdIO open %s: %s\n", szErrFile, strerror(errno));
    }    
    
    closelog();
}


我们主要利用函数dup2 进行输入输出的重定向。关于此函数的详细用法参见, 点击打开链接


@proxy 函数。

/
/* 
 创建代理服务器端,等待客户端的连接
 并对接受到的数据进行相应的处理
*/
void proxy(int proxy_port, char *argv[])
{
    int i,ret, nready, len;
    map<int, int> sockfd_map;
    int efd, listenfd, usersockfd, isosockfd;
    //声明epoll_event结构体的变量,ev用于注册事件,数组用于回传要处理的事件
    struct epoll_event ev, *pevents;
    struct sockaddr_in serv, cli;
    bzero(&serv, sizeof(serv));
    serv.sin_family = AF_INET;
    serv.sin_port = htons(proxy_port);
    serv.sin_addr.s_addr = htonl(INADDR_ANY);
    // daemonize
    daemonize(listenfd);
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd < 0)
        errout("socket error");
    // set listen nonblock
    ret = set_socket_noblock(listenfd);
    if(ret < 0)
        errout("set socket noblock");
    ret = bind(listenfd, (struct sockaddr*)&serv, sizeof(serv));
    if(ret < 0)
        errout("bind error");
    // ready to listen
    ret = listen(listenfd,10);
    if(ret <0 )
        errout("listen error");
    efd = epoll_create1(0);
    if( efd == -1)
        errout("epoll_create1 error");
    //设置与要处理的事件相关的文件描述符
    ev.data.fd = listenfd;
    //设置要处理的事件类型
    ev.events = EPOLLIN | EPOLLET;
    //注册epoll事件
    if(epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &ev) < 0)
        errout("epoll_ctl error");
    //生成用于处理accept的epoll专用的文件描述符
    pevents =(struct epoll_event*)calloc(MAXEVENTS, sizeof(struct epoll_event));
    // event loop
    for(; ;){
        //等待epoll事件的发生
        nready = epoll_wait(efd, pevents, MAXEVENTS, -1);
        //处理所发生的所有事件 
        for(i = 0; i < nready; i++){
            // an error on this fd or not read read
            if((pevents[i].events & EPOLLERR) ||
               (pevents[i].events & EPOLLHUP) ||
               (!(pevents[i].events & EPOLLIN))){
                perror("epoll_wait");
                close(pevents[i].data.fd);
                continue;
            }// 监听到有新的连接
            else if(listenfd == pevents[i].data.fd){
                // one or more connections
                for(; ;){
                    socklen_t    cli_len = sizeof(cli);
                    // 注意我们虽然 不关注 accept 中cli返回信息但是 其参数不能为0
                    // 否则出现错误 Bad address
                    usersockfd = accept(listenfd, (struct sockaddr*)&cli, &cli_len);
                    if(usersockfd == -1){
                        if(errno == EAGAIN || errno == EWOULDBLOCK){
                            //we have processed the connection
                            break;// no new client
                        }else{// accept failed
                            perror("accept");
                            break;
                        }
                    }
                    // 设置为非阻塞
                    ret = set_socket_noblock(usersockfd);
                    if(ret < 0)
                        break;
                    // 连接被代理的远程的主机,其中isosockfd为与远程主机连接返回的套接字
                    if((isosockfd = connect_isolate(usersockfd, argv)) < 0)
                        errout("connect isolate error");
                    ret = set_socket_noblock(isosockfd);
                    if(ret < 0)
                        break;
                     //设置用于读操作的文件描述符(用户端和代理服务器通信的套接字 usersockfd)
                    ev.data.fd = usersockfd;
                    ev.events = EPOLLIN | EPOLLET;
                    //注册ev
                    ret = epoll_ctl(efd, EPOLL_CTL_ADD, usersockfd, &ev);
                    if(ret < 0)
                        errout("epoll ctl error");
                    //设置用于读操作的文件描述符(代理服务器和远程主机通信的套接字 isosockfd)
                    ev.data.fd = isosockfd;
                    ev.events = EPOLLIN | EPOLLET;
                    ret = epoll_ctl(efd, EPOLL_CTL_ADD, isosockfd, &ev);
                    if(ret < 0)
                        errout("epoll ctl error");
                    // 把这对套接字加入到 map中,这样我们只有知道其中的一个套接字就能很快的找到另一个套接字,从而
                    // 通过代理服务器 把信息从客户端和远程主机间进行转发。
                    sockfd_map[usersockfd] = isosockfd;
                    sockfd_map[isosockfd] = usersockfd;
                }
                continue;
            } else {
                /* We have data on the fd waiting to be read. Read and
                 display it to the other end. We must read whatever data 
                 is available completely, as we are running in 
                 edge-triggered mode and won't get a notification again 
                 for the same data.*/
                int done = 0;
                while (1) {
                    ssize_t count;
                    char buf[2048];
                    //read the send data 
                    count = read(pevents[i].data.fd, buf, sizeof(buf));
                    if(count ==-1){    
                        /* If errno == EAGAIN, that means we have read all data. 
                         So go back to the main loop. */
                        if(errno != EAGAIN){
                            perror ("read");
                            done = 1;
                        }
                        break;
                    }
                    else if (count == 0){
                        /* End of file. The remote has closed the connection. */
                        done = 1;
                        break;
                    }
                    // count >0, 通过代理服务器的中转把信息发到另一端
                    write(sockfd_map[pevents[i].data.fd], buf, count);
                    if (ret == -1){    
                        perror ("write");
                        abort ();
                    }
                }
                if (done){
                    /* Closing the descriptor will make epoll remove it
                     from the set of descriptors which are monitored. */
                    fprintf (stderr, "Closed connection on descriptor %d\n", pevents[i].data.fd);
                    close(pevents[i].data.fd);
                    close(sockfd_map[pevents[i].data.fd]);
                    /* 从map 中把无用的描述符从epoll专用的文件描述符中删去 */
                    sockfd_map.erase(pevents[i].data.fd);
                    sockfd_map.erase(sockfd_map[pevents[i].data.fd]);
                }
            }
        }
    }


proxy将与远端主机的相应服务端口(由用户在命令行参数中指定)建立连接,并负责传递用户主机和远端主机之间交换的数据。


     在proxy函数中我们首先定义了是我们所需要的监听套接字、服务端、客户端监听套接字,epoll相关的结构变量。程序先将从main函数传来的命令行参数argv进行解析。把服务端套接口地址结构serv的所有成员清零,通过传进的参数proxy_port设置serv的端口号,一切正常之后我们把此程序转变为守护进程 在系统后台运行,通过调用daemonize 函数实现,此函数后面再做讨论。

套接字和套接字地址结构定义
  这段主程序是一段典型的服务器/客户端程序。网络通讯最重要的就是套接字的使用,在程序中我们对套接字描述符进行了相应的定义。
创建服务器端监听套接字
  下面就是建立一个服务器的详细过程。服务器程序的第一个操作是创建一个套接字。这是通过调用函数socket()来实现的。socket()函数的具体描述

#include<sys/types.h>  
#include<sys/socket.h>  
int  socket(int domain, int type, intprotocol);

   参数domain指定套接字使用的协议族,AF_INET表示使用TCP/IP协议族,AF_UNIX表示使用Unix协议族,AF_ISO表示套接字使用ISO协议族。type指定套接字类型,一般的面向连接通信类型(如TCP)设置为SOCK_STREAM,当套接字为数据报类型时,type应设置为SOCK_DGRAM,如果是可以直接访问IP协议的原始套接字则type应设置为SOCK_RAW。参数protocol一般设置为"0",表示使用默认协议。当socket()函数成功执行时,返回一个标志这个套接字的描述符,如果出错则返回"-1",并设置errno为相应的错误类型。
设置服务器套接字地址结构:

  在通常情况下,首先要将描述服务器信息的套接字地址结构清零,然后在地址结构中填入相应的内容,准备接受客户机送来的连接建立请求。这个清零操作可以用多种字节处理函数来实现,例如bzero()、bcopy()、memset()、memcpy()等,以字母"b"开始的两个函数是和BSD系统兼容的,而后面两个是ANSIC提供的函数。这段代码中使用的bzero()其描述为:void  bzero(void *s, int n);函数的具体操作是将参数s指定的内存的前n个字节清零。memset()同样也很常用,其描述为:void *memset(void* s, int c, size_t n);具体操作是将参数s指定的内存区域的前n个字节设置为参数c的内容。下一步就是在已经清零的服务器套接字地址结构中填入相应的内容。Linux系统的套接字是一个通用的网络编程接口,它应该支持多种网络通信协议,每一种协议都使用专门为自己定义的套接字地址结构(例如TCP/IP网络的套接字地址结构就是struct  sockaddr_in)。

不过为了保持套接字函数调用参数的一致性,Linux系统还定义了一种通用的套接字地址结构:

-----------------------------------------------------------------
<linux/socket.h>
struct sockaddr
{
  unsigned short sa_family; /* address type */
  char sa_data[14]; /* protocol address */
}
-----------------------------------------------------------------
  其中sa_family意指套接字使用的协议族地址类型,对于我们的TCP/IP网络,其值应该是AF_INETsa_data中存储具体的协议地址,不同的协议族有不同的地址格式。这个通用的套接字地址结构一般不用做定义具体的实例,但是常用做套接字地址结构的强制类型转换,如我们经常可以看到这样的用法:
   bind(sockfd,(struct sockaddr *) &servaddr,sizeof(servaddr))
  用于TCP/IP协议族的套接字地址结构是sockaddr_in,其定义为:

      

<linux/in.h>
struct in_addr
{
  __u32 s_addr;
};
  struct sochaddr_in
{
  short int sin_family;
  unsigned short int sin_port;
  struct in_addr sin_addr;
   /*This part has not been taken into use yet*/
   unsigned char_ _ pad[_ _ SOCK_SIZE__- sizeof(short int) -sizeof(unsigned short int) - sizeof(struct in_addr)];
};
#define sin_zero_ - pad



  其中sin_zero成员并未使用,它是为了和通用套接字地址struct sockaddr兼容而特意引入的。在编程时,一般都通过bzero()或是memset()将其置零。其他成员的设置一般是这样的:
   servaddr.sin_family = AF_INET;
  表示套接字使用TCP/IP协议族。
   servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  设置服务器套接字的IP地址为特殊值INADDR_ANY,这表示服务器愿意接收来自任何网络设备接口的客户机连接。htonl()函数的意思是将主机顺序的字节转换成网络顺序的字节。
   servaddr.sin_port = htons(PORT);
  设置通信端口号,PORT应该是我们已经定义好的。在本例中servaddr.sin_port = proxy_port;这是表示端口号是函数的返回值proxy_port
  另外需要说明的一点是,在本例中,我们并没有看到在预编译部分中包含有<linux/socket.h><linux/in.h>这两个头文件,那是因为这两个头文件已经分别被包含在<sys/socket.h ><sys/types.h>中了,而且后面这两个头文件是与平台无关的,所以在网络通信中一般都使用这两个头文件。

服务器公开地址
  如果服务器要接受客户机的连接请求,那么它必须先要在整个网络上公开自己的地址。在设置了服务器的套接字地址结构之后,可以通过调用函数bind()绑定服务器的地址和套接字来完成公开地址的操作。函数bind()的详细描述为:
#include <sys/types.h>
#include <sys/socket.h>
  int bind(int sockfd, struct sockaddr *addr, int addrlen);


  参数sockfd是我们通过调用socket()创建的套接字描述符。参数addr是本机地址,参数addrlen是套接字地址结构的长度。函数执行成功时返回"0",否则返回"-1",并设置errno变量为EADDRINUAER
  如果是服务器调用bind()函数,如果设置了套接字的IP地址为某个本地IP地址,那么这表示服务器只接受来自于这个IP地址的特定主机发出的连接请求。不过一般情况下都是IP地址设置为INADDR_ANY,以便接受所有网络设备接口送来的连接请求。
  客户机一般是不会调用bind()函数的,因为客户机在连接时不用指定自己的套接字地址端口号,系统会自动为客户机选择一个未用端口号,并且用本地IP地址自动填充客户机套接字地址结构中的相应项。但是在某些特定的情况下客户机需要使用特定的端口号,例如Linux中的rlogin命令就要求使用保留端口号,而系统是不能为客户机自动分配保留端口号的,这就需要调用bind()来绑定一个保留端口号了。不过在一些特殊的环境下,这样绑定特定端口号也会带来一些负面影响,如在HTTP服务器进入TIME_WAIT状态后,客户机如果要求再次与服务器建立连接,则服务器会拒绝这一连接请求。如果客户机最后进入TIME_WAIT状态,则马上再次执行bind()函数时会返回出错信息"-1",原因是系统会认为同时有两次连接绑定同一个端口。
转换Listening套接字

  接下来,服务器需要将我们刚才与
IP地址和端口号完成绑定的套接字转换成倾听listening套接字。只有服务器程序才需要执行这一步操作。我们通过调用函数listen()实现这一操作。listen() 的详细描述为:
   
#include <sys/socket.h>
int listen(int sockfd, int backlog);


  参数 sockfd 指定我们要求转换的套接字描述符,参数 backlog 设置请求队列的最大长度 。函数 listen() 主要完成以下操作。
  首先是将套接字转换成
倾听套接字。因为函数socket()创建的套接字都是主动套接字,所以客户机可以通过调用函数connect()来使用这样的套接字主动和服务器建立连接。而服务器的情况恰恰相反,服务器需要通过套接字接收客户机的连接请求,这就需要一个"被动"套接字。listen()就可将一个尚未连接的主动套接字转换成为这样的"被动"套接字,也就是倾听套接字。在执行了listen()函数之后,服务器的TCP就由CLOSED变成LISTEN状态了

另外
listen()可以设置连接请求队列的最大长度。虽然参数backlog的用法非常简单,只是一个简单的整数。但搞清楚请求队列的含义对理解TCP协议的通信过程建立非常重要。TCP协议为每个倾听套接字实际上维护两个队列,一个是未完成连接队列,这个队列中的成员都是未完成3次握手的连接;另一个是完成连接队列,这个队列中的成员都是虽然已经完成了3次握手,但是还未被服务器调用accept()接收的连接。参数backlog实际上指定的是这个倾听套接字完成连接队列的最大长度。在本例中我们是这样用的:listen(sockfd,5);表示完成连接队列的最大长度为5
接收连接

  接下来我们在主程序中看到通过名为
daemonize()的自定义函数创建一个守护进程,关于这个daemonize()以及守护进程的相关概念,我们等一会儿再做详细介绍。然后服务器程序进入一个无条件循环,用于监听接收客户机的连接请求。在此过程中如果有客户机调用connect()请求连接,那么函数accept()可以从倾听套接字的完成连接队列中接受一个连接请求。如果完成连接队列为空,这个进程就睡眠。accept() 的详细描述为:
      
#include <sys/socket.h>
  int accept(int sockfd, struct sockaddr *addr, int *addrlen);


   参数sockfd是我们转换成功的倾听套接字描述符;参数addr是一个指向套接字地址结构的指针,参数addrlen为一个整型指针。当函数成功执行时,返回3个结果,函数返回一个新的套接字描述符,服务器可以通过这个新的套接字描述符和客户机进行通信。参数addr所指向的套接字地址结构中将存放客户机的相关信息addrlen指针将描述前述套接字地址结构的长度。在通常情况下服务器对这些信息不是很感兴趣,因此我们经常可以看到一些源代码中将accept()函数的后两个参数都设置为NULL。不过在这段proxy 源代码中需要用到有关的客户机信息,因此我们看到通过执行
   
usersockfd = accept(sockfd, (struct sockaddr *) &cliaddr, &clilen);
  将客户机的详细信息存放在地址结构
cli中。而proxy_epoll就通过套接字usersockfd与客户机进行通信。值得注意的是这个返回的套接字描述符与我们转换的倾听套接字是不同的。在一段服务器程序中,可以始终只用一个倾听套接字来接收多个客户机的连接请求;而如果我们要和客户机建立一个实际的连接的话,对每一个请求我们都需要调用accept()返回一个新的套接字。当服务器处理完毕客户机的请求后,一定要将相应的套接字关闭;如果整个服务器程序将要结束,那么一定要将倾听套接字关闭如果accept()函数执行失败,则返回"-1",如果accept()函数阻塞等待客户机调用connect()建立连接,进程在此时恰好捕捉到信号,那么函数在返回"-1"的同时将变量errno的值设置为EINTR。这和accept()函数执行失败是有区别的。因此我们在代码中可以看到这样的语句:

 
// one or more connections
                for(; ;){
                    socklen_t    cli_len = sizeof(cli);
                    // 注意我们虽然 不关注 accept 中cli返回信息但是 其参数不能为0
                    // 否则出现错误 Bad address
                    usersockfd = accept(listenfd, (struct sockaddr*)&cli, &cli_len);
                    if(usersockfd == -1) {
                        if(errno == EAGAIN || errno == EWOULDBLOCK) {
                            //we have processed the connection
                            break;// no new client
                        }else{// accept failed
                            perror("accept");
                            break;
               }


   可以看出程序在处理这两种情况时操作是完全不同的,同样是accept()返回"-1",如果有errno== EINTR ,EWOULDBLOCK 那么系统将再次调用accept()接受连接请求,否则服务器进程将直接结束。
采用epoll模型处理客户机请求:

    当服务器与客户机建立连接以后,就可以处理客户机的请求了。一般情况下服务器程序都要创建一个子进程用于处理客户机请求;而父进程则继续监听,时刻准备接受其它客户机的连接请求。而我们的proxy_epoll程序不是采用这种方式。我们通过epoll模型来处理客户机数据。这样我们不需要每当有客户机来我们就fork一个子进程来处理,这样是比较浪费资源的(当然创建子进程也是一种重要的方法建议学习下)。其中的epoll模型采用非阻塞非忙轮询(比如select,polll)的方式。

为什么引入epoll:

首先介绍下阻塞的概念:比如某个时候你在等快递,但是你不知道快递什么时候过来,而且此时你只能有了这份快递后才能干别的事情;那么你在快递到来前只能等待 或许你只能去睡觉,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。可以看到阻塞的方式把你阻在那了。
阻塞忙轮询:接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:“你到了没?”

很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。    大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。

阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论),阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。
为了避免CPU空转,可以引进了一个代理(select的代理、poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。

于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,没一次无差别轮询时间就越长。

epoll的优点:

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))在讨论epoll的实现细节之前,先把epoll的相关操作列出:
epoll_create 创建一个epoll对象,一般epollfd = epoll_create()
epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件

epoll_ctl(epollfd, EPOLL_CTL_ADD, socket, EPOLLIN);//注册缓冲区非空事件,即有数据流入
epoll_ctl(epollfd, EPOLL_CTL_DEL, socket, EPOLLOUT);//注册缓冲区非满事件,即流可以被写入
epoll_wait(epollfd,...)等待直到注册的事件发生。


具体实际操作细节可以参考 具体实例: How to use epoll? A complete example in C

此外 还有一篇比较不错的博文:linux下epoll如何实现高效处理百万句柄的


通过以上,我们知道epoll 采用的是非阻塞模式的,因此在创建完我们的监听套接字后我们应该把这个套接字设置为非阻塞模式,在此我们就绪调用函数set_socket_noblock(listenfd),现在介绍如何把描述符变为非阻塞模式;

@set_socket_noblock

// 设置文件描述符为非阻塞
int set_socket_noblock(int fd)
{
	int flag, ret;
	if((flag = fcntl(fd, F_GETFL, 0)) < 0)
		errout("error fcntl\n");
	ret = fcntl(fd, F_SETFL, flag | O_NONBLOCK);
	return ret;
}

首先通过函数 fcntl 获得描述符fd 的标志 然后我们 再重新设置他的flag 即可。

   继续介绍 proxy 函数, 当我们listenfd套接字通过 listen函数变为被动的监听套接字后,我们就需要调用函数epoll_create1 生成用于处理accept的epoll专用的文件描述符,并把 监听套接字注册为epoll事件。然后就可以通过epoll_wait等待epoll事件的发生nready > 0说明有事件发生,先对事件 类型pevents[i].events 进行判断 进行错误处理。然后通过pevents[i].data.fd来判断是否为 listenfd,是listenfd说明有新的连接,然后我们设置这个新的套接字(用于代理服务器和客户进行通信的套接字)为非阻塞,然后我们的代理服务器与远程主机进行连接,这就需要调用函数connect_isolate下面介绍下 connect_isolate函数。当完成连接远程主机后我们就需要注册相关的时间,一遍epoll模型检测有新的事件发生。 如果pevents[i].data.fd不为监听套接字 listenfd,则说明我们有数据在描述符pevents[i].data.fd上,此时我们需要做的就是把此描述符上的所有数据读完(因为我们用的是edge-triggered mode),然后把读完的数据发送给另一端,这就起到代理服务器的真正的作用了。

  此处可以做个图**

@connect_isolate

int connect_isolate(int usersockfd, char *argv[])
{
	int i, len;
	int isosockfd = -1, connstat = 0;
	struct hostent *hostp; // host entry
	struct servent *servp;
	char buf[64];
	char isolate_host[64];
	char service_name[32];
	strcpy(isolate_host, argv[2]);
	strcpy(service_name, argv[3]);
	struct sockaddr_in  hostaddr;
	bzero(&hostaddr, sizeof(struct sockaddr_in));
	hostaddr.sin_family = AF_INET;
	// parse the isolate  
	if( inet_pton(AF_INET, isolate_host,&hostaddr.sin_addr) != 1){
		if((hostp = gethostbyname(isolate_host)) != NULL)
            bcopy(hostp->h_addr, &hostaddr.sin_addr, hostp->h_length);
		else
            return -1;
    }
    if((servp = getservbyname(service_name, TCP_PROTO)) != NULL)
        hostaddr.sin_port = servp->s_port;
    else if(atoi(service_name) >0)
        hostaddr.sin_port = htons(atoi(service_name));
    else
        return -1;
    // open a socket to connect isolate host
    if((isosockfd = socket(AF_INET, SOCK_STREAM, 0)) <0)
        return -1;
    len = sizeof(hostaddr);
    // attempt a connection
    connstat = connect(isosockfd, (struct sockaddr*)&hostaddr, len);
    switch(connstat){
        case 0:
            break;
        case ETIMEDOUT:
        case ECONNREFUSED:
        case ENETUNREACH:
            strcpy(buf, strerror(errno));
            strcat(buf,"/r/n");
            write(usersockfd,buf,strlen(buf));
            close(usersockfd);
            return -1;
            /*die peacefully if we can't establish a connection*/
        default:
            return -1;
    }
    return isosockfd;
}


 这部分实际上相当于一段标准的客户机程序。首先创建一个新的套接字描述符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()函数调用中对以下三种出错类型进行了处理: ETIMEDOUTECONNREFUSEDENETUNREACH。这三个出错类型的意思分别为:ETIMEDOUT代表超时,产生这种情况的原因有很多,最常见的是服务器忙,无法应答客户机的连接请求;ECONNREFUSED代表连接拒绝,即服务器端没有准备好的倾听套接字,或是没有对倾听套接字的状态进行监听;ENETUNREACH表示网络不可达。

hostaddr的所有成员清零,然后将成员hostaddr.sin_family设置为TCP/IP协议族标志AF_INET。下面就可将命令行的另外两个参数<remote_host><service_port>传进行hostaddr的两个成员hostaddr.sin_porthostaddr.sin_addr了。这里我们用到了两个局部变量struct hostent *hostpstruct servent *servp来传递参数信息。struct hostent的详细描述为:

-----------------------------------------------------------------------------------

struct hostent { 

   char *h_name;  

      char **h_aliases;

   int h_addrtype;
   int h_length;
   char **h_addr_list;
};
#define
h_addr h_addrlist[0]
-----------------------------------------------------------------
  hostent成员的含义是h_name代表主机在网络上的的正式名称,h_aliases是所有主机别名的列表,h_addrtype是指主机的地址类型,一般设置为TCP/IP协议族AF_INETh_length是主机的地址长度,一般设置为4个字节。h_addr_list是主机的IP地址列表。
  我们要用它来传递我们期望绑定的远程主机名或是IP地址。因为命令行中的主机名参数已经被存储进pargs.isolated_host,所以我们就调用inet_addr()函数对主机名或主机的IP地址进行二进制和字节顺序转换inet_addr()函数的描述为:
-----------------------------------------------------------------
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
unsigned long int inet_addr(const char *cp)
-----------------------------------------------------------------
  inet_addr()的作用就是将参数cp指向的Internet主机地址从数字/点的形式转换成二进制形式并同时转换为网络字节顺序,并将转换结果直接返回。如果cp指向的IP地址不可用,则函数返回INADDR_NONE"-1"
  虽然Carl Harris在编写这段程序时使用了这个inet_addr()函数,但是我还是建议大家在编写自己的程序时使用另外一个函数inet_aton()来完成这些功能。原因是inet_addr()IP地址不可用时返回"-1",但我们想想,IP地址255.255.255.255绝对是一个有效地址,那么其二进制返回值也将是"-1",因此inet_addr()无法对这个IP地址进行处理。而函数inet_aton()则采用了一种更好的方法来返回出错信息,它的具体描述为:
-----------------------------------------------------------------
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp)
-----------------------------------------------------------------

  函数执行成功时返回非零,转换结果存入指针inp指向的in_addr结构。这个结构定义我们在前面的文章里已经介绍过了。如果参数cp指向的IP地址不可用,则返回"0"。这就避免发生inet_addr()那样的问题。这我的实现里我们使用了最新的函数inet_pton()来实现的,它支持IPv6协议。

  如果说用户在命令行中键入的是远程主机的IP地址,那么只用inet_addr()就算完成任务了,但如果用户键入的是主机域名那该怎么办呢?所以我们在例程中可以看到这样的语句:
-----------------------------------------------------------------
if ((inaddr = inet_addr(solate_host)) != INADDR_NONE)
  bcopy(&inaddr,&hostaddr.sin_addr,sizeof(inaddr));
  else if ((hostp = gethostbyname(isolate_host)) != NULL)
  bcopy(hostp->h_addr,&hostaddr.sin_addr,hostp->h_length);
  else {
  printf("%s: unknown host/r/n",pargs.isolate_host);
  exit(1);
}
-----------------------------------------------------------------
  其中gethostbyname()函数就是用来转换主机域名的。它的具体描述为:
-----------------------------------------------------------------
#include <netdb.h>
struct hostent *gethostbyname(const char *hostname);
-----------------------------------------------------------------
  参数hostname指向我们需要转换的域名地址,函数直接返回转换结果,如果函数执行成功,则结果直接返回到一个指向hostent结构的指针中,否则返回空指针NULL
  例程就是这样调用inet_addr()gethostbyname()将命令行参数中的主机域名或是主机IP地址传递给全局变量hostaddr的成员sin_addr以便代理执行函数do_proxy()调用。
  下面是传递服务名或是服务端口号。这里要用到结构servent做传递中介,struct servent的详细描述为:
-----------------------------------------------------------------
struct servent {
  char *s_name;
  char **s_aliases;
  int s_port;
  char *s_proto;
};
-----------------------------------------------------------------
  其各成员的含义是s_name为服务的正式名称,如ftphttp等,s_aliases是服务的别名列表,s_port是服务的端口号,例如在一般情况下ftp的端口号为21http服务的端口号为80注意此端口号应该存储为网络字节顺序,s_proto是应用协议的类型。
  例程中使用getservbyname()函数转换命令行参数中的服务名,此函数的详细描述为:
-----------------------------------------------------------------
#include <netdb.h>
struct servent * getservbyname(const char *servname, const char *protoname);
-----------------------------------------------------------------
  它的作用就是转换指针servname指向的服务名为相应的整数表示的端口号,参数protoname表示服务使用的协议,例程中protoname参数的值为TCP_PROTO,这表示使用TCP协议。函数成功时就返回一个struct servent型的指针,其中的s_port成员就是我们关心的服务端口号。如果用户在命令中键入的是端口号而不是服务名,那么和处理代理端口信息一样,使用下面的语句进行处理:
   hostaddr.sin_port = htons(atoi(service_name));

  @daemonize

      般服务器程序在接收客户机连接请求之前,都要创建一个守护进程。守护进程是linux/Unix编程中一个非常重要的概念,因为在创建一个 守护进程的时候,我们要接触到子进程、进程组、会晤期、信号机制以及文件、目录、控制终端等多个概念,因此详细地讨论一下守护进程,对初学者学习进程间关系是非常有帮助的。下面就是例程中的daemonize()函数:

// 守护进程
void daemonize(int listenfd)
{
	pid_t pid;
	// 屏蔽控制终端操作信号I/O stop signal
	signal(SIGTTOU, SIG_IGN);
	signal(SIGTTIN, SIG_IGN);
	signal(SIGTSTP, SIG_IGN);
	// 重设文件创建掩模
	umask(0);
    // 使守护进程后台运行
	if((pid = fork()) <0)
		errout("fork error\n");
	else if(pid != 0)//父进程终止运行;子进程过继给init进程,其退出状态也由init进程处理,避免了产生僵死进程
        exit(0);
    // 脱离控制终端,登录会话和进程组,调用setsid()使子进程成为会话组长
	setsid();
    // 重定向标准输入输出
	redirect_stdIO("/dev/null", LOG_FILE, LOG_FILE);//重定向标准输入输
	// 改变当前工作目录
    chdir("/");
	/* close any open file descriptors */
	int fd, fdtablesize;
	// fd切忌从3开始,想想为什么?
	for(fd = 3, fdtablesize = getdtablesize(); fd < fdtablesize; fd++)
        if (fd != listenfd)
            close(fd);
    // signal(SIGCLD,(sigfunc *)reap_status);
}

 此函数的作用就是创建一个守护进程。在Linux系统中,如果要将一个普通进程转换成为守护进程,并且我们可以在系统文件/tmp/daemon.log 中查看代理服务器的日志信息必须要执行下面的步骤:
  1调用函数fork()创建子进程,然后父进程终止,保留子进程继续运行。之所以要让父进程终止是因为,当一个进程是以前台进程方式由shell启动时,在父进程终止之后子进程自动转为后台进程。另外,我们在下一步要创建一个新的会晤期,这就要求创建会晤期的进程不是一个进程组的组长进程。当父进程终止,子进程运行,这就保证了进程组的组ID与子进程的进程ID不会相等。
函数fork()的定义为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
-----------------------------------------------------------------
  该函数被调用一次,但是返回两次,这两次返回的区别是子进程的返回值为"0",而父进程的返回值为子进程的ID。如果出错则返回"-1"

  2保证进程不会获得任何控制终端。通常的做法是调用函数setsid()创建一个新的会晤期。setsid()的详细描述为:
-----------------------------------------------------------------
#include <sys/types.h>
#include <unistd.h>
pid_t setsid(void);
-----------------------------------------------------------------
  第一步的操作已经保证调用此函数的进程不是进程组的组长,那么此函数将创建一个新的会晤,其结果是:首先,此进程变成该会晤期的首进程(session leader,系统默认会晤期的首进程是创建该会晤期的进程)。而且,此进程是该会晤期中的唯一进程。然后,此进程将成为一个新的进程组的组长进程,新进程组的组ID就是该进程的进程ID。最后,保证此进程没有控制终端,即使在调用setsid()之前此进程拥有控制终端,在创建会晤期后这种联系也将被解除。如果调用该函数的进程为一个进程组的组长,那么函数将返回出错信息"-1"
当然我们还有其他的办法让进程无法获得控制终端,就象例程中所做的那样,
-----------------------------------------------------------------
if ((fd = open("/dev/tty",O_RDWR)) >= 0) {
ioctl(fd,TIOCNOTTY,NULL);
close(fd);
}
-----------------------------------------------------------------
  其中/dev/tty是一个流设备,也是我们的终端映射。调用close()函数将终端关闭。

  3.信号处理。一般是要忽略掉某些信号。这里就涉及到信号的概念了。信号其实相当于软件中断,Linux/Unix下的信号机制提供了一种处理异步事件的方法,终端用户键入引发中断的键,或是系统异常发出信号,这都会通过信号处理机制终止一个或多个程序的运行。
  不同情况下引发的信号是不同的。不过所有的信号都有自己的名字,所有的名字都是以"SIG"开头的,只是后面有所不同,我们可以通过这些名字了解到系统中到底发生了些什么事。

  当信号出现时,我们可以要求系统进行以下三种操作:
  忽略信号。大多数信号都是采取这种方式进行处理的,在例程中我们就可以见到这种用法。但值得注意的是有两个例外,那就是对SIGKILLSIGSTOP信号不能做忽略处理
  捕捉信号。这是一种最为灵活的操作方式。这种处理方式的意思就是,当某种信号发生时,我们可以调用一个函数对这种情况进行相应的处理。最常见的情况就是,如果捕捉到SIGCHID信号,则表示子进程已经终止,然后可在此信号的捕捉函数中调用waitpid()函数取得该子进程的进程ID以及它的终止状态。在我们这段例程中,就有这种用法的一个实例。还有就是如果进程创建了临时文件,那么就要为进程终止信号SIGTERM编写一个信号捕捉函数来清除这些临时文件。
  执行系统的默认动作。对绝大多数信号而言,系统的默认动作都是终止该进程
  在Linux下,信号有很多种,我在这里就不一一介绍了,如果想详细地对这些信号进行了解,可以查看头文件<sigal.h>,这些信号都被定义为正整数,也就是它们的信号编号。在对信号进行处理时,必须要用到函数signal(),此函数的详细描述为:
-----------------------------------------------------------------
#include <signal.h>
void (*signal (int signo, void (*func)(int)))(int);
-----------------------------------------------------------------
  其中参数signo为信号名,参数func的值根据我们的需要可以是以下几种情况:(1)常数SIG_DFL,表示执行系统的默认动作。(2)常数SIG_IGN,表示忽略信号。(3)收到信号后需要调用的处理函数的地址,此信号捕捉程序应该有一个整型参数但是没有返回值。signal()函数返回一个函数指针,而该指针指向的函数应该无返回值(void),这个指针其实指向以前的信号捕捉程序
  下面回到我们的daemonize()函数上来。这个函数在创建守护进程时忽略了三个信号:
   signal(SIGTTOU,SIG_IGN);
   signal(SIGTTIN,SIG_IGN);
   signal(SIGTSTP,SIG_IGN);
  这三个信号的含义分别是:SIGTTOU表示后台进程写控制终端,SIGTTIN表示后台进程读控制终端,SIGTSTP表示终端挂起。

  4关闭不再需要的文件描述符,并为标准输入、标准输出和标准错误输出打开新的文件描述符(也可以继承父进程的标准输入、标准输出和标准错误输出文件描述符,这个操作是可选的)。在我们这段例程中,因为是代理服务器程序,而且是在执行了listen()函数之后执行这个daemonize()的,所以要保留已经转换成功的倾听套接字,所以我们可以见到这样的语句:
if (fd != servfd)
close(fd);

  5.调用函数chdir("/")将当前工作目录更改为根目录。这是为了保证我们的进程不使用任何目录。否则我们的守护进程将一直占用某个目录,这可能会造成超级用户不能卸载一个文件系统。

  6.调用函数umask(0)将文件方式创建屏蔽字设置为"0"。这是因为由继承得来的文件创建方式屏蔽字可能会禁止某些许可权。例如我们的守护进程需要创建一组可读可写的文件,而此守护进程从父进程那里继承来的文件创建方式屏蔽字却有可能屏蔽掉了这两种许可权,则新创建的一组文件其读或写操作就不能生效。因此要将文件方式创建屏蔽字设置为"0"
  在daemonize()函数的最后,我们可以看到这样的信号捕捉处理语句:
   signal(SIGCLD,(Sigfunc *)reap_status);
  这不是创建守护进程过程中必须的一步,它的作用是调用我们自定义的reap_status()函数来处理僵死进程。在这个程序里由于我们不是不需要这一步。

关于linux上守护进程的编写可以到我的这篇博客查看更详细的信息。


最后关于本博客的源码 请移步至: https://github.com/ustcdane/proxy_epoll  ,谢谢!

声明:

最后终于写完了, 这篇博客是在前人 的基础之上加上自己的理解把程序改为 基于 epoll模型的代理服务器,其中参考了一篇名为 《proxy源代码分析——谈谈如何学习Linux网络编程》的文章(你可以google一下很容易就找到了),这篇文章写的确实很不错,以至于博客中有一部分直接引用作者的原话,在此感谢原作者。

你可能感兴趣的:(代理服务器,epoll,Linux网络编程,后台开发)