搭建Websocket簡易聊天室

本文,我們通過Egret和Node.js實現一個線上聊天室的demo。主要包括:聊天,改使用者名稱,檢視其他使用者線上狀態的功能。大致流程為,使用者訪問網頁,即進入聊天狀態,成為新遊客,通過底部的輸入框,可以輸入自己想說的話,點擊發布,資訊呈現給所有在聊天的人的頁面。使用者可以實時修改自己的暱稱,使用者離線上線都會實時廣播給其他使用者。

體驗連結 http://7hds.com:8888/

下圖為最終制作完成的聊天面板

搭建Websocket簡易聊天室
搭建Websocket簡易聊天室

WebSocket伺服器可以用其他語言編寫,本文采用的方法建立在Node.js上 。

在Node.js中我們使用ws第三方模組來實現伺服器業務邏輯的快速搭建,還需使用uuid模組生成隨機id,你需要使用npm包管理器來安裝ws、uuid模組。使用以下命令:

npm install ws -g
npm install uuid -g

安裝完成之後,使用終端工具進入伺服器目錄,開始編寫程式碼:

//引入ws模組
var WebSocket = require('ws');
//建立websocket服務,埠port為:****
var WebSocketServer = WebSocket.Server,
    wss = new WebSocketServer({port: 8180});
//引入uuid模組
var uuid = require('node-uuid');
//定義一個空陣列,存放客戶端的資訊 
var clients = [];
//定義傳送訊息方法wsSend
//引數為 type:型別
//client_uuid:隨機生成的客戶端id
//nickname:暱稱
//message:訊息
//clientcount:客戶端個數
function wsSend(type, client_uuid, nickname, message,clientcount) {
    //遍歷客戶端
  for(var i=0; i<clients.length; i++) {
      //宣告客戶端
    var clientSocket = clients[i].ws;
    if(clientSocket.readyState === WebSocket.OPEN) {
        //客戶端傳送處理過的資訊
      clientSocket.send(JSON.stringify({
        "type": type,
        "id": client_uuid,
        "nickname": nickname,
        "message": message,
        "clientcount":clientcount,
      }));
    }
  }
}
//宣告客戶端index預設為1 
var clientIndex = 1;
//服務端連線
wss.on('connection', function(ws) {
//客戶端client_uuid隨機生成
  var client_uuid = uuid.v4();
  //暱稱為遊客+客戶端index
  var nickname = "遊客"+clientIndex;
  //client++
  clientIndex+=1;
  //將新連線的客戶端push到clients陣列中
  clients.push({"id": client_uuid, "ws": ws, "nickname": nickname});
  //控制檯列印連線的client_uuid
  console.log('client [%s] connected', client_uuid);
 //宣告連線資訊為 暱稱+來了
  // var connect_message = nickname + " 來了";
  var connect_message =  " 來了";

  //伺服器廣播資訊 ***來了
  wsSend("notification", client_uuid, nickname, connect_message,clients.length);
 //當用戶傳送訊息時
  ws.on('message', function(message) {
      // 使用者輸入"/nick"的話為重新命名訊息
    if(message.indexOf('/nick') === 0) {
      var nickname_array = message.split(' ');
      if(nickname_array.length >= 2) {
        var old_nickname = nickname;
        nickname = nickname_array[1];
        var nickname_message = "使用者 " + old_nickname + " 改名為: " + nickname;
        wsSend("nick_update", client_uuid, nickname, nickname_message,clients.length);
      }
    }//傳送訊息 
    else {
      wsSend("message", client_uuid, nickname, message,clients.length);
    }
  });
 //關閉socket連線時
  var closeSocket = function(customMessage) {
      //遍歷客戶端
    for(var i=0; i<clients.length; i++) {
        //如果客戶端存在
        if(clients[i].id == client_uuid) {
            // 宣告離開資訊
            var disconnect_message;
            if(customMessage) {
                disconnect_message = customMessage;
            } else {
                disconnect_message = nickname + " 走了";
            }
         //客戶端陣列中刪掉
          clients.splice(i, 1);
          //服務廣播訊息
          wsSend("notification", client_uuid, nickname, disconnect_message,clients.length);
        }
    }
  }
  ws.on('close', function() {
      closeSocket();
  });

  process.on('SIGINT', function() {
      console.log("Closing things");
      closeSocket('Server has disconnected');
      process.exit();
  });
});

伺服器端主要是接收資訊,判斷是聊天資訊還是重新命名資訊,然後傳送廣播。同時,當用戶連線上伺服器端或者關閉連線時,伺服器也會發送廣播通知其他使用者。

我們封裝了wsSend函式用來處理訊息的廣播。對每個連線的使用者,我們預設給他分配為遊客。為了實現廣播,我們用clients陣列來儲存連線的使用者。

將編寫好的文件儲存為server.js,在終端工具中,使用node server.js來啟動你剛剛編寫的伺服器。如果終端沒有報錯,證明你的程式碼已經正常執行。

在實際專案中,伺服器邏輯遠遠比此示例複雜得多。伺服器端完成後,再來編寫客戶端程式碼。

介面非常簡單,我們通過兩張圖片來實現介面效果,首先建立我們的聊天介面,此專案中為了方便我們使用EUI進行快速開發。如下圖:

搭建Websocket簡易聊天室
搭建Websocket簡易聊天室

首先建立一個Image來放置我們的背景圖。

建立三個Label物件,一個作為title:“多人線上聊天室”,一個作為提示:“當前線上人數”,還有一個id為lb_online的作為線上人數顯示文字。

建立一個EditableText物件id為input_msg作為訊息傳送輸入框,使用者可以在此輸入訊息進行傳送。

