【网络编程小结(三)】利用多进程与多线程

∥☆過路亽.° 提交于 2020-01-12 23:35:49

在第一节的例子中,服务器是一个时间获取程序,只要一次write调用就能立刻完成客户端的任务,但是我们要想的是,服务端不一定每次都会这么快的完成任务,所以,要想办法加快服务端的处理速度。

首先可以想到的是并行处理,c++有两种方式,一个是多进程,一个是多线程。下面描述这两种办法。

 

 

一、压力测试

我们的客户端应当有能力判断服务端处理的快慢,所以我们要写一个压力测试函数:

void request_to_server(int id,const char* ip,const uint16_t port){
    int sockfd=socket(AF_INET,SOCK_STREAM,0);
    const char *id_str=std::to_string(id).c_str();
    sockaddr_in addr;
    bzero(&addr,sizeof(addr));
    addr.sin_family=AF_INET;
    addr.sin_port=htons(port);
    inet_pton(AF_INET,ip,&addr.sin_addr);
    connect(sockfd,(const sockaddr*)&addr,sizeof(addr));
    const int buffersize=1024;
    char buf[buffersize];
    write(sockfd,id_str,strlen(id_str));
    cout<<"has sended id="<<id<<endl;
    auto n=read(sockfd,buf,buffersize);
    buf[n]='\0';
    cout<<"answer message("<<id<<"):"<<buf<<endl;

}
void task(int times,int id,const char* ip,const uint16_t port){
    
    for(int i=0;i<times;++i){
        request_to_server(id,ip,port);
    }
    
}
void pressure_test(int clinet_n,int request_per_client,const char* ip="127.0.0.1",const uint16_t port=9000){
   vector<thread> threads;
    for(int i=0;i<clinet_n;++i){
        threads.push_back(thread(task,request_per_client,i+1,ip,port));
    }
    auto t1=clock();
    for(auto& t:threads){
        t.join();
    }
    auto t2=clock();
    cout<<"all task finish. used time="<<((t2-t1)*1000/(double)CLOCKS_PER_SEC)<<" ms"<<endl;
}

压力测试函数利用多线程,指定用户数和每个用户发起的请求数,可以计算出总的处理时间。

对于我们前面的时间服务器程序——一个迭代处理方式(也就是最慢的那种),我们用100个用户发起10次请求/人。

输出:

answer message(89):Sun Jan 12 19:25:44 2020

answer message(96):Sun Jan 12 19:25:44 2020

answer message(41):Sun Jan 12 19:25:44 2020

all task finish. used time=430 ms

上面是返回数据的一部分,下面是计算得到的总时间是430ms。

 

二、慢服务器

1、迭代做法

void slow_handle_function(int id=0){
    cout<<"start processing...  -"<<id<<endl;
    sleep(3);
    cout<<"ok,finish task.  -"<<id<<endl;
    
}

首先我们有这么一个很慢的函数,要花费3秒才能完成。

 

 

void server1(){
    const uint16_t listened_port=9000;
    const char* localhost="127.0.0.1";
    const int listening_queue_length=1024;
    const int buffersize=1024;

    int listen_fd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in server_addr;

    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    in_addr temp;
    inet_pton(AF_INET,localhost,&server_addr.sin_addr);
    //server_addr.sin_addr.s_addr=htonl(temp.s_addr);
    server_addr.sin_port=htons(listened_port);

    bind(listen_fd,(const sockaddr*)&server_addr,sizeof(server_addr));
    listen(listen_fd,listening_queue_length);
    char buffer[buffersize];
    while(1){
        sockaddr_in client_addr;
        socklen_t len=sizeof(client_addr);
        cout<<"wating for a connection..."<<endl;
        int connect_fd=accept(listen_fd,(sockaddr*)&client_addr,&len);
        cout<<"server get a client connect ;ip="<<inet_ntop(AF_INET,&client_addr.sin_addr,buffer,sizeof(buffer))
            <<" port="<<ntohs(client_addr.sin_port)<<endl;
        auto n=read(connect_fd,buffer,buffersize);
        buffer[n]='\0';
        cout<<"get id="<<buffer<<endl;
        slow_handle_function(std::stoi(buffer));
        write(connect_fd,buffer,strlen(buffer));
    }
}

