在第一节的例子中,服务器是一个时间获取程序,只要一次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之外的时间是计算机处理线程或者进程的时间,那么,虽然不是很明显,但的确可以看见多线程快于多进程!
来源:CSDN
作者:_六六先森
链接:https://blog.csdn.net/weixin_37373020/article/details/103948260