类别归档:App开发

RSS feed of App开发

最后更新于 .

一. 前言

2016年6月17日凌晨5点钟,我们完成了服务器端V3版本的重构,切换的过程十分平滑且没有对线上用户产生任何影响。

这也正式标志着,我们的游戏服务器进入了一个全新的阶段。

我们上一次的重构是在 2014年12月23日,现在看看,时间过的真快啊。

而熟悉我的人应该知道,我特意为上一次重构写过一篇《游戏服务器端架构升级之路》,其中详细的讲述了我们游戏服务器从农业时代跨越到工业时代的历程。

而这次V3版本的重构,我将其定义为第二次工业革命。也许它没有那么的强大和完美,但是他切实的解决了现存的大部分问题。

二. 背景

之前的文章已经说过,V2版本的服务器的几个优点:

  1. 支持服务器代码热更新而不影响外网服务
  2. 架构模式足够简单:push-pull

但是,其简单的架构也存在一些缺点:

  1. 业务??橹淙菀谆ハ嘤跋?/p>

    比如两个游戏玩法 A 和 B,内部使用的逻辑、存储服务器都完全不同,但是在worker层却是共用的。

    所以一旦玩法A的服务器出现问题导致处理变慢,那么worker就会被堵住,而玩法B也会跟着遭殃。

    同时,即时在一个业务??槟?,也存在请求处理优先级的问题,比如拉取牌局记录和跟注,要尽量避免跟注这种核心逻辑受到影响。

  2. 限制了游戏逻辑的实现方式

    V2的多worker的模式,导致worker必须限制为无状态的,因为worker可能处理任何一个请求,而一个请求也可能被分配到任何一个worker上。

    这一点是之前解决服务器热重启的关键,但同时也限制了我们代码逻辑多样性的实现。

    比如我们的游戏桌子数据是存储在redis中,所有人对桌子的写操作可能同时分配到多个worker上,而为了避免写冲突,我们不得不通过redis来实现分布式锁 ...

最后更新于 .

之前在文章里面有提到过,很多事情,并没有绝对的对错,只是度的问题。而度的衡量又取决于时、势二字。所以当形势逼人的时候,基本就是这件事情非做不可的时候了。

先说下背景,公司的服务器一直用的阿里云,包括mysql、redis也都是买了ECS自己搭建的。这里面有几个原因:

  1. 创业的时候,阿里云只提供mysql的存储,redis的存储还没提供。
  2. 没钱,即时现在去看redis的存储价格也是贵的吓人。

这样自己来搞存储有好处也有坏处。 好处:

  1. 完全可控,比如连接数限制,内存限制,存储限制?;褂惺荼阜莸牧榛钚缘鹊?。
  2. 强迫团队服务器研发要有存储运维能力。
  3. 省钱

坏处:

  1. 冷备、热备方案不完善。
  2. 存储运维的成本较高,需要长时间积累。

ok,问题就是这样,接下来再来说一下我们之前的冷备和热备方案。 可以说极其简陋:

  1. mysql、redis每天10点冷备,备份到本地磁盘和阿里云OSS
  2. redis使用rdb落地,每60秒至少有1次写就会触发落地。

这样做的问题其实挺多的,主要几个:

  1. mysql dump的时候会导致游戏卡顿,即使加了 --single-transaction 参数 也仅仅是缓解
  2. 冷备频率过低,真出现问题数据已经太久
  3. 没有热备,风险较大

针对这些问题,我们先做了mysql备份的优化 ...

最后更新于 .

其实打算做游戏内热更新也是几个月之前的事情了,在方案经历了数次变迁之后,最近才终于应用到了外网的bugfix中。
但是就目前数据来看,热更新由于要下载资源,会使新用户的进入门槛变高,所以留存收到了一定影响,基本降低了10个点。
当然,也可能是热更新的功能存在bug。

好了,我们还是进入正题吧!

方案一

最早的时候,我们想用一种类似打patch的方式来更新。
即将lua代码和资源打成一个zip包,而每个zip包只要代码或者资源发生变化,都会有一个自增的小版本号(大版本号为打在apk或者ipa里的版本号)。
不同的zip包之间diff就可以生成一个patch文件,而为了开发的简单,这个patch列表只会将文件路径列出来,之后客户端要去完整的下载新的文件覆盖。

