傳統網頁聊天室的製作方式,是以特定週期的網頁自動更新來達到重新載入文字內容的效果,然而訊息的交換與接收,卻非以即時 的方式呈現,增加不必要的等待時間。晚近發展出的 WebSocket通訊協定,將過去等待特定時間來刷新頁面的方式,改為以即時(Real Time)的內容呈現來完成網頁聊天室的效果。本案例中,將以 Socket.io來協助建立有人物移動的聊天室。

Socket.io為一種 Javascript的 Library,它分成兩部分:執行於用戶端的 library,以及透過 node.js(為一伺服器、後端的 JavaScript函式庫集合,可使 javascript程式執行於瀏覽器之外的環境)來應用於伺服端的 library,兩者擁有相似的 API,並且以「事件」(Event)來進行訊息的接收與傳遞。它模糊了不同傳輸機制間(如瀏覽器和行動裝置)的差異,同時亦達成即時通訊的要求。

圖(一)Socket.io網頁的圖例

在此案例中,測試平台為 Ubuntu 14.04 LTS,以 Virtual Machine來建置 Web server,防火牆開啟 80、8080埠。操作過程將以快速建置環境的方式來進行說明;相關理論基礎以及非核心技術,請另行學習和研究。

圖(二)Linux版本相關訊息,在此為 Ubuntu 14.04

安裝 npm – node package manager
npm是一個 Node JavaScript平台的套件管理工具,它會下載指定的模組(如 socket.io),並將模組放置在 node可以尋找和管理的地方,並智慧地處理模組間的相依性和衝突。我們可透過下面的指令來安裝 npm:「sudo apt-get install npm」

配置網頁資料夾和安裝 socket.io
此案例中,我們設定資料夾名稱為 chatroom, images當中放置了 roles資料夾,皆為自製角色人物圖片;node_modules當中為 socket.io模組,請於 chatroom路徑中,請透過下面的指令來下載:「sudo apt-get npm socket.io」。npm 套件管理器將會自動安裝 socket.io 模組,並配置於 chatroom當中;app.js為我們要執行的檔案,透過 nodejs指令來加以執行;index.html是我們人物移動的展示頁面,並由後端的javascript 程式(app.js)加以呼叫。

圖(三)案例資料夾檔案配置

圖(四)自行製作的人物圖片

圖(五)安裝 socket.io的過程

圖(六)安裝完 socket.io後的結構

當環境配置好後,我們需要透過下面的指令,來運行程式:「sudo nodejs app.js」,此時我們打開瀏覽器,在網址列輸入 http://192.168.56.100/chatroom,將會跳出輸入名稱的視窗,而後即可與該網頁中所有成員互動。

圖(七)進入網頁後,會要求你輸入名稱

圖(八)可輸入訊息,程式會將訊息廣播給線上所有成員知道

圖(九)可在不同瀏覽器中互動,若為 Public IP,即可與全世界互動

圖(十)執行程式時,背景運作過程

socket.emit - 對一個特定的 socket傳訊息
socket.on – 對特定 event的運行結果進行接收
socket.broadcast.emit - 對目前 socket之外所有線上的 socket傳訊息
io.sockets.emit - 對所有線上 socket傳訊息

程式碼分享
index.html

<!DOCTYPE html>
<meta charset= "UTF-8" >
<title>人物移動聊天室</title>

<!-- javascript -->
<script src= 'http://code.jquery.com/jquery-1.11.1.min.js' ></script>
<script src= 'http://code.jquery.com/ui/1.10.4/jquery-ui.min.js' ></script>
<script src= 'node_modules/socket.io/node_modules/socket.io-client/dist/socket.io.min.js' ></script>

<style>
.div_container
position: absolute ;

.div_role
position: absolute ;
top: 0px ;
left: 0px ;
width: 31.5px ;
height: 48px ;
overflow: hidden ;

.div_role img
position: absolute ;
top: 0px ;
left: 0px ;

.div_scene
position: absolute ;
top: 10px ;
left: 10px ;
width: 1024px ;
height: 768px ;
border: #000000 2px solid ;
</style>

<script>

//設定圖片座標時的編號
var imgHorizontalNum = 0;
var imgVerticalNum = 0;

//設定人物換圖的定時器
var timer;

//人物被點選的座標
var current_x = 0;
var current_y = 0;

//人物被點選前的舊座標
var old_x = 0;
var old_y = 0;

//我們的 ID,給程式指定人物元件(div)用
var myID = '';

//顯示在網頁上的人物名稱
var myName = '';

//前端接收 server 上 的 socket 訊息
var socket;

