并发服务器编程

 

并发服务器是socket应用编程中最常见的应用模型。并发服务器模型根据连接方式分为长连接和短连接,长连接为通信双方建立连接后一直保持连接,然后一直用此连接进行读写操作;短连接为通信双方每一次交易过程都建立连接和关闭连接。并发服务器模型根据处理方式可分为同步方式和异步方式,同步是客户端发送请求给服务器等待服务器返回处理结果;异步是指客户端发送请求给服务器,不等待服务器返回处理结果,而直接去完成其他的流程,对于处理结果客户端可以事后查询和让服务器进行主动通知。

1.   并发服务器编程注意事项

进程是一个程序的一次运行过程,它是一个动态实体,是独立的任务,它拥有独立的地址空间、执行堆栈、文件描述符等。每个进程拥有独立的地址空间,在进程不存在父子关系的情况下,互不影响。

进程的终止存在两个可能:父进程先于子进程终止(由init进程领养),子进程先于主进程终止。对于后者,系统内核为子进程保留一定的状态信息(进程ID、终止状态、CPU时间等),并向其父进程发送SIGCHLD信号。当父进程调用wait或waitpid函数时,将获取这些信息,获取后内核将对僵尸进程进行清理。如果父进程设置了忽略SIGCHLD信号或对SIGCHLD信号提供了处理函数,即使不调用wait或waitpid函数内核也会清理僵尸进程。

但父进程调用wait函数处理子进程退出信息时,会存在下面所述的问题。在有多个子进程情况下,wait函数只等待最先到达的子进程的终止信息。下图18-7父进程有3个子进程,由于SIGCHLD信号不排队,在SIGCHLD信号同时到来后,父进程的wait函数只执行一次,这样将留下2个僵尸进程,而使用waitpid函数并设置WNOHANG选项可以解决这个问题。

图18-7 多进程信号图

综上所述,在多进程并发的情况下,防止子进程变成僵尸进程常见有如下两种方法:

①    父进程调用signal(SIGCHLD,SIG_IGN)对子进程退出信号进行忽略,或者把SIG_IGN替换为其他处理函数,设置对SIGCHLD信号的处理。

②    父进程调用waitpid(-1,NULL,WNOHANG)对所有子进程SIGCHLD信号进行处理。

2.   并发服务器文件描述符变化图

图18-8~图18-11画出了并发服务器文件描述符的变化流程图。其中listenfd为服务端的socket监听文件描述符,connfd为accept函数返回的socket连接文件描述符。

服务器调用accept函数后,客户与服务器文件描述符如下图18-8所示。

图18-8 调用accept函数时套接字描述符图

 

服务器调用accept函数后,客户与服务器文件描述符如下图18-9所示。

 

图18-9调用accept函数后套接字描述符图

 

服务器调用fork函数后,客户与服务器文件描述符如下图18-10所示。

 

图18-10调用fork函数后套接字描述符图

 

服务端父进程关闭连接套接字,子进程关闭监听套接字,客户与服务器文件描述符状况如下图18-11所示。

 

图18-11 并发服务器最终连接图

在这里强调的是,并发服务器fork后父进程一定要关闭子进程连接套接字;而子进程要关闭父进程监听套接字,以免误操作。

1.   TCP并发服务器代码实现

(1)并发服务器处理流程

    并发服务器处理流程如下:

①    客户端首先发起连接。

②    服务端进程accept打开一个新的连接套接字与客户端进行连接,accept在一个while(1)循环内等待客户端的连接。

③    服务端fork一个子进程,同时父进程close子进程连接套接字,循环等待下一进程。

④    服务端子进程close父进程监听套接字,并用连接套接字保持与客户端的连接,客户发送数据到服务端,然后阻塞等待服务端返回。

⑤    子进程进行接收数据,进行业务处理,然后再发送数据给客户端。

⑥    子进程关闭连接,然后退出。

 

(2)程序报文协议说明

该程序报文协议模式为常见行业应用软件协议模式,其具体说明如下:

发送和接收报文协议:8位报文长度(不包含本身)+6位交易码+报文内容,交易码标识该交易的类型。

实际应用中服务端进程根据6位交易码调度不同的应用服务。

 

(3)并发服务器服务端代码

tcpsrv.c源代码如下:

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

#define MY_PORT 10000

extern int readn(int fd,void *buffer,int length) ;

extern int writen(int fd,void *buffer,int length) ;

int main(int argc ,char **argv)

