关于服务端:千亿级IM独立开发指南全球即时通讯全套代码4小时速成四

1次阅读

共计 21742 个字符,预计需要花费 55 分钟才能阅读完成。

本文篇幅较长,共计 6227 字,预计浏览时长 20-30min

这是《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成》的第四篇:《服务端搭建与总结》

系列文章可参考:
《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成(一)》:Demo 演示与 IM 设计
《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成(二)》:UI 设计与搭建
《千亿级 IM 独立开发指南!寰球即时通讯全套代码 4 小时速成(三)》:APP 外部流程与逻辑

四、服务端搭建与总结

这篇终于进入了最初的局部:服务端。
随着这部分的实现,一个残缺的,可制撑持千亿级音讯的 IM 便白嫖实现!所以,咱们加一把劲,来看一下这最初的服务端局部。

1. 服务端选型

服务端的开发,首当其冲,且也是最重要的,便是服务端的选型。依据前两篇的需要剖析和功能设计,咱们有三个突出的外围需要:

  1. 能与 RTM 服务端交互:这意味着云上曲率官网必须提供对应语言的 Server 端 SDK
  2. 反对 HTTP/HTTPS 以 GET 和 POST 的形式拜访
  3. 能以简略且疾速的形式,开发咱们所需要的业务

云上曲率 RTM 其实提供了很多不同的 SDK 用于客户与 RTM,官网下面间接列出的有 Go、PHP、C++、Python、Java、C# 六种。其实还存在其余的暗藏款,比方 Node.js。

在这些 SDK 中,从简略和不便水平上而言,疾速开发有以下四个选项:
● C++
● Go
● Python
● Node.js

首先,因为 Node.js SDK 打算会有重大更新,以后 GitHub 上是较老版本,所以咱们暂不抉择。这也是 Node.js SDK 成为暗藏款的外围起因。

而 Python 和 Go 对于 HTTP/HTTPS 来说,则算是经典候选。间接启动 HTTP 服务器,接入 RTM 对应语言的 SDK,实现相干的业务代码即可。而咱们这里抉择 C ++。
咱们抉择 C ++ 的起因有两点:一是在 FPNN 框架的加持下,C++ 开发者将不再须要去解决任何 HTTP/HTTPS 相干的反对,HTTP/HTTPS 的反对能够被齐全透明化。第二点就是,整个 RTM 服务零碎就是基于 C ++ FPNN 的框架进行开发的,所以咱们能够更好地与 RTM 服务器进行交互。

尽管 RTM 服务端的 C ++ SDK 是用 FPNN C++ SDK 进行开发,但 FPNN C++ SDK 是 FPNN 框架的特化子集,大部分状况下简直无需改变便可从 FPNN C++ SDK 改为由 FPNN 框架提供根底反对。这样咱们便可轻松实现开发需要。
但接下来,咱们会采纳更加简略的操作!

2. FPNN 框架的配置

首先从 GitHub 下面下载 FPNN 框架的最新发行版,目前是 1.1.3 版本。
留神:FPNN 框架目前仅反对 CentOS、Ubuntu 和 MacOS 三个操作系统,WIndows 仅有 C++ Widnows SDK。C++ SDK 不含服务器和 HTTP/HTTPS 等性能反对。

依照“FPNN 装置与集成”进行环境配置和框架编译。
留神:如果编译和运行环境不是亚马逊 AWS,而是阿里云、腾讯云等,或者本人的内网虚拟机,切记依据“FPNN 注意事项”进行批改和配置。否则服务可能无响应。因为在默认状况下,适配的是亚马逊 AWS 的运行环境。

3. 服务器框架搭建

3.1. 服务器框架搭建

采纳 FPNN 框架开发的服务,其实必须的就 3 个文件:一个 C ++ 代码文件,一个运行配置文件,一个 Makefile
在这里,咱们把业务代码和框架代码拆散:将负责具体业务申请解决的 QuestProcessor 类的实现和服务框架离开,一共产生 5 个文件:Makefile、IMDemoServer.cpp、QuestProcessor.h、QuestProcessor.cpp、im.conf。这也是采纳 FPNN 框架进行开发的举荐做法。

首先是服务框架 IMDemoServer.cpp,如果咱们不批改业务申请解决类的名称的话,以下代码无需批改,这也算是 FPNN 框架开发的规范代码:

#include <iostream>
#include "TCPEpollServer.h"
#include "QuestProcessor.h"
#include "Setting.h"

using namespace fpnn;

int main(int argc, char* argv[])
{if (argc != 2)
    {std::cout<<"Usage:"<<argv[0]<<"config"<<std::endl;
        return 0;
    }
    if(!Setting::load(argv[1])){std::cout<<"Config file error:"<< argv[1]<<std::endl;
        return 1;
    }

    ServerPtr server = TCPEpollServer::create();
    server->setQuestProcessor(std::make_shared<QuestProcessor>());
    if (server->startup())
        server->run();

    return 0;
}

