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 就好。(參考 JavaPython 的相關 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 留給 ClippedStateUnclippedState 實做。

比對 ClippedStateUnclippedState 實做 method 的差異,差別在於 ClippedState 多了 widthheightlefttop 這幾個 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) 的功能,就留待下回分解了(?)


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