傳統網頁聊天室的製作方式,是以特定週期的網頁自動更新來達到重新載入文字內容的效果,然而訊息的交換與接收,卻非以即時
的方式呈現,增加不必要的等待時間。晚近發展出的 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 臺北市羅斯福路四段一號