对,整个服务器框架就算是实现了。当然,如果不采纳 HTTP 或者 TCP,而改为 UDP,则将代码中的 TCPEpollServer 间接换成 UDPEpollServer,整个服务就从 TCP 或者 HTTP/HTTPS 服务,变成了 UDP 服务。

而后是业务框架:

#ifndef QuestProcessor_H
#define QuestProcessor_H

#include "IQuestProcessor.h"

using namespace fpnn;

class QuestProcessor: public IQuestProcessor
{QuestProcessorClassPrivateFields(QuestProcessor)
    
public:
    virtual ~QuestProcessor() {}

    QuestProcessorClassBasicPublicFuncs
};

#endif

#include "QuestProcessor.h"

嗯,FPNN 空的业务框架这样就实现了!
鉴于咱们须要解决 userLogin、userRegister、createGroup、joinGroup、createRoom、lookup 六个申请,所以咱们批改业务框架代码如下:

#ifndef QuestProcessor_H
#define QuestProcessor_H

#include "IQuestProcessor.h"

using namespace fpnn;

class QuestProcessor: public IQuestProcessor
{QuestProcessorClassPrivateFields(QuestProcessor)
    
public:
    FPAnswerPtr userLogin(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
    FPAnswerPtr userRegister(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
    FPAnswerPtr createGroup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
    FPAnswerPtr joinGroup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
    FPAnswerPtr createRoom(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
    FPAnswerPtr lookup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);

    QuestProcessor();
    virtual ~QuestProcessor() {}

    QuestProcessorClassBasicPublicFuncs
};

#endif
#include "FPLog.h"
#include "QuestProcessor.h"

QuestProcessor::QuestProcessor()
{registerMethod("userLogin", &QuestProcessor::userLogin);
    registerMethod("userRegister", &QuestProcessor::userRegister);
    registerMethod("createGroup", &QuestProcessor::createGroup);
    registerMethod("joinGroup", &QuestProcessor::joinGroup);
    registerMethod("createRoom", &QuestProcessor::createRoom);
    registerMethod("lookup", &QuestProcessor::lookup);
}

FPAnswerPtr QuestProcessor::userLogin(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    //-- TODO
    return nullptr;
}

FPAnswerPtr QuestProcessor::userRegister(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    //-- TODO
    return nullptr;
}

FPAnswerPtr QuestProcessor::createGroup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    //-- TODO
    return nullptr;
}

FPAnswerPtr QuestProcessor::joinGroup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    //-- TODO
    return nullptr;
}

FPAnswerPtr QuestProcessor::createRoom(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    //-- TODO
    return nullptr;
}

FPAnswerPtr QuestProcessor::lookup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    //-- TODO
    return nullptr;
}

到此,业务框架也就搭建实现。

而后是 FPNN 的规范配置文件:

FPNN.server.listening.ip = 
FPNN.server.listening.port = 13601
FPNN.server.name = IMDemoServer

FPNN.server.log.level = WARN
FPNN.server.log.endpoint = std::cout
FPNN.server.log.route = IMDemoServer

配置中,监听 IP 为空示意咱们在本机所有 IP 地址,或者网络接口上进行监听。
监听端口为 13601,监听 IPv4,暂不启动 IPv6 的监听。
而后日志输入等级为 WARN,即仅输入正告,及正告以上级别的日志。正告以下级别的日志将被疏忽。
日志向规范输入输入。

因为 FPNN 框架不是专门的 HTTP/HTTPS 服务框架,而只是带了 HTTP/HTTPS 协定反对,所以这里咱们要减少一行配置,以便关上对 HTTP 的反对:

# FPNN 服务反对 HTTP/HTTPS 拜访
FPNN.server.http.supported = true

因为咱们没有 HTTPS 证书,所以临时仅启动 HTTP 反对。如果须要 HTTPS 反对,则在获取证书后,依照“FPNN HTTP/HTTPS & webSocket (ws/wss) 反对”一文进行配置即可。

最初,是咱们的 Makfile,这也是从 FPNN 框架开发的规范模版批改而来:

EXES_SERVER = IMDemoServer

FPNN_DIR = ../../../infra-fpnn
CFLAGS +=
CXXFLAGS +=
CPPFLAGS += -I$(FPNN_DIR)/extends -I$(FPNN_DIR)/core -I$(FPNN_DIR)/proto -I$(FPNN_DIR)/base -I$(FPNN_DIR)/proto/msgpack -I$(FPNN_DIR)/proto/rapidjson
LIBS += -L$(FPNN_DIR)/core -L$(FPNN_DIR)/proto -L$(FPNN_DIR)/extends -L$(FPNN_DIR)/base -lfpnn

