配套概览见 ../modules/libtaomee.md · 协议 ../protocol.md · 总入口 ../../README.md
本文是概览的"深水区"补充。概览回答"它是什么";本文回答"它在运行时到底怎么跑、跟谁交换数据、算法怎么算"。
取证方式:本模块在仓库里是两个发布版 tar.gz——Libtaomee-0.7.5.tar.gz(C 版)与 Libtaomee++-0.7.2.tar.gz(C++ 版)。tar 内文件无法直接 GitHub 链接,下文凡形如 libtaomee/interprocess/shmq.c:L145 的引用都来自解包到临时目录后真实 Read 的行号(只解到 /tmp,未释放进仓库)。凡能落到仓库工作树里(各游戏对框架的真实调用)的引用,用可点击的 ../../<路径>#L行号。
libtaomee 不是一个跑起来的进程,而是被各游戏服务进程编译进去的工具箱。它的代码运行在调用方进程的地址空间里。真正"在多个进程之间流动数据"的只有一件:shmq 共享内存队列。
┌────────────────────────────────────────────────────────┐
│ 一个游戏服(如 mole/gamesvr、gf/gongfu_online) │
│ ── 进程内直接 #include + -ltaomee / -ltaomee++ 链接 ── │
│ │
│ 收发包: proto_base(字节序) ─▶ 业务 ─▶ qdes(DES加密票据) │
│ 时间轴: timer(秒/毫秒级回调) + time(now/tm_cur 全局) │
│ 配置: conf_parser/config(C) · xmlparser/item_manager │
│ (C++,把 items.xml 解析成内存表) │
│ 日志: log(旧) / tlog(新,带 gameid+svrtype+uid 追踪) │
│ 合规: tm_dirty(AC自动机脏词) · verification_image(验证码)│
└───────────────┬──────────────────────────────────────────┘
│ shm_cq_push / shm_cq_pop
│ (POSIX shm_open + mmap 的环形队列)
▼
┌────────────────────────────────────────────────────────┐
│ 另一进程(同机): dispatcher⇄worker、写库进程、统计 agent │
│ 单读单写,一条队列只许一个进程 push、另一个进程 pop │
└────────────────────────────────────────────────────────┘
│ tlog 写盘 + tm_dirty/stat_agent 走 UDP
▼
磁盘日志目录 / 运维上报服务器
要点:框架的"数据流"基本只在 shmq 这一层是跨进程的;其余(加密、时间、配置、日志)都是进程内的库函数调用。理解 libtaomee 的运行行为 = 理解 shmq 怎么在两个进程间安全传包 + qdes 怎么算加密 + 各游戏怎么把 XML 配置吃进内存表。
这是整个框架里唯一真正"跨进程搬数据"的部件,也是最值得细读的。头文件开宗明义:shmq.c:L4 "目前的 shm_cq 只适用于一个进程只读,另外一个进程只写"。
它在共享内存里铺了什么(shmq.c:L34-46 的 shm_cq + L20-23 的 shm_block):
- 队头结构
shm_cq含head/tail(都是volatile uint32_t偏移量)、shm_size、elem_max_sz、name[64]。head/tail 用 volatile 是因为读写分属两个进程,要防编译器把它缓存进寄存器。 - 每条数据是一个
#pragma pack(1)的shm_block:4 字节len+ 变长data[]。len把"块头 4 字节"也算进去(shmq.c:L149elem_len = len + sizeof(shm_block_t))。
创建/依附(shmq.c:L40-114):
shm_cq_create(L40)=shm_open(O_CREAT|O_EXCL)+ftruncate撑大 +mmap(MAP_SHARED),然后把 head=tail=sizeof(shm_cq_t)(数据区从结构体之后开始)。写进程调它。shm_cq_attach(L87)有个巧妙两段式:先只 mmap 一个sizeof(shm_cq_t)拿到shm_size,munmap 后再用真实大小 mmap 一次(L95-106)——因为 attach 方事先不知道队列多大。读进程调它。
push(写进程,shmq.c:L145-161)实际怎么跑:
- 算
elem_len(数据+4字节块头)。 shm_cq_align_tail(L187):如果 tail 到缓冲区末尾的剩余空间够,直接进shm_cq_push_wait;不够,就尝试环回——在末尾如果还塞得下 4 字节,写一个len=0xFFFFFFFF的"哨兵填充块"(L196-199)标记"此后到末尾是废料,读端请跳回开头",然后把 tail 重置回sizeof(shm_cq_t)。shm_cq_push_wait(L222):自旋判断 head 是否离 tail 够远(head >= tail + len + elem_max_sz,L229),多留一个elem_max_sz是"防止覆盖读端当前正引用的那块"。最多自旋 10 次、每次usleep(5),满了就返回 -1(push 失败)。- 真正写入:
shm_cq_tail()定位 tail,写len、memcpy(data)、tail += elem_len(L154-158)。注意:tail 的推进是最后一步,保证读端只会看到完整写好的块。
pop(读进程,shmq.c:L121-143):
head == tail即空,返回 0。shm_cq_align_head(L174):如果 head 走到了缓冲区尾部不足一个块头、或撞上0xFFFFFFFF哨兵(L182),就把 head 跳回sizeof(shm_cq_t)(对应 push 端的环回)。*data = cur_mb->data(直接给出共享内存内的指针,零拷贝)、head += cur_mb->len,返回len - 4(去掉块头的纯数据长度)。
它跟谁通信:任意两个同机进程,只要约定同一个 name。摩尔庄园/功夫派等都把它当作 dispatcher↔worker、在线服↔写库进程之间传协议包的管道。各游戏 bench.conf 里大量出现 shmq 相关配置(如 gf/gongfu_online/bench.conf、gf/trade_svr/bench.conf)。
坑(直接影响复活时移植):无锁、靠 volatile+操作顺序保证单读单写正确;head==q->tail 既表示空也是环回判断的基准,一条队列绝不能两个进程同时 push 或同时 pop;自旋 10 次失败即丢——上层必须处理 push 返回 -1。samples 里附了最小可复现的读写对:push 示例(tar 内 interprocess/shmq_push.c:L29,shm_cq_create("shmcq_test",1000,600) 后 while(shm_cq_push(...)) 死等)与 shmq_pop.c:L28(shm_cq_attach 后死循环 shm_cq_pop)。
qdes.c 是一份精简的 DES ECB 实现(从 OpenSSL 风格的 PERM_OP/HPERM_OP 宏 + IP/FP 置换表移植而来,qdes.c:L16-19)。对外只暴露 4 个函数:
des_encrypt(key, s_text, d_text)(qdes.c:L424)=des_set_key生成 16 轮子密钥 +des_ecb_encrypt(...,1),加密恰好 8 字节一块。des_decrypt(L431)同上,最后参数传 0 即解密。des_encrypt_n/des_decrypt_n(L438/L453):对 8 字节的 n 倍数据循环跑 ECB(for里s_text += 8; d_text += 8,L446-450)。纯 ECB、无 IV、无填充——每个 8 字节块独立加密。
key 只取前 8 字节(头注释 qdes.h:L20),这正好对应淘米到处用的"8 字符串当 key"的习惯。
它实际被谁用、用哪把 key——这里有一个概览里值得校正的细节:
- 框架自带的"登录 session 加密 key"在
libtaomee/project/constants.h:L35-39:默认LOGIN_DES_KEY = "!tA:mEv,",台湾版(TW_VER)=,.ta0me>。这是框架层面给登录 session 用的约定 key。 - 但摩尔庄园 gamesvr 另有一把自己的 key 用于"积分票据(score session)":mole/gamesvr/game.c #L324-L326 里
des_key = ">ta:mee<"(TW_VER时为,.ta0me>),用在 game.c #L320-L345 的pack_score_session——把"time+ip / time+score / userid+gameid"各凑成 8 字节,用des_encrypt加密成三段拼进gs->sess(#L335/#L339/#L344)。
所以同一套 DES ECB 实现下,至少存在两把不同的 8 字节 key:!tA:mEv,(框架登录 session)与 >ta:mee<(mole 小游戏积分票据),台湾版都切到 ,.ta0me>。复活时要按"加密的是哪种票据"选对应 key,不能一把通吃。
它跟谁通信:不通信,纯计算函数。但它产出的密文(session/票据)会随协议包发给客户端 / 在服务间传递,是协议层能不能对得上的硬契约。
- 旧 log(
libtaomee/log.h/log.c):9 级、按时间自动轮转、每条 ≤8000 字节、可开多线程写。c/框架的 README 注明其日志就是抄自老版 Libtaomee。 - 新 tlog(
libtaomee/tlog/,0.7.4+):核心是一条带"业务身份"的日志。初始化宏INIT_DEFAULT_LOGGER_SYSTEM(logdir, pfx, svc, gameid, svrtp)(tar 内tlog/tlog.h:L10)把 服务名 / gameid / 服务器类型 一起注册(init_looger_svc_info,tlog.h:L28)——这意味着日志里天然带"哪个游戏、哪类服务器"标签,是淘米多游戏统一运维的产物。 - 写日志走
write_to_logger(lvl, logtype, uid, flag, fmt, ...)(tlog.h:L31):带 uid,配合is_traced_uid(tlog.h:L38)可只追踪被标记的玩家。 - 配套在线工具在 tar 内
tlog/tools/:set_trace_uid、set_trace_addr、set_rate_limit、ctrl_tlog——运行中不重启即可"打开对某 uid 的全量日志""按地址追踪""限流",是线上排障设施。
它跟谁通信:主要写本地磁盘;trace 工具通过控制通道(ctrl_tlog)动态改运行中进程的日志行为。
time/time.h维护两个全局变量struct timeval now与struct tm tm_cur(在 timer.h 里extern,tar 内timer.h:L30-31)。整套淘米服务都靠"主循环每帧刷一次 now"来避免频繁gettimeofday——业务代码里到处直接读now.tv_sec。timer.h提供秒级(timer_struct,L48)与毫秒级(micro_timer_struct)两套定时器,都用libtaomee/list.h的内核风格侵入式链表挂接。回调签名int (*timer_cb_func_t)(void*, void*)(timer.h:L37),回调返回 0 表示到期后删除该定时器,非 0 表示保留(L35注释)——这是"一次性 vs 周期性"定时器的开关。- 关键函数:
setup_timer/destroy_timer(L102/L107)、scan_seconds_timer/scan_microseconds_timer(L109/L110,主循环每帧调用扫描到期项)、register_timer_callback(nbr, cb)(L353)按"类型编号"登记回调,定时器结构里只存func_indx(timer.h:L56)而不存函数指针——这样定时器能放进共享内存/序列化而不怕地址失效。 timer_struct里有sprite_list(timer.h:L50)字段,直白暴露了它服务于"精灵/玩家对象"的定时逻辑(摩尔庄园的 sprite)。
它跟谁通信:进程内主循环。是几乎所有游戏逻辑(buff 到期、采集冷却、定时刷怪)的时间底座。
libtaomee/inet/:tcp.c(阻塞/非阻塞切换、收发超时、accept/connect 封装,tcp.h:L24-71)、mcast.c(组播 socket,mole 用它做跨服广播,见 mole/gamesvr/mcast.c)、inet_utils(is_legal_ip/port)、ipaddr_cat.h(按网卡名取本机 IP)。都是薄封装,服务于"服务怎么把 socket 建起来"。
libtaomee/tm_dirty/:核心是 acsm.c(Aho-Corasick 多模式串匹配自动机),一次扫描就能在文本里命中词库 tm_dirty.dat 中的所有敏感词;还处理 UTF-8 标点、支持在线更新词库;命中"广告"类时通过 UDP 上报到广告检测服务器。是聊天/昵称/UGC 合规链路的入口,各游戏 bench.conf 需配脏词服务器地址。
constants.h:base_user_id = 50000(起始米米号)、base_guest_id = 2000000001(起始游客号)(constants.h:L21-26),以及上文的LOGIN_DES_KEY。这些是所有淘米游戏共享的账号体系常量。types.h:typedef uint32_t userid_t(types.h:L26)——全淘米用 32 位无符号整数当用户号。stat_agent/msglog:统计打点(走 hash_table + shm + UDP agent)。
conf_parser/xmlparser.hpp:libxml2 的模板化封装。核心是一组get_xml_prop模板(xmlparser.hpp:L55起):get_xml_prop_arr把一个属性值按 dec/oct/hex 读进数组、get_xml_prop/get_xml_prop_def(带默认值)读单值,解析失败抛XmlParseError(继承std::logic_error)。这是各游戏读配置表的统一入口。conf_parser/ini_parser.hpp:IniParser,readme 示例用的就是它。conf_parser/item_manager.hpp:把 items.xml 解析成内存二维表的公共模板(详见下一节)。
ItemManager<ITEM, MAX_CAT, MAX_ELEM>(tar 内 item_manager.hpp:L198)是 C++ 侧最有料的设计,gf 把它当物品系统的地基。它实际怎么跑:
数据布局:成员是一个编译期定死大小的二维数组 ITEM items_[MAX_CAT][MAX_ELEM](item_manager.hpp:L242)。物品 ID 编码规则写死在 item_base_item_id = 100000(L224):
- 存:类别 cat 的物品基号 =
cat * 100000 + 1(L318),物品落在items_[cat][id - base_id]。 - 取:
get_item(id)(L260)直接算idx1 = (id-1)/100000、idx2 = (id-1)%100000,边界 +id校验后返回指针——纯下标寻址,O(1),不查哈希表。这是用"ID 段编码 = 数组下标"换极致查表速度的经典做法。
解析流程(reload_items,L272):xmlReadFile(..., XML_PARSE_NOBLANKS) → 遍历根节点下的 <Cat>(读 ID/DbCatID/Max 属性,L294-298)→ load_an_item(L315)遍历每个 <Item>,读 ID/Max/Price/SellPrice 和两个位标志属性:
Tradability(默认 3):bit1=全员可买、bit2=全员可卖。VipTradability(默认 3):bit1/bit2=VIP 可买/可卖。VipOnly(默认 0):仅 VIP 可用。 这些用bitmanip的set_bit_on/test_bit_on压进一个uint8_t flag_(Item类,L82-102、L173)。
扩展点:load_an_item 的最后会调 virtual int load_item_extended_info(item, cur)(L240、L365),默认返回 0(只解析公共字段)。游戏要解析自己特有的物品字段,就继承 ITEM + 覆盖这个虚函数——这是"公共部分统一、私有部分各游戏自定义"的契约。
gf 的真实用法(落到仓库工作树,可点击):功夫派把这套模板整份 vendoring 进自己的 gf/kf_common/src/item_manager.hpp 并大幅扩写(框架原版 379 行,gf 版超过 1261 行,加入了 load_item_extended_info(GfItem*, ...) 的实体实现,见 #L829、#L988)。然后 gf/kf_common/src/item_impl.hpp #L19 定义:
const int max_cat = 30;
const int max_elem = 100000;
class GfItemManager : public ItemManager<GfItem, max_cat, max_elem> { ... };即功夫派物品空间 = 30 类 × 10 万件的静态二维数组,实例化在各服:gf/battle_svr/dll_interface.cpp #L98 items = new GfItemManager("./conf/items_nor.xml", ...)、gf/trade_svr/item.cpp #L11 全局 GfItemManager* items。这本身是一个值得记的发现:gf 不是简单继承框架的 item_manager,而是把它 fork 出去自己魔改了 —— 说明框架的"公共部分"在落地时被各游戏当成可改的样板,而非锁死的库。
复述上文最精妙的一处:环形队列在 tail 撞到缓冲区尾、剩余空间塞不下整块时,不是简单从头覆盖,而是:
- 若末尾还能写下 4 字节块头,就写一个
len = 0xFFFFFFFF的填充块作哨兵(shmq.c:L196-199); - tail 重置回数据区起点(
L200)。 - 读端
shm_cq_align_head遇到这个0xFFFFFFFF或剩余不足一个块头,就把 head 也跳回起点(shmq.c:L182-184)。
配合 push 端"tail 最后才推进"和 volatile head/tail,实现了无锁单读单写的安全环回。shm_cq_push_wait 里多预留一个 elem_max_sz 的间距(L229),专门防止写端追上读端、覆盖掉读端正用 *data 引用着的那块零拷贝数据。
追一条真实链路——玩家在摩尔庄园玩完一个小游戏,服务端要把"分数"封成防篡改票据回传:
- 触发:小游戏结束,gamesvr 进入 mole/gamesvr/game.c 的积分处理,
p->waitcmd = proto_game_score(#L317)。 - 取 key:
pack_score_session(#L320)按编译宏选 key——非台湾版>ta:mee<,台湾版,.ta0me>(#L324-L326)。 - 凑 8 字节明文块 + DES 加密三段(#L329-L344):
- 块1 =
now(4字节时间) + remote_ip(4字节)→des_encrypt; - 块2 =
now + score→des_encrypt; - 块3 =
userid + gameid→des_encrypt; 每段 8 字节密文PKG_STR拼进gs->sess。
- 块1 =
- DES 实现:三次
des_encrypt全部落到框架 Libtaomee-0.7.5.tar.gz 内crypt/qdes.c:L424的 ECB 实现(des_set_key+des_ecb_encrypt)。 - 回包:
gs->sess(24 字节 = 3×8)随积分协议包发回客户端,字节序遵循协议约定(C++ 侧若参与则走proto_base.h的小端约定,见 ../protocol.md)。 - 校验闭环:积分入库/积分服收到票据时,用同一把 key
des_decrypt还原出 time/ip/score/uid/gameid,核对一致性——time+ip 防重放、uid+gameid 防冒名,这就是"积分不可被客户端随意伪造"的实现。
这条链路对复活的意义:要让真实 Flash 客户端的积分上报对得上,复活服务端必须复刻"3 个 8 字节块的精确字段顺序 + 同一套 DES ECB + 正确的 key"。字段顺序、key、ECB 三者缺一,客户端票据就解不开。
能直接用 / 必须复刻(硬契约):
- DES ECB 算法:
crypt/qdes.c是标准 DES ECB,任何语言都有等价实现(Nodecrypto的des-ecb、Rustdescrate、Gocrypto/des),关键是无 IV、无填充、8 字节一块、key 只取前 8 字节。 - key 表(逐场景):登录 session =
!tA:mEv,(台湾版,.ta0me>,见project/constants.h:L35);mole 小游戏积分票据 =>ta:mee<(台湾版,.ta0me>,见 game.c #L324)。不要把概览里只提到的>ta:mee<当成唯一 key——它只是积分票据那把。 - 积分票据字段布局:严格照 game.c #L329-L344 的"time+ip / time+score / uid+gameid"三块顺序。
- 账号常量:
base_user_id=50000、base_guest_id=2000000001、userid_t = uint32_t,照搬即可与真实数据/客户端假设一致。
要自己补 / 替换的:
- shmq:复活若用单进程(Rust async / Node 单进程)就不需要 shmq,它是淘米多进程架构的产物;若坚持多进程,直接用 POSIX shm 复刻这套单读单写环形队列即可,逻辑见本文第 2 节,注意自旋失败要丢/重试。
- item_manager:配置解析思路(ID 段编码 = 数组下标的 O(1) 物品表、
Tradability/VipTradability/VipOnly位标志、公共字段+扩展虚函数)非常值得照搬,但 XML 表本身是 gf 的,摩尔庄园物品表需另找(见 ../content-data.md)。 - timer / time:"全局 now + 每帧扫描定时器链表 + 回调返回值决定一次性/周期"的模型可直接照抄到任意事件循环里。
- tm_dirty / verification_image / tlog / inet:属运维与合规设施,对"先把游戏跑起来"非必需,后置。
坑:整套面向 gcc4.1.2 / 32-64 位 Linux,大量 __attribute__((packed))、内核风格链表、volatile-而非原子-的并发假设;移植到现代环境/语言时,packed 结构的字节序与对齐、shmq 的内存序保证都要重新审视,不能假定原样可移植。
返回:总入口 README · 概览:libtaomee 模块文档 · 相关:c 底座库 · mole 摩尔庄园 · gf 功夫派 · protocol 协议专题 · content-data 内容数据