标签 Programmer 下的文章

Golang Channel用法简编

在进入正式内容前,我这里先顺便转发一则消息,那就是Golang 1.3.2已经正式发布了。国内的golangtc已经镜像了golang.org的安装包下载页面,国内go程序员与爱好者们可以到"Golang中 国",即golangtc.com去下载go 1.3.2版本。

Go这门语言也许你还不甚了解,甚至是完全不知道,这也有情可原,毕竟Go在TIOBE编程语言排行榜上位列30开外。但近期使用Golang 实现的一杀手级应用 Docker你却不该不知道。docker目前火得是一塌糊涂啊。你去国内外各大技术站点用眼轻瞥一下,如 果没有涉及到“docker”字样新闻的站点建 议你以后就不要再去访问了^_^。Docker是啥、怎么用以及基础实践可以参加国内一位仁兄的经验之作:《 Docker – 从入门到实践》。

据我了解,目前国内试水Go语言开发后台系统的大公司与初创公司日益增多,比如七牛、京东、小米,盛大,金山,东软,搜狗等,在这里我们可以看到一些公司的Go语言应用列表,并且目前这个列表似乎依旧在丰富中。国内Go语言的推广与布道也再稳步推进中,不过目前来看多以Go入 门与基础为主题,Go idioms、tips或Best Practice的Share并不多见,想必国内的先行者、布道师们还在韬光养晦,积攒经验,等到时机来临再厚积薄发。另外国内似乎还没有一个针对Go的 布道平台,比如Golang技术大会之类的的平台。

在国外,虽然Go也刚刚起步,但在Golang share的广度和深度方面显然更进一步。Go的国际会议目前还不多,除了Golang老东家Google在自己的各种大会上留给Golang展示自己的 机会外,由 Gopher Academy 发起的GopherCon 会议也于今年第一次举行,并放出诸多高质量资料,在这里可以下载。欧洲的Go语言大会.dotgo也即将开幕,估计后续这两个大会将撑起Golang技术分享 的旗帜。

言归正传,这里要写的东西并非原创,自己的Go仅仅算是入门级别,工程经验、Best Practice等还谈不上有多少,因此这里主要是针对GopherCon2014上的“舶来品”的学习心得。来自CloudFlare的工程师John Graham-Cumming谈了关于 Channel的实践经验,这里针对其分享的内容,记录一些学习体会和理解,并结合一些外延知识,也可以算是一种学习笔记吧,仅供参考。

一、Golang并发基础理论

Golang在并发设计方面参考了C.A.R Hoare的CSP,即Communicating Sequential Processes并发模型理论。但就像John Graham-Cumming所说的那样,多数Golang程序员或爱好者仅仅停留在“知道”这一层次,理解CSP理论的并不多,毕竟多数程序员是搞工程 的。不过要想系统学习CSP的人可以从这里下载到CSP论文的最新版本。

维基百科中概要罗列了CSP模型与另外一种并发模型Actor模型的区别:

Actor模型广义上讲与CSP模型很相似。但两种模型就提供的原语而言,又有一些根本上的不同之处:
    – CSP模型处理过程是匿名的,而Actor模型中的Actor则具有身份标识。
    – CSP模型的消息传递在收发消息进程间包含了一个交会点,即发送方只能在接收方准备好接收消息时才能发送消息。相反,actor模型中的消息传递是异步 的,即消息的发送和接收无需在同一时间进行,发送方可以在接收方准备好接收消息前将消息发送出去。这两种方案可以认为是彼此对偶的。在某种意义下,基于交 会点的系统可以通过构造带缓冲的通信的方式来模拟异步消息系统。而异步系统可以通过构造带消息/应答协议的方式来同步发送方和接收方来模拟交会点似的通信 方式。
    – CSP使用显式的Channel用于消息传递,而Actor模型则将消息发送给命名的目的Actor。这两种方法可以被认为是对偶的。某种意义下,进程可 以从一个实际上拥有身份标识的channel接收消息,而通过将actors构造成类Channel的行为模式也可以打破actors之间的名字耦合。

二、Go Channel基本操作语法

Go Channel的基本操作语法如下:

c := make(chan bool) //创建一个无缓冲的bool型Channel

c <- x        //向一个Channel发送一个值
<- c          //从一个Channel中接收一个值
x = <- c      //从Channel c接收一个值并将其存储到x中
x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

不带缓冲的Channel兼具通信和同步两种特性,颇受青睐。

三、Channel用作信号(Signal)的场景

1、等待一个事件(Event)

等待一个事件,有时候通过close一个Channel就足够了。例如:

//testwaitevent1.go
package main

import "fmt"

func main() {
        fmt.Println("Begin doing something!")
        c := make(chan bool)
        go func() {
                fmt.Println("Doing something…")
                close(c)
        }()
        <-c
        fmt.Println("Done!")
}

这里main goroutine通过"<-c"来等待sub goroutine中的“完成事件”,sub goroutine通过close channel促发这一事件。当然也可以通过向Channel写入一个bool值的方式来作为事件通知。main goroutine在channel c上没有任何数据可读的情况下会阻塞等待。

关于输出结果:

根据《Go memory model》中关于close channel与recv from channel的order的定义:The closing of a channel happens before a receive that returns a zero value because the channel is closed.

我们可以很容易判断出上面程序的输出结果:

Begin doing something!
Doing something…
Done!