OBJS_SERVER = IMDemoServer.o QuestProcessor.o

all: $(EXES_SERVER)

clean:
    $(RM) *.o $(EXES_SERVER)
include $(FPNN_DIR)/def.mk

其中,FPNN_DIR 指明了 FPNN 框架的门路;OBJS_SERVER 指明了相干的 cpp 代码文件对应的指标文件。
其余个别状况下,间接照抄即可。

3.2. RTM Server C++ SDK 接入

个别状况下,咱们间接引入 RTM C++ Server SDK 即可。但鉴于 RTM 自身就是用 FPNN 框架进行开发,且咱们所须要和 RTM 打交道的接口很少,而且 FPNN 体系开发又很容易,所以咱们这次不引入 RTM C++ Server SDK,而是通过 FPNN 协定间接拜访 RTM 服务。
但拜访 RTM 须要服务端密钥签名,所以咱们间接从 RTM C++ Server SDK 中摘出两个文件 RTMMidGenerator.h 和 RTMMidGenerator.cpp 退出咱们的服务代码中。两个文件相当简略,总代码量不到 60 行,这里也就不再具体分析。
在 QuestProcessor.cpp 中引入 RTMMidGenerator.h,并初始化 RTMMidGenerator 类:

... ...
#include "RTMMidGenerator.h"
... ...

QuestProcessor::QuestProcessor()
{
    ... ...
    RTMMidGenerator::init();}

而后批改咱们的 Makefile,将 OBJS_SERVER 增加一个参数 RTMMidGenerator.o 即可:

OBJS_SERVER = IMDemoServer.o RTMMidGenerator.o QuestProcessor.o

4. 性能的开发

4.1. 与 RTM 服务器的交互

首先,咱们须要登录云上曲率的用户控制台,在控制台左侧,“控制台概览”中,抉择“实时信令”条目:

进入实时信令的项目选择页 (需注册云上曲率官网账号) 抉择对应的我的项目,进入我的项目控制台。
在我的项目控制台左侧列表中,抉择“服务配置”:

而后在右侧,便可看到服务器我的项目需配置的数据信息:

咱们记录下项目编号、服务端 SDK 接入点,以及密钥,将其配置入 im.conf

RTM.config.endpoint = rtm-nx-back.ilivedata.com:13315
RTM.config.pid = 80000253
RTM.config.secret = xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx

之后,咱们来退出与 RTM 服务器通信的模块。

既然 RTM 服务器由 FPNN 框架开发,各个平台和语言的 SDK 又都由对应的 FPNN SDK 开发,那咱们间接用 FPNN Client 和 RTM 服务器连贯即可。编辑代码,在代码中退出 TCPClient 的应用,以及与 RTM 服务器通信须要的信息:

... ...
#include "TCPClient.h"
... ...
    
class QuestProcessor: public IQuestProcessor
{
    ... ...
    
    int _pid;
    std::string _secret;
    
    TCPClientPtr _rtmServerClient;
    
    ... ...
}
... ...  
#include "Setting.h"  
... ...
    
QuestProcessor::QuestProcessor()
{
    ... ...
    std::string endpoint = Setting::getString("RTM.config.endpoint");
    _rtmServerClient = TCPClient::createClient(endpoint);
    _rtmServerClient->keepAlive();
    _rtmServerClient->setQuestTimeout(10);
    
    _secret = Setting::getString("RTM.config.secret");
    _pid = Setting::getInt("RTM.config.pid");
    
    ... ...
}

其中头文件 Setting.h 中蕴含的动态类 calss Setting 负责从配置文件中提取信息。
咱们通过接入点创立了 TCPClient 之后,开启了 FPNN 的链接包活性能,并批改了默认的超时工夫。
并从配置文件,加载了项目编号,以及拜访密钥。

4.2. 数据签名

从 RTM 公开的 SDK 咱们发现,服务端拜访 RTM 服务,须要对接口和数据进行签名,于是退出辅助函数 makeSignAndSalt():

class QuestProcessor: public IQuestProcessor
{
    ... ...
    
    void makeSignAndSalt(int32_t ts, const std::string& cmd, std::string& sign, int64_t& salt);
    
    ... ...
}
... ...

#include "hex.h"
#include "md5.h"

... ...
    
void QuestProcessor::makeSignAndSalt(int32_t ts, const std::string& cmd, std::string& sign, int64_t& salt)
{salt = RTMMidGenerator::genMid();
    std::string content = std::to_string(_pid) + ":" + _secret + ":" + std::to_string(salt) + ":" + cmd + ":" + std::to_string(ts);
    
    unsigned char digest[16];
    md5_checksum(digest, content.c_str(), content.size());
    char hexstr[32 + 1];
    Hexlify(hexstr, digest, sizeof(digest));

    sign.assign(hexstr); 
}