服务器1是迭代的顺序处理函数,把id回射给客户端。

运行10个客户,每个发起5次请求,结果是:

pressure_test(10,5);

answer message(4):4
answer message(8):8
all task finish. used time=150.025 s


足足花了150s,其实挺好理解,每个处理花3s,一共3*10*5=150s;

 

2、以多进程为基础的服务器

void server2(){
    const uint16_t listened_port=9000;
    const char* localhost="127.0.0.1";
    const int listening_queue_length=1024;
    const int buffersize=1024;

    int listen_fd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in server_addr;

    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    in_addr temp;
    inet_pton(AF_INET,localhost,&server_addr.sin_addr);
    //server_addr.sin_addr.s_addr=htonl(temp.s_addr);
    server_addr.sin_port=htons(listened_port);

    bind(listen_fd,(const sockaddr*)&server_addr,sizeof(server_addr));
    listen(listen_fd,listening_queue_length);
    char buffer[buffersize];

    pid_t pid;
    while(1){
        sockaddr_in client_addr;
        socklen_t len=sizeof(client_addr);
        cout<<"wating for a connection..."<<endl;
        int connect_fd=accept(listen_fd,(sockaddr*)&client_addr,&len);
        if((pid=fork())==0){
            close(listen_fd);
            auto n=read(connect_fd,buffer,buffersize);
            buffer[n]='\0';
            cout<<"get id="<<buffer<<endl;
            slow_handle_function(std::stoi(buffer));
            write(connect_fd,buffer,strlen(buffer));
            exit(0);
        }
        close(connect_fd);
    }
}

利用fork函数可以新建一个进程,fork的特点是调用一次返回2次,在父进程中返回新建的子进程pid,在子进程中返回0;

因此这个if里面的内容都是针对子进程的。

首先关闭listend_fd,是因为只需要主进程监听这个描述符就可以了,不需要子进程,但是由于子进程完全复制父进程的数据,所以,此时子进程首先就要关闭它,处理完后调用exit直接退出,在循环内,把刚才的connect_fd关闭,这个是针对父进程的,因为这个描述符不需要父进程来处理。

同样和上面一样的压力测试:

answer message(10):10
answer message(6):6
answer message(5):5
all task finish. used time=15.0138 s

结果只有15s,快了10倍!

 

三、使用线程

学过操作系统的人都知道,进程的创建/销毁代价很大,可能会严重影响性能。所以在现代的服务器中使用的都是“多线程”而不是“多进程”。

c++11标准可以使用thread类来构造一个线程处理每一个请求。

void task_handle(int fd){
    const int buffersize=1024;
    char buffer[buffersize];
    auto n=read(fd,buffer,buffersize);
    buffer[n]='\0';
    cout<<"get id="<<buffer<<endl;

    slow_handle_function(std::stoi(buffer));
    write(fd,buffer,strlen(buffer));
    close(fd);
}
void server3(){
    const uint16_t listened_port=9000;
    const char* localhost="127.0.0.1";
    const int listening_queue_length=1024;
    const int buffersize=1024;

    int listen_fd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in server_addr;

    bzero(&server_addr,sizeof(server_addr));
    server_addr.sin_family=AF_INET;
    in_addr temp;
    inet_pton(AF_INET,localhost,&server_addr.sin_addr);
    //server_addr.sin_addr.s_addr=htonl(temp.s_addr);
    server_addr.sin_port=htons(listened_port);

    bind(listen_fd,(const sockaddr*)&server_addr,sizeof(server_addr));
    listen(listen_fd,listening_queue_length);

    while(1){
        sockaddr_in client_addr;
        socklen_t len=sizeof(client_addr);
        cout<<"wating for a connection..."<<endl;
        int connect_fd=accept(listen_fd,(sockaddr*)&client_addr,&len);
        thread t(task_handle,connect_fd);
        t.detach();

    }
}

这里面使用了thread类的里面的detach方法,可以在后台用线程处理任务。

任务封装到一个函数里面,并且这次由子线程关闭描述符,测试结果如下:

answer message(10):10
answer message(9):9
answer message(8):8
all task finish. used time=15.0064 s


如果说15s之外的时间是计算机处理线程或者进程的时间,那么,虽然不是很明显,但的确可以看见多线程快于多进程!

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!