zoukankan      html  css  js  c++  java
  • TCP并发server,每个客户一个子进程

    今天笔者带来的是server型号第一,这是最经常使用的模型的最基本的一个–TCP并发server,每个客户一个子进程。

    首先简单介绍:TCP并发server,每个客户一个子进程,并发server调用fork派生一个子进程来处理每一个子进程,使得server能够同一时候为多个客户服务,每一个进程一个客户。

    客户数目的唯一限制是操作系统对以其名义执行server的用户ID能够同一时候拥有多少子进程的限制。

    详细到我们的需求,我们的client发送某个指令,服务端接收。假设符合服务端的要求。就将当时的时间发回给client。

    需求非常easy,我们的着重点在server的模型。

    Okay,来看代码:
    这是服务端的主代码(serv.c):

    #include "pub.h"
    
    #define LISTENQ 1024
    
    void sig_child(int signo);
    
    
    //serv <port>
    int main(int argc, char **argv)
    {
        int listenfd,connfd;
        pid_t childpid;
        int on = 1;
        struct sockaddr_in servaddr, cliaddr;
        socklen_t clilen;
        char *ptr;
        if(argc != 2)
        {
            fprintf(stderr,"usage: serv <port>
    ");
            exit(1);
        }
    
        if((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        {
            perror("serv socket error ");
            exit(-1);
        }
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(atoi(argv[1]));
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        //设置可重用
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
    
        if(bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        {
            perror("serv bind error");
            exit(-1);
        }
    
        if(listen(listenfd, LISTENQ) < 0)
        {
            perror("serv listen error");
            exit(-1);
        }
    
        //信号处理函数
        //这里主要是处理僵死进程
        Signal(SIGCHLD, sig_child); //每一个子进程终止时就会产生SIGCHLD信号,默认是忽略
    
    
        for( ; ; )
        {
            clilen = sizeof(cliaddr);
            if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0)
            {
                if(errno == EINTR)
                    continue;
                else
                {   
                    perror("serv accept error");
                    exit(-1);
                }
            }
            ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
            fprintf(stdout,"%s has connected
    ", ptr);
    
            if((childpid = fork()) == 0)//child
            {
            //注意这里是另一个进程
                close(listenfd);    //子进程关闭listenfd
    
                do_child(connfd);
    
                ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
                fprintf(stdout,"%s has disconnected
    ", ptr);
    
                close(connfd);
                exit(0);
            }
    
            close(connfd);//父进程关闭connfd
    
    
        }
    
        close(listenfd);
    
        exit(0);
    }
    
    
    
    void sig_child(int signo)
    {
        pid_t pid;
        int stat;
    
        while((pid = waitpid(-1, &stat, WNOHANG)) > 0)  //不堵塞。返回child pid
        {//防止同一时候有几个子进程killed,加while,知道处理全然部killed child
            fprintf(stdout,"%d terminated
    ",pid);
            fflush(stdout);
        }
    
        return;
    }
    

    看这小段连接的代码:

    clilen = sizeof(cliaddr);
    if((connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen)) < 0)
    {
        if(errno == EINTR)
            continue;
        else
        {   
            perror("serv accept error");
            exit(-1);
        }
    }
    ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
    fprintf(stdout,"%s has connected
    ", ptr);

    注意我们这里的大循环。每次循环的開始都是accept,也就是从已连接的队列中返回当中一个。假设返回成功,就打印一下IP,显示是哪个IP来连接。

    这里採用的是inet_ntoa()函数,这是个专门为IPV4准备的函数。

    你也能够使用inet_pton()函数,这是IPV4和IPV6通用的函数。

    再看以下的一小段:

    if((childpid = fork()) == 0)//child
            {
            //注意这里是另一个进程
                close(listenfd);    //子进程关闭listenfd
    
                do_child(connfd);
    
                ptr = inet_ntoa(cliaddr.sin_addr);//cliaddr.sin_addr不用取地址
                fprintf(stdout,"%s has disconnected
    ", ptr);
    
                close(connfd);
                exit(0);
            }

    这里就是真正的fork出一个子进程。

    先close(listenfd),注意这里close并不会真正的关闭listenfd,这仅仅是降低了listenfd的一个引用次数,父进程另一个引用。

    然后是do_child(connfd),这里是我们自己定义的来处理连接的函数。注意已经将connfd传递给这个函数。

    稍后我们在讨论一下这个函数。

    假设能从do_child()函数返回,就再打印一下,某一个IP 已经离开。

    最后就是close(connfd),再exit(0)退出,这里的close(connfd),能够省略,子进程退出。就会关掉自身所打开的描写叙述符。

    注意到我们这里另一信号处理函数,sig_child。例如以下所看到的:

    void sig_child(int signo)
    {
        pid_t pid;
        int stat;
    
    while((pid = waitpid(-1, &stat, WNOHANG)) > 0)  //不堵塞。返回child pid
    {//防止同一时候有几个子进程killed。加while,知道处理全然部killed child
        fprintf(stdout,"%d terminated
    ",pid);
        fflush(stdout);
    }
    
        return;
    }

    我们在之前已经注冊了这个信号处理函数,
    //信号处理函数
    Signal(SIGCHLD, sig_child); //每一个子进程终止时就会产生SIGCHLD信号,默认是忽略

    这里我们的处理函数,就是打印一下离开的子进程的pid。就如同凝视所看到的。採用这样的形式:
    while((pid = waitpid(-1, &stat, WNOHANG)) > 0) 能够防止同一时候有几个子进程killed,加while,知道处理全然部killed child

    到了这里,server基本的模型已经展示出来了,如今我们来看基本的处理client连接的do_child()函数。

    来看代码:

    #include "pub.h"
    
    
    void do_child(int sockfd)
    {
        time_t mytime;
        char buff[MAXLINE];
        int n;
    
        for( ; ; )
        {
            if((n = read(sockfd, buff, sizeof(buff))) <= 0)
            {
                if( n < 0 && errno == EINTR)
                    continue;
                else if(n == 0){
                    //交给外面的主循环处理(打印某某离开)
                    break;
                }else{
                    perror("child read error");
                    exit(-1);
                }
            }
            else
            {
            //比較前几个字符串。是不是GETTIME,是则返回时间。否则返回 Gettime Command Error
                if((strncmp(buff,"GETTIME", 7) == 0) || 
                    (strncmp(buff,"gettime", 7) == 0) )
                {
                    mytime = time(NULL);
                    snprintf(buff, sizeof(buff), "%s", ctime(&mytime));
                    writen(sockfd, buff, strlen(buff));//这里最好用writen(自己定义)
                }
                else
                {//不是的话,就返回 Gettime Command Error
                    snprintf(buff, sizeof(buff), "Gettime Command Error ");
                    writen(sockfd, buff, strlen(buff));
                }
            }
    
        }
    
    }
    

    这里採用的处理非常easy,就如同我凝视中讲的,比較前几个字符串。是不是GETTIME或者gettime,是则返回时间,否则返回 Gettime Command Error

    看这里:

    mytime = time(NULL);
    snprintf(buff, sizeof(buff), "%s", ctime(&mytime));
    writen(sockfd, buff, strlen(buff));//这里最好用writen(自己定义)

    通过time()和ctime()用字符串的形式打印出当前时间。

    注意这里採用的是writen()函数。自己定义函数。
    事实上就是write相当数目的buff,同一时候防止信号打断

    来看writen函数的代码:

    #include "pub.h"
    
    
    int writen(int sockfd,const char *buff, int n)
    {
        int nleft = n;
        int ncount = 0;
    
        const char *ptr = buff;
    
        while(nleft > 0)
        {
            if((ncount = write(sockfd, ptr, nleft)) <= 0)
            {
                if(errno == EINTR)
                    ncount = 0; //call again
                else
                    return -1;
            }
            nleft -= ncount;
            ptr += ncount;
        }
    
        return n - nleft;
    }
    

    接下来,我们写了一个測试用的client(client.c):
    来看代码:

    #include "pub.h"
    
    //client <ip> <port>
    int main(int argc, char **argv)
    {
        int sockfd;
        struct sockaddr_in servaddr;
        int n;
    
        if(argc != 3)
        {
            fprintf(stderr,"usage: <ip> <port> 
    ");
            exit(1);
        }
    
        if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        {
            perror("client socket error ");
            exit(-1);
        }
    
        memset(&servaddr, 0,sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(atoi(argv[2]));
        inet_aton(argv[1], &servaddr.sin_addr);
    
        if((connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr))) < 0)
        {
            perror("client connect error");
            exit(-1);
        }
        strcli(sockfd);
    
        exit(0);
    }
    

    这里就是简单的client代码,通过參数传进来要连接的ip与port,发送请求的函数是str_cli()。

    来看str_cli函数:

    #include "pub.h"
    
    
    void strcli(int sockfd)
    {
        char sendbuff[MAXLINE], recvbuff[MAXLINE];
    
        while(fgets(sendbuff, sizeof(sendbuff), stdin) != NULL) 
        {
            writen(sockfd, sendbuff, strlen(sendbuff));
    
            if(read(sockfd, recvbuff, sizeof(recvbuff)) == 0)
                fprintf(stderr,"server has terminated
    ");
            fputs(recvbuff, stdout);
        }
    
    }
    

    这里事实上就是读取标准输入,发送给服务端。并read堵塞,等服务端发送回后。将服务端发送回的数据打印到标准输出上。

    另外,另一个注冊信号处理函数Signal函数,之前讲过。但没有给出代码。
    我这里直接将unix网络编程里的signal拿了过来。能够直接使用。
    也来看一下,signal.c:

    /* include signal */
    #include    "pub.h"
    
    Sigfunc *
    signal(int signo, Sigfunc *func)
    {
        struct sigaction    act, oact;
    
        act.sa_handler = func;
        sigemptyset(&act.sa_mask);
        act.sa_flags = 0;
        if (signo == SIGALRM) {
    #ifdef  SA_INTERRUPT
            act.sa_flags |= SA_INTERRUPT;   /* SunOS 4.x */
    #endif
        } else {
    #ifdef  SA_RESTART
            act.sa_flags |= SA_RESTART;     /* SVR4, 44BSD */
    #endif
        }
        if (sigaction(signo, &act, &oact) < 0)
            return(SIG_ERR);
        return(oact.sa_handler);
    }
    /* end signal */
    
    Sigfunc *
    Signal(int signo, Sigfunc *func)    /* for our signal() function */
    {
        Sigfunc *sigfunc;
    
        if ( (sigfunc = signal(signo, func)) == SIG_ERR)
        {
            perror("signal error");
            exit(1);
        }
        return(sigfunc);
    }
    

    至此,我们的第一种server模型就已经完毕了。全部的代码都已经晒了出来。并经过測试。你能够在你自己的linux上试试。看行不行。

    另外,我们这里就是简单的client与服务端查询时间的处理请求。你也能够进行别的处理请求。比方能够两方通信。採用多线程,或者试试发送文件,都能够。

    Just up to you.并且,你仅仅须要改动do_child()和str_cli()函数。其它的能够不改动,除非你有别的需求。

    最后。还得讲一下这样的模型的问题。主要就是为每一个客户fork一个子进程比較消耗CPU时间,几百或几千的的客户是没有问题的,可是如今的每天的TCP连接都是上百万的。这里就会有问题。当然,假设系统负载较轻,这样的模型是不错好的选择。

    好了。这就是我们的今天博客的全部内容,你懂了吗?假设我有出错的地方,欢迎大家指出。假设认为好。也能够点赞哦。

  • 相关阅读:
    canvas-绘制矩形-读书笔记
    获取页面路径中的参数
    微信小程序引用组件的方式
    this指向知识梳理
    for循环整理
    微信小程序使用wxs(小程序的一套脚本语言)
    textarea层级过高的解决办法
    防止用户连续点击按钮导致页面数据重复
    微信小程序tab切换
    HTML/CSS 知识点01 (转)
  • 原文地址:https://www.cnblogs.com/hrhguanli/p/5040271.html
Copyright © 2011-2022 走看看