如何中設計一款優秀的J2ME軟體
-------------------------
JAVA ME 這個技術出現到現在也有好多年了,這段時間,大家對這項技術的褒貶不一。我們暫且不討論這些,我今天就拿我做項目的經驗來說。該怎麼樣設計一款優秀的 JAVA ME手機軟體。這篇文章不是純技術性的文章,只要稍微做過項目的朋友都是可以看的明白了,裡面的觀點只是代碼自己的個人觀點。說的不對的地方望大家多多指 教。
首先我們來說說J2me的進階UI應用情況吧:
1. 遊戲,根本就不採用進階UI,用到的只是進階UI調用本地IME。
2. 公司專屬應用程式,相對要求互動性高,即時快,使用者介面友好的應用來說,進階UI的應用情況跟遊戲查不錯
3. 簡單的例子教程,更不就拿不出收的手機軟體,大部分採用進階UI,因為進階UI簡單方便,容易描述。
綜上所述,進階UI根本就不適合現在的公司專屬應用程式,以及遊戲開發
這方面的例子有UCWEB, 航海家, Fadato足球用戶端,Bobi足球即時工具,QQ系列軟體,線上直播,手機娛樂軟體,Mino郵件用戶端等等。。。這一系列證明進階UI的應用不多。在這些系統裡面用到最多的就是軟體的配置,IME的調用,錯誤的提示(Alert)等
JSR規範制定的成功之處。
開發手機軟體雖然沒有開發電腦軟體工作量那麼大,但是要考慮到的東西一點都不比電腦軟體少,理由如下:
1. 硬體設定遠不如電腦
2. 作業系統過於分散
JSR就是為了這些差異而出現的。
使用者可以通過查詢api來查看自己的手機是否之處這樣的功能過,而做出選擇。
上面是廢話。下面看看怎麼樣設計一款好軟體吧
1. 使用者介面友好,一套可定製的UI組件。
2. 使用者操作邏輯合理,專業的策劃組織
3. 一套合理的記憶體管理,這樣才可以保證軟體的穩定性
4. 適應需求的軟體結構模式。
5. 效能方面一定要好。
上面是一款軟體基本的條件,要想讓這個軟體被人們廣泛解釋的話,還需要解決下面的問題:
1. 手機型號 支援,盡量匹配多種機型,在不能保證支援全部機型的條件下,最起碼支援主流的手機廠商的手機
2. 為各個機型定製一些特別的服務,比如有些手機支援JSR75,可以考慮儲存更多的東西到本機手機上,有些不支援,可可以考慮用RMS等,有些支援媒體類型多點,手機上也可以為這類手機定製多點服務
3. 一款好產品,必須有一個社區想扶持,所以一個熱門的產品社區是少不了的。
4. 個人感覺,單機版的軟體不可能取得很大的成功,一定要採用C/S的方式來取得使用者對產品的粘度
5. 羅馬也不是1天就可以建成的,所以軟體也不是一出來就很完美,所以就要不停的對軟體進行更新,維護工作,這是最重要的,好軟體的其中之一的標準就是看看這個軟體發展到什麼版本了,版本也高,證明你的軟體的生命力越強 。
-------------------------------
J2ME手機網遊解秘
前言:
目前國內真正掌握手機網遊核心技術的公司並不多,能獨立架構手機網遊用戶端和伺服器端的人更是少數。這些資料也在很多公司被視為機密,網上開源的資料,從網路遊戲“水果機”,到基於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) {}
}
}