學習 NodeJS 第八天:Socket 通訊執行個體,nodejssocket
前言
一般來講,HTTP 是基於文本的“單向”通訊機制。這裡所謂的“單向”,乃相對於“雙向”而言,因為 HTTP 伺服器只需根據請求返還恰當的 HTML 給用戶端即可,不涉及用戶端向服務端的通訊。這種單向的機制比較簡單,對網路品質要求也不高。而更多的情境則是需要可靠、穩定的端到端串連。一般這種服務是即時的、有態的而且是長串連,長串連則暗示兩段須達致相向通訊的能力,也就說是服務端用戶端兩者間能夠即時地相互間通訊。毫無疑問,能夠即時通訊的伺服器正是我們對伺服器基本要求之一。區別於 HTTP 伺服器以 HTTP 為通訊協議, 即時伺服器一般採用較為底層的 TCP/IP 為協議通訊,實現了“套位元組 Socket”的雙向機制。
Socket 是根據博克萊 (U.C.Berkley) 大學早期發展的 Socket 概念寫成的,其設計理念是是將網路傳輸類比成檔案的讀取與寫入 (傳送的動作被視為是寫入/接收的動作被視為是讀取),如此、傳送與接收就簡化為編程人員比較容易懂的 讀取與寫入,降低了網路編程的學習困難度。
聊天室伺服器
聊天室的即時串連基於底層的 TCP 直接連接,為此我們須調用 Node 的 TCP 模組。如果不太熟悉所謂 TCP 網路編程?太底層了是不是?沒關係,我也不熟悉,邊學邊做嘛,只不過千萬不必因為遇到陌生的詞彙而害怕,其實這樣原理並不深奧,而且下面的例子也十分的簡單易懂!咱們就從最簡單的開始吧,下面代碼僅僅十行,它的作用是伺服器向用戶端輸出一段文本,完成 Sever --> Client 的單向通訊。
// Sever --> Client 的單向通訊 var net = require('net'); var chatServer = net.createServer(); chatServer.on('connection', function(client) { client.write('Hi!\n'); // 服務端向用戶端輸出資訊,使用 write() 方法 client.write('Bye!\n'); client.end(); // 服務端結束該次會話 }); chatServer.listen(9000);
用戶端可以是系統內建的 Telnet:
telnet 127.0.0.1 9000
執行 telnet 後,與服務點串連,反饋 Hi! Bye! 的字元,並立刻結束服務端程式終止串連。如果我們要服務端接到到用戶端的資訊?可以監聽 server.data 事件並且不要中止串連(否則會立刻結束無法接受來自用戶端的訊息):
// 在前者的基礎上,實現 Client --> Sever 的通訊,如此一來便是雙向通訊 var net = require('net'); var chatServer = net.createServer(), clientList = []; chatServer.on('connection', function(client) { // JS 可以為對象自由添加屬性。這裡我們添加一個 name 的自訂屬性,用於表示哪個用戶端(用戶端的地址+連接埠為依據) client.name = client.remoteAddress + ':' + client.remotePort; client.write('Hi ' + client.name + '!\n'); clientList.push(client); client.on('data', function(data) { broadcast(data, client);// 接受來自用戶端的資訊 }); }); function broadcast(message, client) { for(var i=0;i<clientList.length;i+=1) { if(client !== clientList[i]) { clientList[i].write(client.name + " says " + message); } } } chatServer.listen(9000);
這裡要說明一下的是,不不同作業系統對連接埠範圍的限制不一樣,有可能是隨機的。
那麼上面是不是一個完整功能的代碼呢?我們說還有一個問題沒有考慮進去:那就是一旦某個用戶端退出,卻仍保留在 clientList 裡面,這明顯是一個null 指標(NullPoint)。如果是在這樣的話我們寫程式太脆弱了,能不能更健壯一些?——請接著看。
首先我們簡單地把 client 從數組 clientList 中移除掉。完成這工作一點都不困難。Node TCP API 已經為我們提供了 end 事件,即用戶端中止與服務端串連的時候發生。移除 client 對象的代碼如下:
chatServer.on('connection', function(client) { client.name = client.remoteAddress + ':' + client.remotePort client.write('Hi ' + client.name + '!\n'); clientList.push(client) client.on('data', function(data) { broadcast(data, client) }) client.on('end', function() { clientList.splice(clientList.indexOf(client), 1); // 刪除數組中的制定元素。這是 JS 基本功哦~ }) })
但是我們還不敢說上述代碼很健壯,因為一旦 end 沒有被觸發,異常仍然存在著。下面我們看看解決之道:重寫 broadcast():
function broadcast(message, client) { var cleanup = [] for(var i=0;i<clientList.length;i+=1) { if(client !== clientList[i]) { if(clientList[i].writable) { // 先檢查 sockets 是否可寫 clientList[i].write(client.name + " says " + message) } else { cleanup.push(clientList[i]) // 如果不可寫,收集起來銷毀。銷毀之前要 Socket.destroy() 用 API 的方法銷毀。 clientList[i].destroy() } } } //Remove dead Nodes out of write loop to avoid trashing loop index for(i=0;i<cleanup.length;i+=1) { clientList.splice(clientList.indexOf(cleanup[i]), 1) } }
TCP API 中還提供一個 error 事件,用於捕捉用戶端的異常:
client.on('error', function(e) { console.log(e); });
Node 網路編程的 API 還豐富,此次僅僅是個入門,更多的內容請接著看,關於瀏覽器 Socket 應用。
Socket.IO
前面說到,瀏覽器雖然也屬於用戶端的一種,但僅支援“單工”的 HTTP 通訊。有見及此,HTML5 新規範中推出了基於瀏覽器的 WebSocket,開發了底層的介面,允許我們能進行 更強大的操作,超越以往的 XHR。
如第一個例子那般,我們無須第三方架構就可以直接與 Node TCP 伺服器 進行 Socket 通訊。
但我們又要認清一個事實,不是每個瀏覽器都可以順利支援 WebSocket 的。於是 Socket.IO (http://socket.io)出現了,它提供了不支援 WebSocket 時候的降級支援,同時使得一些舊版本的瀏覽器也可以“全雙工系統”地工作。優先使用的順序如下:
- WebSocket
- Socket over Flash API
- XHR Polling 長串連
- XHR Multipart Streaming
- Forever Iframe
- JSONP Polling
經過封裝,我們可以不探究用戶端使用上述哪一種技術達致“全雙工系統”;而我們編寫代碼時,亦無論考慮哪种放法,因為 Socket.IO 給我們的 API 只有一套。瞭解 Socket.IO 其用法就可以了。
先在瀏覽器部署 Socket.IO 的前端代碼:
<!DOCTYPE html> <html> <body> <script src="/socket.io/socket.io.js"></script> <script> var socket = io.connect('http://localhost:8080'); // 當服務端發送一條訊息到用戶端,message 事件即被觸發。我們把訊息在控制台列印出來 socket.on('message', function(data){ console.log(data) }) </script> </body> </html>
服務端 Node 代碼:
var http = require('http'), io = require('socket.io'), fs = require('fs'); // 雖然我們這裡使用了同步的方法,那會阻塞 Node 的事件迴圈,但是這是合理的,因為 readFileSync() 在程式周期中只執行一次,而且更重要的是,同步方法能夠避免非同步方法呼叫所帶來的“與 SocketIO 之間額外同步的問題”。當 HTML 檔案讀取完畢,而且伺服器準備好之後,如此按照順序去執行就能讓用戶端馬上得到 HTML 內容。 var sockFile = fs.readFileSync('socket.html'); // Socket 伺服器還是構建於 HTTP 伺服器之上,因此先調用 http.createServer() server = http.createServer(); server.on('request', function(req, res){ // 一般 HTTP 輸出的格式 res.writeHead(200, {'content-type': 'text/html'}); res.end(sockFile); }); server.listen(8080); var socket = io.listen(server); // 交由 Socket.io 接管 // Socket.io 真正的串連事件 socket.on('connection', function(client){ console.log('Client connected'); client.send('Welcome client ' + client.sessionId); // 向用戶端發送文本 });
當用戶端串連時,服務端會同時出發兩個事件:server.onRequest 和 Socket.onConnection。它們之間有什麼區別呢?區別在於 Socket 的是持久性的。
多個 Socket 串連,先是用戶端代碼:
<!DOCTYPE html> <html> <body> <script src="/socket.io/socket.io.js"></script> <script> var upandrunning = io.connect('http://localhost:8080/upandrunning'); var weather = io.connect('http://localhost:8080/weather'); upandrunning.on('message', function(data){ document.write('<br /><br />Node: Up and Running Update<br />'); document.write(data); }); weather.on('message', function(data){ document.write('<br /><br />Weather Update<br />'); document.write(data); }); </script> </body> </html>
服務端代碼:
var sockFile = fs.readFileSync('socket.html'); server = http.createServer(); server.on('request', function(req, res){ res.writeHead(200, {'content-type': 'text/html'}); res.end(sockFile); }); server.listen(8080); var socket = io.listen(server); socket.of('/upandrunning') .on('connection', function(client){ console.log('Client connected to Up and Running namespace.'); client.send("Welcome to 'Up and Running'"); }); socket.of('/weather') .on('connection', function(client){ console.log('Client connected to Weather namespace.'); client.send("Welcome to 'Weather Updates'"); });
如上代碼,我們可以劃分多個命名空間,分別是 upandrunning 和 weather。
關於 Express 中使用 Soclet.io,可以參考《Node:Up and Ruuning》一書的 7.2.2 小節。
今晚時間的關係,涉及 Socket.io 許多方面還沒有談,容小弟我日後再瞭解。
以上就是本文的全部內容,希望對大家的學習有所協助,也希望大家多多支援幫客之家。