使用 Socket.io + Express.js 建立聊天伺服器

Socket.IO 的官方網站上就已經有一篇簡單的聊天 app 的入門教學,如果你更喜歡看英文,可以參考這篇: Socket.IO Get started
官方的教學文章最後也附上了 GitHub repo ,有興趣的也可以前往下載。

由於官方提供的是一個小巧簡單的範例,實際要應用在自己習慣的專案結構中的話,依然會面對很多不知道為什麼就卡住無法執行的狀況。

在這裡想順一遍安裝的過程,以及分享自己遇到的問題和解決方法。


前置

本篇介紹適用於已初步了解 Node.js 、Express.js 的讀者。
請確保已安裝 Node.js 和 Express.js。

步驟

1. 使用 npm 安裝 socket.IO

1
npm install socket.io

2. 將 socket.io 導入 express 伺服器

server 端

1
2
3
4
5
6
7
8
9
10
const express = reqire('express')
const app = express()
const server = require('http').createServer(app)
const io = require('socket.io')(server)

io.on('connection', socket => {
console.log('a user connected')
})

server.listen(3000, () => console.log(`Example app listening on port ${port}!`))

大家可能會發現,這段按照了原本 node.js 建立伺服器的方式,先導入了 http 模組,並使用 createServer 的方法建立伺服器。
習慣使用了 express 的話會覺得困惑, express 不是已經幫我們內建了 http 模組了嗎?
這是因為 socket.io 建立伺服器的參數只接受 http 伺服器,包裝過的 express app 是不符合需求的,所以我們得先讓 app 變成 http server ,才能傳遞給 socket.io 建立伺服器。

此外, const io = require('socket.io')(server) 等同以下程式碼:

1
2
const { Server } = require('socket.io')
const io = new Server(server)

宣告完變數後,讓 socket.io 開始監聽 connection 並在終端機輸出 a user connected 的訊息。
最後就跟一般啟動伺服器一樣,讓伺服器開始監聽 port 3000

client 端

在 html 頁面裡 </body> 前加入以下程式碼:

1
2
3
4
<script src="{SERVER_URL}/socket.io/socket.io.js"></script>
<script>
const socket = io({SERVER_URL})
</script>

{SERVER_URL} 請代入你稍後啟動的伺服器位置。
如果設定和 1. 相同的話,那將會是 http://localhost:3000/

如果 html 頁面就放在 http://localhost:3000/ 底下的話,甚至可以省略網址:

1
2
3
4
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io()
</script>

此時如果重新啟動伺服器,應該會看到終端機開始出現 a user connected 的訊息。
沒有的話就代表沒有連線成功。

出問題了怎麼辦!

1
Access to XMLHttpRequest at '{SERVER_URL}/socket.io/?EIO=....' from origin '{CLIENT_URL}' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

socket.io 預設是會先使用 HTTP 長輪詢(long-polling)的方式進行傳輸,成功以後才會嘗試以 WebSocket 的通訊協定建立連接。
在 server 端與 client 端位於不同網域、不同通訊協定或不同通訊埠(port)的情況下,會被跨來源資訊共用(CORS)的機制阻擋請求。
關於這點,Socket.IO 官方文件有提供相關處理方式:Handling CORS

而這裡,我們直接簡單地禁止使用長輪詢,只用 WebSocket 的方式進行連線。
請修改 const socket = io() 為以下程式碼:

1
const socket = io({ transports: 'websocket' })

3. 設定收發訊息(Emitting events)

client 端(傳送)

這次我們從 client 端開始。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<body>
<ul id="messages"></ul>
<form id="form" action="">
<input id="input" /><button>Send</button>
</form>

<script src="{SERVER_URL}/socket.io/socket.io.js"></script>
<script>
const socket = io({SERVER_URL})

const messages = document.getElementById('messages')
const form = document.getElementById('form')
const input = document.getElementById('input')

form.addEventListener('submit', function onFormSubmit(e) {
e.preventDefault()
if (input.value) {
socket.emit('chat message', input.value)
input.value = ''
}
})
</script>
</body>

server 端

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = reqire('express')
const app = express()
const server = require('http').createServer(app)
const io = require('socket.io')(server)

io.on('connection', socket => {
console.log('a user connected')
socket.on('chat message', msg => {
io.emit('chat message', msg)
})
})

server.listen(3000, () => console.log(`Example app listening on port ${port}!`))

socket.io 以 socket.emit({EVENT_NAME}, {EMIT_COMMENT}) 的方法將資訊送出,並以 socket.on({EVENT_NAME}, function ({EMIT_COMMENT})) 的方法監聽 {EVENT_NAME} 事件並接受訊息。
在接收到訊息後,socket.io 的伺服器應把該訊息廣播出去,要傳入的參數和 client 端傳送訊息時相同,只是這次是由伺服器進行發送。

在這裡使用 io.emit({EVENT_NAME}, {EMIT_COMMENT}) 進行廣播,這會把資訊傳給所有當前連線的 client 端。

client 端(接收)

現在,我們該讓 client 端監聽同樣的事件並接收訊息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const socket = io({SERVER_URL})

const messages = document.getElementById('messages')
const form = document.getElementById('form')
const input = document.getElementById('input')

form.addEventListener('submit', function onFormSubmit(e) {
e.preventDefault()
if (input.value) {
socket.emit('chat message', input.value)
input.value = ''
}
})

socket.on('chat message', msg => {
const item = document.createElement('li')
item.textContent = msg
messages.appendChild(item)
})

和 server 端一樣,這裡也是用 socket.on({EVENT_NAME}, function ({EMIT_COMMENT})) 的方法接收資訊。
試著在瀏覽器的輸入框裡打些什麼並送出,這時應該能看見上頭出現訊息了。

到這裡,我們有了個又簡易又陽春的聊天平台,你可以用不同分頁開啟同個頁面試著發送內容,如果在舊頁面也有出現相同的訊息,那就成功了!

不只是純字串,emit 也能夠傳送 JSON檔,配合 JSON.stringify()JSON.parse() 能夠做出很多有趣的變化。


參考資料