TeamTalk 服务端源码分析(一)loginserv

  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_map<net_handle_t, CBaseSocket*> SocketMap;
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的角色。

posted @ 2017-10-13 21:03  xiaomengaliang  阅读(857)  评论(0编辑  收藏  举报