【nginx】服务器业务逻辑处理框架

业务逻辑之多线程、线程池实战

一、多线程的提出

用 “线程” 来解决客户端发送过来的数据包。

一个进程跑起来之后,缺省就自动启动了一个 “主线程”,也就是我们一个worker进程一启动就等于只有一个“主线程”在跑。

多线程处理的必要性:充值服务器通讯,一般需要数秒到数十秒的通讯时间,一个线程因为充值被卡住,还有其他线程可以提供给其他玩家及时的服务。所以,服务器端处理用户需求(用户逻辑/业务)的时候一般都会启动几十甚至上百个线程来处理,以保证用户的需求能够得到及时处理。

【设计思路】主线程往消息队列中用inMsgRecvQueue()扔完整包(用户需求),那么一堆线程要从这个消息对列中取走这个包,所在必须要用互斥。也就是说,同一时刻只能有一个线程来操作这个队列。

POSIX(可移植操作系统接口)和POSIX线程:定义了一堆我们可以调用的函数,一般是以pthread_开头,比较成熟,比较好用。

二、线程池实战代码

1、为什么引入线程池

线程池: 就是提前创建好一堆线程,并搞一个类来统一管理和调度这一堆线程(这一堆线程我们就叫做线程池)。

当来了一个消息的时候,从这一堆线程中找一个空闲的线程去处理这个消息,活干完之后,这个线程里边有一个循环语句,可以循环回来等待新任务,再有新任务的时候再去执行新的任务,也就是回收再利用。

【线程池存在意义和价值】

a)事先创建好一堆线程,避免动态创建线程来执行任务,提高了程序的稳定性,有效的规避程序运行之中创建线程有可能失败的风险,如果事先创建一般会成功(因为还没怎么占用系统资源),而且即使失败了程序员也能知道为什么失败。

b)提高程序运行效率:线程池中的线程,反复循环再利用,不用创建和释放线程(都需要资源和时间)。

c)容易管理(使编码更清晰简单)

【注意】为了支持pthread多线程库,gcc 末尾要增加 -lpthread。

讲解了 Create(),ThreadFunc(),StopAll()。

三、线程池的使用

1、线程池的初始化 :Create()

2、线程池工作的激发:就是让线程池开始干活了,当收到了一个完整的用户来的消息的时候,就要激发这个线程池来获取消息开始工作;用pthread_cond_signal来唤醒一个(或多个)卡在pthread_cond_wait的线程。精华代码如下:

while((jobbuf = g_socket.outMsgRecvQueue()) == NULL && m_shutdown == false)
{
    //如果这个pthread_cond_wait被唤醒【被唤醒后程序执行流程往下走的前提是拿到了锁--官方:pthread_cond_wait()返回时,互斥量再次被锁住】,
    //那么会立即再次执行g_socket.outMsgRecvQueue(),如果拿到了一个NULL,则继续在这里wait着();
    if(pThread->ifrunning == false)            
    pThread->ifrunning = true; //标记为true了才允许调用StopAll():测试中发现如果Create()和StopAll()紧挨着调用,就会导致线程混乱,所以每个线程必须执行到这里,才认为是启动成功了;        
    //ngx_log_stderr(0,"执行了pthread_cond_wait-------------begin");
    //刚开始执行pthread_cond_wait()的时候,会卡在这里,而且m_pthreadMutex会被释放掉;
    pthread_cond_wait(&m_pthreadCond, &m_pthreadMutex); //整个服务器程序刚初始化的时候,所有线程必然是卡在这里等待的;
    //ngx_log_stderr(0,"执行了pthread_cond_wait-------------end");
}

3、线程池完善和测试

如果只开一个线程,而来多个消息,发现消息会堆积但是不会丢消息,逐条处理,因为每次while消息队列都不为空,所以不会卡在pthread_cond_wait,也就不需要多次call来调用pthread_cond_signal用来唤醒,一次足矣;直到消息队列中没有消息了,它才会再次趴在pthread_cond_wait处等待唤醒。

【注意】pthread_cond_signal可能唤醒多个pthread_cond_wait,惊群(虚假唤醒)现象,但是因为唤醒后,都需要回while去消息队列中取数据,所以如果来了一个消息,唤醒了3个线程,那么一定有一个线程先拿到锁,走下来,可以取到数据,别的线程虽然依次拿到锁走下来,但是因为消息队列中没有数据,所以跳不出while,依旧卡在pthread_cond_wait处。

