如何在服务端新增一个协议

1、杂谈

最近实在是太忙太忙有很多同学来问我问题,我也没办法很仔细的去回答,在对开源群的关注中也相对减少了,实在是抱歉。

TeamTalk虽然开源有一段时间了,但是很多人似乎对整个架构或者说细节的掌握上并没有做到很好。最近在群里以及很多人私下问我想在TT上做扩展,但是不知道如何去下手。基于以上考虑,我打算写一篇博客介绍如何在服务端
新增一个协议,并处理,我会尽量详细的去描述整个处理过程。

很多人私下跟我聊天,希望我能够写更多的博客来介绍TT,其实我自己也希望这么去做,我也会尽量去做。很多人给我建议,让我以TT的名义发起类似“众筹”的东西,这个我没办法去做,首先TT是蘑菇街开源的项目,而不是我个人的,这样做不是很好,其次管理资金不是我所擅长的事情。

随着微博打赏,微信公众号打赏等功能,也有同学建议我在博客中增加打赏的地方,经过再三考虑,我决定在博客中去尝试尝试,并在后续的博客更新中,都会在最后增加一个打赏的微信二维码。

好,言归正传。

2、如何增加一个协议

很多人问我如何增加一个处理协议,我先大致讲下整个过程,在后面针对每一步进行详细的讲解,本次就以群里一个群友问得如何增加修改密码的协议为例。

因为新版TT是基于PB处理的,所以,

1、我们要在pb文件中增加相应的命令号,协议定义。

2、重新生成协议文件。

3、在服务端相应的地方增加逻辑处理。

4、客户端如何处理。

下面我们就根据上面的描述来进行讲解,由于时间太晚,我并没有编译过,是根据自己的经验来写的,如果你在尝试的过程中出现错误,可以很容易的根据错误提示来排查。

3、修改PB文件

首先我们得为修改密码定义一个唯一的命令号,我们暂且将修改密码的命令放到Login模块中。那我们在pb目录下找到IM.BaseDefine.proto文件,并打开,在19行找到LoginCmdID的枚举定义:

// command id for login
enum LoginCmdID{
    CID_LOGIN_REQ_MSGSERVER         = 0x0101;
    CID_LOGIN_RES_MSGSERVER         = 0x0102;
    CID_LOGIN_REQ_USERLOGIN         = 0x0103;
    CID_LOGIN_RES_USERLOGIN         = 0x0104;
    CID_LOGIN_REQ_LOGINOUT          = 0x0105;
    CID_LOGIN_RES_LOGINOUT          = 0x0106;
    CID_LOGIN_KICK_USER             = 0x0107;
    CID_LOGIN_REQ_DEVICETOKEN       = 0x0108;
    CID_LOGIN_RES_DEVICETOKEN       = 0x0109;
    CID_LOGIN_REQ_KICKPCCLIENT      = 0x010a;
    CID_LOGIN_RES_KICKPCCLIENT      = 0x010b;
}

该枚举定义了各种与登录相关的命令号。我们对它进行修改,增加两个命令号,分别表示修改密码请求协议与修改密码响应协议,命令号分别为:0x010c, 0x010d,修改后如下:

// command id for login
enum LoginCmdID{
    CID_LOGIN_REQ_MSGSERVER         = 0x0101;
    CID_LOGIN_RES_MSGSERVER         = 0x0102;
    CID_LOGIN_REQ_USERLOGIN         = 0x0103;
    CID_LOGIN_RES_USERLOGIN         = 0x0104;
    CID_LOGIN_REQ_LOGINOUT          = 0x0105;
    CID_LOGIN_RES_LOGINOUT          = 0x0106;
    CID_LOGIN_KICK_USER             = 0x0107;
    CID_LOGIN_REQ_DEVICETOKEN       = 0x0108;
    CID_LOGIN_RES_DEVICETOKEN       = 0x0109;
    CID_LOGIN_REQ_KICKPCCLIENT      = 0x010a;
    CID_LOGIN_RES_KICKPCCLIENT      = 0x010b;
    CID_LOGIN_REQ_MODIFY_PASS       = 0x010c;
    CID_LOGIN_RES_MODIFY_PASS       = 0x010d;
}

那接下来,我们我们需要考虑的是如何设定修改密码的协议,从我个人来考虑的话,修改密码的请求至少需要包含以下三个字段:
1、用户ID,该字段用来查找是修改哪个用户。

2、原始密码,这个用来验证这个用户是否具有修改密码的权限,相当于再一次做校验。

3、新密码,这个标志用户想用的新密码。

OK,上面我们已经考虑好了修改密码请求所需的各个字段,那么我们就用pb的“语法”来描述这么一个协议,我们打开pb/IM.Login.proto文件,在文件的最后增加如下代码:

