[Socket 入門系列 04] 擴充功能 - 獨立聊天室

[Socket 入門系列 04] 擴充功能 - 獨立聊天室

前情提要

在前一篇中我們想了很多功能要來撰寫
幾個功能過於簡單,若是特別花一篇文章來敘述顯得有點單薄
就在正文開始前把一些簡單的功能快速說明

當有人連接或斷開連接時,向連接的用戶廣播消息

寫在 connectiondisconnect 監聽內就可以了

加入使用者自訂暱稱的功能

因為傳送給伺服器端的資料可以不只是字串,把暱稱包進物件或是 json 內傳遞即可

顯示訊息時間

同暱稱功能,只是要處理時間格式

顯示在線人數

伺服器定義在線人數變數,在 connectiondisconnect 監聽並處理數量

別送訊息給自己,而是在按下送出時直接顯示在畫面上

聊天室用 Broadcasting 發送與接收資料,而發送者本身使用 javascript 渲染畫面

{user} is typing 功能

在使用者端監測到按下鍵盤則發送 start typing
若是幾秒內沒有再次輸入,則發送 stop typing

以上功能牽涉到比較多的畫面設計等前端技能
這些又是我非常薄弱的部分,也許未來學前端框架後會拿來實作
但目前這些項目實作就先暫時放置了

主文

根據官方文件的說法,要建立分別的聊天室可以使用 Rooms and Namespace
簡單來說會使用到的就是 join/leave roomsay to room

// join和leave
io.on('connection', function(socket){
  socket.join('some room');
  // socket.leave('some room');
});

// say to room
io.to('some room').emit('some event'):

每個 Socket 都有獨自的獨立隨機識別編號 Scocket#id
方便起見每個對話都會自動加入所定義的 id 編號房間

io.on('connection', function(socket){  
 socket.on('say to someone', function(id, msg){  
 socket.broadcast.to(id).emit('my message', msg);  
 });  
});

然後就可以開始寫程式
在這個練習裡,我完成了最基本的新使用者加入提醒以及傳送、接收聊天室訊息
使用者不會收到不屬於自己聊天室的訊息
為了和之前的聊天室做比較,我另外開了兩隻檔案 chatroom.js 以及 chatroom.html

其實在跟上次的大鍋炒聊天室沒什麼太大的不同
但是為了講解方便還是複製貼上了

chatroom.html

首先呢,在進入頁面時我們抓取網址路徑來作為聊天室識別碼
藉由初始化頁面就發送 join room 來傳送資料給 socket 伺服器

socket.emit('join room', room_name)

之前說過的 socket 傳遞的內容也可以是一個物件
而這邊我們就把聊天室名稱與訊息組成物件一同發送,指定要發送給哪個聊天室

socket.emit('chat message', {room_name, msg})

其他的部份和原本的沒有什麼區別,下方為完整程式碼

<!doctype  html>
<html>
  <head>
  <title>Socket.IO chat</title>
  <style>
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
      }
      body {
        font: 13px Helvetica, Arial;
      }
      form {
        background: #000;
        padding: 3px;
        position: fixed;
        bottom: 0;
        width: 100%;
      }
      form input {
        border: 0;
        padding: 10px;
        width: 90%;
        margin-right: .5%;
      }
      form button {
        width: 9%;
        background: rgb(130, 224, 255);
        border: none;
        padding: 10px;
      }
      #messages {
        list-style-type: none;
        margin: 0;
        padding: 0;
      }
      #messages li {
        padding: 5px 10px;
      }
      #messages li:nth-child(odd) {
        background: #eee;
      }
    </style>
</head>

<body>
<ul id="messages"></ul>
<p id="announcement" style="display: none; position: absolute; bottom: 50px; left: 5px;"></p>

<form action="">
  <input id="msg_input" autocomplete="off" /><button>送出</button>
</form>
<script src="/socket.io/socket.io.js"></script>
<script src="https://code.jquery.com/jquery-1.11.1.js"></script>
<script>
  $(function () {
    var socket = io()
    
    // 初始進加入聊天室
    room_name = window.location.pathname
    room_name = room_name.replace('/', '')
    socket.emit('join room', room_name)

    // 新使用者進入聊天室
    socket.on('new user', function (msg) {
      $('#announcement').text(msg).fadeIn(1500)
      $('#announcement').fadeOut(1500)
    });

    // 傳送訊息
    $('form').submit(function (e) {
      e.preventDefault()
      let msg = $('#msg_input').val()
      socket.emit('chat message', {room_name, msg})
      $('#msg_input').val('')
      return  false
    });

    // 接收從伺服器傳來的其他使用者訊息
    socket.on('chat message', function (msg) {
      $('#messages').append($('<li>').text(msg))
    })
  })
</script>
</body>
</html>

chatroom.js

const  app = require('express')()
const  http = require('http').createServer(app)
const  io = require('socket.io')(http)
app.get('/:room_name', function (req, res) {
  res.sendFile(__dirname + '/view/chatroom.html')
});

io.on('connection', function (socket) {

  // 客戶端加入聊天室
  socket.on('join room', (room_name) => {
    socket.join(room_name)
    io.to(room_name).emit('new user', `有個使用者進入了聊天室 ${room_name} 跟他說聲 Hi 吧`)
  })

  // 接收客戶端訊息
  socket.on('chat message', ({room_name, msg}) => {
    console.log(`get message from room name '${room_name}' msg = ${msg}`)
    io.to(room_name).emit('chat message', msg)
  })
})

http.listen(3000, function () {
  console.log('express chatroom service on, listening on :3000')
})

在這支檔案裡
我們定義了新的監聽器 join room,來監聽我們上面提到的聊天室識別碼
在使用者端,當使用者進入時我們從用戶端發送事件,而伺服器接收到之後就會使用

socket.join(room_name)

來把指定連線加入指定的聊天室
再來只需要把訊息內容推送給指定聊天室

這邊我們修改原先的發送方法
使用 io.to() 來發送訊息給指定聊天室的連線

io.to(room_name).emit('chat message', msg)

成果

建立的兩個聊天室 phpnode
在 php 聊天室內的對話不會被 node 聊天室成員接收到
在傳送訊息的時候旁邊終端機視窗則展示了接收訊息並分派給不同聊天室的過程

聊天室最終結果

結論

個別聊天室的撰寫方法需要注意的地方在
如何加入聊天室
一開始我在連線(connection)時就直接在 server 端抓取網址路徑進行 socket.join()
而造成我的連線一直跳來跳去,當不同聊天室開啟連線
原有連線就會錯亂,有時候會有一兩個使用者被連到新的連線上
在觀察了連線時的 log 發現有時候不是連上我指定的聊天室
而是連到 undefined 或是 favicon.ico 這絕對不是我要的連線
這時候又會回歸到大鍋炒聊天室的情況

最後參考了 github 上其他人的專案,發現他們在頁面載入後另外發送了加入聊天室給 socket
而我這麼做之後也不再有連線錯亂的問題

再來是這次的練習中 server 端使用了 es6 中的箭頭函數,來發送訊息
也使用解構運算來接收參數,並把服務初始化改為 const 型別

整個 Socket 入門系列也就差不多到這了
剩下更深入的部份也許就等需要用到的時候再來補齊

留言