TeamTalk整体梳理

在这里插入图片描述

各个服务程序的作用描述

  • LoginServer: 负载均衡服务器,分配一个负载小的MsgServer给客户端使用
  • MsgServer:消息服务器,提供客户端大部分信令处理功能,包括单聊、群聊等
  • RouteServer: 路由服务器,为登录在不同MsgServer的用户提供消息转发功能
  • DBProxy: 数据库代理服务器,提供mysql以及redis的访问服务,屏蔽其他服务器与mysql与redis的直接交互
  • FileServer: 文件服务器,提供客户端之间文件传输服务,支持在线和离线文件传输
  • MsfsServer: 图片存储服务器,提供头像,图片传输中的图片存储服务

db_proxy_server

1. 为什么设计db_proxy_server服务?

答:db_proxy_server是数据库代理服务。提供mysql以及redis的访问服务,屏蔽其他服务器与mysql和redis的直接交互。

2. db_proxy_server reactor响应处理流程

  1. 初始化epoll(I/O线程)+线程池(工作线程)
  2. 数据入口 reactor CProxyConn:: HandlePduBuf(I/O线程)
  3. 任务封装,把任务放入线程池(I/O线程)
  4. 执行任务(工作线程)
  5. 把要回应的数据放入回复列表CProxyConn::SendResponsePdulist(工作线程)
  6. epoll所在线程读取回复列表的数据发给请求端(I/O线程)
void CEventDispatch::StartDispatch(uint32_t wait_timeout)
{
    struct epoll_event events[1024];
    int nfds = 0;

    if(running)
        return;
    running = true;
    
    while (running)
    {
        nfds = epoll_wait(m_epfd, events, 1024, wait_timeout);
        for (int i = 0; i < nfds; i++)
        {
            int ev_fd = events[i].data.fd;
            CBaseSocket* pSocket = FindBaseSocket(ev_fd);
            if (!pSocket)
                continue;
            
            //Commit by zhfu @2015-02-28
            #ifdef EPOLLRDHUP
            if (events[i].events & EPOLLRDHUP)
            {
                //log("On Peer Close, socket=%d, ev_fd);
                pSocket->OnClose();
            }
            #endif
            // Commit End

            if (events[i].events & EPOLLIN)
            {
                //log("OnRead, socket=%d\n", ev_fd);
                pSocket->OnRead();
            }
            
            if (events[i].events & EPOLLOUT)
            {
                //log("OnWrite, socket=%d\n", ev_fd);
                pSocket->OnWrite();
            }

            if (events[i].events & (EPOLLPRI | EPOLLERR | EPOLLHUP))
            {
                //log("OnClose, socket=%d\n", ev_fd);
                pSocket->OnClose();
            }

            pSocket->ReleaseRef();
        }

        _CheckTimer();
        _CheckLoop();
    }
}
// 由于数据包是在另一个线程处理的,
// 所以不能在主线程delete数据包,(哪里delete?)
// 所以需要Override这个方法(?)
void CProxyConn::OnRead()
{
  for (;;) {
    uint32_t free_buf_len = m_in_buf.GetAllocSize() 
                            - m_in_buf.GetWriteOffset();
    if (free_buf_len < READ_BUF_SIZE)
        m_in_buf.Extend(READ_BUF_SIZE);

    int ret = netlib_recv(m_handle, m_in_buf.GetBuffer() 
              + m_in_buf.GetWriteOffset(), READ_BUF_SIZE);
    if (ret <= 0)
        break;

    m_recv_bytes += ret;
    m_in_buf.IncWriteOffset(ret);
    m_last_recv_tick = get_tick_count();
  }

  uint32_t pdu_len = 0;
  try {
    while(CImPdu::IsPduAvailable(m_in_buf.GetBuffer(), 
                m_in_buf.GetWriteOffset(), pdu_len) ) {
                
            HandlePduBuf(m_in_buf.GetBuffer(), pdu_len);
            m_in_buf.Read(NULL, pdu_len);
    }
  } catch (CPduException& ex) {
    log("!!!catch exception, err_code=%u, err_msg=%s, 
            close the connection ",
            ex.GetErrorCode(), ex.GetErrorMsg());
    OnClose();
  }
        
}
2.1 回发如何找到原来的链接?

