引言
在前一篇文章中讲解了通过入口存储过程动态调用业务过程的原理。下面来说明如何实现游戏状态的更新。
项目的源码可在CSDN资源中下载
实现原理
由于http协议是无状态的请求响应式协议,用户可以主动向服务器发起请求,得到回复。但如果服务器状态发生了变化,却不能直接地推送消息给用户。
在web应用中,消息推送是一个长期讨论的话题。现在已经有了比较成熟的解决方案,比如websocket。
不过本项目使用的是最基本的轮询方式,即客户端定时向服务器发起一个请求,检查是否有留给自已的消息,如果有属于自己的消息,则根据消息更新浏览器上显示的内容。与此同时,定时向服务器发起的请求,也是一个心跳信号,定时向服务器报告自己还活着。如果在指定的时间期限过后,还未收到某客户的心跳信号,服务器则会认为该客户已离线。
具体实现
项目引入了jquery.timers-1.2.js,这是一个可以实现定时功能的jquery扩展。
现在看一下源文件:GameEvents.js
//游戏过程中的各种事件,通过不断查询数据库中的公告信息,形成各种游戏事件处理框架
function GameEvents(deskID,playerNo,onEnterRoom,onReady,onDealCards,onBid,onBidOver,onPlayCard,onGameOver,onExitRoom,onNoneBidOver,onSurrenderonTimeTick)
{
//处理各种公告信息
this.OnReceivedNoteInfo=function(result)
{
var resObj=eval("("+result+")");
//enter room
if (resObj.noteType == 1)
{
onEnterRoom(resObj.content);
}
......
}
var eventHandler = this.OnReceivedNoteInfo;
this.StartTime=new Date();
this.start=function()
{
this.StartTime=new Date();
//$('body').everyTime
//$('body').oneTime
$('body').everyTime
('1s','pluse',
function()
{
callProc("readNote" , deskID +","+playerNo , eventHandler);
onTimeTick();
},
0,true);
}
this.stop = function()
{
$('body').stopTime ('pluse');
}
this.ElapsedMinutes = function()
{
var nowTime = new Date();
return parseInt(Math.abs(nowTime - this.StartTime)/1000/60);
}
}
其中的this.start方法
this.start=function()
{
this.StartTime=new Date();
$('body').everyTime
('1s','pluse',
function()
{
callProc("readNote" , deskID +","+playerNo , eventHandler);
onTimeTick();
},
0,true);
}
实现每秒1次的定时请求,在这个请求中调用 callProc(“readNote” , deskID +","+playerNo , eventHandler);
按照上一篇文章的说明,这里调用了存储过程 readNote。现在看一下readNote存储过程
CREATE procedure readNote
(
@deskID int,
@playerNo int,
@outMsg varchar(500) out
)
as
begin
--更新到访时间
update deskPlayer set lastTime = getdate()
where deskID = @deskID and playerNo = @playerNo
--接收通告
declare @noteID int
select @noteID = min(noteID) from v_note
where deskID = @deskID and receiverNo = @playerNo
if @noteID is null
begin
set @outMsg='{noteType:0}'
return 0
end
declare @noteType int,@content varchar(500)
select @content = content,@noteType = noteType from note where noteID = @noteID
delete noteRecipient where noteID = @noteID and playerNo=@playerNo
delete note where noteID not in(select noteID from noteRecipient)
set @outMsg='{noteType:'+cast(@noteType as varchar)+',content:' +@content + '}'
return 0
end
从以上代码可知,这个存储过程实现了两个功能,
一是将本次请求的访问时间记录在数据表 deskPlayer当中
二是从note 表中取出属于自己的通告消息
下面先看一下deskPlayer表
其中的lastTime列记录了每次用户的心跳,也就是调用存储过程readNote的时刻。
现在看一下检查用户是否离线的存储过程
CREATE procedure checkOffLine
as
begin
--检查是否有人离线
declare @offPlayer table(idx int identity(1,1),roomID int,playerID int)
insert into @offPlayer(roomID,playerID)
select roomID,playerID from v_roomPlayer
where dateDiff(second,lastTime,getdate()) > 5 -- off line seconds
declare @roomID int,@offplayerID int, @offPlayerCnt int
select @offPlayerCnt = count(*) from @offPlayer
declare @outMsg varchar(100)
declare @i int
set @i=1
while @i <= @offPlayerCnt
begin
select @roomID = roomID,@offplayerID = playerID from @offPlayer where idx = @i
set @i = @i +1
exec exitRoom @roomID,@offplayerID,@outMsg out
end
return 0
end
其中的核心查询语句是
insert into @offPlayer(roomID,playerID)
select roomID,playerID from v_roomPlayer
where dateDiff(second,lastTime,getdate()) > 5 -- off line seconds
这个语句从v_roomPlayer 视图,该视图基于表deskPlayer,查找出当前时间与心跳报告时间之差大于5秒的记录。也就是说超过5秒,服务器都没有收到浏览器的心跳报告,就会被认为已经离线。
针对每一个被判定已经离线的用户,调用exitRoom存储过程,而在exitRoom存储过程向其它应该接收消息的用户留下公告消息,在1秒之后,其它活跃用户将会再次向服务器发起请求,就会得到用户离线的公告消息。
接下来看一下接收公告消息的实现
由playNo得到要接收的消息的noteID.
--接收通告
declare @noteID int
select @noteID = min(noteID) from v_note
where deskID = @deskID and receiverNo = @playerNo
在这里要解释一下,为什么不用用户的ID,而是要用playerNo做为查询的依据。 playerNo是在一张牌桌上用户的编号,取值只能是1,2,3中的一个,分别代表西,南,东三个方位的玩家。而playerID是用户的唯一ID,但这个ID不能代表玩家在牌桌上的方位。 而且比较有趣的一点是,对于每个浏览器前的用户而言,他的方位永远是南方,但是它分配得到的playerNo在用户没有离开时永远保持不变。
下面先看一下note表,此表中记录的所有的公告消息
以上的公告消息在readNote过程中,如果已经被读取,则会被删除掉。
小结
将心跳机制与公告消息机制结合起来,运用轮询的方式实现游戏状态的定时更新。在未引入websocket的前提下,该方案不失为一种简单方便的解决方案。