然而这个方案有很多的问题。

  1. 小版本号不好维护 因为代码和资源会不停的修改,而如果人工来维护小版本号,会极其复杂。如果通过机器来维护也是个很麻烦的工程

  2. patch包太多 如果我们将小版本号的patch包先生成好放到服务器上,那么如果我们有100个小版本,就会有非常多 1->100, 2->100, 3->100 ... 这种格式的patch包。 并且如果小版本号继续增长,patch包的增长基本会失控。

  3. 消耗大量流量,速度缓慢 也许有同学会说,不如只有 1->2, 2->3, 3->4 这样的单个小版本之间的升级patch,客户端自己挨个下载就好。

    但是这样客户端如果是个很小的版本号,就不会不停的重复下载,导致迟迟进入不了游戏,并且流量也大量消耗。

综上所述,方案一有太多的弊端。
这也是为什么我们做完了之后迟迟不敢上线的原因 ...

最后更新于 .

一. 发现问题

最近新版本上线,公司内部的几台iphone测试机测试都一切正常,但是外网用户却频频反馈崩溃。

在友盟上看了错误率统计,曲线如下:

可以看出,错误率确实有了很大的上浮?;究梢匀啡弦欢ㄊ浅鑫侍饬?。

二. 还原崩溃现场

我们游戏用得cocos2dx-lua,既然是崩溃,那就一定是到了c++端了,我们点击友盟上数量最多的那个看一下:

到了这里,遇到了第一个难点,因为按照友盟文档,使用他的错误dump工具是无效的,一旦看不到具体的错误内容,基本就无法继续了。

(而因为这个问题,发现有人推荐bugly,打算在下一个版本里面试用一下,再给大家反馈)

既然友盟提供的工具不行,我们就得像别的办法,起码现在错误的原始内容是有了,只是要想办法还原出来而已。

仔细研究了一下,友盟其实也是使用了mac原本就提供的工具来做了二次封装,那不如直接用原生的工具了。

具体方法如下:

  1. 找到当时出包得那个archive文件,显示包文件,在其中找到如下两个文件放到同一目录:

    xxx.app xxx.app.dSYM

    其中 xxx.app.dSYM 即为符号表文件。

  2. 查看其对应的UUID,一定要与crash log的UUID一致

    $: dwarfdump --uuid xxx.app ...

最后更新于 .

之前一直用友盟的自动更新功能,但是友盟一直没有内置实现强制更新的功能,如果要在其基础上模拟实现会很麻烦,所以干脆就自己做了。

其实实现上比较简单,这里跟大家介绍下。

1. web接口

需要提供一个接口供客户端查询更新状态,并且在需要更新时,告知客户端新APK地址。

接口参数如下:

  • package ? 包名,因为有时候会出现同一个应用换包名打包的情况
  • version 版本号,即android清单文件里面的versionCode
  • channel 渠道号
  • os 操作系统,android/ios。ios 这里仅作预留。

?

之所以传入这些字段,是要在与服务器端的包匹配时,务必满足:

package, channel, os 相等,并且服务器端的version 大于 客户端传入的version

代码如下:

os = request.GET.get('os')
pkg_name = request.GET.get('package')
channel = request.GET.get('channel')
version = request.GET ...

最后更新于 .

这几天的心情非常好,主要原因是我们把服务器端的架构升级到了 2.0,这样最大的一个好处就是:

Server重启完全不会影响外网服务

所以,也是想趁此机会,服务器端整个发展的历程,跟大家分享一下,干货比较多,框架代码也会全部开源:)

 

一. 农业时代

创业最重要的就是一个“快”字,所以最开始的时候,所有的架构都以快速出模型为前提。

而??次也┛偷呐笥延Ω弥牢叶詐ython情有独钟,所以自然的,python成为了我开发服务端框架的语言。

python自带的多线程tcp服务器框架非常简单:ThreadingTCPServer,即每个链接一个线程的模式:

import SocketServer

class RequestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        f = self.request.makefile('r')

        while True:
            message = f.readline()
            if not message:
                print 'client closed'
                break
            print "message, len: %s, content ...

最后更新于 .

发现自己经?;嵋黄恼滦戳耍ㄒ唬┲?,很久都不写(二),搞得最后自己都快要忘记了,所以这次赶紧把统一支付的文章给补上。

上次的文章中将统一支付的v1版本已经讲解ok了,但是还剩下两个问题:

  • 服务器端没有办法做分布式
  • 客户端对支付sdk进行插件式管理十分困难

?

我们一个个来说

一. 解决服务器端分布式的问题

解决这个问题的核心思路比较简单:

之前我们是把event的通知放在进程内存中,现在我们做成网络通信

由于支付的请求量本身不属于高并发,所以就放弃了打算直接写通知server的想法,转而看一下有没有什么简单的解决方案。