答:由于处理请求和发送回复在两个线程,socket的handle可能重用,所以需要用一个一直增加的uuid来表示一个连接。
多个msg_server和db_proxy_server建立连接时,会将每个proxyconn连接和m_uuid一一对应映射起来。然后包装task时也会将m_uuid 传进去。

CProxyConn::CProxyConn()
{
        m_uuid = ++CProxyConn::s_uuid_alloctor;
        if (m_uuid == 0) {
                m_uuid = ++CProxyConn::s_uuid_alloctor;
        }

        g_uuid_conn_map.insert(make_pair(m_uuid, this));
}
void CProxyConn::HandlePduBuf(uchar_t* pdu_buf, uint32_t pdu_len)
{
    CImPdu* pPdu = NULL;
    pPdu = CImPdu::ReadPdu(pdu_buf, pdu_len);
    if (pPdu->GetCommandId() == IM::BaseDefine::CID_OTHER_HEARTBEAT) {
        return;
    }
    
    pdu_handler_t handler = s_handler_map->GetHandler(pPdu->GetCommandId());
    
    if (handler) {
        CTask* pTask = new CProxyTask(m_uuid, handler, pPdu);
        g_thread_pool.AddTask(pTask);
    } else {
        log("no handler for packet type: %d", pPdu->GetCommandId());
    }
}

CProxyConn::AddResponsePdu(conn_uuid, pPduResp);

/*
 * static method
 * add response pPdu to send list for another thread to send
 * if pPdu == NULL, it means you want to close connection with conn_uuid
 * e.g. parse packet failed
 */
void CProxyConn::AddResponsePdu(uint32_t conn_uuid, CImPdu* pPdu)
{
        ResponsePdu_t* pResp = new ResponsePdu_t;
        pResp->conn_uuid = conn_uuid;
        pResp->pPdu = pPdu;

        s_list_lock.lock();
        s_response_pdu_list.push_back(pResp);
        s_list_lock.unlock();
}

void CProxyConn::SendResponsePduList()
{
    s_list_lock.lock();
    while (!s_response_pdu_list.empty()) {
        ResponsePdu_t* pResp = s_response_pdu_list.front();
        s_response_pdu_list.pop_front();
        s_list_lock.unlock();

        CProxyConn* pConn = get_proxy_conn_by_uuid(pResp->conn_uuid);
        if (pConn) {
            if (pResp->pPdu) {
                pConn->SendPdu(pResp->pPdu);
            } else {
                log("close connection uuid=%d by parse pdu error\b", pResp->conn_uuid);
                pConn->Close();
            }
        }

        if (pResp->pPdu)
            delete pResp->pPdu;
            delete pResp;

            s_list_lock.lock();
        }

        s_list_lock.unlock();
}

3. 连接池

3.1 为什么使用连接池?

答:对象复用,减小频繁创建链接释放链接的开销时间。

3.2 redis连接池
  • 第一步我们先创建redis连接池类(CachePool类),该类的私有成员变量主要有存放redis的连接list容器;每个redis连接池的对象的名字(m_pool_name);该类的方法主要有获取和释放redis连接,获取连接池的对象的名字等。
  • 第二步我们创建CacheManager类用来管理redis连接池对象。该类的私有成员变量主要有map容器在将连接池对象的pool_name和连接池对象映射起来。db_proxy_server的main函数首先初始化CacheManager类的单例对象(pCacheManager)。主要逻辑是读取配置文件来创建不同的redis连接池,不同的连接池有不同的名字,创建连接池对象后,放到一个map容器里管理起来。连接池主要包括(unread group_member等)我们后续的相关业务逻辑主要实现了unread 连接池,里面包括主要包括单聊和群聊的未读消息计数。
  • unread的业务逻辑展开:数据库代理模块之redis

3.3 MySQL连接池

  1. 第一步我们先创建MySQL连接池类(CDBPool类),该类的私有成员变量主要有存放MySQL的连接list容器;每个MySQL连接池的对象的名字(m_pool_name);
  2. 第二步我们创建CDBManager类用来管理MySQL连接池对象。该类的私有成员变量主要用map容器将MySQL连接池对象的名字pool_name和MySQL连接池对象映射起来。在db_proxy_server的main函数初始化CDBManager类的单例对象。主要逻辑是读取配置文件来创建不同的MySQL连接池(主库master和从库slave),创建连接池对象后,insert map容器里管理起来。
3.4 为什么分开不同的db redis?