业务逻辑之打通业务处理脉搏实战

一、线程池代码调整及补充说明

支撑线程池的运作主要靠两个函数:

pthread_cond_signal(&m_pthreadCond); => 触发,pthread_cond_wait(&m_pthreadCond, &m_pthreadMutex); => 等待

条件变量:是线程可用的另一种同步机制,条件变量给多个线程提供了一个会合的场所,条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。

a)条件本身【while ( (pThreadPoolObj->m_MsgRecvQueue.size() == 0) && m_shutdown == false)】 是由互斥量保护的(也就是在执行这一行之前要先锁住互斥量,这样保证了在拿到锁的线程执行下来的时候,别的线程是不会拿到锁执行下来,也就不会执行这条语句,也就觉察不到条件改变(消息队列有变化))。

c++11,也有条件变量的说法 my_cond.wait(....), my_cond.notify_one(...)。

b)传递给pthread_cond_wait的互斥量m_pthreadMutex是用来对条件【 while ( (pThreadPoolObj->m_MsgRecvQueue.size() == 0) && m_shutdown == false)】进行保护的,调用者把锁住的互斥量传递给函数pthread_cond_wait,函数自动把调用线程放在等待条件的线程列表 上,对互斥量解锁(说白了就是卡在这里等条件直到signal触发 ,然后放开互斥量)。

条件变化时,pthread_cond_wait会尝试去拿锁,pthread_cond_wait返回成功,互斥量也就被再次锁定,没拿到继续等。

二、线程池实现具体业务之准备代码

1、一个简单的crc32校验算法介绍

CCRC32类:主要目的是对收发的数据包进行一个简单的校验,以确保数据包中的内容没有被篡改过。

Get_CRC():给一段buffer,也就是一段内存,然后给这段内存长度,该函数计算出一个数字来(CRC32值)返回来。

客户端把要发送的包体计算出来一个CRC32值放入包头对应字段,服务器端收到数据时也要取出包体计算这个CRC32值,然后与收到的包头中对应字段比较,如果相等,则认为该数据包的内容没有被篡改。

2、引入新的CSocket子类

真正项目中要把CSocekt类当成父类使用,具体业务逻辑代码应该放在CSocket的子类中。

threadRecvProcFunc()收到消息之后的处理函数(替换上一节的测试代码)。

3、设计模式题外话

不要乱用设计模式,不要乱封装!

4、消息的具体设计

为了能够根据客户端发送过来的消息代码(msgCode)迅速定位到要执行的函数,我们就把客户端发送过来的消息代码直接当做一个数组的下标来用。

服务器开发工作(业务逻辑),主要集中在三个文件中:ngx_logiccomm.h,ngx_c_slogic.cxx,ngx_c_slogic.h

三、threadRecvProcFunc()函数讲解

四、整体测试工作的开展

服务器开发工作,公司配备专门客户端开发人员来开发客户端工作;c/s配合工作,配合指定通讯协议;协议的制定一般是服务器程序员来主导。

a)确定通讯格式是包头+包体,包头固定多少个字节,这种规则在开发一个项目之前,要明确地和客户端交代好;要求客户端给服务器发送数据包时严格遵循这种格式。

b)注册,登录,都属于具体的业务逻辑命令,这种命令一般都是由服务器牵头来制定。

服务器开发难度往往比客户端大很多,要站在客户端的角度负责通讯协议的制定工作,要学会和客户端商量,共同指定协议(消息代码)和数据结构(双方通信结构)。

此外,服务器端有责任把crc32算法给到客户端(如果用的不是c++还要客户端自己改造)!

预发包,多线程资源回收深度思考

一、业务逻辑细节写法说明

_HandleRegister(),_HandleLogIn()里边到底执行什么,是属于业务逻辑代码,自己写的。

二、连接池中连接回收的深度思考

1、一个问题

服务器是一周7天一天24小时不间断工作的,服务器稳定性是第一位的。

【连接池连接回收问题】

如果客户端【张三】断线,服务器端立即回收连接,这个连接很可能被紧随其后连入的新客户端【李四】所使用,那么这里就很可能产生麻烦:

a)张三发数据,服务端从线程池中找一个线程调用 funca();处理,要执行10秒;

b)执行到第5秒的时候,张三断线;

c)张三断线这个事情会被服务器立即感知到(epoll机制),服务器随后调用ngx_close_connection把原来属于张三这个连接池中的连接给回收了;

