2011-09-28

用 Channel API 實作簡易聊天室

Google App Engine 自 1.4.0 版推出 Channel API,使 server 與 browser 之間可以不透過 pooling 的方式做到 server push。不過 Google 官方教學文件摻雜了井字遊戲的元素,反而無法專注於 Channel API 上。這篇文章打算用最原始的聊天室,透過實做的過程來體驗一下 Java 版 Channel API。

因為是簡易聊天室,所以只打算提供一個共用的聊天室,然後用兩個 JSP 檔來解決:room.jsp、server.jsp。room.jsp  負責處理使用者輸入訊息、顯示對話;server.jsp(應該寫成 servlet 比較好,因為是簡易聊天室......)負責接收訊息、並廣播出去。

對應到 Channel API 的用詞,一個聊天室就是一個 channel,同一個 channel 的成員(client 端)就會接收到其他成員所發出的訊息(message)。所以,從 server 的角度,需要作這些事情:
  1. 開啟一個 channel
  2. 給加入這個 channel 的 client 專屬識別碼(token)
  3. 接收訊息的管道
  4. 發送訊息的功能
在 Channel API 當中只要決定 channel 的 名字就算做到第一點了,因為是簡易聊天室,所以把名稱訂死為「PsMonkey」。產生 token 的方法是:
//Java code
ChannelService channel = ChannelServiceFactory.getChannelService();
String token = channel.createChannel("PsMonkey");

因為是簡易聊天室,所以把 token 透過 JSP 產生、並塞進 client 端的 JavaScript 碼。

接下來看 client 端的部份。首先建立連線的部份:
//JavaScript code
var channel = new goog.appengine.Channel("<%=token%>");  //token 傳入
var handler = {
	'onopen' : onOpened,  //建立連線、channel.open() 就會觸發
	'onmessage' : onMessage,  //有訊息傳入時
	'onerror' : onError,  //發生錯誤時
	'onclose' : onClosed,  //連線結束時
};
socket = channel.open(handler);

socket 就是實際處理通訊的部份。這段的重點是透過 handler 設訂 socket 各種狀況會觸發的 method。仔細想一下就會發現:「只有接收訊息的 method,那傳送訊息呢?」

Channel API 要解決的問題是 server push,client 端傳送訊息的功能並不包含在其中,得透過 HTTP request 來作到。所以送訊息的 method 會長的像這樣:
//JavaScript code
sendMessage = function(message) {
	var xhr = new XMLHttpRequest();
	xhr.open('POST', 'server.jsp?msg='+message, true);
	xhr.send();
};

於是用 sendMessage() 來設定在 client 端連線時送出「____ 進入聊天室」、離線時送出「____ 離開聊天室」:
//JavaScript code
onOpened = function() {
	sendMessage("「"+name + "」進入聊天室......");
};

onClosed = function(){
	sendMessage("「"+name + "」離開聊天室......");
}

有訊息進來的時候,就把訊息塞進某一個 element 的尾巴。這邊要注意一點,參數 msg 不是單純的字串,而是一個物件,data 這個 field 才是真正回傳的資料。
//JavaScript code
onMessage = function(msg) {
	document.getElementById("output").innerHTML += msg.data +"<br />";
}

接著來看一下負責處理 client 端 sendMessage() 的 server.jsp,直接貼程式碼:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@page import="com.google.appengine.api.channel.*"%>
<%
String message = request.getParameter("msg");
ChannelService channel = ChannelServiceFactory.getChannelService();
channel.sendMessage(
	new ChannelMessage("PsMonkey", message)
);
%>

在從 request 當中取得 clinet 端傳上來的訊息後,連同 channel 的名稱包成一個  ChannelMessage,透過 ChannelService.sendMessage() 傳出去,就會觸發 client 端的 socket.onMessage 了。

再透過一些簡單的 HTML 與 JavaScript 來讓 client 端送出訊息時能呼叫到 sendMessage(),聊天室就完成了!是不是很簡單呢? [扭扭]

最後補充 socket 的幾件事情:

  • onerror 會傳入一個參數,其中有兩個 field:
    • description:錯誤的描述
    • code:HTTP 錯誤代碼
    • 目前只測出 idle 太久、server 切斷連線時會觸發
  • socket 還有一個 method:close(),呼叫成功會觸發 socket.onclose。
