這次的筆記是跟著這部影片來完成的,想要一邊看到會動的老闆請走這邊:
什麼是 Socket.io?
socket.io 是一個可以讓應用程式建立即時通訊的 JavaScript 函式庫,透過在 Server(伺服器)與Client(裝置)之間建立持續的連線,可以即時的傳送資料給對方。想要瞭解更多的話可以參考 socket.io 的通訊協定基礎 WebSocket。
可以把由 socket 所建立的應用想像成是一個學校的廣播系統,學務處(Server)可以向每個班級(Client)統一廣播上課的鐘聲,而每個班級在點名完之後,也可以透過班長把點名的結果回報給學務處。要注意的是一般來說學務處可以單獨發送訊息給班級,但是班級之間沒有正規的溝通管道。
socket.io 的這幾個優點造成了他在即時通訊、遊戲上的活用:
- 簡化溝通:透過
on
方法,像 JavaScript 一樣接收事件並觸發 callback function。 - 即時:可以非常即時的同步資料。
- 資料同步:可以在執行的階段在瀏覽器同步保存資料。
應用範例 2018 印象清華 – 物聯網科技藝術節 作品
光譜原色:透過建立 WebSocket 連線即時變換湖面上的燈光。影片
英文8-2:將作答題目的結果與得分即時顯示在頁面上。
程式實作 – 即時聊天室
在這個範例中我們將會建立一個即時聊天室,透過 socket.io 來實現 Server 與多個 Client 之間的溝通,並在用戶登入的時候讀取所有對話記錄、送出訊息的時候發送到所有的用戶介面。
初始化 server 端專案
- 在 terminal 先新建並進入資料夾
mkdir socket-server && cd socket-server
- 接著安裝 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 的部分了!
我們一樣先測試 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
的話就是連線成功囉。
基本的訊息傳送
不論在 server 或是 client,socket 都是透過 on
來監聽事件、用 emit
來發送事件,大致的關係會是這樣:
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” 事件:
萬丈高樓平地起,我們緊接著就可以在這個基礎之上建立聊天室的介面囉
聊天室的功能
我們列出聊天室會有的基本功能:
- 進入聊天室時印出聊天室目前的對話記錄
- 可以輸入用戶名稱與訊息,並且點擊後發送
- 可以接收別人發送的新訊息
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>