答:方便扩展。

3.5 pool_name的意义?

答:抽象,不必关注redis是否分布式(一台机器或者多台机器都可以实现,我们就不必用操心了)。

4. 线程池

设计初衷:db_proxy_server的主线程(IO线程)用来进行网络IO(收发数据包以及解包逻辑,耗时的查询数据库等操作会包装成task对象,投递到工作线程池里,由线程池的工作线程进行处理)。
具体实现:
CWorkerThread类私有成员变量包含互斥锁,条件变量,线程idx,list<CTask*> m_task_list;对外提供接口有start()用于启动一个线程;Execute()执行任务task。

void CWorkerThread::Execute()
{
    while (true) {
        m_thread_notify.Lock();

        // put wait in while cause there can be spurious wake up (due to signal/ENITR)
        while (m_task_list.empty()) {
            m_thread_notify.Wait();
        }

        CTask* pTask = m_task_list.front();
        m_task_list.pop_front();
        m_thread_notify.Unlock();

        pTask->run();

        delete pTask;

        m_task_cnt++;
        //log("%d have the execute %d task\n", m_thread_idx, m_task_cnt);
    }
}

我们使用的是CThreadPool类对象,CThreadPool类私有成员变量包含线程个数以及工作线程数组。对外接口提供初始化(根据入参工作线程个数动态创建工作线程数组)和销毁工作线程数组操作;接收task(往工作线程池里投递任务task),现在是随机选取一个工作线程的idx来进行投递,其实更好的应该是做负载均衡。

void CThreadPool::AddTask(CTask* pTask)
{
/*
* select a random thread to push task
* we can also select a thread that has less task to do
* but that will scan the whole thread list 
* and use thread lock to get each task size
*/
    uint32_t thread_idx = random() % m_worker_size;
    m_worker_list[thread_idx].PushTask(pTask);
}

void CWorkerThread::PushTask(CTask* pTask)
{
    m_thread_notify.Lock();
    m_task_list.push_back(pTask);
    m_thread_notify.Signal();
    m_thread_notify.Unlock();
}

msg_server

  • 连接login_server成功以后,告诉login_server自己的ip地址、端口号和当前登录的用户数量和可容纳的最大用户数量,这样login_server将来对于一个需要登录的用户,会根据不同的msg_server的负载状态来决定用户到底登录哪个msg_server。
  • 连接route_server成功以后,给route_server发包告诉当前登录在本msg_server上有哪些用户(用户id、用户状态、用户客户端类型)。这样将来A用户给B发聊天消息,msg_server将该聊天消息转给route_server,route_server就知道用户B在哪个msg_server上了,以便将该聊天消息发给B所在的msg_server。
typedef struct {
    uint32_t user_id;
    uint32_t status;
    uint32_t client_type;
} user_stat_t;

void CRouteServConn::OnConfirm()
{
  log("connect to route server success ");
  m_bOpen = true;
  m_connect_time = get_tick_count();
  g_route_server_list[m_serv_idx].reconnect_cnt=MIN_RECONNECT_CNT/2;

  if (g_master_rs_conn == NULL) {
    update_master_route_serv_conn();
  }

  //连接route_server成功以后,给route_server发包告诉当前登录在本msg_server上有哪些
  //用户(用户id、用户状态、用户客户端类型)
  list<user_stat_t> online_user_list;
  CImUserManager::GetInstance()->GetOnlineUserInfo(&online_user_list);
  IM::Server::IMOnlineUserInfo msg;
  for (list<user_stat_t>::iterator it = online_user_list.begin(); 
      it != online_user_list.end(); it++) {
        
    user_stat_t user_stat = *it;
    IM::BaseDefine::ServerUserStat* server_user_stat = 
    msg.add_user_stat_list();
    
    server_user_stat->set_user_id(user_stat.user_id);
    server_user_stat->set_status((::IM::BaseDefine::UserStatType)user_stat.status);
    server_user_stat->set_client_type((::IM::BaseDefine::ClientType)user_stat.client_type);

  }
  CImPdu pdu;
  pdu.SetPBMsg(&msg);
  pdu.SetServiceId(SID_OTHER);
  pdu.SetCommandId(CID_OTHER_ONLINE_USER_INFO);
  SendPdu(&pdu);
}

单聊消息