d)第7秒的时候,李四连上来了,系统会把刚才张三用过的连接池中的连接分配给李四(现在这个连接归李四使用);

e)10秒到了,这个线程很可能会继续操纵连接(修改读数据),连接属于李四但是程序还以为是张三的。

【解决办法】一个连接,如果我们程序判断这个连接不用了,那么不应该把这个连接立即放到空闲队列里,而是应该放到一个地方,等待一段时间(60s),60秒之后,再真正的回收这个连接到连接池/空闲队列中去,这种连接才可以真正的分配给其他用户使用。

为什么要等待60秒?就是需要确保即便用户张三真断线了,那么我执行的该用户的业务逻辑也一定能在这个等待时间内全部完成;这个连接不立即回收是非常重要的,有个时间缓冲,这个可以在极大程度上确保服务器的稳定。

2、灵活创建连接池

如果按上述所述,连接池满了,我断开一个连接,这个连接不会立即放回空闲队列,这时如果再来一个连接就无法连入了(因为满了,空的那个还用不了),所以最好是动态创建连接池,也就是需要的话就重新new,而不是一上来就new定好的内存。

3、连接池中连接的回收

(1)立即回收(accept用户没有接入时可以立即回收):gx_free_connection();

(2)延迟回收(用户接入进来开始干活了):inRecyConnectQueue()把要回收的线程放入一个“垃圾队列”,隔80s后调用ServerRecyConnectionThread()函数来释放回收放入空连接队列。

三、程序退出时线程的安全终止

如何优雅的退出程序(回收资源等)?

四、epoll事件处理的改造

1、增加新的事件处理函数,引入ngx_epoll_oper_event()函数取代ngx_epoll_add_event(),扩展性好;

2、调整对事件处理函数的调用:改动ngx_epoll_init(),ngx_event_accept(),ngx_epoll_process_events();

iCurrsequence这个变量可以防止套接字的张冠李戴,因为释放的时候也要加一,只要不等就丢包即可!

五、连接延迟回收的具体应用

recvproc()调用了inRecyConnectQueue(延迟回收)取代了ngx_close_connection(立即回收);

Initialize_subproc()子进程中干;Shutdown_subproc()子进程中干:先让线程停止干活,再清队列,再清信号、互斥量。

LT发数据机制深释、gdb调试浅谈

一:水平触发模式(LT)下发送数据深度解释

在水平触发模式下,发送数据有哪些注意事项?

调用 ngx_epoll_oper_event(EPOLL_CTL_MOD,EPOLLOUT)修改红黑树结点,那么当socket可写的时候,会触发socket的可写事件,我得到了这个事件(系统通知我)我就可以发送数据了。

什么叫socket可写:每一个tcp连接(socket),都会有一个接收缓冲区和一个发送缓冲,发送缓冲区缺省大小一般10几k,接收缓冲区大概几十k,setsocketopt()来设置;服务端send(),write()发送数据时,实际上这两个函数是把数据放到了发送缓冲区,之后这两个函数返回了;客户端用recv(),read()来接受数据。

如果服务器端的发送缓冲区满了,那么服务器再调用send(),write()发送数据的时候,那么send()、write()函数就会返回一个EAGAIN,EAGAIN不是一个错误,只是示意发送缓冲区已经满了,让程序员迟一些再调用send()、write()来发送数据。

二、gdb调试浅谈

1、当socket可写的时候(发送缓冲区没满),会不停的触发socket可写事件(水平触发模式),已经验证。

2、连续发包2次,子进程worker进程突然崩溃消失,但是不知道是哪行出了问题,所以需要借助gdb调试来找到崩溃行。

a)编译时 g++ 要带这个 -g 选项;

b)su进入root权限,然后gdb nginx调试

c)gdb缺省调试主进程,但是gdb7.0以上版本可以调试子进程(需要调试子进程,因为干活的是worker process是子进程)

d)为了让gdb支持多进程调试,要设置一下 follow-fork-mode 选项 ,这是个调试多进程的开关;

取值可以是parent[主] /child[子] ,这里需要设置成child才能调试worker process子进程;

在gdb下输入show follow-fork-mode可以查看follow-fork-mode的值,输入 set follow-fork-mode child 可以变parent为child。

e)还有个选项 detach-on-fork, 取值为 on/off,默认是on(表示只调试父进程或者子进程其中的一个)

