場景:後端更新數據推送到客戶端(Java部分使用Tomcat服務器)。
後端推送數據的解決方案有很多,比如輪詢、Comet、WebSocket。
1. 輪詢對於後端來說開發成本最低,就是按照傳統的方式處理Ajax請求並返回數據,在學校的時候實驗室的項目一直都采用輪詢,因為它最保險也最容易實現。但輪詢帶來的通信資源的浪費是無法忽視的,無論數據是否改變,都照常發送請求並響應,而且每次HTTP請求都帶有很長的頭部信息。
2. Comet的概念是長連接,客戶端發送請求後,後端將連接保持下來,直到連接超時或後端返回數據時再重新建立連接,有效的將通信資源轉移到了服務器上,實際消耗的是服務器資源。
3. WebSocket是HTML5提供的一種全雙工通信技術,通過“握手”實現客戶端與服務器之間的通信,實時性好,攜帶的頭部也較小,目前支持的浏覽器如下:
理想的情況是采取WebSocket與Comet結合的方式,對IE8等浏覽器采取Comet方式,做降級處理。但是這樣一來,後端需要實現兩種處理請求的邏輯,即WebSocket與Comet。所以,本文加入Node.js,之所以這樣做,是將處理WebSocket(或Comet)的邏輯轉移到Node.js部分,不給後端“添麻煩”,因為在實際情況下,前端開發人員推動後端開發人員並不容易。Node.js作為浏覽器與Java業務邏輯層通信的中間層,連接客戶端與Tomcat,通過Socket與Tomcat進行通信(是Socket,不是WebSocket,後端需要實現Socket接口。
在客戶端,WebSocket與Comet通過Socket.io實現,Socket.io會針對不同的浏覽器版本或者不同客戶端選擇合適的實現方式(WebSocket, long pull..),Socket.io的引入讓處理WebSocket(或長連接)變的很容易。Socket.io
客戶端引入socket.io:
<script src="static/js/socket.io.js"></script>
客戶端JavaScript代碼:
var socket = io.connect('127.0.0.1:8181'); // 發送數據至服務器 socket.emit('fromWebClient', jsonData); // 從服務器接收數據 socket.on('pushToWebClient', function (data) { // do sth. });
Node.js服務器代碼:
var http = require('http'), app = http.createServer().listen('8181'), io = require('socket.io').listen(app); io.sockets.on('connection', function (socketIO) { // 從客戶端接收數據 socketIO.on('fromWebClient', function (webClientData) { // do sth. }); // 客戶端斷開連接 socketIO.on('disconnect', function () { console.log('DISCONNECTED FROM CLIENT'); }); // 向客戶端發送數據 socketIO.emit('pushToWebClient', jsonData); });
建立好客戶端同Node.js服務器的連接只是第一步,下面還需要建立Node.js服務器與Java業務邏輯層的聯系。這時,Node.js服務器則作為客戶端,向Tomcat發送TCP連接請求。連接成功後,Node.js服務器和Tomcat建立了一條全雙工的通道,而且是唯一的一條,不論有多少個客戶端請求,都從Node.js服務器轉發至Tomcat;同樣,Tomcat推送過來的數據,也經由Node.js服務器分發至各個客戶端。
這裡存在一個問題,就是在WebSocket連接與Socket連接都建立好之後,兩次連接彼此之間是屏蔽的。Tomcat不知道是哪次WebSocket連接發送過來的數據,也不知道是哪個客戶端發來的數據。當然,Node.js可以利用session id發送至Tomcat來標識是哪一個客戶端,但本文采用的是另外一種辦法。
客戶端同Node.js建立WebSocket連接時,每個連接都會包含一個實例,這裡稱它為socketIO。每個socketIO都有一個id屬性用來唯一標識這個連接,這裡稱它為socket_id。利用socket_id,在Node.js服務器建立一個映射表,存儲每一個socketIO與socket_id的映射關系。Node.js服務器發送數據給Tomcat時帶上這個socket_id,再由Java部分進行一系列處理以後封裝好每個客戶端需要的不同數據一並返回,返回的數據裡要有與socket_id的對應關系。這樣,Node.js服務器收到Tomcat發來的數據時,通過前面提到的映射表由不同的socketIO分發至不同的客戶端。
Node.js服務器代碼:
var http = require('http'), net = require('net'), app = http.createServer().listen('8181'), io = require('socket.io').listen(app), nodeServer = new net.Socket(); // 連接到Tomcat nodeServer.connect(8007, '127.0.0.1', function() { console.log('CONNECTED'); }); // 存儲客戶端的WebSocket連接實例 var aSocket = {}; // 同客戶端建立連接 io.sockets.on('connection', function (socketIO) { // 從客戶端接收數據,然後發送至Tomcat socketIO.on('fromWebClient', function (webClientData) { // 存儲至映射表 aSocket[socketIO.id] = socketIO; // 發送至Tomcat的數據中添加socket_id webClientData['sid'] = socketIO.id; // 發送String類型的數據至Tomcat nodeServer.write(JSON.stringify(webClientData)); }); // 客戶端斷開連接 socketIO.on('disconnect', function () { console.log('DISCONNECTED FROM CLIENT'); }); }); // 從Tomcat接收數據 nodeServer.on('data', function (data) { var jsonData = JSON.parse(data.toString()); // 分發數據至客戶端 for (var i in jsonData.list) { aSocket[jsonData.list[i]['sid']].emit('pushToWebClient', jsonData.list[i].data); } });
上面的代碼省略了一些邏輯,比如Node.js服務器從Tomcat接收的數據分為兩種,一種是推送過來的數據,另外一種是響應請求的數據,這裡統一處理推送過來的數據。
在處理通信時,Node.js發送至Tomcat的數據是String格式,而從Tomcat接收的數據為Buffer對象(8進制),需要轉化為String之後再轉化為json發送至客戶端。
本文只是給出一個這樣兩次連接的簡單例子,具體的業務中需要加入許多東西。既然在項目中引入了Node.js,就需要前端承擔更多的事情,比如對數據的處理、緩存、甚至加入很多業務邏輯。