前言:
目前國內真正掌握手機網游核心技術的公司並不多,能獨立架構手機網游客戶端和服務器端的人更是少數。這些資料也在很多公司被視為機密,網上開源的資料,從網絡游戲“水果機”,到基於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)
{ 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) {}
}
}