Scheduler (#194)

简单协程调度器的实现,详细说明请看 docs/dev/scheduler.rst
This commit is contained in:
notify 2023-06-16 10:56:33 +08:00 committed by GitHub
parent 623007aca2
commit b75d8afe62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 782 additions and 423 deletions

1
.gitignore vendored
View File

@ -1,6 +1,7 @@
# Compile output
/build/
/docs/build
/docs/.venv
/*.o
/zh_CN.qm
/fk_ver

View File

@ -19,3 +19,4 @@
ui.rst
hegemony.rst
shendiaochan.rst
schedule.rst

102
docs/dev/schedule.rst Normal file
View File

@ -0,0 +1,102 @@
.. SPDX-License-Identifier: GFDL-1.3-or-later
关于Fk在同一个Lua中运行多个游戏房间的思考
=========================================
目前Fk的实现中每个游戏房间是一根线程这就出现了一个问题
当同时游戏的房间很多的时候会给RAM带来相当大的负担毕竟lua_State太多了
每个Room都有一个。 目前比较流行的模式是只需三人就可进行的斗地主,
有时候同时运行着30多桌斗地主直接把内存推向1GB占用云服务器那才多少点资源。
如果这么多房间都在同一个Lua运行的话情况应该会有很大改观。
多个房间如何调度?
------------------
在当前阶段已经实现了重连和观战机制,这是借助于协程机制实现的。
当Lua正在执行delay或者等待用户答复时Lua会从主协程中切换出来
然后去另一个协程处理诸如旁观、重连等游戏逻辑之外的请求。
如果把这种空白时间更加充分的利用起来的话,或许就能同时执行多个房间了。
现在每个游戏房间在Lua中都是一个Room对象的实例而Room
本身是通过一个协程执行着主游戏逻辑。
考虑修改一下使得同一个Lua中能执行多个Room协程。
暂且考虑开个数组保存所有正在运行中的房间。
就绪队列
--------
调度器除了维护运行中房间的数组之外,还维护一个就绪队列。
当房间不处于阻塞状态时,或者已经可以脱离阻塞状态,那么就认为他就绪。
此外还有一个特殊的协程,他用来处理托管、旁观等请求。
当他的这个请求队列为空的时候,也视为他未就绪。
.. note::
所谓阻塞状态目前指的是正在delay或者正在等候答复时。自然而然的
脱离阻塞状态就是delay的时间已经结束或者已收到答复。
当所有房间全部未就绪时调度器调用sleep睡一段时间让出CPU。
当房间因为delay而延时时可以知道他恢复就绪的确切等待时间。但如果是等待答复、
等待新请求这种依赖玩家操作的协程,其就绪时间就完全不可预测了。
正是这种不可预测的等待时间才使得我们调度器只能小睡个几毫秒。
如何多睡一段时间?
------------------
假设有多个房处于delay中那么能睡的最长时间就是其中所有delay剩余时间的最小值。
但问题在于那些不可预测的就绪时间。如果另一个线程能告诉Lua答复已经就绪的话
那么睡眠问题就好解决的多了。
实际上可以借助信号量机制。当调度器开始睡觉时肯定是调用了cpp函数。
这个cpp函数里面先将一个信号量置为0然后再tryAcquire这个信号量一段时间。
当收到答复/收到请求时,把这个信号量+1。这样调度器就知道自己该醒过来啦。
如何避免房间等待太久?
----------------------
目前的调度思路如下:
- 若就绪队列为空,那么从所有运行中的房间进行一遍筛选。
- 若还为空,再筛选。
- 假如第二遍还空,那就睡觉。
- 假如就绪队列不空,那么弹出第一个,然后把控制权交给这个协程,等他自己让出。
在这个过程中,由于有些房间在等待就绪队列变空的过程中满足了就绪的条件,
那么再次筛选时,有可能重复的房间又会再次位于就绪队列靠前的位置。
这虽然不会导致某个房间永久等待下去的情况,但却可能让一个房间等待太长时间,
那我就又要被艾特了。
一种或许可行的解法是,在再次刷新队列的时候,优先选出上次队列里面没有的房间。
不知道这样如何呢?
怎么把现有的改成这样?
----------------------
CPP代码方面首先Room已经不再是一个线程了自然不能再当QThread的子类。
另外开个什么类来占据线程吧。
既然不是QThread的子类的话那Room::run就得改了。原本是启动线程现在还是改成
pushRequest吧。新请求立刻唤醒调度器然后调度器切到请求处理协程处理新房间请求
然后就开战!
既然要pushRequest那么原先的请求队列也要从Room中移走了这个得直接归属于线程。
当然lua_State也要从Room撤走。
Lua代码方面首先由于Engine成了所有房间共用的变量必须杜绝所有对Engine
内变量的修改。点名cost_data和card.mark这两个东西得通过__index大法托管给
Room挂着。剩下的没啥好说的当然还有实现调度器。
协程池
------
虽然题文无关但是FK确实在运行过程中不断的产生着协程。
为了避免因为不断产生新的协程导致的开销,可以使用协程池来管理。
用完的协程就存在某个数组留待下次使用。当需要启动一个新协程的时候,
先从协程池找,没有的话就新建一个。

View File

@ -4,5 +4,36 @@
inspect = require "inspect"
dbg = require "debugger"
function PrintWhere()
local info = debug.getinfo(2)
local name = info.name
local line = info.currentline
local namewhat = info.namewhat
local shortsrc = info.short_src
if (namewhat == "method") and
(shortsrc ~= "[C]") and
(not string.find(shortsrc, "/lib")) then
print(shortsrc .. ":" .. line .. ": " .. name)
end
end
--debug.sethook(PrintWhere, "l")
function Traceback()
print(debug.traceback())
end
local msgh = function(err)
fk.qCritical(err)
print(debug.traceback(nil, 2))
end
function Pcall(f, ...)
local ret = { xpcall(f, msgh, ...) }
local err = table.remove(ret, 1)
if err ~= false then
return table.unpack(ret)
end
end
function p(v) print(inspect(v)) end
function pt(t) for k, v in pairs(t) do print(k, v) end end

View File

@ -77,7 +77,6 @@ function Player:initialize()
self.chained = false
self.dying = false
self.dead = false
self.state = ""
self.drank = 0
self.player_skills = {}

View File

@ -57,6 +57,20 @@ function Skill:initialize(name, frequency)
self.attached_equip = nil
end
function Skill:__index(k)
if k == "cost_data" then
return Fk:currentRoom().skill_costs[self.name]
end
end
function Skill:__newindex(k, v)
if k == "cost_data" then
Fk:currentRoom().skill_costs[self.name] = v
else
rawset(self, k, v)
end
end
function Skill:__tostring()
return "<Skill " .. self.name .. ">"
end

View File

@ -26,7 +26,9 @@ Util.lockTable = function(t)
return setmetatable({}, new_mt)
end
function printf(fmt, ...) print(string.format(fmt, ...)) end
function printf(fmt, ...)
print(string.format(fmt, ...))
end
-- the iterator of QList object
local qlist_iterator = function(list, n)
@ -307,34 +309,6 @@ function string:endsWith(e)
return e == "" or self:sub(-#e) == e
end
---@class Sql
Sql = {
---@param filename string
open = function(filename)
return fk.OpenDatabase(filename)
end,
---@param db fk.SQLite3
close = function(db)
fk.CloseDatabase(db)
end,
--- Execute an SQL statement.
---@param db fk.SQLite3
---@param sql string
exec = function(db, sql)
fk.ExecSQL(db, sql)
end,
--- Execute a `SELECT` SQL statement.
---@param db fk.SQLite3
---@param sql string
---@return table[] @ Array of Json object, the key is column name and value is row value
exec_select = function(db, sql)
return json.decode(fk.SelectFromDb(db, sql))
end,
}
FileIO = {
pwd = fk.QmlBackend_pwd,
ls = function(filename)

View File

@ -8,42 +8,12 @@ FPlayer = {}
---@return integer id
function FPlayer:getId()end
---@param id integer
function FPlayer:setId(id)end
---@return string name
function FPlayer:getScreenName()end
---@param name string
function FPlayer:setScreenName(name)end
---@return string avatar
function FPlayer:getAvatar()end
---@param avatar string
function FPlayer:setAvatar(avatar)end
---@return string state
function FPlayer:getStateString()end
---@param state string
function FPlayer:setStateString(state)end
---@class fk.ServerPlayer : fk.Player
FServerPlayer = {}
---@return fk.Server
function FServerPlayer:getServer()end
---@return fk.Room
function FServerPlayer:getRoom()end
---@param room fk.Room
function FServerPlayer:setRoom(room)end
---@param msg string
function FServerPlayer:speak(msg)end
--- Send a request to client, and allow client to reply within *timeout* seconds.
---
--- *timeout* must not be negative or **nil**.

View File

@ -1,52 +1 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---@meta
---@class fk.Server
FServer = {}
---@type fk.Server
fk.ServerInstance = {}
---@class fk.Room
--- Room (C++)
FRoom = {}
---@param owner fk.ServerPlayer
---@param name string
---@param capacity integer
function FServer:createRoom(owner,name,capacity)end
---@param id integer
---@return fk.Room
function FServer:findRoom(id)end
---@return fk.Room
function FServer:lobby()end
---@param id integer
---@return fk.ServerPlayer
function FServer:findPlayer(id)end
---@return fk.SQLite3
function FServer:getDatabase()end
function FRoom:getServer()end
function FRoom:getId()end
function FRoom:isLobby()end
function FRoom:getName()end
function FRoom:setName(name)end
function FRoom:getCapacity()end
function FRoom:setCapacity(capacity)end
function FRoom:isFull()end
function FRoom:isAbandoned()end
function FRoom:addPlayer(player)end
function FRoom:removePlayer(player)end
function FRoom:getOwner()end
function FRoom:setOwner(owner)end
function FRoom:getPlayers()end
function FRoom:findPlayer(id)end
function FRoom:getTimeout()end
function FRoom:isStarted()end
function FRoom:doBroadcastNotify(targets,command,jsonData)end
function FRoom:gameOver()end

View File

@ -1,22 +1 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
---@meta
---@class fk.SQLite3
SQLite3 = {}
---@param filename string
---@return fk.SQLite3
function fk.OpenDatabase(filename)end
---@param db fk.SQLite3
---@param sql string
---@return string jsonData
function fk.SelectFromDb(db, sql)end
---@param db fk.SQLite3
---@param sql string
function fk.ExecSQL(db, sql)end
---@param db fk.SQLite3
function fk.CloseDatabase(db)end

View File

@ -47,16 +47,6 @@ local function discardInit(room, player)
end
end
local function checkNoHuman(room)
for _, p in ipairs(room.players) do
-- TODO: trust
if p.serverplayer:getStateString() == "online" then
return
end
end
room:gameOver("")
end
GameEvent.functions[GameEvent.DrawInitial] = function(self)
local room = self.room
@ -96,6 +86,13 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self)
local currentTime = os.time()
local elapsed = 0
for _, id in ipairs(luck_data.playerList) do
local pl = room:getPlayerById(id)
if luck_data[id].luckTime > 0 then
pl.serverplayer:setThinking(true)
end
end
while true do
elapsed = os.time() - currentTime
if remainTime - elapsed <= 0 then
@ -112,14 +109,16 @@ GameEvent.functions[GameEvent.DrawInitial] = function(self)
end
for _, id in ipairs(ldata.playerList) do
if room:getPlayerById(id)._splayer:getStateString() ~= "online" then
local pl = room:getPlayerById(id)
if pl._splayer:getState() ~= fk.Player_Online then
ldata[id].luckTime = 0
pl.serverplayer:setThinking(false)
end
end
-- room:setTag("LuckCardData", ldata)
checkNoHuman(room)
room:checkNoHuman()
coroutine.yield("__handleRequest", (remainTime - elapsed) * 1000)
end

View File

@ -186,13 +186,10 @@ function GameEvent:clear()
logic.game_event_stack:pop()
local err, msg
err, msg = xpcall(self.exit_func, debug.traceback, self)
if err == false then fk.qCritical(msg) end
Pcall(self.exit_func, self)
for _, f in ipairs(self.extra_exit_funcs) do
if type(f) == "function" then
err, msg = xpcall(f, debug.traceback, self)
if err == false then fk.qCritical(msg) end
Pcall(f, self)
end
end
end

View File

@ -105,6 +105,8 @@ request_handlers["luckcard"] = function(room, id, reqlist)
if pdata.luckTime > 0 then
p:doNotify("AskForLuckCard", pdata.luckTime)
else
p.serverplayer:setThinking(false)
end
room:setTag("LuckCardData", luck_data)
@ -132,23 +134,34 @@ request_handlers["changeself"] = function(room, id, reqlist)
})
end
request_handlers["newroom"] = function(s, id)
s:registerRoom(id)
end
-- 处理异步请求的协程,本身也是个死循环就是了。
-- 为了适应调度器,目前又暂且将请求分为“耗时请求”和不耗时请求。
-- 耗时请求处理后会立刻挂起。不耗时的请求则会不断处理直到请求队列空后再挂起。
local function requestLoop(self)
local rest_time = 0
while true do
local ret = false
local request = self.room:fetchRequest()
local request = self.thread:fetchRequest()
if request ~= "" then
ret = true
local reqlist = request:split(",")
local id = tonumber(reqlist[1])
local command = reqlist[2]
request_handlers[command](self, id, reqlist)
elseif rest_time > 10 then
-- let current thread sleep 10ms
-- otherwise CPU usage will be 100% (infinite yield <-> resume loop)
fk.QThread_msleep(10)
local roomId = tonumber(table.remove(reqlist, 1))
local room = self:getRoom(roomId)
if room then
RoomInstance = room
local id = tonumber(reqlist[1])
local command = reqlist[2]
request_handlers[command](room, id, reqlist)
RoomInstance = nil
end
end
if not ret then
coroutine.yield()
end
rest_time = coroutine.yield(ret)
end
end

View File

@ -5,6 +5,8 @@
--- 一个房间中只有一个Room实例保存在RoomInstance全局变量中。
---@class Room : Object
---@field public room fk.Room @ C++层面的Room类实例别管他就是了用不着
---@field public id integer @ 房间的id
---@field private main_co any @ 本房间的主协程
---@field public players ServerPlayer[] @ 这个房间中所有参战玩家
---@field public alive_players ServerPlayer[] @ 所有还活着的玩家
---@field public observers fk.ServerPlayer[] @ 旁观者清单这是c++玩家列表,别乱动
@ -24,6 +26,7 @@
---@field public logic GameLogic @ 这个房间使用的游戏逻辑,可能根据游戏模式而变动
---@field public request_queue table<userdata, table>
---@field public request_self table<integer, integer>
---@field public skill_costs table<string, any> @ 存放skill.cost_data用
local Room = class("Room")
-- load classes used by the game
@ -62,46 +65,7 @@ dofile "lua/server/ai/init.lua"
---@param _room fk.Room
function Room:initialize(_room)
self.room = _room
_room.startGame = function(_self)
Room.initialize(self, _room) -- clear old data
self.settings = json.decode(_room:settings())
Fk.disabled_packs = self.settings.disabledPack
Fk.disabled_generals = self.settings.disabledGenerals
local main_co = coroutine.create(function()
self:run()
end)
local request_co = coroutine.create(function()
self:requestLoop()
end)
local ret, err_msg, rest_time = true, true
while not self.game_finished do
ret, err_msg, rest_time = coroutine.resume(main_co, err_msg)
-- handle error
if ret == false then
fk.qCritical(err_msg)
print(debug.traceback(main_co))
break
end
ret, err_msg = coroutine.resume(request_co, rest_time)
if ret == false then
fk.qCritical(err_msg)
print(debug.traceback(request_co))
break
end
-- If ret == true, then when err_msg is true, that means no request
end
coroutine.close(main_co)
coroutine.close(request_co)
if not self.game_finished then
self:doBroadcastNotify("GameOver", "")
self.room:gameOver()
end
end
self.id = _room:getId()
self.players = {}
self.alive_players = {}
@ -123,8 +87,119 @@ function Room:initialize(_room)
end
self.request_queue = {}
self.request_self = {}
self.skill_costs = {}
self.settings = json.decode(self.room:settings())
Fk.disabled_packs = self.settings.disabledPack
Fk.disabled_generals = self.settings.disabledGenerals
end
-- 供调度器使用的函数。能让房间开始运行/从挂起状态恢复。
function Room:resume()
-- 如果还没运行的话就先创建自己的主协程
if not self.main_co then
self.main_co = coroutine.create(function()
self:run()
end)
end
local ret, err_msg, rest_time = true, true, nil
local main_co = self.main_co
if self:checkNoHuman() then
return true
end
if not self.game_finished then
ret, err_msg, rest_time = coroutine.resume(main_co, err_msg)
-- handle error
if ret == false then
fk.qCritical(err_msg)
print(debug.traceback(main_co))
goto GAME_OVER
end
if rest_time == "over" then
goto GAME_OVER
end
return false, rest_time
end
::GAME_OVER::
coroutine.close(main_co)
self.main_co = nil
return true
end
-- 供调度器使用的函数,用来指示房间是否就绪。
-- 如果没有就绪的话,可能会返回第二个值来告诉调度器自己还有多久就绪。
function Room:isReady()
-- 没有活人了?那就告诉调度器我就绪了,恢复时候就会自己杀掉
if self:checkNoHuman(true) then
return true
end
-- 因为delay函数而延时判断延时是否已经结束。
-- 注意整个delay函数的实现都搬到这来了delay本身只负责挂起协程了。
if self.in_delay then
local rest = self.delay_duration - (os.getms() - self.delay_start) / 1000
if rest <= 0 then
self.in_delay = false
return true
end
return false, rest
end
-- 剩下的就是因为等待应答而未就绪了
-- 检查所有正在等回答的玩家,如果已经过了烧条时间
-- 那么就不认为他还需要时间就绪了
-- 然后在调度器第二轮刷新的时候就应该能返回自己已就绪
local ret = true
local rest
for _, p in ipairs(self.players) do
-- 这里判断的话需要用_splayer了不然一控多的情况下会导致重复判断
if p._splayer:thinking() then
ret = false
-- 烧条烧光了的话就把thinking设为false
rest = p.request_timeout * 1000 - (os.getms() -
p.request_start) / 1000
if rest <= 0 then
p._splayer:setThinking(false)
end
end
end
return ret, (rest and rest > 1) and rest or nil
end
function Room:checkNoHuman(chkOnly)
if #self.players == 0 then return end
for _, p in ipairs(self.players) do
-- TODO: trust
if p.serverplayer:getState() == fk.Player_Online then
return
end
end
if not chkOnly then
self:gameOver("")
end
return true
end
function Room:__tostring()
return string.format("<Room #%d>", self.id)
end
--[[ 敢删就寄,算了
function Room:__gc()
self.room:checkAbandoned()
end
--]]
--- 正式在这个房间中开始游戏。
---
--- 当这个函数返回之后整个Room线程也宣告结束。
@ -636,6 +711,12 @@ function Room:doRaceRequest(command, players, jsonData)
table.removeOne(players, p)
table.insertIfNeed(canceled_players, p)
end
-- 骗过调度器让他以为自己尚未就绪
if p.id > 0 then
p.request_timeout = remainTime - elapsed
p.serverplayer:setThinking(true)
end
end
if winner then
self:doBroadcastNotify("CancelRequest", "")
@ -657,21 +738,17 @@ function Room:doRaceRequest(command, players, jsonData)
return ret
end
Room.requestLoop = require "server.request"
--- 延迟一段时间。
---
--- 这个函数只应该在主协程中使用。
--- 这个函数不应该在请求处理协程中使用。
---@param ms integer @ 要延迟的毫秒数
function Room:delay(ms)
local start = os.getms()
while true do
local rest = ms - (os.getms() - start) / 1000
if rest <= 0 then
break
end
coroutine.yield("__handleRequest", rest)
end
self.delay_start = start
self.delay_duration = ms
self.in_delay = true
coroutine.yield("__handleRequest", ms)
end
--- 向多名玩家告知一次移牌行为。
@ -1158,7 +1235,7 @@ function Room:askForGeneral(player, generals, n)
if #generals == n then return n == 1 and generals[1] or generals end
local defaultChoice = table.random(generals, n)
if (player.state == "online") then
if (player.serverplayer:getState() == fk.Player_Online) then
local result = self:doRequest(player, command, json.encode{ generals, n })
local choices
if result == "" then
@ -2766,6 +2843,8 @@ end
--- 结束一局游戏。
---@param winner string @ 获胜的身份,空字符串表示平局
function Room:gameOver(winner)
if not self.game_started then return end
self.logic:trigger(fk.GameFinished, nil, winner)
self.game_started = false
self.game_finished = true
@ -2794,7 +2873,16 @@ function Room:gameOver(winner)
end
self.room:gameOver()
coroutine.yield("__handleRequest", 0)
if table.contains(
{ "running", "normal" },
coroutine.status(self.main_co)
) then
coroutine.yield("__handleRequest", "over")
else
coroutine.close(self.main_co)
self.main_co = nil
end
end
---@param card Card
@ -2913,6 +3001,4 @@ function Room:updateQuestSkillState(player, skillName, failed)
})
end
function CreateRoom(_room)
RoomInstance = Room:new(_room)
end
return Room

149
lua/server/scheduler.lua Normal file
View File

@ -0,0 +1,149 @@
-- SPDX-License-Identifier: GPL-3.0-or-later
local Room = require "server.room"
--[[
local verbose = function(...)
printf(...)
end
--]]
-- 所有当前正在运行的房间(即游戏尚未结束的房间)
---@type table<integer, Room>
local runningRooms = {}
-- 所有处于就绪态的房间以及request协程如果就绪的话
---@type Room[]
local readyRooms = {}
local requestCo = coroutine.create(function(room)
require "server.request"(room)
end)
-- 仿照Room接口编写的request协程处理器
local requestRoom = setmetatable({
-- minDelayTime 是当没有任何就绪房间时,可以睡眠的时间。
-- 因为这个时间是所有房间预期就绪用时的最小值故称为minDelayTime。
minDelayTime = -1,
getRoom = function(_, roomId)
return runningRooms[roomId]
end,
resume = function(self)
local err, msg = coroutine.resume(requestCo, self)
if err == false then
fk.qCritical(msg)
print(debug.traceback(requestCo))
end
return nil, 0
end,
isReady = function(self)
return self.thread:hasRequest()
end,
registerRoom = function(self, id)
local cRoom = self.thread:getRoom(id)
local room = Room:new(cRoom)
runningRooms[room.id] = room
end,
}, {
__tostring = function()
return "<Request Room>"
end,
})
runningRooms[-1] = requestRoom
-- 从所有运行中房间中挑出就绪的房间。
-- 方法暂时就是最简单的遍历。
local function refreshReadyRooms()
-- verbose '[+] Refreshing ready queue...'
for k, v in pairs(runningRooms) do
local ready, rest = v:isReady()
if ready then
table.insertIfNeed(readyRooms, v)
elseif rest and rest >= 0 then
local time = requestRoom.minDelayTime
time = math.min((time <= 0 and 9999999 or time), rest)
requestRoom.minDelayTime = math.ceil(time)
end
end
-- verbose('[+] now have %d ready rooms...', #readyRooms)
end
-- 主循环。只要线程没有被杀掉,就一直循环下去。
-- 函数每轮循环会从队列中取一个元素并交给控制权,
-- 如果没有,则尝试刷新队列,无法刷新则开始睡眠。
local function mainLoop()
-- request协程的专用特判变量。因为处理request不应当重置睡眠时长
local rest_sleep_time
while not requestRoom.thread:isTerminated() do
local room = table.remove(readyRooms, 1)
if room then
-- verbose '============= LOOP =============='
-- verbose('[*] Switching to %s...', tostring(room))
RoomInstance = (room ~= requestRoom and room or nil)
local over, rest = room:resume()
RoomInstance = nil
if over then
-- verbose('[#] %s is finished, removing ...', tostring(room))
room.logic = nil
runningRooms[room.id] = nil
else
local time = requestRoom.minDelayTime
if room == requestRoom then
rest = rest_sleep_time
end
if rest and rest >= 0 then
time = math.min((time <= 0 and 9999999 or time), rest)
else
time = -1
end
requestRoom.minDelayTime = math.ceil(time)
-- verbose("[+] minDelay is %d ms...", requestRoom.minDelayTime)
-- verbose('[-] %s successfully yielded, %d ready rooms left...',
-- tostring(room), #readyRooms)
end
else
refreshReadyRooms()
if #readyRooms == 0 then
refreshReadyRooms()
if #readyRooms == 0 then
local time = requestRoom.minDelayTime
-- verbose('[.] Sleeping for %d ms...', time)
local cur = os.getms()
-- 调用RoomThread的trySleep函数开始真正的睡眠。会被wakeUp(c++)唤醒。
requestRoom.thread:trySleep(time)
-- verbose('[!] Waked up after %f ms...', (os.getms() - cur) / 1000)
if time > 0 then
rest_sleep_time = math.floor(time - (os.getms() - cur) / 1000)
else
rest_sleep_time = -1
end
requestRoom.minDelayTime = -1
end
end
end
end
-- verbose '=========== LOOP END ============'
-- verbose '[:)] Goodbye!'
end
-- 当Cpp侧的RoomThread运行时以下这个函数就是这个线程的主函数。
-- 而这个函数里面又调用了上面的mainLoop。
function InitScheduler(_thread)
requestRoom.thread = _thread
Pcall(mainLoop)
end

View File

@ -24,7 +24,6 @@ function ServerPlayer:initialize(_self)
self._splayer = _self -- 真正在玩的玩家
self._observers = { _self } -- "旁观"中的玩家,然而不包括真正的旁观者
self.id = _self:getId()
self.state = _self:getStateString()
self.room = nil
-- Below are for doBroadcastRequest
@ -86,46 +85,36 @@ function ServerPlayer:doRequest(command, jsonData, timeout)
self.serverplayer:doRequest(command, jsonData, timeout)
end
local function checkNoHuman(room)
for _, p in ipairs(room.players) do
-- TODO: trust
if p.serverplayer:getStateString() == "online" then
return
end
end
room:gameOver("")
end
local function _waitForReply(player, timeout)
local result
local start = os.getms()
local state = player.serverplayer:getStateString()
if state ~= "online" then
if state ~= "robot" then
checkNoHuman(player.room)
local state = player.serverplayer:getState()
player.request_timeout = timeout
player.request_start = start
if state ~= fk.Player_Online then
if state ~= fk.Player_Robot then
player.room:checkNoHuman()
player.room:delay(500)
return "__cancel"
end
-- Let AI make reply. First handle request
local ret_msg = true
while ret_msg do
-- when ret_msg is false, that means there is no request in the queue
ret_msg = coroutine.yield("__handleRequest", 1)
end
-- coroutine.yield("__handleRequest", 0)
checkNoHuman(player.room)
player.room:checkNoHuman()
player.ai:readRequestData()
local reply = player.ai:makeReply()
return reply
end
while true do
player.serverplayer:setThinking(true)
result = player.serverplayer:waitForReply(0)
if result ~= "__notready" then
player.serverplayer:setThinking(false)
return result
end
local rest = timeout * 1000 - (os.getms() - start) / 1000
if timeout and rest <= 0 then
player.serverplayer:setThinking(false)
return ""
end
coroutine.yield("__handleRequest", rest)
@ -277,7 +266,7 @@ end
function ServerPlayer:reconnect()
local room = self.room
self.serverplayer:setStateString("online")
self.serverplayer:setState(fk.Player_Online)
self:doNotify("Setup", json.encode{
self.id,

View File

@ -11,6 +11,7 @@ set(freekill_SRCS
"server/server.cpp"
"server/serverplayer.cpp"
"server/room.cpp"
"server/roomthread.cpp"
"ui/qmlbackend.cpp"
"swig/freekill-wrap.cxx"
)

View File

@ -3,6 +3,7 @@
#include "router.h"
#include "client.h"
#include "client_socket.h"
#include "roomthread.h"
#include <qjsondocument.h>
#ifndef FK_CLIENT_ONLY
#include "server.h"
@ -311,6 +312,11 @@ void Router::handlePacket(const QByteArray &rawPacket) {
else if (type & TYPE_REPLY) {
QMutexLocker locker(&replyMutex);
ServerPlayer *player = qobject_cast<ServerPlayer *>(parent());
player->setThinking(false);
// qDebug() << "wake up!";
player->getRoom()->getThread()->wakeUp();
if (requestId != this->expectedReplyId)
return;
@ -328,6 +334,7 @@ void Router::handlePacket(const QByteArray &rawPacket) {
extraReplyReadySemaphore->release();
extraReplyReadySemaphore = nullptr;
}
locker.unlock();
emit replyReady();
}

View File

@ -5,48 +5,56 @@
#include <qjsonarray.h>
#include <qjsondocument.h>
#include "roomthread.h"
#include "server.h"
#include "serverplayer.h"
#include "util.h"
Room::Room(Server *server) {
setObjectName("Room");
Room::Room(RoomThread *m_thread) {
auto server = ServerInstance;
id = server->nextRoomId;
server->nextRoomId++;
this->server = server;
setParent(server);
this->m_thread = m_thread;
if (m_thread) { // In case of lobby
m_thread->addRoom(this);
}
// setParent(server);
m_abandoned = false;
owner = nullptr;
gameStarted = false;
robot_id = -2; // -1 is reserved in UI logic
timeout = 15;
m_ready = true;
// 如果是普通房间而不是大厅就初始化Lua否则置Lua为nullptr
L = nullptr;
if (!isLobby()) {
// 如果不是大厅,那么:
// * 只要房间添加人了,那么从大厅中移掉这个人
// * 只要有人离开房间,那就把他加到大厅去
connect(this, &Room::playerAdded, server->lobby(), &Room::removePlayer);
connect(this, &Room::playerRemoved, server->lobby(), &Room::addPlayer);
L = CreateLuaState();
DoLuaScript(L, "lua/freekill.lua");
DoLuaScript(L, "lua/server/room.lua");
initLua();
}
}
Room::~Room() {
if (isRunning()) {
wait();
if (gameStarted) {
gameOver();
}
if (m_thread) {
m_thread->removeRoom(this);
}
if (L)
lua_close(L);
}
Server *Room::getServer() const { return server; }
RoomThread *Room::getThread() const { return m_thread; }
void Room::setThread(RoomThread *t) { m_thread = t; }
int Room::getId() const { return id; }
void Room::setId(int id) { this->id = id; }
@ -81,7 +89,20 @@ bool Room::isAbandoned() const {
return true;
}
void Room::setAbandoned(bool abandoned) { m_abandoned = abandoned; }
// Lua专用lua room销毁时检查c++的Room是不是也差不多可以销毁了
void Room::checkAbandoned() {
if (isAbandoned()) {
bool tmp = m_abandoned;
m_abandoned = true;
if (!tmp) {
emit abandoned();
} else {
deleteLater();
}
}
}
void Room::setAbandoned(bool a) { m_abandoned = a; }
ServerPlayer *Room::getOwner() const { return owner; }
@ -218,6 +239,8 @@ void Room::removePlayer(ServerPlayer *player) {
// 原先的跑路机器人会在游戏结束后自动销毁掉
server->addPlayer(runner);
m_thread->wakeUp();
// 发出信号,让大厅添加这个人
emit playerRemoved(runner);
}
@ -397,7 +420,7 @@ void Room::gameOver() {
// 玩家也不能在这里清除,因为要能返回原来房间继续玩呢
// players.clear();
// owner = nullptr;
clearRequest();
// clearRequest();
}
void Room::manuallyStart() {
@ -405,43 +428,11 @@ void Room::manuallyStart() {
foreach (auto p, players) {
p->setReady(false);
}
start();
gameStarted = true;
m_thread->pushRequest(QString("-1,%1,newroom").arg(QString::number(id)));
}
}
QString Room::fetchRequest() {
if (!gameStarted)
return "";
request_queue_mutex.lock();
QString ret = "";
if (!request_queue.isEmpty()) {
ret = request_queue.dequeue();
}
request_queue_mutex.unlock();
return ret;
}
void Room::pushRequest(const QString &req) {
if (!gameStarted)
return;
request_queue_mutex.lock();
request_queue.enqueue(req);
request_queue_mutex.unlock();
}
void Room::clearRequest() {
request_queue_mutex.lock();
request_queue.clear();
request_queue_mutex.unlock();
}
bool Room::hasRequest() const { return !request_queue.isEmpty(); }
void Room::run() {
gameStarted = true;
clearRequest();
// 此处调用了Lua Room:run()函数
roomStart();
m_thread->pushRequest(QString("%1,%2").arg(QString::number(id), req));
}

View File

@ -5,16 +5,19 @@
class Server;
class ServerPlayer;
class RoomThread;
class Room : public QThread {
class Room : public QObject {
Q_OBJECT
public:
explicit Room(Server *m_server);
explicit Room(RoomThread *m_thread);
~Room();
// Property reader & setter
// ==================================={
Server *getServer() const;
RoomThread *getThread() const;
void setThread(RoomThread *t);
int getId() const;
void setId(int id);
bool isLobby() const;
@ -26,7 +29,8 @@ class Room : public QThread {
const QByteArray getSettings() const;
void setSettings(QByteArray settings);
bool isAbandoned() const;
void setAbandoned(bool abandoned); // never use this function
void checkAbandoned();
void setAbandoned(bool a);
ServerPlayer *getOwner() const;
void setOwner(ServerPlayer *owner);
@ -55,17 +59,8 @@ class Room : public QThread {
void updateWinRate(int id, const QString &general, const QString &mode,
int result);
void gameOver();
void initLua();
void roomStart();
void manuallyStart();
LuaFunction startGame;
QString fetchRequest();
void pushRequest(const QString &req);
void clearRequest();
bool hasRequest() const;
signals:
void abandoned();
@ -73,11 +68,9 @@ class Room : public QThread {
void playerAdded(ServerPlayer *player);
void playerRemoved(ServerPlayer *player);
protected:
virtual void run();
private:
Server *server;
RoomThread *m_thread;
int id; // Lobby's id is 0
QString name; // “阴间大乱斗”
int capacity; // by default is 5, max is 8
@ -90,12 +83,9 @@ class Room : public QThread {
QList<int> runned_players;
int robot_id;
bool gameStarted;
bool m_ready;
int timeout;
lua_State *L;
QMutex request_queue_mutex;
QQueue<QString> request_queue; // json string
};
#endif // _ROOM_H

107
src/server/roomthread.cpp Normal file
View File

@ -0,0 +1,107 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#include "roomthread.h"
#include "server.h"
#include "util.h"
#include <qtpreprocessorsupport.h>
RoomThread::RoomThread(Server *m_server) {
setObjectName("Room");
this->m_server = m_server;
m_capacity = 100; // TODO: server cfg
terminated = false;
L = CreateLuaState();
DoLuaScript(L, "lua/freekill.lua");
DoLuaScript(L, "lua/server/scheduler.lua");
start();
}
RoomThread::~RoomThread() {
tryTerminate();
if (isRunning()) {
wait();
}
lua_close(L);
// foreach (auto room, room_list) {
// room->deleteLater();
// }
}
Server *RoomThread::getServer() const {
return m_server;
}
bool RoomThread::isFull() const {
// return room_list.count() >= m_capacity;
return m_capacity <= 0;
}
Room *RoomThread::getRoom(int id) const {
return m_server->findRoom(id);
}
void RoomThread::addRoom(Room *room) {
Q_UNUSED(room);
m_capacity--;
}
void RoomThread::removeRoom(Room *room) {
room->setThread(nullptr);
m_capacity++;
}
QString RoomThread::fetchRequest() {
// if (!gameStarted)
// return "";
request_queue_mutex.lock();
QString ret = "";
if (!request_queue.isEmpty()) {
ret = request_queue.dequeue();
}
request_queue_mutex.unlock();
return ret;
}
void RoomThread::pushRequest(const QString &req) {
// if (!gameStarted)
// return;
request_queue_mutex.lock();
request_queue.enqueue(req);
request_queue_mutex.unlock();
wakeUp();
}
void RoomThread::clearRequest() {
request_queue_mutex.lock();
request_queue.clear();
request_queue_mutex.unlock();
}
bool RoomThread::hasRequest() {
request_queue_mutex.lock();
auto ret = !request_queue.isEmpty();
request_queue_mutex.unlock();
return ret;
}
void RoomThread::trySleep(int ms) {
if (sema_wake.available() > 0) {
sema_wake.acquire(sema_wake.available());
}
sema_wake.tryAcquire(1, ms);
}
void RoomThread::wakeUp() {
sema_wake.release(1);
}
void RoomThread::tryTerminate() {
terminated = true;
wakeUp();
}
bool RoomThread::isTerminated() const {
return terminated;
}

49
src/server/roomthread.h Normal file
View File

@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#ifndef _ROOMTHREAD_H
#define _ROOMTHREAD_H
#include <qsemaphore.h>
class Room;
class Server;
class RoomThread : public QThread {
Q_OBJECT
public:
explicit RoomThread(Server *m_server);
~RoomThread();
Server *getServer() const;
bool isFull() const;
Room *getRoom(int id) const;
void addRoom(Room *room);
void removeRoom(Room *room);
QString fetchRequest();
void pushRequest(const QString &req);
void clearRequest();
bool hasRequest();
void trySleep(int ms);
void wakeUp();
void tryTerminate();
bool isTerminated() const;
protected:
virtual void run();
private:
Server *m_server;
// QList<Room *> room_list;
int m_capacity;
lua_State *L;
QMutex request_queue_mutex;
QQueue<QString> request_queue; // json string
QSemaphore sema_wake;
volatile bool terminated;
};
#endif // _ROOMTHREAD_H

View File

@ -14,6 +14,7 @@
#include "packman.h"
#include "player.h"
#include "room.h"
#include "roomthread.h"
#include "router.h"
#include "server_socket.h"
#include "serverplayer.h"
@ -44,6 +45,8 @@ Server::Server(QObject *parent) : QObject(parent) {
connect(lobby(), &Room::playerAdded, this, &Server::updateRoomList);
connect(lobby(), &Room::playerRemoved, this, &Server::updateRoomList);
threads.append(new RoomThread(this));
// 启动心跳包线程
auto heartbeatThread = QThread::create([=]() {
while (true) {
@ -63,7 +66,7 @@ Server::Server(QObject *parent) : QObject(parent) {
foreach (auto p, this->players.values()) {
if (p->getState() == Player::Online && !p->alive) {
p->kicked();
emit p->kicked();
}
}
}
@ -77,11 +80,11 @@ Server::~Server() {
isListening = false;
ServerInstance = nullptr;
m_lobby->deleteLater();
foreach (auto room, idle_rooms) {
room->deleteLater();
}
foreach (auto room, rooms) {
room->deleteLater();
// foreach (auto room, idle_rooms) {
// room->deleteLater();
// }
foreach (auto thread, threads) {
thread->deleteLater();
}
sqlite3_close(db);
RSA_free(rsa);
@ -102,14 +105,27 @@ void Server::createRoom(ServerPlayer *owner, const QString &name, int capacity,
return;
}
Room *room;
RoomThread *thread = nullptr;
foreach (auto t, threads) {
if (!t->isFull()) {
thread = t;
}
}
if (!thread && nextRoomId != 0) {
thread = new RoomThread(this);
threads.append(thread);
}
if (!idle_rooms.isEmpty()) {
room = idle_rooms.pop();
room->setId(nextRoomId);
nextRoomId++;
room->setAbandoned(false);
room->setThread(thread);
rooms.insert(room->getId(), room);
} else {
room = new Room(this);
room = new Room(thread);
connect(room, &Room::abandoned, this, &Server::onRoomAbandoned);
if (room->isLobby())
m_lobby = room;
@ -270,12 +286,10 @@ void Server::processRequest(const QByteArray &msg) {
body
<< (cmp < 0
? QString("[\"server is still on version %%2\",\"%1\"]")
.arg(FK_VERSION)
.arg("1")
.arg(FK_VERSION, "1")
: QString(
"[\"server is using version %%2, please update\",\"%1\"]")
.arg(FK_VERSION)
.arg("1"));
.arg(FK_VERSION, "1"));
client->send(JsonArray2Bytes(body));
client->disconnectFromHost();
@ -464,26 +478,15 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString &name,
void Server::onRoomAbandoned() {
Room *room = qobject_cast<Room *>(sender());
if (room->isRunning()) {
room->wait();
}
room->gameOver();
rooms.remove(room->getId());
updateRoomList();
// 按理说这时候就可以删除了但是这里肯定比Lua先检测到。
// 倘若在Lua的Room:gameOver时C++的Room被删除了问题就大了。
// FIXME: 但是这终归是内存泄漏!以后啥时候再改吧。
// room->deleteLater();
idle_rooms.push(room);
// 懒得改了!
// 这里出bug的原因还是在于room的销毁工作没做好
// room销毁这块bug很多
// if (idle_rooms.length() > 10) {
// auto junk = idle_rooms[0];
// idle_rooms.removeFirst();
// junk->deleteLater();
// }
#ifdef QT_DEBUG
qDebug() << rooms.size() << "running room(s)," << idle_rooms.size()
<< "idle room(s).";
#endif
room->getThread()->removeRoom(room);
}
void Server::onUserDisconnected() {

View File

@ -11,6 +11,7 @@
class ServerSocket;
class ClientSocket;
class ServerPlayer;
class RoomThread;
#include "room.h"
@ -62,8 +63,9 @@ private:
Room *m_lobby;
QMap<int, Room *> rooms;
QStack<Room *> idle_rooms;
QList<RoomThread *> threads;
int nextRoomId;
friend Room::Room(Server *server);
friend Room::Room(RoomThread *m_thread);
QHash<int, ServerPlayer *> players;
RSA *rsa;

View File

@ -3,6 +3,7 @@
#include "serverplayer.h"
#include "client_socket.h"
#include "room.h"
#include "roomthread.h"
#include "router.h"
#include "server.h"
@ -16,6 +17,7 @@ ServerPlayer::ServerPlayer(Room *room) {
alive = true;
m_busy = false;
m_thinking = false;
}
ServerPlayer::~ServerPlayer() {
@ -108,3 +110,16 @@ void ServerPlayer::kick() {
}
setSocket(nullptr);
}
bool ServerPlayer::thinking() {
m_thinking_mutex.lock();
bool ret = m_thinking;
m_thinking_mutex.unlock();
return ret;
}
void ServerPlayer::setThinking(bool t) {
m_thinking_mutex.lock();
m_thinking = t;
m_thinking_mutex.unlock();
}

View File

@ -40,6 +40,9 @@ public:
bool busy() const { return m_busy; }
void setBusy(bool busy) { m_busy = busy; }
bool thinking();
void setThinking(bool t);
signals:
void disconnected();
void kicked();
@ -49,7 +52,9 @@ private:
Router *router;
Server *server;
Room *room; // Room that player is in, maybe lobby
bool m_busy;
bool m_busy; // (Lua专用) 是否有doRequest没处理完见于神貂蝉这种一控多的
bool m_thinking; // 是否在烧条?
QMutex m_thinking_mutex; // 注意setBusy只在Lua使用所以不需要锁。
QString requestCommand;
QString requestData;

View File

@ -7,6 +7,7 @@
#include "serverplayer.h"
#include "clientplayer.h"
#include "room.h"
#include "roomthread.h"
#include "util.h"
#include "qmlbackend.h"
class ClientPlayer *Self = nullptr;
@ -17,4 +18,3 @@ class ClientPlayer *Self = nullptr;
%include "qml-nogui.i"
%include "player.i"
%include "server.i"
%include "sqlite3.i"

View File

@ -8,6 +8,7 @@
#include "serverplayer.h"
#include "clientplayer.h"
#include "room.h"
#include "roomthread.h"
#include "qmlbackend.h"
#include "util.h"
%}
@ -17,4 +18,3 @@
%include "player.i"
%include "client.i"
%include "server.i"
%include "sqlite3.i"

View File

@ -8,6 +8,8 @@ public:
Invalid,
Online,
Trust,
Run,
Robot, // only for real robot
Offline
};
@ -21,12 +23,7 @@ public:
void setAvatar(const QString &avatar);
State getState() const;
QString getStateString() const;
void setState(State state);
void setStateString(const QString &state);
bool isReady() const;
void setReady(bool ready);
};
%nodefaultctor ClientPlayer;

View File

@ -1,63 +1,22 @@
// SPDX-License-Identifier: GPL-3.0-or-later
%nodefaultctor Server;
%nodefaultdtor Server;
class Server : public QObject {
public:
Room *lobby() const;
void createRoom(ServerPlayer *owner, const QString &name, int capacity);
Room *findRoom(int id) const;
ServerPlayer *findPlayer(int id) const;
sqlite3 *getDatabase();
};
extern Server *ServerInstance;
%nodefaultctor Room;
%nodefaultdtor Room;
class Room : public QThread {
class Room : public QObject {
public:
// Property reader & setter
// ==================================={
Server *getServer() const;
int getId() const;
bool isLobby() const;
QString getName() const;
void setName(const QString &name);
int getCapacity() const;
void setCapacity(int capacity);
bool isFull() const;
bool isAbandoned() const;
ServerPlayer *getOwner() const;
void setOwner(ServerPlayer *owner);
void addPlayer(ServerPlayer *player);
void addRobot(ServerPlayer *player);
void removePlayer(ServerPlayer *player);
QList<ServerPlayer *> getPlayers() const;
ServerPlayer *findPlayer(int id) const;
QList<ServerPlayer *> getObservers() const;
int getTimeout() const;
bool isStarted() const;
// ====================================}
void doBroadcastNotify(
const QList<ServerPlayer *> targets,
const QString &command,
const QString &jsonData
);
void checkAbandoned();
void updateWinRate(int id, const QString &general, const QString &mode,
int result);
void gameOver();
LuaFunction startGame;
QString fetchRequest();
bool hasRequest() const;
};
%extend Room {
@ -66,14 +25,28 @@ public:
}
}
%nodefaultctor RoomThread;
%nodefaultdtor RoomThread;
class RoomThread : public QThread {
public:
Room *getRoom(int id);
QString fetchRequest();
void clearRequest();
bool hasRequest();
void trySleep(int ms);
bool isTerminated() const;
};
%{
void Room::initLua()
void RoomThread::run()
{
lua_getglobal(L, "debug");
lua_getfield(L, -1, "traceback");
lua_replace(L, -2);
lua_getglobal(L, "CreateRoom");
SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0);
lua_getglobal(L, "InitScheduler");
SWIG_NewPointerObj(L, this, SWIGTYPE_p_RoomThread, 0);
int error = lua_pcall(L, 1, 0, -2);
lua_pop(L, 1);
if (error) {
@ -81,46 +54,20 @@ void Room::initLua()
qCritical() << error_msg;
}
}
void Room::roomStart() {
Q_ASSERT(startGame);
lua_getglobal(L, "debug");
lua_getfield(L, -1, "traceback");
lua_replace(L, -2);
lua_rawgeti(L, LUA_REGISTRYINDEX, startGame);
SWIG_NewPointerObj(L, this, SWIGTYPE_p_Room, 0);
int error = lua_pcall(L, 1, 0, -3);
if (error) {
const char *error_msg = lua_tostring(L, -1);
qCritical() << error_msg;
lua_pop(L, 2);
}
lua_pop(L, 1);
}
%}
%nodefaultctor ServerPlayer;
%nodefaultdtor ServerPlayer;
class ServerPlayer : public Player {
public:
Server *getServer() const;
Room *getRoom() const;
void setRoom(Room *room);
void speak(const QString &message);
void doRequest(const QString &command,
const QString &json_data, int timeout);
QString waitForReply(int timeout);
void doNotify(const QString &command, const QString &json_data);
void prepareForRequest(const QString &command, const QString &data);
bool busy() const;
void setBusy(bool busy);
bool thinking();
void setThinking(bool t);
};

View File

@ -1,8 +0,0 @@
// SPDX-License-Identifier: GPL-3.0-or-later
struct sqlite3;
sqlite3 *OpenDatabase(const QString &filename);
QString SelectFromDb(sqlite3 *db, const QString &sql);
void ExecSQL(sqlite3 *db, const QString &sql);
void CloseDatabase(sqlite3 *db);