调试是父进程还是子进程,由上边的 follow-fork-mode选项说了算;如果detach-on-fork = off,就表示父子都可以调试,但是要注意,调试一个进程时,另外一个进程会被暂停,比如ffm是parent,那么fork后的子进程是不运行的。

f)b logic/ngx_c_slogic.cxx:198 掐断点(b 文件名:行号)

g)run 运行程序运行到断点

h)停在断点了,可以print打印变量值来做一些测试

i)c 继续运行

ev.data.ptr = (void *)pConn; 解决问题!

三、前面问题的解决

【面试】epoll,水平触发模式下,不停的触发socket可写事件(可读虽然也会不断提醒,但是只要接收缓冲区里的数据都取走处理了,就不会在提醒了,但只要发送缓冲区中有空,就会不停触发可写事件,很麻烦),如何解决?

两种解决方案:

(1)第一种最普遍的解决方案

需要向socket写数据的时候把socket写事件通知加入到epoll中,等待可写事件,当可写事件来时操作系统会通知(epoll_wait走下来);此时可以调用wirte/send函数发送数据,当发送数据完毕后,把socket的写事件通知从红黑树中移除;

缺点:即使发送很少的数据,也需要把事件通知加入到epoll,写完毕后,又需要把写事件通知从红黑树干掉,对效率有一定的影响(有一定的操作代价)

(2)改进方案(利用EAGAIN)

开始不把socket写事件通知加入到epoll,当我需要写数据的时候,直接调用write/send发送数据;如果返回了EAGIN(发送缓冲区满了,需要等待可写事件才能继续往缓冲区里写数据),再把写事件通知加入到epoll,此时,就变成了在epoll驱动下写数据,全部数据发送完毕后,再把写事件通知从epoll中干掉。

优点:数据不多的时候,可以避免epoll的写事件的增加/删除,提高了程序的执行效率。

发数据、信号量、并发、多线程综合实战

一、发送数据指导思想

把要发送的数据放到一个队列中(msgSend),然后专门创建一个线程(ServerSendQueueThread)来统一负责数据发送。

二、发送数据代码实战

1、信号量:也是一种同步机制,跟互斥量有什么不同呢?

互斥量:线程之间同步;信号量:提供进程之间的同步,也能提供线程之间的同步。

【使用方法】

if(sem_init(&m_semEventSendQueue,0,0) == -1){}
//第一个参数:信号量名字
//第二个参数=0:表示信号量在线程之间共享,如果非0,表示在进程之间共享
//第三个参数=0:表示信号量的初始值,为0时,调用sem_wait()就会卡在那里卡着

用之前调用sem_init()初始化一下,信号量的初始值设置为0,用完后用sem_destroy()释放信号量。

sem_wait():测试指定信号量的值,如果该值>0,那么将该值-1然后该函数立即返回;如果该值等于0,那么该线程将投入睡眠中,一直到该值>0,这个时候那么将该值-1然后该函数立即返回。

sem_post():能够将指定信号量值+1,即便当前没有其他线程在等待该信号量值也没关系。

2、数据发送线程

ServerSendQueueThread:按照上节的方法,还处理了多种错误和特殊情况。

3、可写通知到达后数据的继续发送函数

ngx_write_request_handler():这里还可能发不完,那就继续靠系统驱动,此外程序最后一定要执行一次sem_post,因为可能有别的线程正卡在sem_wait。

4、发送数据的简单测试

发送缓冲区大概十几k到几十k,如何把发送缓冲区撑满?

(1)每次服务器给客户端发送65K左右的数据,发送到第20次才出现服务器的发送缓冲区满;这时客户端收了一个包(点击收包),此时执行了 ngx_write_request_handler();

(2)又发包,连续成功发送了16次,才又出现发送缓冲区满;客户端再收包,结果连续收了16次包,服务器才又出现ngx_write_request_handler()函数被成功执行,这表示客户端连续收了16次包,服务器的发送缓冲区才倒腾出地方来;

(3)此后,大概服务器连续发送16次才再出现发送缓冲区满提示,客户端再连续收16次,服务器端ngx_write_request_handler被执行(服务器的发送缓冲区有地方)。

当发送端调用send()发送数据时,操作系统底层已经把数据发送到了该连接的接收端的接收缓存,这个接收缓存大概有几百K。只有当发送的数据量把服务端的发送缓冲区和客户端的接收缓冲区都填满才可能返回EAGAIN,不要以为几十K就可以填满。

猜你喜欢

转载自blog.csdn.net/u012836896/article/details/89315292