用 Socket.io 做一個即時聊天室吧!(直播筆記)

這次的筆記是跟著這部影片來完成的,想要一邊看到會動的老闆請走這邊:

什麼是 Socket.io

socket.io 是一個可以讓應用程式建立即時通訊的 JavaScript 函式庫,透過在 Server(伺服器)與Client(裝置)之間建立持續的連線,可以即時的傳送資料給對方。想要瞭解更多的話可以參考 socket.io 的通訊協定基礎 WebSocket

可以把由 socket 所建立的應用想像成是一個學校的廣播系統,學務處(Server)可以向每個班級(Client)統一廣播上課的鐘聲,而每個班級在點名完之後,也可以透過班長把點名的結果回報給學務處。要注意的是一般來說學務處可以單獨發送訊息給班級,但是班級之間沒有正規的溝通管道。

socket.io 的這幾個優點造成了他在即時通訊、遊戲上的活用:

  1. 簡化溝通:透過on方法,像 JavaScript 一樣接收事件並觸發 callback function。
  2. 即時:可以非常即時的同步資料。
  3. 資料同步:可以在執行的階段在瀏覽器同步保存資料。

應用範例 2018 印象清華 – 物聯網科技藝術節 作品

光譜原色:透過建立 WebSocket 連線即時變換湖面上的燈光。影片

英文8-2:將作答題目的結果與得分即時顯示在頁面上。


程式實作 – 即時聊天室

在這個範例中我們將會建立一個即時聊天室,透過 socket.io 來實現 Server 與多個 Client 之間的溝通,並在用戶登入的時候讀取所有對話記錄、送出訊息的時候發送到所有的用戶介面。

初始化 server 端專案

  1. 在 terminal 先新建並進入資料夾 mkdir socket-server && cd socket-server
  2. 接著安裝 socket.io 與 express(網頁伺服器框架) npm i socket.io express -s

設定 socket 與 http(s) 連線

建立 index.js ,index.js 是後端的主程式,負責處理用戶端傳來的事件並將結果廣播給所有用戶。我們首先設定 socket 與 api 的監聽端口。

*因為此範例都在本機電腦開發,並且直接連線到 localhost,故沒有設定 ssl 加密與 https。

var fs = require('fs')
// var https = require('https')
// 如果不用 https 的話,要改成引用 http 函式庫
var http = require('http')
var socketio = require('socket.io')

//https 的一些設定,如果不需要使用 ssl 加密連線的話,把內容註解掉就好
var options = {
    // key: fs.readFileSync('這個網域的 ssl key 位置'),
    // cert: fs.readFileSync('這個網域的 ssl fullchain 位置')
}

//http & socket port
var server = http.createServer(options);
server.listen(4040)
var io = socketio(server);
console.log("Server socket 4040 , api 4000")

//api port
var app = require('express')();
var port = 4000;
app.listen(port, function () {
    console.log('API listening on *:' + port);
});

//用 api 方式建立連線
app.get('/api/messages', function (req, res) {
    let messages = 'hellow world'
    res.send(messages);
})

//用 socket 方式建立連線
io.on('connection', function (socket) {
    console.log('user connected')
})

執行 npm index.js ,如果出現以下訊息的話,代表我們的 http server 初步建立完成囉~

Server socket 4040 , api 4000
API listening on *:4000

為了測試 api 連線是不是也是正常,我們直接使用瀏覽器連線到 server 監聽的 api 網址,成功看到透過 api GET 取的的回覆顯示在螢幕上,再打開 devtools 的 Network 也確認無誤,接下來可以進入 socket 的部分了!

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/3575cada-13cd-4bbb-bf2b-91bd53a9963a/Screen_Shot_2020-03-24_at_00.15.34.png
https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e6b2fa09-b128-490e-9856-d6aa6560a30e/Screen_Shot_2020-03-24_at_00.13.05.png

我們一樣先測試 socket 的連線能不能成功,但是要怎麼讓瀏覽器端可以連線到 socket 呢?

只要在 html 的 head 引用 socket.io 的裝置端套件就可以了 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>。我們直接在 <script> 裡面建立與伺服器的連線:var socket = io("<http://localhost:4040>"),將瀏覽器打開之後如果看到 server 有打印 user connected 的話就是連線成功囉。

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e27f893c-ea9b-4e1b-b717-944801b77e07/Screen_Shot_2020-03-24_at_14.50.50.png
到 network 儀表版也可以看到我們的請求成功

