TCP并发服务器,每个客户一个子进程

在阅读完《unix 网络编程:卷一》之后,感觉作者真是unix下编程的大师级的人物。而对于我个人而言,每次阅读完一本技术书籍之后,一定还是得自己重新再写一遍程序(换点内容),复习书本中的内容(大致结构,或者说思想,相同),否则,你很难做到真的理解并掌握的地步。

Okay,今天我带来的是服务器模型中的第一种,也是最基本最常用的一种模型–TCP并发服务器,每个客户一个子进程。

先简单介绍一下:TCP并发服务器,每个客户一个子进程,也就是说并发服务器调用fork派生一个子进程来处理每个子进程,使得服务器能够同时为多个客户服务,每个进程一个客户。客户数目的唯一限制是操作系统对以其名义运行服务器的用户ID能够同时拥有多少子进程的限制。

具体到我们的需求,我们的客户端发送某个指令,服务端接收。如果符合服务端的要求,就将当时的时间发回给客户端。需求很简单,我们的着重点在服务器的模型。

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>\n");
        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\n", 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\n", 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\n",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\n", 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\n", 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\n",pid);
    fflush(stdout);
}

    return;
}

我们在之前已经注册了这个信号处理函数,

//信号处理函数

Signal(SIGCHLD, sig_child); //每一个子进程终止时就会产生SIGCHLD信号,默认是忽略

这里我们的处理函数,就是打印一下离开的子进程的pid,就如同注释所示,采用这种形式:

while((pid = waitpid(-1, &stat, WNOHANG)) > 0) 可以防止同时有几个子进程killed,加while,知道处理完所有killed child

到了这里,服务器主要的模型已经展示出来了,现在我们来看主要的处理客户端连接的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));
            }
        }

    }

}

这里采用的处理很简单,就如同我注释中讲的,比较前几个字符串,是不是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.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> \n");
        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);
}

这里就是简单的客户端代码,通过参数传进来要连接的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\n");
        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);
}

至此,我们的第一种服务器模型就已经完成了。全部的代码都已经晒了出来,并经过测试。你可以在你自己的linux上试试,看行不行。

另外,我们这里就是简单的客户端与服务端查询时间的处理请求。你也可以进行别的处理请求。比如可以双方通信,采用多线程,或者试试发送文件,都可以。Just depend on yourself.而且,你只需要修改do_child()和str_cli()函数,其他的可以不修改,除非你有别的需求。

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

好了,这就是我们的今天博客的全部内容,你懂了吗?如果我有出错的地方,欢迎大家指出,如果觉得好,也可以点赞哦。

时间: 2024-10-14 05:54:16

TCP并发服务器,每个客户一个子进程的相关文章

TCP并发服务器(四)&mdash;&mdash;预创建子进程,accept互斥锁

1.说明 Posix文件上锁可移植到所有Posix兼容系统,但是涉及到文件系统操作,可能比较费时. 本次使用线程上锁保护accept,这不仅适用于同一进程中各线程之间上锁,也适用于不同进程之间上锁. 2.进程间使用互斥锁要求 (1) 互斥锁变量必须存放在由所有进程共享的内存去. (2) 必须告知线程函数库这是在不同进程之间共享的互斥锁.要求线程支持PTHREAD_PROCESS_SHARED属性.默认属性PTHREAD_PROCESS_PRIVATE, 只允许在单个进程内使用.   3.代码 支

TCP并发服务器(一)——每个客户一个子进程

TCP并发服务器(一)——每个客户一个子进程 1.说明 这是最传统的并发服务器,对于每一个客户请求fork一个子进程.问题在于每次fork一个子进程比较耗费时间,下面会讲预创建进程. 程序代码基于UNP的库. 程序在使用进程的模式下是最慢的. 2.代码 #include "unp.h" int main(int argc, char *argv[]) { int listenfd; socklen_t addrlen; if (argc = 2) { listenfd = Tcp_li

TCP并发server,每个客户一个子进程

今天笔者带来的是server型号第一,这是最经常使用的模型的最基本的一个–TCP并发server,每个客户一个子进程. 首先简单介绍:TCP并发server,每个客户一个子进程,并发server调用fork派生一个子进程来处理每一个子进程,使得server能够同一时候为多个客户服务,每一个进程一个客户. 客户数目的唯一限制是操作系统对以其名义执行server的用户ID能够同一时候拥有多少子进程的限制. 详细到我们的需求,我们的client发送某个指令,服务端接收.假设符合服务端的要求.就将当时的

TCP并发服务器(五)——每个客户一个线程

TCP并发服务器(五)——每个客户一个线程 1.说明 前面4个版本都是关于进程的,可以将进程改为线程来实现. 这个最简单的版本也快于前面的所有预先派生进程的版本. 2.代码 #include "unpthread.h" void sig_int(int signo) { DPRINTF("sig_int()\n"); void pr_cpu_time(void); pr_cpu_time(); exit(0); } void *doit(void *arg) { v

Linux网络编程——tcp并发服务器(多进程)

一.tcp并发服务器概述 一个好的服务器,一般都是并发服务器(同一时刻可以响应多个客户端的请求).并发服务器设计技术一般有:多进程服务器.多线程服务器.I/O复用服务器等. 二.多进程并发服务器 在 Linux 环境下多进程的应用很多,其中最主要的就是网络/客户服务器.多进程服务器是当客户有请求时,服务器用一个子进程来处理客户请求.父进程继续等待其它客户的请求.这种方法的优点是当客户有请求时,服务器能及时处理客户,特别是在客户服务器交互系统中.对于一个 TCP 服务器,客户与服务器的连接可能并不

Linux网络编程——tcp并发服务器(多线程)

tcp多线程并发服务器 多线程服务器是对多进程服务器的改进,由于多进程服务器在创建进程时要消耗较大的系统资源,所以用线程来取代进程,这样服务处理程序可以较快的创建.据统计,创建线程与创建进程要快 10100 倍,所以又把线程称为"轻量级"进程.线程与进程不同的是:一个进程内的所有线程共享相同的全局内存.全局变量等信息,这种机制又带来了同步问题. tcp多线程并发服务器框架: 我们在使用多线程并发服务器时,直接使用以上框架,我们仅仅修改client_fun()里面的内容. 代码示例: #

tcp并发服务器(c20w)

** 原创文章,请勿转载 ** 并发服务器是一个老生常谈的话题,今天这里也写一个. 1. 目标: 同时在线连接20万(c20w). 开发语言:重要的事情说三遍,GOLANG, GOLANG, GOLANG! 那为什么是20W,不是30W或其它? 这个数字随意.   :) 2. 环境: 虚拟机(xenserver),    虚出6台机器(OS: CentOS 6.6 64bit) : . 一台服务器8核CPU,2G内存 . 五台客户端2核CPU,2G内存 3.  改centos几个参数, 6台机器

Linux网络编程——tcp并发服务器(poll实现)

想详细彻底地了解poll或看懂下面的代码请参考<Linux网络编程--I/O复用之poll函数> 代码: #include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/select.h> #include <sys/time.h> #include <sys/socket.h> #incl

Java基础知识强化之网络编程笔记07:TCP之服务器给客户端一个反馈案例

1. 首先我们搭建服务器端的代码,如下: 1 package cn.itcast_07; 2 3 import java.io.IOException; 4 import java.io.InputStream; 5 import java.io.OutputStream; 6 import java.net.ServerSocket; 7 import java.net.Socket; 8 9 public class ServerDemo { 10 public static void ma