整体梳理
各个服务程序的作用描述
- 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响应处理流程
- 初始化epoll(I/O线程)+线程池(工作线程)
- 数据入口 reactor CProxyConn:: HandlePduBuf(I/O线程)
- 任务封装,把任务放入线程池(I/O线程)
- 执行任务(工作线程)
- 把要回应的数据放入回复列表CProxyConn::SendResponsePdulist(工作线程)
- 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连接池
- 第一步我们先创建MySQL连接池类(CDBPool类),该类的私有成员变量主要有存放MySQL的连接list容器;每个MySQL连接池的对象的名字(m_pool_name);
- 第二步我们创建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. 消息如何封装?如何保证对端完整解析一帧消息?协议格式?
- 答:消息封装采用包头(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。
该函数主要做(如上图二):
- 获取会话ID(不存在则创建),并且两个会话ID独立
- 获取关系ID(不存在则创建),生成消息ID (msg_id),写入消息到数据库
注:通过两个人之间的映射关系(关系ID)生成消息ID。如果按照时间排列,两个客户端之间的时间可能不一样,所以按照序号生成消息id。每条消息id唯一;使用redis 生成消息 id。 - db_proxy_server 回复 msg_server
- 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
登录流程
- 客户端发送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掉相同类型的连接)