/* 登录流程: client ---> msg_server ---> db_proxy_server route_server 【client】 ---> 【CMsgConn --> (msg_server) --> CDBServConn】 ---> 【CProxyConn ---> (db_proxy_server)】 | | | | | | | | | | | IM::Login::IMLoginReq pdu.CID = CID_LOGIN_REQ_USERLOGIN; CMsgConn::HandlePdu() case CID_LOGIN_REQ_USERLOGIN: _HandleLoginRequest(pPdu); IM::Server::IMValidateReq | pdu.CID = CID_OTHER_VALIDATE_REQ; -----------------------> | CProxyConn::HandlePdu() DB_PROXY::doLogin(); | IM::Server::IMValidateRsp | <--------------- pdu.CID = CID_OTHER_VALIDATE_RSP; CDBServConn::HandlePdu() case CID_OTHER_VALIDATE_RSP: _HandleValidateResponse(pPdu); if(UserValidateFail) | IM::Login::IMLoginRes | <------------------------------------------- pdu.CID = CID_LOGIN_RES_USERLOGIN; if(UserValidateSucc) IM::Server::IMServerKickUser | pdu.CID = CID_OTHER_SERVER_KICK_USER ----------------------------------------> | */
void CMsgConn::_HandleLoginRequest(CImPdu* pPdu) { //step 1. 从所有的db_proxy_server列表中找出一个可用的连接: CDBServConn* pDbConn = get_db_serv_conn_for_login(); if(!pDbConn) { result = IM::BaseDefine::REFUSE_REASON_NO_DB_SERVER; reslt_string = "服务器异常"; } if(result) { //如果服务器异常,例如没有可用的 msg_server到proxy_server的连接,则直接给客户端返回异常 IM::Login::IMLoginRes msg; SendPdu(&pdu); Close(); return; } //step 2. 正常找到连接,对收到的msg反序列化,准备给proxy_server发登录请求消息: IM::Login::IMLoginReq msg; msg.ParseFromArray(pdu->GetBodyData(), pdu->GetBodyLength()); //反序列化 m_login_name = msg.user_name(); //CMsgConn对应一个登录的端,CMsgConn.m_login_name 保存登录的用户名。(一个 msg_server 上会同时维护无数个 CMsgConn) string password = msg.password(); m_online_status = IM::BaseDefine::USER_STATUS_ONLINE; //连接状态为在线 //step 3. CImUser: 根据用户名去找CImUser实例 CImUser* pImUser = CImUserManager::getInstance()->GetImUserByLoginName(m_login_name); if(!pImUser) { //如果没有找到,则是首个端登录,新创建一个实例: pImUser = new CImUser(m_login_name); CImUserManager::getInstance()->AddImUserByLoginName(m_login_name, pImUser); //将新建的CImUser实例加入到CImUserManager中:map<string(user_name), CImUser*> ImUserMapByName_t; } pImUser->AddUnValidateMsgConn(this); //加入到 m_unvalidate_conn_set 未鉴权set中 //step 4. 构造消息发给 db_proxy_server: CDBAttachData attach_data(ATTACH_TYPE_HANDLE, m_handle, 0); //附加上connfd IM::Server::IMValidateReq msg2; msg2.set_user_name(); msg2.set_password(); msg2.set_attach_data(); CImPdu pdu; pdu.SetPBMsg(&msg2); pdu.SetServiceId(SID_OTHER); pdu.SetCommandId(CID_OTHER_VALIDATE_REQ); pdu.SetSeqNum(); pDbConn->SendPdu(&pdu); } //关键点: CImUserManager ---> CImUser ---> CMsgConn : //msg_server上维护一个 CImUserManager, 用于维护此msg_server上登录的所有用户(CImUser), //CImUserManager的实现也比较简单,数据成员是两个map,分别对应 user_id 和 user_name到 CImUser实例指针的索引,成员函数是向其中添加成员和删除成员 //每个 CImUser 对应一个登录用户,CMsgConn 对应一个端的登录,CImUser和CMsgConn是 1:n 的关系 //对应的包含关系如下: // CImUserManager --> CImUser --> CMsgConn class CImUserManager { private: ImUserMap_t m_im_user_map; //map<uint32_t(user_id), CImUser*> ImUserMapByName_t m_im_user_map_by_name; //map<string(login_name), CImUser*> static CImUserManager* s_manager; }; class CImUser { private: uint32_t m_user_id; string m_login_name; uint32_t m_pc_login_status; bool m_bValidate; map<uint32_t, CMsgConn*> m_conn_map; //<connfd, CMsgConn*> set<CMsgConn*> m_unvalidate_conn_set; //刚刚连接到msg_server上,还未经过proxy_server密码验证的CMsgConn。等验证通过后就会转入到上面的m_conn_map中。 //我认为这里用map代替set会更好,因为后面的是在for循环遍历整个set找到匹配的CMsgConn,不如直接connfd--->CMsgConn索引更快 }; class CMsgConn : public CImConn { private: string m_login_name; //用于标识本连接属于哪个user uint32_t m_user_id; uint32_t m_client_type; //端类型(mobile/pc) uint32_t m_online_status;//连接状态(OFFLINE/ONLINE) //属于 CImConn中的: net_handle_t m_handle; }; static CDBServConn* get_db_serv_conn_for_login(uint32_t start_pos, uint32_t stop_pos) { uint32_t i = 0; CDBServConn* pDbConn = NULL; //determine if there is a valid DB server connection: for(i = start_pos; i < stop_pos; i++) { pDbConn = (CDBServConn*)g_db_server_list[i].serv_conn; //g_db_server_list 是从原始conf配置文件中界消息出来的所有db_server列表 if(pDbConn && pDbConn->IsOpen()) { break; } } if(i == stop_pos) { return NULL; } while(true) { int i = rand() % (stop_pos - start_pos) + start_pos; pDbConn = (CDBServConn*)g_db_server_list[i].serv_conn; if(pDbConn && pDbConn->IsOpen()) { break; } } return pDbConn; }
namespace DB_PROXY { void doLogin(CImPdu* pPdu, uint32_t conn_uuid) { //conn_uuid是一个保持单调递增的uint32_t整型值,在CProxyConn新建时赋值 m_uuid CImPdu *pPduResp = new CImPdu; //用于给msg_server回复消息的pdu IM::Server::IMValidateRsp msgResp; //用于给msg_server回复消息的pb //step 0. 反序列化msg: IM::Server::IMValidateReq msg; if(!msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength())) { //如果反序列化失败,则直接回复错误: msgResp.set_result_code(2); msgResp.ser_result_string("服务器内部错误"); } else { //正常情况,对收到的msg_server的pb消息反序列化成功后: //step 1. 检验是否已在30分钟内连续密码错误10次,如是,则禁止登录,直接给msg_server回复异常响应并return返回: if(!VerifyFailTimesIn30min()) { return ; } //step 2. 进行登录验证: IM::BaseDefine::UserInfo cUser; //如果检验成功,则将用户的各个信息存储在cUser中,返回给msg_server int ret = g_loginStrategy.doLogin(msg.user_name(), msg.password(), cUser); if(ret == fail) { //如果验证失败,则记录一次: lsErrorTime.push_front(time(NULL)); msgResp.set_result_code(1); msgResp.ser_result_string("用户名/密码错误"); } else { //登录验证成功: IM::BaseDefine::UserInfo* pUser = msgResp.mutable_user_info(); //填充pdu中的pb内容 pUser->set_user_id(); pUser->set_user_name(); pUser->set_dapartment_id(); ...... msgResp->set_result_code(0); msgResp->set_result_string("成功"); lsErrorTime.clear(); //已经登录成功了,就清空失败记录 } } //step 3. 无论成功失败,统一给msg_server回复结果:(注意这里只是加入到发送列表) pPduResp->SetPBMsg(&msgResp); pPduResp->SetSeqNum(pPdu->GetSeqNum()); pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER); pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP); CProxyConn::AddResponsePdu(conn_uuid, pPduResp); } }; //校验是否在30分钟内失败了10次,如是,则禁止登录,即使这次带的密码是正确的: int VerifyFailTimesIn30min() { uint32_t tmNow = time(NULL); auto itTime = lsErrorTime.begin(); //list<uint32_t>& lsErrorTime = g_hmLimits[msg.user_name()]; //g_hmLimits = unordered_map<string user_name, list<uint32_t> error_time>; 用于记录该user_names上历次的密码失败时刻 for(; itTime != lsErrorTime.end(); ++itTime) { if(tmNow - *itTime > 30*60) { break; } } if(itTime != lsErrorTime.end()) { lsErrorTime.erase(itTime, lsErrorTime.end()); //lsErrorTime链表中存储的格式是 (60,50,40,30,20,10...) 新元素会push_front插入到链表头部,删除时则删除后面的老时间 } if(lsErrorTime.size() > 30) { msgResp.set_result_code(6); msgResp.ser_result_string("用户名/密码错误次数过多"); pPduResp->SetPBMsg(msgResp); pPduResp->SetSeqNum(); pPduResp->SetServiceId(IM::BaseDefine::SID_OTHER); pPduResp->SetCommandId(IM::BaseDefine::CID_OTHER_VALIDATE_RSP); CProxyConn::AddResponsePdu(conn_uuid, pPduResp); return 1; //fail } return 0; //succ } //查询MySQL数据库,进行用户名密码验证: bool CInterLoginStrategy::doLogin(const std::string &strName, const std::string &strPass, IM::BaseDefine::UserInfo& user) { CDBConn* pDbConn = CDBManager::getInstance()->GetDBConn("teamtalk_slave"); if(pDbConn) { string strSql = "select * from IMUser where name = '" + strName + "' and status = 0"; CResultSet* pResultSet = pDbConn->ExcueteQuery(strSql.c_str); if(pResultSet) { while(pResultSet->Next()) { } if(strOutPass == strResult) { user.set...... //user是引用&类型,密码验证通过后,把查询到的用户信息都拷贝出去 } } } } //ResponseList中的消息的发送: //关键点在于 db_proxy_server 上会维护一个map,保存 uuid 到 CProxyConn 之间的索引,这样需要从ResponseList中统一发送消息时,就可以通过uuid知道发送给哪个msg_server(一个CProxyConn对应一个连接的msg_server) //另外注意CProxyConn中的两个static静态成员的写法,所有CProxyConn实例都共享同一个 s_uuid_allocator 和 s_response_list tpepdef unordered_map<uint32_t, CImConn*> UserMap_t; //map<uuid, CProxyConn*> static UserMap_t g_uuid_conn_map; class CProxyConn : public CImConn { public: CProxyConn(); static void AddResponsePdu(uint32_t conn_uuid, CImPdu* pPdu); static void SendResponsePduList(); private: static uint32_t s_uuid_allocator; //注意s_uuid_allocator是static静态对象,所有CProxyConn实例共享一个,保证单调递增 uint32_t m_uuid; //这个才是每个CProxyConn对应的专属 conn_uuid static list<ResponsePdu_t*> s_response_pdu_list; //同样是static静态对象,所有CProxyConn的Response消息都会插入到这里list链表中,因此会产生互斥,需要对其加锁访问 static CLock s_list_clock; }; CProxyConn::CProxyConn() { m_uuid = ++CProxyConn::s_uuid_allocator; if(m_uuid == 0) { m_uuid = ++CProxyConn::s_uuid_allocator; } g_uuid_conn_map.insert( make_pair(m_uuid, this) ); //新建的CProxyConn及其对应的m_uuid 插入到map中 } void CProxyConn::AddResponsePdu(uint32_t conn_uuid, CImPdu* pPdu) { ResponsePdu_t* pPduResp = new ResponsePdu_t(conn_uuid, pPdu); s_list_clock.lock(); s_response_pdu_list.push_back(pPduResp); s_list_clock.unlock(); } void CProxyConn::SendResponsePduList() { s_list_clock.lock(); while(!s_response_pdu_list.empty()) { ResponsePdu_t* pResp = s_response_list.font(); s_response_list.pop_font(); s_list_clock.unlock(); CProxyConn* pConn = get_proxy_conn_by_uuid(pResp->conn_uuid); //g_uuid_conn_map.find(); if(pConn) { if(pResp->pPdu) { pConn->SendPdu(pResp->pPdu); } } s_response_pdu_list.lock(); } s_list_clock.unlock(); }
void CDBServConn::_HandleValidateResponse(CImPdu* pPdu) { //step 0. 对收到的返回结果消息反序列化: IM::Server::IMValidateRsp msg; msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength()); CImUser* pImUser = CImUserManager::GetInstance()->GetImUserByLoginName(msg.user_name()); if(pImUser) { CMsgConn* pConn = pImUser->GetUnvalidateMsgConn(msg.attach_data().GetHandle()); //根据attach_data中保存的用户的connfd,从CIMUser::m_unvalidate_conn_set中找到未鉴权的CMsgConn } //step 1. 检查密码校验的结果是成功还是失败: if(result != 0) { //step 1.1 : 如果密码校验失败,则只需要返回结果给客户端,无需做其他处理 result = IM::BaseDefine::REFUSE_REASON_DB_VALIDATE_FAILED; IM::Login::IMLoginRes msg4; msg4.set_server_time(); msg4.set_result_code(); msg4.set_result_string(); CImPdu pdu3; pdu3.SetPBMsg(&msg4); pdu3.SetServiceId(SID_LOGIN); pdu3.SetCommandId(CID_LOGIN_RES_USERLOGIN); pdu3.SetSeqNum(); pMsgConn->SendPdu(pdu3); pMsgConn->Close(); //注意发送会失败的校验结果后要close关闭连接,因为后面不会再有聊天报文上来 } else { //result = 0, 说明密码校验成功 //step 1.2 : 如果登录成功,则需要发送route_server去踢掉此用户的其他同类型的登录端: CImUserManager::GetInstance()->AddImUserById(user_id, pUser); //维护user_id到CImUser的映射关系 //step 2. 在本msg_server上踢掉同类型登录端 + 通知route_server踢掉其他msg_server上的同类型登录端: pUser->KickOutSameClientType(pMsgConn->GetClientType(), IM::BaseDefine::KICK_REASON_DUPLICATE_USER, pMsgConn); CRouteServConn* pRouteConn = get_route_serv_conn(); if(pRouteConn) { IM::Server::IMServerKickUser msg2; msg2.set_user_id(); CImPdu pdu; pdu.SetCommandId(CID_OTHER_SERVER_KICK_USER); pRouteConn->SendPdu(&pdu); } //step 3. 通知所有 route_server 更新用户的登录状态: pMsgConn->SendUserStatusUpdate(IM::BaseDefine::USER_STATUS_ONLINE); pUser->ValidateMsgConn(); //将这个CMsgConn 从CImUser的未完成鉴权set集合中转移到 已完成鉴权map中 //step 4. 给客户端回复成功登录响应: IM::Login::IMLoginRes msg3; CImPdu pdu2; pdu2.SetCommandId(CID_LOGIN_RES_USERLOGIN); pMsgConn->SendPdu(&pdu2); } }
功能:用户表
字段说明:
id : 用户ID,自增,唯一 (主键) password : 密码,规则: ( md5(password) + salt ) salt : 密码混淆 sex : 性别 name : 用户名 domain : 拼音 nick : 昵称 phone : 电话号码 email : 邮箱 avatar : 头像 departId : 部门 status : 状态 create : 创建时间 update : 更新时间