5. 消息如何封装?如何保证对端完整解析一帧消息?协议格式?

  1. 答:消息封装采用包头(Header)+包体(Body)的格式。包头自定义格式如下代码所示,包体采用protobuf序列化。

PDU包头结构

typedef struct {
    uint32_t    length;        // the whole pdu length
    uint16_t    version;       // pdu version number
    uint16_t    flag;          // not used
    uint16_t    service_id;
    uint16_t    command_id;
    uint16_t    seq_num;       // 包序号
    uint16_t    reversed;      // 保留
} PduHeader_t;

6. 如何保证对端完整解析一帧消息?

  • 采用tcp保证数据传输可靠性
  • 通过包头的 length 字段标记一帧消息的长度
  • 通过service id 和 command id区分不同的命令(比如登录、退出等)
  • 解决数据TCP粘包(包头长度字段)、半包(放入网络库的缓冲区)问题
void CImConn::OnRead()
{
    for (;;) {
        uint32_t free_buf_len = m_in_buf.GetAllocSize() 
                            - m_in_buf.GetWriteOffset();
                            
        if (free_buf_len < READ_BUF_SIZE)
            m_in_buf.Extend(READ_BUF_SIZE);

        int ret = netlib_recv(m_handle, m_in_buf.GetBuffer() 
                + m_in_buf.GetWriteOffset(), READ_BUF_SIZE);
                
        if (ret <= 0)
            break;

        m_recv_bytes += ret;
        m_in_buf.IncWriteOffset(ret);

        m_last_recv_tick = get_tick_count();
    }

    CImPdu* pPdu = NULL;
    try
    {
        while ( ( pPdu = CImPdu::ReadPdu(m_in_buf.GetBuffer(), 
                m_in_buf.GetWriteOffset()) ) )
        {
            uint32_t pdu_len = pPdu->GetLength();
            
            HandlePdu(pPdu);

            m_in_buf.Read(NULL, pdu_len);
            delete pPdu;
            pPdu = NULL;
            //++g_recv_pkt_cnt;
        }
    } catch (CPduException& ex) {
        log("!!!catch exception, sid=%u, cid=%u, 
            err_code=%u, err_msg=%s, close the connection ",
            ex.GetServiceId(), 
            ex.GetCommandId(), 
            ex.GetErrorCode(), ex.GetErrorMsg());
        
        if (pPdu) {
            delete pPdu;
            pPdu = NULL;
        }
        OnClose();
    }
}

7. 单聊消息流转流程

在这里插入图片描述
在这里插入图片描述

图二
答:A发送消息给客户端B,msg_server收到后,直接将数据包转发db_proxy_server,db_proxy_server有一个map来映射信令所对应的处理函数。最后是调用 DB_PROXY::sendMessage。
该函数主要做(如上图二):

  1. 获取会话ID(不存在则创建),并且两个会话ID独立
  2. 获取关系ID(不存在则创建),生成消息ID (msg_id),写入消息到数据库
    注:通过两个人之间的映射关系(关系ID)生成消息ID。如果按照时间排列,两个客户端之间的时间可能不一样,所以按照序号生成消息id。每条消息id唯一;使用redis 生成消息 id。
  3. db_proxy_server 回复 msg_server
  4. CDBServConn::_HandleMsgData 处理消息有三点
  • 首先 ack 客户端A
  • 然后发送到route_server广播
  • 如果有多端登录,也要广播给其他客户端
    msg_server广播给所有的客户端有讲究,会将消息id等封装成 msg_ack_t 结构体塞入m_send_msg_list 发送列表,等到收到对端的CID_MSG_DATA_ACK,再将此msg_ack_t 结构体从发送列表中移除。其实就是业务层的ACK机制,避免丢消息。
    收到确认 CID_MSG_DATA_ACK信令后,移除该确认结构体。

route_server

route_server的作用主要是用户不同msg_server之间消息路由。

  • CID_OTHER_ONLINE_USER_INFO
    这个消息是msg_server连接上route_server后告知route_server自己上面的用户登录情况。route_server处理后,只是简单地记录下每个msg_server上的用户数量和用户id:
void CRouteConn::_HandleOnlineUserInfo(CImPdu* pPdu)
{
    IM::Server::IMOnlineUserInfo msg;
    CHECK_PB_PARSE_MSG(msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength()));

    uint32_t user_count = msg.user_stat_list_size();

    log("HandleOnlineUserInfo, user_cnt=%u ", user_count);

    for (uint32_t i = 0; i < user_count; i++) {
        IM::BaseDefine::ServerUserStat server_user_stat = 
        msg.user_stat_list(i);
        
        _UpdateUserStatus(server_user_stat.user_id(), 
                            server_user_stat.status(), 
                            server_user_stat.client_type());
    }
}
  • CID_OTHER_USER_STATUS_UPDATE
    这个消息是当某个msg_server上有用户上下线时,msg_server会给route_server发送自己最近的用户数量和在线用户id信息,route_server的处理也就是更新下记录的该msg_server上的用户数量和用户id。
void CRouteConn::HandlePdu(CImPdu* pPdu)
{
        switch (pPdu->GetCommandId()) {
        case CID_OTHER_HEARTBEAT:
// do not take any action, heart beat only update m_last_recv_tick
            break;
        case CID_OTHER_ONLINE_USER_INFO:
            _HandleOnlineUserInfo( pPdu );
            break;
        case CID_OTHER_USER_STATUS_UPDATE:
            _HandleUserStatusUpdate( pPdu );
            break;
        case CID_OTHER_ROLE_SET:
            _HandleRoleSet( pPdu );
            break;
        case CID_BUDDY_LIST_USERS_STATUS_REQUEST:
            _HandleUsersStatusRequest( pPdu );
            break;
        case CID_MSG_DATA:
        case CID_SWITCH_P2P_CMD:
        case CID_MSG_READ_NOTIFY:
        case CID_OTHER_SERVER_KICK_USER:
        case CID_GROUP_CHANGE_MEMBER_NOTIFY:
        case CID_FILE_NOTIFY:
        case CID_BUDDY_LIST_REMOVE_SESSION_NOTIFY:
            _BroadcastMsg(pPdu, this);
            break;
        case CID_BUDDY_LIST_SIGN_INFO_CHANGED_NOTIFY:
            _BroadcastMsg(pPdu);
            break;
        
        default:
            log("CRouteConn::HandlePdu, wrong cmd id: %d ", 
                    pPdu->GetCommandId());
            break;
        }
}

这几个消息都是往外广播消息,由于msg_server 可以配置多个,A给B发了一条消息,必须广播在各个msg_server 才能知道B到底在哪个msg_server上。

login_server

登录流程

  1. 客户端发送http请求到login_server login-server挑选负载最轻(登录人数最少的)
    返回给客户端(json)
    首先,程序初始化的时候,会初始化如下功能:
    //1. 在8008端口监听客户端连接

//2. 在8100端口上监听msg_server的连接

//3. 在8080端口上监听客户端http连接
其中连接对象CLoginConn代表着login_server与msg_server之间的连接;而CHttpConn代表着与客户端的http连接。我们先来看CLoginConn对象,其业务代码主要在其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;
        }
}

msg_server连上login_server后会立刻给login_server发一个数据包,该数据包里面含有该msg_server上的用户数量、最大可容纳的用户数量、自己的ip地址和端口号。

void CLoginServConn::OnConfirm()
{
        log("connect to login server success ");
        m_bOpen = true;
        g_login_server_list[m_serv_idx].reconnect_cnt 
                        = MIN_RECONNECT_CNT / 2;

        uint32_t cur_conn_cnt = 0;
        uint32_t shop_user_cnt = 0;
    
    //连接login_server成功以后,告诉login_server自己的ip地址、端口号
    //和当前登录的用户数量和可容纳的最大用户数量
    list<user_conn_t> user_conn_list;
    CImUserManager::GetInstance()
            ->GetUserConnCnt(&user_conn_list, cur_conn_cnt);
        char hostname[256] = {0};
        gethostname(hostname, 256);
    IM::Server::IMMsgServInfo msg;
    msg.set_ip1(g_msg_server_ip_addr1);
    msg.set_ip2(g_msg_server_ip_addr2);
    msg.set_port(g_msg_server_port);
    msg.set_max_conn_cnt(g_max_conn_cnt);
    msg.set_cur_conn_cnt(cur_conn_cnt);
    msg.set_host_name(hostname);
    CImPdu pdu;
    pdu.SetPBMsg(&msg);
    pdu.SetServiceId(SID_OTHER);
    pdu.SetCommandId(CID_OTHER_MSG_SERV_INFO);
    SendPdu(&pdu);
}
  • CID_OTHER_MSG_SERV_INFO
    我们来看下login_server如何处理这个命令的:
void CLoginConn::_HandleMsgServInfo(CImPdu* pPdu)
{
    msg_serv_info_t* pMsgServInfo = new msg_serv_info_t;
    IM::Server::IMMsgServInfo msg;
    msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength());
    
    pMsgServInfo->ip_addr1 = msg.ip1();
    pMsgServInfo->ip_addr2 = msg.ip2();
    pMsgServInfo->port = msg.port();
    pMsgServInfo->max_conn_cnt = msg.max_conn_cnt();
    pMsgServInfo->cur_conn_cnt = msg.cur_conn_cnt();
    pMsgServInfo->hostname = msg.host_name();
    g_msg_serv_info.insert(make_pair(m_handle, pMsgServInfo));

    g_total_online_user_cnt += pMsgServInfo->cur_conn_cnt;

    log("MsgServInfo, ip_addr1=%s, ip_addr2=%s, port=%d, 
        max_conn_cnt=%d, cur_conn_cnt=%d, "\
                "hostname: %s. ",
            pMsgServInfo->ip_addr1.c_str(), 
            pMsgServInfo->ip_addr2.c_str(), 
            pMsgServInfo->port,pMsgServInfo->max_conn_cnt,
            pMsgServInfo->cur_conn_cnt, 
            pMsgServInfo->hostname.c_str());
}

其实所做的工作无非就是记录下的该msg_server上的ip、端口号、在线用户数量和最大可容纳用户数量等信息而已。存在一个全局map里面:

map<uint32_t, msg_serv_info_t*> g_msg_serv_info;
typedef struct  {
    string        ip_addr1;        // 电信IP
    string        ip_addr2;        // 网通IP
    uint16_t      port;
    uint32_t      max_conn_cnt;
    uint32_t      cur_conn_cnt;
    string        hostname;        // 消息服务器的主机名
} msg_serv_info_t;
  • CID_OTHER_USER_CNT_UPDATE
    是当msg_server上的用户上线或下线时,msg_server给login_server发该类型的命令号,让login_server更新保存的msg_server的上的在线用户数量:
void CLoginConn::_HandleUserCntUpdate(CImPdu* pPdu)
{
    map<uint32_t, msg_serv_info_t*>::iterator it = g_msg_serv_info.find(m_handle);
    if (it != g_msg_serv_info.end()) {
        msg_serv_info_t* pMsgServInfo = it->second;
        IM::Server::IMUserCntUpdate msg;
        msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength());

                uint32_t action = msg.user_action();
                if (action == USER_CNT_INC) {
                        pMsgServInfo->cur_conn_cnt++;
                        g_total_online_user_cnt++;
                } else {
                        pMsgServInfo->cur_conn_cnt--;
                        g_total_online_user_cnt--;
                }

        log("%s:%d, cur_cnt=%u, total_cnt=%u ", pMsgServInfo->hostname.c_str(),
        pMsgServInfo->port, pMsgServInfo->cur_conn_cnt, g_total_online_user_cnt);
    }
}

http连接

接着说login_server与客户端的http连接处理,这个连接收取数据和解包是直接在CHttpConn的OnRead函数里面处理的:

void CHttpConn::OnRead()
{
    for (;;)
    {
        uint32_t free_buf_len = m_in_buf.GetAllocSize() \
            - m_in_buf.GetWriteOffset();
            
        if (free_buf_len < READ_BUF_SIZE + 1)
            m_in_buf.Extend(READ_BUF_SIZE + 1);

        int ret = netlib_recv(m_sock_handle, \
        m_in_buf.GetBuffer() + m_in_buf.GetWriteOffset(), \
                                READ_BUF_SIZE);

        if (ret <= 0)
            break;

        m_in_buf.IncWriteOffset(ret);

        m_last_recv_tick = get_tick_count();
    }

    // 每次请求对应一个HTTP连接,所以读完数据后,
    // 不用在同一个连接里面准备读取下个请求
    char* in_buf = (char*)m_in_buf.GetBuffer();
    uint32_t buf_len = m_in_buf.GetWriteOffset();
    in_buf[buf_len] = '\0';
    
    // 如果buf_len 过长可能是受到攻击,则断开连接
    // 正常的url最大长度为2048,我们接受的所有数据长度不得大于1K
    if(buf_len > 1024)
    {
        log("get too much data:%s ", in_buf);
        Close();
        return;
    }

//log("OnRead, buf_len=%u, conn_handle=%u\n", buf_len, m_conn_handle); // for debug

        
    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();
        }
    }
}
如果用户发送的http请求的地址形式是http://192.168.226.128:8080/msg_server,即路径是/msg_server,则调用_HandleMsgServRequest()函数处理:
void CHttpConn::_HandleMsgServRequest(string& url, string& post_data)
{
    msg_serv_info_t* pMsgServInfo;
    uint32_t min_user_cnt = (uint32_t)-1;
    map<uint32_t, msg_serv_info_t*>::iterator it_min_conn = g_msg_serv_info.end();
    map<uint32_t, msg_serv_info_t*>::iterator it;
    if(g_msg_serv_info.size() <= 0)
    {
        Json::Value value;
        value["code"] = 1;
        value["msg"] = "没有msg_server";
        string strContent = value.toStyledString();
        char* szContent = new char[HTTP_RESPONSE_HTML_MAX];
        snprintf(szContent, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, strContent.length(), strContent.c_str());
        Send((void*)szContent, strlen(szContent));
        delete [] szContent;
        return ;
    }
    
    for (it = g_msg_serv_info.begin() ; it != g_msg_serv_info.end(); it++) {
        pMsgServInfo = it->second;
        if ( (pMsgServInfo->cur_conn_cnt < pMsgServInfo->max_conn_cnt) &&
            (pMsgServInfo->cur_conn_cnt < min_user_cnt)) {
            it_min_conn = it;
            min_user_cnt = pMsgServInfo->cur_conn_cnt;
        }
    }
    
    if (it_min_conn == g_msg_serv_info.end()) {
        log("All TCP MsgServer are full ");
        Json::Value value;
        value["code"] = 2;
        value["msg"] = "负载过高";
        string strContent = value.toStyledString();
        char* szContent = new char[HTTP_RESPONSE_HTML_MAX];
        snprintf(szContent, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, strContent.length(), strContent.c_str());
        Send((void*)szContent, strlen(szContent));
        delete [] szContent;
        return;
    } else {
        Json::Value value;
        value["code"] = 0;
        value["msg"] = "";
        if(pIpParser->isTelcome(GetPeerIP()))
        {
            value["priorIP"] = string(it_min_conn->second->ip_addr1);
            value["backupIP"] = string(it_min_conn->second->ip_addr2);
            value["msfsPrior"] = strMsfsUrl;
            value["msfsBackup"] = strMsfsUrl;
        }
        else
        {
            value["priorIP"] = string(it_min_conn->second->ip_addr2);
            value["backupIP"] = string(it_min_conn->second->ip_addr1);
            value["msfsPrior"] = strMsfsUrl;
            value["msfsBackup"] = strMsfsUrl;
        }
        value["discovery"] = strDiscovery;
        value["port"] = int2string(it_min_conn->second->port);
        string strContent = value.toStyledString();
        char* szContent = new char[HTTP_RESPONSE_HTML_MAX];
        uint32_t nLen = strContent.length();
        snprintf(szContent, HTTP_RESPONSE_HTML_MAX, HTTP_RESPONSE_HTML, nLen, strContent.c_str());
        Send((void*)szContent, strlen(szContent));
        delete [] szContent;
        return;
    }
}
其实就是根据记录的msg_server的负载情况,返回一个可用的msg_server ip和端口给客户端,这是一个json格式:
{
    "backupIP" : "localhost",
    "code" : 0,
    "discovery" : "http://192.168.226.128</span>/api/discovery",
    "msfsBackup" : "http://127.0.0.1:8700/",
    "msfsPrior" : "http://127.0.0.1:8700/",
    "msg" : "",
    "port" : "8000",
    "priorIP" : "localhost"
 }

里面含有msg_server和聊天图片存放的服务器地址(msfsPrior)字段。这样客户端可以拿着这个地址去登录msg_server和图片服务器了。
发出去这个json之后会调用OnWriteComplete()函数,这个函数立刻关闭该http连接,也就是说这个与客户端的http连接是短连接:
void CHttpConn::OnWriteComlete()
{
log("write complete ");
Close();
}
难点踢人逻辑(kick掉相同类型的连接)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值