... ...

4.3. 数据存储性能

咱们开发这个后端服务的外围目标,便是对用户信息的治理。于是咱们须要保留、查找和增改相干数据。
为了简化起见,咱们采纳 json 格局进行本地存储。尽管 FPNN 框架带有 RapidJSON 的最新版本,但间接用 RapidJSON 进行操作,还是太过麻烦和不便。于是咱们应用 FPNN 框架自带的另外一个 Json 库 FPJson。采纳 FPJson,不仅简略不便,不管多少层级的数据拜访,或者如许简单的 STL 组合容器数据打包,FPJson 一律一行代码间接搞定。而且对于咱们目前的需要,甚至都能够间接作为容器应用,而无需以任何模式解决 json 格局。

因而,咱们决定采纳 FPJson,进行数据的治理和存储。此外,咱们心愿当服务器启动的时候,加载本地存储的数据;当服务器进行的时候,保留相干的数据到磁盘。

编辑配置文件 im.conf,减少对数据保留门路的配置:

# 用户账号信息保留门路
IMDemoServer.Store.path = ./im.users.json

编辑 QuestProcessor 类,退出对 FPJson 的援用,以及对服务器启动和进行事件的解决。

... ...
    
#include "FPJson.h"
    
... ...
    
class QuestProcessor: public IQuestProcessor
{
    ... ...
    
    std::mutex _mutex;
    JsonPtr _root;
    
    ... ...
    
public:
    virtual void start();
    virtual void serverStopped();
    
    ... ...
}
#include "FileSystemUtil.h"

... ...
    
void QuestProcessor::start()
{std::string userFile = Setting::getString("IMDemoServer.Store.path");
    std::string userData;
    FileSystemUtil::readFileContent(userFile, userData);
    if (userData.size() > 0)
        _root = Json::parse(userData.c_str());
        
    if (!_root)
    {_root.reset(new Json());
        (*_root)["nextUid"] = 1;
        (*_root)["nextGid"] = 1;
        (*_root)["nextRid"] = 1;
    }
}

void QuestProcessor::serverStopped()
{std::string userData = _root->str();
    std::string userFile = Setting::getString("BizServer.Store.path");
    FileSystemUtil::saveFileContent(userFile, userData);
}

至此,数据的加载、保留和根底治理,开发结束。
同时 im.conf 后续也不再批改。

4.4. userLogin

咱们规定 userLogin 接口须要两个参数:username 和 pwd。两者皆为字符串模式。
返回三个参数:pid、uid 和 token。其中 pid 为项目编号,整型;uid 为用户惟一数字 ID,整型;token 为从 RTM 服务器获取的登陆密钥,字符串类型。

尽管咱们在 iOS 篇中,规定是通过 HTTP/HTTPS 的 GET 形式拜访,但 FPNN 框架自身反对 HTTP、HTTPS、FPNN 协定(TCP & UDP)、FPNN 加密协议(TCP & UDP)、FPNN over TLS、WebSocket(ws)、平安的 WebSocket(wss) 几种形式拜访,且后续的其余版本的 App 可能会以不同的模式拜访,所以咱们在这里须要兼容以上几种协定传输的参数获取。好在对于 FPNN 框架,仅有 HTTP/GET 和其余形式参数获取不同,所以 userLogin 接口及相干代码如下:

class QuestProcessor: public IQuestProcessor
{
    ... ...
    
    FPQuestPtr genTokenQuest(int64_t uid);
    void getToken(int64_t uid, std::shared_ptr<IAsyncAnswer> async);
    
    ... ...

public:

    ... ...
    
