本文篇幅较长,共计6227字,预计浏览时长20-30min
这是《千亿级IM独立开发指南!寰球即时通讯全套代码4小时速成》的第四篇:《服务端搭建与总结》
系列文章可参考:
《千亿级IM独立开发指南!寰球即时通讯全套代码4小时速成(一)》:Demo演示与IM设计
《千亿级IM独立开发指南!寰球即时通讯全套代码4小时速成(二)》:UI设计与搭建
《千亿级IM独立开发指南!寰球即时通讯全套代码4小时速成(三)》:APP 外部流程与逻辑
四、服务端搭建与总结
这篇终于进入了最初的局部:服务端。
随着这部分的实现,一个残缺的,可制撑持千亿级音讯的IM便白嫖实现!所以,咱们加一把劲,来看一下这最初的服务端局部。
1. 服务端选型
服务端的开发,首当其冲,且也是最重要的,便是服务端的选型。依据前两篇的需要剖析和功能设计,咱们有三个突出的外围需要:
- 能与 RTM 服务端交互:这意味着云上曲率官网必须提供对应语言的Server 端 SDK
- 反对 HTTP/HTTPS 以 GET 和 POST 的形式拜访
- 能以简略且疾速的形式,开发咱们所需要的业务
云上曲率 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和文章。
相干后续文档,请关注云上曲率官网账号。
发表回复