hiredis异步接口封装并导出到Lua
(金庆的专栏 2017.1)
hiredis 不支持 Windows, Windows 下使用 wasppdotorg / hiredis-for-windows 。
Linux 下仍是 redis/hiredis。
hiredis-for-windows 是以 hiredis 0.13.3 为基础移植的。
hiredis-for-windows 需要稍加修正:
* 去除 inline 宏
* TCP_NODELAY 改在连接之前设置。
详见其Issue.
Cluster 支持采用 shinberg/cpp-hiredis-cluster。这是个CPP库,支持异步,
要求 hiredis >= 0.12.0。
jinq0123/cpp-hiredis-cluster 在 develop 分支上更改了接口,让它更好用。
因为网络库是boost asio, 所以需要asio适配器,采用 jinq0123/hiredis-boostasio-adapter。
cpp-hiredis-cluster 提供的是统一的Command接口,接收字符串命令,返回 redisReply.
对于常用命令,需要更简单的接口。
在Lua手游服务器代码中新建CRedis类,封装 cpp-hiredis-cluster,
为常用redis命令封装更好用的接口。
CRedis类封装了asio, 接口是根据应用需要定义的,所以是专用接口,
不在 cpp-hiredis-cluster 中实现。
bool CRedis::Init(io_service& rIos, const std::string& sHost, uint16_t uPort)
创建 RedisCluster 对象。
io_service 用于创建一个 redis 事件适配器,
RedisCluster创建需要一个适配器。
sHost, uPort 用于初始化连接redis cluster, 获取集群信息。
using Cmd = RedisCluster::AsyncHiredisCommand;
bool CRedis::Init(io_service& rIos, const std::string& sHost, uint16_t uPort)
{
m_pAdapter.reset(new Adapter(rIos));
try
{
m_pCluster.reset(Cmd::createCluster("127.0.0.1", 7000, *m_pAdapter));
}
catch (const RedisCluster::ClusterException &e)
{
LOG_ERROR("Cluster exception: " << e.what());
return false;
}
return true;
}
static Cmd::Action handleException(const RedisCluster::ClusterException &exception,
RedisCluster::HiredisProcess::processState state)
{
// Check the exception type.
// Retry in case of non-critical exceptions.
if (!dynamic_cast<const RedisCluster::CriticalException*>(&exception))
{
LOG_WARN("Exception in processing async redis callback: "
<< exception.what() << " Retry...");
// retry to send a command to redis node
return Cmd::RETRY;
}
LOG_ERROR("Critical exception in processing async redis callback: "
<< exception.what());
return Cmd::FINISH;
}
static void handleSetReply(const redisReply &reply, const CRedis::SetCb& setCb)
{
if (!setCb) return;
if (reply.type == REDIS_REPLY_STATUS)
{
const std::string OK("OK");
if (OK == reply.str)
{
setCb(true);
return;
}
}
LOG_WARN("Set reply: " << reply.str);
setCb(false);
}
void CRedis::Set(const string& sKey, const string& sValue, const SetCb& setCb)
{
assert(m_pCluster);
Cmd::commandf2(*m_pCluster, sKey,
[setCb](const redisReply& reply) {
handleSetReply(reply, setCb);
},
handleException,
"set %s %s", sKey.c_str(), sValue.c_str());
}
static void handleGetReply(const redisReply& reply,
const CRedis::ReplyStringCb& hdlStrReply)
{
if (!hdlStrReply) return;
using Rt = CRedis::ReplyType;
if (reply.type == REDIS_REPLY_NIL)
{
hdlStrReply(Rt::NIL, "");
return;
}
std::string sReply(reply.str, reply.len);
if (reply.type == REDIS_REPLY_STRING)
hdlStrReply(Rt::OK, sReply);
else
hdlStrReply(Rt::ERR, sReply);
}
void CRedis::Get(const std::string& sKey, const ReplyStringCb& hdlStrRepl)
{
assert(m_pCluster);
Cmd::commandf2(*m_pCluster, sKey,
[hdlStrRepl](const redisReply& reply) {
handleGetReply(reply, hdlStrRepl);
},
handleException,
"get %s", sKey.c_str());
}
handleException 是Cmd::command() 接口中需要的异常处理,这里是尽量重试。
Cmd::command() 中的第3个参数是 redis 应答的回调,读取 redisReply, 然后触发命令的回调。
CRedis::Get() 执行 redis GET 命令,固定1个参数,返回是字符串,nil, 或错误。
enum class ReplyType
{
OK = 0, // redis replys string/integer/array
NIL = 1, // redis replys nil
ERR = 2, // redis replys error status
};
// 简单的常用命令会自动解析reply, 使用更易用的回调。
using ReplyStringCb = function<void (ReplyType, const string& sReply)>;
CRedis::Get() 的回调就是 ReplyStringCb。
void CRedis::Set() 的回调只需知道成功或失败,SetCb 定义为:
using SetCb = function<void (bool ok)>;
失败时会有错误信息,已统一打印日志,不再暴露出来。
SET 命令完整的参数列表还有其他参数:
set key value [EX seconds] [PX milliseconds] [NX|XX]
因为常用的就 "set key value", 其他扩展的命令需要使用通用的 command() 接口,
并需要读取 redisReply.
使用 LuaIntf 导出 CRedis 到 Lua.
因为只有一个 CRedis, 所以导出为模块 c_redis:
#include <LuaIntf/LuaIntf.h>
namespace {
void Set(const string& sKey, const string& sValue, const LuaRef& luaSetCb)
{
// Default is empty callback.
auto setCb = ToFunction<CRedis::SetCb>(luaSetCb);
GetRedis().Set(sKey, sValue, setCb);
}
void Get(const string& sKey, const LuaRef& luaReplyStringCb)
{
auto replyStringCb = ToFunction<CRedis::ReplyStringCb>(
luaReplyStringCb);
GetRedis().Get(sKey, replyStringCb);
}
} // namespace
void Bind(lua_State* L)
{
LuaBinding(L).beginModule("c_redis")
.addFunction("set", &Set)
.addFunction("get", &Get)
.endModule();
}
需要将 lua 的回调函数转成 cpp 的回调:
template <class Function>
Function ToFunction(const LuaIntf::LuaRef& luaFunction)
{
// Default is empty.
if (!luaFunction)
return Function(); // skip nil
if (luaFunction.isFunction())
return luaFunction.toValue<Function>(); // Todo: catch
LOG_WARN_TO("ToFunction", "Lua function expected, but got "
<< luaFunction.typeName());
return Function();
}
Lua 这样调用:
c_redis.set("FOO", "1234")
c_redis.set("FOO", "1234", function(ok) print(ok) end)
c_redis.get("FOO", function(reply_type, reply)
assert("string" == type(reply))
if 0 == reply_type or 1 == reply_type then
print("FOO="..reply)
else
print("Error: "..reply)
end
end)