#處理異步結果
1.爲什麼使用異步Results?
直到現在,我們只能計算直接發送到客戶端的result。這種方式並非總是奏效,result或許要花費高昂的計算代價,或者很長的web服務調用。
因爲Play2.0工作時,action代碼必須儘可能得快(例如非阻塞IO)。那麼如果我們還沒有計算完畢我們要返回什麼結果呢?答案是響應一個
結果的承諾(對承諾有結果)。
2.如何創建一個Promise<Result>。
要創建一個承諾結果,我們首先需要一個給我嗎計算出結果的真實數據的承諾:
Promise<Double> promiseOfPIValue = computePIAsynchronously();
Promise<Result> promiseOfResult = promiseOfPIValue.map(
new Function<Double,Result>() {
public Result apply(Double pi) {
return ok("PI value computed: " + pi);
}
}
);
Play2.0的異步API方法會給你一個承諾(promise)。例如在你通過Play.libs.WS的API調用外部Web服務時,
或者你使用akka計劃異步任務,或者使用akka和Actors溝通時。
一種簡單的方法是使用akka的一段異步程序來獲得一個Promise,這一過程在新的線程中執行,如:
Promise<Integer> promiseOfInt = Akka.future(
new Callable<Integer>() {
public Integer call() {
intensiveComputation();
}
}
);
3.異步結果
現在我們要用到Results.Status了,在發送異步結果前,需要使用Results.AsyncResult包裝一下實際的結果。,如:
public static Result index() {
Promise<Integer> promiseOfInt = Akka.future(
new Callable<Integer>() {
public Integer call() {
intensiveComputation();
}
}
);
async(
promiseOfInt.map(
new Function<Integer,Result>() {
public Result apply(Integer i) {
return ok("Got result: " + i);
}
}
)
);
}
注意async()是用來從Promise<Result>建立一個AsyncResult的幫助方法。
#流Http響應
1.標準響應和Content-Length頭
自Http 1.1,保持一個單一的連接爲若干個Http請求和響應服務,服務器在做出響應時
必須發送正確的Content-Length Http頭。
默認的,你可以發送一個簡單的result,例如:
public static Result index() {
return ok("Hello World")
}
在這個例子中你沒有設置Content-Length頭,當然,這是因爲你發送的數據大家都認識,
Play能夠爲你計算出內容長度(注意基於文本的內容長度與你的編碼格式有關),
併爲你生成一個正確的頭。
爲了能計算出內容的長度(大小),你需要將全部的相應數據加載到內存中。
2.文件服務
如果把簡單的內容數據全部裝載進內存不是個問題,那麼如果要裝載一個巨大的數據呢?
比如說,我們要把一個很大的文件返回給web客戶端。Play爲這種常見的任務體統了易用的幫助方法:
public static Result index() {
return ok(new java.io.File("/tmp/fileToServe.pdf"));
}
另外,這個方法也會自動爲你計算文件名的Content-Type頭。並且還會添加一個說明web客戶端如果操作
這個響應的Content-Disposition頭。默認使用Content-Disposition: attachment; filename=fileToServe.pdf
詢問web瀏覽器下載該文件。
3.分塊響應
現在,能夠橫好的和文件流內容一起工作了,因爲我們訥訥感在將文件流化前算出它的長度來。
但是如何動態計算一個得不到內容大小的文件呢?
這種類型的響應我們得使用分塊傳輸編碼。
分塊傳輸編碼在Http 1.1中十一中數據傳輸機制,Web服務器提供若干塊數據。它使用Transfer-Encoding Http響應頭
而不是Content-Length頭,不過Content-Length也是需要的,但是協議不用它。
因爲服務器在開始將響應傳輸到客戶端之前並不需要知道內容總共有多大。
每個塊能夠正確發送自身的大小,客戶端可以正確接收這個塊。數據傳輸最終以一個大小爲0的塊結束(就是說,
所有塊發送完畢後,還要發送一個大小爲0的塊)。
這種方式的優勢在於,我們可以提供實時數據。就是說一旦得到一個數據塊,我們可以立即發送出去。缺點是,因爲客戶端
不知道整個內容的大小,因而不能顯示一個正確的下載進度條。
比如說,我們有一個計算一些數據的動態輸入流的服務。我們可以直接使用Play的分塊響應來流化其內容:
public static Result index() {
InputStream is = getDynamicStreamSomewhere();
return ok(is);
}
你也可以設置你自己的分塊響應器。Play的JavaAPI支持文本和二進制的分塊流(通過String和byte[])。
public static index() {
// Prepare a chunked text stream
Chunks<String> chunks = new StringChunks() {
// Called when the stream is ready
public void onReady(Chunks.Out<String> out) {
registerOutChannelSomewhere(out);
}
}
// Serves this stream with 200 OK
ok(chunks);
}
當流可以安全的寫是onReady方法被調用。它爲你提供了Chunks。Out,你可以向裏面寫。
#Comet 套接字(Comet sockets)
注意:詳細的Comet技術請參閱Bayeux Protocol。http://svn.cometd.org/trunk/bayeux/bayeux.html
1.使用分塊響應創建Comet套接字
分塊響應(chunked responses)的一個很有用的用途是創建Comet 套接字,Comet套接字只是一個僅包含<script>元素的text/html分塊響應。
每個分塊裏,我們寫一個包含JavaScript的<script>標籤,立即由web瀏覽器執行。通過這種方式,我們可以把事件
由服務器端發送到客戶端:對每個消息而言,把它包裝進JavaScript回調函數裏,發給分塊響應即可。
我們來寫一個對這一概念的證明:創建一個產生調用<script>標籤調用瀏覽器console.log功能的舉例:
public static Result index() {
// Prepare a chunked text stream
Chunks<String> chunks = new StringChunks() {
// Called when the stream is ready
public void onReady(Chunks.Out<String> out) {
out.write("<script>console.log('kiki')</script>");
out.write("<script>console.log('foo')</script>");
out.write("<script>console.log('bar')</script>");
out.close();
}
}
response().setContentType("text/html");
ok(chunks);
}
在瀏覽器上訪問這個Action,你會看到有三個時間在瀏覽器控制檯被記錄。
2.使用play.libs.Comet幫助方法。
我們提供了一個Comet的幫助來操作這些comet分塊流。它可以支持String和Json。
前面的代碼可以改寫成這樣:
public static Result index() {
Comet comet = new Comet("console.log") {
public void onConnected() {
sendMessage("kiki");
sendMessage("foo");
sendMessage("bar");
close();
}
};
ok(comet);
}
3.永遠的iframe技術
標準的技術是寫一個Comet套接字,在一個iframe中加載無限循環的Comet響應,並且指定一個回調函數調用其父Fram。
public static Result index() {
Comet comet = new Comet("parent.cometMessage") {
public void onConnected() {
sendMessage("kiki");
sendMessage("foo");
sendMessage("bar");
close();
}
};
ok(comet);
}
Html頁面類似於這樣:
<script type="text/javascript">
var cometMessage = function(event) {
console.log('Received event: ' + event)
}
</script>
<iframe src="/comet"></iframe>
#WebSocket
1.用WebSocket代替Comet
Comet Socket是一種向web瀏覽器發送活動事件的黑客手段。Comet僅支持從服務器到客戶端的單向通信。要把事件推到服務器端,web瀏覽器要使用Ajax請求。
現代Web瀏覽器原生支持WebSockets的通過雙向實時通信。
WebSocket是一個網絡技術,通過一個單一傳輸控制協議(TCP)套接字,提供雙向,全雙工的通信通道。 WebSocket的API是由W3C制定,WebSocket協議已經被IETF規範爲RFC6455。
WebSocket被設計爲在Web瀏覽器和Web服務器上實現,但它可以通過任何客戶端或服務器應用程序使用。因爲一般的非80端口的tcp連接經常被除家庭環境外的管理員封鎖,
當複用一個端口實現多個Websocket服務時,可以通過增加額外的協議開銷,從而避開這些限制。這使得web應用提供實時的雙向通信成爲可能。
在Websocket出現之前,要實現全雙工的雙向通信只能使用Comet通道。
Comet的傳輸並不可靠,同時,由於TCP握手和HTTP頭的存在,在傳輸小消息時它會低效。Websocket旨在不影響網絡安全的前提下解決這些問題。
2.處理Websocket
到現在爲止,我們使用一個簡單的操作方法來處理標準的HTTP請求和發送標準的HTTP結果。WebSockets是一個完全不同的猛獸,無法通過標準的行動處理。
爲了處理WebSocket你的方法必須返回WebSocket的,而不是一個Result:
public static WebSocket<String> index() {
return new WebSocket<String>() {
// Called when the Websocket Handshake is done.
public void onReady(WebSocket.In<String> in, WebSocket.Out<String> out) {
// For each event received on the socket,
in.onMessage(new Callback<String>() {
public void invoke(String event) {
// Log events to the console
println(event);
}
});
// When the socket is closed.
in.onClose(new Callback0() {
public void invoke() {
println("Disconnected")
}
});
// Send a single 'Hello!' message
out.write("Hello!");
}
}
}
一個WebSocket可以訪問請求頭(從HTTP請求發起的WebSocket連接),使您可以檢索標準頭文件和會話數據。但它不會訪問任何請求體,也不訪問HTTP響應。
當WebSocket準備好了,你得到輸入和輸入的通道。
在這個例子中,我們打印每個消息到控制檯,我們發送一個Hello!消息:
public static WebSocket<String> index() {
return new WebSocket<String>() {
public void onReady(WebSocket.In<String> in, WebSocket.Out<String> out) {
out.write("Hello!");
out.close()
}
}
}