Reconnect (#46)

* 1

* room:delay and other coroutine

* yield in waitForReply

* fix stack overflow bug

* compress large message when transmiting

* delete useless func

* reconnect

* update doc for reconnect
This commit is contained in:
notify 2023-01-17 22:34:15 +08:00 committed by GitHub
parent 2ea7acfb7a
commit a8cd7431ef
13 changed files with 252 additions and 74 deletions

View File

@ -83,9 +83,9 @@ ___
___ ___
## 断线重连TODO ## 断线重连
根据用户找到掉线的那位玩家,将玩家的状态设置为“在线”,并将房间的状态都发送给他即可。 根据用户id找到掉线的那位玩家,将玩家的状态设置为“在线”,并将房间的状态都发送给他即可。
但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。 但是为了[UI不出错](./ui.md#mainStack),依然需要对重连的玩家走一遍进大厅的流程。
@ -109,20 +109,30 @@ ___
3. 此外还需要让玩家知道牌堆、弃牌堆、轮数之类的。 3. 此外还需要让玩家知道牌堆、弃牌堆、轮数之类的。
4. 玩家的信息就更多了武将、身份、血量、id... 4. 玩家的信息就更多了武将、身份、血量、id...
信息要怎么发呢: 所以Lua要在某时候让出一段时间处理重连等其他内容可能还会处理一下AI。
- 一步一步的告诉重连中的玩家。 这种让出时间处理的东西时间要尽可能的短,不要在里面搞个大循环。
- 全部汇总成字符串或者别的什么,然后可以压缩并发送。
- 但以上两种都有问题许多信息保存在Lua中而Lua的运行是绝对不容其他线程打搅的。
- 而且粗略一想这些东西都应该非常耗时而如今的线程只有Main线程和各大Room线程。有必要给Room加个子线程专门处理掉线这块的然后Room该怎么跑继续怎么跑。
或者换个思路 会阻塞住lua代码的函数有
1. 首先EnterRoom消息需要**人数**和**操作时长**。 - ServerPlayer:waitForReplay()
2. 服务端将这个客户端的*录像信息*发给客户端客户端满速且不影响UI的播放录像。 - Room:delay()
3. 在“播放录像”的过程中,客户端对于正在被收到的消息需进行特殊处理。
4. 一个录像文件的体积会非常大。所以服务端所保存的客户端录像应该和真正的录像有差别才行。比如聊天、战报这种数据量大但又无关紧要的东西就不保存。 在这里让出主线程然后调度函数查找目前的请求列表。事实上整个Room的游戏主流程就是一个协程
5. 顺便这样也解决了多视角录像的问题,服务端给每个视角都录像就行了。
```lua
-- room.lua:53
local co_func = function()
self:run()
end
local co = coroutine.create(co_func)
while not self.game_finished do
local ret, err_msg = coroutine.resume(co)
...
end
```
如果在游戏流程中调用yield的话那么这里的resume会返回true然后可以带有额外的返回值。不过只要返回true就好了这时候lua就可以做一些简单的任务。而这个简单的任务其实也可以另外写个协程解决。
___ ___

View File

@ -47,6 +47,7 @@ function GameLogic:assignRoles()
local p = room.players[i] local p = room.players[i]
p.role = roles[i] p.role = roles[i]
if p.role == "lord" then if p.role == "lord" then
p.role_shown = true
room:broadcastProperty(p, "role") room:broadcastProperty(p, "role")
else else
room:notifyProperty(p, p, "role") room:notifyProperty(p, p, "role")
@ -168,7 +169,7 @@ function GameLogic:action()
if room.game_finished then break end if room.game_finished then break end
room.current = room.current:getNextAlive() room.current = room.current:getNextAlive()
if checkNoHuman() then if checkNoHuman() then
room:gameOver() room:gameOver("")
end end
end end
end end

View File

@ -50,17 +50,26 @@ function Room:initialize(_room)
self.room.startGame = function(_self) self.room.startGame = function(_self)
Room.initialize(self, _room) -- clear old data Room.initialize(self, _room) -- clear old data
local co_func = function() local main_co = coroutine.create(function()
self:run() self:run()
end end)
local co = coroutine.create(co_func) local request_co = coroutine.create(function()
self:requestLoop()
end)
while not self.game_finished do while not self.game_finished do
local ret, err_msg = coroutine.resume(co) local ret, err_msg = coroutine.resume(main_co)
-- handle error -- handle error
if ret == false then if ret == false then
fk.qCritical(err_msg) fk.qCritical(err_msg)
print(debug.traceback(co)) print(debug.traceback(main_co))
break
end
ret, err_msg = coroutine.resume(request_co)
if ret == false then
fk.qCritical(err_msg)
print(debug.traceback(request_co))
break break
end end
end end
@ -383,6 +392,33 @@ function Room:doRaceRequest(command, players, jsonData)
end end
end end
-- main loop for the request handling coroutine
function Room:requestLoop()
while true do
local request = self.room:fetchRequest()
if request ~= "" then
local id, command = table.unpack(request:split(","))
id = tonumber(id)
if command == "reconnect" then
self:getPlayerById(id):reconnect()
end
end
coroutine.yield()
end
end
-- delay function, should only be used in main coroutine
---@param ms integer @ millisecond to be delayed
function Room:delay(ms)
local start = fk.GetMicroSecond()
while true do
if fk.GetMicroSecond() - start >= ms * 1000 then
break
end
coroutine.yield()
end
end
---@param players ServerPlayer[] ---@param players ServerPlayer[]
---@param card_moves CardsMoveStruct[] ---@param card_moves CardsMoveStruct[]
---@param forceVisible boolean ---@param forceVisible boolean

View File

@ -11,6 +11,7 @@
---@field skipped_phases Phase[] ---@field skipped_phases Phase[]
---@field phase_state table[] ---@field phase_state table[]
---@field phase_index integer ---@field phase_index integer
---@field role_shown boolean
local ServerPlayer = Player:subclass("ServerPlayer") local ServerPlayer = Player:subclass("ServerPlayer")
function ServerPlayer:initialize(_self) function ServerPlayer:initialize(_self)
@ -50,18 +51,28 @@ function ServerPlayer:doRequest(command, jsonData, timeout)
self.serverplayer:doRequest(command, jsonData, timeout) self.serverplayer:doRequest(command, jsonData, timeout)
end end
local function _waitForReply(player, timeout)
local result
local start = fk.GetMicroSecond()
while true do
result = player.serverplayer:waitForReply(0)
if result ~= "__notready" then
return result
end
if timeout and (fk.GetMicroSecond() - start) / 1000 >= timeout * 1000 then
return ""
end
coroutine.yield()
end
end
--- Wait for at most *timeout* seconds for reply from client. --- Wait for at most *timeout* seconds for reply from client.
--- ---
--- If *timeout* is negative or **nil**, the function will wait forever until get reply. --- If *timeout* is negative or **nil**, the function will wait forever until get reply.
---@param timeout integer @ seconds to wait ---@param timeout integer @ seconds to wait
---@return string @ JSON data ---@return string @ JSON data
function ServerPlayer:waitForReply(timeout) function ServerPlayer:waitForReply(timeout)
local result = "" local result = _waitForReply(self, timeout)
if timeout == nil then
result = self.serverplayer:waitForReply()
else
result = self.serverplayer:waitForReply(timeout)
end
self.request_data = "" self.request_data = ""
self.client_reply = result self.client_reply = result
if result == "__cancel" then if result == "__cancel" then
@ -72,6 +83,130 @@ function ServerPlayer:waitForReply(timeout)
return result return result
end end
---@param player ServerPlayer
function ServerPlayer:marshal(player)
local room = self.room
room:notifyProperty(player, self, "maxHp")
room:notifyProperty(player, self, "hp")
-- TODO
--room:notifyProperty(player, self, "gender")
if self.kingdom ~= Fk.generals[self.general].kingdom then
room:notifyProperty(player, self, "kingdom")
end
if self.dead then
room:notifyProperty(player, self, "dead")
room:notifyProperty(player, self, "role")
else
room:notifyProperty(player, self, "seat")
room:notifyProperty(player, self, "phase")
end
if not self.faceup then
room:notifyProperty(player, self, "faceup")
end
if self.chained then
room:notifyProperty(player, self, "chained")
end
local card_moves = {}
if #self.player_cards[Player.Hand] ~= 0 then
local info = {}
for _, i in ipairs(self.player_cards[Player.Hand]) do
table.insert(info, { cardId = i, fromArea = Card.DrawPile })
end
local move = {
moveInfo = info,
to = self.id,
toArea = Card.PlayerHand
}
table.insert(card_moves, move)
end
if #self.player_cards[Player.Equip] ~= 0 then
local info = {}
for _, i in ipairs(self.player_cards[Player.Equip]) do
table.insert(info, { cardId = i, fromArea = Card.DrawPile })
end
local move = {
moveInfo = info,
to = self.id,
toArea = Card.PlayerEquip
}
table.insert(card_moves, move)
end
if #self.player_cards[Player.Judge] ~= 0 then
local info = {}
for _, i in ipairs(self.player_cards[Player.Judge]) do
table.insert(info, { cardId = i, fromArea = Card.DrawPile })
end
local move = {
moveInfo = info,
to = self.id,
toArea = Card.PlayerJudge
}
table.insert(card_moves, move)
end
if #card_moves > 0 then
room:notifyMoveCards({ player }, card_moves)
end
-- TODO: pile, mark
for _, s in ipairs(self.player_skills) do
player:doNotify("AddSkill", json.encode{self.id, s.name})
end
for k, v in pairs(self.cardUsedHistory) do
player:doNotify("AddCardUseHistory", json.encode{k, v})
end
if self.role_shown then
room:notifyProperty(player, self, "role")
end
end
function ServerPlayer:reconnect()
local room = self.room
self.serverplayer:setStateString("online")
self:doNotify("Setup", json.encode{
self.id,
self.serverplayer:getScreenName(),
self.serverplayer:getAvatar(),
})
self:doNotify("EnterLobby", "")
self:doNotify("EnterRoom", json.encode{
#room.players, room.timeout,
})
room:notifyProperty(self, self, "role")
-- send player data
for _, p in ipairs(room:getOtherPlayers(self)) do
self:doNotify("AddPlayer", json.encode{
p.id,
p.serverplayer:getScreenName(),
p.serverplayer:getAvatar(),
})
end
local player_circle = {}
for i = 1, #room.players do
table.insert(player_circle, room.players[i].id)
end
self:doNotify("ArrangeSeats", json.encode(player_circle))
for _, p in ipairs(room.players) do
room:notifyProperty(self, p, "general")
p:marshal(self)
end
-- TODO: tell drawPile
room:broadcastProperty(self, "state")
end
function ServerPlayer:isAlive() function ServerPlayer:isAlive()
return self.dead == false return self.dead == false
end end

View File

@ -35,8 +35,11 @@ void ClientSocket::connectToHost(const QString &address, ushort port)
void ClientSocket::getMessage() void ClientSocket::getMessage()
{ {
while (socket->canReadLine()) { while (socket->canReadLine()) {
char msg[16000]; // buffer auto msg = socket->readLine();
socket->readLine(msg, sizeof(msg)); if (msg.startsWith("Compressed")) {
msg = msg.sliced(10);
msg = qUncompress(QByteArray::fromBase64(msg));
}
emit message_got(msg); emit message_got(msg);
} }
} }
@ -48,6 +51,12 @@ void ClientSocket::disconnectFromHost()
void ClientSocket::send(const QByteArray &msg) void ClientSocket::send(const QByteArray &msg)
{ {
if (msg.length() >= 1024) {
auto comp = qCompress(msg);
auto _msg = "Compressed" + comp.toBase64() + "\n";
socket->write(_msg);
socket->flush();
}
socket->write(msg); socket->write(msg);
if (!msg.endsWith("\n")) if (!msg.endsWith("\n"))
socket->write("\n"); socket->write("\n");

View File

@ -70,7 +70,7 @@ void Router::request(int type, const QString& command,
expectedReplyId = requestId; expectedReplyId = requestId;
replyTimeout = timeout; replyTimeout = timeout;
requestStartTime = QDateTime::currentDateTime(); requestStartTime = QDateTime::currentDateTime();
m_reply = QString(); m_reply = "__notready";
replyMutex.unlock(); replyMutex.unlock();
QJsonArray body; QJsonArray body;
@ -126,14 +126,6 @@ void Router::cancelRequest()
#endif #endif
} }
QString Router::waitForReply()
{
#ifndef Q_OS_WASM
replyReadySemaphore.acquire();
return m_reply;
#endif
}
QString Router::waitForReply(int timeout) QString Router::waitForReply(int timeout)
{ {
#ifndef Q_OS_WASM #ifndef Q_OS_WASM

View File

@ -43,7 +43,6 @@ public:
void cancelRequest(); void cancelRequest();
void abortRequest(); void abortRequest();
QString waitForReply();
QString waitForReply(int timeout); QString waitForReply(int timeout);
signals: signals:

View File

@ -29,9 +29,7 @@ Room::Room(Server* server)
Room::~Room() Room::~Room()
{ {
// TODO
if (isRunning()) { if (isRunning()) {
terminate();
wait(); wait();
} }
if (L) lua_close(L); if (L) lua_close(L);
@ -259,16 +257,6 @@ bool Room::isStarted() const
return gameStarted; return gameStarted;
} }
void Room::doRequest(const QList<ServerPlayer *> targets, int timeout)
{
// TODO
}
void Room::doNotify(const QList<ServerPlayer *> targets, int timeout)
{
// TODO
}
void Room::doBroadcastNotify(const QList<ServerPlayer *> targets, void Room::doBroadcastNotify(const QList<ServerPlayer *> targets,
const QString& command, const QString& jsonData) const QString& command, const QString& jsonData)
{ {
@ -302,6 +290,22 @@ void Room::gameOver()
owner = nullptr; owner = nullptr;
} }
QString Room::fetchRequest() {
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) {
request_queue_mutex.lock();
request_queue.enqueue(req);
request_queue_mutex.unlock();
}
void Room::run() void Room::run()
{ {
gameStarted = true; gameStarted = true;

View File

@ -40,9 +40,6 @@ public:
bool isStarted() const; bool isStarted() const;
// ====================================} // ====================================}
void doRequest(const QList<ServerPlayer *> targets, int timeout);
void doNotify(const QList<ServerPlayer *> targets, int timeout);
void doBroadcastNotify( void doBroadcastNotify(
const QList<ServerPlayer *> targets, const QList<ServerPlayer *> targets,
const QString &command, const QString &command,
@ -57,6 +54,9 @@ public:
void roomStart(); void roomStart();
LuaFunction startGame; LuaFunction startGame;
QString fetchRequest();
void pushRequest(const QString &req);
signals: signals:
void abandoned(); void abandoned();
@ -82,7 +82,8 @@ private:
int timeout; int timeout;
lua_State *L; lua_State *L;
QMutex lua_mutex; QMutex request_queue_mutex;
QQueue<QString> request_queue; // json string
}; };
#endif // _ROOM_H #endif // _ROOM_H

View File

@ -240,8 +240,16 @@ void Server::handleNameAndPassword(ClientSocket *client, const QString& name, co
passed = (passwordHash == arr[0].toString()); passed = (passwordHash == arr[0].toString());
if (!passed) error_msg = "username or password error"; if (!passed) error_msg = "username or password error";
} else { } else {
// TODO: reconnect here auto player = players.value(id);
error_msg = "others logged in with this name"; if (player->getState() == Player::Offline) {
auto room = player->getRoom();
player->setSocket(client);
client->disconnect(this);
room->pushRequest(QString("%1,reconnect").arg(id));
return;
} else {
error_msg = "others logged in with this name";
}
} }
} }
} else { } else {
@ -289,7 +297,6 @@ void Server::onRoomAbandoned()
updateRoomList(); updateRoomList();
//room->deleteLater(); //room->deleteLater();
if (room->isRunning()) { if (room->isRunning()) {
room->terminate();
room->wait(); room->wait();
} }
idle_rooms.push(room); idle_rooms.push(room);
@ -306,6 +313,7 @@ void Server::onUserDisconnected()
Room *room = player->getRoom(); Room *room = player->getRoom();
if (room->isStarted()) { if (room->isStarted()) {
player->setState(Player::Offline); player->setState(Player::Offline);
player->setSocket(nullptr);
// TODO: add a robot // TODO: add a robot
} else { } else {
player->deleteLater(); player->deleteLater();

View File

@ -80,20 +80,6 @@ void ServerPlayer::abortRequest()
router->abortRequest(); router->abortRequest();
} }
QString ServerPlayer::waitForReply()
{
QString ret;
Player::State state = getState();
if (state != Player::Online) {
if (state != Player::Run)
QThread::sleep(1);
ret = QString("__state=%1").arg(getStateString());
} else {
ret = router->waitForReply();
}
return ret;
}
QString ServerPlayer::waitForReply(int timeout) QString ServerPlayer::waitForReply(int timeout)
{ {
QString ret; QString ret;

View File

@ -27,7 +27,6 @@ public:
const QString &jsonData, int timeout = -1); const QString &jsonData, int timeout = -1);
void abortRequest(); void abortRequest();
QString waitForReply(int timeout); QString waitForReply(int timeout);
QString waitForReply();
void doNotify(const QString &command, const QString &jsonData); void doNotify(const QString &command, const QString &jsonData);
void prepareForRequest(const QString &command, void prepareForRequest(const QString &command,

View File

@ -42,8 +42,6 @@ public:
bool isStarted() const; bool isStarted() const;
// ====================================} // ====================================}
void doRequest(const QList<ServerPlayer *> targets, int timeout);
void doNotify(const QList<ServerPlayer *> targets, int timeout);
void doBroadcastNotify( void doBroadcastNotify(
const QList<ServerPlayer *> targets, const QList<ServerPlayer *> targets,
const QString &command, const QString &command,
@ -53,6 +51,7 @@ public:
void gameOver(); void gameOver();
LuaFunction startGame; LuaFunction startGame;
QString fetchRequest();
}; };
%{ %{
@ -105,7 +104,6 @@ public:
void doRequest(const QString &command, void doRequest(const QString &command,
const QString &json_data, int timeout); const QString &json_data, int timeout);
QString waitForReply();
QString waitForReply(int timeout); QString waitForReply(int timeout);
void doNotify(const QString &command, const QString &json_data); void doNotify(const QString &command, const QString &json_data);