建立一個Button物件id為btn_ok,點選按鈕可以執行傳送訊息動作。

建立介面的操作和WebSocket物件建立動作在同時進行,在init方法中建立WebSocket物件,並執行伺服器連線操作,程式碼如下:

public ws;
    private init() {
        /**WebSocket連線 */
        this.ws = new WebSocket('ws://127.0.01:8180');
        this.ws.onopen = function (e) {
            console.log('Connection to server opened');
        }
    }

由於伺服器開放了8180埠,我們也需要使用8180埠進行連線。當連線成功,可執行onopen方法。

伺服器連線成功了,在控制檯列印 'Connection to server opened'。

onmessage方法中讀取伺服器傳遞過來的資料,並通過appendLog方法將資料顯示在對應的文本里,

使用newLabel方法並將一條新訊息插入到訊息框中。

private init() {
        /**WebSocket連線 */
        this.ws = new WebSocket('ws://127.0.01:8180');
        this.ws.onopen = function (e) {
            console.log('Connection to server opened');
        }
        /**暱稱 */
        var nickname;
        var self = this;
        this.ws.onmessage = function (e) {
            var data = JSON.parse(e.data);
            nickname = data.nickname;
            appendLog(data.type, data.nickname, data.message, data.clientcount);
            console.log("ID: [%s] = %s", data.id, data.message);
            //插入訊息
            self.group_msg.addChild(self.newLabel(data.nickname, data.message))
        }
        function appendLog(type, nickname, message, clientcount) {
            console.log(clientcount)
            /**聊天資訊 */
            var messages = this.list_msg;
            /**提示 */
            var preface_label;
            if (type === 'notification') {
                preface_label = "提示:";
            } else if (type === 'nick_update') {
                preface_label = "警告:";
            } else {
                preface_label = nickname;
            }
            self.preface_label = preface_label;
            var message_text = self.message_text = message;
            /**線上人數 */
            self.lb_online.text = clientcount;
        }
        /**點選OK傳送 */
        this.btn_ok.addEventListener(egret.TouchEvent.TOUCH_TAP, this.sendMessage, this);
    }
    private newLabel(name: string, msg: string) {
          var label1: eui.Label = new eui.Label();
          label1.text = name + ":" + msg;
          label1.textColor = 0x000000
          return label1;
    }

最後我們來編寫傳送訊息的函式,在btn_ok中egret.TouchEvent.TOUCH_TAP點選之後的相應函式為sendMessage方法。

/**傳送訊息 */
    private sendMessage() {
        var message = this.input_msg.text;
        if (message.length < 1) {
            // console.log("不能傳送空內容!");
            return;
        }
        this.ws.send(message);
        /**清空輸入框內容 */
        this.input_msg.text = "";
    }

如果輸入框中內容不為空的話就將資料通過 this.ws.send(message); 傳送給伺服器,並清除輸入框的內容。

最終執行後,我們就可以實現多人線上聊天功能了。

完整版程式碼如下:

class Chat extends eui.Component implements eui.UIComponent {
    /**線上人數文字 */
    public lb_online: eui.Label;
    /**聊天視窗 */
    public scr_msg: eui.Scroller;
    /**聊天資訊 */
    public list_msg: eui.List;
    /**輸入框 */
    public input_msg: eui.EditableText;
    /**確定按鈕 */
    public btn_ok: eui.Button;
    /**聊天視窗訊息組 */
    public group_msg: eui.Group;

    public constructor() {
        super();
    }
    protected partAdded(partName: string, instance: any): void {
        super.partAdded(partName, instance);
    }
    protected childrenCreated(): void {
        this.init();
        super.childrenCreated();
    }
    /**WebSocket */
    public ws;
    public preface_label;
    public message_text;
    private init() {
        /**WebSocket連線 */
         //線上測試連結,服務端程式碼需在伺服器啟動
        //this.ws = new WebSocket('ws://7hds.com:8180');
        this.ws = new WebSocket('ws://127.0.01:8180');
        this.ws.onopen = function (e) {
            console.log('Connection to server opened');
        }
        /**暱稱 */
        var nickname;
        var self = this;
        this.ws.onmessage = function (e) {
            var data = JSON.parse(e.data);
            nickname = data.nickname;
            appendLog(data.type, data.nickname, data.message, data.clientcount);
            console.log("ID: [%s] = %s", data.id, data.message);
            //插入訊息
            self.group_msg.addChild(self.newLabel(data.nickname, data.message))
        }
        function appendLog(type, nickname, message, clientcount) {
            console.log(clientcount)
            /**聊天資訊 */
            var messages = this.list_msg;
            /**提示 */
            var preface_label;
            if (type === 'notification') {
                preface_label = "提示:";
            } else if (type === 'nick_update') {
                preface_label = "警告:";
            } else {
                preface_label = nickname;
            }
            self.preface_label = preface_label;
            var message_text = self.message_text = message;
            /**線上人數 */
            self.lb_online.text = clientcount;
        }
        /**點選OK傳送 */
        this.btn_ok.addEventListener(egret.TouchEvent.TOUCH_TAP, this.sendMessage, this);
    }
    /**傳送訊息 */
    private sendMessage() {
        var message = this.input_msg.text;
        if (message.length < 1) {
            // console.log("不能傳送空內容!");
            return;
        }
        this.ws.send(message);
        /**清空輸入框內容 */
        this.input_msg.text = "";
        // console.log(this.ws.bufferedAmount);
    }
    private newLabel(name: string, msg: string) {
        var label1: eui.Label = new eui.Label();
        label1.text = name + ":" + msg;
        label1.textColor = 0x000000
        return label1;
    }

}

本文的demo增加了客戶端與伺服器的互動,同時也實現了客戶端之間的聯絡。