loginserv的功能和名称,是有些不一致,它并不处理登陆。这个服务相当于一个msgserv的负载管理器,客户端连进来,需要先连接到loginserv,从loginserv获取一个可用的msgserv的信息,剩下的就客户端就直接和msgserv打交道了,所以loginserv需要交互的对像分为两类,一类是客户端,给客户端返回msgserv信息,一类是msgserv,记录msgserv的ip,端口,用户数等信息。客户端因为协议的不同,又分为http客户端和其他客户端。
我们从loginserv的入口看下,代码在login_server.cpp,下面对一些步骤做了注释。
int main(int argc, char* argv[]) {
if ((argc == 2) && (strcmp(argv[1], "-v") == 0)) { printf("Server Version: LoginServer/%s\n", VERSION); printf("Server Build: %s %s\n", __DATE__, __TIME__); return 0; } signal(SIGPIPE, SIG_IGN); CConfigFileReader config_file("loginserver.conf"); //读取配置文件 char* client_listen_ip = config_file.GetConfigName("ClientListenIP"); // 这个IP用来接入客户端,使用PB协议 char* str_client_port = config_file.GetConfigName("ClientPort"); char* http_listen_ip = config_file.GetConfigName("HttpListenIP"); // 这个IP用来接入网页,使用的是http协议 char* str_http_port = config_file.GetConfigName("HttpPort"); char* msg_server_listen_ip = config_file.GetConfigName("MsgServerListenIP"); //这个IP用来和msgserv进行通信,长连接 char* str_msg_server_port = config_file.GetConfigName("MsgServerPort"); char* str_msfs_url = config_file.GetConfigName("msfs"); // 这个是配置文件服务器 char* str_discovery = config_file.GetConfigName("discovery"); if (!msg_server_listen_ip || !str_msg_server_port || !http_listen_ip || !str_http_port || !str_msfs_url || !str_discovery) { log("config item missing, exit... "); return -1; } uint16_t client_port = atoi(str_client_port); uint16_t msg_server_port = atoi(str_msg_server_port); uint16_t http_port = atoi(str_http_port); strMsfsUrl = str_msfs_url; strDiscovery = str_discovery; pIpParser = new IpParser(); // 这个用来判断IP是电信还是网通 int ret = netlib_init(); if (ret == NETLIB_ERROR) return ret;
//分别启动监听,并且传入回调函数
CStrExplode client_listen_ip_list(client_listen_ip, ';'); for (uint32_t i = 0; i < client_listen_ip_list.GetItemCnt(); i++) { ret = netlib_listen(client_listen_ip_list.GetItem(i), client_port, client_callback, NULL); if (ret == NETLIB_ERROR) return ret; } CStrExplode msg_server_listen_ip_list(msg_server_listen_ip, ';'); for (uint32_t i = 0; i < msg_server_listen_ip_list.GetItemCnt(); i++) { ret = netlib_listen(msg_server_listen_ip_list.GetItem(i), msg_server_port, msg_serv_callback, NULL); if (ret == NETLIB_ERROR) return ret; } CStrExplode http_listen_ip_list(http_listen_ip, ';'); for (uint32_t i = 0; i < http_listen_ip_list.GetItemCnt(); i++) { ret = netlib_listen(http_listen_ip_list.GetItem(i), http_port, http_callback, NULL); if (ret == NETLIB_ERROR) return ret; } printf("server start listen on:\nFor client %s:%d\nFor MsgServer: %s:%d\nFor http:%s:%d\n", client_listen_ip, client_port, msg_server_listen_ip, msg_server_port, http_listen_ip, http_port); init_login_conn(); // 这里传入计时器的回调函数 init_http_conn(); printf("now enter the event loop...\n"); writePid(); //写线程文件 netlib_eventloop(); // 启动事件监听 return 0; }
总体来说,有三个listern比较重要,下面先介绍下listen这个函数,后面再分别介绍三个listen的作用。
netlib_listen的原型如下:
int netlib_listen( const char* server_ip, uint16_t port, callback_t callback, void* callback_data) { CBaseSocket* pSocket = new CBaseSocket(); if (!pSocket) return NETLIB_ERROR; int ret = pSocket->Listen(server_ip, port, callback, callback_data); if (ret == NETLIB_ERROR) delete pSocket; return ret; }
这里new了一个CBaseSocket,并且调用了CBaseSocket的Listen,CBaseSocket的Listen实现如下
int CBaseSocket::Listen(const char* server_ip, uint16_t port, callback_t callback, void* callback_data) { m_local_ip = server_ip; m_local_port = port; m_callback = callback; m_callback_data = callback_data; m_socket = socket(AF_INET, SOCK_STREAM, 0); //调用底层socket if (m_socket == INVALID_SOCKET) { printf("socket failed, err_code=%d\n", _GetErrorCode()); return NETLIB_ERROR; } _SetReuseAddr(m_socket); // 设置socket属性 _SetNonblock(m_socket); sockaddr_in serv_addr; _SetAddr(server_ip, port, &serv_addr); int ret = ::bind(m_socket, (sockaddr*)&serv_addr, sizeof(serv_addr)); //绑定端口 if (ret == SOCKET_ERROR) { log("bind failed, err_code=%d", _GetErrorCode()); closesocket(m_socket); return NETLIB_ERROR; } ret = listen(m_socket, 64); // 调用listen if (ret == SOCKET_ERROR) { log("listen failed, err_code=%d", _GetErrorCode()); closesocket(m_socket); return NETLIB_ERROR; } m_state = SOCKET_STATE_LISTENING; log("CBaseSocket::Listen on %s:%d", server_ip, port); AddBaseSocket(this); CEventDispatch::Instance()->AddEvent(m_socket, SOCKET_READ | SOCKET_EXCEP); return NETLIB_OK; }
前面的没什么特别的,看下AddBaseSocket这个方法,把创建好的socket,加入到一个全局的map中,定义如下:
typedef hash_mapSocketMap; SocketMap g_socket_map; void AddBaseSocket(CBaseSocket* pSocket) { g_socket_map.insert(make_pair((net_handle_t)pSocket->GetSocket(), pSocket)); }
后续的操作时,需要从g_socket_map中取到对应的对象的指针即可操作。CBaseSocket本身继承了引用计数类。
CEventDispatch是个单例,这个类根据平台,实现了select/epoll/kevent的内容封装,把事件分为两类
读事件,调用CBaseSocket的onRead方法,函数实现如下:
void CBaseSocket::OnRead() { if (m_state == SOCKET_STATE_LISTENING) { _AcceptNewSocket(); } else { u_long avail = 0; if ( (ioctlsocket(m_socket, FIONREAD, &avail) == SOCKET_ERROR) || (avail == 0) ) { m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL); } else { m_callback(m_callback_data, NETLIB_MSG_READ, (net_handle_t)m_socket, NULL); } } }
写事件,调用CBaseSocket的onWrite方法,函数实现如下:
void CBaseSocket::OnWrite() { #if ((defined _WIN32) || (defined __APPLE__)) CEventDispatch::Instance()->RemoveEvent(m_socket, SOCKET_WRITE); #endif if (m_state == SOCKET_STATE_CONNECTING) { int error = 0; socklen_t len = sizeof(error); #ifdef _WIN32 getsockopt(m_socket, SOL_SOCKET, SO_ERROR, (char*)&error, &len); #else getsockopt(m_socket, SOL_SOCKET, SO_ERROR, (void*)&error, &len); #endif if (error) { m_callback(m_callback_data, NETLIB_MSG_CLOSE, (net_handle_t)m_socket, NULL); } else { m_state = SOCKET_STATE_CONNECTED; m_callback(m_callback_data, NETLIB_MSG_CONFIRM, (net_handle_t)m_socket, NULL); } } else { m_callback(m_callback_data, NETLIB_MSG_WRITE, (net_handle_t)m_socket, NULL); } }
这两类事件中,分别调用回调函数,所以在使用的过程中,需要传入回调函数。
在main函数中,netlib_eventloop();执行之后,传入的回调函数开始工作,我们先看第一个listen的执行过程,功能上来说,主要是建立和msgserv的连接和通信
1、netlib_listen(msg_server_listen_ip_list.GetItem(i), msg_server_port, msg_serv_callback, NULL);
小L(loginserv):开始接客了(启动了),给你(msgserv)留了后门(端口),你来吧!
从上面Listen函数可以知道,服务端的fd,设置了事件为read,并且加入到EventDispatch,所以会调用CBaseSocket的OnRead函数,因为state = listenning,所以会走到_AcceptNewSocket()的逻辑
2、_AcceptNewSocket接收socket的函数,在接收到msgserv的连接,并且调用回调函数,如下
void CBaseSocket::_AcceptNewSocket()
{
SOCKET fd = 0;
sockaddr_in peer_addr;
socklen_t addr_len = sizeof(sockaddr_in);
char ip_str[64];
while ( (fd = accept(m_socket, (sockaddr*)&peer_addr, &addr_len)) != INVALID_SOCKET )
{
CBaseSocket* pSocket = new CBaseSocket();
uint32_t ip = ntohl(peer_addr.sin_addr.s_addr);
uint16_t port = ntohs(peer_addr.sin_port);
snprintf(ip_str, sizeof(ip_str), "%d.%d.%d.%d", ip >> 24, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF);
log("AcceptNewSocket, socket=%d from %s:%d\n", fd, ip_str, port);
pSocket->SetSocket(fd);
pSocket->SetCallback(m_callback);
pSocket->SetCallbackData(m_callback_data);
pSocket->SetState(SOCKET_STATE_CONNECTED);
pSocket->SetRemoteIP(ip_str);
pSocket->SetRemotePort(port);
_SetNoDelay(fd);
_SetNonblock(fd);
AddBaseSocket(pSocket);
CEventDispatch::Instance()->AddEvent(fd, SOCKET_READ | SOCKET_EXCEP);
m_callback(m_callback_data, NETLIB_MSG_CONNECT, (net_handle_t)fd, NULL);
}
}
小M(msgserv):我来了(accetp到新的连接)
3、这里就走到了msg_serv_callback的调用逻辑,传入参数是NETLIB_MSG_CONNECT
// this callback will be replaced by imconn_callback() in OnConnect() void msg_serv_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam) { log("msg_server come in"); if (msg == NETLIB_MSG_CONNECT) { CLoginConn* pConn = new CLoginConn(); pConn->OnConnect2(handle, LOGIN_CONN_TYPE_MSG_SERV); } else { log("!!!error msg: %d ", msg); } }
小M(msgserv):也没带啥礼物,帮你干点啥吧(调用msg_serv_callback)
4、回调函数的第三个参数,传入的是msgserv接入的fd,这个fd有新的任务,所以他的回调函数,需要重新设置,具体的设置是在CLoginConnet的OnConnect2里完成
void CLoginConn::OnConnect2(net_handle_t handle, int conn_type) { m_handle = handle; m_conn_type = conn_type; ConnMap_t* conn_map = &g_msg_serv_conn_map; if (conn_type == LOGIN_CONN_TYPE_CLIENT) { conn_map = &g_client_conn_map; }else conn_map->insert(make_pair(handle, this)); netlib_option(handle, NETLIB_OPT_SET_CALLBACK, (void*)imconn_callback); netlib_option(handle, NETLIB_OPT_SET_CALLBACK_DATA, (void*)conn_map); }
小L(loginserv):这门我一直给你留着(长连接),你下次来有啥事直接自己搞吧(设置回调函数,imconn_callback)
5、我们看到,这里设置的回调函数是imconn_callback,这个函数在CLoginConn的父类中实现。具体如下:
void imconn_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam) { NOTUSED_ARG(handle); NOTUSED_ARG(pParam); if (!callback_data) return; ConnMap_t* conn_map = (ConnMap_t*)callback_data; CImConn* pConn = FindImConn(conn_map, handle); if (!pConn) return; //log("msg=%d, handle=%d ", msg, handle); switch (msg) { case NETLIB_MSG_CONFIRM: pConn->OnConfirm(); break; case NETLIB_MSG_READ: pConn->OnRead(); break; case NETLIB_MSG_WRITE: pConn->OnWrite(); break; case NETLIB_MSG_CLOSE: pConn->OnClose(); break; default: log("!!!imconn_callback error msg: %d ", msg); break; } pConn->ReleaseRef(); }
这个连接至此已经建立完成,接下来就可以互相发消息了,
msgserv向loginserv发的消息有三个,可以从CLoginConn的handlePdu的方法中看出,handlePdu是父类CImConn的onRead方法中会调用的方案,CLoginConn重载了handlePdu,onRead在执行时,调用的是子类的handlePdu方法,实现如下:
void CLoginConn::HandlePdu(CImPdu* pPdu) { switch (pPdu->GetCommandId()) { case CID_OTHER_HEARTBEAT: break; case CID_OTHER_MSG_SERV_INFO: _HandleMsgServInfo(pPdu); break; case CID_OTHER_USER_CNT_UPDATE: _HandleUserCntUpdate(pPdu); break; case CID_LOGIN_REQ_MSGSERVER: _HandleMsgServRequest(pPdu); break; default: log("wrong msg, cmd id=%d ", pPdu->GetCommandId()); break; } }
1、心跳CID_OTHER_HERTBBEAT 2、告知msgserv的服务器信息,CID_OTHER_MSG_SERV_INFO 3、更新msgserv的承载用户数 CID_OTHER_USER_CNT_UPDATE
第四个消息是给客户端的。
下面介绍第二个listen函数,这里是客户端接入的逻辑,套路和服务端类似
1、netlib_listen(client_listen_ip_list.GetItem(i), client_port, client_callback, NULL);
2、会走到_AcceptNewSocket
3、走到 client_callback的逻辑
void client_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam) { if (msg == NETLIB_MSG_CONNECT) { CLoginConn* pConn = new CLoginConn(); pConn->OnConnect2(handle, LOGIN_CONN_TYPE_CLIENT); } else { log("!!!error msg: %d ", msg); } }
4、设置客户端fd的回调imconn_callback
5、连接建立成功,客户端的交互只有一个消息,即CID_LOGIN_REQ_MSGSERVER,这个消息是申请一个msgserv,所以msgserv会先和loginserv进行通信,客户端才能申请到msgserv。
HttpConn的部分套路和上面略有不同,这里也罗列下:
1、netlib_listen(http_listen_ip_list.GetItem(i), http_port, http_callback, NULL);
2、会走到_AcceptNewSocket
3、走到http_callback的逻辑
void http_callback(void* callback_data, uint8_t msg, uint32_t handle, void* pParam) { if (msg == NETLIB_MSG_CONNECT) { CHttpConn* pConn = new CHttpConn(); pConn->OnConnect(handle); } else { log("!!!error msg: %d ", msg); } }
4、OnConnet方法设置客户端的回调函数 httpconn_callback
void httpconn_callback(void* callback_data, uint8_t msg, uint32_t handle, uint32_t uParam, void* pParam) { NOTUSED_ARG(uParam); NOTUSED_ARG(pParam); // convert void* to uint32_t, oops uint32_t conn_handle = *((uint32_t*)(&callback_data)); CHttpConn* pConn = FindHttpConnByHandle(conn_handle); if (!pConn) { return; } switch (msg) { case NETLIB_MSG_READ: pConn->OnRead(); break; case NETLIB_MSG_WRITE: pConn->OnWrite(); break; case NETLIB_MSG_CLOSE: pConn->OnClose(); break; default: log("!!!httpconn_callback error msg: %d ", msg); break; } }
5、连接成功建立,CHttpConn本身不继承自CImConn,因为协议不一样,所以消息处理,直接在onRead方法中,只处理了一个消息,即CID_LOGIN_REQ_MSGSERVER,但不是通过消息判断的,而是通过url判断的,具体如下:
m_cHttpParser.ParseHttpContent(in_buf, buf_len); if (m_cHttpParser.IsReadAll()) { string url = m_cHttpParser.GetUrl(); if (strncmp(url.c_str(), "/msg_server", 11) == 0) { string content = m_cHttpParser.GetBodyContent(); _HandleMsgServRequest(url, content); } else { log("url unknown, url=%s ", url.c_str()); Close(); } }
判断url中包含/msg_server字符串,即执行_HandleMsgServRequest函数。
loginserv服务角色上,也是msgserv的资源管理器,或者说进行了简单的负载均衡器,loginserv还有点类似医院的挂号处,所有客户端都得来走一遭,挂个号,给你分配医生(msgserv)。
说点别的,看了下TT架构师的关于TT架构的文章http://mogu.io/im-server-develop-01-8,里面提到这样一句话:
“目前我们的IM服务器架构设计的单机并发连接10万用户,总并发用户量可以达百万级,对于这样规模的服务器后台,可以采用很简单的架构来处理。有一个DispatchServer来分配客户端到一个消息服务器,消息服务器之间的数据交互通过一个中心的RouteServer来转发,和数据持久化层之间的交互由DBProxy服务器来处理.”
所以我想loginserv,应该是预想的DispatchServer的角色。