一个服务器进程有一个服务器对象,在主线程中,可以较好的以面向对象的方式,处理一些服务器初始化业务和接收网络连接。
设计上:
(1)网络线程池和信号处理初始化:
1)注册信号函数
2)建立一个(或多个)被动连接线程池。在主循环中监听和建立网络接收连接对象,并投放到对应端口的被动连接线程池
3)在析构时回收资源
(2) 负责处理连接对象的建立:
1)把需要监听的socket注册到该epoll描述符中(只监听该socket的读状态的变化)。每个需要监听的端口(一个进程中可能会有多个需要监听的端口)会被一个socket绑定并监听
2)服务器对象的主线程主函数中,不断循环检查监听epoll描述符中的所有epoll事件(连接的事件)的读状态的变化(短时间阻塞检查)。
3)对有读状态变化的epoll事件列表,获取每个事件的socket,并接受该socket(事实上就是接受tcp连接)。对每个成功接受的连接,根据不同端口,建立不同的连接对象(该连接对象包含对应的socket),并投放到对应的被动连接线程池中。被动连接线程池就开始负责该连接的网络业务了。
每个被动连接线程池对应处理一个端口上的接收的连接对象的网络业务(根据端口判断需要生成的是哪类连接对象)。
1、服务器对象
(1)服务器对象构造和析构
创建epoll描述符和初始化epoll事件列表(vector)。nMNetService是服务器对象的基类。如登录服务器对象:class login_server: public nMNetServicenMTCPServer::nMTCPServer(const std::string &name) : name(name)
{
kdpfd = epoll_create(1);
assert(-1 != kdpfd);
epfds.resize(8);
}
nMTCPServer::~nMTCPServer()
{
TEMP_FAILURE_RETRY(::close(kdpfd));//关闭epoll描述符
for(Sock2Port_const_iterator it = mapper.begin(); it != mapper.end(); it++)
{
if (-1 != it->first)
{
::shutdown(it->first, SHUT_RD);关闭socket的读
TEMP_FAILURE_RETRY(::close(it->first));//回收socket描述符
}
}
mapper.clear();
}
(2)服务器对象绑定端口
创建监听socket,设置监听端口的socket的属性,作为epoll事件(只是监听读事件)注册到epoll描述符里,由epoll描述符监听socket读状态的变化
bool nMTCPServer::bind(const std::string &name, const unsigned short port)
{
auto_mutex_lock scope_lock(mlock);
struct sockaddr_in addr;
int sock;
for(Sock2Port_const_iterator it = mapper.begin(); it != mapper.end(); it++)//检查端口是否已经被本进程占用
{
if (it->second == port)
{
g_log->warn("端口 %u 已经绑定服务", port);
return false;
}
}
sock = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//tcp协议的socket,af协议簇
if (-1 == sock)
{
g_log->error("创建套接口失败");
return false;
}
//设置套接口为可重用状态
int reuse = 1;
if (-1 == ::setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse))) //设置地址重用
{
g_log->error("不能设置套接口为可重用状态");
TEMP_FAILURE_RETRY(::close(sock));
return false;
}
//设置套接口发送接收缓冲,并且服务器的必须在accept之前设置
socklen_t window_size = 128 * 1024;
if (-1 == ::setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &window_size, sizeof(window_size)))//设置接收缓存大小
{
TEMP_FAILURE_RETRY(::close(sock));
return false;
}
if (-1 == ::setsockopt(sock, SOL_SOCKET, SO_SNDBUF, &window_size, sizeof(window_size)))//设置发送缓存大小
{
TEMP_FAILURE_RETRY(::close(sock));
return false;
}
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port);
//绑定socket需要监听的端口和地址
int retcode = ::bind(sock, (struct sockaddr *) &addr, sizeof(addr));
if (-1 == retcode)
{
g_log->error("不能绑定服务器端口");
TEMP_FAILURE_RETRY(::close(sock));
return false;
}
retcode = ::listen(sock, MAX_WAITQUEUE);
if (-1 == retcode)
{
g_log->error("监听套接口失败");
TEMP_FAILURE_RETRY(::close(sock));
return false;
}
struct epoll_event ev={0};
ev.events = EPOLLIN;
ev.data.fd = sock;//把socket注册到epoll事件(读事件)中
//把需要监听读取的socket描述符注册到epoll描述符中
//这个epoll描述符是用来监听需要接收的连接的
//可以注册多个epoll 事件到epoll描述符
assert(0 == epoll_ctl(kdpfd, EPOLL_CTL_ADD, sock, &ev));
mapper.insert(Sock2Port_value_type(sock, port));//记录绑定端口和socket文件描述符
if (mapper.size() > epfds.size())
{
epfds.resize(mapper.size() + 8); //扩大epoll监听事件列表
}
g_log->info("服务器 %s:%u 端口初始化绑定成功", name.c_str(), port);
return true;
}
(3)服务器对象接收连接
把在epoll描述符上监听的事件中出现读状态的epoll事件中的socket描述符记录下来作为结果( stl::map结构,内容是 socket描述符:端口)。
这个接口是在主线程的主函数循环里不断检查访问。
int nMTCPServer::accept(Sock2Port &res)
{
auto_mutex_lock scope_lock(mlock);
int retval = 0;
int rc = epoll_wait(kdpfd, &epfds[0], mapper.size(), T_MSEC);//短时间阻塞接收连接,epfds 里的是监听的epoll事件,epoll事件里有socket描述符
if (rc > 0)
{
for(int i = 0; i < rc; i++)
{
if (epfds[i].events & EPOLLIN)//检查epoll事件变化的状态是读入
{
res.insert(Sock2Port_value_type(TEMP_FAILURE_RETRY(::accept(epfds[i].data.fd, NULL, NULL)), mapper[epfds[i].data.fd]));
retval++;
}
}
}
return retval;
}
2、服务器对象初始化
(1)创建一个(或多个)被动连接线程池,监听多个端口
创建一个(或多个)被动连接线程池,并让进程主线程监听多个端口,在建立连接对象时会根据端口判断投放到哪个被动连接线程池里。
以下是登录服务器中的登录服务器对象的例子。
bool login_server::init()
{
char pttype[16];
bzero(pttype, 16);
strncpy(pttype,g_xml_config.get_string("Global.PingTai.pttype"),16);
...
// ..pttype 是平台名称,pingtai是平台编号
g_log->info("平台的类型:%u,%s",pingtai,pttype);
phpIP1 = g_xml_config.get_string("Global.PhpServer.IP1");//对外开放的php服务器的ip1(需要限制可以连接过来的ip权限,在加入连接池的验证线程里时需要检查)
phpIP2 = g_xml_config.get_string("Global.PhpServer.IP2");//对外开放的php服务器的ip2
mysqlPool = new mysql_handle_pool;//mysql句柄池初始化
if(mysqlPool == NULL ||!mysqlPool->putUrl(0,g_xml_config.get_string("Global.MYSQL.Config")))//初始化到mysql的连接
{
g_log->error("连接数据库失败");
return false;
}
POINT_DEBUG_INFO(info,"NP---初始化数据库连接池指针%p",mysqlPool);
if(!nMNetService::init())//服务器对象初始化
{
return false;
}
//接收中心服务器的连接的被动连接线程池
center_session_pool = new tcp_session_pool(g_xml_config.get_integer("Global.ThreadCarryLink.Config"));//被动连接线程池(只是处理中心服务器的连接)
if(center_session_pool == NULL || !center_session_pool->init())
{
return false;
}
//被动连接线程池
login_session_pool = new tcp_session_pool(g_xml_config.get_integer("Global.ThreadCarryLink.Config"));//被动连接线程池
if(login_session_pool == NULL || !login_session_pool->init())
{
return false;
}
//接收php服务器的连接的被动连接线程池
php_session_pool = new tcp_session_pool(g_xml_config.get_integer("Global.ThreadCarryLink.Config"));//被动连接线程池(只是处理php服务器的连接)
if(php_session_pool == NULL || !php_session_pool->init())
{
return false;
}
//接收支付服务器的连接的被动连接线程池
pay_session_pool = new tcp_session_pool(g_xml_config.get_integer("Global.ThreadCarryLink.Config"));//被动连接线程池(只是处理中心服务器的连接)
if(pay_session_pool == NULL || !pay_session_pool->init())
{
return false;
}
centerPort = g_xml_config.get_integer("Global.Login.CenterPort");//中心服务器连接端口
loginPort = g_xml_config.get_integer("Global.Login.LoginPort");//登录服务器连接端口
phpPort = g_xml_config.get_integer("Global.Login.PHPPort");//php服务器连接端口
payPort = g_xml_config.get_integer("Global.Login.PayPort");//支付服务器连接端口
if(!nMNetService::bind("centerserver端口",centerPort))//监听等待中心服务器连接的端口
{
g_log->error("绑定centerserver端口出错");
return false;
}
if(!nMNetService::bind("login端口",loginPort))//监听等待登录服务器连接的端口
{
g_log->error("绑定login端口出错");
return false;
}
if(!nMNetService::bind("php端口",phpPort))//监听等待php服务器连接的端口
{
g_log->error("绑定php端口出错");
return false;
}
if(!nMNetService::bind("pay端口",payPort))//监听等待支付服务器连接的端口
{
g_log->error("绑定pay端口出错...");
return false;
}
initAccount();//加载账号信息
if(!g_main_logic_thread.start())
{
g_log->error("初始化main_logic模块..失败");
}
g_log->info("Login 启动完毕");
return true;
}
(2)初始化服务器对象
bool nMNetService::init()
{
if (!nService::init())
return false;
//初始化服务器
tcpServer = new nMTCPServer(this->name);
if (NULL == tcpServer)
return false;
return true;
}
bool nService::init()
{
//存储环境变量
int i = 0;
while(environ[i])//environ 是个存储了环境变量的字符串数组
{
std::string s(environ[i++]);
std::vector<std::string> v;
StringTool::split(s,'=',v,2);
if (!v.empty() && v.size() == 2)
env[v[0]] = v[1]; //记录环境变量到自定义哈希表中
}
//设置信号处理回调(自动处理的:处理函数返回后,所有打开的文件描述符将会被关闭,流也会被flush)
struct sigaction sig;
sig.sa_handler = ctrlcHandler;
sigemptyset(&sig.sa_mask);//清空掩码(需要阻塞的信号)
sig.sa_flags = 0;//清空特殊标识
//以下4个信号的回调函数是自定义函数ctrlcHandler
sigaction(SIGINT, &sig, NULL);//中断信号
sigaction(SIGQUIT, &sig, NULL);//退出信号
sigaction(SIGABRT, &sig, NULL);//终止信号(系统发出的,或者自主调用abort函数也会引发该信号)
sigaction(SIGTERM, &sig, NULL);
sig.sa_handler = hupHandler;// 设置信号处理函数
sigaction(SIGHUP, &sig, NULL);
sig.sa_handler = SIG_IGN;//忽略该信号
sigaction(SIGPIPE, &sig, NULL);//在读者关闭之后写Pipe的时候引发该信号
//初始化随机数
srand(time(NULL));
return true;
}
(3)创建新的连接,放置到合适的被动连接线程池
创建新的连接,根据是从哪个端口接收的连接来创建合适的连接对象并投放到合适的被动连接线程池
void login_server::newTCPTask(const int sock,const unsigned short srcPort)
{
if(srcPort == centerPort)
{
center_session *task = new center_session(sock,NULL);
if(task == NULL)
{
TEMP_FAILURE_RETRY(::close(sock));
}
else if(!center_session_pool->addVerify(task))
{
SAFE_DELETE(task);
}
}
else if(srcPort == loginPort)
{
login_session *task = new login_session(sock,NULL);
if(task == NULL)
{
TEMP_FAILURE_RETRY(::close(sock));
}
if(!login_session_pool->addVerify(task))
{
SAFE_DELETE(task);
}
}else if(srcPort == phpPort)
{
php_session *task = new php_session(sock,NULL);
if(task == NULL)
{
TEMP_FAILURE_RETRY(::close(sock));
}
std::string taskIP = task->getIP();
if(taskIP != phpIP1 && taskIP != phpIP2)
{
g_log->error("php 服务器IP:%s 错误", taskIP.c_str());
SAFE_DELETE(task);
TEMP_FAILURE_RETRY(::close(sock));
return;
}
if(!php_session_pool->addVerify(task))
{
SAFE_DELETE(task);
}
}
else if(srcPort == payPort)
{
pay_session *task = new pay_session(sock,NULL);//充值服务器连接
if(task == NULL)
{
TEMP_FAILURE_RETRY(::close(sock));
}
if(!pay_session_pool->addVerify(task))
{
SAFE_DELETE(task);
}
}
else
{
TEMP_FAILURE_RETRY(::close(sock));
}
}
(4)服务器资源回收
void login_server::final()
{
if(center_session_pool)
{
center_session_pool->final();
POINT_DEBUG_INFO(info,"DP---销毁中心服务器连接池指针%p",center_session_pool);
SAFE_DELETE(center_session_pool);
}
if(login_session_pool)
{
login_session_pool->final();
POINT_DEBUG_INFO(info,"DP---销毁登录连接池指针%p",login_session_pool);
SAFE_DELETE(login_session_pool);
}
if(php_session_pool)
{
php_session_pool->final();
POINT_DEBUG_INFO(info,"DP---销毁PHP连接池指针%p",php_session_pool);
SAFE_DELETE(php_session_pool);
}
if(pay_session_pool)
{
pay_session_pool->final();
POINT_DEBUG_INFO(info,"DP---销毁PAY连接池指针%p",pay_session_pool);
SAFE_DELETE(pay_session_pool);
}
g_log->debug("%s",__PRETTY_FUNCTION__);
}
3、服务器主函数循环
服务对象的主函数循环,在main函数中会调用该函数。
不断检查接收连接,并生成连接对象。
void nService::loop()
{
...
//初始化程序,并确认服务器启动成功
if (init()&& validate())
{
//运行主回调线程
while(!isTerminate())
{
if (!servicecallback())//不断调用回调函数检查
{
break;
}
}
}
//结束程序,释放相应的资源
final();
}
网络服务的服务器对象的循环里处理的回调函数,会接收和建立连接对象,并投放到合适的被动连接线程池中。
bool nMNetService::servicecallback()
{
if (NULL == tcpServer) return false;
nMTCPServer::Sock2Port res;
if (tcpServer->accept(res) > 0) //接收连接
{
for(nMTCPServer::Sock2Port_const_iterator it = res.begin(); it != res.end(); it++)
{
if (it->first >= 0)//接收成功的socket描述符
{
//接收连接成功,处理连接
newTCPTask(it->first, it->second);
}
}
}
return true;
}