如果将close(c)换成c<-true,则根据《Go memory model》中的定义:A receive from an unbuffered channel happens before the send on that channel completes.
"<-c"要先于"c<-true"完成,但也不影响日志的输出顺序,输出结果仍为上面三行。

2、协同多个Goroutines

同上,close channel还可以用于协同多个Goroutines,比如下面这个例子,我们创建了100个Worker Goroutine,这些Goroutine在被创建出来后都阻塞在"<-start"上,直到我们在main goroutine中给出开工的信号:"close(start)",这些goroutines才开始真正的并发运行起来。

//testwaitevent2.go
package main

import "fmt"

func worker(start chan bool, index int) {
        <-start
        fmt.Println("This is Worker:", index)
}

func main() {
        start := make(chan bool)
        for i := 1; i <= 100; i++ {
                go worker(start, i)
        }
        close(start)
        select {} //deadlock we expected
}

3、Select

【select的基本操作】
select是Go语言特有的操作,使用select我们可以同时在多个channel上进行发送/接收操作。下面是select的基本操作。

select {
case x := <- somechan:
    // … 使用x进行一些操作

case y, ok := <- someOtherchan:
    // … 使用y进行一些操作,
    //
检查ok值判断someOtherchan是否已经关闭

case outputChan <- z:
    // … z值被成功发送到Channel上时

default:
    // … 上面case均无法通信时,执行此分支
}

【惯用法:for/select】

我们在使用select时很少只是对其进行一次evaluation,我们常常将其与for {}结合在一起使用,并选择适当时机从for{}中退出。

for {
        select {
        case x := <- somechan:
            // … 使用x进行一些操作

        case y, ok := <- someOtherchan:
            // … 使用y进行一些操作,
            // 检查ok值判断someOtherchan是否已经关闭

        case outputChan <- z:
            // … z值被成功发送到Channel上时

        default:
            // … 上面case均无法通信时,执行此分支
        }
}

【终结workers】

下面是一个常见的终结sub worker goroutines的方法,每个worker goroutine通过select监视一个die channel来及时获取main goroutine的退出通知。

//testterminateworker1.go
package main

import (
    "fmt"
    "time"
)

func worker(die chan bool, index int) {
    fmt.Println("Begin: This is Worker:", index)
    for {
        select {
        //case xx:
            //做事的分支
        case <-die:
            fmt.Println("Done: This is Worker:", index)
            return
        }
    }
}

func main() {
    die := make(chan bool)

    for i := 1; i <= 100; i++ {
        go worker(die, i)
    }

    time.Sleep(time.Second * 5)
    close(die)
    select {}
//deadlock we expected
}

【终结验证】

有时候终结一个worker后,main goroutine想确认worker routine是否真正退出了,可采用下面这种方法:

//testterminateworker2.go
package main

import (
    "fmt"
    //"time"
)

func worker(die chan bool) {
    fmt.Println("Begin: This is Worker")
    for {
        select {
        //case xx:
        //做事的分支
        case <-die:
            fmt.Println("Done: This is Worker")
            die <- true
            return
        }
    }
}

func main() {
    die := make(chan bool)

    go worker(die)

    die <- true
    <-die
    fmt.Println("Worker goroutine has been terminated")
}

【关闭的Channel永远不会阻塞】

下面演示在一个已经关闭了的channel上读写的结果:

//testoperateonclosedchannel.go
package main

import "fmt"

func main() {
        cb := make(chan bool)
        close(cb)
        x := <-cb
        fmt.Printf("%#v\n", x)

        x, ok := <-cb
        fmt.Printf("%#v %#v\n", x, ok)

        ci := make(chan int)
        close(ci)
        y := <-ci
        fmt.Printf("%#v\n", y)

        cb <- true
}

$go run testoperateonclosedchannel.go
false
false false
0
panic: runtime error: send on closed channel

可以看到在一个已经close的unbuffered channel上执行读操作,回返回channel对应类型的零值,比如bool型channel返回false,int型channel返回0。但向close的channel写则会触发panic。不过无论读写都不会导致阻塞。

【关闭带缓存的channel】

将unbuffered channel换成buffered channel会怎样?我们看下面例子:

//testclosedbufferedchannel.go
package main

import "fmt"

func main() {
        c := make(chan int, 3)
        c <- 15
        c <- 34
        c <- 65
        close(c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)
        fmt.Printf("%d\n", <-c)

        c <- 1
}

$go run testclosedbufferedchannel.go
15
34
65
0
panic: runtime error: send on closed channel

可以看出带缓冲的channel略有不同。尽管已经close了,但我们依旧可以从中读出关闭前写入的3个值。第四次读取时,则会返回该channel类型的零值。向这类channel写入操作也会触发panic。

【range】

Golang中的range常常和channel并肩作战,它被用来从channel中读取所有值。下面是一个简单的实例:

//testrange.go
package main

import "fmt"

func generator(strings chan string) {
        strings <- "Five hour's New York jet lag"
        strings <- "and Cayce Pollard wakes in Camden Town"
        strings <- "to the dire and ever-decreasing circles"
        strings <- "of disrupted circadian rhythm."
        close(strings)
}

func main() {
        strings := make(chan string)
        go generator(strings)
        for s := range strings {
                fmt.Printf("%s\n", s)
        }
        fmt.Printf("\n")
}

四、隐藏状态

下面通过一个例子来演示一下channel如何用来隐藏状态:

1、例子:唯一的ID服务

//testuniqueid.go
package main

import "fmt"