基本的訊息傳送

不論在 server 或是 client,socket 都是透過 on 來監聽事件、用 emit 來發送事件,大致的關係會是這樣:

簡易 socket.io 通訊關係

Server 端建立連線/事件傳送方向Client 端
io.on(‘connection’, function (socket) {…})建立連線socket = io(“socket ip:port”)
io.emit(“要對所有 Client 廣播的事件名稱”, data)

socket.emit(“要對當前連線的 Client 發送的事件名稱”, data)
———>socket.on(“來自client 的事件名稱”, callback)
socket.on(“來自client 的事件名稱”, callback)<———socket.emit(“要對 server 發送的事件名稱”,data)

先由 client emit 一個最簡單的訊息看看,送出一個包含 name 與 message 的物件:

// index.html
// 建立與 server 的連線
var socket = io("<http://localhost:4040>")

// 發送一個 "sendMessage" 事件
socket.emit("sendMessage", {
            name: "majer",
            message: "hello everyone"
        })
// 監聽來自 server 的 "allMessage" 事件
socket.on("allMessage", function(message){
    console.log(message)
})

當然也別忘了在 server 加上”sendMessage” 事件的監聽器:

// index.js
io.on('connection', function (socket) {
    console.log('user connected')
    // 建立一個 "sendMessage" 的監聽
    socket.on("sendMessage", function (message) {
        console.log(message)
	// 當收到事件的時候,也發送一個 "allMessage" 事件給所有的連線用戶
	io.emit("allMessage", message)
    })
})

看一下 server 顯示的結果,果然把我們剛剛 emit 的資料印出來了

Server socket 4040 , api 4000
API listening on *:4000
user connected
{ name: 'majer', message: 'hello everyone' }

而瀏覽器也可以看到從 server 發送過來的 “allMessage” 事件:

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/83cc5d8d-2f09-41b2-a22f-6155cfedb177/Screen_Shot_2020-03-24_at_15.42.19.png

萬丈高樓平地起,我們緊接著就可以在這個基礎之上建立聊天室的介面囉

聊天室的功能

我們列出聊天室會有的基本功能:

  1. 進入聊天室時印出聊天室目前的對話記錄
  2. 可以輸入用戶名稱與訊息,並且點擊後發送
  3. 可以接收別人發送的新訊息

Server 端 🖥

我們先在 server 端把所有的對話內容與用戶儲存在 messages 陣列內,每當有新的用戶建立連線,就把之前的對話透過 allMessage 傳送給用戶。除此之外,我們也監聽用戶發送的 "sendMessage" 事件,除了發送新訊息給所有用戶之外,也把新收到的訊息塞到 messages 裡面,讓新用戶進來的時候可以看到。

// index.js
var messages = [
    { name: "Majer", message: "Welcome!"  }
]

io.on('connection', function (socket) {
    console.log('user connected')
    // 發送之前的全部訊息
    io.emit("allMessage", messages)
    // 當此用戶發送訊息的時候,先把新訊息放到 messages 陣列裡面
    // 再 emit 給所有用戶
    socket.on("sendMessage", function (message) {
        console.log(message)
        messages.push(message)
        io.emit("newMessage", message)
    })
})

User 端 👨🏼‍💻

在進入頁面的時候,我們使用 on("allMessage") 把之前的對話記錄都儲存到 messages 裡面,再透過 v-for 處理 messages 陣列,把對話的內容與用戶名稱印在畫面上。此外,加上新訊息的監聽 on("newMessage"),如果有新的訊息,也更新到 messages 的最後面。

發送訊息的部分,我們使用 Vue 把用戶的名稱與訊息綁定在 temp 上,每次發送的時候就直接送出 temp 物件,再把 temp.message 設定成空字串 '' 清空。

<body>
    <div id="app">
        <ul>
            <li v-for="m in messages">
                <h4>{{m.message}}<span>-- {{m.name}}</span></h4>
            </li>
        </ul>
        <!-- 將 name 與 message 綁定到 data 的 temp 物件內 -->
        <input v-model="temp.message" placeholder="訊息" @keydown.enter="sendMessage" />
        <input v-model="temp.name" placeholder="你是誰?" />
        <button @click="sendMessage">送出</button>
    </div>
</body>

