找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 4399|回复: 0

关于苹果服务,用户付费的处理模式

[复制链接]
发表于 2011-12-14 17:45:16 | 显示全部楼层 |阅读模式
最近忙着改东西,越来越发现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的也行,不过我觉得这样的需求,用缓冲层有点大材小用,必要性不大,毕竟,崩溃了我再重新加载一次就好了,这样的成本我现在的需求支付的起。
考虑到以上的情况,我的数据库构建如下,来解决三大部分的衔接。
  1. CREATE TABLE `Game_XXXXX` (
  2.   `ID` int(11) NOT NULL AUTO_INCREMENT,
  3.   `GameID` varchar(20) DEFAULT NULL COMMENT '游戏产品ID',
  4.   `UserID` varchar(50) DEFAULT NULL COMMENT '玩家ID',
  5.   `Param` varchar(100) DEFAULT NULL COMMENT '产品附加参数',
  6.   `IAPKey` text COMMENT '苹果扣费ID',
  7.   `Currency` int(11) DEFAULT NULL COMMENT '当前币种',
  8.   `Money` int(11) DEFAULT NULL COMMENT '当前金额',
  9.   `OperationState` int(11) DEFAULT NULL COMMENT '操作状态',
  10.   `Channel` varchar(50) DEFAULT NULL COMMENT '渠道ID',
  11.   `IAPState` int(11) DEFAULT NULL COMMENT '苹果返回状态',
  12.   `IAPMessage` varchar(300) DEFAULT NULL COMMENT '苹果返回描述',
  13.   `GameState` int(11) DEFAULT NULL COMMENT '游戏返回状态',
  14.   `GameMessage` varchar(100) DEFAULT NULL COMMENT '游戏返回描述',
  15.   `GameParam` varchar(100) DEFAULT NULL COMMENT '游戏返回参数',
  16.   `CreateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  17.   PRIMARY KEY (`ID`)
  18. ) 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++,脚本语言非常适合干这样的事情。
  1. #获得支付信息,并实现和游戏的对接
  2. def PayInfo(request):
  3.     try:
  4.         #获取参数   
  5.         strGameID = request.POST.get('GameID')
  6.         if strGameID is None:
  7.             strGameID = request.GET.get('GameID')
  8.         strUserID = request.POST.get('UserID')
  9.         if strUserID is None:
  10.             strUserID = request.GET.get('UserID')
  11.         strParam = request.POST.get('Param')
  12.         if strParam is None:
  13.             strParam = request.GET.get('Param')
  14.         strIAPKey = request.POST.get('IAPKey')
  15.         if strIAPKey is None:
  16.             strIAPKey = request.GET.get('IAPKey')
  17.         strChannel = request.POST.get('Channel')
  18.         if strChannel is None:
  19.             strChannel = request.GET.get('Channel')
  20.         strCurrency = request.POST.get('Currency')
  21.         if strCurrency is None:
  22.             strCurrency = request.GET.get('Currency')                     
  23.         strMoney = request.POST.get('Money')
  24.         if strMoney is None:
  25.             strMoney = request.GET.get('Money')
  26.         
  27.         nOperation = 0
  28.         nAppState  = 1
  29.         if strGameID is not None and strUserID is not None and strIAPKey is not None:
  30.             #链接数据库
  31.             objDBOption = CDBOption(G_PAYDBSERVER, G_PAYDB, G_PAYDBUSER, G_PAYDBPASS)
  32.             
  33.             #去数据库查询
  34.             nNeedInsertBase = 0             #是否需要插入基本数据
  35.             strSQLProc = "call pay_Info_Select('" + strIAPKey + "','" + strGameID + "');"
  36.             objRecordSet = objDBOption.ExecuteSQL(strSQLProc)
  37.             if ((objRecordSet is not None) and (objDBOption.GetCount() <> 0)):
  38.                 strGameState   = ""
  39.                 strGameMessage = ""
  40.                 strGameParam   = ""
  41.                 strIAPState    = ""
  42.                
  43.                 #判断此key的状态
  44.                 for res in objRecordSet:
  45.                     nOperation      = int(res[7])
  46.                     nAppState       = int(res[9])
  47.                     strGameState    = str(res[11])
  48.                     strGameMessage  = str(res[12])
  49.                     strGameParam    = str(res[13])
  50.                     print "[PayInfo]strGameState=" + strGameState + ",strGameMessage=" + strGameMessage + ",strGameParam=" + strGameParam
  51.                     
  52.                     nNeedInsertBase = 1
  53.                     break
  54.                
  55.                 #如果加钱流程已经完成,则直接返回结果
  56.                 if nOperation == 3:
  57.                     #已经验证,就不在返回了
  58.                     objClientReturn = {}
  59.                     objClientReturn["RetID"]      = strGameState
  60.                     objClientReturn["RetMessage"] = strGameMessage
  61.                     objClientReturn["Param"]      = strGameParam
  62.                     
  63.                     strClient = simplejson.dumps(objClientReturn)
  64.                     return HttpResponse(strClient)
  65.             
  66.             #如果数据库中没有基本数据,则直接添加新项目
  67.             if nOperation < 1:
  68.                 #是否插入基本数据
  69.                 if nNeedInsertBase == 0:
  70.                     strSQLProc = "call pay_Info_Insert('" + strGameID + "','" + strUserID + "','" + strParam + "','" + strIAPKey + "','" + strCurrency + "','" + strMoney + "','" + strChannel + "');"
  71.                     objDBOption.ExecuteSQL(strSQLProc)
  72.             
  73.             #如果没有去苹果验证过,则去验证
  74.             if nOperation < 2:
  75.                 #向远端服务器发送信息
  76.                 objSendAppData = {}
  77.                 objSendAppData["receipt-data"] = strIAPKey
  78.             
  79.                 #向苹果发送验证数据
  80.                 strPost        = simplejson.dumps(objSendAppData)
  81.                 response = urllib2.urlopen(BUY_APPLE, strPost)
  82.                 objAppleResult = simplejson.loads(response.read())
  83.                 nAppState     = int(objAppleResult.get("status", "0"))
  84.                 strAppMessage = str(objAppleResult.get("exception", ""))
  85.                
  86.                 #插入数据库结果
  87.                 strSQLProc = "call pay_IAP_Insert('" + strIAPKey + "','" + strGameID + "','" + str(nAppState) + "','" + strAppMessage + "');"
  88.                 objDBOption.ExecuteSQL(strSQLProc)
  89.                
  90.             #获取一下最新的状态        
  91.             if nAppState == 0:
  92.                 #验证成功,向相应的游戏调用充值接口
  93.                 strURL  = ""
  94.                 strPost = ""
  95.                 if strGameID == GAMEID1:
  96.                     strURL  = GAME_URL_1
  97.                     strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
  98.                 if strGameID == GAMEID2:
  99.                     strURL  = GAME_URL_2
  100.                     strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
  101.                 if strGameID == GAMEID3:
  102.                     strURL  = GAME_URL_3
  103.                     strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
  104.                
  105.                 print "[PayInfo]strURL="  + strURL
  106.                 print "[PayInfo]strPost=" + strPost
  107.                
  108.                 response = urllib2.urlopen(strURL, strPost)
  109.                 objAppleResult = simplejson.loads(response.read())
  110.                 strGameState      = str(objAppleResult.get("RetID", "0"))
  111.                 strGameMessage    = str(objAppleResult.get("RetMessage", ""))
  112.                 strGameParam      = str(objAppleResult.get("Param", ""))
  113.                
  114.                 #添加数据库返回结果
  115.                 strSQLProc = "call pay_Game_Insert('" + strIAPKey + "','" + strGameID + "','" + strGameState + "','" + strGameMessage + "','" + strGameParam + "');"
  116.                 strSQLProc = strSQLProc.decode("utf8")
  117.                 objDBOption.ExecuteSQL(strSQLProc)               
  118.                
  119.                 objClientReturn = {}
  120.                 objClientReturn["RetID"]      = strGameState
  121.                 objClientReturn["RetMessage"] = strGameMessage
  122.                 objClientReturn["Param"]      = strGameParam               
  123.                 strClient = simplejson.dumps(objClientReturn)
  124.                 return HttpResponse(strClient)                                                                              
  125.             else:
  126.                 #验证失败
  127.                 strGameState   = G_ERROR_1001
  128.                 strGameMessage = "unused Key"
  129.                 strGameParam   = ""
  130.                
  131.                 strSQLProc = "call pay_Game_Insert('" + strIAPKey + "','" + strGameID + "','" + strGameState + "','" + strGameMessage + "','" + strGameParam + "');"
  132.                 strSQLProc = strSQLProc.decode("utf8")
  133.                 objDBOption.ExecuteSQL(strSQLProc)
  134.                
  135.                 #返回失败信息
  136.                 objClientReturn = {}
  137.                 objClientReturn["RetID"]      = strGameState
  138.                 objClientReturn["RetMessage"] = strGameMessage
  139.                 objClientReturn["Param"]      = strGameParam
  140.                
  141.                 strClient = simplejson.dumps(objClientReturn)
  142.                 return HttpResponse(strClient)
  143.                
  144.             
  145.             return HttpResponse(simplejson.dumps(objAppleResult))
  146.         else:
  147.             #返回失败信息
  148.             objClientReturn = {}
  149.             objClientReturn["RetID"]      = G_ERROR_1002
  150.             objClientReturn["RetMessage"] = ""
  151.             objClientReturn["Param"]      = ""
  152.             
  153.             strClient = simplejson.dumps(objClientReturn)
  154.             return HttpResponse(strClient)
  155.                        
  156.     except Exception:
  157.         traceback.print_exc()
  158.         objClientReturn = {}
  159.         objClientReturn["RetID"]      = G_ERROR_1003
  160.         objClientReturn["RetMessage"] = ""
  161.         objClientReturn["Param"]      = ""
  162.         
  163.         strClient = simplejson.dumps(objClientReturn)        
  164.         return HttpResponse(strClient)
复制代码

就这么多,呵呵,不复杂吧,代码也就半天的工作量。全部代码不能贴出来,毕竟涉及到一些公司的一些技术机密,这里只做一个参考。
另外,追加一个遍历key组的进程,用于处理那些上述两种情况发生的"坏死"的key。
后来实际测试我发现,苹果的https服务真的不是一般的慢。一般5-10s才会返回,我后来把上述代码改造了一下,先给钱,不验证,后面有一个进程定时去做验证的事情。如果发现异常,就直接封禁这个用户的充值。
呵呵,其实,很多东西可以很简单的。
现在这套系统,我们的三个游戏项目组公用之。
在这里记录一下,给有兴趣的朋友做一个参考。
另外,开源服务器0.83版本我做了三大修改。
1.添加了当客户端block发生的时候,服务器不再阻塞当前发送线程等待,而是直接跳过,过一段时间定时重发。这样就能解决在网络环境不好的时候,慢客户端拖累服务器的问题。
2.添加了一个"性能追迹者",我把目前的框架分为三个部分,接受 工作逻辑 和发送。这三部分独立线程运作,互不干扰,并都有独立的时间统计,计数工具,超时的处理都会记录在日志中,便于开发查阅。主要的想法是解决"到底服务器在哪里慢了?",或者说"慢在哪个环节了?",一目了然。
3.重写了模块重加载代码,对于模块的卸载和加载,我以前的做法比较粗暴,等待一段时间强行杀死模块。这种方法野蛮而且绝对会引起出错。我最近腾出手来,重写了这部分,用引用计数,只有当前模块引用计数为0的时候,才会释放并加载,这样是比较合理的。
正在测试上述三部分,代码已经完成,简单的测试也通过了,争取在年底发布前,更详细的测试一下。
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-5-3 00:06 , Processed in 0.026230 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表