这章主要介绍并发服务器的实现方法
在linux系统下创建进程使用fock()函数,通过getpid()函数可以查看当前进程的PID.
父进程就是当前main()函数的进程。下图如果if后面还有代码,各个进程执行了属于自己的if部分的操作后还会执行if后面的代码块,即父子进程都会执行if块后面的代码。
include<iostream>
#include<unistd.h>
int a=10;
int main(int argc,char* argv[]){
std::cout<<"hello"<<std::endl;
std::cout<<"the pid is: "<<(int)getpid()<<std::endl;
++a;
pid_t s=fork();//进程开始
if((int)s==0){
std::cout<<" i am cl:"<<(int)getpid()<<std::endl;
a=a*2;
std::cout<<a<<std::endl;
}else if((int)s>0){
a++;
std::cout<<"the fa pid is: "<<(int)getpid()<<std::endl;
std::cout<<a<<std::endl;
}
std::cout<<a<<std::endl;//各个进程除了执行括号内的操作 还会执行此代码。
return 0;
}
一个进程被创建后执行完相应代码后应改被消除,如果不被消除就会不断占用CPU资源,这样的进程被称之为僵尸进程。
下面将介绍消除僵尸进程的方法:
1.调用wait函数
下面是代码演示:
#include<iostream>
#include<unistd.h>
#include <stdlib.h>
#include<sys/wait.h>
int main(){
int sat;
int pid=fork();
if(pid==0){
return 3;//向父进程传递子进程return的值 子进程1结束
}
else{
std::cout<<"the chiid1 :"<<pid<<std::endl;
std::cout<<"the far :"<<(int)getpid()<<std::endl;
pid=fork();
if(pid==0){
exit(7);//子进程2结束
}
else{
std::cout<<"the child2 :"<<pid<<std::endl;
wait(&sat);
if(WIFEXITED(sat)){
std::cout<<"the child11 :"<<WEXITSTATUS(sat)<<std::endl;
}
wait(&sat);
if(WIFEXITED(sat)){
std::cout<<"the child22 :"<<WEXITSTATUS(sat)<<std::endl;
}
sleep(5);
}
}
}
注意return 3,向父进程返回子进程的值,不会产生僵尸进程 exit(7)也避免产生僵尸进程。
如上,将子进程返回的消息存入在sat中,因为创建了两个子进程,所以调用两次wait函数。
但要注意:
wait函数可能会陷入阻塞。
方法是调用waitpid函数:
注意是第三个参数是防止阻塞的,即使没有结束的子进程,也会返回0
下面是代码演示:
#include<iostream>
#include<unistd.h>
#include <stdlib.h>
#include<sys/wait.h>
int main(){
int sat;
int pid=fork();
if(pid==0){
sleep(15);//子进程代码先推迟15秒 这样子进程就不会消亡
return 24;
}else{
while(!waitpid(-1,&sat,WNOHANG)){//由于子进程未消亡 该函数一直返回0 所以一直执行父进程代码
sleep(3);
std::cout<<"sleep 3 mins"<<std::endl;
}
}
if(WIFEXITED(sat)){
std::cout<<"chile setid: "<<WEXITSTATUS(sat)<<std::endl;
}
return 0;
}
调用sleep和wait函数可以调用子父进程执行的顺序。 这个小技巧要注意。
那么子进程究竟何时才会停止?需要信号量机制,注册信号量机制要特殊的函数,且是由操作系统调用,调用信号量函数会调用自定义函数。下面是函数:
下面介绍alarm函数:
#include<iostream>
#include<unistd.h>
#include<signal.h>
void timeout(int sig){//定义信号处理函数
if(sig==SIGALRM){
std::cout<<"Time out"<<std::endl;
}
alarm(2);//信号处理函数中调用alarm函数 每隔两秒产生SIGALRM信号 因此就会一直调用
}
void aa(int sid){//定义信号处理函数
if(sid==SIGINT){
std::cout<<"CTRL+C pressed"<<std::endl;
}
}
int main(){
int i;
signal(SIGALRM,timeout);//注册信号量和信号处理器
signal(SIGINT,aa);//注册信号量和信号处理器
alarm(2);//预约两秒后产生信号 产生后将会调用timeout函数
for(int i=0;i<3;i++){
puts("wait..");
sleep(100);
}
return 0;
}
注意上述是sleep了三次 当产生信号时会唤醒因为sleep函数而陷入阻塞的进程,当进程被唤醒时不会再进入睡眠状态。所以上述函数是如下运行,当先执行到main()里的alarm(2)时,约定两秒后产生信号,在这两秒内执行for循环内的代码,本应该每次循环拖迟100秒,但是两秒过后产生了信号,执行了timeout函数 被阻塞的进程被唤醒,依次循环三次。
下面将介绍sigcation函数 以此代替signal函数。
代码演示如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>
void timeout(int sig){//定义信号处理函数
if(sig==SIGALRM){
std::cout<<"Time out"<<std::endl;
}
alarm(2);//信号处理函数中调用alarm函数 每隔两秒产生SIGALRM信号 因此就会一直调用
}
void aa(int sid){//定义信号处理函数
if(sid==SIGINT){
std::cout<<"CTRL+C pressed"<<std::endl;
}
}
int main(){
struct sigaction act;//定义结构体变量
act.sa_handler=timeout;//变量内部声名函数指针
act.sa_flags=0;//初始化该成员为0
sigemptyset(&act.sa_mask);//调用函数初始化该成员
sigaction(SIGALRM,&act,0);//注册信号量信息。
alarm(2);
for(int i=0;i<3;i++){
puts("wait..");
sleep(100);
}
return 0;
}
下面我们将利用引号量消除僵尸进程,代码如下,请不用关注子进程的运行顺序,只关系是否消除关于僵尸进程:
#include<iostream>
#include<unistd.h>
#include<sys/wait.h>
#include<netdb.h>
#include<string.h>
void read(int sig){
pid_t pid;
int state;
pid=waitpid(-1,&state,WNOHANG);//结束进程的消息存入在此变量中
if(WIFEXITED(state)){
std::cout<<" reomve the chrl pid: "<<pid<<std::endl;
std::cout<<"chrl send : "<<(WEXITSTATUS(state))<<std::endl;
}
}
int main(int argc,char* argv[]){
pid_t pid;
int state;
struct sigaction act;
act.sa_flags=0;
act.sa_handler=read;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD,&act,0);//注册信号量 当有子进程消失时会调用act中的函数指针
pid=fork();//创建子进程
if(pid==0){
std::cout<<"hello im chrl pid:"<<(int)getpid()<<std::endl;//显示子进程的ID号
sleep(10);//推迟12秒
return 12;
}else{
std::cout<<" hello im father :"<<(int)getpid()<<std::endl;
std::cout<<" the chrl pid :"<<pid<<std::endl;
pid=fork();
if(pid==0){
std::cout<<" im another chr pid :"<<(int)getpid()<<std::endl;
sleep(10);
return 24;
}else{
std::cout<<"the another chr :"<<pid<<std::endl;
for(int i=0;i<5;i++){
puts("wait...");
sleep(5);
}
}
}
return 0;
}
下面我们将开始简单的并发服务器:
如上图,每当由客户端请求连接时,父进程都会创建一个子进程进行提供服务。
下面是并发服务器的客户端,如果有五个请求连接 那么可以同时为五个客户端解决数据,再次注意信号量机制是为了防止僵尸进程的产生,先不要在意各个进程的运行顺序。
#include<iostream>
#include<sys/socket.h>
#include<unistd.h>
#include<cstring>
#include<arpa/inet.h>
#include<sys/wait.h>
const int MAX_SIZE=30;
void read(int sig);
int main(int argc,char* argv[]){
int sock_ser,cil_ser,state,len;
socklen_t cil_size;
struct sockaddr_in serv,cil;
pid_t pid;
char buf[MAX_SIZE];
if(argc!=2){
std::cout<<"error"<<std::endl;
exit(1);
}
sock_ser=socket(PF_INET,SOCK_STREAM,0);
memset(&serv,0,sizeof(serv));
serv.sin_family=AF_INET;
serv.sin_addr.s_addr=htonl(INADDR_ANY);
serv.sin_port=htons(atoi(argv[1]));
struct sigaction act;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
act.sa_handler=read;
state=sigaction(SIGCHLD,&act,0);//信号量设置为 子进程结束时才调用函数 防止僵尸进程产生
if(bind(sock_ser,(struct sockaddr*)&serv,sizeof(serv))==-1){
std::cout<<"bind()error"<<std::endl;
}
if(listen(sock_ser,5)==-1){
std::cout<<"listen()error"<<std::endl;
}
while(1){
cil_size=sizeof(cil);
cil_ser=accept(sock_ser,(struct sockaddr*)&cil,&cil_size);
if(cil_ser==-1){//创造失败跳过
continue;
}else{
puts("new cil connect........");
pid=fork();
if(pid==-1){
close(cil_ser);//子进程创建失败 关闭套接字
continue;//继续
}
if(pid==0){
close(sock_ser);
while((len=read(cil_ser,buf,MAX_SIZE))!=0)
write(cil_ser,buf,len);//传输信息
close(cil_ser); //传输完毕 关闭套接字
puts("disconncet........");
return 0;//结束循环
}else{
close(cil_ser);
}
}
}
close(sock_ser);
return 0;
}
void read(int sig){
pid_t pid;
int state;
pid=waitpid(-1,&state,WNOHANG);
std::cout<<"remove id:"<<pid<<std::endl;
}//当有一个子进程结束时 会调用移除ID
//先不要关心子进程的运行顺序
上面代码注意:while(1)中,pid=0时关闭了服务器的套接字,这里是因为父进程将创建的服务器套接字的文件描述符传递给了子进程,但子进程只进行数据传输,所以要关闭子进程带有的服务端套接字文件描述符,保留父进程的套接字文件描述符。 还有pid>0部分关闭了连接客户端的套接字文件描述符,这是因为 调用的accept函数创建的套接字文件描述符已经赋值给了子进程,传输数据任务交给子进程,传输完毕后由子进程关闭连接客户端的套接字文件描述符。所以由父进程产生的受理客户端的套接字文件描述符不要忘了同时关闭。
多个文件描述符对应一个套接字,父进程不会赋值套接字给子进程,因为套接字属于操作系统,将文件描述符看作套接字的定位。只有关闭所有的套接字描述符才会使得套接字关闭。
下面将介绍切割I/O,对于一般的回声客户端,其传输数据后需要等待服务器返回的数据,因为代码中一直调用write和read函数且只有一个进程在进程,现在可以创造多个进程分割IO,这样不论客户端是否接受数据都可以接受,如下图:
如上图,父进程只读,子进程只些,确认分工,而不是一个进程做多个事。具体效果对比下图,左边是未使用的,右边是使用I/O切割。
下面是简单的代码实现:
#include<sys/socket.h>
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<string.h>
const int MAX_SIZE=30;
void read1(int ,char*);
void write1(int,char*);
int main(int argc,char* argv[]){
int sock;
pid_t pid;
char buf[MAX_SIZE];
struct sockaddr_in cil_serx;
if(argc!=3){
std::cout<<"error()"<<std::endl;
exit(1);
}
sock=socket(PF_INET,SOCK_STREAM,0);
memset(&cil_serx,0,sizeof(cil_serx));
cil_serx.sin_family=AF_INET;
cil_serx.sin_addr.s_addr=inet_addr(argv[1]);
cil_serx.sin_port=htons(atoi(argv[2]));
if(connect(sock,(struct sockaddr*)&cil_serx,sizeof(cil_serx))==-1){
std::cout<<"connect()error()"<<std::endl;
}
pid=fork();//创建子进程 注意已经赋值了文件描述符
if(pid==0){
write1(sock,buf);
}else{
read1(sock,buf);
}
close(sock);
return 0;
}
void read1(int sock,char* buf){
while(1){
int leth=read(sock,buf,MAX_SIZE);//考虑传输数据过大是TCP会分包传送 所以这里都是传输的是短数据
if(leth==0)
return;
buf[leth]=0;
std::cout<<buf<<std::endl;
}
}
void write1(int sock,char* buf){
while(1){
fgets(buf,MAX_SIZE,stdin);
if(!strcmp(buf,"Q\n")||!strcmp(buf,"q\n")){
shutdown(sock,SHUT_WR);//由于只有关闭所有文件描述符才能关闭套接字,这里采用半关闭的原因是 客户端不能向服务端传输数据,但还可以接受服务端的数据,因为是切割I/O,不管服务端接受是否都可以发送,
break;//如果全部关闭可能会导致信息接受不全。
}
write(sock,buf,strlen(buf));
}
}
注意套接字的半关闭,如上半关闭写的操作后,套接字最后在接受服务端传来的数据,接受了数据之后,会完全关闭客户端套接字。