2012年9月10日 星期一

聊天廣播 - Socket.io

關於網路的架構,Server-Client 這樣的結合己經延用己久,相信大家會了解 HTTP 的協定是 stateless,一但 client 和 server 通訊完,server 就不知道 client 跑去那了?如果 server 有更新想要 Push 給 client,就要反過來從 Client 定期的去向 server 要求。今天要來使用 socket.io 這個 module 就是可以達到 server 和 client 的雙向溝通,只要一建立連線 server 可以一直傳訊息給 client,反過來也行,直接某一方斷線為止。先姑且不論 socket.io 底層是什麼,我們就用一個聊天廣播室來建立這個服務。client 端我們用 HTML 來實現,建立如下的 HTML,命名為 chat.html。
 
<html lang="en"> 
  <head>
  <meta charset="utf-8">
  <title>Chat Room</title> 
  </head>
  <body>
    <div id="output">
    </div>
    <div id="send">
    <input type="text" id="data" size="100" /><br />
    <input type="button" id="sendtext" value="Send Text" />
    </div>
  </body>
</html>
 
這個 HTML 在 Browser 執行會是如下畫面
有一個 Text Field 可以輸入文字然後有一個按鈕
接著加上一些 javascript,在 <head> </head> 中間。如下
 
<head>
<meta charset="utf-8">
<title>Chat Room</title> 
<script src="http://localhost:8124/socket.io/socket.io.js"></script> 
<script>
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme', prompt('Who are you?')); 
});
socket.on('chat',function(username, data) { 
    var p = document.createElement('p'); 
    p.innerHTML = username + ': ' + data;
    document.getElementById('output').appendChild(p); 
});
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', text);
    }, false); 
}, false);

</script>
</head>

一開始這個文件用了 <script src="http://localhost:8124/socket.io/socket.io.js"> 因為要用到 socket.io 的功能,所以要指向 server 的 socket.io.js ,在 chat.html 使用 。
我們一一來看一下。
var socket = io.connect('http://localhost:8124'); 
socket.on('connect', function() {
    socket.emit('addme', prompt('Who are you?')); 
});
這個是要用 io 去連結 server,然後就等待, connect 這個事件,也就是和 server 連上線後,就會觸發。觸發的動作就是發送一個訊息給 server 事件名為 addme,內容為 prompt('Who are you?')。prompt() 的用途是會跳出一個需要輸入的視窗,然後把使用者在這個視窗輸入的值,再經由 socket.emit 傳給 server。接著再來看一下,下一個 socket 事件
socket.on('chat',function(username, data) { 
    var p = document.createElement('p'); 
    p.innerHTML = username + ': ' + data;
    document.getElementById('output').appendChild(p); 
});
在 socket.on 等待 chat 事件,然後 server 會傳兩個值過來分別存在 username 和 data 這兩個變數中,接著更新網頁內容在 <output> 裡多加一個 <p> 內容就是某個 user 打了什麼字,這樣。
在 Client 端最後,我們看到
window.addEventListener('load',function() { 
    document.getElementById('sendtext').addEventListener('click',function() {
        var text = document.getElementById('data').value; 
        socket.emit('sendchat', text);
    }, false); 
}, false);
這個是新增按鈕按下去的觸發動作,主要就是把 TextField 中使用者輸入的文字,抓出來
var text = document.getElementById('data').value; 
然後再利用 socekt.emit 送給 server。這樣 Client 端就完成了。 Server 端也非常之簡單,如下就是全部了,存成 chatServer.js。

var app = require('http').createServer(handler) 
 , io = require('socket.io').listen(app)
 , fs = require('fs');
app.listen(8124);