<script>
    var vm = new Vue({
        el: "#app",
        data: {
            messages: [],
            temp: {},
            socket: null,
        },
        mounted() {
            this.socket = socket = io("<http://localhost:4040>")

            // 進入聊天室時,會收到之前的全部訊息,並更新到 messages
            this.socket.on("allMessage", obj => {
                console.log('received all messages')
                this.messages = obj
            })

            // 設定接收到新訊息的監聽器
            this.socket.on("newMessage", obj => {
                console.log('received new message')
                this.messages.push(obj)
            })
        },
        methods: {
            sendMessage() {
                console.log('sending new message')
                this.socket.emit("sendMessage", this.temp)
                this.temp.message = ""
            }
        }
    })
</script>

如此一來,我們就完成了最基礎的聊天室介面與功能了!

快來看看實際運行起來的狀況吧:

其他延伸

我們也可以加入其他的功能,像是:

  • 顯示其他人在輸入中
  • 顯示用戶上線/下線
  • 設定用戶不重複的名字
  • 寄送私人訊息
  • 傳送圖片、gif

完成品

最後附上有加上輸入中版本的完整程式碼,或是也可以到專案的 github 看到這個範例呦:https://github.com/Monoame-Design/bosscoding-examples

server

var fs = require('fs')
// var https = require('https')
// 如果不需要用 https 的話,要改成引用 http 喔
var http = require('http')
var socketio = require('socket.io')

//https 的一些設定,如果不需要使用 ssl 加密連線的話,把內容註解掉就好
var options = {
    // key: fs.readFileSync('這個網域的 ssl key 位置'),
    // cert: fs.readFileSync('這個網域的 ssl fullchain 位置')
}

//http & socket port
var server = http.createServer(options);
server.listen(4040)
var io = socketio(server);
console.log("Server socket 4040 , api 4000")

//api port
var app = require('express')();
var port = 4000;
app.listen(port, function () {
    console.log('API listening on *:' + port);
});

//用 api 方式取得
app.get('/api/messages', function (req, res) {
    let messages = 'hellow world'
    res.send(messages);
})

var messages = [
    { name: "Majer", message: "Welcome!" }
]

var typing = false
var timer = null
//用 socket 方式取得
io.on('connection', function (socket) {
    console.log('user connected')
    socket.emit("allMessage", messages)

    socket.on("sendMessage", function (message) {
        console.log(message)
        messages.push(message)
        io.emit("newMessage", message)
    })

    socket.on('sendTyping', function () {
        console.log('typing')
        typing = true
        io.emit("someoneIsTyping", typing)
        clearTimeout(timer)
        timer = setTimeout(() => {
            typing = false
            io.emit("someoneIsTyping", typing)
        }, 3000)
    })
})

client

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>
</head>

<body>
    <div id="app">
        <ul>
            <li v-for="m in messages">
                <h4>{{m.message}}<span>-- {{m.name}}</span></h4>
            </li>
        </ul>

        <div>{{ typing?'輸入中...':'' }}</div>
        <br>
        <!-- 將 name 與 message 綁定到 data 的 temp 物件內 -->
        <input v-model="temp.message" placeholder="訊息" @keydown.enter="sendMessage" @keypress="sendTyping" />
        <input v-model="temp.name" placeholder="你是誰?" />
        <button @click="sendMessage">送出</button>
    </div>
</body>

<script>
    var vm = new Vue({
        el: "#app",
        data: {
            messages: [],
            temp: {},
            socket: null,
            typing: false
        },
        mounted() {
            this.socket = socket = io("http://localhost:4040")

            // 進入聊天室時,會收到之前的全部訊息,並更新到 messages
            this.socket.on("allMessage", obj => {
                console.log('received all messages')
                console.log(obj)
                this.messages = obj
            })

            // 設定接收到新訊息的監聽器
            this.socket.on("newMessage", obj => {
                console.log('received new message')
                this.messages.push(obj)
            })

            this.socket.on("someoneIsTyping", value => {
                this.typing = value
            })
        },
        methods: {
            sendMessage() {
                console.log('sending new message')
                this.socket.emit("sendMessage", this.temp)
                this.temp.message = ""
            },
            sendTyping() {
                this.socket.emit("sendTyping")
            }
        }
    })
</script>

</html>

墨雨設計banner

訂閱 Creative Coding Taiwan 電子報:

PHP Code Snippets Powered By : XYZScripts.com