|
最近忙着改东西,越来越发现ICE在高并发下,IO占用的问题。当然是可以通过配置文件解决,不过毕竟文档较少,很多东西需要尝试。ICE和Python结合,还是有界限的。python进程只能占据一个CPU,就算你有100个线程也是如此,更加确认了我对虚拟脚本语言的使用方法,那就是需要多线程的地方,让C++去做,逻辑具体实现,可以让脚本代劳,前提是逻辑调用次数不能过于频繁,如果很频繁,那就C++搞定。先说一个自己最近在搞的东西,以前一直踌躇苹果的用户购买了我的一个服务,我怎么在服务器进行验证的问题。
参考了一些游戏的扣费,感觉有的快,有的慢,不过,如果无法在服务器上验证一个用户付费行为的合法性,那么无论如何服务器的处理行为都是有问题的。查了一下APPLE API,查到了确实苹果提供了验证一个用户是否合法的接口。
具体可以参考:
http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/VerifyingStoreReceipts/VerifyingStoreReceipts.html#//apple_ref/doc/uid/TP40008267-CH104-SW1
有了这个接口,我开始尝试,写一个通用的游戏充值接口。这个接口完成以下的需求:
1.公司的所有游戏都走这个接口(Http)的,根据这个接口,将充值内部调用不同游戏的指定充值接口。这样做,用户不在直接调用游戏充值接口。
2.在这里可以对游戏的充值进行统计,和查询,而各个游戏本身,充值将只记录log。
这样做,可以避免暴露出实际的游戏充值接口,也给充值本身带来一层保护。
于是和公司的几个游戏组商量,统一了一个接口。
GameID | 游戏ID | string | UserID | 用户ID | string | Param | 游戏参数 | string | IAPKey | 支付返回的Apple key | string | Channel | 渠道名称 | string | Currency | 币种(美元,人民币) | int | Money | 货币钱数 | int | 这是客户端传给我的,用于验证,这里有几个主要的参数,IAPKey是客户端向苹果付费后,所获得的Base64编码的结果。或许有人会问,既然有了key,其它的部分都是不需要的吧。这里我需要解释一下
Param是考虑到通用化,可能某次用户付费,客户端需要额外附加一些说明,那么就在这里给出。游戏验证服务不处理这个信息,而是直接抛给游戏充值接口,在那里做自己的逻辑判断。
Channel是充值渠道,考虑到以后可能有别的支付模式,比如支付宝,比如信用卡。这部分渠道,可能会需要区分开,另外,或许游戏策划会根据渠道给与不同的额外奖励,这部分也是原封不动的传给游戏充值接口。
那么,和游戏充值接口的对接是怎么样的呢?
GameID | 游戏ID | string | UserID | 用户ID | string | Param | 游戏参数 | string | Channel | 渠道名称 | string | Currency | 币种(美元,人民币) | int | Money | 货币钱数 | int |
这里,就是少了一个IAPKey,剩下的部分,全部给游戏逻辑处理,这部分要求所有游戏充值接口统一,提供给我Http调用接口,按照以上的规则。
运行顺序是 (客户端用户充值-》获得验证码-》调用统一充值接口-》去苹果验证-》验证后调用游戏充值接口-》返回操作结果)
那么,在这里,可以根据渠道,我做不同的逻辑处理,如果是Apple,那么我就会去走验证IAPKey的部分。
但是在写代码之前,要考虑的充分一些极端情况,这个里面主要有几个崩溃点。
(1)如果我的服务器和苹果服务器之间的链接断了,怎么办?
这种是一种很常见的情况,我的服务器链接不上苹果了,取不回相应的结果。那么,我必须告诉客户端,苹果验证目前有问题,请稍后重试。当然,这部分是不能展现给玩家看的。比较好的处理方法是,我默认这次操作是成功的,先把钱加上,然后后面有一个进程,独立去链接苹果服务器,取得验证结果,再修正对应的游戏充值结果,如果链接恢复,我会尝试重新验证没有验证过的IAPKey。
(2)如果我和游戏服务器充值接口之间的链接中断了,怎么办?
这一部的意思是,如果我从苹果获得了正确的验证,但是此时我和游戏服务器的http接口中断了,这时候怎么办?一样的道理,我必须得提供一个重试的机制,验证过的key,我会尝试充值,直到调用游戏充值接口返回完成。
(3)如果一个玩家反复拿一个可以通过验证的key进行充值,恶意的key,如何防止?
这里,要对每一个经过验证的key进行记录,当一个用户key请求的时候,我会先去查找,有没有一个与此匹配的key,如果有这个key,且在于充值完成状态,那么我不应该去验证苹果服务器,也不应该执行任何操作。那么有人会问,日积月累,我的key会有很多很多,如果你每次select所有的key,会不会很变态?我的回答是,不仅会很变态,还会让我的程序服务越来越慢。
那么,怎么处理呢?想一下,其实有很多轻盈的解决方法。
对于用户,我只想知道这个key处在什么状态,那么我假设,有三种状态,1:什么都没有进行状态,2:通过了苹果验证状态,3:通过了游戏充值并结束状态。这样我建立三个map,去命中,会非常合适,而且,我只关心是否在当前集合下,并不关心详细信息,这里很适合用一个单件搞定,如果你觉得还不够安全,那么用一个类似redis的也行,不过我觉得这样的需求,用缓冲层有点大材小用,必要性不大,毕竟,崩溃了我再重新加载一次就好了,这样的成本我现在的需求支付的起。
考虑到以上的情况,我的数据库构建如下,来解决三大部分的衔接。
- CREATE TABLE `Game_XXXXX` (
- `ID` int(11) NOT NULL AUTO_INCREMENT,
- `GameID` varchar(20) DEFAULT NULL COMMENT '游戏产品ID',
- `UserID` varchar(50) DEFAULT NULL COMMENT '玩家ID',
- `Param` varchar(100) DEFAULT NULL COMMENT '产品附加参数',
- `IAPKey` text COMMENT '苹果扣费ID',
- `Currency` int(11) DEFAULT NULL COMMENT '当前币种',
- `Money` int(11) DEFAULT NULL COMMENT '当前金额',
- `OperationState` int(11) DEFAULT NULL COMMENT '操作状态',
- `Channel` varchar(50) DEFAULT NULL COMMENT '渠道ID',
- `IAPState` int(11) DEFAULT NULL COMMENT '苹果返回状态',
- `IAPMessage` varchar(300) DEFAULT NULL COMMENT '苹果返回描述',
- `GameState` int(11) DEFAULT NULL COMMENT '游戏返回状态',
- `GameMessage` varchar(100) DEFAULT NULL COMMENT '游戏返回描述',
- `GameParam` varchar(100) DEFAULT NULL COMMENT '游戏返回参数',
- `CreateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
- PRIMARY KEY (`ID`)
- ) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
复制代码
XXXXX代表游戏的编号,这样我可以根据不同的游戏,建立若干个这样的表,分拆减少压力,便于统计。
由于我的这个系统实际运行过程中,并不会对这个库进行select,最多也就是insert和update,那么我完全可以用多写少读的引擎模式(我用的是mysql)。因为读我只在系统第一次启动的时候去读,慢一点就慢一点,不影响大局。只要不频繁崩溃。
那么,为了简洁一些,我写了三个存储过程,来解决三个状态的追加。
pay_Info_Insert
这个存储过程是当一个key到来的时候,如果不在我已有的key组中被命中,那么我就会把这个key插入到我的key组中,并在数据库同步插入一条。
pay_IAP_Insert
这个存储过程是当苹果验证服务器返回的时候,我需要在将这个key添加到我的IAP组里面去,同时在数据库里面更新这个key的状态。
pay_Game_Insert
当我的游戏服务器充值接口返回数据的时候,我会在Game的组里面添加进去,并更新数据库里面的这条key的状态。
这样的架构,就差不多了。
于是,我用python写了一个架构,我觉得,简单的东西,有时候可以不必用C++,脚本语言非常适合干这样的事情。
- #获得支付信息,并实现和游戏的对接
- def PayInfo(request):
- try:
- #获取参数
- strGameID = request.POST.get('GameID')
- if strGameID is None:
- strGameID = request.GET.get('GameID')
- strUserID = request.POST.get('UserID')
- if strUserID is None:
- strUserID = request.GET.get('UserID')
- strParam = request.POST.get('Param')
- if strParam is None:
- strParam = request.GET.get('Param')
- strIAPKey = request.POST.get('IAPKey')
- if strIAPKey is None:
- strIAPKey = request.GET.get('IAPKey')
- strChannel = request.POST.get('Channel')
- if strChannel is None:
- strChannel = request.GET.get('Channel')
- strCurrency = request.POST.get('Currency')
- if strCurrency is None:
- strCurrency = request.GET.get('Currency')
- strMoney = request.POST.get('Money')
- if strMoney is None:
- strMoney = request.GET.get('Money')
-
- nOperation = 0
- nAppState = 1
- if strGameID is not None and strUserID is not None and strIAPKey is not None:
- #链接数据库
- objDBOption = CDBOption(G_PAYDBSERVER, G_PAYDB, G_PAYDBUSER, G_PAYDBPASS)
-
- #去数据库查询
- nNeedInsertBase = 0 #是否需要插入基本数据
- strSQLProc = "call pay_Info_Select('" + strIAPKey + "','" + strGameID + "');"
- objRecordSet = objDBOption.ExecuteSQL(strSQLProc)
- if ((objRecordSet is not None) and (objDBOption.GetCount() <> 0)):
- strGameState = ""
- strGameMessage = ""
- strGameParam = ""
- strIAPState = ""
-
- #判断此key的状态
- for res in objRecordSet:
- nOperation = int(res[7])
- nAppState = int(res[9])
- strGameState = str(res[11])
- strGameMessage = str(res[12])
- strGameParam = str(res[13])
- print "[PayInfo]strGameState=" + strGameState + ",strGameMessage=" + strGameMessage + ",strGameParam=" + strGameParam
-
- nNeedInsertBase = 1
- break
-
- #如果加钱流程已经完成,则直接返回结果
- if nOperation == 3:
- #已经验证,就不在返回了
- objClientReturn = {}
- objClientReturn["RetID"] = strGameState
- objClientReturn["RetMessage"] = strGameMessage
- objClientReturn["Param"] = strGameParam
-
- strClient = simplejson.dumps(objClientReturn)
- return HttpResponse(strClient)
-
- #如果数据库中没有基本数据,则直接添加新项目
- if nOperation < 1:
- #是否插入基本数据
- if nNeedInsertBase == 0:
- strSQLProc = "call pay_Info_Insert('" + strGameID + "','" + strUserID + "','" + strParam + "','" + strIAPKey + "','" + strCurrency + "','" + strMoney + "','" + strChannel + "');"
- objDBOption.ExecuteSQL(strSQLProc)
-
- #如果没有去苹果验证过,则去验证
- if nOperation < 2:
- #向远端服务器发送信息
- objSendAppData = {}
- objSendAppData["receipt-data"] = strIAPKey
-
- #向苹果发送验证数据
- strPost = simplejson.dumps(objSendAppData)
- response = urllib2.urlopen(BUY_APPLE, strPost)
- objAppleResult = simplejson.loads(response.read())
- nAppState = int(objAppleResult.get("status", "0"))
- strAppMessage = str(objAppleResult.get("exception", ""))
-
- #插入数据库结果
- strSQLProc = "call pay_IAP_Insert('" + strIAPKey + "','" + strGameID + "','" + str(nAppState) + "','" + strAppMessage + "');"
- objDBOption.ExecuteSQL(strSQLProc)
-
- #获取一下最新的状态
- if nAppState == 0:
- #验证成功,向相应的游戏调用充值接口
- strURL = ""
- strPost = ""
- if strGameID == GAMEID1:
- strURL = GAME_URL_1
- strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
- if strGameID == GAMEID2:
- strURL = GAME_URL_2
- strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
- if strGameID == GAMEID3:
- strURL = GAME_URL_3
- strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
-
- print "[PayInfo]strURL=" + strURL
- print "[PayInfo]strPost=" + strPost
-
- response = urllib2.urlopen(strURL, strPost)
- objAppleResult = simplejson.loads(response.read())
- strGameState = str(objAppleResult.get("RetID", "0"))
- strGameMessage = str(objAppleResult.get("RetMessage", ""))
- strGameParam = str(objAppleResult.get("Param", ""))
-
- #添加数据库返回结果
- strSQLProc = "call pay_Game_Insert('" + strIAPKey + "','" + strGameID + "','" + strGameState + "','" + strGameMessage + "','" + strGameParam + "');"
- strSQLProc = strSQLProc.decode("utf8")
- objDBOption.ExecuteSQL(strSQLProc)
-
- objClientReturn = {}
- objClientReturn["RetID"] = strGameState
- objClientReturn["RetMessage"] = strGameMessage
- objClientReturn["Param"] = strGameParam
- strClient = simplejson.dumps(objClientReturn)
- return HttpResponse(strClient)
- else:
- #验证失败
- strGameState = G_ERROR_1001
- strGameMessage = "unused Key"
- strGameParam = ""
-
- strSQLProc = "call pay_Game_Insert('" + strIAPKey + "','" + strGameID + "','" + strGameState + "','" + strGameMessage + "','" + strGameParam + "');"
- strSQLProc = strSQLProc.decode("utf8")
- objDBOption.ExecuteSQL(strSQLProc)
-
- #返回失败信息
- objClientReturn = {}
- objClientReturn["RetID"] = strGameState
- objClientReturn["RetMessage"] = strGameMessage
- objClientReturn["Param"] = strGameParam
-
- strClient = simplejson.dumps(objClientReturn)
- return HttpResponse(strClient)
-
-
- return HttpResponse(simplejson.dumps(objAppleResult))
- else:
- #返回失败信息
- objClientReturn = {}
- objClientReturn["RetID"] = G_ERROR_1002
- objClientReturn["RetMessage"] = ""
- objClientReturn["Param"] = ""
-
- strClient = simplejson.dumps(objClientReturn)
- return HttpResponse(strClient)
-
- except Exception:
- traceback.print_exc()
- objClientReturn = {}
- objClientReturn["RetID"] = G_ERROR_1003
- objClientReturn["RetMessage"] = ""
- objClientReturn["Param"] = ""
-
- strClient = simplejson.dumps(objClientReturn)
- return HttpResponse(strClient)
复制代码
就这么多,呵呵,不复杂吧,代码也就半天的工作量。全部代码不能贴出来,毕竟涉及到一些公司的一些技术机密,这里只做一个参考。
另外,追加一个遍历key组的进程,用于处理那些上述两种情况发生的"坏死"的key。
后来实际测试我发现,苹果的https服务真的不是一般的慢。一般5-10s才会返回,我后来把上述代码改造了一下,先给钱,不验证,后面有一个进程定时去做验证的事情。如果发现异常,就直接封禁这个用户的充值。
呵呵,其实,很多东西可以很简单的。
现在这套系统,我们的三个游戏项目组公用之。
在这里记录一下,给有兴趣的朋友做一个参考。
另外,开源服务器0.83版本我做了三大修改。
1.添加了当客户端block发生的时候,服务器不再阻塞当前发送线程等待,而是直接跳过,过一段时间定时重发。这样就能解决在网络环境不好的时候,慢客户端拖累服务器的问题。
2.添加了一个"性能追迹者",我把目前的框架分为三个部分,接受 工作逻辑 和发送。这三部分独立线程运作,互不干扰,并都有独立的时间统计,计数工具,超时的处理都会记录在日志中,便于开发查阅。主要的想法是解决"到底服务器在哪里慢了?",或者说"慢在哪个环节了?",一目了然。
3.重写了模块重加载代码,对于模块的卸载和加载,我以前的做法比较粗暴,等待一段时间强行杀死模块。这种方法野蛮而且绝对会引起出错。我最近腾出手来,重写了这部分,用引用计数,只有当前模块引用计数为0的时候,才会释放并加载,这样是比较合理的。
正在测试上述三部分,代码已经完成,简单的测试也通过了,争取在年底发布前,更详细的测试一下。 |
|