完整可以跑的範例放在這裡...

2011-09-16

App Engine 1.5.4 版發佈


原文網址:http://googleappengine.blogspot.com/2011/09/app-engine-154-sdk-release.html

這四個禮拜真的是忙翻啦(你可能有聽說),但我們仍然保持每個月發佈新版本的期程。今天我們提供了新的 SDK,以及一些新功能和錯誤修正。

整體更新
  • Blobstore API——我們加入了一個選項,讓你可以指定上傳 blob 的大小限制。這個功能讓你可以接觸到使用者上傳的 blob,同時也可以確保他們上傳的東西沒有超過你預設的限制。
  • Datastore 查詢改進——我們延續過去幾個版本的主題,調整 Datastore 的查詢規劃,讓使用者感覺更靈活。從 1.5.4 版開始,對多個 property 作 filter 的查詢,會持續運作直到 Datastore 的查詢時間上限(高達 30 秒!)。過去,許多這類的查詢都會因為沒有效率的 index 而導致產生錯誤,現在則會運作正常。
  • 在 SDK 中顯示 Datastore 寫入次數——依據最近改版為 Side by Side 帳單時所得到的回饋意見,現在我們會在 SDK 的 dataviewer 當中顯示儲存一個 entity 需要的寫入次數。寫入的次數包含寫入 entity、以及增加 entity 時 index 的寫入。你可以透過把 index 的 property 調整成 unindex 的 properties,來降低寫入的次數,只要確定你沒有在其他 query 當中參考到這些 property 就好。(參考 Java 與 Python 的相關 API)
Java
  • Prospective Search API——我們發佈了實驗性質、Java 版本的 Prospective Search API。這讓你在符合某些條件的資料被寫入到 Datastore 時,能偵測到、並有所作為。
Python
  • Memcache——你現在可以非同步呼叫 Memcache API。有了非同步的 Memcache,你的 application 就不會在呼叫 Memcache 時被 block 住,可以繼續處理 request 而不用等待 Memcache 的回應。寫 Java 的朋友們也不要擔心,我們即將釋出的版本會有對應的功能。
在老地方(Java 版,Python 版)可以找到完整的版本更新紀錄。你們在 Google Group 提出的回饋意見,我們都會仔細閱讀。祝 coding 快樂!

2011-09-08

App Engine 1.5.3 版發佈


原文網址:http://googleappengine.blogspot.com/2011/08/app-engine-153-sdk-released.html

今天,我們很高興地宣佈新版的 App Engine 發佈了。你可能有注意到,在過去幾個月中,版本更新的頻率略有上升。我們做了一些內部改組,期待能每個月推出一個新版本。這個月的更新包含了一些 datastore 的更新、一些 blobstore API、memcache API 的更新,以及一個針對 Java 開發者的新功能。

Python 與 Java 的改變
  • blobstore API——我們已經取消 blob 上傳的大小限制。你現在可以上傳任何大小的檔案,讓你的 application 可以處理圖片、影片、或任何 internet 連線可以處理的東西。

