文章目录
链接: 原文章链接
感兴趣的可以关注一下公众号,会第一时间给您推送更多精彩的内容,欢迎大家前来指正,欢迎欢迎~~
1、项目介绍
1、按照包头+包体的格式收发数据包,解决粘包的问题
2、非常完整的多线程高并发服务器
3、根据收到数据包执行,不同的业务逻辑函数
用到的技术:
epoll高并发通讯技术,用的是水平触发【LT】水平触发模式
通过线程池技术处理业务逻辑
多线程、之间同步技术使用,互斥量、和条件变量
一个master管理进程,多个worker工作进程
信号、守护进程
2、项目流程
2.1、环境变量搬家
1、统计argv总大小,以及环境变量总共占用多少内存,然后创建出来一块内存,将原先的环境变量拷贝到我们创建的内存中,为后续设置进程名做铺垫。
2、代码如下,和我们描述过程一直,没有多余操作
//统计argv所占的内存
g_argvneedmem = 0;
for(int i = 0; i < argc; i++) //argv = ./nginx -a -b -c asdfas
{
g_argvneedmem += strlen(argv[i]) + 1; //+1是给\0留空间。
}
//统计环境变量所占的内存。注意判断方法是environ[i]是否为空作为环境变量结束标记
for(int i = 0; environ[i]; i++)
{
g_envneedmem += strlen(environ[i]) + 1; //+1是因为末尾有\0,是占实际内存位置的,要算进来
} //end for
g_os_argc = argc; //保存参数个数
g_os_argv = (char **) argv; //保存参数指针
//设置可执行程序标题相关函数:分配内存,并且把环境变量拷贝到新内存中来
//这里无需判断penvmen == NULL,有些编译器new会返回NULL,有些会报异常,但不管怎样,如果在重要的地方new失败了,你无法收场,让程序失控崩溃,助你发现问题为好;
gp_envmem = new char[g_envneedmem];
memset(gp_envmem,0,g_envneedmem); //内存要清空防止出现问题
char *ptmp = gp_envmem;
//把原来的内存内容搬到新地方来
for (int i = 0; environ[i]; i++)
{
size_t size = strlen(environ[i])+1 ; //不要拉下+1,否则内存全乱套了,因为strlen是不包括字符串末尾的\0的
strcpy(ptmp,environ[i]); //把原环境变量内容拷贝到新地方【新内存】
environ[i] = ptmp; //然后还要让新环境变量指向这段新内存
ptmp += size;
}
2.2、设置进程title
将进程名字传进来,计算名字长度要是大于(argv参数总长度和环境变量总长度之和)就不然设置,反之可以,然后将argv[1]设置为NULL以免影响后续,然后将title拷贝进去,将名字之后的内存空间都置为空
void ngx_setproctitle(const char *title)
{
//我们假设,所有的命令 行参数我们都不需要用到了,可以被随意覆盖了;
//注意:我们的标题长度,不会长到原始标题和原始环境变量都装不下,否则怕出问题,不处理
//(1)计算新标题长度
size_t ititlelen = strlen(title);
//(2)计算总的原始的argv那块内存的总长度【包括各种参数】
size_t esy = g_argvneedmem + g_envneedmem; //argv和environ内存总和
if( esy <= ititlelen)
{
//你标题多长啊,我argv和environ总和都存不下?注意字符串末尾多了个 \0,所以这块判断是 <=【也就是=都算存不下】
return;
}
//空间够保存标题的,够长,存得下,继续走下来
//(3)设置后续的命令行参数为空,表示只有argv[]中只有一个元素了,这是好习惯;防止后续argv被滥用,因为很多判断是用argv[] == NULL来做结束标记判断的;
g_os_argv[1] = NULL;
//(4)把标题弄进来,注意原来的命令行参数都会被覆盖掉,不要再使用这些命令行参数,而且g_os_argv[1]已经被设置为NULL了
char *ptmp = g_os_argv[0]; //让ptmp指向g_os_argv所指向的内存
strcpy(ptmp,title);
ptmp += ititlelen; //跳过标题
//(5)把剩余的原argv以及environ所占的内存全部清0,否则会出现在ps的cmd列可能还会残余一些没有被覆盖的内容;
size_t cha = esy - ititlelen; //内存总和减去标题字符串长度(不含字符串末尾的\0),剩余的大小,就是要memset的;
memset(ptmp,0,cha);
}
2.3、信号初始化
需要设置信号处理函数的信号数组定义
typedef struct
{
int signo; //信号对应的数字编号 ,每个信号都有对应的#define
const char *signame; //信号对应的中文名字 ,比如SIGHUP
//信号处理函数,这个函数由我们自己来提供,但是它的参数和返回值是固定的
void (*handler)(int signo, siginfo_t *siginfo, void *ucontext); //函数指针, siginfo_t:系统定义的结构
} ngx_signal_t;
static ngx_signal_t signals[] = {
// signo signame handler
{ SIGHUP, "SIGHUP", ngx_signal_handler }, //终端断开信号,对于守护进程常用于reload重载配置文件通知--标识1
{ SIGINT, "SIGINT", ngx_signal_handler }, //标识2
{ SIGTERM, "SIGTERM", ngx_signal_handler }, //标识15
{ SIGCHLD, "SIGCHLD", ngx_signal_handler }, //子进程退出时,父进程会收到这个信号--标识17
{ SIGQUIT, "SIGQUIT", ngx_signal_handler }, //标识3
{ SIGIO, "SIGIO", ngx_signal_handler }, //指示一个异步I/O事件【通用异步I/O信号】
{ SIGSYS, "SIGSYS, SIG_IGN", NULL }, //我们想忽略这个信号,SIGSYS表示收到了一个无效系统调用,如果我们不忽略,进程会被操作系统杀死,--标识31
//所以我们把handler设置为NULL,代表 我要求忽略这个信号,请求操作系统不要执行缺省的该信号处理动作(杀掉我)
//...日后根据需要再继续增加
{ 0, NULL, NULL } //信号对应的数字至少是1,所以可以用0作为一个特殊标记
};
为以下的信号,通过sigaction函数设置信号处理函数,
//初始化信号的函数,用于注册信号处理程序
//返回值:0成功 ,-1失败
int ngx_init_signals()
{
ngx_signal_t *sig; //指向自定义结构数组的指针
struct sigaction sa; //sigaction:系统定义的跟信号有关的一个结构,我们后续调用系统的sigaction()函数时要用到这个同名的结构
for (sig = signals; sig->signo != 0; sig++) //将signo ==0作为一个标记,因为信号的编号都不为0;
{
//我们注意,现在要把一堆信息往 变量sa对应的结构里弄 ......
memset(&sa,0,sizeof(struct sigaction));
if (sig->handler) //如果信号处理函数不为空,这当然表示我要定义自己的信号处理函数
{
sa.sa_sigaction = sig->handler; //sa_sigaction:指定信号处理程序(函数),注意sa_sigaction也是函数指针,是这个系统定义的结构sigaction中的一个成员(函数指针成员);
sa.sa_flags = SA_SIGINFO; //sa_flags:int型,指定信号的一些选项,设置了该标记(SA_SIGINFO),就表示信号附带的参数可以被传递到信号处理函数中
//说白了就是你要想让sa.sa_sigaction指定的信号处理程序(函数)生效,你就把sa_flags设定为SA_SIGINFO
}
else
{
sa.sa_handler = SIG_IGN; //sa_handler:这个标记SIG_IGN给到sa_handler成员,表示忽略信号的处理程序,否则操作系统的缺省信号处理程序很可能把这个进程杀掉;
//其实sa_handler和sa_sigaction都是一个函数指针用来表示信号处理程序。只不过这两个函数指针他们参数不一样, sa_sigaction带的参数多,信息量大,
//而sa_handler带的参数少,信息量少;如果你想用sa_sigaction,那么你就需要把sa_flags设置为SA_SIGINFO;
} //end if
sigemptyset(&sa.sa_mask); //比如咱们处理某个信号比如SIGUSR1信号时不希望收到SIGUSR2信号,那咱们就可以用诸如sigaddset(&sa.sa_mask,SIGUSR2);这样的语句针对信号为SIGUSR1时做处理,这个sigaddset
//这里.sa_mask是个信号集(描述信号的集合),用于表示要阻塞的信号,sigemptyset():把信号集中的所有信号清0,本意就是不准备阻塞任何信号;
//设置信号处理动作(信号处理函数),说白了这里就是让这个信号来了后调用我的处理程序,有个老的同类函数叫signal,不过signal这个函数被认为是不可靠信号语义,不建议使用,大家统一用sigaction
if (sigaction(sig->signo, &sa, NULL) == -1) //参数1:要操作的信号
//参数2:主要就是那个信号处理函数以及执行信号处理函数时候要屏蔽的信号等等内容
//参数3:返回以往的对信号的处理方式【跟sigprocmask()函数边的第三个参数是的】,跟参数2同一个类型,我们这里不需要这个东西,所以直接设置为NULL;
{
return -1; //有失败就直接返回
}
else
{
}
} //end for
return 0; //成功
}
信号处理函数功能:目前只要只是做了master进程和worker进程信号分类处理(因为我们是进程程序),当woker进程死亡时,使用waitpid进行回收,防止变成僵尸进程。
//信号处理函数
//siginfo:这个系统定义的结构中包含了信号产生原因的有关信息
static void ngx_signal_handler(int signo, siginfo_t *siginfo, void *ucontext)
{
//printf("来信号了\n");
ngx_signal_t *sig; //自定义结构
char *action; //一个字符串,用于记录一个动作字符串以往日志文件中写
for (sig = signals; sig->signo != 0; sig++) //遍历信号数组
{
//找到对应信号,即可处理
if (sig->signo == signo)
{
break;
}
} //end for
action = (char *)""; //目前还没有什么动作;
if(ngx_process == NGX_PROCESS_MASTER) //master进程,管理进程,处理的信号一般会比较多
{
//master进程的往这里走
switch (signo)
{
case SIGCHLD: //一般子进程退出会收到该信号
ngx_reap = 1; //标记子进程状态变化,日后master主进程的for(;;)循环中可能会用到这个变量【比如重新产生一个子进程】
break;
//.....其他信号处理以后待增加
default:
break;
} //end switch
}
else if(ngx_process == NGX_PROCESS_WORKER) //worker进程,具体干活的进程,处理的信号相对比较少
{
//worker进程的往这里走
//......以后再增加
//....
}
else
{
//非master非worker进程,先啥也不干
//do nothing
} //end if(ngx_process == NGX_PROCESS_MASTER)
//这里记录一些日志信息
//siginfo这个
if(siginfo && siginfo->si_pid) //si_pid = sending process ID【发送该信号的进程id】
{
printf("signal %d (%s) received from %P%s", signo, sig->signame, siginfo->si_pid, action);
}
else
{
printf("signal %d (%s) received %s",signo, sig->signame, action);//没有发送该信号的进程id,所以不显示发送该信号的进程id
}
//子进程状态有变化,通常是意外退出
if (signo == SIGCHLD)
{
ngx_process_get_status(); //获取子进程的结束状态
} //end if
return;
}
通过waitpid函数,回收woker进程回收,设置WNOHANG为非阻塞调用。
返回值:
进程id:正常返回
0 表示子进程还没有结束
-1:
errno = EINTR 信号中断
errno = ECHILD 没有子进程
//获取子进程的结束状态,防止单独kill子进程时子进程变成僵尸进程
static void ngx_process_get_status(void)
{
pid_t pid;
int status;
int err;
int one=0; //抄自官方nginx,应该是标记信号正常处理过一次
//当你杀死一个子进程时,父进程会收到这个SIGCHLD信号。
for ( ;; )
{
// 这个waitpid说白了获取子进程的终止状态,这样,子进程就不会成为僵尸进程了;
//第一次waitpid返回一个> 0值,表示成功,
//第二次再循环回来,再次调用waitpid会返回一个0,表示子进程还没结束,然后这里有return来退出;
pid = waitpid(-1, &status, WNOHANG); //第一个参数为-1,表示等待任何子进程,
//第二个参数:保存子进程的状态信息(大家如果想详细了解,可以百度一下)。
//第三个参数:提供额外选项,WNOHANG表示不要阻塞,让这个waitpid()立即返回
if(pid == 0) //子进程没结束,会立即返回这个数字,但这里应该不是这个数字【因为一般是子进程退出时会执行到这个函数】
{
return;
} //end if(pid == 0)
//-------------------------------
if(pid == -1)//这表示这个waitpid调用有错误,有错误也理解返回出去,我们管不了这么多
{
//这里处理代码抄自官方nginx,主要目的是打印一些日志。考虑到这些代码也许比较成熟,所以,就基本保持原样照抄吧;
err = errno;
if(err == EINTR) //调用被某个信号中断
{
continue;
}
if(err == ECHILD && one) //没有子进程
{
return;
}
if (err == ECHILD) //没有子进程
{
return;
}
return;
} //end if(pid == -1)
//-------------------------------
//走到这里,表示 成功【返回进程id】 ,这里根据官方写法,打印一些日志来记录子进程的退出
one = 1; //标记waitpid()返回了正常的返回值
if(WTERMSIG(status)) //获取使子进程终止的信号编号
{
printf("pid = %P exited on signal %d!",pid,WTERMSIG(status)); //获取使子进程终止的信号编号
}
else
{
printf("pid = %P exited with code %d!",pid,WEXITSTATUS(status)); //WEXITSTATUS()获取子进程传递给exit或者_exit参数的低八位
}
} //end for
return;
}
2.4、开始监听端口
监听这个端口是否有连接到来,有连接上来并完成三次握手,就会将这个连接放入以完成队列中,等待accpet去连接这个用户,并且将监听套接字设置为非阻塞。
// 监听套接字
void listen_socket()
{
connfd = socket(AF_INET,SOCK_STREAM,0);
if(connfd<0)
{
cout<<"socket failed:"<<connfd<<endl;
return;
}
// 设置端口服用解决TIME_WAIT状态
int op = 1;
if(setsockopt(connfd,SOL_SOCKET,SO_REUSEADDR,(const void*)&op,sizeof(op))==-1)
{
cout<<"setsockopt failed"<<endl;
close(connfd);
return;
}
// 设置非阻塞
int nb = 1;
if(ioctl(connfd,FIONBIO,&nb) == -1)
{
cout<<"ioctl failed"<<endl;
close(connfd);
return;
}
// bind端口和ip地址
struct sockaddr_in ser_addr;
ser_addr.sin_family = AF_INET;
ser_addr.sin_port = (in_port_t)(htons(PORT));
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(connfd,(struct sockaddr*)&ser_addr,sizeof(ser_addr)) == -1)
{
cout<<"bind failed"<<endl;
close(connfd);
return;
}
// listen 监听套接字
if(listen(connfd,511)==-1)
{
cout<<"listen failed"<<endl;
close(connfd);
return;
}
}
设置套接字非阻塞方法
bool setnonblocking(int sockfd)
{
int nb=1; //0:清除,1:设置
if(ioctl(sockfd, FIONBIO, &nb) == -1) //FIONBIO:设置/清除非阻塞I/O标记:0:清除,1:设置
{
return false;
}
return true;
//如下也是一种写法,跟上边这种写法其实是一样的,但上边的写法更简单
/*
//fcntl:file control【文件控制】相关函数,执行各种描述符控制操作
//参数1:所要设置的描述符,这里是套接字【也是描述符的一种】
int opts = fcntl(sockfd, F_GETFL); //用F_GETFL先获取描述符的一些标志信息
if(opts < 0)
{
ngx_log_stderr(errno,"CSocekt::setnonblocking()中fcntl(F_GETFL)失败.");
return false;
}
opts |= O_NONBLOCK; //把非阻塞标记加到原来的标记上,标记这是个非阻塞套接字【如何关闭非阻塞呢?opts &= ~O_NONBLOCK,然后再F_SETFL一下即可】
if(fcntl(sockfd, F_SETFL, opts) < 0)
{
ngx_log_stderr(errno,"CSocekt::setnonblocking()中fcntl(F_SETFL)失败.");
return false;
}
return true;
*/
}
2.5、创建守护进程
使程序脱离终端,在后台运行
1、fork出一个子进程,主进程退出所有流程,子进程继续向下走
2、设置setsid,脱离终端,与父进程脱关系
3、设置umask文件权限
4、以读写方式打开/dev/null
5、将输入输出指向黑洞
6、关闭刚才打开的文件描述符
// 子进程返回0,父进程返回1,执行失败返回-1
static int ngx_daemon()
{
switch (fork())
{
case -1:
cout<<"ngx_daemon failed"<<endl;
return -1;
case 0:
break;
default:
return 1;
}
// 只有fork出来的子进程才能走到这个流程
if(setsid() == -1)
{
cout<<"ngx_daemon setsid failed"<<endl;
return -1;
}
umask(0);
// 打开黑洞设备,以读写方式打开
int fd = open("/dev/null",O_RDWR);
if(fd == -1)
{
cout<<"ngx_daemon open failed"<<endl;
return -1;
}
if(dup2(fd,STDIN_FILENO) == -1)
{
cout<<"ngx_daemon dup2 failed"<<endl;
return -1;
}
if(dup2(fd,STDOUT_FILENO)==-1)
{
cout<<"ngx_daemon dup2 failed"<<endl;
return -1;
}
if(fd>STDERR_FILENO)
{
if(close(fd)==-1)
{
cout<<"ngx_daemon close failed"<<endl;
return -1;
}
}
return 0;
}
2.6、创建子进程
master进程流程,为了不让创建woker进程过程中被信号给打断,为master进程设置信号集,创建完子进程,将信号集设置为空,此时master进程进入死循环,通过sigsuspend函数处理信号,它会讲我们刚才恢复为空的信号集设置进去,然后等待信号来临,一旦收到信号将恢复之前我们设置的信号屏蔽字,调用信号处理函数,等待信号处理函数返回,它才返回。
static u_char master_process[] = "master process";
//描述:创建worker子进程
void ngx_master_process_cycle()
{
sigset_t set; //信号集
sigemptyset(&set); //清空信号集
//建议fork()子进程时学习这种写法,防止信号的干扰;
sigaddset(&set, SIGCHLD); //子进程状态改变
sigaddset(&set, SIGALRM); //定时器超时
sigaddset(&set, SIGIO); //异步I/O
sigaddset(&set, SIGINT); //终端中断符
sigaddset(&set, SIGHUP); //连接断开
sigaddset(&set, SIGUSR1); //用户定义信号
sigaddset(&set, SIGUSR2); //用户定义信号
sigaddset(&set, SIGWINCH); //终端窗口大小改变
sigaddset(&set, SIGTERM); //终止
sigaddset(&set, SIGQUIT); //终端退出符
//.........可以根据开发的实际需要往其中添加其他要屏蔽的信号......
//设置,此时无法接受的信号;阻塞期间,你发过来的上述信号,多个会被合并为一个,暂存着,等你放开信号屏蔽后才能收到这些信号。。。
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) //第一个参数用了SIG_BLOCK表明设置 进程 新的信号屏蔽字 为 “当前信号屏蔽字 和 第二个参数指向的信号集的并集
{
printf("ngx_master_process_cycle()中sigprocmask()失败!");
}
//即便sigprocmask失败,程序流程 也继续往下走
//首先我设置主进程标题---------begin
size_t size;
int i;
size = sizeof(master_process); //注意我这里用的是sizeof,所以字符串末尾的\0是被计算进来了的
size += g_argvneedmem; //argv参数长度加进来
if(size < 1000) //长度小于这个,我才设置标题
{
char title[1000] = {0};
strcpy(title,(const char *)master_process); //"master process"
strcat(title," "); //跟一个空格分开一些,清晰 //"master process "
for (i = 0; i < g_os_argc; i++) //"master process ./nginx"
{
strcat(title,g_os_argv[i]);
}//end for
ngx_setproctitle(title); //设置标题
printf("%s %P 【master进程】启动并开始运行......!",title,ngx_pid); //设置标题时顺便记录下来进程名,进程id等信息到日志
}
//首先我设置主进程标题---------end
int workprocess = 5;
ngx_start_worker_processes(workprocess); //这里要创建worker子进程
//创建子进程后,父进程的执行流程会返回到这里,子进程不会走进来
sigemptyset(&set); //信号屏蔽字为空,表示不屏蔽任何信号
for ( ;; )
{
// usleep(100000);
//ngx_log_error_core(0,0,"haha--这是父进程,pid为%P",ngx_pid);
// sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。
// sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。
//sigsuspend是一个原子操作,包含4个步骤:
//a)根据给定的参数设置新的mask 并 阻塞当前进程【因为是个空集,所以不阻塞任何信号】
//b)此时,一旦收到信号,便恢复原先的信号屏蔽【我们原来调用sigprocmask()的mask在上边设置的,阻塞了多达10个信号,从而保证我下边的执行流程不会再次被其他信号截断】
//c)调用该信号对应的信号处理函数
//d)信号处理函数返回后,sigsuspend返回,使程序流程继续往下走
//printf("for进来了!\n"); //发现,如果print不加\n,无法及时显示到屏幕上,是行缓存问题,以往没注意;可参考https://blog.csdn.net/qq_26093511/article/details/53255970
sigsuspend(&set); //阻塞在这里,等待一个信号,此时进程是挂起的,不占用cpu时间,只有收到信号才会被唤醒(返回);
//此时master进程完全靠信号驱动干活
// printf("执行到sigsuspend()下边来了\n");
//printf("master进程休息1秒\n");
//ngx_log_stderr(0,"haha--这是父进程,pid为%P",ngx_pid);
sleep(1); //休息1秒
//以后扩充.......
}// end for(;;)
return;
}
通过for循环将所有子进程创建出来,并且设置进程名字、信号集设置为空,进入死循环,后续子进程,后续业务逻辑主要在子进程中完成,master进程主要起管理作用。
//描述:根据给定的参数创建指定数量的子进程,因为以后可能要扩展功能,增加参数,所以单独写成一个函数
//threadnums:要创建的子进程数量
static void ngx_start_worker_processes(int threadnums)
{
int i;
for (i = 0; i < threadnums; i++) //master进程在走这个循环,来创建若干个子进程
{
ngx_spawn_process(i,"worker process");
} //end for
return;
}
//描述:产生一个子进程
//inum:进程编号【0开始】
//pprocname:子进程名字"worker process"
static int ngx_spawn_process(int inum,const char *pprocname)
{
pid_t pid;
pid = fork(); //fork()系统调用产生子进程
switch (pid) //pid判断父子进程,分支处理
{
case -1: //产生子进程失败
ngx_log_error_core(NGX_LOG_ALERT,errno,"ngx_spawn_process()fork()产生子进程num=%d,procname=\"%s\"失败!",inum,pprocname);
return -1;
case 0: //子进程分支
ngx_parent = ngx_pid; //因为是子进程了,所有原来的pid变成了父pid
ngx_pid = getpid(); //重新获取pid,即本子进程的pid
ngx_worker_process_cycle(inum,pprocname); //我希望所有worker子进程,在这个函数里不断循环着不出来,也就是说,子进程流程不往下边走;
break;
default: //这个应该是父进程分支,直接break;,流程往switch之后走
break;
}//end switch
//父进程分支会走到这里,子进程流程不往下边走-------------------------
//若有需要,以后再扩展增加其他代码......
return pid;
}
//描述:worker子进程的功能函数,每个woker子进程,就在这里循环着了(无限循环【处理网络事件和定时器事件以对外提供web服务】)
// 子进程分叉才会走到这里
//inum:进程编号【0开始】
static void ngx_worker_process_cycle(int inum,const char *pprocname)
{
//设置一下变量
ngx_process = NGX_PROCESS_WORKER; //设置进程的类型,是worker进程
//重新为子进程设置进程名,不要与父进程重复------
sigset_t set; //信号集
sigemptyset(&set); //清空信号集
if (sigprocmask(SIG_SETMASK, &set, NULL) == -1) //原来是屏蔽那10个信号【防止fork()期间收到信号导致混乱】,现在不再屏蔽任何信号【接收任何信号】
{
cout<<"ngx_worker_process_cycle failed"<<endl;
}
ngx_setproctitle(pprocname); //设置标题
printf("%s %P 【worker进程】启动并开始运行......!",pprocname,ngx_pid); //设置标题时顺便记录下来进程名,进程id等信息到日志
for(;;)
{
} //end for(;;)
return;
}