message IMModifyPassReq {
    //cmd id:       0x010c
    required uint32 user_id = 1;
    required string old_pass = 2;
    required string new_pass = 3;
    optional bytes attach_data = 20;
}

如上所示,我们就定义好了一个修改密码请求协议,在这里我们做个约定,密码是经过md5加密后的数据。

既然后修改请求,就一定要有个修改密码响应协议,来告诉客户端修改的结果。我们同样需要思考一个返回协议该包含哪些信息,经过考虑我在pb/IM.Login.proto文件中增加如下协议:

message IMModifyPassRes {
    //cmd id:       0x010d
    required uint32 user_id = 1;
    required uint32 status = 2;
    optional bytes attach_data = 20;
}

user_id是请求中的user_id,status标识修改的结果是成功还是失败。

经过以上的修改,我们协议已经定义好了。

4、重新生成协议文件

现在我们已经修改完成了新的协议,下面就要根据pb文件生成协议文件代码了。过程比较简单,我们上面总共修改了pb/IM.BaseDefine.proto、pb/IM.Login.proto两个文件,我们可以用如下命令生成协议代码:

cd pb

protoc IM.BaseDefine.proto IM.Login.proto --cpp_out=./

执行完以上命令,如果不出意外,当前目录下就会生成四个文件:

IM.BaseDefine.pb.h
IM.Login.pb.h
IM.BaseDefine.pb.cc
IM.Login.pb.cc

下面我们需要将新生成的代码文件拷贝到我们的代码目录里面。

cp IM.BaseDefine.pb.h IM.BaseDefine.pb.cc IM.Login.pb.h IM.Login.pb.cc ../server/src/base/pb/protocol/

这样我们将新协议代码已经拷贝到我们服务端代码目录中了。

其实为了简便,我们已经在pb的目录下面已经有两个脚本文件用来方便大家生成协议跟拷贝代码文件,分别在pb目录下执行以下两个shell脚本就行了:

sh create.sh
sh sync.sh

执行这两个脚本后就会自动生成一份最新的协议代码,并拷贝到服务端相应的目录中。

5、服务端逻辑的修改

经过以上的步骤,我们已经有了修改密码的协议了,剩下的要做的就是就是在服务端增加相应的处理逻辑。

5.1 对msg_server的修改。

客户端直连的是msg_server,大部分时间都是与msg_server交互的。所以大部分协议都是在于msg_server交互。我们修改密码的协议也是需要msg_server去处理的。

在msg_server的代码中,处理与客户端的链接是在MsgConn.cpp中,我们首先来看下270行中得HandlePdu函数。其整体结构如下:

void CMsgConn::HandlePdu(CImPdu* pPdu)
{
    //检查是否已登录过,或者是否是登陆包
    if(xxx) {
        throw a exception;
        return;
    }
    
    switch(pPdu->GetCommandId()) {
        case xxx1:
            _HandleXxx1(pPdu);
            break;
        case xxx2:
            _HandleXxx2(pPdu);
            break;
        ...
        default:
            log("");
            break;
    }
}

这段逻辑主要就是对客户端的各个协议进行处理,根据不同的协议命令来调用不同的逻辑处理函数。那我们增加一条处理修改密码请求协议。

在相应的地方(根据个人习惯,选择一个合适的地方增加,我个人习惯于将一类协议放在一块处理),我们在294行之后,即:

case CID_LOGIN_REQ_KICKPCCLIENT:
    _HandleKickPCClient(pPdu);
    break;

增加.增加后如下所示,为了排版,我省略部分代码,只保留了修改前后的代码。

void CMsgConn::HandlePdu(CImPdu* pPdu)
{
    // request authorization check
    if (pPdu->GetCommandId() != CID_LOGIN_REQ_USERLOGIN && !IsOpen() && IsKickOff()) {
        log("HandlePdu, wrong msg. ");
        throw CPduException(pPdu->GetServiceId(), pPdu->GetCommandId(), ERROR_CODE_WRONG_SERVICE_ID, "HandlePdu error, user not login. ");
        return;
    }

    switch (pPdu->GetCommandId()) {
        case CID_OTHER_HEARTBEAT:
            _HandleHeartBeat(pPdu);
            break;
        ...
        
        case CID_LOGIN_REQ_KICKPCCLIENT:
            _HandleKickPCClient(pPdu);
            break;
        case 
        
        case CID_LOGIN_REQ_MODIFY_PASS:
            _HandleModifyPassRequest(pPdu);
            break;
        case CID_MSG_DATA:
            _HandleClientMsgData(pPdu);
            break;
        ...
        
        default:
            log("wrong msg, cmd id=%d, user id=%u. ", pPdu->GetCommandId(), GetUserId());
            break;
    }
}

我们看到这里调用了一个函数:

_HandleKickPCClient(pPdu);

这个函数就是msg_server里面处理修改密码逻辑的函数了,我们需要自己添加。在MsgConn.h的private中增加函数的声明:

void _HandleModifyPassRequest(CImPdu* pPdu);

在MsgConn.cpp中,我们去实现该函数。在实现之前,我们先思考下,修改密码中,msg_server有什么工作需要做。msg_server在修改密码的过程中,基本没有什么实质性的逻辑需要处理,所要做的,就是将该协议转发给db_proxy_server去处理,当db_proxy_server处理完成返回之后,msg_server将处理结果返回给client,在该过程中,msg_server基本只做了一个协议转发的作用。在这个过程中,由于msg_server与db_proxy_server之间的通信是异步的,当db_proxy_server处理完成返回给msg_server的时候,msg_server如何知道这个协议是需要返回给哪个客户端会是一个问题。所以msg_server在转发协议的时候,会将处理该客户端连接的handle打包成CDbAttachData放在协议的最后。具体过程见代码:

1   void CMsgConn::_HandleModifyPassRequest(CImPdu *pPdu)
2   {
3       IM::Login::IMModifyPassReq msg;
4       CHECK_PB_PARSE_MSG(msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength()));
5       log("_HandleModifyPassRequest. user_id=%u.", msg.user_id());
6       CDBServConn* pDBConn = get_db_serv_conn();
7       if (pDBConn) {
8           CDbAttachData attach(ATTACH_TYPE_HANDLE, m_handle, 0);
9           msg.set_attach_data(attach.GetBuffer(), attach.GetLength());
10          pPdu->SetPBMsg(&msg);
11          pDBConn->SendPdu(pPdu);
12      }
13  }

对以上代码简单的说明下.
第3行:我们定义了一个ModifyPass的协议类。
第4行:解析协议,并对协议做检查。
第6行:获取一个db连接
第7行:判断连接是否为空
第8行:把与客户端的链接句柄打包成CDbAttachData。
第9行:将打包好的CDbAttachData 放到pb协议中.
第10行:重新将pb协议放到CImPdu协议中。
第11行:将pdu发送到db_proxy_server中。

至此,我们的msg_server已经处理了转发的请求了,接下来,我们需要去db_proxy_server中做处理了。

5.2 对db_proxy_server做修改

当msg_server把请求转发到db_proxy_server后,db_proxy_server就要做接下来的工作了。那是怎样的一个流程呢,下面我们做个简单的介绍。

db_proxy_server处理链接的类是CProxyConn(在文件ProxyConn.h/ProxyConn.cpp中),在OnRead之后,如果达到了一个完整的协议包,会调用HandlePduBuf来对协议进行分发处理。在HandlePduBuf中,会通过s_handler_map根据命令号来调用GetHandler获取处理函数。然后将相关资源封装成一个Task,放到任务池中,供某个闲置的线程来调用。

以上简单的描述了了db_proxy_server的处理过程,下面我们讲解具体如何操作。由于修改密码属于用户范畴,所以,我们接下来的主要逻辑放在UserAction.cpp及UserModel.cpp中。

首先我们需要在UserModel中增加修改密码的逻辑,在db_proxy_server/business/UserModel.h中增加函数声明:

uint32_t modifyUserPass(uint32_t nUserId, const string& strOldPass, const string& strNewPass);

这里返回值我们使用的是uint32_t类型,当返回0的时候,表示修改成功,其他的值分别对应一个错误。
我们去db_proxy_server/business/UserModel.cpp中去实现该函数:

uint32_t CUserModel::modifyUserPass(uint32_t nUserId, const string &strOldPass, const string &strNewPass)
{
    uint32_t nRet = 0;

    CDBManager* pDBManager = CDBManager::getInstance();
    CDBConn* pDBConn = pDBManager->GetDBConn("teamtalk_master");
    if (pDBConn)
    {
        string strPass, strSalt;
        string strSql = "select password, salt from IMUser where id="+int2string(nUserId);
        CResultSet* pResultSet = pDBConn->ExecuteQuery(strSql.c_str());
        if(pResultSet)
        {
            while (pResultSet->Next()) 
            {
                strPass = pResultSet->GetString("password");
                strSalt = pResultSet->GetString("salt");
            }
            delete pResultSet;
        
            string strInPassOld = strOldPass + strSalt;
            char szMd5Old[33];
            CMd5::MD5_Calculate(strInPassOld.c_str(), strInPassOld.length(), szMd5Old);
            string strOutPassOld(szMd5Old);
            if(strOutPassOld == strPass)
            {
                string strInPass = strNewPass + strSalt;
                char szMd5[33];
                CMd5::MD5_Calculate(strInPass.c_str(), strInPass.length(), szMd5);
                string strOutPass(szMd5);
                strSql = "update IMUser set password='" + strOutPass + "' where id=" + int2string(nUserId);
                if(!pDBConn->ExecuteUpdate(strSql.c_str()))
                {
                    nRet = -4;
                }
            }
            else
            {
                nRet = -3;
            }
        }
        else
        {
            log("no result for sql:%s.", strSql.c_str());
            nRet = -2;
        }
    
        pDBManager->RelDBConn(pDBConn);
    }
    else {
        log("no db connection for teamtalk_master");
        nRet = -1;
    }
    return nRet;
}