    FPAnswerPtr userLogin(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
    
    ... ...
}
FPQuestPtr QuestProcessor::genTokenQuest(int64_t uid)
{int32_t ts = slack_real_sec();
    std::string sign;
    int64_t salt;
    makeSignAndSalt(ts, "gettoken", sign, salt);

    FPQWriter qw(5, "gettoken");
    qw.param("pid", _pid);
    qw.param("sign", sign);
    qw.param("salt", salt);
    qw.param("ts", ts);
    qw.param("uid", uid);
    return qw.take();}

void QuestProcessor::getToken(int64_t uid, std::shared_ptr<IAsyncAnswer> async)
{
    int pid = _pid;
    FPQuestPtr tokenQuest = genTokenQuest(uid);
    bool launchAsync = _rtmServerClient->sendQuest(tokenQuest, [uid, pid, async](FPAnswerPtr answer, int errorCode){if (errorCode == FPNN_EC_OK)
        {FPAReader ar(answer);
            FPAWriter aw(3, async->getQuest());
            aw.param("pid", pid);
            aw.param("uid", uid);
            aw.param("token", ar.getString("token"));

            async->sendAnswer(aw.take());
        }
        else
            async->sendErrorAnswer(errorCode, "BizServer error.");
    });

    if (launchAsync == false)
        async->sendErrorAnswer(FPNN_EC_CORE_UNKNOWN_ERROR, "BizServer error. You can retry.");
}

FPAnswerPtr QuestProcessor::userLogin(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    std::string username, password;
    if (quest->isHTTP())
    {
        //-- HTTP/HTTPS GET 拜访
        username = quest->http_uri("username");
        password = quest->http_uri("pwd");

        //-- 如果是 POST 拜访,而不是 GET 拜访
        if (username.empty())
            username = args->wantString("username");

        if (password.empty())
            password = args->wantString("pwd");
    }
    else
    {
        //-- FPNN/WebSocket/HTTP POST/HTTPS POST 拜访
        username = args->wantString("username");
        password = args->wantString("pwd");
    }

    int64_t uid = 0;
    bool passwordMached = true;
    {std::unique_lock<std::mutex> lck(_mutex);
        if ((*_root)["account"].exist(username))
        {if ((std::string)((*_root)["account"][username]["pwd"]) == password)
                uid = (*_root)["account"][username]["uid"];
            else
                passwordMached = false;
        }
    }

    if (passwordMached == false)
        return FpnnErrorAnswer(quest, FPNN_EC_CORE_UNKNOWN_ERROR, "Password is wrong!");

    if (uid == 0)
        return FpnnErrorAnswer(quest, FPNN_EC_CORE_UNKNOWN_ERROR, "User is unregistered!");

    getToken(uid, genAsyncAnswer(quest));

    return nullptr;
}

其中,函数 genTokenQuest() 生成向 RTM 服务集群申请用户登陆 token 的申请数据;函数 getToken() 则调用 TCPClient 向 RTM 集群发送获取用户登陆 token 的申请,并在异步回调中生成对 App 的返回数据。

而在 userLogin() 函数中,则展现了如何从 FPNN 框架所反对的各种拜访模式中,获取对应的输出参数。
而后通过 FPJson 检查用户是否存在。当用户不存在时,返回对应的谬误;当用户存在时,产生一个异步应答对象,交给函数 getToken(),以便当用户登陆 touken 申请返回时,能对 App 的申请进行异步应答。而期间如果有任何谬误,导致流程中断,FPNN 的异步应答对象将主动对 App 的申请进行回应。

4.5. userRegister

用户注册流程与用户登录流程高度相似。
当 FPJson 确认注册的用户不存在,提交的用户名能够注册后,记录用户的注册信息,而后通过 getToken() 函数,向 RTM 集群申请用户的登录 token。具体代码如下:

FPAnswerPtr QuestProcessor::userRegister(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    std::string username, password;
    if (quest->isHTTP())
    {
        //-- HTTP/HTTPS GET 拜访
        username = quest->http_uri("username");
        password = quest->http_uri("pwd");

        //-- 如果是 POST 拜访,而不是 GET 拜访
        if (username.empty())
            username = args->wantString("username");

        if (password.empty())
            password = args->wantString("pwd");
    }
    else
    {
        //-- FPNN/WebSocket/HTTP POST/HTTPS POST 拜访
        username = args->wantString("username");
        password = args->wantString("pwd");
    }

    int64_t uid = 0;
    {std::unique_lock<std::mutex> lck(_mutex);
        if ((*_root)["account"].exist(username) == false)
        {(*_root)["account"][username]["pwd"] = password;

            uid = (*_root)["nextUid"];

            (*_root)["nextUid"] = uid + 1;
            (*_root)["account"][username]["uid"] = uid;
        }
    }

    if (uid == 0)
        return FpnnErrorAnswer(quest, FPNN_EC_CORE_UNKNOWN_ERROR, "Username is existed!");

    getToken(uid, genAsyncAnswer(quest));

    return nullptr;
}

4.6. createGroup

从创立群组开始,为了后续不便,咱们摘出两个通用函数 sendAsyncQuest() 以及 extraParams()。
因为在后续的接口中,咱们须要返回给 App 的应答其实曾经筹备好了,然而还须要 RTM 做进一步的操作能力返回,所以咱们提取出了异步应答函数 sendAsyncQuest():

class QuestProcessor: public IQuestProcessor
{
    ... ...
    
    void sendAsyncQuest(FPQuestPtr quest, std::shared_ptr<IAsyncAnswer> async, FPAnswerPtr realAnswer = nullptr);
    