$(document).ready( function (){
//人物換圖的間距時間
timer = setInterval(setAction, 300);

//透過 web server 的 8080 port 來進行連線
socket = io.connect('http://192.168.56.100:8080/');
//告訴server您的名字
socket.on('connect', function () {
socket.emit('check_login', prompt('貴姓大名?'));

//新增自己的角色 (server回傳您的名稱,再寫到網頁上)
socket.on('add_new_user_myself', function (obj) {
myID = obj.new_user_id;
myName = obj.new_user_name;
var html = "<div class='div_container' id='role_" + obj.new_user_id + "'><div id='myMsg_" + obj.new_user_id + "' style='position: absolute; top: -35px; width: 500px;'></div><div class='div_role' id='myRole_" + obj.new_user_id + "'><img src='./images/roles/a" + getRandRoleImg() + ".png' /></div></div>";
$('.div_scene').append(html);

//加入其他使用者
socket.on('add_new_user', function (obj){
var html = "<div class='div_container' id='role_" + obj.new_user_id + "'><div id='myMsg_" + obj.new_user_id + "' style='position: absolute; top: -35px; width: 500px;'></div><div class='div_role' id='myRole_" + obj.new_user_id + "'><img src='./images/roles/a" + getRandRoleImg() + ".png' /></div></div>";
$('.div_scene').append(html);

//告訴別人自己在哪裡(因為使用者上線時,場境是無人狀態,所以要告訴新上線的人)
socket.emit('feedback_other_exist', {
id: myID,
name: myName,
new_user_id: obj.new_user_id

//將他人角色的位置加以移動
socket.on('feedback_user_position', function (obj) {
//人物移動
$(".div_container[id=role_" + obj.otherID + "]").animate({
'left': (obj.left - 35) + 'px',
'top': (obj.top - 35) + 'px'
{duration: 2000});

//置換圖片
$('#myRole_' + obj.otherID + ' img').css({
'top': obj.imgV * - 48 + 'px'

//通知最後進聊天室的人,我在哪裡
socket.on('feedback_where_I_am', function (obj){
if ( myID == obj.new_user_id )
var html = "<div class='div_container' id='role_" + obj.id + "'><div id='myMsg_" + obj.id + "' style='position: absolute; top: -35px; width: 500px;'></div><div class='div_role' id='myRole_" + obj.id + "'><img src='./images/roles/a" + getRandRoleImg() + ".png' /></div></div>";
$('.div_scene').append(html);

//回傳使用者所傳出的訊息(*重要)
socket.on('return_msg', function (obj) {
$('#myMsg_' + obj.id).html( '[' + obj.time + '] ' + obj.name + ' 說: ' + obj.msg);
//按下Enter時,送出文字
$('#txt_type').on('keypress', function (e) {
if (e.keyCode == 13)
e.preventDefault();
socket.emit( 'send_msg', {
id: myID,
name: myName,
msg: $('#txt_type').val()
$('#txt_type').val('');

//離開時的訊息
socket.on('leave_msg', function (obj) {
$('#role_' + obj.id).remove();
alert( obj.name + ' 已離開聊天室' );

$(document).on('mousemove', '.div_scene', function (event) {
current_x = event.pageX;
current_y = event.pageY;

$('#myInfo').html(
'目前滑鼠座標:<br />' +
'top:' + current_x + '<br />' +
'left:' + current_y + '<br />' +
'人物座標:<br />' +
'top: ' + old_x + ' <br />' +
'left: ' + old_y );

$(document).on('click', '.div_scene', function (event){
//置換人物方向的圖片編號(縱軸)
if ( current_x > old_x )
imgVerticalNum = 2;
else if ( current_x < old_x )
imgVerticalNum = 1;
if ( current_y >= old_y )
imgVerticalNum = 0;
else if ( current_y < old_y)
imgVerticalNum = 3;

//將人物移動的座標,設定成舊座標,以利未來新座標與之比對
old_x = current_x;
old_y = current_y;

//告訴其他人,你的移動位置
socket.emit('other_user_position', {
id: myID,
left: current_x,
top: current_y,
imgV: imgVerticalNum
//人物移動
$(".div_container[id=role_" + myID + "]").animate({
'left': (current_x - 35) + 'px',
'top': (current_y - 35) + 'px'
{duration: 2000});

//置換圖片
$('#myRole_' + myID + ' img').css({
'top': imgVerticalNum * - 48 + 'px'

//取得隨機人物圖案
function getRandRoleImg()
//人物圖片編號(min = 第一張;max = 最後一張)。
//未來這裡可以改成「登入時選擇」
var min = 1;
var max = 4;

//取得隨機人物圖片編號
var num = Math.floor( Math.random() * (max - min + 1) ) + min;

//在此範例中,因為人物圖片是01 ~ 04,所以不足 10 要補 0 在字串前面
if ( num >= 10 )
num = num.toString();
num = '0' + num.toString();

return num;

//人物原地連續動作
function setAction()
//共四個動作,故從0開始,到2就是最後一張的範圍。若是超過,就到第一個圖示
if ( imgHorizontalNum > 3 ) imgHorizontalNum = 0;

//切換圖片
$('.div_container img').css({
'left': imgHorizontalNum * - 31.5 + 'px'
imgHorizontalNum++;
</script>

</head>

<div class= 'div_scene' ></div>
<div style='position: absolute ; top: 790px ; left: 10px ;'>
請輸入訊息:<input type= 'text' id= 'txt_type' style='width: 400px ;' value= '' />
<div style='position: absolute ; top: 820px ; left: 10px ;' id= 'myInfo' ></div>
</body>

</html>

app.js

//在此設定 IP 為 192.168.56.100,開啟 8080 port
var server = require('http').createServer(handler),
ip = "192.168.56.100",
port = 8080,
url = require('url'),
fs = require('fs'),
si = require('socket.io');

server.listen(port, ip);

//啟動網頁時所執行的函式
function handler(req, res) {
//讀取 index.html 網頁
fs.readFile('./index.html', function (err, data) {
//若讀取錯誤,就回傳 http 代碼為 500 的訊息(Internal Server Error)
if (err) {
res.writeHead(500);
return res.end('Error loading index.html');
//若無任何錯誤,即回傳 http 代碼 200(OK, The request has succeeded)
res.writeHead(200);
res.end(data);

var io = si.listen(server);

io.sockets.on('connection', function (socket)
//剛進入聊天室的連線回傳
socket.on('check_login', function (username) {
//自己上線(創造隨機 ID 給前端,讓自己的人物可以被程式辨識)
var numID = Math.random();
numID = numID.toString();
numID = numID.replace(".", "");
socket.username = username;
socket.userid = numID;
//新增自己的角色
socket.emit('add_new_user_myself', {
new_user_id: numID,
new_user_name: username
//告訴別人自己上線
socket.broadcast.emit('add_new_user', {
new_user_id: numID,
new_user_name: username
//你的移動位置
socket.on('other_user_position', function (data){
//告訴別人你的位置
socket.broadcast.emit('feedback_user_position', {
otherID: data.id,
left: data.left,
top: data.top,
imgV: data.imgV
//告訴新上線的人,我在哪裡
socket.on('feedback_other_exist', function (data){
//告訴別人你的位置
socket.broadcast.emit('feedback_where_I_am', {
id: data.id,
name: data.name,
new_user_id: data.new_user_id
// 回傳前端所丟之訊息
socket.on('send_msg', function (data) {
//自己說了什麼
socket.emit('return_msg', {
id: data.id,
name: data.name,
msg: data.msg,
time: getTodayDate()
//告訴別人我說了什麼
socket.broadcast.emit('return_msg', {
id: data.id,
name: data.name,
msg: data.msg,
time: getTodayDate()
//離開聊天室
socket.on('disconnect', function () {
socket.broadcast.emit('leave_msg', {
id: socket.userid,
name: socket.username

//在伺服器的 command line 畫面,秀出相關訊息
console.log("Server running at http://" + ip + ":" + port + "/");

//取得今天的日期(ISO 8601),讓使用者送出訊息時參考用
function getTodayDate() {
var str = '';

// 宣告日期物件
var today = new Date();

var today_year = today.getFullYear();
str += today_year;

var today_month = today.getMonth() + 1;
if (today_month >= 10)
str += '-' + today_month;
str += '-0' + today_month;

var today_date = today.getDate();
if (today_date >= 10)
str += '-' + today_date;
str += '-0' + today_date;

var today_hour = today.getHours();
if (today_hour >= 10)
str += ' ' + today_hour;
str += ' 0' + today_hour;

var today_minute = today.getMinutes();
if (today_minute >= 10)
str += ':' + today_minute;
str += ':0' + today_minute;

var today_second = today.getSeconds();
if (today_second >= 10)
str += ':' + today_second;
str += ':0' + today_second;

return str;
socket.io的出現,為 web開發者提供了很好的設計模組,線上通訊不須再花時間等待網頁刷新,即可馬上獲取網頁聊天室中的訊息,達到即時通訊的效果。

[1] Socket.io官網
http://socket.io/
[2] 線上人物自製工具
http://www.geocities.jp/kurororo4/looseleaf/
[3] 聊天廣播 - Socket.io功能介紹與解說
http://iosdevelopersnote.blogspot.tw/2012/09/socketio.html

版權所有 © 國立台灣大學計算機及資訊網路中心 AllRights Reserved.
電話:02-33665022 或 3366-5023 傳真: 02-23637204
讀者意見信箱: ntuccepaper@ntu.edu.tw 地址:10617 臺北市羅斯福路四段一號