接上回,按照我們一期項目的需求,昨天我簡單設計了資料庫裡的資料格式。資料庫采用的是 Redis ,我把它看成一個遠端的資料結構儲存裝置。它提供基本的 Key-Value 儲存功能,沒有層級表。如果需要兩層結構,可以在 Value 裡儲存一組 Hashes 。
這是我第一次實戰使用 Redis ,沒有什麼經驗。不過類似的設施幾年前自己實作過,差別不大。經過這幾年,有了 Redis 這個開源項目,就不需要重造輪子了。但其模式還是比較熟悉的。也就是說,是按我曆史經驗來使用 Redis 。
一期項目需要比較簡單,不打算把資料拆分到不同的資料伺服器上。但為日後的拆分需求做好設計準備。以後有需要,可以按 Key 的字首把資料分到不同的位置。例如,account 資訊是最可能獨立出去的,因為它和具體遊戲無關。
使用者系統使用 email 來做使用者名,但在資料庫中的唯一辨別是一個 uid 。使用者應該允許修改登陸名(使用者很可能更換 email)。使用者的身份識别是用 id 來定位的。是以,在資料庫中就應該有如下幾組 Key :
- account:count id
- account:userlist set(id)
- account:email:[email] id
這裡,account:userlist 對應的 value 是一個 set ,裡面存放了所有存在的 user id 。用于周遊所有的 user 。這個暫時可能用不上,而且當使用者量相當大的時候可能有問題。不過暫時不用考慮這麼多問題,等以後改進。
account:count 是一個計數器,可以用來生成唯一 id 。
account:email:[email] 用來标示每個注冊的 account 的登陸名。[email] 指登陸用 email 位址。
這裡,email 内可能也存在符号 ":" ,為了回避這個問題,許多對 email 進行編碼。我的方案是,将字母數字 @ . _ 之外的字元編碼為 %XX 的形式。用 lua 幹這件事情非常簡單:
local function _encode(str)
return string.format("%%%02X",string.byte(str))
end
function emailEncode(str)
return string.gsub(str,"([^%[email protected]])",_encode)
end
當然,解碼回來也很簡單
local function _decode(str)
return string.char(tonumber(str,16))
end
function emailDecode(str)
return string.gsub(str,"%%(%w%w)",_decode)
end
之後,就是 account 下每個 id 的資料:
- account:[id]:version number
- account:[id]:email string
- account:[id]:password string // md5(password..salt)
- account:[id]:nickname string
- account:[id]:lastlogin hashes
- ip string
- time string
- account:[id]:history list(string)
- account:[id]:available enum(open/locked/delete)
其中,密碼不想儲存為明文。因為任何可能的資料洩露都會導緻使用者的損失,我也不想任何人看到使用者的密碼。是以采用 md5(password .. salt) 的風格。
md5 運算前,加一個 salt 字尾,是因為單純的文本 md5 值也是有資料庫可查的。
lastlogin 下儲存了使用者最後一次登陸的資訊,使用了一張 hashes 表,因為這些資訊在未來會進一步擴充。
history 儲存了使用者登陸的所有曆史記錄,用一個 string 連結清單記錄。
使用者删除自己的賬戶時,不想把資料從資料庫删除,隻想在 available 下做一個标記。
考慮到資料庫内資料結構有可能發生變化,是以加了 version 域做版本辨別。
我不想讓各種服務可以直接讀寫這份資料,是以,會單獨寫一個認證伺服器做處理。
認證伺服器提供三項服務:
- 使用者注冊
- 使用者名 密碼 認證 (用于 ssl 連接配接上的 web 服務)
- 使用者名 密碼 挑戰式認證 (用于 client 的認證服務)
下面是基本的場景服務用的資料:
- account:[id]:avatars set(id)
- avatar:count id
- avatar:[id]:version number
- avatar:[id]:account id
- avatar:[id]:scene string
- avatar:[id]:available enum(open/delete)
- avatar:[id]:data hashes
- name string
- figure string
- world:scene hashes
- [name] id
- scene:count id
- scene:[id]:name string
- scene:[id]:available enum(open/close/delete)
- scene:[id]:info hashes
- time string
- pc number
- scene:[id]:pc hashes
- [id] enum[online/offline]
- scene:[id]:pc:[id] hashes
- status string
使用者賬号下可以有許多遊戲角色,清單放在 account:[id]:avatars 下。
每個角色也擁有一個唯一 id 。這個 id 原則上和 account id 是獨立體系,但是為了人類好區格,avatar:count 的起點和 account:count 不同。
角色所在場景記錄一個字元串的場景名 avatar:[id]:scene ,角色的其它各種資料放在一個 hashes 裡。
所有的場景索引方在 world:scene 下。如果日後有多個世界,可以采用 world:[id]:scene 。但目前不必考慮。
scene 下面的所有 pc 的線上狀态放在 scene:[id]:pc hashes 中,pc 離線也把它的 id 記錄在内,隻有 pc 轉移場景才移除。
每個 PC 的位置狀态資訊記錄在 scene:[id]:pc:[id] 中,第一個 id 是 scene 的 id ,第二個則是 PC 的 avatar id 。
btw. 這是一份草稿,雖然思考不周,但足夠滿足項目一期的需求。當然許多欠考慮的地方也并非是考慮不到,而是希望盡量簡單,以滿足一期需求為目的。這個日後修改的代價并不大。
最後吐槽一下 Redis 的 Windows 版。辦公室的 Linux 伺服器還沒有裝好,我暫時在 Windows 下做開發。取了一份 google 搜到的 非官方 Redis 的 Windows 版 。為了圖友善,使用的是 luajit ffi 去調用 hiredis 的 dll 。一開始怎麼都搞不定。建立不了 socket 連接配接,出錯碼也取不到。
對比了源代碼,發現修改版把 C Struct 結構改了,前面增加了幾個域,而我以 hiredis 官方标準來定義的接口。
改好後,能夠正确取出出錯碼了。發現萬惡的 Windows socks api 需要調用 WSAStartup 才可以用。而 hiredis 的 Windows 修改版居然沒有去調用。讓我大費周折才改好,前後折騰了一個多小時。
再吐槽一下 hiredis 的 API 設計,居然依賴 C Struct 的布局。良好的 C 庫的接口設計不會這麼幹的吧。比如 lua ,又比如 zmq 。唉,用這種東西有點小不爽。不過比 C++ 庫還是好太多了。