func newUniqueIDService() <-chan string {
        id := make(chan string)
        go func() {
                var counter int64 = 0
                for {
                        id <- fmt.Sprintf("%x", counter)
                        counter += 1
                }
        }()
        return id
}
func main() {
        id := newUniqueIDService()
        for i := 0; i < 10; i++ {
                fmt.Println(<-id)
        }
}

$ go run testuniqueid.go
0
1
2
3
4
5
6
7
8
9

newUniqueIDService通过一个channel与main goroutine关联,main goroutine无需知道uniqueid实现的细节以及当前状态,只需通过channel获得最新id即可。

五、默认情况

我想这里John Graham-Cumming主要是想告诉我们select的default分支的实践用法。

1、select  for non-blocking receive

idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列

select {
case b = <-idle:
 //尝试从idle队列中读取
    …
default:  //队列空,分配一个新的buffer
        makes += 1
        b = make([]byte, size)
}

2、select for non-blocking send

idle:= make(chan []byte, 5) //用一个带缓冲的channel构造一个简单的队列

select {
case idle <- b: //尝试向队列中插入一个buffer
        //…
default: //队列满?

}

六、Nil Channels

1、nil channels阻塞

对一个没有初始化的channel进行读写操作都将发生阻塞,例子如下:

package main

func main() {
        var c chan int
        <-c
}

$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!

package main

func main() {
        var c chan int
        c <- 1
}

$go run testnilchannel.go
fatal error: all goroutines are asleep – deadlock!

2、nil channel在select中很有用

看下面这个例子:

//testnilchannel_bad.go
package main

import "fmt"
import "time"

func main() {
        var c1, c2 chan int = make(chan int), make(chan int)
        go func() {
                time.Sleep(time.Second * 5)
                c1 <- 5
                close(c1)
        }()

        go func() {
                time.Sleep(time.Second * 7)
                c2 <- 7
                close(c2)
        }()

        for {
                select {
                case x := <-c1:
                        fmt.Println(x)
                case x := <-c2:
                        fmt.Println(x)
                }
        }
        fmt.Println("over")
}

我们原本期望程序交替输出5和7两个数字,但实际的输出结果却是:

5
0
0
0
… … 0死循环

再仔细分析代码,原来select每次按case顺序evaluate:
    – 前5s,select一直阻塞;
    – 第5s,c1返回一个5后被close了,“case x := <-c1”这个分支返回,select输出5,并重新select
    – 下一轮select又从“case x := <-c1”这个分支开始evaluate,由于c1被close,按照前面的知识,close的channel不会阻塞,我们会读出这个 channel对应类型的零值,这里就是0;select再次输出0;这时即便c2有值返回,程序也不会走到c2这个分支
    – 依次类推,程序无限循环的输出0

我们利用nil channel来改进这个程序,以实现我们的意图,代码如下:

//testnilchannel.go
package main

import "fmt"
import "time"

func main() {
        var c1, c2 chan int = make(chan int), make(chan int)
        go func() {
                time.Sleep(time.Second * 5)
                c1 <- 5
                close(c1)
        }()

        go func() {
                time.Sleep(time.Second * 7)
                c2 <- 7
                close(c2)
        }()

        for {
                select {
                case x, ok := <-c1:
                        if !ok {
                                c1 = nil
                        } else {
                                fmt.Println(x)
                        }
                case x, ok := <-c2:
                        if !ok {
                                c2 = nil
                        } else {
                                fmt.Println(x)
                        }
                }
                if c1 == nil && c2 == nil {
                        break
                }
        }
        fmt.Println("over")
}

$go run testnilchannel.go
5
7
over

可以看出:通过将已经关闭的channel置为nil,下次select将会阻塞在该channel上,使得select继续下面的分支evaluation。

七、Timers

1、超时机制Timeout

带超时机制的select是常规的tip,下面是示例代码,实现30s的超时select:

func worker(start chan bool) {
        timeout := time.After(30 * time.Second)
        for {
                select {
                     // … do some stuff
                case <- timeout:
                    return
                }
        }
}

2、心跳HeartBeart

与timeout实现类似,下面是一个简单的心跳select实现:

func worker(start chan bool) {
        heartbeat := time.Tick(30 * time.Second)
        for {
                select {
                     // … do some stuff
                case <- heartbeat:
                    //… do heartbeat stuff
                }
        }
}

Ubuntu Server 14.04安装docker

近期在研究docker这一轻量级容器引擎,研究docker对日常开发测试工作以及产品部署运维工作能带来哪些便利。前些时候刚刚将工作环境从 Ubuntu搬到了Mac Air上,对Mac OS X的一切均不甚熟悉,给docker研究带来了不便,于是打算在VirtualBox中安装一Ubuntu Server作为docker之承载平台。这里记录一下安装配置过程,主要为了备忘,如果能给其他人带来帮助,我会甚感欣慰。

docker官方对ubuntu的支持是蛮好的。docker对Linux内核版本有要求,要>=3.8,Ubuntu Server目前最新版本14.04.1恰符合这一要求,其kernel version = 3.13.0-32。

一、VirtualBox安装Ubuntu Server 14.04.1

VirtualBox安装Ubuntu OS做过了不止一遍,即便是换成最新的14.04.1 Server版,差别也没有太多,无非是按照安装提示,逐步Next。这里给Ubuntu Server 14.04分配了1G Memory, 32G动态硬盘空间。