    ... ...
}
void QuestProcessor::sendAsyncQuest(FPQuestPtr quest, std::shared_ptr<IAsyncAnswer> async, FPAnswerPtr realAnswer)
{bool launchAsync = _rtmServerClient->sendQuest(quest, [async, realAnswer](FPAnswerPtr answer, int errorCode){if (errorCode == FPNN_EC_OK)
        {if (realAnswer)
                async->sendAnswer(realAnswer);
            else
                async->sendEmptyAnswer();}
        else
            async->sendErrorAnswer(errorCode, "BizServer error.");
    });

    if (launchAsync == false)
        async->sendErrorAnswer(FPNN_EC_CORE_UNKNOWN_ERROR, "BizServer error. You can retry.");
}

而后续的接口,大部分输出参数高度一致,所以咱们有了对立的参数提取函数 extraParams():

class QuestProcessor: public IQuestProcessor
{
    ... ...
    
    FPAnswerPtr extraParams(const FPReaderPtr args, const FPQuestPtr quest, const char* xidKey, const char* xnameKey,
        int64_t &xid, std::string& xname);
    
    ... ...
}
FPAnswerPtr QuestProcessor::extraParams(const FPReaderPtr args, const FPQuestPtr quest,
            const char* xidKey, const char* xnameKey, int64_t &xid, std::string& xname)
{if (quest->isHTTP())
    {
        //-- HTTP/HTTPS GET 拜访
        std::string xidString = quest->http_uri(xidKey);
        if (xidString.empty())
            xid = 0;
        else
            xid = std::stoll(xidString);

        xname = quest->http_uri(xnameKey);

        //-- 如果是 POST 拜访,而不是 GET 拜访
        if (xid == 0)
            xid = args->wantInt(xidKey);

        if (xname.empty())
            xname = args->wantString(xnameKey);
    }
    else
    {
        //-- FPNN/WebSocket/HTTP POST/HTTPS POST 拜访
        xid = args->wantInt(xidKey);
        xname = args->wantString(xnameKey);
    }

    if (xid == 0)
        return FpnnErrorAnswer(quest, FPNN_EC_CORE_UNKNOWN_ERROR, std::string("Invalid").append(xidKey).append("!").c_str());

    if (xname.empty())
        return FpnnErrorAnswer(quest, FPNN_EC_CORE_UNKNOWN_ERROR, std::string("Invalid").append(xnameKey).append("!").c_str());

    return nullptr;
}

在以上根底上,咱们再来实现创立群组的性能:

FPAnswerPtr QuestProcessor::createGroup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    int64_t uid = 0;
    std::string groupname;

    FPAnswerPtr answer = extraParams(args, quest, "uid", "group", uid, groupname);
    if (answer)
        return answer;

    int64_t gid = 0;

    {std::unique_lock<std::mutex> lck(_mutex);
        if ((*_root)["group"].exist(groupname) == false)
        {gid = (*_root)["nextGid"];

            (*_root)["nextGid"] = gid + 1;
            (*_root)["group"][groupname] = gid;
        }
    }

    if (gid == 0)
        return FpnnErrorAnswer(quest, FPNN_EC_CORE_UNKNOWN_ERROR, "Group is existed!");

    int32_t ts = slack_real_sec();
    std::string sign;
    int64_t salt;
    makeSignAndSalt(ts, "addgroupmembers", sign, salt);

    FPQWriter qw(6, "addgroupmembers");
    qw.param("pid", _pid);
    qw.param("sign", sign);
    qw.param("salt", salt);
    qw.param("ts", ts);
    qw.param("gid", gid);
    qw.param("uids", std::set<int64_t>{uid});

    FPAWriter aw(1, quest);
    aw.param("gid", gid);

    sendAsyncQuest(qw.take(), genAsyncAnswer(quest), aw.take());

    return nullptr;
}

在 createGroup 中,咱们先验证须要创立的群组惟一名称并不存在,而后给群组调配惟一数字 ID,记录群组惟一名称和 ID 信息,而后向 RTM 服务集群提交 addgroupmembers 申请。当 RTM 服务集群对 addgroupmembers 申请通过后,再返回给 App 对应的应答数据。

4.7. joinGroup

退出群组与创立群组相似。
当确认群组存在后,咱们的服务器便向 RTM 服务集群提交 addgroupmembers 申请。当 RTM 服务集群对 addgroupmembers 申请通过后,再返回给 App 对应的应答数据。

