編輯室臺大首頁計中首頁
第0029期 • 2014.06.20發行
ISSN 2077-8813
歷史回顧 訂閱/取消 校務服務 專題報導   技術論壇 推薦刊物
首頁 > 技術論壇
技術論壇

透過 socket.io來建立人物移動聊天室

作者:楊德倫 / 臺灣大學計算機及資訊網路中心教學研究組幹事

傳統網頁聊天室的製作方式,是以特定週期的網頁自動更新來達到重新載入文字內容的效果,然而訊息的交換與接收,卻非以即時 的方式呈現,增加不必要的等待時間。晚近發展出的 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>
<html>
<head>
<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;
           }
           else
           {
                     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();
           }
           else
           {
                     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>

<body>
           <div class='div_scene'></div>
           <div style='position: absolute; top: 790px; left: 10px;'>
                     請輸入訊息:<input type='text' id='txt_type' style='width: 400px;' value='' />
           </div>
           <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;
           else
                     str += '-0' + today_month;

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

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

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

           var today_second = today.getSeconds();
           if (today_second >= 10)
                     str += ':' + today_second;
           else
                     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 臺北市羅斯福路四段一號
建議最佳螢幕解析度 1024*768