datastore 的改變
  • 取回索引——對現存於 datastore 的索引,我們加上了讓你可以用程式取回索引列表及狀態的功能。
  • datastore 管理——現在你可以在 Admin Console 啟用 datastore 管理功能。這讓 Java 使用者可以做到「刪除某一類下的所有 entity」的功能,而無須上傳 Python 版的 application。對於 Python 的開發人員而言,這表示你不用再到 app.yaml 檔裡頭將這個功能開啟。
  • HRD 整合的可靠測試者——我們正在找尋前期就採用 HRD 的人,來實驗強化 HRD 整合的工具。這個工具需要一段唯讀時間,這段時間與你的 datastore 寫入頻率有關(目前這個版本的行為是相對於 datastore 的大小)。請參閱版本更新紀錄,裡頭有更多資訊。(謎之聲:這是哪門子的版本更新 ==")

Python 的更新
  • memcache API——我們在 Python 的 memcache API 當中支援 CAS(compare-and-swap,比較並替換)的操作方式(Java 已經有了)。當你取值回來並要更新它時,這個操作讓你可以在沒有其他更新的 request 時,才作更新的動作。

Java 的更新
  • 下載 application——使用 AppCfg download_app 指令,你可以下載上次更新版本當中,war 目錄下的任何檔案。

這個版本還包含一些微幅的更新與錯誤修正(無論是 Python 版或 Java 版),所以一定要參閱完整的版本更新紀錄。回饋意見與問題討論都可以在我們的 Google Group 當中發表。

2011-09-06

App Engine 1.5.2 版發佈

原文網址:http://googleappengine.blogspot.com/2011/07/app-engine-152-sdk-released.html

server 產能的更新
  • 可調整的 Scheduler 參數 - 正如之前討論過,我們推出了兩款 scheduler 旋鈕(好吧,實際上看起來比較像滑動軸),讓你能控制有多少 instance 在 application 上執行的參數。現在,你將可以設定 panding latency 的最小值、以及閒置 instance 的最大量。

Datastore 的更新
  • 進階 query 計畫 - 我們移除了爆量索引的需要、也減少了許多 query 要自訂 index 的需求。在許多狀況下 SDK 會建議更佳的索引方式,接下來的文章也會介紹如何進一步最佳化。
  • namespace 層級的 datastore 統計資料:現在除了取得全部的 datastore 統計資料,我們也提供一個新的選擇,讓你可以以 namespace 來查詢。

Task Queue 的更新
  • 新的 Task Queue 細目頁面——我們改寫了 Administration Console 裡的 Task Queue 細目頁面,提供了執行中的 task 的更多資訊。你現在可以看到排隊中的 task、有效承載量、以及前一個執行 task 的資料。
  • 1MB 的 Pull Task 容量——對於容量大小的限制,我們的信念就是:只能變得越來越大!所以,在這個版本當中,我們把 Pull Task 的容量變成 1MB。
  • Pull Queue 時效變更——針對 Pull Queue 我們推出了新的 method,如果當初設定的時效不足時,讓你可以將現有的 task 延長時效。

最後,我們有一些令人振奮的消息是關於實驗中 Go 執行環境。雖然 Go 仍然在實驗階段,但從 1.5.2 之後,Go 將可以存取所有 HRD 的 application。

一如往常,也有一些小功能與錯誤修復,在我們的版本更新紀錄當中可以找到完整的清單(Python 版,Java 版)。我們期待在論壇當中看到您的回饋意見與問題。

2011-09-04

整合 GMail 帳號

我有四個常用的 GMail 帳號。

當然,GMail 早就不是邀請制了,甚至自己弄個 Google Apps,要有幾個帳號都不是問題。事實上,因為 Google 的 service 用得有點兇,從 GMail、Blogger、Reader、Sites、WebMaster、譯者工具包還有賺錢用的 AdSense,我還寧願只用一個帳號就滿足全部需要。

大多數的 service 的確可以做到,像 Blogger 這種寫作類的 service 都有提供其他作者協同寫作的功能、Reader 用匯入匯出的功能解決,剩下的 WebMaster 跟 AdSense 偶爾上去看一看(似乎也能開放給其他帳號,沒細究),倒也還過得去。

不過 GMail 就沒這麼簡單了,總不能用公司的帳號收信、卻用私人的帳號回信吧?所以之前的作法是同時開 Chrome 跟 Firefox,搭配無痕/私密瀏覽來解決......

實在有夠土砲的...... Orz

後來經高人指點,發現 GMail 早就想到這點了,還提供了兩個招數......

萬用基本招:POP + Send mail as
GMail 裡頭要把 A 帳號的信都轉到 B 帳號有兩種方法。第一種是選擇轉寄(Forwarding)、第二種是透過 POP3 取得信件。這裡建議是用 POP3 的方式,步驟如下:
  • 進入 A 帳號,將「POP 下載(POP Download)」的功能開啟。
  • 進入 B 帳號,在「從其他帳戶取得郵件(check mail using POP3)」新增一個帳號。這裡要注意一點,如果 A 帳號不是 xxx@gmail.com,而是由 Google Apps 提供的,記得在「使用者名稱」輸入完整的 email 帳號。另外 POP 伺服器就輸入「pop.gmail.com」、通訊埠選擇「995」
設定完成後,系統就會開始到 A 帳號檢查有無信件,有的話會抓回來並掛上對應(可設定)的標籤。

如果 Google 只做到這邊,那也不過就是功能稍微強一點 web 版的郵件軟體。重點在於「Send mail as」的功能,這功能在設定完「POP 下載」應該就會出現,也可以獨立設定。這是幹什麼的呢?它可以讓你用 B 帳號登入寄信,但是收信人看到的寄信者卻是 A 帳號。

如此,透過 POP 將另一個帳號的信彙整進來、寄信的時候可以用指定的帳號寄出去,事情解決了,萬歲! \囧/

萬能大絕招:Grant access to your account
萬用基本招雖然解決了,可是總覺得還是哪裡不滿足。最根本的問題是通訊錄:A 帳號的通訊錄得透過匯入匯出的方式才能帶到 B 帳號(雖然說回信之後會自動建立通訊錄),而且很容易就會變成公私不分的大雜燴。再者就是信件得依賴標籤的功能來區分,看/找起來並不是那麼快樂。有沒有更簡單的方式呢?

當然有!

在 A 帳號的「授權這些使用者存取我的帳戶(Grant access to your account)」增加 B 帳號的資訊,過一段時間(號稱半個小時)之後重新登入 B 帳號,就可以點選畫面右上角顯示帳號的地方,選擇「切換帳號」會出現 A 帳號,再點下去會另外開一個視窗...... YES! 直接變成 A 帳號登入的畫面了!

是的,除了沒有 Buzz、Chat、Calendar 這些 mail 以外的功能,其餘畫面操作跟 A 帳號登入時一模一樣。A、B 帳號可以同時掛在同一個瀏覽器底下,而且不用知道/輸入 A 帳號的密碼。另外,要同時代管幾個帳號也都不是問題、完全不會混亂。

不過這招有個限制,就是 A、B 兩個帳號的 domain name 必須是一樣的,foo@gmail.com 跟 mail@foo.com 是無法這樣設定的。

心得感想
不管是從哪個角度來看,都是感觸良多。

從軟體開發的角度,只能說自嘆不如。當然,上頭說得這些功能並不需要什麼高深的技術或演算法,問題在於細節很多:得同時兼顧方便性與安全性、還不能造成使用上的混亂與衝突,如果連版本演進的功夫一起算下去,Google 在軟體架構設計上真的是很可怕...... Orz

從評論軟體的角度,只能說,Google 始終展現了「不怕你用」的恢宏氣度。在那個 Hotmail、Yahoo 信箱免費空間大小了不起 50、100MB,GMail 直接給 1GB 的容量、一年後變成 2GB,然後玩起「空間隨著時間慢慢增加」的噱頭,到現在 7.xGB 還在持續增長中。我有點好奇誰能在正常使用下塞滿它......

Google 也不怕你開多個帳號佔據它的資源,不但不怕,還提供了許多功能方便讓你管理眾分身。我是已經很久沒用其他 mail service 了,不知道有哪家有做到如此程度?

從另一個角度來看,Google 的功能大多悄悄無聲無息地出現,像「POP3 下載」這功能,在開放時網路上有喧囂一陣,所以還有點印象。但是這篇提到的其他功能,就根本也不知道啥時候冒出來的,更不用講 GMail 裡頭 LAB 那一卡車的東西了。

不知道這些功能,使用上也沒啥大妨礙;多了這些功能,你也不會覺得被妨礙。

又不免吐槽一下 M$,M$ 常常怕使用者不知道他做了多少改變,問題是改變之後反而不知道要怎麼用了,想回頭用舊的版本還不行(最近才遇到 Windows 2000 安裝 Skype 5.x 版會死翹翹,還好網路上還有 4.x 的免安裝版可以用 [怒]),說明文件又爛得要命,難怪 M$ 連 Office 都可以推證照制度...... Orz

最後說回行銷面,我想這篇就是受到 Google 行銷手法(個人覺得不算行銷,但是人家科班出身的都這樣講了.....)影響下的產物:

  • 「這是啥功能...... =="」 or 「最近有一批功能好便宜 [誤]」
  • 哇靠!這他 x 的超好用的啦...
  • 什麼,你居然不知道? 好好好,我教你......
於是乎,推銷員有了、教育人員有了、甚至連文件撰寫人員也有了(雖然 Google 本身的文件、即使是中文版的也不差)。Google 沒有想一次就改變全世界,甚至可能根本沒有想改變全世界,比較像是「我需要用這個,你要用也歡迎」。但是,世界改變了......

「如何吃下一頭大象?」「一次吃一口」

嗯... 還有很長的路要走...... [遠目]

2011-09-01

粗探 GWT Image 內的實做方式

GWT 當中的 Widget,大概只有 Button 的使用率大於 Image(這也難講,說不定有些人直接用 Image 作 button...... XD)。所以來探究一下 source code 寫了啥東西。

要建立一個 Image,最直接了當的用法是給它 url:
Image img = new Image("http://an.url/pic.jpg");

這裡的 url 使用相對路徑亦可,不過得注意是相對於載入這個 GWT module 的頁面就是了。

切到這個 Image(String) 這個 constructor,會發現當中做了兩件事情:
  1. 將 state 設定為新產生的 UnclippedState 物件
  2. 設定 style name 為 "gwt-Image"

設定 style name 這檔子事情有點無關緊要,忽略不管。至於 state 這個 field 是怎麼一回事?這似乎得要回頭看開頭 JavaDoc 寫的:
「The image can be in 'unclipped' mode (the default) or 'clipped' mode.」
也就是說,Image 透過 state 來決定當下是哪一種使用方式,這可以解釋為甚麼有另一個 Image(String, int, int, int, int) 的 constructor。實際去看 Image 的 method 行為,getter、setter、onLoad() 等都是由 state 負責,只有跟 event handler 有關的 method 是用 Widget.addHandler() 處理。

於是找到 Image 裡頭 State 這個 private 的 abstract class。它只有兩個 method 不是 abstract 的:onLoad(Image)、fireSyntheticLoadEvent(Image),這兩個都跟 event 有關,在這篇文章當中先略過,以免枝節太多。其餘的 abstract method 留給 ClippedState、UnclippedState 實做。

比對 ClippedState 與 UnclippedState 實做 method 的差異,差別在於 ClippedState 多了 width、height、left、top 這幾個 field,UnclippedState 在取這些值時,是直接對 Image 所屬的 ImageElement 取值,而 ClippedState 則直接回傳 field 的值。另外,如果叫用到 ClippedState.setUrl() 會將 state 切換到 UnclippedState;反之,如果叫用到 UnclippedState.setVisibleRect() 與 setUrlAndVisibleRect() 會將 state 切換到 ClippedState。

這到底在幹什麼?

回歸實際用途,ClippedState 的目的是只顯示圖片上的某個矩形區塊,所以得加上長、寬、起始位置等資訊。製造出這種效果的方法,則是靠 CSS 設定 span 的 background 來辦到。怎麼知道的呢?答案在 constructor 當中的這行:
image.replaceElement(impl.createStructure(url, left, top, width, height));

先用 ClippedImageImpl.createStructure() 製造出一個 Element 物件,然後再把原本 Image 的 element 換成這個。再仔細看一下 ClippedImageImpl 裡頭的寫法,發現它直接用最硬幹的方式設定 element 的 HTML 與 style,也難怪會抽出去自成一個 class。這說明了為甚麼 JavaDoc 強調當 clipped 與 unclipped 互換時,所有的 style 設定都會消失。

重新回到 Image,會發現這個 class 只是邏輯上的存在,為了做到 clipped/unclipped 的效果,所以透過 State 來處理;因為底層實做方式的不同,所以當物件的行為模式轉換時變更到對應的 state。而真正對應 DOM,則是 ImageElement(unclipped mode)與 SpanElement(clipped mode)。使用 Image 時哪會想到底下這麼多怪東西?好的 class 如當是也......

Image 最後還有一個 perfetch(String) 的功能,就留待下回分解了(?)


這篇是退伍後的復健治療... 所以... [遮臉]