FPAnswerPtr QuestProcessor::joinGroup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    int64_t uid = 0;
    std::string groupname;

    FPAnswerPtr answer = extraParams(args, quest, "uid", "group", uid, groupname);
    if (answer)
        return answer;

    int64_t gid = 0;

    {std::unique_lock<std::mutex> lck(_mutex);
        if ((*_root)["group"].exist(groupname))
            gid = (*_root)["group"][groupname];
        else
            return FpnnErrorAnswer(quest, FPNN_EC_CORE_UNKNOWN_ERROR, "Group is not existed!");        
    }

    int32_t ts = slack_real_sec();
    std::string sign;
    int64_t salt;
    makeSignAndSalt(ts, "addgroupmembers", sign, salt);

    FPQWriter qw(6, "addgroupmembers");
    qw.param("pid", _pid);
    qw.param("sign", sign);
    qw.param("salt", salt);
    qw.param("ts", ts);
    qw.param("gid", gid);
    qw.param("uids", std::set<int64_t>{uid});

    FPAWriter aw(1, quest);
    aw.param("gid", gid);

    sendAsyncQuest(qw.take(), genAsyncAnswer(quest), aw.take());

    return nullptr;
}

4.8. createRoom

创立房间与创立群组高度类似。区别只是一个是群组,一个是房间。而且房间 App 端能够间接退出,因而不再有对 RTM 集群的申请操作。

FPAnswerPtr QuestProcessor::createRoom(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    std::string roomname;

    if (quest->isHTTP())
    {
        //-- HTTP/HTTPS GET 拜访
        roomname = quest->http_uri("room");

        //-- 如果是 POST 拜访,而不是 GET 拜访
        if (roomname.empty())
            roomname = args->wantString("room");
    }
    else
    {
        //-- FPNN/WebSocket/HTTP POST/HTTPS POST 拜访
        roomname = args->wantString("room");
    }

    int64_t rid = 0;

    {std::unique_lock<std::mutex> lck(_mutex);
        if ((*_root)["room"].exist(roomname) == false)
        {rid = (*_root)["nextRid"];

            (*_root)["nextRid"] = rid + 1;
            (*_root)["room"][roomname] = rid;
        }
        else
            rid = (*_root)["room"][roomname];
    }

    FPAWriter aw(1, quest);
    aw.param("rid", rid);

    return aw.take();}

4.9. lookup
最初,是查问性能。
查问其实就是在 Json 的数据字典中,进行查找或遍历。为了简略易于了解,这里没有做任何的优化和技巧解决。流程也很简略,不再做具体阐明。相干代码如下:

FPAnswerPtr QuestProcessor::lookup(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
{
    std::set<int64_t> uids, gids, rids;
    std::set<std::string> users, groups, rooms;

    uids = args->get("uids", uids);
    gids = args->get("gids", gids);
    rids = args->get("rids", rids);

    users = args->get("users", users);
    groups = args->get("groups", groups);
    rooms = args->get("rooms", rooms);

    std::map<std::string, int64_t> userResult, groupResult, roomResult;

    //====================================//
    {std::unique_lock<std::mutex> lck(_mutex);

        for (auto& username: users)
        {if ((*_root)["account"].exist(username))
                userResult[username] = (*_root)["account"][username]["uid"];
        }

        for (auto& groupname: groups)
        {if ((*_root)["group"].exist(groupname))
                groupResult[groupname] = (*_root)["group"][groupname];
        }

        for (auto& roomname: rooms)
        {if ((*_root)["room"].exist(roomname))
                roomResult[roomname] = (*_root)["room"][roomname];
        }

        if (uids.size() > 0)
        {const std::map<std::string, JsonPtr> * accountDict = _root->getDict("account");
            for (auto& node: *accountDict)
            {int64_t uid = (int64_t)(node.second->wantInt("uid"));
                if (uids.find(uid) != uids.end())
                {userResult[node.first] = uid;
                    uids.erase(uid);

                    if (uids.empty())
                        break;
                }
            }
        }
            
        if (gids.size() > 0)
        {const std::map<std::string, JsonPtr> * groupDict = _root->getDict("group");
            for (auto& node: *groupDict)
            {int64_t gid = *(node.second);
                if (gids.find(gid) != gids.end())
                {groupResult[node.first] = gid;
                    gids.erase(gid);

                    if (gids.empty())
                        break;
                }
            }
        }

        if (rids.size() > 0)
        {const std::map<std::string, JsonPtr> * roomDict = _root->getDict("room");
            for (auto& node: *roomDict)
            {int64_t rid = *(node.second);
                if (rids.find(rid) != rids.end())
                {roomResult[node.first] = rid;
                    rids.erase(rid);

                    if (rids.empty())
                        break;
                }
            }
        }
    }
    //====================================//

    FPAWriter aw(3, quest);
    aw.param("users", userResult);
    aw.param("groups", groupResult);
    aw.param("rooms", roomResult);

    return aw.take();}

至此,整个 IMDemo 服务端边开发实现。
Server 端残缺代码请参见:https://github.com/highras/rt…

5. 编译 & 运行

编译好 FPNN 框架后,进入 IMDemoServer 的代码目录,间接 make 就行。

[swxlion@ip-10-65-5-131 demoServer]$ make
g++ -c  -std=c++11 -DHOST_PLATFORM_AWS -I../../../infra-fpnn/extends -I../../../infra-fpnn/core -I../../../infra-fpnn/proto -I../../../infra-fpnn/base -I../../../infra-fpnn/proto/msgpack -I../../../infra-fpnn/proto/rapidjson -g -Wall -Werror -fPIC -O2  -o BizServer.o BizServer.cpp
g++ -c  -std=c++11 -DHOST_PLATFORM_AWS -I../../../infra-fpnn/extends -I../../../infra-fpnn/core -I../../../infra-fpnn/proto -I../../../infra-fpnn/base -I../../../infra-fpnn/proto/msgpack -I../../../infra-fpnn/proto/rapidjson -g -Wall -Werror -fPIC -O2  -o RTMMidGenerator.o RTMMidGenerator.cpp
g++ -c  -std=c++11 -DHOST_PLATFORM_AWS -I../../../infra-fpnn/extends -I../../../infra-fpnn/core -I../../../infra-fpnn/proto -I../../../infra-fpnn/base -I../../../infra-fpnn/proto/msgpack -I../../../infra-fpnn/proto/rapidjson -g -Wall -Werror -fPIC -O2  -o QuestProcessor.o QuestProcessor.cpp
g++  -std=c++11 -DHOST_PLATFORM_AWS -I../../../infra-fpnn/extends -I../../../infra-fpnn/core -I../../../infra-fpnn/proto -I../../../infra-fpnn/base -I../../../infra-fpnn/proto/msgpack -I../../../infra-fpnn/proto/rapidjson -g -Wall -Werror -fPIC -O2  -o BizServer BizServer.o RTMMidGenerator.o QuestProcessor.o -L../../../infra-fpnn/core -L../../../infra-fpnn/proto -L../../../infra-fpnn/extends -L../../../infra-fpnn/base -lfpnn -O2 -rdynamic -lstdc++ -lfpnn -lfpproto -lextends -lfpbase -lpthread -lz -lssl -lcrypto -lcurl -ltcmalloc
[swxlion@ip-10-65-5-131 demoServer]$ 

运行:

[swxlion@ip-10-65-5-131 demoServer]$ ./IMDemoServer im.conf

6. 我的项目后记

到此,IMDemo 服务端开发结束,整个 IMDemo 我的项目曾经处于齐全可用的状态。
整个我的项目的运行成果可参见上面的动画演示:

https://www.bilibili.com/vide…

残缺的我的项目代码请参见:https://github.com/highras/rt…
出于篇幅的起因,咱们没有介绍文件的发送、离线语音、实时音视频,以及多语言翻译、语音辨认、文本审核、图像审核、音视频审核等性能。这些将后续以本 Demo 扩大的形式,或者另起新篇进行介绍。如果有感兴趣的同学,能够后行浏览云上曲率官网进行理解。

安全性正告

本服务器代码仅做演示和解说之用,没有做进一步的安全措施。
此外,对于用户,仅做了用户名的冲突检测,没有做进一步的平安管控。
理论我的项目中,请防止在公网执行非加密传输,或者未验证操作。

应用云上曲率 RTM 的知名企业与我的项目

FunPlus,趣加团体。中国出海品牌榜第 19 名,游戏畛域 SLG 品类寰球第一。
其中,
《阿瓦隆之王》(King of Avalon):曾间断半年以上进入 Apple AppStore 游戏支出榜前五的 SLG 类型游戏。全面应用云上曲率 RTM 作为信令传输和 IM 聊天音讯渠道。单我的项目音讯量日峰值超过 240 亿条 / 天。
《火枪纪元》(Guns of Glory):同样曾间断半年以上进入 Apple AppStore 游戏支出榜前五的 SLG 类型游戏,同样全面应用云上曲率 RTM 作为信令传输和 IM 聊天音讯渠道。

Century Games:点点互动 / 世纪创游。旗下多款出名游戏的聊天零碎采纳云上曲率 RTM。

其它相干的 SDK

云上曲率基于 RTM,面向游戏和社交娱乐行业,还别离有不带 UI 界面的 IM Lib SDK,以及带有可自定义界面的 IM Kit SDK。
绝对于 RTM SDK 而言,IM Lib 和 IM Kit 更加下层,封装更加面相 IM 和社交娱乐行业。接如何应用更加简便。咱们后续会推出基于 IM Lib 和 IM Kit 的文章和教程。

IM Kit 成果参考:

7. 其余的版本与计划

本篇打算后续推出 Android/Kotlin 版本,以及 Go 的服务端版本。
RTC 局部正在思考适合的 demo 模式。
此外,RTM 是信令层面的 SDK,咱们后续也将推出 IM Lib 和 IM Kit 层面的 demo 和文章。
相干后续文档,请关注云上曲率官网账号。

正文完
 0