{

    int listen_fd,accept_fd;

    struct sockaddr_in server_addr;

    struct sockaddr_in cli_addr;

    int n;

    int cliaddr_len ;

    char buffer[1024];

    char data[1024] ;

    long length ;

    int nbytes ;

    if((listen_fd=socket(AF_INET,SOCK_STREAM,0))<0)

    {

        printf("Socket Error:%s\n",strerror(errno));

        return -1;

    }

    memset(&server_addr,0x00, sizeof(struct sockaddr_in));

    memset(&cli_addr,0x00, sizeof(struct sockaddr_in));

    server_addr.sin_family=AF_INET;

    server_addr.sin_port=htons(MY_PORT);

    server_addr.sin_addr.s_addr=htonl(INADDR_ANY);

    n=1;

    /* 如果服务器终止后,服务器可以第二次快速启动而不用等待一段时间 */

    setsockopt(listen_fd,SOL_SOCKET,SO_REUSEADDR,&n,sizeof(int));

    if(bind(listen_fd,(struct sockaddr *)&server_addr,sizeof(server_addr))<0)

    {

        printf("Bind Error:%s\n",strerror(errno));

        return -1;

    }

    listen(listen_fd,5);

    while(1)

    {

        cliaddr_len= sizeof( cli_addr ) ;

        accept_fd=accept(listen_fd, (struct sockaddr *)&cli_addr, &cliaddr_len );

        if((accept_fd<0)&&(errno==EINTR))

            continue;

        else if(accept_fd<0)

        {

            printf("Accept Error:%s\n",strerror(errno));

            continue;

        }

        if((n=fork())==0)

        {

            /* 子进程处理客户端的连接 */

            fprintf(stdout,"listen_fd:%d accept_fd:%d\n",listen_fd, accept_fd);

            close(listen_fd);

            memset(buffer, 0x00, sizeof(buffer)) ;

            memset(data, 0x00, sizeof(data));

            if((nbytes=readn(accept_fd, data, 8 ))==-1)

            {

                fprintf(stderr,"Read Error:%s\n",strerror(errno));

                fprintf(stderr,"data:%s\n",data );

                close(accept_fd);

                return -1;

            }

            fprintf(stdout,"data:%s,nbytes=%d\n",data, nbytes );

            data[nbytes]='\0' ;

            length=atol(data) ;

            fprintf(stdout,"data:%s,nbytes=%d\n",data, nbytes );

            if((nbytes=readn(accept_fd, data, length ))==-1)

            {

                fprintf(stderr,"Read Error:%s\n",strerror(errno));

                close(accept_fd);

                return -1;

            }

            data[nbytes]='\0' ;

            fprintf(stdout,"data:%s,nbytes=%d\n",data, nbytes );

            if( strncmp(data, "000000", 6 )==0 )

            {

                strcpy(buffer, "I am sorry! who am  I? I don't know also.") ;

                length=strlen(buffer) ;

                sprintf(data,"%08ld%6.6s%s", (length+6),"000000", buffer ) ;

                if((nbytes=writen(accept_fd,data, (length+6+8)))==-1)

                {

                    fprintf(stderr,"Read Error:%s\n",strerror(errno));

                    close(accept_fd);

                    return -1;

                }

                fprintf(stdout,"data:%s\n",data );

            }else{

                /*非000000交易请求为非法,沉默是最好的回答*/

            }

            close(accept_fd);

            return 0;

        }

        else if(n<0)

            printf("Fork Error:%s\n\a",strerror(errno));

        close(accept_fd);

        while(waitpid(-1,NULL,WNOHANG) > 0); /* clean up child processes */ 

    }

}

 

(4)客户端代码

tcpcli.c源代码如下:

#include

#include

#include

#include

#include

#include

#include

#include

#include

#include

extern int readn(int fd,void *buffer,int length) ;

extern int writen(int fd,void *buffer,int length) ;

int main(int argc, char *argv[])

{

    int sockfd;

    char buffer[1024];

    char data[1024];

    long length ;

    struct sockaddr_in server_addr;

    struct hostent *host;

    int portnumber,nbytes;

    if(argc!=3)

    {

        fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);

        return -1;

    }

    if((host=gethostbyname(argv[1]))==NULL)

    {

        fprintf(stderr,"Gethostname error\n");

        return -1;

    }

    if((portnumber=atoi(argv[2]))<0)

    {

        fprintf(stderr,"Usage:%s hostname portnumber\a\n",argv[0]);

        return -1;

    }

    /* 客户程序开始建立 sockfd描述符 */

    if((sockfd=socket(AF_INET,SOCK_STREAM,0))==-1)

    {

        fprintf(stderr,"Socket Error:%s\a\n",strerror(errno));

        return -1;

    }

    /* 客户程序填充服务端的地址端口信息 */

    bzero(&server_addr,sizeof(server_addr));

    server_addr.sin_family=AF_INET;

    server_addr.sin_port=htons(portnumber);

   server_addr.sin_addr= *((struct in_addr *)host->h_addr);

    /* 客户程序发起连接请求 */

    if(connect(sockfd,(struct sockaddr *)(&server_addr),sizeof(struct sockaddr)

              )==-1)

    {

        fprintf(stderr,"Connect Error:%s\a\n",strerror(errno));

        return -1;

    }

    memset(buffer, 0x00, sizeof(buffer)) ;

    memset(data, 0x00, sizeof(data));

    strcpy(buffer, "Hello! who are  you? could you tell me?") ;

    length=strlen(buffer) ; 

    /***000000为假设的交易码***/

    sprintf(data,"%08ld%6.6s%s", (length+6),"000000", buffer ) ;

    length=length+8+6 ;

    if((nbytes=writen(sockfd,data, length ))==-1)

    {

        fprintf(stderr,"Read Error:%s\n",strerror(errno));

        close(sockfd);

        return -1;

    }

    printf("I have send:%s\n", data+8);

    if((nbytes=readn(sockfd, data, 8 ))==-1)

    {

        fprintf(stderr,"Read Error:%s\n",strerror(errno));

        close(sockfd);

        return -1;

    }

    data[nbytes]='\0' ;

    length=atol(data) ;

    if((nbytes=readn(sockfd, data, length ))==-1)

    {

        fprintf(stderr,"Read Error:%s\n",strerror(errno));

        close(sockfd);

        return -1;

    }

    data[nbytes]='\0' ;

    printf("I have received:%s\n", data);

    close(sockfd);

    return 0;

}

(5)编译与执行

编译 gcc tcpsrv.c  tcpio.c -o tcpsrv。

编译 gcc tcpcli.c  tcpio.c -o tcpcli。

在一界面下启动服务端进程 ./tcpsrv。

在另一界面下执行./tcpcli 127.0.0.1 10000,执行结果如下:

I have send:000000Hello! who are  you? could you tell me?

I have received:000000I am sorry! who am  I? I don't know also.

 

摘录自《深入浅出Linux工具与编程》

 

你可能感兴趣的:(C语言)