function handler (req, res) { 
 fs.readFile(__dirname + '/chat.html', function (err, data) {
     if (err) { 
                res.writeHead(500);
         return res.end('Error loading chat.html'); 
            }
     res.writeHead(200);
     res.end(data); 
        });
}
io.sockets.on('connection', function (socket) {
 socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 });
 socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });

 socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
});
一開始需要用到 http,socket.io,fs 等 module 就引用就好了。然後來看一下 handler 這個 function 被用在一開始的 require('http').createServer(handler) 。
function handler (req, res) { 
 fs.readFile(__dirname + '/chat.html', function (err, data) {
     if (err) { 
                res.writeHead(500);
         return res.end('Error loading chat.html'); 
            }
     res.writeHead(200);
     res.end(data); 
        });
}
這個 funciton 主要是說不管 client 端在 Browser 輸入什麼路徑,都是執行這個 function ,無論是輸入
http://127.0.0.1:8124/ashdf;asdlf/has;dfasdf
或是
http://127.0.0.1:8124/
結果都是一樣的,就是把同一個目錄下的 chat.html 讀出來然後送給 Browser, 也就是說,現在 Browser 看到的就是一開始我們寫的 chat.html ,一個 Text Field 和一個按鈕的樣式。
io.sockets.on('connection', function (socket) {
 socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 });
 socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });

 socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
});
接著看到一滿大的 function,就是 io.socket.on 等待 connection 事件, 然後會得到一個 socket 也就是和一個 client 連上線。要注意的是這個 socket 會因為不同的 client 而有不同的記憶體 接著看裡面一點
socket.on('addme',function(username) {
  socket.username = username;
  socket.emit('chat', 'SERVER', 'You have connected'); 
  socket.broadcast.emit('chat', 'SERVER', username + ' is on deck');
 }); 
然後這個 socket 會等待 addme 這個事件,會得到一個 username 再存到 socket.name 保存下來。 然後用 socket.emit 回給連上來的 client,再用 socket.broadcast.emit 廣播給所有在線上其他的client。接著再往下看。
socket.on('sendchat', function(data) { 
  io.sockets.emit('chat', socket.username, data);
 });
這個 socket 等待 sendchat 的事件,然後收到資料後用 io.sockets.emit 廣播給所有線上的 client。說到這裡,我們來看這三個相似的 function
  • socket.emit - 對於一個特定的 socket 傳訊息
  • socket.broadcast.emit - 對於除了目前這個 socket 之外所有線上的 socket 傳訊息
  • io.sockets.emit - 對於所有線上 socket 傳訊息
然後就是最後一個 function 了
socket.on('disconnect', function() {
  io.sockets.emit('chat', 'SERVER', socket.username + ' has left the building');
 });
也就是 socket 等待 disconnect 之後,就廣播給其他 client 說某 socket 離線了。 先執行 node chatServer.js
測試的方法就是用兩個不同的 Browser 試試連看看,這樣就可以互相聊天了。
 原始碼都放在 GitHub
補充說明一下,在這個例子我們都使用 socket.emit() 來傳送訊息,也許讀者有參考到其他教學有用到 socket.send() 這個 function,我們用一個例子來解釋
socket.send("Hi") - 相當於 socket.emit("message", "Hi");
也就是說,socket.send("Hi") 省略了 socket.emit("message", "Hi"); 前面的第一個 "message",而接受的一端就是用

socket.on('message', function (message) {
  console.log(message);
});
來接收 socket.send("Hi") 的訊息。

7 則留言:

  1. 你好,謝謝你的分享,但我有個問題想問,
    我用node.js架起server後,
    在本機可以執行,但將自己的電腦當作server別人卻連不進來,
    不知道要用本機架設server還需要其他步驟嗎?謝謝!

    回覆刪除
  2. 這個是Public IP 的問題,如果您不懂的話,需要請教有架過站的朋友哦。用說的說半天也會搞的很不清楚。

    回覆刪除
  3. 您好,感謝您的分享,有一些問題想請教您
    如果要把聊天的內容記錄下來,例如記錄在一個檔案裡,請問要怎麼做呢?
    謝謝!

    回覆刪除
  4. 還請參考這幾篇文章。http://blog.allenchou.cc/nodejs-tuts-3-use-filesystem/ 和 http://ccckmit.wikidot.com/js:nodefile

    回覆刪除
  5. 請問用 android 當作client的話 server 仍是node.js
    有範例可以介紹嗎

    回覆刪除
  6. 或者用 android 的 webview 載入 網頁IP
    不過該如何修改?

    回覆刪除