前言:
目前国内真正掌握手机网游核心技术的公司并不多,能独立架构手机网游客户端和服务器端的人更是少数。这些资料也在很多公司被视为机密,网上开源的资料,从网络游戏“水果机”,到基于socket通信的“Slug”,基本上讲解的都是一些技术上的细节,看完后也是模糊不清。
我由于工作上的原因,从参加工作伊始,一直从事手机网络游戏的开发。虽然谈不上多么精通,但自问也知其皮毛。所以在这里就不吝这点知识,贡献给大家。也为了带动J2ME版块的人气。
我会从“水果机”“Slug”讲起,然后讲我自己的架构方法,做个比较。从而让大家更明白技术细节和整体架构。
由于时间的原因,再加上工作比较忙,我可能会不定期的写一点,希望得到大家的支持。
(一)基于HTTP的手机网络游戏
因为在所有的MIDP规范中规定:都必须支持HTTP协议,而据业内人士透露消息,中国电信在将来也只会支持HTTP,所以现在很多的手机网游都是架构在HTTP上的。但由于HTTP协议封装上的完整性,给它带来了好处,也带来了坏处。
首先我们看HTTP协议的优点:
1:servelt容器会自动管理线程池,在我们的程序里可以不必自己去管理线程了,当然,我说的线程是客户端发送请求的连接到服务器端产生的一个线程。
2:HTTP是安全的,利用session来管理每个会话,省去了让人头疼的客户端冒充问题。
3:几乎所有支持java的手机都支持HTTP协议。
当然,还有其它优点,我不可能一一道来,自己去体会吧......
其次就是HTTP协议的缺点:
1:就是大家都比较头疼的HTTP协议的无连接性,曾经有人提过去修改HTTP协议,不知道成功了没?当然,这个不在我们讨论的范围之内。
2:就是网络流量的问题,这个也是大家都比较头疼的问题。如果不是包月,对用户来说,这个费用确实是一大笔开支。
下面我先讲解一下比较出名的手机网络游戏“fruite-machine”的客户端和服务器端的架构:
Phone ---------------→Servlet--------------------→Web Browser
上面的是“水果机”的整体的架构图。
“水果机”曾一度流行于各个电玩厅内,做为一种赌博机的形式出现。这个游戏虽然设计的简单,但却很耐玩,勘称能和“俄罗斯方块”想媲美的一个经典游戏。
在架构后面的web Browser一层,是用于管理用户的web界面,可以操作数据库,从而达到管理用户的目的。
因为用户在登陆时会在手机上面输入“username”和password“,所以,安全性是个很大的问题。
在fruite-machine里的设计文档里,是这么解决这个问题的:
1:用端到端的加密连接HTTS来代替HTTP
2:基于一个安全的无线网络上面用HTTP,经由一个安全的无线网关把username和password传送到servlet端。
3:和servlet在同一个防火墙内传送username和password。
在解决问户欺骗的问题上,因为一个用户可能把MIDlet客户端下载后修改源代码,从而可能传送假报文给servlet端,“水果机”里面把一些用户可能修改的数据在servlet端生成,然后传送给MIDlet,这样用户就无法修改了。比如MIDlet并不能生成随即旋转的结果,而是由服务器端生成的。
(二)
有关通信的协议部分,其实就是客户端和服务器端约定一种规则来进行通信。因为客户端的请求和服务器端的回复内容都在HTTP的body里面,而这个body只不过是一个字节流,因此客户端和服务器端必须在理解这些字节流上保持一致。
Fruite-machine里面是用↓来代表一行新的字符信息,如果新的字符信息里面还需要隔离的话,就利用\来进行隔离。
所以整个发送的报文看起来就是这样的:login↓drap↓ secret
做为例子,我们来看看玩家在选中一个pet后和服务器端的报文交互过程:
MIDelt---------------------servlet
首先,MIDlet会发送旋转请求到servlet服务器端。这个请求的报文body中包含选择宠物的位置,以及宠物下面的标志(true或者false来表示)。
然后,服务器端在接受到这个报文后,会处理。并根据处理的结果返回相应的报文。如果是赢了的话,服务器端会返回玩家赢的位置,以及盈后的积分,还有旋转后停的位置。如果失败的话,服务器端也会返回一个失败的报文给玩家。
客户端的程序我就不说了,我来重点讲讲服务器端的程序。
下面先看看整体的结构:
当fruitemachineservlet接收到一个Request的请求的时候,首先分析这个请求是来自哪里:是手机终端的请求还是web管理页面的 请求,并把请求交给相应的程序处理。Web页面的请求主要是一些更新数据库的操作。手机终端请求会先分析请求的类型:是登陆,还是游戏,还是其它的……并 把它们交给相应的程序处理。如果是登陆的话,游戏处理程序会从数据库内取出用户的username和password,验证用户。并产生一个新的 HTTPsession会话来管理这个连接。如果用户是退出的话,游戏逻辑就会销毁Httpsession。
首先我们来看看servlet程序:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class FruitMachineServelt extends HttpServlet{
private UserDatabase userDatabase;
private AdminProtocolHandler adminProtocolHandler;
private GameProtocolHandler gameProtocolHandler;
public void init(ServletConfig config) throws ServletException{
super.init(config);
userDatabase = new UserDatabase();
userDatabase.createUser(“guest”,””);
adminProtocolHandler = new AdminProtocolHandler(userDatabase);
gameProtocolHandler = new GameProtocolHandler(userDatabase);
}
public void doGet(HttpServletRequest request,HttpServletResponse response)
throws IOException,ServletException{
try{
String pathInfo = request.getPathInfo();
If(pathInfo == null){
Reponse.sendError(HttpServletResponse.SC_BAD_REQUEST,”Missing path info”);
}else if(pathInfo.startsWith(“/admin”)){
adminProtocolHandler.doGet(request,response);
}else{
response.sendError(HttpServletResponse.SC_BAD_REQUEST,”Unexpected path info”);
}
}catch(IOException e){
e.printStackTrace();
throw e;
}catch(Exception e){
e.printStackTrace();
throw new ServletException(e.getMessage());
}
}
(三)
今天有时间了,就再写一点。
Fruite-machine严格上来说,只是一个排列高低分的游戏,并不是真正意义上的网络游戏,但是它也实现了网络游戏的一些简单的功能。
Fruite -machine虽然用了HTTP协议,却并不轮循服务器,而是通过callback的方式,向服务器端发送一个报文后,再接受一个报文处理。这样在真正 的网络游戏中就容易产生问题,因为在真正的网络游戏中,服务器可能会主动的给一个玩家发送报文。而在fruite-machine中,服务器是无法给玩家 主动的发送报文的,它只是在用户发送报文给服务器端时,再回调函数处理response,这样一应一答的报文传输方式。
严格意义上来说,在真正的网络游戏中采用HTTP协议只能采用轮循服务器的方式来解决服务器主动发送报文给某个客户端的问题。就是所谓的心跳报文。
下面我们来看看实现callback功能的程序:
import java.io.*;
import java.util.*;
import javax.microedition.io.*;
/**
* This class accepts and queues POST requests for a particular URL, and
* services them in first-in-first-out order. Using the queue allows it
* to be thread-safe without forcing its clients ever to block.
*/
public class HttpPoster
implements Runnable
{
private String url;
private volatile boolean aborting = false;
private Vector requestQueue = new Vector();
private Vector listenerQueue = new Vector();
public HttpPoster(String url)
{
this.url = url;
Thread thread = new Thread(this);
thread.start();
}
public synchronized void sendRequest(String request,
HttpPosterListener listener)
throws IOException
{
requestQueue.addElement(request);
listenerQueue.addElement(listener);
notify(); // wake up sending thread
}
public void run()
{
running:
while (!aborting)
{
String request;
HttpPosterListener listener;
synchronized (this)
{
while (requestQueue.size() == 0)
{
try
{
wait(); // releases lock
}
catch (InterruptedException e)
{
}
if (aborting)
break running;
}
request = (String)(requestQueue.elementAt(0));
listener = (HttpPosterListener)(listenerQueue.elementAt(0));
requestQueue.removeElementAt(0);
listenerQueue.removeElementAt(0);
}
// sendRequest must have notified us
doSend(request, listener);
}
}
private void doSend(String request,
HttpPosterListener listener)
{
HttpConnection conn = null;
InputStream in = null;
OutputStream ut = null;
String responseStr = null;
String errorStr = null;
boolean wasError = false;
try
{
conn = (HttpConnection)Connector.open(url);
// Set the request method and headers
conn.setRequestMethod(HttpConnection.POST);
conn.setRequestProperty("Content-Length",Integer.toString(request.length()));
// Getting the output stream may flush the headers
ut = conn.openOutputStream();
int requestLength = request.length();
for (int i = 0; i < requestLength; ++i)
{
out.write(request.charAt(i));
}
// Opening the InputStream will open the connection
// and read the HTTP headers. They are stored until
// requested.
in = conn.openInputStream();
// Get the length and process the data
StringBuffer responseBuf;
long length = conn.getLength();
if (length > 0)
{
responseBuf = new StringBuffer((int)length);
}
else
{
responseBuf = new StringBuffer(); // default length
}
int ch;
while ((ch = in.read()) != -1)
{
responseBuf.append((char)ch);
}
responseStr = responseBuf.toString();
// support URL rewriting for session handling
String rewrittenUrl = conn.getHeaderField("X-RewrittenURL");
if (rewrittenUrl != null)
{
url = rewrittenUrl; // use this new one in future
}
}
catch (IOException e)
{
wasError = true;
errorStr = e.getMessage();
}
catch (SecurityException e)
{
wasError = true;
errorStr = e.getMessage();
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (IOException e)
{
}
}
if (out != null)
{
try
{
out.close();
}
catch (IOException e)
{
}
}
if (conn != null)
{
try
{
conn.close();
}
catch (IOException e)
listener.receiveHttpResponse(responseStr);
{
}
}
}
if (wasError)
{
listener.handleHttpError(errorStr);
}
else
{
listener.receiveHttpResponse(responseStr);
}
}
// This is just for tidying up - the instance is useless after it has
// been called
public void abort()
{
aborting = true;
synchronized (this)
{
notify(); // wake up our posting thread and kill it
}
}
}
从HttpPoster 类中我们可以看出来:HttpPoster类采用单独的一个线程,只要调用HttpPoster类的sendRequest(String request,HttpPosterListener listener)方法,线程就会运行,然后调用doSend(String request, HttpPosterListener listener)方法来把request传送到服务器端。
注意 listener.receiveHttpResponse(responseStr);这句程序回调HttpPosterListener的 receiveHttpResponse(String response)方法,这里也运用了多态性,listener会根据相应实现的类调用相应类的receiveHttpResponse(String reponse)方法来处理服务器返回的报文。
今天先写到这,总之,HttpPoster这个类写的很不错,值得大家细细品位。包括体会多线程的wait(),notify()机制。
这个话题给大家深入的讲解一下this关键字及其主要的功能。
大凡初次接触程序设计的人,都会对this关键字的用法和含义不太清楚。
那么我在这篇文章就给大家深刻的分析一下它的含义和作用。
从字面意义上看:this是指自己,那么它在程序里面也指自身的一个实例了。其实从本质上来说它是java类里面自身的一个隐含指针,但是在实例产生之前,它不指向具体的内存,这点也可以在类里面用
System.out.println(this);
输出查看,会发现输出的为null。
它随着具体实例的产生而指向具体的实例,并在内存中为其分配相应的存储单元。
恩!
现在不知道你到底明白了this的用法没,说到底它是一个指针,指向自己。随着对象的产生而指向具体的对象实例,随着对象的消失而消失。
既然是对象,可想而知,this是用在面向对象的程序设计中的。
下面看看this的用法:
1:区分全局变量和局部变量
我们知道,如果一个函数里面有个局部变量和全局变量的名称和类型一样,那么全局变量对于这个方法来说就为不可见的。
如下:
public class Test{
int a,b;
public void change(int a,int b){
a = a;
b = b;
}
}
我们原本的意思是想通过change()方法改变全局变量a和b的值,但是因为这个时候名称冲突,全局变量对chang()方法来说为不可见,也就是覆盖了。
但是如果改成这样呢?
public void change(int a,int b){
this.a = a;
this.b = b;
}
从而编译器就知道了我们要通过change()方法改变a和b的值,你不要以为编译器很聪明。其实它很笨,在你的程序摸棱两可的时候,它就傻了,不知道该怎么处理了!
而this关键字明白的指出了我们要改变的是全局变量a和b的值。
2:在一些启动类里充当实例的角色
我们知道,MIDlet是J2ME的启动类。
编译器会自动启动它,因此我们无法来new一个MIDlet出来。编译器是不允许这样做的!
因为你这样一做,它就不知道到底该从那个启动了!我说过,编译器很笨的,你不要认为它很聪明。在很多情况下,它是糊涂的!
这个时候,我们唯一的办法就是在需要MIDlet对象实例的地方传入this关键字,在编译器启动并产生MIDlet实例的时候,相应的this指向这个产生的实例,从而可以操纵我们的MIDlet对象以及调用它的方法。
例如:
public class SpaceShooter extends MIDlet {
private GameCanvas gameCanvas;
public SpaceShooter() {
gameCanvas = new GameCanvas(this); //传入当前类的隐含指针,在MIDlet产生时传入,从而在GameCanvas里可以调用MIDlet的方法
Display.getDisplay(this).setCurrent(gameCanvas);
}
/** MIDlet开始时所调用的方法 */
protected void startApp() throws MIDletStateChangeException {
gameCanvas.doStartApp();
}
/** MIDlet暂停时所调用的方法 */
protected void pauseApp() {
gameCanvas.doPauseApp();
}
/** MIDlet结束时所调用的方法 */
protected void destroyApp(boolean unconditional)
throws MIDletStateChangeException {}
/** 结束MIDlet时所调用的方法 */
void doExit() {
try {
destroyApp(false);
notifyDestroyed();
}catch(MIDletStateChangeException e) {}
}
}