這次的筆記是跟著這部影片來完成的,想要一邊看到會動的老闆請走這邊:
什麼是 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>