上面我们已经实现了修改密码的逻辑。函数中用到了md5计算,所以,我们需要包含EncDec.h头文件:

#include "EncDec.h"

整个逻辑比较简单,这里就不再详细赘述了。

接下来我们要在在db_proxy_server/business/UserAction.h中增加一个Action处理函数声明:

void modifyUserPass(CImPdu* pPdu, uint32_t conn_uuid);

然后我们在db_proxy_server/business/UserAction.cpp中去实现:

void modifyUserPass(CImPdu* pPdu, uint32_t conn_uuid)
{
    IM::Login::IMModifyPassReq msg;
    IM::Login::IMModifyPassRes msgResp;
    if(msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength()))
    {
        CImPdu* pPduRes = new CImPdu;
        
        uint32_t nUserId = msg.user_id();
        string strOldPass = msg.old_pass();
        string strNewPass = msg.new_pass();
        uint32_t nRet = CUserModel::getInstance()->modifyUserPass(nUserId, strOldPass, strNewPass);
        
        log("modifyUserPass. userId=%u, result=%d.", nUserId, nRet);
        msgResp.set_user_id(nUserId);
        msgResp.set_status(nRet);
        msgResp.set_attach_data(msg.attach_data());
        pPduRes->SetPBMsg(&msgResp);
        pPduRes->SetSeqNum(pPdu->GetSeqNum());
        pPduRes->SetServiceId(IM::BaseDefine::SID_LOGIN);
        pPduRes->SetCommandId(IM::BaseDefine::CID_LOGIN_RES_MODIFY_PASS);
        CProxyConn::AddResponsePdu(conn_uuid, pPduRes);
    }
    else
    {
        log("parse pb failed");
    }
}

好了,这样我们就实现了修改密码的逻辑处理,现在我们还需要最后一步,将UserAction处理修改密码的函数注册一下,我们到db_proxy_server/HandlerMap.cpp的Init函数中增加一行:

m_handler_map.insert(make_pair(uint32_t(CID_LOGIN_REQ_MODIFY_PASS), DB_PROXY::modifyUserPass));
5.3 再次修改msg_server

前面我们已经完成了db_proxy_server对修改密码的逻辑处理,db_proxy_server完成修改之后,会将结果返回给msg_server。msg_server处理与db_proxy_server通信的类是DBServerConn。

我们在msg_server/DBServerConn.h中定义一个处理返回的函数:

void _HandleModifyPassResponse(CImPdu *pPdu);

并在msg_server/DBServerConn.cpp中实现它:

void CDBServConn::_HandleModifyPassResponse(CImPdu *pPdu)
{
    IM::Login::IMModifyPassRes msg;
    CHECK_PB_PARSE_MSG(msg.ParseFromArray(pPdu->GetBodyData(), pPdu->GetBodyLength()));
    uint32_t user_id = msg.user_id();
    uint32_t status = msg.status();
    CDbAttachData attach_data((uchar_t*)msg.attach_data().c_str(), msg.attach_data().length());
    uint32_t handle = attach_data.GetHandle();
    log("_HandleModifyPassResponse, user_id=%u, status=%u.", user_id, status);

    CMsgConn* pMsgConn = CImUserManager::GetInstance()->GetMsgConnByHandle(user_id, handle);
    if (pMsgConn) {
        pMsgConn->SendPdu(pPdu);
    }
}

逻辑也很简单,这里就不细讲了,需要注意的是,这里是从msg的attach_data中获取到之前我们放入的handle。

我们在HandlePdu函数中的switch增加对CID_LOGIN_RES_MODIFY_PASS的处理:

case CID_LOGIN_RES_MODIFY_PASS:
    _HandleModifyPassResponse(pPdu);
    break;

OK 了,所有的服务端逻辑处理完成,我们就可以编译了。

6 客户端的处理

由于我自己是做服务端得开发的,对客户端的开发并不是十分了解,所以这里只简单提一下,不涉及界面等具体的操作,如有错误欢迎大家指正。

Android:可以通过pb命令产生java的协议代码,将对应的代码拷贝到Android工程中的对应目录下即可。

iOS:由于pb官方不支持oc,所以采用的是第三方库,有很多种实现,不同的库生成的代码不同,有点麻烦,我们所使用的是:https://github.com/alexeyxo/protobuf-objc.生成完代码后,将代码拷贝到对应的目录。

Mac: 同上。

Win:可以通过pb生成c++代码,具体同上。