轉帖:http://www.developersky.net/thread-63-1-1.html
一直以來,HTTP協定都是嚴格遵循Request-Response模型的。用戶端發送一個Request到伺服器,伺服器對Request作出響應并将Response發送回用戶端。也就是說,所有的互動都是由用戶端發起的,伺服器不會發起任何互動。
為了建立互動性更強的web應用程式,AJAX出現了,AJAX實作了一個動态的從Server擷取資料的方法。通過使用AJAX,浏覽器通過XMLHttpRequest API來發送HTTP request。XMLHttpRequest使得我們可以在不阻塞使用者界面的情況下向伺服器發送異步的HTTP request來擷取資料。但是AJAX并沒有定義新的HTTP request類型,隻是将發送HTTP request的工作移到了背景,不影響使用者的操作。是以AJAX也沒有打破Request-Response的模型,還是由浏覽器從伺服器‘拉’資料。
另外一種技術是Comet,也稱為反向Ajax。和Ajax一樣,Comet也是建立在已經存在的HTTP協定之上的。Comet會維護一個長期存活的HTTP連接配接,發送‘假’的請求進而得到response。
這些都是為了打破HTTP協定的限制的解決方法。但是在HTML5中,這種限制會被打破。HTML5規範中包含很多功能強大的特性,能夠将浏覽器變成功能齊全的RIA用戶端平台。Server-Sent Event和WebSockets就是其中的兩個特性,這兩個特性能夠幫助我們實作伺服器将資料‘推送’到用戶端的功能。
在這篇文章中我們先介紹一下Server-Sent Events特性
Server-Sent Events
Server-Sent Events實際上将Comet技術進行了标準化。Server-Sent Events規範“定義了API來打開一個HTTP連接配接,通過該連接配接能夠擷取從伺服器推送的通知”。Server-Sent Events包含新的HTML元素EventSource和新的MIME類型 text/event-stream,這個MIME類型定義了事件架構格式。
<html>
<head>
<mce:script type=\'text/javascript\'><!--
var source = new EventSource(\'Events\');
source.onmessage = function (event) {
ev = document.getElementById(\'events\');
ev.innerHTML += "<br>[in] " + event.data;
};
// --></mce:script>
</head>
<body>
<div id="events"></div>
</body>
</html>
EventSource代表的是接收事件的用戶端的終點。用戶端通過建立EventSource對象來打開一個event stream。建立EventSource對象時,該對象接收一個事件來源的URL作為其構造函數的參數。當每次收到新的事件資料時onmessage事件處理器會被調用。
通常情況下,浏覽器會限制到每個伺服器的連接配接的數量。在有些情況下,裝載多個包含到同一個域的EventSource對象的頁面會導緻對每個EventSource建立一個專屬于該EventSource的連接配接,這種情況下很快就會超出連接配接數量限制。為了處理這種情況,我們可以使用共享的WebWorker,該對象共享一個EventSource的執行個體。另外,通過定義浏覽器特定的EventSource實作,我們可以做到如果兩個EventSource的URL是相同的,那麼他們就重用相同的連接配接。這時,共享的連接配接就由浏覽器特定的EventSource實作來管理。
當event stream打開的時候,浏覽器會發送如下的HTTP request。
REQUEST:
GET /Events HTTP/1.1
Host: myServer:8875
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE)
AppleWebKit/532+ (KHTML, like Gecko) Version/4.0.4
Safari/531.21.10
Accept-Encoding: gzip, deflate
Referer: http://myServer:8875/
Accept: text/event-stream
Last-Event-Id: 6
Accept-Language: de-DE
Cache-Control: no-cache
Connection: keep-alive
Accept定義了需要的格式 text/event-stream。 雖然Server-Sent Events規範定義了text/event-stream的MIME 類型,該規範同時允許使用其他的事件架構格式。但是Server-Sent Events的實作必須支援test/event-stream格式。
根據text/event-stream的格式,一個事件有一個或多個注釋行和字段行組成。注釋行是由冒号:開始的行。字段域行由字段名和字段值組成,字段名和字段值也是由冒号:分隔。多個事件之間用空行分隔。下面就是一個Response的例子:
RESPONSE:
HTTP/1.1 200 OK
Server: xLightweb/2.12-HTML5Preview6
Content-Type: text/event-stream
Expires: Fri, 01 Jan 1990 00:00:00 GMT
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Connection: close
: time stream
retry: 5000
id: 7
data: Thu Mar 11 07:31:30 CET 2010
id: 8
data: Thu Mar 11 07:31:35 CET 2010
[...]
根據定義,Event stream不應該被緩存。為了避免緩存,在Response的頭中包含了Cache-Control,禁止了緩存該response。
上面的例子中,該response中包含三個事件。第一個事件包含一個注釋行和一個retry字段;第二個事件和第三個事件都是包含一個id字段和一個data字段。data字段中包含的是事件的資料,在上面的例子中是目前的時間。id字段是用來在event stream中跟蹤處理程序的。上面的例子中,伺服器端的應用程式會每隔5秒向event stream中寫入一個事件。當EventSource接收到該事件後,onmessage事件處理器就會被調用。
不同的是,第一個事件不會觸發onmessage處理器。第一個個事件沒有data字段,隻包含一個注釋行和一個retry字段,retry字段是用于重新連接配接的目的的。retry字段定義了重新連接配接的時間,機關是毫秒。如果收到了這樣的字段,EventSource會更新其相關的重新連接配接時間的屬性。在發生網絡錯誤的情況下,重新連接配接時間在提高可靠性方面扮演了重要的角色。當EventSource執行個體發現連接配接斷開了,在指定的重新連接配接時間之後會自動的重建連接配接。
我們可以看到,在HTTP request中,我們可以指定Last-Event-Id。EventSource在重建連接配接的時候會指定該值。每次EventSource收到包含id字段的事件時,EventSource的last event id屬性會被更改,在重建連接配接的時候,EventSource的last event id屬性會被寫入HTTP request的Last-Event-Id中。這樣如果伺服器端實作了lastEventId的處理,就可以保證在重建的連接配接中不會發送已經收到的事件了。
下面的代碼是一個基于Java HTTP 庫xLightweb(包含HTML5預覽擴充)的HttpServer的例子。
class ServerHandler implements IHttpRequestHandler {
private final Timer timer = new Timer(false);
public void onRequest(IHttpExchange exchange) throws IOException {
String requestURI = exchange.getRequest().getRequestURI();
if (requestURI.equals("/ServerSentEventExample")) {
sendServerSendPage(exchange, requestURI);
} else if (requestURI.equals("/Events")) {
sendEventStream(exchange);
} else {
exchange.sendError(404);
}
}
private void sendServerSendPage(IHttpExchange exchange,
String uri) throws IOException {
String page = "<html>/r/n " +
" <head>/r/n" +
" <mce:script type=\'text/javascript\'><!--
/r/n" +
" var source = new EventSource(\'Events\');/r/n" +
" source.onmessage = function (event) {/r/n" +
" ev = document.getElementById(\'events\');/r/n" +
" ev.innerHTML += /"<br>[in] /" + event.data;/r/n"+
" };/r/n" +
"
// --></mce:script>/r/n" +
" </head>/r/n" +
" <body>/r/n" +
" <div id=/"events/"></div>/r/n" +
" </body>/r/n" +
"</html>/r/n ";
exchange.send(new HttpResponse(200, "text/html", page));
}
private void sendEventStream(final IHttpExchange exchange)
throws IOException {
// get the last id string
final String idString = exchange.getRequest().getHeader(
"Last-Event-Id", "0");
// sending the response header
final BodyDataSink sink = exchange.send(new
HttpResponseHeader(200, "text/event-stream"));
TimerTask tt = new TimerTask() {
private int id = Integer.parseInt(idString);
public void run() {
try {
Event event = new Event(new Date().toString(), ++id);
sink.write(event.toString());
} catch (IOException ioe) {
cancel();
sink.destroy();
}
};
};
Event event = new Event();
event.setRetryMillis(5 * 1000);
event.setComment("time stream");
sink.write(event.toString());
timer.schedule(tt, 3000, 3000);
}
}
XHttpServer server = new XHttpServer(8875, new ServerHandler());
server.start();
Server-Sent Events規範推薦如果沒有其他的資料要發送,那麼定期的發送keep-alive注釋。這樣代理伺服器就可以在某個HTTP連接配接有一段時間不活躍時關閉該連接配接,這樣代理伺服器能夠關閉空閑的連接配接來避免浪費連接配接在沒有響應的HTTP伺服器上。發送注釋事件使得這種機制不會發生在有效的連接配接上。盡管EventSource會自動重建連接配接,但是發送注釋事件還是能夠避免不必要的重新連接配接。
Server-Sent Event是基于HTTP streaming的。如上所述,response會一直打開,當伺服器端有事件發生的時候,事件會被寫入response中。理論上來說,如果網絡的中介如HTTP代理不立即轉發部分的response,HTTP streaming會導緻一些問題。現在的HTTP RFC (RFC2616 Hypertext Transfer Protocal – HTTP/1.1)沒有要求部分的response必須被立刻轉發。但是,很多已經存在的流行的、工作良好的web應用程式是基于HTTP streaming的。而且,産品級别的中介通常會避免緩沖大量的資料來降低記憶體的占用率。
和其他的流行的Coment協定如Bayeux和BOSH不同,Server-Sent Event隻支援單向的從伺服器到用戶端的通道。Bayeux協定支援雙向的通信通道。另外,Bayeux能夠使用HTTP Streaming和輪詢。BOSH協定也支援雙向通信通道,但是BOSH是基于輪詢機制的。(所謂的輪詢就是用戶端定期發送request到伺服器端來擷取資料)。
盡管Server-Sent Events比Bayeux和BOSH的功能要少,但是在隻需要單向的伺服器向用戶端推送資料的情況下(在很多情況下都是這樣),Server-Sent Events有潛力成為占主導地位的協定。Server-Sent Events協定被Bayeus和BOSH要簡單的多。另外,Server-Sent Events被所有相容HTML5的浏覽器支援(這就是規範的威力啊)。