Reverse Ajax, Phần 1: Giới thiệu về Comet
Ajax, Reverse Ajax và WebSockets
Ajax (Asynchronous JavaScript và XML), một kỹ thuật dành cho trình duyệt dựa trên JavaScript,
cho phép sử dụng một đoạn mã lệnh để đáp ứng các yêu cầu HTTP cho từng thành phần mà
không cần phải refresh lại toàn bộ trang web. Ajax đã được ứng dụng hơn 10 năm nay. Mặc dù
tên của nó có kèm theo XML, nhưng bạn có thể truyền tải bất cứ thứ gì trong một yêu cầu Ajax.
Dữ liệu được sử dụng phổ biến nhất là JSON, nó có cú pháp gần với cú pháp của JavaScript và
tiêu thụ ít băng thông hơn. Liệt kê 1 là ví dụ về một yêu cầu Ajax để lấy ra tên của một vùng địa
phương từ mã bưu chính của nó.
Liệt kê 1. Ví dụ về yêu cầu Ajax
var url = '
+ $('#postalCode').val() + '&country='
+ $('#country').val() + '&callback=?';
$.getJSON(url, function(data) {
$('#placeName').val(data.postalcodes[0].placeName);
});
Bạn có thể xem cách hoạt động của ví dụ trên trong file listing1.html có ở đây.
Về cơ bản Reverse Ajax là một khái niệm: có thể gửi dữ liệu từ máy chủ đến máy khách. Trong
một yêu cầu Ajax của HTTP tiêu chuẩn, dữ liệu được gửi đến máy chủ. Reverse Ajax có thể
được mô phỏng để tạo ra một yêu cầu Ajax, theo những cách cụ thể được nêu ra trong bài viết
này, do đó, máy chủ có thể gửi các sự kiện đến máy khách càng nhanh càng tốt (giao tiếp với độ
trễ thấp).
WebSockets, có kèm theo HTML5, là một kỹ thuật mới hơn. Nhiều trình duyệt đã hỗ trợ nó
(Firefox, Google Chrome, Safari và những trình duyệt khác). WebSockets tạo nên các kênh
truyền thông song song, hai chiều. Kết nối này được mở ra thông qua một yêu cầu HTTP được
gọi là WebSockets handshake với một số header đặc biệt. Kết nối này được duy trì và bạn có thể
viết và nhận dữ liệu bằng JavaScript, như thể bạn đang sử dụng một TCP socket nguyên bản.
WebSockets sẽ được trình bày trong phần 2 của loạt bài này.
Về đầu trang
Các kỹ thuật Reverse Ajax
Mục tiêu của kỹ thuật Reverse Ajax là giúp các máy chủ đẩy thông tin đến máy khách. Theo mặc
định các yêu cầu Ajax là không chính thống và chỉ có thể được bắt đầu từ máy khách đến máy
chủ. Bạn có thể vượt qua hạn chế này bằng cách sử dụng các kỹ thuật để mô phỏng truyền thông
đáp ứng giữa máy chủ và máy khách.
HTTP polling và JSONP polling
Polling bao gồm việc gửi một thông điệp từ phía máy khách đến máy chủ để yêu cầu một thông
tin dữ liệu nào đó. Thực ra đây chỉ là một yêu cầu HTTP của Ajax. Để có được các sự kiện từ
máy chủ càng sớm thì khoảng thời gian polling (thời gian giữa các yêu cầu) phải càng ngắn càng
tốt. Có một nhược điểm là: nếu khoảng thời gian này càng ngắn, trình duyệt của máy khách sẽ
đưa ra nhiều yêu cầu hơn, trong đó có những yêu cầu sẽ không trả về bất kỳ dữ liệu có ích nào
khiến cho băng thông bị hao tốn và xử lý tài nguyên vô ích.
Bảng thời gian trong Hình 1 cho thấy cách mà máy khách gửi các yêu cầu polling nhưng chẳng
có thông tin nào được trả về cả. Máy khách phải chờ đến lần polling tiếp theo để có được hai sự
kiện do máy chủ thu nhận được.
Hình 1. Reverse Ajax với việc polling của HTTP
Về bản chất, polling của JSONP giống như polling của HTTP. Tuy nhiên, có sự khác biệt ở chỗ
với JSONP bạn có thể đưa ra yêu cầu giữa các domain (các yêu cầu không có trong domain của
bạn). JSONP được dùng trong Liệt kê 1 để nhận về một tên địa phương từ một mã bưu chính.
Thường thì có thể nhận ra một yêu cầu JSONP bằng cách xem tham số gọi lại và nội dung trả về
của nó, đó chính là mã JavaScript có thể chạy được.
Để thực hiện polling trong JavaScript, bạn có thể sử dụng hàm setInterval để định thời gian
gửi các yêu cầu Ajax, như trong Liệt kê 2:
Liệt kê 2. Polling trong JavaScript
setInterval(function() {
$.getJSON('events', function(events) {
console.log(events);
});
}, 2000);
Đoạn mã demo kỹ thuật polling ở đây cho thấy phương pháp này tiêu thụ băng thông như thế
nào. Khoảng thời gian cho các lần polling ngắn nhưng có thể bạn sẽ nhận các kết quả trả về mà
chẳng có sự kiện (event) mới nào. Liệt kê 3 hiển thị kết quả của việc polling ở ví dụ mẫu.
Liệt kê 3. Kết quả của đoạn demo polling trong ví dụ mẫu
[client] checking for events
[client] no event
[client] checking for events
[client] 2 events
[event] At Sun Jun 05 15:17:14 EDT 2011
[event] At Sun Jun 05 15:17:14 EDT 2011
[client] checking for events
[client] 1 events
[event] At Sun Jun 05 15:17:16 EDT 2011
Polling trong JavaScript có các ưu và nhược điểm.
Ưu điểm: Nó thực sự dễ thực hiện và không đòi hỏi bất kỳ tính năng đặc biệt nào ở phía
máy chủ. Nó cũng làm việc trong tất cả các trình duyệt.
Nhược điểm: Phương pháp này hiếm khi được sử dụng vì nó không hề linh động. Hãy
tưởng tượng, giả sử có 100 máy khách, trong đó mỗi máy gửi các yêu cầu polling trong 2
giây thì số lượng băng thông và tài nguyên bị hao tốn như thế nào, ở đây 30% yêu cầu
được trả về không hề có chút dữ liệu.
Piggyback
Piggyback polling là một phương pháp thông minh hơn nhiều so với polling đơn thuần vì nó có
xu hướng loại bỏ tất cả các yêu cầu không cần thiết (các yêu cầu không trả về dữ liệu nào).
Không cần định sẵn một khoảng thời gian interval nào cả; yêu cầu sẽ được gửi đi khi máy khách
cần gửi một yêu cầu đến máy chủ. Sự khác biệt nằm ở cách phản hồi được chia thành hai phần:
phản hồi với dữ liệu được yêu cầu và các sự kiện từ máy chủ. Hình 2 chính là ví dụ.
Hình 2. Reverse Ajax với Piggyback polling
Khi thực hiện kỹ thuật Piggyback, thông thường tất cả các yêu cầu Ajax gửi đến máy chủ có thể
được trả về một phản hồi hỗn hợp. Ví dụ mẫu về cách thực hiện kỹ thuật này có ở đây và trong
Liệt kê 4 dưới đây.
Liệt kê 4. Ví dụ mẫu về mã piggyback
$('#submit').click(function() {
$.post('ajax', function(data) {
var valid = data.formValid;
// process validation results
// then process the other part of the response (events)
processEvents(data.events);
});
});
Liệt kê 5 hiển thị kết quả khi sử dụng phương pháp piggyback.
Liệt kê 5. Kết quả piggyback
[client] checking for events
[server] form valid ? true
[client] 4 events
[event] At Sun Jun 05 16:08:32 EDT 2011
[event] At Sun Jun 05 16:08:34 EDT 2011
[event] At Sun Jun 05 16:08:34 EDT 2011
[event] At Sun Jun 05 16:08:37 EDT 2011
Bạn có thể thấy kết quả của việc kiểm tra hợp lệ một khuôn mẫu (form validation) và các sự kiện
được thêm vào phản hồi. Một lần nữa, phương pháp này có những ưu và nhược điểm.
Ưu điểm: Không có các yêu cầu nào trả về mà không có dữ liệu, vì máy khách kiểm soát
khi nó gửi các yêu cầu, nên bạn tiêu thụ ít tài nguyên hơn. Nó cũng làm việc trong tất cả
các trình duyệt và không yêu cầu các tính năng đặc biệt ở phía máy chủ.
Nhược điểm: Bạn sẽ không biết khi nào mà các sự kiện ở phía máy chủ được gửi tới máy
khách vì nó đòi hỏi phải có một hành động từ phía máy khách để yêu cầu chúng.
Về đầu trang
Comet
Reverse Ajax với kỹ thuật polling hay piggyback cũng còn rất hạn chế: vì nó không co giãn và
không cung cấp khả năng giao tiếp với độ trễ thấp (tức là các sự kiện phải được gửi đến trình
duyệt ngay khi chúng đến máy chủ). Comet là một mô hình ứng dụng web, ở đây một yêu cầu
được gửi đến máy chủ và vẫn tiếp tục trong một thời gian dài cho đến khi hết giờ hoặc xuất hiện
một sự kiện từ máy chủ. Khi yêu cầu này được hoàn thành thì sẽ có một yêu cầu Ajax khác được
gửi đi để chờ các sự kiện khác từ máy chủ. Với Comet, các máy chủ web có thể gửi dữ liệu cho
máy khách mà cần phải có một yêu cầu cụ thể nào.
Ưu điểm lớn của Comet ở chỗ mỗi máy khách luôn có một liên kết giao tiếp đến máy chủ. Máy
chủ có thể đẩy các sự kiện vào các máy khách bằng cách thực hiện commit (hay hoàn thành)
ngay lập tức các phản hồi ngay khi chúng đến hoặc thậm chí nó có thể tích lũy và gửi một lần. Vì
một yêu cầu được mở trong một thời gian dài, nên cần có các tính năng đặc biệt ở phía máy chủ
để xử lý tất cả những long-lived này (long-lived requets - các yêu cầu có thời gian sống lâu).
Hình 3 là ví dụ. (Phần 2 của loạt bài này sẽ giải thích các ràng buộc máy chủ chi tiết hơn).
Hình 3. Reverse Ajax với Comet
Các cách thực hiện của Comet có thể được chia thành hai loại: một loại sử dụng chế độ
streaming và một loại khác sử dụng long polling.
Về đầu trang
Comet sử dụng kỹ thuật HTTP Streaming
Trong chế độ streaming, một kết nối được mở liên tục. Sẽ chỉ có một yêu cầu long-lived (#1
trong Hình 3) do mỗi sự kiện đến phía máy chủ được gửi đi thông qua cùng một kết nối. Do đó,
nó đòi hỏi ở phía máy khách phải có cơ chế phân chia các phản hồi đến từ cùng một nguồn kết
nối đó. Về mặt kỹ thuật, hai kỹ thuật phổ biến về streaming là Forever Iframes (Các IFrame ẩn)
và tính năng đa-phần (multi-part) của đối tượng XMLHttpRequest được sử dụng để tạo ra các
yêu cầu Ajax trong JavaScript.
Forever Iframes
Kỹ thuật Forever Iframes sử dụng một thẻ Iframe ẩn đặt trong trang với thuộc tính src trỏ đến
đường dẫn servlet nhằm trả về các sự kiện máy chủ. Mỗi khi nhận được một sự kiện, servlet sẽ
viết và đổ vào một thẻ script với mã JavaScript bên trong. Nội dung của iframe sẽ được thêm vào
thẻ script này và được thực thi.
Ưu điểm: Dễ thực hiện và nó hoạt động trong tất cả các trình duyệt hỗ trợ các iframe.
Nhược điểm: Không có cách nào để thực hiện xử lý lỗi hoặc theo dõi trạng thái của kết
nối, bởi vì tất cả các kết nối và dữ liệu đều được trình duyệt xử lý thông qua các thẻ
HTML. Do đó bạn không biết khi nào thì kết nối bị ngắt ở cả hai phía.
Tính năng multi-part của XMLHttpRequest
Kỹ thuật thứ hai đáng tin cậy hơn là sử dụng cờ multi-part được hỗ trợ bởi một số trình duyệt
(như Firefox) trên đối tượng XMLHttpRequest. Một yêu cầu Ajax được gửi và được mở ở phía
máy chủ. Mỗi lần một sự kiện đến, một phản hồi multi-part được viết thông qua cùng một kết
nối. Liệt kê 6 mô tả ví dụ này.
Liệt kê 6. Mẫu JavaScript để thiết lập một yêu cầu streaming multi-part
var xhr = $.ajaxSettings.xhr();
xhr.multipart = true;
xhr.open('GET', 'ajax', true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
processEvents($.parseJSON(xhr.responseText));
}
};
xhr.send(null);
Về phía máy chủ, mọi thứ phức tạp hơn một chút. Trước tiên, bạn phải thiết lập yêu cầu multi-
part, rồi treo (suspend) kết nối. Liệt kê 7 cho thấy cách treo một yêu cầu HTTP streaming. (Phần
3 của loạt bài này sẽ trình bày chi tiết hơn về các API).
Liệt kê 7. Ngắt một HTTP treaming trong một servlet khi sử dụng API Servlet 3
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// start the suspension of the request
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0);
// send the multipart separator back to the client
resp.setContentType("multipart/x-mixed-replace;boundary=\""
+ boundary + "\"");
resp.setHeader("Connection", "keep-alive");
resp.getOutputStream().print(" " + boundary);
resp.flushBuffer();
// put the async context in a list for future usage
asyncContexts.offer(asyncContext);
}
Giờ đây, mỗi khi một sự kiện xảy ra, bạn có thể lặp qua tất cả các kết nối đang bị treo và viết dữ
liệu tới chúng, như thể hiện trong Liệt kê 8:
Liệt kê 8. Gửi các sự kiện tới một yêu cầu multi-part đang bị treo bằng cách sử dụng API
Servlet 3
for (AsyncContext asyncContext : asyncContexts) {
HttpServletResponse peer = (HttpServletResponse)
asyncContext.getResponse();
peer.getOutputStream().println("Content-Type: application/json");
peer.getOutputStream().println();
peer.getOutputStream().println(new JSONArray()
.put("At " + new Date()).toString());
peer.getOutputStream().println(" " + boundary);
peer.flushBuffer();
}
Các file mà bạn tải về theo bài viết này, trong thư mục Comet-streaming, trình bày về HTTP
streaming . Khi bạn chạy ví dụ mẫu và mở trang chủ ra, bạn sẽ thấy các sự kiện lập tức xuất hiện
không đồng bộ ngay khi đến máy chủ. Ngoài ra, nếu bạn sử dụng addon Firebug, bạn có thể thấy
rằng chỉ có một yêu cầu Ajax được mở. Nếu bạn xem kỹ hơn, bạn sẽ thấy các phản hồi JSON
được hiển thị ở tab Response, như thể hiện trong Hình 4:
Hình 4. Cửa sổ Firebug của một yêu cầu HTTP streaming
Như thường lệ, kỹ thuật này cũng có những ưu và nhược điểm sau.
Ưu điểm: Chỉ có một kết nối liên tục được mở. Đây là kỹ thuật Comet, tiết kiệm việc sử
dụng băng thông nhất.
Nhược điểm: Không phải tất cả các trình duyệt đều hỗ trợ cờ multi-part. Một số thư viện
được sử dụng rộng rãi, chẳng hạn như CometD trong Java, đã cho thấy nhiều vấn đề về
bộ nhớ đệm (buffer). Ví dụ, các khối dữ liệu (multi-part) có thể được lưu giữ trong một
buffer và chỉ được gửi đi chỉ khi kết nối hoàn thành hoặc khi buffer đầy, điều này có thể
tạo ra độ trễ cao hơn dự kiến.
Về đầu trang
Comet sử dụng kỹ thuật HTTP long-polling
Chế độ long-polling liên quan đến các kỹ thuật để mở một kết nối. Kết nối được mở bởi máy chủ
và ngay khi sự kiện xảy ra thì phản hồi được commit và kết nối được đóng lại. Sau đó, một kết
nối long-polling mới lại ngay lập tức được máy khách mở để đón các sự kiện gửi đến.
Bạn có thể thực hiện HTTP long-polling bằng cách sử dụng các thẻ script hoặc đơn giản chỉ
thông qua một đối tượng XMLHttpRequest.
Các thẻ script
Với các iframe, mục tiêu là nối thêm một thẻ script vào trang của bạn để chạy kịch bản lệnh này.
Máy chủ sẽ: treo kết nối cho đến khi một sự kiện xảy ra, gửi nội dung kịch bản lệnh lại cho trình
duyệt và sau đó mở lại một thẻ script để nhận những sự kiện tiếp theo.
Ưu điểm: Vì dựa trên các thẻ HTML, nên kỹ thuật này rất dễ thực hiện và làm việc trên
các domain (theo mặc định, XMLHttpRequest không cho phép thực hiện yêu cầu trên
domain hoặc các domain con khác).
Nhược điểm: Tương tự như kỹ thuật iframe, thiếu xử lý lỗi và bạn không thể biết trạng
thái hay khả năng để ngắt một kết nối.
Về đầu trang
Long-polling của XMLHttpRequest
Phương pháp Comet thứ hai được khuyên dùng là mở một yêu cầu Ajax đến máy chủ và chờ
phản hồi. Đòi hỏi máy chủ phải có tính năng đặc thù cho phép treo các yêu cầu. Ngay khi sự kiện
xảy ra, máy chủ sẽ gửi lại phản hồi theo yêu cầu bị treo và đóng nó lại, giống như khi bạn đóng
luồng dữ liệu kết quả của một phản hồi servlet. Sau đó máy khách sẽ sử dụng phản hồi đó và mở
một yêu cầu Ajax long-lived mới đến máy chủ, như trong Liệt kê 9:
Liệt kê 9. Mã JavaScript mẫu để thiết lập các yêu cầu long-polling
function long_polling() {
$.getJSON('ajax', function(events) {
processEvents(events);
long_polling();
});
}
long_polling();
Ở tầng sau, mã này cũng sử dụng API Servlet 3 để treo yêu cầu, cũng như HTTP streaming,
nhưng bạn không cần tất cả mã xử lý multi-part. Liệt kê 10 thể hiện ví dụ này.
Liệt kê 10. Treo một yêu cầu Ajax long-polling
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
AsyncContext asyncContext = req.startAsync();
asyncContext.setTimeout(0);
asyncContexts.offer(asyncContext);
}
Khi nhận một sự kiện, chỉ cần thực hiện tất cả các yêu cầu bị treo và hoàn thành chúng, như
trong Liệt kê 11:
Liệt kê 11. Hoàn thành một yêu cầu Ajax long-polling khi một sự kiện xảy ra
while (!asyncContexts.isEmpty()) {
AsyncContext asyncContext = asyncContexts.poll();
HttpServletResponse peer = (HttpServletResponse)
asyncContext.getResponse();
peer.getWriter().write(
new JSONArray().put("At " + new Date()).toString());
peer.setStatus(HttpServletResponse.SC_OK);
peer.setContentType("application/json");
asyncContext.complete();
}
Trong các file mã nguồn đi kèm, thư mục comet-long-polling có một ứng dụng web mẫu về
long-polling mà bạn có thể chạy bằng cách sử dụng lệnh mvn jetty:run.
Ưu điểm: Dễ thực hiện bên phía máy khách với một hệ thống xử lý lỗi và quản lý thời
gian chờ tốt. Kỹ thuật này cũng cho phép khứ hồi giữa các kết nối ở phía máy chủ, do kết
nối không liên tục (đây là một tin mừng khi bạn có rất nhiều máy khách trên ứng dụng
của mình). Nó cũng làm việc trên tất cả các trình duyệt; bạn chỉ cần sử dụng đối tượng
XMLHttpRequest bằng cách gửi một yêu cầu Ajax đơn giản.
Nhược điểm: Không có nhược điểm đáng kể nào so với các kỹ thuật khác. Tuy nhiên,
giống như tất cả các kỹ thuật mà chúng ta đã thảo luận, vẫn có một nhược điểm là dựa
vào một kết nối HTTP không trạng thái, mà nó yêu cầu các tính năng đặc biệt bên phía
máy chủ để có thể treo nó tạm thời.
Về đầu trang
Khuyến cáo
Vì tất cả các trình duyệt hiện đại đều hỗ trợ đặc tả CORS (Cross-Origin Resource Sharing - Chia
sẻ giữa các nguồn tài nguyên gốc), cho phép XHR thực hiện yêu cầu qua các domain, nên nhu
cầu về các kỹ thuật dựa trên kịch bản lệnh và dựa trên khung nội tuyến không được ủng hộ.
Cách tốt nhất để thực hiện và sử dụng Comet với Reverse Ajax là thông qua đối tượng
XMLHttpRequest, cung cấp một xử lý kết nối thực và xử lý lỗi. Nếu cho rằng không phải tất cả
các trình duyệt đều hỗ trợ cờ multi-part và multi-part streaming có thể là một vấn đề về buffer thì
điều quan trọng là bạn sử dụng Comet thông qua kỹ thuật HTTP long-polling với đối tượng
XMLHttpRequest (một yêu cầu Ajax đơn giản bị treo bên phía máy chủ). Tất cả các trình duyệt
hỗ trợ Ajax cũng sẽ hỗ trợ phương pháp này.
Về đầu trang
Kết luận
Bài viết này đã giới thiệu về các kỹ thuật Reverse Ajax. Đã chỉ ra các cách khác nhau để thực
hiện giao tiếp Reverse Ajax và cũng giải thích những ưu và nhược điểm của mỗi cách. Tùy vào
tình huống cụ thể và các yêu cầu trong ứng dụng của bạn sẽ dẫn đến việc lựa chọn phương pháp
nào là tốt nhất. Dù vậy, phương pháp Comet với kỹ thuật Ajax long-polling là cách hay có thể
lựa chọn nếu như bạn muốn các tính năng: giao tiếp với độ trễ thấp; phát hiện lỗi và thời gian
chờ; tính đơn giản; và hỗ trợ tốt từ tất cả các trình duyệt và nền tảng.
Tiếp theo Phần 2 của loạt bài này, bạn sẽ khám phá một kỹ thuật Reverse Ajax thứ ba: đó là
WebSockets. Mặc dù không phải tất cả các trình duyệt đều hỗ trợ nó nhưng chắc chắn
WebSockets sẽ là một phương tiện truyền thông rất tốt cho Reverse Ajax. WebSockets loại bỏ tất
cả các ràng buộc liên quan đến đặc tính không trạng thái (stateless) của một kết nối HTTP. Phần
2 cũng sẽ trình bày các ràng buộc phía máy chủ do các kỹ thuật Comet và WebSocket gây ra.