【配置源】

  默认情况下,/etc/apt/sources.list中只有一组源:cn.archive.ubuntu.com/ubuntu。这个国外源的下载速度显然无法满足我的要求,于是我把我常用的sohu源加入sources.list中,并且放在前面:

  deb http://mirrors.sohu.com/ubuntu/ trusty main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-security main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted
  deb http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted

  deb-src http://mirros.sohu.com/ubuntu/ trusty main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-security main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-updates main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-proposed main restricted
  deb-src http://mirrors.sohu.com/ubuntu/ trusty-backports main restricted

  公司采用代理访问外网,于是还得在/etc/apt/apt.conf中加上代理的设置,否则无法更新源,也就无法安装第三方软件:

  Acquire::http::Proxy "http://username:passwd@proxyhost:proxyport";

 【乱码处理】

  由于安装时候选择了中国区域(locale zh_CN.UTF-8),因此在VirtualBox的窗口中直接执行命令的提示信息可能是乱码。对于Server,我们一般是不会直接通过其主机显示 器登录使用的,都是通过终端访问,但在未安装和开启ssh服务和未配置端口转发前,我们只能先凑合这个窗口了。可先将/etc/default /locale中的LANGUAGE由"zh_CN:zh"改为"en_US:en", logout后重新登录就可以看到非乱码的英文提示信息了。

【安装VirtualBox增强组件】

  Ubuntu Server默认是不安装图形桌面的,只有一个命令行窗口,连鼠标都无法使用。因此增强组件安装的意义没有桌面系统那么强烈。我能想到的只有“共享目录”这一个功能有些用处。

  安装方法也不难,按下面步骤逐步操作即可:

  sudo apt-get install build-essential linux-headers-$(uname -r) dkms gcc g++
  sudo mnt /dev/cdrom /mnt
  cd /mnt
  sudo bash ./VBoxLinuxAdditions.run

  如果结果都是"done",重启后就ok了。

【安装ssh服务】

    ssh服务由openssh-server提供:
    sudo apt-get openssh-server
   
   安装成功后,ssh server服务就会自动启动起来。

   不过我们还是需要修改一些配置,比如允许Root登录:打开/etc/ssh/sshd_config,将PermitRootLogin后面的内容改为yes。
   
【设置端口转发】

  前面说过,对于Server,我们更多是在其他主机上通过ssh或telnet远程访问该Server并执行各种操作。由于这里是VirtualBox安 装的虚拟机,其他主机无法看到这台Server,我们需要设置端口转发将外部访问的数据转发给这个内部虚拟Server。

  我们通过VirtualBox软件提供的图形界面即可完成这个操作:
    1、“设置”这个虚拟机
    2、在“网络”标签中,点击“端口转发”按钮,进入端口转发规则添加窗口。
    3、添加一条规则:
          名称:ssh-rules
          协议:TCP
          主机IP、子系统IP可以为空。
          主机端口:2222
          子系统端口:22
   4、配置结束

    配置结束后,我们在宿主机上netstat -an|grep 2222,可以看到VirtualBox增加了该端口2222的监听。

  现在我们就可以在其他机器上通过ssh -l tonybai 宿主机ip -p 2222的方式登录到我们新安装的这台虚拟Server了。

  
二、安装docker

docker目前的最新版本号是1.2.0,但14.04源中的docker还是正式稳定版1.0之前的版本,显然这是无法满足我的要求的。我们只能另外添加docker源来安装最新版docker。

  【安装docker】

    我们在/etc/apt/sources.list中加入下面这个源:
       deb http://mirror.yandex.ru/mirrors/docker/ docker main
  
    执行apt-get update。

    sudo apt-get install lxc-docker

正在读取软件包列表… 完成
正在分析软件包的依赖关系树      
正在读取状态信息… 完成      
将会安装下列额外的软件包:
  aufs-tools cgroup-lite git git-man liberror-perl lxc-docker-1.2.0
建议安装的软件包:
  git-daemon-run git-daemon-sysvinit git-doc git-el git-email git-gui gitk
  gitweb git-arch git-bzr git-cvs git-mediawiki git-svn
下列新软件包将被安装:
  aufs-tools cgroup-lite git git-man liberror-perl lxc-docker lxc-docker-1.2.0
升级了 0 个软件包,新安装了 7 个软件包,要卸载 0 个软件包,有 59 个软件包未被升级。
需要下载 7,477 kB 的软件包。
解压缩后会消耗掉 35.4 MB 的额外空间。
您希望继续执行吗? [Y/n] y

  这个源里的docker居然是最新版。于是安装之。安装后,我们执行docker version来确认一下安装是否成功。

  tonybai@ubuntu-Server-14:~$ docker version
Client version: 1.2.0
Client API version: 1.14
Go version (client): go1.3.1
Git commit (client): fa7b24f
OS/Arch (client): linux/amd64
2014/09/26 13:56:53 Get http:///var/run/docker.sock/v1.14/version: dial unix /var/run/docker.sock: permission denied

  【为docker设置http代理】

    在公司内使用代理才能访问到外网,于是我们也需要为docker命令设置代理以使其顺利执行命令。

    我们安装的docker实际上分为两部分,docker命令行和docker daemon。两者是C/S结构,docker命令行将用户的请求转发给docker daemon,后者会真正与外部通信完成各种操作。

    于是我们可以这样为docker daemon设置http_proxy:
    sudo service docker stop
    sudo http_proxy='http://user:passwd@proxyhost:port' docker -d &

    这样设置启动后,我们可以通过下面命令测试设置是否ok:

      sudo docker search ubuntu

    如果你看到下面信息,说明设置成功了:

    tonybai@ubuntu-Server-14:~$ sudo docker search ubuntu
