前言:在大数据时代里,云平台的角色愈来愈吃重了。其中多层框架云平台成为主流(例如,上层Java+下层C/C++)。然而,云平台的主人只有一个,我们可能只是客人角色,有没有可能反客为主,挟天子以令诸侯呢?这就看架构师的思考技术了。
1. 认识云(端)平台
1.1 以百货公司比喻云平台
传统的网络服务是封闭型的Client/Server架构的延伸。其中的Client与Server两端的软件程序是各自开发的,Server端开发人员撰写完整的Server端程序,而Client端开发人员则撰写完整的Client端程序并且呼叫Server端的程序。这种传统的网络服务系统,其Server端如同一座四合院,庭院深锁,外人(即Client程序)只能在大门口与四合院内的主人沟通。如今的云(Cloud)概念里,其系统架构和软件开发,大多来自传统技术的延伸,并非特别的创新。然而它有了开放心怀,不再紧闭大门、深锁庭院了,而是打开大门,提供庭院让外人进来搭帐篷露营。此外,四合院主人还愿意提供各项资源(软硬件)和服务,甚至免付费呢!
上述的四合院比喻,还不能完整看出云所带来的商业模式和价值。开放型的四合院,也相当于目前大家常常去逛的百货公司,如下图:
图1 云平台就像百货公司
在四合院里搭帐篷,也相当于在百货公司里开设专柜做生意。例如,你到许多百货公司处处可见到香奈儿、SK-II等化妆品专柜,如下图:
图2 百货公司里的连锁专柜(企业)
基于云的开放架构,Android开发者能到各式各样的云(如Google、Facebook等)平台上,开发云层上的应用程序(就如同进入别人的四合院里搭帐篷露营、也如同到各个百货公司开设连锁专柜)。于是,Android应用服务就如同SK-II的国际连锁服务。Android手机端成为连锁店的营运总部,Android手机也成为连锁店总经理的随身指挥利器,和运筹帷幄中心了。
1.2 以连锁专柜比喻特定领域的(跨)云平台
一般而言,有两种常见的云分类:
公有云(Public Cloud)-- 开放给各行业或各领域的人们进来写软件或使用服务。
私有云(Private Cloud)-- 并不开放给别人进来写软件或使用服务。
如果你学过Java或C++的话,就知道类别里的函数可分为:public、private和protected共三种。如果对应到云的分类别上,将可以得到第三种云,就是:
限定云(Protected Cloud),即<特定领域的云(Domain-Specific
Cloud)>-- 特定领域就是特定行业,例如医疗行业、物联网行业、保险行业、网络游戏行业、KTV行业等,各有其专属(Protected)范围。只开放熟悉该行业的人进来写软件或使用服务。例如,医疗领域云平台的系统架构图如下:
图3 医疗领域的(跨)云平台
从图可以看出来,这个医疗领域云平台,是建立于多的公有(Public)云平台上,甚至,可能也有自己的私有(Private)云。所以它是一种跨云(公有及私有)的云。由于它是建立于公有云的虚拟平台上,不需要巨大投资于硬设备上,所以建置成本远低于公有云。欲理解这种新潮跨云平台的最好比喻就是:
公有云,如同「百货公司」。
特定领域云,如同百货公司里的 Chanel连锁专柜。
大家都知道:跨百货公司的连锁专柜也是公司,所以跨公有云的特定领域跨云平台,也是云。
2. 认识GAE(Google AppEngine)云平台
GAE(Google AppEngine)是Google的云服务引擎,第三方应用程序开发者能开发应用程序(AP),然后放在Google服务器上执行,不需担心频宽、系统负载、安全维护等问题,一切由Google代管。只要AP每月不超过500万网页面的流量就可享受免费的待遇。GAE平台的系统架构如下图:
图4 GAE云平台架构(摘自Google公司文件)
从上图可看到,从手机、PC、MID等众多端设备上,都能随时上网发出要求(Incoming
Requests)来存取GAE上的服务。在GAE后台的AppServer里,GAE提供了API(即API
Layer)来衔接你的云端应用程序(即AP)。
3. 小强龙的云框架API-- 以GAE的SM游戏为例
水果盘拉霸机(Slot Machine,简称SM)游戏。如下图:
图5 Android拉霸机游戏画面
其玩法是先输入投注金额(Bet),然后拉动点击把手或点击<SPIN>钮来转动滚动条,滚动条会各自转动,然后随机出现不同图案,如果停定时,有出现符合相同或特定相同图案连成线者,即依其赔率而胜出。同一家游戏场里的拉霸机通常会联网,以投注额厘定累积大奖(Jackpot)金额,并随时更新累绩大奖金额,以便增加吸引性。
3.1 SM游戏的框架设计图
这游戏软件可分为两部分:
游戏(Game)端部分,也就是Android手机端的应用程序。
柜台(Console)端部分,也就是GAE云层Servlet程序。
当玩家押注后,按下<SPIN> 按钮(开始加速滚动),游戏端就将「目前余额」和「押注金额」传送给GAE的柜台端程序。等待柜台端程序计算出中奖金额后,将「新余额」和「奖项级别」回传给Android游戏端(滚动开始减速),并更新游戏端的画面。其中,Android游戏端程序(ac01.java)发送HTTP来呼叫GAE云层的Servlet接口,如下图所示:
图6 拉霸机游戏的系统架构图
Android游戏端透过HTTP和Servlet接口来传送三种讯息给GAE
云层。这三种讯息为:
当玩家启动Android游戏端时,发送"Init:"讯息给GAE云层程序--
GAE就从DB里读取玩家的余额(即上回的余额),并回传给游戏端。
当玩家按下<SPIN>按钮时,发送"Bett:amount,bet"
讯息给GAE云程序-- 此讯息附有余额(amount)和押注金额(bet),要求GAE程序决定「奖项级别(Rank)」,计算奖金和新余额,然后回传给游戏端。
当玩家欲结束时,按下<Exit>按钮发送"Fini:amount"讯息给GAE云层--
此讯息附有目前余额(amount),GAE接到讯息,就依据将余额存入DB,完成时立即回复给游戏端,关闭游戏端画面。
GAE Console端应用程序包含两个部份:Servlet模块和GM模块。GM(全名是Game
Machine)类别是Console端应用程序的决策核心,例如决定游戏获奖的奖项,计算奖金等都是GM负责的任务。至于Servlet类别体系则是负责与Android游戏端的沟通任务。
◆ Slot Machine的框架设计图(方案一)
兹先设计(和开发)Slot Machine应用框架,如下图所示:
图7 Console端游戏框架设计图(依据方案一)
当Android游戏端(简称SM)呼叫HttpServlet类别的Servlet接口时,会转而呼叫smConsoleStub类别的doGet()函数,此doGet()转而呼叫process()函数去解析来自Android游戏端的讯息,然后呼叫GM类别的函数,或呼叫应用程序的onInitialAmount()和onFinished()函数。
值得留意的是,此框架设计者(即smConsoleStub类别设计者)决定了它与游戏端沟通的讯息格式(Format),例如游戏端必须使用"Init:"讯息格式、"Bett:"讯息格式和"Fini:"讯息格式。一旦框架设计者决定了沟通接口,则应用程序开发者就必须遵循这些接口,而不能更改之。此时,框架就藉由这些接口来「框住」应用程序(及开发者)了。此外,框架也决定了它与子类别间的接口,也就是决定了onInitialAmount()和onFinished()函数的名称及参数格式。例如下图:
图8 Console端的应用子类别(smConsoleServlet)
其中,smConsoleServlet子类别就必须遵循smConsoleStub类别的接口规定而实作(Implement)
onInitialAmount()和onFinished()两个抽象函数。
◆Slot Machine的框架设计图(方案二)
在上述方案一里,框架设计师决定了Android游戏端与云层之间沟通讯息的格式,而应用程序开发者只能遵循之而不能制定自己喜欢的讯息格式。如果想让应用程序开发者能自行决定上述的讯息格式,就可更改框架设计如下图:
图9 Console端游戏框架设计图(依据方案二)
在此新方案里,smConsoleStub22类别的process()是抽象函数,让应用程序的smConsoleServlet22子类别来实作之。框架里的smConsoleStub22类别只是将讯息转达给应用子类别smConsoleServlet22而已,并不决定讯息格式,也不解析讯息。而是由smConsoleServlet22子类别的process()函数来解析讯息。由于Android游戏端的ac01类别和云层的smConsoleServlet22子类别都属于应用程序,由ac01类别与smConsoleServlet22子类别之间的沟通讯息格式,是应用开发者可以自订了。
3.2 SM游戏的云端框架范例代码
这云层框架是依据上述的方案一而设计的,其包含两个类别:GM类别和smConsoleStub类别。兹在GAE开发项目里定义上述类别,如下图:
兹撰写GM.java类别和smConsoleStub.java类别,其代码如下:
其中,RC类别是依据随机值而换算出获奖的奖项(Rank),其实各家游戏场都有不一样的奖项决定规则,而且随时都可能更换新的奖项规则。上述RC类别只是一个简单范例而已。Android游戏机端传送HTTP讯息给GAE云层,就转而呼叫上述的doGet()函数。此时诞生一个GM对象,并从session取得"gmState"的值,并将此值存入GM对象里,设定了GM对象的状态值。接着,转而呼叫process()函数来解析讯息内容,在依据内容而呼叫GM对象的函数或应用子类别的函数,最后回传讯息给游戏机端。
3.3 SM游戏的应用程序(App)范例代码
这拉霸机应用程序包括:GAE云层的smConsoleServlet子类别和Android游戏端的ac01子类别。
GAE云层的应用程序代码
其包括smConsoleServlet子类别。兹在GAE开发项目里定义之,如下:
兹撰写smConsoleServlet类别,其程序代码如下:
/*---- smConsoleServlet.java ----*/
package misooGAE001;
import com.google.appengine.api.users.User;
import GameFramework.smConsoleStub;
@SuppressWarnings("serial")
public classsmConsoleServletextends smConsoleStub{
@Override protected int onInitialAmount(User
user) {
// 读取user's initial(recorded) amount from DB
int init_amt = 1000;
return init_amt;
}
@Override protected boolean onFinished(User
user, int final_amount) {
// 将user's final amount 存入 DB
return true;
}} |
云层框架smConsoleStub类别并不负责向DB存取游戏玩家的目前余额。所以在游戏启动时,smConsoleStub呼叫应用程序子类别smConsoleServlet的onInitialAmount()函数去DB读取玩家的余额。而且在游戏结束时,框架类别smConsoleStub则呼叫应用程序子类别smConsoleServlet的onFinished()函数去将玩家余额写入DB里,储存起来。
Android游戏端应用程序代码
应用程序开发者除了上述smConsoleServlet子类别之外,还要开发Android游戏端的应用程序。兹在Android开发项目里定义ac01子类别,如下图:
兹撰写ac01.java子类别,其程序代码如下:
此ac01类别的invokeServlet()函数会启动一个子执行绪(Thread)去发送HTTP讯息给GAE云层的Servlet程序。例如,游戏启动时,立即发送"Init:"讯息给云层,此时画面上出现一个「等待」窗口,如下图:
一直等到接获云层Servlet回复时,就出现测试用的简易游戏画面,如下:
其中的Amt:1000表示目前此玩家的余额是1000元。此时,玩家可以连续按下<Add>按钮来投注更多金额。例如,投注100元的画面如下:
此时,玩家可按下<SPIN>按钮,ac01类别的invokeServlet()函数会启动一个子执行绪(Thread)去发送"Bett:"讯息给云层,此时画面上出现一个「等待」窗口。一直等到接获云层Servlet回复时,就出现更新游戏画面上的金额,如下:
其中的Rank:3 表示获得第3级别的奖项,押注100元,赢得300元奖金,目前余额为1300元。当玩家按下<Exit>按钮,ac01类别的invokeServlet()函数会启动一个子执行绪(Thread)去发送"Fini:"讯息给云层,此时画面上出现一个「等待」窗口。一直等到接获云层Servlet回复时,就表示云层已经将余额存入DB,于是ac01就结束了。
3.4 搭配上漂亮的UI画面
上述的拉霸机范例程序,只要将Android游戏端应用程序更换掉,就可以换上漂亮的操作画面了,如下图所示:
图10 美观的Android拉霸机游戏画面
然而,由于篇幅的限制,本范例只采用简易操作画面,以便列出完整的程序代码。
4. Android框架 + GAE框架
Android的跨进程界面IBinder,其角色相当于云层里的Servlet界面:
图11 Android端框架与GAE云框架的完美整合
无论Android还是GAE云层都是以框架来支撑「强龙/地头蛇」商业样式。
兹写个GAE框架范例,并进行架构设计如下:
图12 Android框架+ GAE框架的范例
其中的UploadPost和myUploadPost两个类别的程序代码如下:
//UploadPost.java
package com.patrick.ccpmediastore;
import java.io.IOException;
import java.net.URLEncoder;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@SuppressWarnings("serial")
public abstract class UploadPost extends HttpServlet {
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
try { PMF.get().getPersistenceManager().makePersistent(getMediaObject(req));
resp.sendRedirect("/");
} catch (Exception e) {
resp.sendRedirect("/?error=" +
URLEncoder.encode("Object save failed: " + e.getMessage(), "UTF-8"));
}}
protected abstract MediaObject getMediaObject(HttpServletRequest req) ;
}
|
// myUploadPost.java
package com.patrick.ccpmediastore;
import java.io.IOException;
import java.util.Date;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.appengine.api.blobstore.*;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
@SuppressWarnings("serial")
public class myUploadPost extends UploadPost
{
private BlobstoreService blobstoreService =
BlobstoreServiceFactory.getBlobstoreService();
protected MediaObject getMediaObject(HttpServletRequest
req) {
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
Map<String, BlobKey> blobs = blobstoreService.getUploadedBlobs(req);
Iterator<String> names = blobs.keySet().iterator();
String blobName = names.next();
BlobKey blobKey = blobs.get(blobName);
BlobInfoFactory blobInfoFactory = new BlobInfoFactory();
BlobInfo blobInfo = blobInfoFactory.loadBlobInfo(blobKey);
String contentType = blobInfo.getContentType();
long size = blobInfo.getSize();
Date creation = blobInfo.getCreation();
String fileName = blobInfo.getFilename();
String title = req.getParameter("title");
String description = req.getParameter("description")
+ "from myNewUploadPost";
boolean isShared = "public".equalsIgnoreCase(req.getParameter("share"));
MediaObject mediaObj = new MediaObject(user,
blobKey, creation,
contentType, fileName, size, title, description,
isShared);
return mediaObj ;
}} |
一样的框架设计思维、方法和技术,应用于Android行动端上,同时应用于GAE云层上。
|