在上一章《Linux C++ 多线程高并发服务器实战项目一》中,讲完了进程相关的一些操作。
1、环境变量搬家,修改进程名字
2、设置信号处理函数,通过waitpid函数回收子进程,防止变成僵尸进程
3、bind绑定ip端口,设置套接字为非阻塞, 监听套接字是否连接
3、进程信号集,防止创建子进程事被信号给中断
4、创建守护进程,脱离终端,在后台运行
5、master进程主要通过sigsuspend函数等待信号发送,不参加业务处理
6、业务处理主要在woker进程中
下面介绍woker进程中主要业务处理
将监听套接字添加到红黑树中,并且将读事件设置进去,当有三次握手成功的连接、和客户端断开时就会通知我们。
创建一个连接池,然后从后向前遍历的方式将其串成一个空闲的连接队列,m_pfree_connections头指针
void epoll_listen_socket()
{
// 创建一颗红黑树
epoll_fd = epoll_create(1024);
if(epoll_fd == -1)
{
cout<<"epoll_create failed"<<endl;
return;
}
// 创建连接池
m_pconnections = new ngx_connection_t[m_pconnection_n];
int i = m_pconnection_n;
lpngx_connection_t next = NULL;
lpngx_connection_t c = m_pconnections;
do
{
i--;
c[i].data = next;
c[i].fd = -1;
c[i].iCurrsequence = 0;
next = &c[i]; // 向前移动
}while(i);
m_free_connection_n = m_pconnection_n;
m_pfree_connections = next;
// 在连接池中取一个空闲链接
c = ngx_get_connection(connfd);
if(c == NULL)
{
cout<<"epoll_listen_socket:连接池中没有空闲链接"<<endl;
return;
}
// 绑定读事件处理函数,监听端口有连接来时触发这个函数
c->rhandler = &ngx_event_accept;
// 添加事件
if(ngx_epoll_oper_event(connfd,EPOLL_CTL_ADD,
EPOLLIN|EPOLLRDHUP,
0,c)==-1)
{
cout<<"ngx_epoll_add_event 调用失败"<<endl;
}
return;
}
连接池引入是为了,将每一个套接字绑定到一块内存结构中,里面记录着后续和客户端通信的参数,
连接池结构
typedef struct ngx_connection_s ngx_connection_t,*lpngx_connection_t;
typedef void (*ngx_event_handler_pt)(lpngx_connection_t c); // 成员函数指针
struct ngx_connection_s
{
int fd;
struct sockaddr_in s_sockaddr;
uint64_t iCurrsequence;
ngx_event_handler_pt rhandler; // 读事件的相关处理方法
ngx_event_handler_pt whandler; // 写事件的相关处理方法
unsigned instance:1; // 【位域】
uint32_t events; // 和epoll相关事件
// 收包相关
unsigned char curStat; // 当前收包状态
char dataHeadInfo[_DATA_BUFSIZE_]; // 用于保存收到的数据包头信息
char *precvbuf; // 接收数据的缓冲区头指针
unsigned int irecvlen; // 要收到多少数据
// 写相关
atomic<int> iThrowsendCount; // 发送消息如果满了需要epoll驱动
char* psendMenPoint; // 发送完成后释放用,整个数据头指针
char* psendbuf; // 发送数据的缓冲区头指针
unsigned int isendlen; // 要发送多少数据
time_t lastPingTime; // 上次ping的时间【上次发送心跳包的事件】
time_t inRecyTime; // 入到资源回收站里面去的时间
// 网络安全相关
uint64_t FloodkicklastTime; // flood攻击上次收到包的时间
int FloodAttackCount; // Flood攻击在该时间内收到包的次数统计
bool ifnewrecvMem;
char *pnewMemPointer; // new出来的用于收包的内存收地址
lpngx_connection_t data; // next成员,后续指针
};
取一个空闲链接,用完归还一个空闲链接
取空闲连接:每次将链表头指针指向当前头的->data指针(后一个),然后把当前头返回
归还连接:归还回来连接放到链表头前面(c->data指向当前头),然后将头指针指向c,保证头指针永远指向头,从而利用一个头指针对空闲连接进行操作。
// 从连接池中取一个空闲连接
lpngx_connection_t ngx_get_connection(int isock)
{
lpngx_connection_t c = m_pfree_connections;
if(c == NULL)
{
cout<<"连接池满了"<<endl;
return NULL;
}
// 相当于将连接池头指向了当前的头的下一个,把当前头返回
m_pfree_connections = c->data;
m_free_connection_n--;
memset(c,0,sizeof(c));
c->fd = isock;
// 初始化收包状态
c->curStat = _PKG_HD_INIT;
c->precvbuf = c->dataHeadInfo;
c->irecvlen = m_iLenPkgHeader;
c->instance = 0;
c->iThrowsendCount = 0;
// 包内存标识符
c->ifnewrecvMem = false;
c->pnewMemPointer = NULL;
c->lastPingTime = time(NULL);
c->FloodAttackCount = 0;
c->FloodkicklastTime = 0;
c->iThrowsendCount = 0;
uint64_t iCurrsequence = c->iCurrsequence; // 暂存序列号
c->iCurrsequence = iCurrsequence;
++c->iCurrsequence;
return c;
}
// 归还参数c所代表的连接到连接池中
void ngx_free_connection(lpngx_connection_t c)
{
if(c->ifnewrecvMem)
{
if(c->pnewMemPointer!=NULL)
{
delete[] c->pnewMemPointer;
c->pnewMemPointer = NULL;
c->ifnewrecvMem = false;
}
// 归还回来的元素放到最前面,链表头的前面,作为新的链表头
c->data = m_pfree_connections;
m_pfree_connections = c;
++c->iCurrsequence;
// 空闲连接+1
++m_free_connection_n;
--m_onlineUserCount;
}
return;
}
主要添加事件,和修改当事件,将分配的连接池的连接传入到ptr指针中,后续使用
//对epoll事件的具体操作
//返回值:成功返回1,失败返回-1;
int ngx_epoll_oper_event(
int fd, //句柄,一个socket
uint32_t eventtype, //事件类型,一般是EPOLL_CTL_ADD,EPOLL_CTL_MOD,EPOLL_CTL_DEL ,说白了就是操作epoll红黑树的节点(增加,修改,删除)
uint32_t flag, //标志,具体含义取决于eventtype
int bcaction, //补充动作,用于补充flag标记的不足 : 0:增加 1:去掉 2:完全覆盖 ,eventtype是EPOLL_CTL_MOD时这个参数就有用
lpngx_connection_t pConn //pConn:一个指针【其实是一个连接】,EPOLL_CTL_ADD时增加到红黑树中去,将来epoll_wait时能取出来用
)
{
struct epoll_event ev;
memset(&ev, 0, sizeof(ev));
if(eventtype == EPOLL_CTL_ADD) //往红黑树中增加节点;
{
//红黑树从无到有增加节点
//ev.data.ptr = (void *)pConn;
ev.events = flag; //既然是增加节点,则不管原来是啥标记
pConn->events = flag; //这个连接本身也记录这个标记
}
else if(eventtype == EPOLL_CTL_MOD)
{
//节点已经在红黑树中,修改节点的事件信息
ev.events = pConn->events; //先把标记恢复回来
if(bcaction == 0)
{
//增加某个标记
ev.events |= flag;
}
else if(bcaction == 1)
{
//去掉某个标记
ev.events &= ~flag;
}
else
{
//完全覆盖某个标记
ev.events = flag; //完全覆盖
}
pConn->events = ev.events; //记录该标记
}
else
{
//删除红黑树中节点,目前没这个需求【socket关闭这项会自动从红黑树移除】,所以将来再扩展
return 1; //先直接返回1表示成功
}
//原来的理解中,绑定ptr这个事,只在EPOLL_CTL_ADD的时候做一次即可,但是发现EPOLL_CTL_MOD似乎会破坏掉.data.ptr,因此不管是EPOLL_CTL_ADD,还是EPOLL_CTL_MOD,都给进去
//找了下内核源码SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event),感觉真的会覆盖掉:
//copy_from_user(&epds, event, sizeof(struct epoll_event))),感觉这个内核处理这个事情太粗暴了
ev.data.ptr = (void *)pConn;
if(epoll_ctl(m_epollhandle,eventtype,fd,&ev) == -1)
{
printf("CSocekt::ngx_epoll_oper_event()中epoll_ctl(%d,%ud,%ud,%d)失败.",fd,eventtype,flag,bcaction);
return -1;
}
return 1;
}
woker进程,死循环中不停调用,epoll_wait等待事件来临,通过events判断是读事件,还是写事件,然后分别调用不同的处理函数。
//开始获取发生的事件消息
//参数unsigned int timer:epoll_wait()阻塞的时长,单位是毫秒;
//返回值,1:正常返回 ,0:有问题返回,一般不管是正常还是问题返回,都应该保持进程继续运行
//本函数被ngx_process_events_and_timers()调用,而ngx_process_events_and_timers()是在子进程的死循环中被反复调用
int ngx_epoll_process_events(int timer)
{
//等待事件,事件会返回到m_events里,最多返回NGX_MAX_EVENTS个事件【因为我只提供了这些内存】;
//如果两次调用epoll_wait()的事件间隔比较长,则可能在epoll的双向链表中,积累了多个事件,所以调用epoll_wait,可能取到多个事件
//阻塞timer这么长时间除非:a)阻塞时间到达 b)阻塞期间收到事件【比如新用户连入】会立刻返回c)调用时有事件也会立刻返回d)如果来个信号,比如你用kill -1 pid测试
//如果timer为-1则一直阻塞,如果timer为0则立即返回,即便没有任何事件
//返回值:有错误发生返回-1,错误在errno中,比如你发个信号过来,就返回-1,错误信息是(4: Interrupted system call)
// 如果你等待的是一段时间,并且超时了,则返回0;
// 如果返回>0则表示成功捕获到这么多个事件【返回值里】
int events = epoll_wait(m_epollhandle,m_events,NGX_MAX_EVENTS,timer);
if(events == -1)
{
//有错误发生,发送某个信号给本进程就可以导致这个条件成立,而且错误码根据观察是4;
//#define EINTR 4,EINTR错误的产生:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
//例如:在socket服务器端,设置了信号捕获机制,有子进程,当在父进程阻塞于慢系统调用时由父进程捕获到了一个有效信号时,内核会致使accept返回一个EINTR错误(被中断的系统调用)。
if(errno == EINTR)
{
//信号所致,直接返回,一般认为这不是毛病,但还是打印下日志记录一下,因为一般也不会人为给worker进程发送消息
ngx_log_error_core(NGX_LOG_INFO,errno,"CSocekt::ngx_epoll_process_events()中epoll_wait()失败!");
return 1; //正常返回
}
else
{
//这被认为应该是有问题,记录日志
ngx_log_error_core(NGX_LOG_ALERT,errno,"CSocekt::ngx_epoll_process_events()中epoll_wait()失败!");
return 0; //非正常返回
}
}
if(events == 0) //超时,但没事件来
{
if(timer != -1)
{
//要求epoll_wait阻塞一定的时间而不是一直阻塞,这属于阻塞到时间了,则正常返回
return 1;
}
//无限等待【所以不存在超时】,但却没返回任何事件,这应该不正常有问题
ngx_log_error_core(NGX_LOG_ALERT,0,"CSocekt::ngx_epoll_process_events()中epoll_wait()没超时却没返回任何事件!");
return 0; //非正常返回
}
//会惊群,一个telnet上来,4个worker进程都会被惊动,都执行下边这个
//ngx_log_stderr(0,"惊群测试:events=%d,进程id=%d",events,ngx_pid);
//ngx_log_stderr(0,"----------------------------------------");
//走到这里,就是属于有事件收到了
lpngx_connection_t p_Conn;
//uintptr_t instance;
uint32_t revents;
for(int i = 0; i < events; ++i) //遍历本次epoll_wait返回的所有事件,注意events才是返回的实际事件数量
{
p_Conn = (lpngx_connection_t)(m_events[i].data.ptr); //ngx_epoll_add_event()给进去的,这里能取出来
/*
instance = (uintptr_t) c & 1; //将地址的最后一位取出来,用instance变量标识, 见ngx_epoll_add_event,该值是当时随着连接池中的连接一起给进来的
//取得的是你当时调用ngx_epoll_add_event()的时候,这个连接里边的instance变量的值;
p_Conn = (lpngx_connection_t) ((uintptr_t)p_Conn & (uintptr_t) ~1); //最后1位干掉,得到真正的c地址
//仔细分析一下官方nginx的这个判断
//过滤过期事件的;
if(c->fd == -1) //一个套接字,当关联一个 连接池中的连接【对象】时,这个套接字值是要给到c->fd的,
//那什么时候这个c->fd会变成-1呢?关闭连接时这个fd会被设置为-1,哪行代码设置的-1再研究,但应该不是ngx_free_connection()函数设置的-1
{
//比如我们用epoll_wait取得三个事件,处理第一个事件时,因为业务需要,我们把这个连接关闭,那我们应该会把c->fd设置为-1;
//第二个事件照常处理
//第三个事件,假如这第三个事件,也跟第一个事件对应的是同一个连接,那这个条件就会成立;那么这种事件,属于过期事件,不该处理
//这里可以增加个日志,也可以不增加日志
ngx_log_error_core(NGX_LOG_DEBUG,0,"CSocekt::ngx_epoll_process_events()中遇到了fd=-1的过期事件:%p.",c);
continue; //这种事件就不处理即可
}
//过滤过期事件的;
if(c->instance != instance)
{
//--------------------以下这些说法来自于资料--------------------------------------
//什么时候这个条件成立呢?【换种问法:instance标志为什么可以判断事件是否过期呢?】
//比如我们用epoll_wait取得三个事件,处理第一个事件时,因为业务需要,我们把这个连接关闭【麻烦就麻烦在这个连接被服务器关闭上了】,但是恰好第三个事件也跟这个连接有关;
//因为第一个事件就把socket连接关闭了,显然第三个事件我们是不应该处理的【因为这是个过期事件】,若处理肯定会导致错误;
//那我们上述把c->fd设置为-1,可以解决这个问题吗? 能解决一部分问题,但另外一部分不能解决,不能解决的问题描述如下【这么离奇的情况应该极少遇到】:
//a)处理第一个事件时,因为业务需要,我们把这个连接【假设套接字为50】关闭,同时设置c->fd = -1;并且调用ngx_free_connection将该连接归还给连接池;
//b)处理第二个事件,恰好第二个事件是建立新连接事件,调用ngx_get_connection从连接池中取出的连接非常可能就是刚刚释放的第一个事件对应的连接池中的连接;
//c)又因为a中套接字50被释放了,所以会被操作系统拿来复用,复用给了b)【一般这么快就被复用也是醉了】;
//d)当处理第三个事件时,第三个事件其实是已经过期的,应该不处理,那怎么判断这第三个事件是过期的呢? 【假设现在处理的是第三个事件,此时这个 连接池中的该连接 实际上已经被用作第二个事件对应的socket上了】;
//依靠instance标志位能够解决这个问题,当调用ngx_get_connection从连接池中获取一个新连接时,我们把instance标志位置反,所以这个条件如果不成立,说明这个连接已经被挪作他用了;
//--------------------我的个人思考--------------------------------------
//如果收到了若干个事件,其中连接关闭也搞了多次,导致这个instance标志位被取反2次,那么,造成的结果就是:还是有可能遇到某些过期事件没有被发现【这里也就没有被continue】,照旧被当做没过期事件处理了;
//如果是这样,那就只能被照旧处理了。可能会造成偶尔某个连接被误关闭?但是整体服务器程序运行应该是平稳,问题不大的,这种漏网而被当成没过期来处理的的过期事件应该是极少发生的
ngx_log_error_core(NGX_LOG_DEBUG,0,"CSocekt::ngx_epoll_process_events()中遇到了instance值改变的过期事件:%p.",c);
continue; //这种事件就不处理即可
}
//存在一种可能性,过期事件没被过滤完整【非常极端】,走下来的;
*/
//能走到这里,我们认为这些事件都没过期,就正常开始处理
revents = m_events[i].events;//取出事件类型
/*
if(revents & (EPOLLERR|EPOLLHUP)) //例如对方close掉套接字,这里会感应到【换句话说:如果发生了错误或者客户端断连】
{
//这加上读写标记,方便后续代码处理,至于怎么处理,后续再说,这里也是参照nginx官方代码引入的这段代码;
//官方说法:if the error events were returned, add EPOLLIN and EPOLLOUT,to handle the events at least in one active handler
//我认为官方也是经过反复思考才加上着东西的,先放这里放着吧;
revents |= EPOLLIN|EPOLLOUT; //EPOLLIN:表示对应的链接上有数据可以读出(TCP链接的远端主动关闭连接,也相当于可读事件,因为本服务器小处理发送来的FIN包)
//EPOLLOUT:表示对应的连接上可以写入数据发送【写准备好】
} */
if(revents & EPOLLIN) //如果是读事件
{
//ngx_log_stderr(errno,"数据来了来了来了 ~~~~~~~~~~~~~.");
//一个客户端新连入,这个会成立,
//已连接发送数据来,这个也成立;
//c->r_ready = 1; //标记可以读;【从连接池拿出一个连接时这个连接的所有成员都是0】
(this->* (p_Conn->rhandler) )(p_Conn); //注意括号的运用来正确设置优先级,防止编译出错;【如果是个新客户连入
//如果新连接进入,这里执行的应该是CSocekt::ngx_event_accept(c)】
//如果是已经连入,发送数据到这里,则这里执行的应该是 CSocekt::ngx_read_request_handler()
}
if(revents & EPOLLOUT) //如果是写事件【对方关闭连接也触发这个,再研究。。。。。。】,注意上边的 if(revents & (EPOLLERR|EPOLLHUP)) revents |= EPOLLIN|EPOLLOUT; 读写标记都给加上了
{
//ngx_log_stderr(errno,"22222222222222222222.");
if(revents & (EPOLLERR | EPOLLHUP | EPOLLRDHUP)) //客户端关闭,如果服务器端挂着一个写通知事件,则这里个条件是可能成立的
{
//EPOLLERR:对应的连接发生错误 8 = 1000
//EPOLLHUP:对应的连接被挂起 16 = 0001 0000
//EPOLLRDHUP:表示TCP连接的远端关闭或者半关闭连接 8192 = 0010 0000 0000 0000
//我想打印一下日志看一下是否会出现这种情况
//8221 = 0010 0000 0001 1101 :包括 EPOLLRDHUP ,EPOLLHUP, EPOLLERR
//ngx_log_stderr(errno,"CSocekt::ngx_epoll_process_events()中revents&EPOLLOUT成立并且revents & (EPOLLERR|EPOLLHUP|EPOLLRDHUP)成立,event=%ud。",revents);
//我们只有投递了 写事件,但对端断开时,程序流程才走到这里,投递了写事件意味着 iThrowsendCount标记肯定被+1了,这里我们减回
--p_Conn->iThrowsendCount;
}
else
{
(this->* (p_Conn->whandler) )(p_Conn); //如果有数据没有发送完毕,由系统驱动来发送,则这里执行的应该是 CSocekt::ngx_write_request_handler()
}
}
} //end for(int i = 0; i < events; ++i)
return 1;
}
前面我们将监听套接字的读事件添加到epoll红黑树中,当有读事件来临时,表示有连接成功完成三次握手,现在需要使用accept从已完成队列中取,完成连接,连接成功之后将套接字设置为非阻塞,并为套接字分配一个连接池中连接,然后将读写函数绑定,然后将套接字读事件添加到epoll中
//建立新连接专用函数,当新连接进入时,本函数会被ngx_epoll_process_events()所调用将
void ngx_event_accept(lpngx_connection_t oldc)
{
//因为listen套接字上用的不是ET【边缘触发】,而是LT【水平触发】,意味着客户端连入如果我要不处理,这个函数会被多次调用,所以,我这里这里可以不必多次accept(),可以只执行一次accept()
//这也可以避免本函数被卡太久,注意,本函数应该尽快返回,以免阻塞程序运行;
struct sockaddr mysockaddr; //远端服务器的socket地址
socklen_t socklen;
int err;
int level;
int s;
static int use_accept4 = 1; //我们先认为能够使用accept4()函数
lpngx_connection_t newc; //代表连接池中的一个连接【注意这是指针】
//ngx_log_stderr(0,"这是几个\n"); 这里会惊群,也就是说,epoll技术本身有惊群的问题
socklen = sizeof(mysockaddr);
do //用do,跳到while后边去方便
{
if(use_accept4)
{
//以为listen套接字是非阻塞的,所以即便已完成连接队列为空,accept4()也不会卡在这里;
s = accept4(oldc->fd, &mysockaddr, &socklen, SOCK_NONBLOCK); //从内核获取一个用户端连接,最后一个参数SOCK_NONBLOCK表示返回一个非阻塞的socket,节省一次ioctl【设置为非阻塞】调用
}
else
{
//以为listen套接字是非阻塞的,所以即便已完成连接队列为空,accept()也不会卡在这里;
s = accept(oldc->fd, &mysockaddr, &socklen);
}
//惊群,有时候不一定完全惊动所有4个worker进程,可能只惊动其中2个等等,其中一个成功其余的accept4()都会返回-1;错误 (11: Resource temporarily unavailable【资源暂时不可用】)
//所以参考资料:https://blog.csdn.net/russell_tao/article/details/7204260
//其实,在linux2.6内核上,accept系统调用已经不存在惊群了(至少我在2.6.18内核版本上已经不存在)。大家可以写个简单的程序试下,在父进程中bind,listen,然后fork出子进程,
//所有的子进程都accept这个监听句柄。这样,当新连接过来时,大家会发现,仅有一个子进程返回新建的连接,其他子进程继续休眠在accept调用上,没有被唤醒。
//ngx_log_stderr(0,"测试惊群问题,看惊动几个worker进程%d\n",s); 【我的结论是:accept4可以认为基本解决惊群问题,但似乎并没有完全解决,有时候还会惊动其他的worker进程】
/*
if(s == -1)
{
ngx_log_stderr(0,"惊群测试:ngx_event_accept()中accept失败,进程id=%d",ngx_pid);
}
else
{
ngx_log_stderr(0,"惊群测试:ngx_event_accept()中accept成功,进程id=%d",ngx_pid);
} */
if(s == -1)
{
err = errno;
//对accept、send和recv而言,事件未发生时errno通常被设置成EAGAIN(意为“再来一次”)或者EWOULDBLOCK(意为“期待阻塞”)
if(err == EAGAIN) //accept()没准备好,这个EAGAIN错误EWOULDBLOCK是一样的
{
//除非你用一个循环不断的accept()取走所有的连接,不然一般不会有这个错误【我们这里只取一个连接,也就是accept()一次】
return ;
}
level = NGX_LOG_ALERT;
if (err == ECONNABORTED) //ECONNRESET错误则发生在对方意外关闭套接字后【您的主机中的软件放弃了一个已建立的连接--由于超时或者其它失败而中止接连(用户插拔网线就可能有这个错误出现)】
{
//该错误被描述为“software caused connection abort”,即“软件引起的连接中止”。原因在于当服务和客户进程在完成用于 TCP 连接的“三次握手”后,
//客户 TCP 却发送了一个 RST (复位)分节,在服务进程看来,就在该连接已由 TCP 排队,等着服务进程调用 accept 的时候 RST 却到达了。
//POSIX 规定此时的 errno 值必须 ECONNABORTED。源自 Berkeley 的实现完全在内核中处理中止的连接,服务进程将永远不知道该中止的发生。
//服务器进程一般可以忽略该错误,直接再次调用accept。
level = NGX_LOG_ERR;
}
else if (err == EMFILE || err == ENFILE) //EMFILE:进程的fd已用尽【已达到系统所允许单一进程所能打开的文件/套接字总数】。可参考:https://blog.csdn.net/sdn_prc/article/details/28661661 以及 https://bbs.csdn.net/topics/390592927
//ulimit -n ,看看文件描述符限制,如果是1024的话,需要改大; 打开的文件句柄数过多 ,把系统的fd软限制和硬限制都抬高.
//ENFILE这个errno的存在,表明一定存在system-wide的resource limits,而不仅仅有process-specific的resource limits。按照常识,process-specific的resource limits,一定受限于system-wide的resource limits。
{
level = NGX_LOG_CRIT;
}
//ngx_log_error_core(level,errno,"CSocekt::ngx_event_accept()中accept4()失败!");
if(use_accept4 && err == ENOSYS) //accept4()函数没实现,坑爹?
{
use_accept4 = 0; //标记不使用accept4()函数,改用accept()函数
continue; //回去重新用accept()函数搞
}
if (err == ECONNABORTED) //对方关闭套接字
{
//这个错误因为可以忽略,所以不用干啥
//do nothing
}
if (err == EMFILE || err == ENFILE)
{
//do nothing,这个官方做法是先把读事件从listen socket上移除,然后再弄个定时器,定时器到了则继续执行该函数,但是定时器到了有个标记,会把读事件增加到listen socket上去;
//我这里目前先不处理吧【因为上边已经写这个日志了】;
}
return;
} //end if(s == -1)
//走到这里的,表示accept4()/accept()成功了
if(m_onlineUserCount >= m_worker_connections) //用户连接数过多,要关闭该用户socket,因为现在也没分配连接,所以直接关闭即可
{
//ngx_log_stderr(0,"超出系统允许的最大连入用户数(最大允许连入数%d),关闭连入请求(%d)。",m_worker_connections,s);
close(s);
return ;
}
//如果某些恶意用户连上来发了1条数据就断,不断连接,会导致频繁调用ngx_get_connection()使用我们短时间内产生大量连接,危及本服务器安全
if(m_connectionList.size() > (m_worker_connections * 5))
{
//比如你允许同时最大2048个连接,但连接池却有了 2048*5这么大的容量,这肯定是表示短时间内 产生大量连接/断开,因为我们的延迟回收机制,这里连接还在垃圾池里没有被回收
if(m_freeconnectionList.size() < m_worker_connections)
{
//整个连接池这么大了,而空闲连接却这么少了,所以我认为是 短时间内 产生大量连接,发一个包后就断开,我们不可能让这种情况持续发生,所以必须断开新入用户的连接
//一直到m_freeconnectionList变得足够大【连接池中连接被回收的足够多】
close(s);
return ;
}
}
//ngx_log_stderr(errno,"accept4成功s=%d",s); //s这里就是 一个句柄了
newc = ngx_get_connection(s); //这是针对新连入用户的连接,和监听套接字 所对应的连接是两个不同的东西,不要搞混
if(newc == NULL)
{
//连接池中连接不够用,那么就得把这个socekt直接关闭并返回了,因为在ngx_get_connection()中已经写日志了,所以这里不需要写日志了
if(close(s) == -1)
{
ngx_log_error_core(NGX_LOG_ALERT,errno,"CSocekt::ngx_event_accept()中close(%d)失败!",s);
}
return;
}
//...........将来这里会判断是否连接超过最大允许连接数,现在,这里可以不处理
//成功的拿到了连接池中的一个连接
memcpy(&newc->s_sockaddr,&mysockaddr,socklen); //拷贝客户端地址到连接对象【要转成字符串ip地址参考函数ngx_sock_ntop()】
//{
// //测试将收到的地址弄成字符串,格式形如"192.168.1.126:40904"或者"192.168.1.126"
// u_char ipaddr[100]; memset(ipaddr,0,sizeof(ipaddr));
// ngx_sock_ntop(&newc->s_sockaddr,1,ipaddr,sizeof(ipaddr)-10); //宽度给小点
// ngx_log_stderr(0,"ip信息为%s\n",ipaddr);
//}
if(!use_accept4)
{
//如果不是用accept4()取得的socket,那么就要设置为非阻塞【因为用accept4()的已经被accept4()设置为非阻塞了】
if(setnonblocking(s) == false)
{
//设置非阻塞居然失败
ngx_close_connection(newc); //关闭socket,这种可以立即回收这个连接,无需延迟,因为其上还没有数据收发,谈不到业务逻辑因此无需延迟;
return; //直接返回
}
}
newc->listening = oldc->listening; //连接对象 和监听对象关联,方便通过连接对象找监听对象【关联到监听端口】
//newc->w_ready = 1; //标记可以写,新连接写事件肯定是ready的;【从连接池拿出一个连接时这个连接的所有成员都是0】
newc->rhandler = &CSocekt::ngx_read_request_handler; //设置数据来时的读处理函数,其实官方nginx中是ngx_http_wait_request_handler()
newc->whandler = &CSocekt::ngx_write_request_handler; //设置数据发送时的写处理函数。
//客户端应该主动发送第一次的数据,这里将读事件加入epoll监控,这样当客户端发送数据来时,会触发ngx_wait_request_handler()被ngx_epoll_process_events()调用
if(ngx_epoll_oper_event(
s, //socekt句柄
EPOLL_CTL_ADD, //事件类型,这里是增加
EPOLLIN|EPOLLRDHUP, //标志,这里代表要增加的标志,EPOLLIN:可读,EPOLLRDHUP:TCP连接的远端关闭或者半关闭 ,如果边缘触发模式可以增加 EPOLLET
0, //对于事件类型为增加的,不需要这个参数
newc //连接池中的连接
) == -1)
{
//增加事件失败,失败日志在ngx_epoll_add_event中写过了,因此这里不多写啥;
ngx_close_connection(newc);//关闭socket,这种可以立即回收这个连接,无需延迟,因为其上还没有数据收发,谈不到业务逻辑因此无需延迟;
return; //直接返回
}
/*
else
{
//打印下发送缓冲区大小
int n;
socklen_t len;
len = sizeof(int);
getsockopt(s,SOL_SOCKET,SO_SNDBUF, &n, &len);
ngx_log_stderr(0,"发送缓冲区的大小为%d!",n); //87040
n = 0;
getsockopt(s,SOL_SOCKET,SO_RCVBUF, &n, &len);
ngx_log_stderr(0,"接收缓冲区的大小为%d!",n); //374400
int sendbuf = 2048;
if (setsockopt(s, SOL_SOCKET, SO_SNDBUF,(const void *) &sendbuf,n) == 0)
{
ngx_log_stderr(0,"发送缓冲区大小成功设置为%d!",sendbuf);
}
getsockopt(s,SOL_SOCKET,SO_SNDBUF, &n, &len);
ngx_log_stderr(0,"发送缓冲区的大小为%d!",n); //87040
}
*/
if(m_ifkickTimeCount == 1)
{
AddToTimerQueue(newc);
}
++m_onlineUserCount; //连入用户数量+1
break; //一般就是循环一次就跳出去
} while (1);
return;
}
刚才我们通过accept将完成三次握手的连接,添加到红黑树中,当epoll_wait发现有读事件来临时,执行对应的读数据函数。
我们将一个数据包接收分为4中状态
1、初始化状态,准备接收包头
2、包头接收中
3、包头接收玩,准备接收包体
4、包体接收中
当有读事件来临时,使用read函数接收包头长度,通过返回的n,判断是否成功接收完包头,要是没有接收完,将指针和还需要接收的字节数计算出来,等待下次epoll有读事件来临我们继续接收,如果接收完,将包的状态改为,准备接受包体,因为接收完包头,可以通过包头存储的包大小内存new出来,将包头拷贝到new出来的内存中,并且将需要接收的包体大小设置进去,将刚才new出来的放入到连接池中用来接收包体,接下来继续接收包体,接收完包体,将包体放入消息队列中,并且将包状态恢复成初始状态,准备接收包头,指针也是指向包头数组,需要接收长度也是包头的长度。
成功接收完整个包之后,通过调用线程池里面的线程将,包添加到消息队列中,当消息队列中有消息时,线程池中的线程就会去处理,通过具体的业务逻辑处理,处理完之后将消息内存释放,然后线程返回。
//来数据时候的处理,当连接上有数据来的时候,本函数会被ngx_epoll_process_events()所调用 ,官方的类似函数为ngx_http_wait_request_handler();
void ngx_read_request_handler(lpngx_connection_t pConn)
{
bool isflood = false; //是否flood攻击;
//收包,注意我们用的第二个和第三个参数,我们用的始终是这两个参数,因此我们必须保证 c->precvbuf指向正确的收包位置,保证c->irecvlen指向正确的收包宽度
ssize_t reco = recvproc(pConn,pConn->precvbuf,pConn->irecvlen);
if(reco <= 0)
{
return;//该处理的上边这个recvproc()函数处理过了,这里<=0是直接return
}
//走到这里,说明成功收到了一些字节(>0),就要开始判断收到了多少数据了
if(pConn->curStat == _PKG_HD_INIT) //连接建立起来时肯定是这个状态,因为在ngx_get_connection()中已经把curStat成员赋值成_PKG_HD_INIT了
{
if(reco == m_iLenPkgHeader)//正好收到完整包头,这里拆解包头
{
ngx_wait_request_handler_proc_p1(pConn,isflood); //那就调用专门针对包头处理完整的函数去处理把。
}
else
{
//收到的包头不完整--我们不能预料每个包的长度,也不能预料各种拆包/粘包情况,所以收到不完整包头【也算是缺包】是很可能的;
pConn->curStat = _PKG_HD_RECVING; //接收包头中,包头不完整,继续接收包头中
pConn->precvbuf = pConn->precvbuf + reco; //注意收后续包的内存往后走
pConn->irecvlen = pConn->irecvlen - reco; //要收的内容当然要减少,以确保只收到完整的包头先
} //end if(reco == m_iLenPkgHeader)
}
else if(pConn->curStat == _PKG_HD_RECVING) //接收包头中,包头不完整,继续接收中,这个条件才会成立
{
if(pConn->irecvlen == reco) //要求收到的宽度和我实际收到的宽度相等
{
//包头收完整了
ngx_wait_request_handler_proc_p1(pConn,isflood); //那就调用专门针对包头处理完整的函数去处理把。
}
else
{
//包头还是没收完整,继续收包头
//pConn->curStat = _PKG_HD_RECVING; //没必要
pConn->precvbuf = pConn->precvbuf + reco; //注意收后续包的内存往后走
pConn->irecvlen = pConn->irecvlen - reco; //要收的内容当然要减少,以确保只收到完整的包头先
}
}
else if(pConn->curStat == _PKG_BD_INIT)
{
//包头刚好收完,准备接收包体
if(reco == pConn->irecvlen)
{
//收到的宽度等于要收的宽度,包体也收完整了
if(m_floodAkEnable == 1)
{
//Flood攻击检测是否开启
isflood = TestFlood(pConn);
}
ngx_wait_request_handler_proc_plast(pConn,isflood);
}
else
{
//收到的宽度小于要收的宽度
pConn->curStat = _PKG_BD_RECVING;
pConn->precvbuf = pConn->precvbuf + reco;
pConn->irecvlen = pConn->irecvlen - reco;
}
}
else if(pConn->curStat == _PKG_BD_RECVING)
{
//接收包体中,包体不完整,继续接收中
if(pConn->irecvlen == reco)
{
//包体收完整了
if(m_floodAkEnable == 1)
{
//Flood攻击检测是否开启
isflood = TestFlood(pConn);
}
ngx_wait_request_handler_proc_plast(pConn,isflood);
}
else
{
//包体没收完整,继续收
pConn->precvbuf = pConn->precvbuf + reco;
pConn->irecvlen = pConn->irecvlen - reco;
}
} //end if(c->curStat == _PKG_HD_INIT)
if(isflood == true)
{
//客户端flood服务器,则直接把客户端踢掉
//ngx_log_stderr(errno,"发现客户端flood,干掉该客户端!");
zdClosesocketProc(pConn);
}
return;
}
//接收数据专用函数--引入这个函数是为了方便,如果断线,错误之类的,这里直接 释放连接池中连接,然后直接关闭socket,以免在其他函数中还要重复的干这些事
//参数c:连接池中相关连接
//参数buff:接收数据的缓冲区
//参数buflen:要接收的数据大小
//返回值:返回-1,则是有问题发生并且在这里把问题处理完毕了,调用本函数的调用者一般是可以直接return
// 返回>0,则是表示实际收到的字节数
ssize_t recvproc(lpngx_connection_t pConn,char *buff,ssize_t buflen) //ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t就是无符号型的ssize_t
{
ssize_t n;
n = recv(pConn->fd, buff, buflen, 0); //recv()系统函数, 最后一个参数flag,一般为0;
if(n == 0)
{
//客户端关闭【应该是正常完成了4次挥手】,我这边就直接回收连接,关闭socket即可
//ngx_log_stderr(0,"连接被客户端正常关闭[4路挥手关闭]!");
//ngx_close_connection(pConn);
//inRecyConnectQueue(pConn);
zdClosesocketProc(pConn);
return -1;
}
//客户端没断,走这里
if(n < 0) //这被认为有错误发生
{
//EAGAIN和EWOULDBLOCK[【这个应该常用在hp上】应该是一样的值,表示没收到数据,一般来讲,在ET模式下会出现这个错误,因为ET模式下是不停的recv肯定有一个时刻收到这个errno,但LT模式下一般是来事件才收,所以不该出现这个返回值
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
//我认为LT模式不该出现这个errno,而且这个其实也不是错误,所以不当做错误处理
ngx_log_stderr(errno,"CSocekt::recvproc()中errno == EAGAIN || errno == EWOULDBLOCK成立,出乎我意料!");//epoll为LT模式不应该出现这个返回值,所以直接打印出来瞧瞧
return -1; //不当做错误处理,只是简单返回
}
//EINTR错误的产生:当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。
//例如:在socket服务器端,设置了信号捕获机制,有子进程,当在父进程阻塞于慢系统调用时由父进程捕获到了一个有效信号时,内核会致使accept返回一个EINTR错误(被中断的系统调用)。
if(errno == EINTR) //这个不算错误,是我参考官方nginx,官方nginx这个就不算错误;
{
//我认为LT模式不该出现这个errno,而且这个其实也不是错误,所以不当做错误处理
ngx_log_stderr(errno,"CSocekt::recvproc()中errno == EINTR成立,出乎我意料!");//epoll为LT模式不应该出现这个返回值,所以直接打印出来瞧瞧
return -1; //不当做错误处理,只是简单返回
}
//所有从这里走下来的错误,都认为异常:意味着我们要关闭客户端套接字要回收连接池中连接;
//errno参考:http://dhfapiran1.360drm.com
if(errno == ECONNRESET) //#define ECONNRESET 104 /* Connection reset by peer */
{
//如果客户端没有正常关闭socket连接,却关闭了整个运行程序【真是够粗暴无理的,应该是直接给服务器发送rst包而不是4次挥手包完成连接断开】,那么会产生这个错误
//10054(WSAECONNRESET)--远程程序正在连接的时候关闭会产生这个错误--远程主机强迫关闭了一个现有的连接
//算常规错误吧【普通信息型】,日志都不用打印,没啥意思,太普通的错误
//do nothing
//....一些大家遇到的很普通的错误信息,也可以往这里增加各种,代码要慢慢完善,一步到位,不可能,很多服务器程序经过很多年的完善才比较圆满;
}
else
{
//能走到这里的,都表示错误,我打印一下日志,希望知道一下是啥错误,我准备打印到屏幕上
if(errno == EBADF) // #define EBADF 9 /* Bad file descriptor */
{
//因为多线程,偶尔会干掉socket,所以不排除产生这个错误的可能性
}
else
{
ngx_log_stderr(errno,"CSocekt::recvproc()中发生错误,我打印出来看看是啥错误!"); //正式运营时可以考虑这些日志打印去掉
}
}
//ngx_log_stderr(0,"连接被客户端 非 正常关闭!");
//这种真正的错误就要,直接关闭套接字,释放连接池中连接了
//ngx_close_connection(pConn);
//inRecyConnectQueue(pConn);
zdClosesocketProc(pConn);
return -1;
}
//能走到这里的,就认为收到了有效数据
return n; //返回收到的字节数
}
//包头收完整后的处理,我们称为包处理阶段1【p1】:写成函数,方便复用
//注意参数isflood是个引用
void ngx_wait_request_handler_proc_p1(lpngx_connection_t pConn,bool &isflood)
{
CMemory *p_memory = CMemory::GetInstance();
LPCOMM_PKG_HEADER pPkgHeader;
pPkgHeader = (LPCOMM_PKG_HEADER)pConn->dataHeadInfo; //正好收到包头时,包头信息肯定是在dataHeadInfo里;
unsigned short e_pkgLen;
e_pkgLen = ntohs(pPkgHeader->pkgLen); //注意这里网络序转本机序,所有传输到网络上的2字节数据,都要用htons()转成网络序,所有从网络上收到的2字节数据,都要用ntohs()转成本机序
//ntohs/htons的目的就是保证不同操作系统数据之间收发的正确性,【不管客户端/服务器是什么操作系统,发送的数字是多少,收到的就是多少】
//不明白的同学,直接百度搜索"网络字节序" "主机字节序" "c++ 大端" "c++ 小端"
//恶意包或者错误包的判断
if(e_pkgLen < m_iLenPkgHeader)
{
//伪造包/或者包错误,否则整个包长怎么可能比包头还小?(整个包长是包头+包体,就算包体为0字节,那么至少e_pkgLen == m_iLenPkgHeader)
//报文总长度 < 包头长度,认定非法用户,废包
//状态和接收位置都复原,这些值都有必要,因为有可能在其他状态比如_PKG_HD_RECVING状态调用这个函数;
pConn->curStat = _PKG_HD_INIT;
pConn->precvbuf = pConn->dataHeadInfo;
pConn->irecvlen = m_iLenPkgHeader;
}
else if(e_pkgLen > (_PKG_MAX_LENGTH-1000)) //客户端发来包居然说包长度 > 29000?肯定是恶意包
{
//恶意包,太大,认定非法用户,废包【包头中说这个包总长度这么大,这不行】
//状态和接收位置都复原,这些值都有必要,因为有可能在其他状态比如_PKG_HD_RECVING状态调用这个函数;
pConn->curStat = _PKG_HD_INIT;
pConn->precvbuf = pConn->dataHeadInfo;
pConn->irecvlen = m_iLenPkgHeader;
}
else
{
//合法的包头,继续处理
//我现在要分配内存开始收包体,因为包体长度并不是固定的,所以内存肯定要new出来;
char *pTmpBuffer = (char *)p_memory->AllocMemory(m_iLenMsgHeader + e_pkgLen,false); //分配内存【长度是 消息头长度 + 包头长度 + 包体长度】,最后参数先给false,表示内存不需要memset;
pConn->precvMemPointer = pTmpBuffer; //内存开始指针
//a)先填写消息头内容
LPSTRUC_MSG_HEADER ptmpMsgHeader = (LPSTRUC_MSG_HEADER)pTmpBuffer;
ptmpMsgHeader->pConn = pConn;
ptmpMsgHeader->iCurrsequence = pConn->iCurrsequence; //收到包时的连接池中连接序号记录到消息头里来,以备将来用;
//b)再填写包头内容
pTmpBuffer += m_iLenMsgHeader; //往后跳,跳过消息头,指向包头
memcpy(pTmpBuffer,pPkgHeader,m_iLenPkgHeader); //直接把收到的包头拷贝进来
if(e_pkgLen == m_iLenPkgHeader)
{
//该报文只有包头无包体【我们允许一个包只有包头,没有包体】
//这相当于收完整了,则直接入消息队列待后续业务逻辑线程去处理吧
if(m_floodAkEnable == 1)
{
//Flood攻击检测是否开启
isflood = TestFlood(pConn);
}
ngx_wait_request_handler_proc_plast(pConn,isflood);
}
else
{
//开始收包体,注意我的写法
pConn->curStat = _PKG_BD_INIT; //当前状态发生改变,包头刚好收完,准备接收包体
pConn->precvbuf = pTmpBuffer + m_iLenPkgHeader; //pTmpBuffer指向包头,这里 + m_iLenPkgHeader后指向包体 weizhi
pConn->irecvlen = e_pkgLen - m_iLenPkgHeader; //e_pkgLen是整个包【包头+包体】大小,-m_iLenPkgHeader【包头】 = 包体
}
} //end if(e_pkgLen < m_iLenPkgHeader)
return;
}
//线程入口函数,当用pthread_create()创建线程后,这个ThreadFunc()函数都会被立即执行;
void* CThreadPool::ThreadFunc(void* threadData)
{
//这个是静态成员函数,是不存在this指针的;
ThreadItem *pThread = static_cast<ThreadItem*>(threadData);
CThreadPool *pThreadPoolObj = pThread->_pThis;
CMemory *p_memory = CMemory::GetInstance();
int err;
pthread_t tid = pthread_self(); //获取线程自身id,以方便调试打印信息等
while(true)
{
//线程用pthread_mutex_lock()函数去锁定指定的mutex变量,若该mutex已经被另外一个线程锁定了,该调用将会阻塞线程直到mutex被解锁。
err = pthread_mutex_lock(&m_pthreadMutex);
if(err != 0) ngx_log_stderr(err,"CThreadPool::ThreadFunc()中pthread_mutex_lock()失败,返回的错误码为%d!",err);//有问题,要及时报告
//以下这行程序写法技巧十分重要,必须要用while这种写法,
//因为:pthread_cond_wait()是个值得注意的函数,调用一次pthread_cond_signal()可能会唤醒多个【惊群】【官方描述是 至少一个/pthread_cond_signal 在多处理器上可能同时唤醒多个线程】
//老师也在《c++入门到精通 c++ 98/11/14/17》里第六章第十三节谈过虚假唤醒,实际上是一个意思;
//老师也在《c++入门到精通 c++ 98/11/14/17》里第六章第八节谈过条件变量、wait()、notify_one()、notify_all(),其实跟这里的pthread_cond_wait、pthread_cond_signal、pthread_cond_broadcast非常类似
//pthread_cond_wait()函数,如果只有一条消息 唤醒了两个线程干活,那么其中有一个线程拿不到消息,那如果不用while写,就会出问题,所以被惊醒后必须再次用while拿消息,拿到才走下来;
//while( (jobbuf = g_socket.outMsgRecvQueue()) == NULL && m_shutdown == false)
while ( (pThreadPoolObj->m_MsgRecvQueue.size() == 0) && 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");
}
//能走下来的,必然是 拿到了真正的 消息队列中的数据 或者 m_shutdown == true
/*
jobbuf = g_socket.outMsgRecvQueue(); //从消息队列中取消息
if( jobbuf == NULL && m_shutdown == false)
{
//消息队列为空,并且不要求退出,则
//pthread_cond_wait()阻塞调用线程直到指定的条件有信号(signaled)。
//该函数应该在互斥量锁定时调用,当在等待时会自动解锁互斥量【这是重点】。在信号被发送,线程被激活后,互斥量会自动被锁定,当线程结束时,由程序员负责解锁互斥量。
//说白了,某个地方调用了pthread_cond_signal(&m_pthreadCond);,这个pthread_cond_wait就会走下来;
ngx_log_stderr(0,"--------------即将调用pthread_cond_wait,tid=%d--------------",tid);
if(pThread->ifrunning == false)
pThread->ifrunning = true; //标记为true了才允许调用StopAll():测试中发现如果Create()和StopAll()紧挨着调用,就会导致线程混乱,所以每个线程必须执行到这里,才认为是启动成功了;
err = pthread_cond_wait(&m_pthreadCond, &m_pthreadMutex);
if(err != 0) ngx_log_stderr(err,"CThreadPool::ThreadFunc()pthread_cond_wait()失败,返回的错误码为%d!",err);//有问题,要及时报告
ngx_log_stderr(0,"--------------调用pthread_cond_wait完毕,tid=%d--------------",tid);
}
*/
//if(!m_shutdown) //如果这个条件成立,表示肯定是拿到了真正消息队列中的数据,要去干活了,干活,则表示正在运行的线程数量要增加1;
// ++m_iRunningThreadNum; //因为这里是互斥的,所以这个+是OK的;
//走到这里时刻,互斥量肯定是锁着的。。。。。。
//先判断线程退出这个条件
if(m_shutdown)
{
pthread_mutex_unlock(&m_pthreadMutex); //解锁互斥量
break;
}
//走到这里,可以取得消息进行处理了【消息队列中必然有消息】,注意,目前还是互斥着呢
char *jobbuf = pThreadPoolObj->m_MsgRecvQueue.front(); //返回第一个元素但不检查元素存在与否
pThreadPoolObj->m_MsgRecvQueue.pop_front(); //移除第一个元素但不返回
--pThreadPoolObj->m_iRecvMsgQueueCount; //收消息队列数字-1
//可以解锁互斥量了
err = pthread_mutex_unlock(&m_pthreadMutex);
if(err != 0) ngx_log_stderr(err,"CThreadPool::ThreadFunc()中pthread_mutex_unlock()失败,返回的错误码为%d!",err);//有问题,要及时报告
//能走到这里的,就是有消息可以处理,开始处理
++pThreadPoolObj->m_iRunningThreadNum; //原子+1【记录正在干活的线程数量增加1】,这比互斥量要快很多
g_socket.threadRecvProcFunc(jobbuf); //处理消息队列中来的消息
//ngx_log_stderr(0,"执行开始---begin,tid=%ui!",tid);
//sleep(5); //临时测试代码
//ngx_log_stderr(0,"执行结束---end,tid=%ui!",tid);
p_memory->FreeMemory(jobbuf); //释放消息内存
--pThreadPoolObj->m_iRunningThreadNum; //原子-1【记录正在干活的线程数量减少1】
} //end while(true)
//能走出来表示整个程序要结束啊,怎么判断所有线程都结束?
return (void*)0;
}
//处理收到的数据包,由线程池来调用本函数,本函数是一个单独的线程;
//pMsgBuf:消息头 + 包头 + 包体 :自解释;
void CLogicSocket::threadRecvProcFunc(char *pMsgBuf)
{
LPSTRUC_MSG_HEADER pMsgHeader = (LPSTRUC_MSG_HEADER)pMsgBuf; //消息头
LPCOMM_PKG_HEADER pPkgHeader = (LPCOMM_PKG_HEADER)(pMsgBuf+m_iLenMsgHeader); //包头
void *pPkgBody; //指向包体的指针
unsigned short pkglen = ntohs(pPkgHeader->pkgLen); //客户端指明的包宽度【包头+包体】
if(m_iLenPkgHeader == pkglen)
{
//没有包体,只有包头
if(pPkgHeader->crc32 != 0) //只有包头的crc值给0
{
return; //crc错,直接丢弃
}
pPkgBody = NULL;
}
else
{
//有包体,走到这里
pPkgHeader->crc32 = ntohl(pPkgHeader->crc32); //针对4字节的数据,网络序转主机序
pPkgBody = (void *)(pMsgBuf+m_iLenMsgHeader+m_iLenPkgHeader); //跳过消息头 以及 包头 ,指向包体
//ngx_log_stderr(0,"CLogicSocket::threadRecvProcFunc()中收到包的crc值为%d!",pPkgHeader->crc32);
//计算crc值判断包的完整性
int calccrc = CCRC32::GetInstance()->Get_CRC((unsigned char *)pPkgBody,pkglen-m_iLenPkgHeader); //计算纯包体的crc值
if(calccrc != pPkgHeader->crc32) //服务器端根据包体计算crc值,和客户端传递过来的包头中的crc32信息比较
{
ngx_log_stderr(0,"CLogicSocket::threadRecvProcFunc()中CRC错误[服务器:%d/客户端:%d],丢弃数据!",calccrc,pPkgHeader->crc32); //正式代码中可以干掉这个信息
return; //crc错,直接丢弃
}
else
{
//ngx_log_stderr(0,"CLogicSocket::threadRecvProcFunc()中CRC正确[服务器:%d/客户端:%d],不错!",calccrc,pPkgHeader->crc32);
}
}
//包crc校验OK才能走到这里
unsigned short imsgCode = ntohs(pPkgHeader->msgCode); //消息代码拿出来
lpngx_connection_t p_Conn = pMsgHeader->pConn; //消息头中藏着连接池中连接的指针
//我们要做一些判断
//(1)如果从收到客户端发送来的包,到服务器释放一个线程池中的线程处理该包的过程中,客户端断开了,那显然,这种收到的包我们就不必处理了;
if(p_Conn->iCurrsequence != pMsgHeader->iCurrsequence) //该连接池中连接以被其他tcp连接【其他socket】占用,这说明原来的 客户端和本服务器的连接断了,这种包直接丢弃不理
{
return; //丢弃不理这种包了【客户端断开了】
}
//(2)判断消息码是正确的,防止客户端恶意侵害我们服务器,发送一个不在我们服务器处理范围内的消息码
if(imsgCode >= AUTH_TOTAL_COMMANDS) //无符号数不可能<0
{
ngx_log_stderr(0,"CLogicSocket::threadRecvProcFunc()中imsgCode=%d消息码不对!",imsgCode); //这种有恶意倾向或者错误倾向的包,希望打印出来看看是谁干的
return; //丢弃不理这种包【恶意包或者错误包】
}
//能走到这里的,包没过期,不恶意,那好继续判断是否有相应的处理函数
//(3)有对应的消息处理函数吗
if(statusHandler[imsgCode] == NULL) //这种用imsgCode的方式可以使查找要执行的成员函数效率特别高
{
ngx_log_stderr(0,"CLogicSocket::threadRecvProcFunc()中imsgCode=%d消息码找不到对应的处理函数!",imsgCode); //这种有恶意倾向或者错误倾向的包,希望打印出来看看是谁干的
return; //没有相关的处理函数
}
//一切正确,可以放心大胆的处理了
//(4)调用消息码对应的成员函数来处理
(this->*statusHandler[imsgCode])(p_Conn,pMsgHeader,(char *)pPkgBody,pkglen-m_iLenPkgHeader);
return;
}
//心跳包检测时间到,该去检测心跳包是否超时的事宜,本函数是子类函数,实现具体的判断动作
void CLogicSocket::procPingTimeOutChecking(LPSTRUC_MSG_HEADER tmpmsg,time_t cur_time)
{
CMemory *p_memory = CMemory::GetInstance();
if(tmpmsg->iCurrsequence == tmpmsg->pConn->iCurrsequence) //此连接没断
{
lpngx_connection_t p_Conn = tmpmsg->pConn;
if(/*m_ifkickTimeCount == 1 && */m_ifTimeOutKick == 1) //能调用到本函数第一个条件肯定成立,所以第一个条件加不加无所谓,主要是第二个条件
{
//到时间直接踢出去的需求
zdClosesocketProc(p_Conn);
}
else if( (cur_time - p_Conn->lastPingTime ) > (m_iWaitTime*3+10) ) //超时踢的判断标准就是 每次检查的时间间隔*3,超过这个时间没发送心跳包,就踢【大家可以根据实际情况自由设定】
{
//踢出去【如果此时此刻该用户正好断线,则这个socket可能立即被后续上来的连接复用 如果真有人这么倒霉,赶上这个点了,那么可能错踢,错踢就错踢】
//ngx_log_stderr(0,"时间到不发心跳包,踢出去!"); //感觉OK
zdClosesocketProc(p_Conn);
}
p_memory->FreeMemory(tmpmsg);//内存要释放
}
else //此连接断了
{
p_memory->FreeMemory(tmpmsg);//内存要释放
}
return;
}
具体业务逻辑函数
//----------------------------------------------------------------------------------------------------------
//处理各种业务逻辑
bool CLogicSocket::_HandleRegister(lpngx_connection_t pConn,LPSTRUC_MSG_HEADER pMsgHeader,char *pPkgBody,unsigned short iBodyLength)
{
//ngx_log_stderr(0,"执行了CLogicSocket::_HandleRegister()!");
//(1)首先判断包体的合法性
if(pPkgBody == NULL) //具体看客户端服务器约定,如果约定这个命令[msgCode]必须带包体,那么如果不带包体,就认为是恶意包,直接不处理
{
return false;
}
int iRecvLen = sizeof(STRUCT_REGISTER);
if(iRecvLen != iBodyLength) //发送过来的结构大小不对,认为是恶意包,直接不处理
{
return false;
}
//(2)对于同一个用户,可能同时发送来多个请求过来,造成多个线程同时为该 用户服务,比如以网游为例,用户要在商店中买A物品,又买B物品,而用户的钱 只够买A或者B,不够同时买A和B呢?
//那如果用户发送购买命令过来买了一次A,又买了一次B,如果是两个线程来执行同一个用户的这两次不同的购买命令,很可能造成这个用户购买成功了 A,又购买成功了 B
//所以,为了稳妥起见,针对某个用户的命令,我们一般都要互斥,我们需要增加临界的变量于ngx_connection_s结构中
CLock lock(&pConn->logicPorcMutex); //凡是和本用户有关的访问都互斥
//(3)取得了整个发送过来的数据
LPSTRUCT_REGISTER p_RecvInfo = (LPSTRUCT_REGISTER)pPkgBody;
p_RecvInfo->iType = ntohl(p_RecvInfo->iType); //所有数值型,short,int,long,uint64_t,int64_t这种大家都不要忘记传输之前主机网络序,收到后网络转主机序
p_RecvInfo->username[sizeof(p_RecvInfo->username)-1]=0;//这非常关键,防止客户端发送过来畸形包,导致服务器直接使用这个数据出现错误。
p_RecvInfo->password[sizeof(p_RecvInfo->password)-1]=0;//这非常关键,防止客户端发送过来畸形包,导致服务器直接使用这个数据出现错误。
//(4)这里可能要考虑 根据业务逻辑,进一步判断收到的数据的合法性,
//当前该玩家的状态是否适合收到这个数据等等【比如如果用户没登陆,它就不适合购买物品等等】
//这里大家自己发挥,自己根据业务需要来扩充代码,老师就不带着大家扩充了。。。。。。。。。。。。
//。。。。。。。。
//(5)给客户端返回数据时,一般也是返回一个结构,这个结构内容具体由客户端/服务器协商,这里我们就以给客户端也返回同样的 STRUCT_REGISTER 结构来举例
//LPSTRUCT_REGISTER pFromPkgHeader = (LPSTRUCT_REGISTER)(((char *)pMsgHeader)+m_iLenMsgHeader); //指向收到的包的包头,其中数据后续可能要用到
LPCOMM_PKG_HEADER pPkgHeader;
CMemory *p_memory = CMemory::GetInstance();
CCRC32 *p_crc32 = CCRC32::GetInstance();
int iSendLen = sizeof(STRUCT_REGISTER);
//a)分配要发送出去的包的内存
//iSendLen = 65000; //unsigned最大也就是这个值
char *p_sendbuf = (char *)p_memory->AllocMemory(m_iLenMsgHeader+m_iLenPkgHeader+iSendLen,false);//准备发送的格式,这里是 消息头+包头+包体
//b)填充消息头
memcpy(p_sendbuf,pMsgHeader,m_iLenMsgHeader); //消息头直接拷贝到这里来
//c)填充包头
pPkgHeader = (LPCOMM_PKG_HEADER)(p_sendbuf+m_iLenMsgHeader); //指向包头
pPkgHeader->msgCode = _CMD_REGISTER; //消息代码,可以统一在ngx_logiccomm.h中定义
pPkgHeader->msgCode = htons(pPkgHeader->msgCode); //htons主机序转网络序
pPkgHeader->pkgLen = htons(m_iLenPkgHeader + iSendLen); //整个包的尺寸【包头+包体尺寸】
//d)填充包体
LPSTRUCT_REGISTER p_sendInfo = (LPSTRUCT_REGISTER)(p_sendbuf+m_iLenMsgHeader+m_iLenPkgHeader); //跳过消息头,跳过包头,就是包体了
//。。。。。这里根据需要,填充要发回给客户端的内容,int类型要使用htonl()转,short类型要使用htons()转;
//e)包体内容全部确定好后,计算包体的crc32值
pPkgHeader->crc32 = p_crc32->Get_CRC((unsigned char *)p_sendInfo,iSendLen);
pPkgHeader->crc32 = htonl(pPkgHeader->crc32);
//f)发送数据包
msgSend(p_sendbuf);
/*if(ngx_epoll_oper_event(
pConn->fd, //socekt句柄
EPOLL_CTL_MOD, //事件类型,这里是增加
EPOLLOUT, //标志,这里代表要增加的标志,EPOLLOUT:可写
0, //对于事件类型为增加的,EPOLL_CTL_MOD需要这个参数, 0:增加 1:去掉 2:完全覆盖
pConn //连接池中的连接
) == -1)
{
ngx_log_stderr(0,"1111111111111111111111111111111111111111111111111111111111111!");
} */
/*
sleep(100); //休息这么长时间
//如果连接回收了,则肯定是iCurrsequence不等了
if(pMsgHeader->iCurrsequence != pConn->iCurrsequence)
{
//应该是不等,因为这个插座已经被回收了
ngx_log_stderr(0,"插座不等,%L--%L",pMsgHeader->iCurrsequence,pConn->iCurrsequence);
}
else
{
ngx_log_stderr(0,"插座相等哦,%L--%L",pMsgHeader->iCurrsequence,pConn->iCurrsequence);
}
*/
//ngx_log_stderr(0,"执行了CLogicSocket::_HandleRegister()并返回结果!");
return true;
}
bool CLogicSocket::_HandleLogIn(lpngx_connection_t pConn,LPSTRUC_MSG_HEADER pMsgHeader,char *pPkgBody,unsigned short iBodyLength)
{
if(pPkgBody == NULL)
{
return false;
}
int iRecvLen = sizeof(STRUCT_LOGIN);
if(iRecvLen != iBodyLength)
{
return false;
}
CLock lock(&pConn->logicPorcMutex);
LPSTRUCT_LOGIN p_RecvInfo = (LPSTRUCT_LOGIN)pPkgBody;
p_RecvInfo->username[sizeof(p_RecvInfo->username)-1]=0;
p_RecvInfo->password[sizeof(p_RecvInfo->password)-1]=0;
LPCOMM_PKG_HEADER pPkgHeader;
CMemory *p_memory = CMemory::GetInstance();
CCRC32 *p_crc32 = CCRC32::GetInstance();
int iSendLen = sizeof(STRUCT_LOGIN);
char *p_sendbuf = (char *)p_memory->AllocMemory(m_iLenMsgHeader+m_iLenPkgHeader+iSendLen,false);
memcpy(p_sendbuf,pMsgHeader,m_iLenMsgHeader);
pPkgHeader = (LPCOMM_PKG_HEADER)(p_sendbuf+m_iLenMsgHeader);
pPkgHeader->msgCode = _CMD_LOGIN;
pPkgHeader->msgCode = htons(pPkgHeader->msgCode);
pPkgHeader->pkgLen = htons(m_iLenPkgHeader + iSendLen);
LPSTRUCT_LOGIN p_sendInfo = (LPSTRUCT_LOGIN)(p_sendbuf+m_iLenMsgHeader+m_iLenPkgHeader);
pPkgHeader->crc32 = p_crc32->Get_CRC((unsigned char *)p_sendInfo,iSendLen);
pPkgHeader->crc32 = htonl(pPkgHeader->crc32);
//ngx_log_stderr(0,"成功收到了登录并返回结果!");
msgSend(p_sendbuf);
return true;
}
1、什么叫socket可写?
每一个tcp连接对应都有一个接收,和发送缓存区,当发送缓存区没有满是就是可写,要是满了write和send类函数就会返回EAGAIN错误。
2、当时epoll的LT模式下,只有事件来临,要是没有处理就会一直通知,ET,反之就是有事件来临时只会通知一次,不管处没处理都不通知了,如果我们将连接写事件添加到epoll中,只要缓存区没有满,他就会一直通知,我们应该怎么解决这个问题?
解决方法1:
需要发送数据时,将连接写事件添加到epoll中,等待数据发送完毕之后将连接移除,缺点数据量要是不大,就会一直添加移除,会影响效率。
解决方式2:
需要发送数据时,先通过read,send类函数发送数据,要是能成功发送完就不用添加到epoll中,要是缓存区满了,就会出现EAGAIN错误,我们此时将连接添加写事件添加到epoll中,等待epoll通知,当数据发送完毕之后移除事件。
这里我们使用后者处理数据发送
这里是通过一个独立的线程去完成这个任务,我们将需要发送的消息,将消息以固定的格式放入到发送消息队列中,然后同信号量触动发送消息线程去完成消息发送。取出消息队列中消息,然后通过send发送消息,要是没有发送完,就将还需要发送多少字节数重新设置,将套接字添加到epoll中,等待通知来时继续发送消息。
//处理发送消息队列的线程
void* CSocekt::ServerSendQueueThread(void* threadData)
{
ThreadItem *pThread = static_cast<ThreadItem*>(threadData);
CSocekt *pSocketObj = pThread->_pThis;
int err;
std::list <char *>::iterator pos,pos2,posend;
char *pMsgBuf;
LPSTRUC_MSG_HEADER pMsgHeader;
LPCOMM_PKG_HEADER pPkgHeader;
lpngx_connection_t p_Conn;
unsigned short itmp;
ssize_t sendsize;
CMemory *p_memory = CMemory::GetInstance();
while(g_stopEvent == 0) //不退出
{
//如果信号量值>0,则 -1(减1) 并走下去,否则卡这里卡着【为了让信号量值+1,可以在其他线程调用sem_post达到,实际上在CSocekt::msgSend()调用sem_post就达到了让这里sem_wait走下去的目的】
//******如果被某个信号中断,sem_wait也可能过早的返回,错误为EINTR;
//整个程序退出之前,也要sem_post()一下,确保如果本线程卡在sem_wait(),也能走下去从而让本线程成功返回
if(sem_wait(&pSocketObj->m_semEventSendQueue) == -1)
{
//失败?及时报告,其他的也不好干啥
if(errno != EINTR) //这个我就不算个错误了【当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调用可能返回一个EINTR错误。】
ngx_log_stderr(errno,"CSocekt::ServerSendQueueThread()中sem_wait(&pSocketObj->m_semEventSendQueue)失败.");
}
//一般走到这里都表示需要处理数据收发了
if(g_stopEvent != 0) //要求整个进程退出
break;
if(pSocketObj->m_iSendMsgQueueCount > 0) //原子的
{
err = pthread_mutex_lock(&pSocketObj->m_sendMessageQueueMutex); //因为我们要操作发送消息对列m_MsgSendQueue,所以这里要临界
if(err != 0) ngx_log_stderr(err,"CSocekt::ServerSendQueueThread()中pthread_mutex_lock()失败,返回的错误码为%d!",err);
pos = pSocketObj->m_MsgSendQueue.begin();
posend = pSocketObj->m_MsgSendQueue.end();
while(pos != posend)
{
pMsgBuf = (*pos); //拿到的每个消息都是 消息头+包头+包体【但要注意,我们是不发送消息头给客户端的】
pMsgHeader = (LPSTRUC_MSG_HEADER)pMsgBuf; //指向消息头
pPkgHeader = (LPCOMM_PKG_HEADER)(pMsgBuf+pSocketObj->m_iLenMsgHeader); //指向包头
p_Conn = pMsgHeader->pConn;
//包过期,因为如果 这个连接被回收,比如在ngx_close_connection(),inRecyConnectQueue()中都会自增iCurrsequence
//而且这里有没必要针对 本连接 来用m_connectionMutex临界 ,只要下面条件成立,肯定是客户端连接已断,要发送的数据肯定不需要发送了
if(p_Conn->iCurrsequence != pMsgHeader->iCurrsequence)
{
//本包中保存的序列号与p_Conn【连接池中连接】中实际的序列号已经不同,丢弃此消息,小心处理该消息的删除
pos2=pos;
pos++;
pSocketObj->m_MsgSendQueue.erase(pos2);
--pSocketObj->m_iSendMsgQueueCount; //发送消息队列容量少1
p_memory->FreeMemory(pMsgBuf);
continue;
} //end if
if(p_Conn->iThrowsendCount > 0)
{
//靠系统驱动来发送消息,所以这里不能再发送
pos++;
continue;
}
--p_Conn->iSendCount; //发送队列中有的数据条目数-1;
//走到这里,可以发送消息,一些必须的信息记录,要发送的东西也要从发送队列里干掉
p_Conn->psendMemPointer = pMsgBuf; //发送后释放用的,因为这段内存是new出来的
pos2=pos;
pos++;
pSocketObj->m_MsgSendQueue.erase(pos2);
--pSocketObj->m_iSendMsgQueueCount; //发送消息队列容量少1
p_Conn->psendbuf = (char *)pPkgHeader; //要发送的数据的缓冲区指针,因为发送数据不一定全部都能发送出去,我们要记录数据发送到了哪里,需要知道下次数据从哪里开始发送
itmp = ntohs(pPkgHeader->pkgLen); //包头+包体 长度 ,打包时用了htons【本机序转网络序】,所以这里为了得到该数值,用了个ntohs【网络序转本机序】;
p_Conn->isendlen = itmp; //要发送多少数据,因为发送数据不一定全部都能发送出去,我们需要知道剩余有多少数据还没发送
//这里是重点,我们采用 epoll水平触发的策略,能走到这里的,都应该是还没有投递 写事件 到epoll中
//epoll水平触发发送数据的改进方案:
//开始不把socket写事件通知加入到epoll,当我需要写数据的时候,直接调用write/send发送数据;
//如果返回了EAGIN【发送缓冲区满了,需要等待可写事件才能继续往缓冲区里写数据】,此时,我再把写事件通知加入到epoll,
//此时,就变成了在epoll驱动下写数据,全部数据发送完毕后,再把写事件通知从epoll中干掉;
//优点:数据不多的时候,可以避免epoll的写事件的增加/删除,提高了程序的执行效率;
//(1)直接调用write或者send发送数据
//ngx_log_stderr(errno,"即将发送数据%ud。",p_Conn->isendlen);
sendsize = pSocketObj->sendproc(p_Conn,p_Conn->psendbuf,p_Conn->isendlen); //注意参数
if(sendsize > 0)
{
if(sendsize == p_Conn->isendlen) //成功发送出去了数据,一下就发送出去这很顺利
{
//成功发送的和要求发送的数据相等,说明全部发送成功了 发送缓冲区去了【数据全部发完】
p_memory->FreeMemory(p_Conn->psendMemPointer); //释放内存
p_Conn->psendMemPointer = NULL;
p_Conn->iThrowsendCount = 0; //这行其实可以没有,因此此时此刻这东西就是=0的
//ngx_log_stderr(0,"CSocekt::ServerSendQueueThread()中数据发送完毕,很好。"); //做个提示吧,商用时可以干掉
}
else //没有全部发送完毕(EAGAIN),数据只发出去了一部分,但肯定是因为 发送缓冲区满了,那么
{
//发送到了哪里,剩余多少,记录下来,方便下次sendproc()时使用
p_Conn->psendbuf = p_Conn->psendbuf + sendsize;
p_Conn->isendlen = p_Conn->isendlen - sendsize;
//因为发送缓冲区慢了,所以 现在我要依赖系统通知来发送数据了
++p_Conn->iThrowsendCount; //标记发送缓冲区满了,需要通过epoll事件来驱动消息的继续发送【原子+1,且不可写成p_Conn->iThrowsendCount = p_Conn->iThrowsendCount +1 ,这种写法不是原子+1】
//投递此事件后,我们将依靠epoll驱动调用ngx_write_request_handler()函数发送数据
if(pSocketObj->ngx_epoll_oper_event(
p_Conn->fd, //socket句柄
EPOLL_CTL_MOD, //事件类型,这里是增加【因为我们准备增加个写通知】
EPOLLOUT, //标志,这里代表要增加的标志,EPOLLOUT:可写【可写的时候通知我】
0, //对于事件类型为增加的,EPOLL_CTL_MOD需要这个参数, 0:增加 1:去掉 2:完全覆盖
p_Conn //连接池中的连接
) == -1)
{
//有这情况发生?这可比较麻烦,不过先do nothing
ngx_log_stderr(errno,"CSocekt::ServerSendQueueThread()ngx_epoll_oper_event()失败.");
}
//ngx_log_stderr(errno,"CSocekt::ServerSendQueueThread()中数据没发送完毕【发送缓冲区满】,整个要发送%d,实际发送了%d。",p_Conn->isendlen,sendsize);
} //end if(sendsize > 0)
continue; //继续处理其他消息
} //end if(sendsize > 0)
//能走到这里,应该是有点问题的
else if(sendsize == 0)
{
//发送0个字节,首先因为我发送的内容不是0个字节的;
//然后如果发送 缓冲区满则返回的应该是-1,而错误码应该是EAGAIN,所以我综合认为,这种情况我就把这个发送的包丢弃了【按对端关闭了socket处理】
//这个打印下日志,我还真想观察观察是否真有这种现象发生
//ngx_log_stderr(errno,"CSocekt::ServerSendQueueThread()中sendproc()居然返回0?"); //如果对方关闭连接出现send=0,那么这个日志可能会常出现,商用时就 应该干掉
//然后这个包干掉,不发送了
p_memory->FreeMemory(p_Conn->psendMemPointer); //释放内存
p_Conn->psendMemPointer = NULL;
p_Conn->iThrowsendCount = 0; //这行其实可以没有,因此此时此刻这东西就是=0的
continue;
}
//能走到这里,继续处理问题
else if(sendsize == -1)
{
//发送缓冲区已经满了【一个字节都没发出去,说明发送 缓冲区当前正好是满的】
++p_Conn->iThrowsendCount; //标记发送缓冲区满了,需要通过epoll事件来驱动消息的继续发送
//投递此事件后,我们将依靠epoll驱动调用ngx_write_request_handler()函数发送数据
if(pSocketObj->ngx_epoll_oper_event(
p_Conn->fd, //socket句柄
EPOLL_CTL_MOD, //事件类型,这里是增加【因为我们准备增加个写通知】
EPOLLOUT, //标志,这里代表要增加的标志,EPOLLOUT:可写【可写的时候通知我】
0, //对于事件类型为增加的,EPOLL_CTL_MOD需要这个参数, 0:增加 1:去掉 2:完全覆盖
p_Conn //连接池中的连接
) == -1)
{
//有这情况发生?这可比较麻烦,不过先do nothing
ngx_log_stderr(errno,"CSocekt::ServerSendQueueThread()中ngx_epoll_add_event()_2失败.");
}
continue;
}
else
{
//能走到这里的,应该就是返回值-2了,一般就认为对端断开了,等待recv()来做断开socket以及回收资源
p_memory->FreeMemory(p_Conn->psendMemPointer); //释放内存
p_Conn->psendMemPointer = NULL;
p_Conn->iThrowsendCount = 0; //这行其实可以没有,因此此时此刻这东西就是=0的
continue;
}
} //end while(pos != posend)
err = pthread_mutex_unlock(&pSocketObj->m_sendMessageQueueMutex);
if(err != 0) ngx_log_stderr(err,"CSocekt::ServerSendQueueThread()pthread_mutex_unlock()失败,返回的错误码为%d!",err);
} //if(pSocketObj->m_iSendMsgQueueCount > 0)
} //end while
return (void*)0;
}
//发送数据专用函数,返回本次发送的字节数
//返回 > 0,成功发送了一些字节
//=0,估计对方断了
//-1,errno == EAGAIN ,本方发送缓冲区满了
//-2,errno != EAGAIN != EWOULDBLOCK != EINTR ,一般我认为都是对端断开的错误
ssize_t CSocekt::sendproc(lpngx_connection_t c,char *buff,ssize_t size) //ssize_t是有符号整型,在32位机器上等同与int,在64位机器上等同与long int,size_t就是无符号型的ssize_t
{
//这里参考借鉴了官方nginx函数ngx_unix_send()的写法
ssize_t n;
for ( ;; )
{
n = send(c->fd, buff, size, 0); //send()系统函数, 最后一个参数flag,一般为0;
if(n > 0) //成功发送了一些数据
{
//发送成功一些数据,但发送了多少,我们这里不关心,也不需要再次send
//这里有两种情况
//(1) n == size也就是想发送多少都发送成功了,这表示完全发完毕了
//(2) n < size 没发送完毕,那肯定是发送缓冲区满了,所以也不必要重试发送,直接返回吧
return n; //返回本次发送的字节数
}
if(n == 0)
{
//send()返回0? 一般recv()返回0表示断开,send()返回0,我这里就直接返回0吧【让调用者处理】;我个人认为send()返回0,要么你发送的字节是0,要么对端可能断开。
//网上找资料:send=0表示超时,对方主动关闭了连接过程
//我们写代码要遵循一个原则,连接断开,我们并不在send动作里处理诸如关闭socket这种动作,集中到recv那里处理,否则send,recv都处理都处理连接断开关闭socket则会乱套
//连接断开epoll会通知并且 recvproc()里会处理,不在这里处理
return 0;
}
if(errno == EAGAIN) //这东西应该等于EWOULDBLOCK
{
//内核缓冲区满,这个不算错误
return -1; //表示发送缓冲区满了
}
if(errno == EINTR)
{
//这个应该也不算错误 ,收到某个信号导致send产生这个错误?
//参考官方的写法,打印个日志,其他啥也没干,那就是等下次for循环重新send试一次了
ngx_log_stderr(errno,"CSocekt::sendproc()中send()失败."); //打印个日志看看啥时候出这个错误
//其他不需要做什么,等下次for循环吧
}
else
{
//走到这里表示是其他错误码,都表示错误,错误我也不断开socket,我也依然等待recv()来统一处理断开,因为我是多线程,send()也处理断开,recv()也处理断开,很难处理好
return -2;
}
} //end for
}
//设置数据发送时的写处理函数,当数据可写时epoll通知我们,我们在 int CSocekt::ngx_epoll_process_events(int timer) 中调用此函数
//能走到这里,数据就是没法送完毕,要继续发送
void CSocekt::ngx_write_request_handler(lpngx_connection_t pConn)
{
CMemory *p_memory = CMemory::GetInstance();
//这些代码的书写可以参照 void* CSocekt::ServerSendQueueThread(void* threadData)
ssize_t sendsize = sendproc(pConn,pConn->psendbuf,pConn->isendlen);
if(sendsize > 0 && sendsize != pConn->isendlen)
{
//没有全部发送完毕,数据只发出去了一部分,那么发送到了哪里,剩余多少,继续记录,方便下次sendproc()时使用
pConn->psendbuf = pConn->psendbuf + sendsize;
pConn->isendlen = pConn->isendlen - sendsize;
return;
}
else if(sendsize == -1)
{
//这不太可能,可以发送数据时通知我发送数据,我发送时你却通知我发送缓冲区满?
ngx_log_stderr(errno,"CSocekt::ngx_write_request_handler()时if(sendsize == -1)成立,这很怪异。"); //打印个日志,别的先不干啥
return;
}
if(sendsize > 0 && sendsize == pConn->isendlen) //成功发送完毕,做个通知是可以的;
{
//如果是成功的发送完毕数据,则把写事件通知从epoll中干掉吧;其他情况,那就是断线了,等着系统内核把连接从红黑树中干掉即可;
if(ngx_epoll_oper_event(
pConn->fd, //socket句柄
EPOLL_CTL_MOD, //事件类型,这里是修改【因为我们准备减去写通知】
EPOLLOUT, //标志,这里代表要减去的标志,EPOLLOUT:可写【可写的时候通知我】
1, //对于事件类型为增加的,EPOLL_CTL_MOD需要这个参数, 0:增加 1:去掉 2:完全覆盖
pConn //连接池中的连接
) == -1)
{
//有这情况发生?这可比较麻烦,不过先do nothing
ngx_log_stderr(errno,"CSocekt::ngx_write_request_handler()中ngx_epoll_oper_event()失败。");
}
//ngx_log_stderr(0,"CSocekt::ngx_write_request_handler()中数据发送完毕,很好。"); //做个提示吧,商用时可以干掉
}
//能走下来的,要么数据发送完毕了,要么对端断开了,那么执行收尾工作吧;
/* 2019.4.2注释掉,调整下顺序,感觉这个顺序不太好
//数据发送完毕,或者把需要发送的数据干掉,都说明发送缓冲区可能有地方了,让发送线程往下走判断能否发送新数据
if(sem_post(&m_semEventSendQueue)==-1)
ngx_log_stderr(0,"CSocekt::ngx_write_request_handler()中sem_post(&m_semEventSendQueue)失败.");
p_memory->FreeMemory(pConn->psendMemPointer); //释放内存
pConn->psendMemPointer = NULL;
--pConn->iThrowsendCount; //建议放在最后执行
*/
//2019.4.2调整成新顺序
p_memory->FreeMemory(pConn->psendMemPointer); //释放内存
pConn->psendMemPointer = NULL;
--pConn->iThrowsendCount;//这个值恢复了,触发下面一行的信号量才有意义
if(sem_post(&m_semEventSendQueue)==-1)
ngx_log_stderr(0,"CSocekt::ngx_write_request_handler()中sem_post(&m_semEventSendQueue)失败.");
return;
}
心跳包就是一个普通的包没有包体的包,一般隔几十秒发送一次,客户端主动给服务器端发送,一般服务器也会客户端返回一个心跳包,因为网线突然断开,客户端和服务端都感知不到,所以利用心跳包检查客户端是否在线。
也是放在一个单独的线程中完成,有一个键值队列,专门存储当前时间和消息头,当有连接成功连入,我们就将这个客户端添加到时间队列中,线程一直在判断时间队列中有没有连接,有连接到来时,进行判断,要是连接的时间小于当前时间我们
//设置踢出时钟(向multimap表中增加内容),用户三次握手成功连入,然后我们开启了踢人开关【Sock_WaitTimeEnable = 1】,那么本函数被调用;
void CSocekt::AddToTimerQueue(lpngx_connection_t pConn)
{
CMemory *p_memory = CMemory::GetInstance();
time_t futtime = time(NULL);
futtime += m_iWaitTime; //20秒之后的时间
CLock lock(&m_timequeueMutex); //互斥,因为要操作m_timeQueuemap了
LPSTRUC_MSG_HEADER tmpMsgHeader = (LPSTRUC_MSG_HEADER)p_memory->AllocMemory(m_iLenMsgHeader,false);
tmpMsgHeader->pConn = pConn;
tmpMsgHeader->iCurrsequence = pConn->iCurrsequence;
m_timerQueuemap.insert(std::make_pair(futtime,tmpMsgHeader)); //按键 自动排序 小->大
m_cur_size_++; //计时队列尺寸+1
m_timer_value_ = GetEarliestTime(); //计时队列头部时间值保存到m_timer_value_里
return;
}
//从multimap中取得最早的时间返回去,调用者负责互斥,所以本函数不用互斥,调用者确保m_timeQueuemap中一定不为空
time_t CSocekt::GetEarliestTime()
{
std::multimap<time_t, LPSTRUC_MSG_HEADER>::iterator pos;
pos = m_timerQueuemap.begin();
return pos->first;
}
//从m_timeQueuemap移除最早的时间,并把最早这个时间所在的项的值所对应的指针 返回,调用者负责互斥,所以本函数不用互斥,
LPSTRUC_MSG_HEADER CSocekt::RemoveFirstTimer()
{
std::multimap<time_t, LPSTRUC_MSG_HEADER>::iterator pos;
LPSTRUC_MSG_HEADER p_tmp;
if(m_cur_size_ <= 0)
{
return NULL;
}
pos = m_timerQueuemap.begin(); //调用者负责互斥的,这里直接操作没问题的
p_tmp = pos->second;
m_timerQueuemap.erase(pos);
--m_cur_size_;
return p_tmp;
}
//根据给的当前时间,从m_timeQueuemap找到比这个时间更老(更早)的节点【1个】返回去,这些节点都是时间超过了,要处理的节点
//调用者负责互斥,所以本函数不用互斥
LPSTRUC_MSG_HEADER CSocekt::GetOverTimeTimer(time_t cur_time)
{
CMemory *p_memory = CMemory::GetInstance();
LPSTRUC_MSG_HEADER ptmp;
if (m_cur_size_ == 0 || m_timerQueuemap.empty())
return NULL; //队列为空
time_t earliesttime = GetEarliestTime(); //到multimap中去查询
if (earliesttime <= cur_time)
{
//这回确实是有到时间的了【超时的节点】
ptmp = RemoveFirstTimer(); //把这个超时的节点从 m_timerQueuemap 删掉,并把这个节点的第二项返回来;
if(/*m_ifkickTimeCount == 1 && */m_ifTimeOutKick != 1) //能调用到本函数第一个条件肯定成立,所以第一个条件加不加无所谓,主要是第二个条件
{
//如果不是要求超时就提出,则才做这里的事:
//因为下次超时的时间我们也依然要判断,所以还要把这个节点加回来
time_t newinqueutime = cur_time+(m_iWaitTime);
LPSTRUC_MSG_HEADER tmpMsgHeader = (LPSTRUC_MSG_HEADER)p_memory->AllocMemory(sizeof(STRUC_MSG_HEADER),false);
tmpMsgHeader->pConn = ptmp->pConn;
tmpMsgHeader->iCurrsequence = ptmp->iCurrsequence;
m_timerQueuemap.insert(std::make_pair(newinqueutime,tmpMsgHeader)); //自动排序 小->大
m_cur_size_++;
}
if(m_cur_size_ > 0) //这个判断条件必要,因为以后我们可能在这里扩充别的代码
{
m_timer_value_ = GetEarliestTime(); //计时队列头部时间值保存到m_timer_value_里
}
return ptmp;
}
return NULL;
}
//把指定用户tcp连接从timer表中抠出去
void CSocekt::DeleteFromTimerQueue(lpngx_connection_t pConn)
{
std::multimap<time_t, LPSTRUC_MSG_HEADER>::iterator pos,posend;
CMemory *p_memory = CMemory::GetInstance();
CLock lock(&m_timequeueMutex);
//因为实际情况可能比较复杂,将来可能还扩充代码等等,所以如下我们遍历整个队列找 一圈,而不是找到一次就拉倒,以免出现什么遗漏
lblMTQM:
pos = m_timerQueuemap.begin();
posend = m_timerQueuemap.end();
for(; pos != posend; ++pos)
{
if(pos->second->pConn == pConn)
{
p_memory->FreeMemory(pos->second); //释放内存
m_timerQueuemap.erase(pos);
--m_cur_size_; //减去一个元素,必然要把尺寸减少1个;
goto lblMTQM;
}
}
if(m_cur_size_ > 0)
{
m_timer_value_ = GetEarliestTime();
}
return;
}
//清理时间队列中所有内容
void CSocekt::clearAllFromTimerQueue()
{
std::multimap<time_t, LPSTRUC_MSG_HEADER>::iterator pos,posend;
CMemory *p_memory = CMemory::GetInstance();
pos = m_timerQueuemap.begin();
posend = m_timerQueuemap.end();
for(; pos != posend; ++pos)
{
p_memory->FreeMemory(pos->second);
--m_cur_size_;
}
m_timerQueuemap.clear();
}
//时间队列监视和处理线程,处理到期不发心跳包的用户踢出的线程
void* CSocekt::ServerTimerQueueMonitorThread(void* threadData)
{
ThreadItem *pThread = static_cast<ThreadItem*>(threadData);
CSocekt *pSocketObj = pThread->_pThis;
time_t absolute_time,cur_time;
int err;
while(g_stopEvent == 0) //不退出
{
//这里没互斥判断,所以只是个初级判断,目的至少是队列为空时避免系统损耗
if(pSocketObj->m_cur_size_ > 0)//队列不为空,有内容
{
//时间队列中最近发生事情的时间放到 absolute_time里;
absolute_time = pSocketObj->m_timer_value_; //这个可是省了个互斥,十分划算
cur_time = time(NULL);
if(absolute_time < cur_time)
{
//时间到了,可以处理了
std::list<LPSTRUC_MSG_HEADER> m_lsIdleList; //保存要处理的内容
LPSTRUC_MSG_HEADER result;
err = pthread_mutex_lock(&pSocketObj->m_timequeueMutex);
if(err != 0) ngx_log_stderr(err,"CSocekt::ServerTimerQueueMonitorThread()中pthread_mutex_lock()失败,返回的错误码为%d!",err);//有问题,要及时报告
while ((result = pSocketObj->GetOverTimeTimer(cur_time)) != NULL) //一次性的把所有超时节点都拿过来
{
m_lsIdleList.push_back(result);
}//end while
err = pthread_mutex_unlock(&pSocketObj->m_timequeueMutex);
if(err != 0) ngx_log_stderr(err,"CSocekt::ServerTimerQueueMonitorThread()pthread_mutex_unlock()失败,返回的错误码为%d!",err);//有问题,要及时报告
LPSTRUC_MSG_HEADER tmpmsg;
while(!m_lsIdleList.empty())
{
tmpmsg = m_lsIdleList.front();
m_lsIdleList.pop_front();
pSocketObj->procPingTimeOutChecking(tmpmsg,cur_time); //这里需要检查心跳超时问题
} //end while(!m_lsIdleList.empty())
}
} //end if(pSocketObj->m_cur_size_ > 0)
usleep(500 * 1000); //为简化问题,我们直接每次休息500毫秒
} //end while
return (void*)0;
}
//心跳包检测时间到,该去检测心跳包是否超时的事宜,本函数只是把内存释放,子类应该重新事先该函数以实现具体的判断动作
void CSocekt::procPingTimeOutChecking(LPSTRUC_MSG_HEADER tmpmsg,time_t cur_time)
{
CMemory *p_memory = CMemory::GetInstance();
p_memory->FreeMemory(tmpmsg);
}
//心跳包检测时间到,该去检测心跳包是否超时的事宜,本函数是子类函数,实现具体的判断动作
void CLogicSocket::procPingTimeOutChecking(LPSTRUC_MSG_HEADER tmpmsg,time_t cur_time)
{
CMemory *p_memory = CMemory::GetInstance();
if(tmpmsg->iCurrsequence == tmpmsg->pConn->iCurrsequence) //此连接没断
{
lpngx_connection_t p_Conn = tmpmsg->pConn;
if(/*m_ifkickTimeCount == 1 && */m_ifTimeOutKick == 1) //能调用到本函数第一个条件肯定成立,所以第一个条件加不加无所谓,主要是第二个条件
{
//到时间直接踢出去的需求
zdClosesocketProc(p_Conn);
}
else if( (cur_time - p_Conn->lastPingTime ) > (m_iWaitTime*3+10) ) //超时踢的判断标准就是 每次检查的时间间隔*3,超过这个时间没发送心跳包,就踢【大家可以根据实际情况自由设定】
{
//踢出去【如果此时此刻该用户正好断线,则这个socket可能立即被后续上来的连接复用 如果真有人这么倒霉,赶上这个点了,那么可能错踢,错踢就错踢】
//ngx_log_stderr(0,"时间到不发心跳包,踢出去!"); //感觉OK
zdClosesocketProc(p_Conn);
}
p_memory->FreeMemory(tmpmsg);//内存要释放
}
else //此连接断了
{
p_memory->FreeMemory(tmpmsg);//内存要释放
}
return;
}
恶意包:
比如100毫秒内收到10数据包,我们称它为恶意包,当发现恶意包时,我们显示将包丢弃,然后将该用户提出
在接收完整包之后调用这个函数,将包放入收消息队列中前调用
//测试是否flood攻击成立,成立则返回true,否则返回false
bool CSocekt::TestFlood(lpngx_connection_t pConn)
{
struct timeval sCurrTime; //当前时间结构
uint64_t iCurrTime; //当前时间(单位:毫秒)
bool reco = false;
gettimeofday(&sCurrTime, NULL); //取得当前时间
iCurrTime = (sCurrTime.tv_sec * 1000 + sCurrTime.tv_usec / 1000); //毫秒
if((iCurrTime - pConn->FloodkickLastTime) < m_floodTimeInterval) //两次收到包的时间 < 100毫秒
{
//发包太频繁记录
pConn->FloodAttackCount++;
pConn->FloodkickLastTime = iCurrTime;
}
else
{
//既然发布不这么频繁,则恢复计数值
pConn->FloodAttackCount = 0;
pConn->FloodkickLastTime = iCurrTime;
}
//ngx_log_stderr(0,"pConn->FloodAttackCount=%d,m_floodKickCount=%d.",pConn->FloodAttackCount,m_floodKickCount);
if(pConn->FloodAttackCount >= m_floodKickCount)
{
//可以踢此人的标志
reco = true;
}
return reco;
}