而由于自己之前redis的使用经历,恰好知道redis有一个pubsub模式,很适合做这种监听和通知的工作。

python的实现示例代码如下:

import time
import config
from share.vals import rds
from share.utils import safe_str
from gevent.timeout import Timeout
from urllib import quote, unquote

class RedisPubSub(object):
    """
    用redis订阅/发布消息
    """
    # 订阅频道
    sub_key ...

最后更新于 .

其实想跟大家分享这套支付系统的架构已经很久了,今天总算有时间写出来了。

先说说这套系统的需求由来吧:

  1. 笔者公司的游戏产品已经有几款了,每次上各种渠道都是要搭配不同的计费方式,并且每开发游戏都要重复一遍痛苦的接入sdk流程
  2. 游戏的支付需要出各种报表以及统计,每个游戏单独去做对人力的消耗巨大

基于以上几点,我这边设计了统一支付系统。

这个系列一共会分两篇文章,分别对应系统的v1版和v2版,我们这一篇先从v1起介绍。

在仔细分析了国内的大多数支付sdk之后,我们梳理出游戏的支付流程大体可以实现为两类:

  • 第三方sdk服务器进行支付结果通知
  • 第三方sdk客户端直接返回支付结果通知,没有服务器支付结果通知。

对于调用方而言,这两种方式各有好处。

  • 第一种方式更加安全,但是支付调用的时间相对较长
  • 第二种方式速度更快,但是很容易被不怀好意的人破解。参见之前的文章:google支付接口被刷以及解决方案

接下来,我们来看一下我这边设计的统一支付流程。

客户端:

1

服务器端:

2

简单解释一下:

  • 每次支付开始,都要让服务器生成一个订单作为此次支付的记录,订单的id即为 bill_id。订单有4中状态:订单生成,支付失败,支付成功,发货成功。
  • pay_server即为统一支付系统的服务器端,考虑到调用量和方便调试,使用了简单的http协议+json+sign的方式

对于服务器内部,唯一麻烦的一点是,《等待pay_server支付结果通知》这个接口。因为这个http请求需要支持挂起,在第三方支付服务器通知了pay_server之后,pay_server 根据通知里面透传的bill_id 将订单状态修改后,再给客户端结果 ...

最后更新于 .

上一篇文章聊了下数据存储和常用的传输协议,不过对于自定义传输协议这里留了个坑,正好有点时间,就抓紧填上:)

既然选择原生socket,那么有个基本的选择就是tcp/udp的问题.

这个其实还是看业务自己的选择,只是如果选择了udp的话,那么很多问题都可以不用考虑,比如粘包问题。但是udp有个限制是每次传输的数据大小不能超过64K,这个要注意。

为了考虑复杂的情况,我们还是主要说tcp的实现,这篇文章先说下socket使用相关的一些库和代码吧

Android端开发

对于android端,我们有两个主要选择:阻塞socket和非阻塞socket。

阻塞socket就是正常的socket,当调用recv的时候,会阻塞住直到返回数据。

非阻塞socket在android上可以直接使用nio,因为自己之前一直是做c++和python,所以其实一开始nio的时候真心有些不太使用,把几个要注意的点列一下:

1. 当网络断线的时候,有些手机如S4,会出现这个问题,channel.finishConnect()会一直等超时 60秒。之后会抛出一个 TimeoutException.

解决方法是:

channel.socket().setSoTimeout(5)

来设置成只等5秒。

比较诡异的是,明明是异步io了,为什么还是会有等待超时的情况?;挂谘芯肯?。

2. 当网络不在线的时候,启动connect,依然可以进入isConnectable的判断中,并且可以调用finishconnect。

解决方案:finishConnect之后 ...

最后更新于 .

经过这么久在android客户端和服务器端的开发,感觉还是积累了不少东西想要和大家分享一下,但是好想单独拎一个点出来又不太值得,所以就汇集到一起写成系列吧。

一. 关于用户数据存储

首先在注册的协议里,定义如下公共传输字段:

version: 这个其实是xml中配置的versionCode。versionName个人认为没有什么必要,所以就不传了。

channel: ?用户渠道,这个和xml中的UMENG_CHANNEL 是共用的,因为一直在使用umeng,所以这样定义反而清楚一些。

device_id: 设备ID

os: 操作系统类型,这里默认传入android

os_version: 操作系统版本

对于小数据量,用户数据存储在mysql中是相对较好的选择,这里直接以django的model为例:

from django.db import models
class User(models.Model):
    device_id = models.CharField(max_length=255, null=True, blank=True)
    version = models.IntegerField()
    channel = models.CharField(max_length=64, null ...