[info] GET /v1.14/images/search?term=ubuntu
[b36518a9] +job search(ubuntu)
[b36518a9] -job search(ubuntu) = OK (0)
NAME                                             DESCRIPTION                                     STARS                                   OFFICIAL   AUTOMATED
ubuntu                                           Official Ubuntu base image                      709                                     [OK]      
dockerfile/ubuntu                                Trusted automated Ubuntu (http://www.ubunt…   24                                                 [OK]
crashsystems/gitlab-docker                       A trusted, regularly updated build of GitL…   20                                                 [OK]
ubuntu-upstart                                   Upstart is an event-based replacement for …   13                                      [OK]      

… ….

 

Cocos2d-x集成Amazon内购和GameCircle服务

由于种种原因,这篇文章已经拖延了N多时间了。今天花了些时间把如何在Cocos2d-x(我用的版本是2.2.2)游戏中集成Amazon内购GameCircle服务(仅适用于Android版本)整理一下,发出来,作备忘。

之前在做“手指足球世界杯2014”时,想给这款小游戏加上内购(In-App Purchasing)和积分榜(ScoreBoard)功能。说到Android手机游戏的内购,人们第一时间想到的就是Google Play,不过悲催的是,Google Play在国内各种无法访问,行货机也不预装,其相关Service的测试十分困难,翻看了一些集成Google Game Service的文章,其过程坎坷之程度让人望而却步。于是我将目光转而投向了Amazon Game Service。亚马逊的游戏服务起步要晚些,成熟性肯定不如Google,但在国内来说也不失为另一个不错的选择,Google虽好,但访问不了有啥 法。但似乎国内同行使用Amazon游戏服务的并不多,度娘上相关中文资料甚少。但从Amazon发布的数据来看,其市场正在逐步扩大,并紧紧跟随 Google Play的脚步。

之前用kindle paperwhite时在amazon.com上注册了一个国际帐号,这次正好用这个。不过你要使用Amazon的Game Service,普通Amazon帐号是不行的。你要升级为Amazon的Developer。申请Developer帐号的过程还是蛮繁琐的,要提交一 堆资料,具体细节我大致忘的差不多了,这里就不说了。按照Amazon网站的提示一步一步做就是了。

有了帐号后,你可以下载Amazon的Game SDK了,这个包有近50M大小,本地解压后可以看到其提供的Android SDK种类:

AmazonSDK/Android$ ls
Ads  AmazonInsights  DeviceMessaging  GameCircle  InAppPurchasing  LoginWithAmazon  Maps  MobileAssociates  README.txt

Ads我之前用的是Google Admob,这里就不再用Amazon的了,我需要的是这里的InAppPurchasing和GameCircle。我们接下来一个一个来说。

* Amazon InAppPurchasing

Amazon支持三种内购类型:Consumables、Entitlements和Subscriptions:
    Consumables就像游戏中的红心、金币等,用户可以多次购买,每次可以买多个,并根据游戏规则,每次消耗若干个以达到某种游戏目的;在哪台设备上购买,就只能在哪台设备上使用。
    Entitlements是某种授权协议,一个用户只需购买一次,即可长期使用某种特权功能,并与设备无关,可在多个设备下授权使用。比如鳄鱼洗澡游戏中购买高级关卡等。
    Subscriptions有订阅的意思,需要某种Entitlements或某种访问权,在一定时间段内绑定有效,到期后自动renew,比如某种杂志的阅读权等。

我只想给游戏增加一些红心功能,一颗红心,可以让游戏者有一次续命的机会,因此我需要实现Consumables型内购。Amazon SDK中提供了Consumeables类内购的Android范例AmazonSDK/Android/InAppPurchasing/samples/SampleIAPConsumablesApp。我们可以参考这个例子来实现我的"红心内购"。

    1、添加依赖的jar包
    在你的游戏proj中添加内购功能所依赖的Amazon SDK jar包,包括AmazonInsights-android-sdk-2.1.26.jar、in-app-purchasing-1.0.3.jar 和login-with-amazon-sdk.jar。

    2、添加源文件
    参照例子,将AppPurchasingObserver.java、AppPurchasingObserverListener.java和MySKU.java拷贝到你的与XXActivity.java同级目录下。

    3、初始化Amazon IAP
   
    在你的XXActivity类中添加如下方法:

    public PurchaseDataStorage purchaseDataStorage;

    private void setupIAPOnCreate() {
        purchaseDataStorage = new PurchaseDataStorage(this);

        AppPurchasingObserver purchasingObserver
              = new AppPurchasingObserver(this, purchaseDataStorage);
        purchasingObserver.setListener(this);

        Log.i(TAG, "onCreate: registering AppPurchasingObserver");
        PurchasingManager.registerObserver(purchasingObserver);
    }

    protected void onCreate(Bundle savedInstanceState){
        … …
        setupIAPOnCreate();
    }

        protected void onResume() {
        super.onResume();
       
        Log.i(TAG, "onResume: call initiateGetUserIdRequest");
        PurchasingManager.initiateGetUserIdRequest();

        Log.i(TAG, "onResume: call initiateItemDataRequest for skus: "
                        + MySKU.getAll());
        PurchasingManager.initiateItemDataRequest(MySKU.getAll());
    }

    4、添加购买方法

    在Cocos2d-x的某个Scene或Layer中实现的购买方法事件的callback,后者通过Jni调用Java静态方法:

    void BuyHeartScene::buyHearts(int number) {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    JniMethodInfo t;
    if (JniHelper::getStaticMethodInfo(t, "net/iwobi/game/flickworldcup/FlickWorldCupActivity",
                "onBuyHeartClick", "(I)V")) {
        t.env->CallStaticVoidMethod(t.classID, t.methodID, number);
        if (t.env->ExceptionOccurred()) {
            t.env->ExceptionDescribe();
            t.env->ExceptionClear();
            return;
        }
        t.env->DeleteLocalRef(t.classID);
    }
#endif
    }

    该Java方法的实现如下(我这里有五种商品ONEHEART到FIVEHEART):

    public static void onBuyHeartClick(int type) {
        String requestId;
      
        switch (type) {
            case 1:
                requestId = PurchasingManager.initiatePurchaseRequest(MySKU.ONEHEART.getSku());
                break;
            case 2:
                requestId = PurchasingManager.initiatePurchaseRequest(MySKU.TWOHEART.getSku());
                break;
            case 3:
                requestId = PurchasingManager.initiatePurchaseRequest(MySKU.THREEHEART.getSku());
                break;
            case 4:
                requestId = PurchasingManager.initiatePurchaseRequest(MySKU.FOURHEART.getSku());
                break;
            case 5:
                requestId = PurchasingManager.initiatePurchaseRequest(MySKU.FIVEHEART.getSku());
                break;
            default:
                requestId = PurchasingManager.initiatePurchaseRequest(MySKU.ONEHEART.getSku());
                break;
        }

        PurchaseData purchaseData = ((FlickWorldCupActivity)context).purchaseDataStorage
                .newPurchaseData(requestId);
        Log.i(TAG, "onBuyHeartClick: requestId (" + requestId
                + ") requestState (" + purchaseData.getRequestState() + ")");
    }

    5、修改各种回调方法

    将SampleIAPConsumablesApp/src/com/amazon/sample/iap/consumable /MainActivity.java中的onPurchase为前缀名的方法以及onGetUserIdResponseSuccessful挪到你的 Activity源文件中。这些方法绝大部分是不需要修改的,除非你不喜欢例子中日志输出的格式,或是想用toast之类的提示方式改造各种 callback的结果显示方式。

    这里我主要修改了一个方法:onPurchaseResponseSuccess。该方法在购买成功后被调用,我们在这个事件发生时更新Scene或Layer的显示(updateHeartInScene)。

    @Override
    public void onPurchaseResponseSuccess(String userId, String sku,
            String purchaseToken) {
        Log.i(TAG, "onPurchaseResponseSuccess: for userId (" + userId
                + ") sku (" + sku + ")");
        SKUData skuData = purchaseDataStorage.getSKUData(sku);
        if (skuData == null)
            return;

        if (MySKU.ONEHEART.getSku().equals(skuData.getSKU())) {
            updateHeartInScene(1);
        }

        if (MySKU.TWOHEART.getSku().equals(skuData.getSKU())) {
            updateHeartInScene(2);
        }

        if (MySKU.THREEHEART.getSku().equals(skuData.getSKU())) {
            updateHeartInScene(3);
        }

        if (MySKU.FOURHEART.getSku().equals(skuData.getSKU())) {
            updateHeartInScene(4);
        }

        if (MySKU.FIVEHEART.getSku().equals(skuData.getSKU())) {
            updateHeartInScene(5);
        }
    }

    6、AndroidManifest.xml和其他Java文件

    AppPurchasingObserver.java和AppPurchasingObserverListener.java你可以原封不动的使用。MySKU.java可以根据你的内购项目做改造:

    public enum MySKU {

    ONEHEART("net.iwobi.game.flickworldcup.iap.consumable.oneheart", 1),
    TWOHEART("net.iwobi.game.flickworldcup.iap.consumable.twoheart", 1),
    THREEHEART("net.iwobi.game.flickworldcup.iap.consumable.threeheart", 1),
    FOURHEART("net.iwobi.game.flickworldcup.iap.consumable.fourheart", 1),
    FIVEHEART("net.iwobi.game.flickworldcup.iap.consumable.fiveheart", 1);
   
    private String sku;
    private int quantity;

    private MySKU(String sku, int quantity) {
        this.sku = sku;
        this.quantity = quantity;
    }

    public static MySKU valueForSKU(String sku) {
        if (ONEHEART.getSku().equals(sku)) {
            return ONEHEART;
        }
       
        if (TWOHEART.getSku().equals(sku)) {
            return TWOHEART;
        }

        if (THREEHEART.getSku().equals(sku)) {
            return THREEHEART;
        }

        if (FOURHEART.getSku().equals(sku)) {
            return FOURHEART;
        }

        if (FIVEHEART.getSku().equals(sku)) {
            return FIVEHEART;
        }
       
        return null;
    }

    public String getSku() {
        return sku;
    }

    public int getQuantity() {
        return quantity;
    }

    private static Set<String> SKUS = new HashSet<String>();
    static {
        SKUS.add(ONEHEART.getSku());
        SKUS.add(TWOHEART.getSku());
        SKUS.add(THREEHEART.getSku());
        SKUS.add(FOURHEART.getSku());
        SKUS.add(FIVEHEART.getSku());
    }

    public static Set<String> getAll() {
        return SKUS;
    }

 }

 AndroidManifest.xml中在application标签下添加如下配置:
        <receiver android:name="com.amazon.inapp.purchasing.ResponseReceiver" >
            <intent-filter>
                <action
                    android:name="com.amazon.inapp.purchasing.NOTIFY"
                    android:permission="com.amazon.inapp.purchasing.Permission.NOTIFY" />
            </intent-filter>
        </receiver>
 有了以上代码,我们的内购就可以运行起来了。
   

* 内购测试

使用Amazon In-app Purchasing API一个最大好处就是测试简单。Amazon提供一个本地测试程序Amazon App Tester(安装到Android模拟器中),可以模拟内购Server,SDK自动判断当前场景,如果是测试,你的集成了内购SDK的游戏将连接本地 测试程序完成内购流程。通过在本地测试程序中设置模拟不同的内购流程,我们可以轻松完成测试。

你需要给Amazon App Tester提供一个名为amazon.sdktester.json的文件,这样Amazon App Tester可以知道你的游戏有哪些内购项目,并模拟出这些内购项目。这个json文件可以自行编辑,也可以在Amazon deveoper网站上生成下载。

我直接将内购项目添加到我的Amazon帐号的游戏应用下面,一共五个,添加成功后,下载json文件。将该文件放在模拟器的/mnt/sdcard下,绝对路径为/mnt/sdcard/amazon.sdktester.json。

之后,启动App Tester,再启动你的游戏,点击内购项目,看看是否能购买成功。

* 内购上线

按照Amazon官方说法,SDK会自动区分测试场景和正式场景,因此通过App Tester测试的游戏在发布后,理论上内购是没有问题的。不过我上线后还是遇到了问题,即点击购买某个项目后,游戏没有任何反应,等了若干分钟都是这 样。我将这个问题反馈给Amazon Support,得到的答复居然是游戏代码没有问题,他们测试了若干中机型,都可以打开内购页面,并进行内购。只是有时内购页面打开有些延迟,但都能打 开。看到这里,我猜是否又是大陆网络的问题呢!不管它了,至少通过Amazon Support的回复可以证明我的代码是ok的。只能希望美国人民多多购买我的内购项目了^_^。

* Amazon游戏圈

想给游戏增加成就榜和成就提交功能,如果自己实现服务端,显然麻烦,工作量大不说,还得维护一个Server。但市面上提供这类服务的游戏平台不多。 Google Play的游戏Service提供这种服务,不过还是上面提到的原因,我与Google的这个服务无缘啊。Amazon Game SDK后期推出了GameCircle服务。

GameCircle目前提供achievements, leaderboards和Whispersync三种特性:
    achievements就是奖励机制,帮助游戏提高玩家粘性。
    leaderboards类似于积分榜,可以用于提交玩家积分以及显示玩家的全球排名。
    Whispersync是一种数据游戏同步服务,同步玩家进度,保寸玩家个性化数据等。

这里我要用到的是leaderboards。

    1、建立GameCircle
    使用游戏圈前,你需要在Amazon官方的Amazon Apps & Services Developer Console下创建属于你的Game Circle,然后创建一个LeaderBoard,设置LeaderBoard属性。SDK中提供了GameCircle的Demo:AmazonSDK/Android/GameCircle

    2、导入jar包,设置AndroidManifest.xml
    要想使用GameCircle,我们需要导入相应的SDK jar包:gamecirclesdk.jar。

    在AndroidManifest.xml中,需要在application标签下添加以下配置:

                <activity
            android:name="com.amazon.ags.html5.overlay.GameCircleUserInterface"
            android:hardwareAccelerated="false"
            android:theme="@style/GCOverlay" >
        </activity>
        <activity
            android:name="com.amazon.identity.auth.device.authorization.AuthorizationActivity"
            android:allowTaskReparenting="true"
            android:launchMode="singleTask"
            android:theme="@android:style/Theme.NoDisplay" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:host="net.iwobi.game.flickworldcup"
                    android:scheme="amzn" />
            </intent-filter>
        </activity>
        <activity
            android:name="com.amazon.ags.html5.overlay.GameCircleAlertUserInterface"
            android:hardwareAccelerated="false"
            android:theme="@style/GCAlert" >
        </activity>

        <receiver
            android:name="com.amazon.identity.auth.device.authorization.PackageIntentReceiver"
            android:enabled="true" >
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_INSTALL" />
                <action android:name="android.intent.action.PACKAGE_ADDED" />

                <data android:scheme="package" />
            </intent-filter>
        </receiver>

   
        这些配置中需要的res,可以从AmazonSDK/Android/GameCircle/GameCircleSDK/res/中找到并copy到你的project中。

    3、初始化GameCircle

    GameCircleSDK这个Demo中没有提供太多源码,src目录下是空的。因此我们只能参考Amazon Developer站点上页面上的说明一步步的添加和调整我们的代码了。

    在你的XXActivity类中,我们添加如下方法:

    //reference to the agsClient
    public AmazonGamesClient agsClient;
    
    AmazonGamesCallback callback = new AmazonGamesCallback() {
            @Override
            public void onServiceNotReady(AmazonGamesStatus status) {
                Message msg = new Message();
                switch (status) {
                // The SDK failed to initialize correctly.
                case CANNOT_INITIALIZE:
                    Log.i(TAG, "onServiceNotReady: CANNOT_INITIALIZE");
                    msg.obj = "Can not initialize Amazon Game Services";
                    break;

                // The SDK is in the process of initializing.
                case INITIALIZING:
                    Log.i(TAG, "onServiceNotReady: INITIALIZING");
                    msg.obj = "Initializing Amazon Game Services";
                    break;

                // The device not registered with an account
                case NOT_AUTHENTICATED:
                    Log.i(TAG, "onServiceNotReady: NOT_AUTHENTICATED");
                    msg.obj = "The Device does not registered with an account";
                    break;

                // The game is not authorized to use this service.
                case NOT_AUTHORIZED:
                    Log.i(TAG, "onServiceNotReady: NOT_AUTHORIZED");
                    msg.obj = "Not authorized to use Amazon Game Services";                   
                    break;
                }
               
                //unable to use service
                msg.what = 21;               
                notifyHandler.sendMessage(msg);
            }
            @Override
            public void onServiceReady(AmazonGamesClient amazonGamesClient) {
                agsClient = amazonGamesClient;
             
                //ready to use GameCircle
                if (agsClient != null)
                    Log.i(TAG, "on AmazonGamesCallback: call onServiceReady, agsClient init ok");
                else
                    Log.i(TAG, "on AmazonGamesCallback: call onServiceReady, agsClient init failed");
            }
    };
    
    //list of features your game uses (in this example, achievements and leaderboards)
    EnumSet<AmazonGamesFeature> myGameFeatures = EnumSet.of(
            AmazonGamesFeature.Leaderboards);

    protected void onResume() {
        super.onResume();
       
        … …
        AmazonGamesClient.initialize(this, callback, myGameFeatures);
    }

        public void onPause() {
        super.onPause();
        if (agsClient != null) {
            agsClient.release();
        }
    }

   
    4、提交成就积分

    当玩家结束游戏时,可以选择将此次的高分上传到leaderboards上。游戏中应对积分提交的代码也在XXActivity中。

    public static void onSubmitScoreToLeaderBoard(int score) {
        if (((FlickWorldCupActivity)context).agsClient == null) {
            Message msg = new Message();
            msg.what = 21;
            msg.obj = "Unable to use Amazon Game Services";
            notifyHandler.sendMessage(msg);
            return;
        }
       
        LeaderboardsClient lbClient = ((FlickWorldCupActivity)context).agsClient.getLeaderboardsClient();
        AGResponseHandle<SubmitScoreResponse> handle = lbClient.submitScore("FlickWorldCupTopScore", score);
         
        // Optional callback to receive notification of success/failure.
        handle.setCallback(new AGResponseCallback<SubmitScoreResponse>() {
         
            @Override
            public void onComplete(SubmitScoreResponse result) {
                if (result.isError()) {
                    // Add optional error handling here.  Not strictly required
                    // since retries and on-device request caching are automatic.
                    Message msg = new Message();
                    msg.what = 22;
                    msg.obj = "Submit Score to LeaderBoard Failed!";
                    notifyHandler.sendMessage(msg);
                } else {
                    // Continue game flow.
                    Message msg = new Message();
                    msg.what = 23;
                    msg.obj = "Submit Score to LeaderBoard OK!";
                    notifyHandler.sendMessage(msg);
                }
            }
        });       
    }

    如果仅是查看积分排行,可以用下面这个方法:

    public static void onShowLeaderBoardOverlay() {
        if (((FlickWorldCupActivity)context).agsClient == null) {
            Message msg = new Message();
            msg.what = 21;
            msg.obj = "Unable to use Amazon Game Services";
            notifyHandler.sendMessage(msg);
            return;
        }
       
        LeaderboardsClient lbClient = ((FlickWorldCupActivity)context).agsClient.getLeaderboardsClient();
        AGResponseHandle<RequestResponse> handle = lbClient.showLeaderboardOverlay("FlickWorldCupTopScore");
       
        handle.setCallback(new AGResponseCallback<RequestResponse>() {
            
            @Override
            public void onComplete(RequestResponse result) {
                if (result.isError()) {
                    // Add optional error handling here.  Not strictly required
                    // since retries and on-device request caching are automatic.
                    Log.i(TAG, "onShowLeaderBoardOverlay – onComplete: Show LeaderBoard Request Failed!");
                }
            }
        });     
    }

   
* 游戏圈上线

游戏圈无法在本地进行测试,只能在真实的游戏圈中测试代码是否ok。不过Amazon的游戏圈提供了管理功能,在测试后发布前可将游戏圈 leaderboard的值reset。游戏圈leaderboard发布后,你就可以使用leaderboard了。游戏圈功能在国内访问是没有任何问 题的,查看积分榜,提交分数到积分榜都很顺畅。

* 小结

Amazon游戏SDK在国内的应用估计比较小众,大家可能更多的选择用Google Play提供的服务或是AppStore的,但Amazon毕竟为游戏开发者提供了一个选择(而且是完全免费的哦),另外Amazon的Support对 提交问题的反馈较为及时(无论是mail还是forum上的提问),基本24小时内就会有答复。各种设施的发布也比较快,有时候3-4个小时即可生效。

目前Amazon Game SDK的资料多为英文,且集中在Amazon官方站点以及官方维护的support论坛中。遇到问题,亚马逊的论坛是第一选择。




这里是Tony Bai的个人Blog,欢迎访问、订阅和留言!订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:


以太币:


如果您喜欢通过微信App浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:



本站Powered by Digital Ocean VPS。

选择Digital Ocean VPS主机,即可获得10美元现金充值,可免费使用两个月哟!

著名主机提供商Linode 10$优惠码:linode10,在这里注册即可免费获得。

阿里云推荐码:1WFZ0V立享9折!

View Tony Bai's profile on LinkedIn


文章

评论

  • 正在加载...

分类

标签

归档











更多