[NodeJS Become A Full Stack Developer] — 透過 Socket.io 來製作即時聊天室吧
Introduction & 前言
這是一篇關於 Socket.io 的淺入淺出文章,如果你正在尋找相關知識或想知道什麼是 Socket.io 甚至有想要嘗試自己做一個即時聊天室,那麼這篇文章就很適合你。
Summary & 摘要
本篇文章後端將會使用 Node.js,前端將會使用 **React(Create React APP)**,不同框架語言使用方式其實應該都差不多,但如果你有打算使用其他框架,可以再多爬幾篇文章比較一下。
這邊會簡單的提一下 Socket.io 是什麼,但不會深度解析什麼是 Socket.io 及背後的原理;最後我們將會實作出有下列幾個簡單功能的聊天室。
- 透過 Socket.io 達到即時聊天效果。
- 不同聊天室分群聊天效果。
- 全頻道/分群廣播效果。
- 透過打 api 後,操作 Socket.io。
淺入淺出關於 Sokect.io 的介紹
在實作 Socket.io 聊天室之前,筆者雖然預設大家都已經知道這個東西是什麼,但或許還是有一些讀者是第一次接觸或不了解 Socket.io 的。如果你已經知道,這個地方可以跳過。
一般的伺服器與使用者溝通方式
首先我們一般前端使用者與後端溝通都是透過 API 來發送請求,然後再透過 Response 去接收回覆的訊息,但是伺服器並無法直接主動發出訊息給使用者;使用者在送出表單或者點擊某顆按鈕後透過 ajax 或 axios 去打 API 亦或者連接網站後,發出任何請求至後端。
像上方左邊的圖,1 就是使用者發起請求,而伺服器收到後會傳送 Response 回給發起請求的使用者,但無法發送給其他使用者,例如 使用者A 發起請求後,伺服器只能回傳 Response 給這個使用者,無法發給 使用者B 或 使用者C。
使用 Socket.io 的伺服器與使用者溝通方式
透過 Socket.io 我們可以達到上面一般方式我們想達到的事情,可以把 Socket.io 想像成 某個訂閱服務,當使用者主動發起請求至訂閱服務,可以再回傳事件,但這時候已經不是依靠 API 方式去溝通,而是透過 Socket.io 提供的 API,待會後面會說到。
最主要的地方來了,當訂閱服務收到使用者的請求後,他除了可以主動回覆給該使用者,也可以一併的主動發出訊息給所有訂閱的使用者,聰明的你就知道這個可以達到聊天室的需求。
其實現在很多東西都會用到 Socket.io:
遊戲
聊天室
客服中心
任何需要即時更新的資料,股票、博彩…等等
何謂 Socket.io Websocket Socket
這邊其實沒有要詳細說明三者差異,有興趣可以參考 【筆記】Socket,Websocket,Socket.io的差異。
但還是快速地說一下:
Socket 就是以前的 TCP/IP,現在也變成通訊的標準之一。
Websocket 在七層模型中屬於「應用層」,也是一種協議。
Socket.io 和上面兩者不同,他是 JavaScript 的一個函式庫,使伺服器和客戶端之間即時雙向的通信成為可能
前置作業
在開始前我們都需要先認識幾個簡單的 Socket.io 的 API,這邊會分成前端及後端部分去講解,就跟 Socket.io 官方文件差不多。
關於後端的 Socket.io
因為會講到連接的方式,所以這邊會從後端開始講起,這邊會先提供最終程式碼,傳送門請點我。
我們需要先安裝 express-generator
,這是一個 Express 的應用程式產生器,然後透過 express-generator
去產生一個 Node.js 專案。
1 | $ npm install express-generator -g // 全域安裝 |
建立完成後大概會長得像下面的架構。
1 | $ npm install socket.io // 現在的版本都會預設 -S 可以不用加了 |
如果不知道 -S 是什麼意思,可以參考 [Tool Notes] — 關於Webpack #2 - Babel? 的 開動了 一小段。
- 安裝完後就可以開始使用了,打開
/bin/www
這隻檔案,然後引入相關套件。
1 | // /bin/www |
因為我們的聊天室會有使用者們及聊天房間,所以在加入下面程式碼,因為這次範例是全部分開聊天,如果你想要也可以在弄一個房間作為大廳。
1 | // /bin/www |
- 開始使用 socket.io 套件,繼續編輯我們的
/bin/www
,加上下面程式碼
1 | // /bin/www |
上面這幾行其實也是使用了 Socket.io 最常使用的兩個 API,on
及 emit
,前者就是監聽事件,後者就是發送事件。
io.on("connection", (socket) => {
這行就是前端在連接(訂閱)這個 Socket.io 後會觸發的事件,這邊都是使用 connection 或 connect,兩者都可以,差別就在於如果你兩者都有使用的話 connect 會先被觸發,之後再跑 connection,詳細可參考 **你知道socket.io中connect事件和connection事件的区别吗?**,如果想實驗可以改為下列程式碼。
1 | // /bin/www |
修改完就會發現兩個 console.log 都被觸發,但如果你反過來像下面程式碼就只會觸發一個。
1 | io.on("connection", (socket) => { |
等等後面會在提到 Namespace 及 room 的部分。
在跑完 connection 後會回傳一個 value,這邊我們跟官方文件一樣使用 socket,這邊會發現使用了 socket.emit("connectionSuccess"
,但後面又有 socket.broadcast.emit("connectionSuccess"
及 io.emit("connectionSuccess"
,這邊其實先搞懂 io 跟 socket 的差異之後發事件就大同小異了。
socket.emit("connectionSuccess"
就是發送事件給發送事件給後端的這個使用者,可能有點饒舌,但簡單說就是發送事件給透過 io.on("connection", //...略
連線(訂閱)的使用者。
socket.broadcast.emit("connectionSuccess"
就是發送事件給除了發起事件給後端的前端使用者外的人。
io.emit("connectionSuccess"
就是發送事件給所有人。
後面基本上我們會大量使用到 socket
去做 socket.on
或者 socket.emit
,到這邊我們大致完成了後端的部分,接下來開始著手完成前端的部分吧。
接著就可以啟用 npm start
了,筆者自己跟預設應該都是跑在 port 3000
。
關於前端的 Socket.io
首先我們需要在前端刻出聊天室的樣子,這邊會先提供程式碼,傳送門請點我,這邊就不詳細講解怎麼切版;
- 前端主要需要安裝 socket.io-client 這個套件 及 uuid 產生獨一無二的 ID。
1 | $ npm i socket.io-client -S // or yarn add |
再來前端很簡單只要記得三個基本的東西:
- 連線
- 監聽事件(on, once)
- 發送事件(emit)
- 讓我們跟後端 Socket.io 連線,根據你想連線的檔案位子可以做調整,例如我想在 component 是聊天室頁面的時候再連線。
參考 src/pages/Lobby/index.tsx
這隻檔案,可以發現在 useEffect 剛進來的時候去做連線的動作。
1 | // src/pages/Lobby/index.tsx 範例檔案 |
- 做好連線後我們就可以在需要的地方發起事件通知後端,在需要連線得地方輸入下面程式碼,這樣一來後端就可以接到事件了。
1 | // src/pages/ChatRoom/index.tsx 範例檔案 |
- 接下來為監聽事件,監聽的事件如果在後端發起了事件,而名稱剛好對到,就會收到訊息。
1 | // src/pages/Lobby/index.tsx |
關於 on 及 once 的差異可以簡單分為,前者為事件發生當下會收到, once 則為下一次才收到。
- 我們有接收的監聽事件,理所當然使用者端也可以主動發起事件,這邊就是透過 emit。
1 | data.state.ws.emit("createNewChatRoom", { |
使用者端基本就是這三個 API,再詳細的話可以參考官方文件 Socket.io
初階版的小晉級
上面有提到 Namespace 及 Room,雖然我們有使用 chatRooms 去分不同的聊天室,但其實後端透過 io.emit()
去發送事件,全部的人都會收到,這樣會照成一個尷尬的情況,就是 使用者A 在 房間A 聊天,透過前端的 io.emit()
把訊息發事件到後端,然後後端使用 io.emit 或 socket.broadcast.emit 發事件給使用者,這時候 房間B 的全部使用者也都會接收到訊息。
為了做到真正的分流,我們可以使用官方提供的方法 Namespace 或 Room。
Namespace
這邊先來説說 Namespace,一般我們前端跟 Socket.io 連線,後端的部分都是預設 io.on("connection", //...略
,但如果透過 Namespace 就可以達到一個 socket.io 有很多個 channel。
- 將原本的
/bin/www
多加下列的程式碼
1 | // /bin/www |
- 再把前端加上下列程式碼
1 | import { io } from "socket.io-client"; // 原本就有的 |
如此就達到分 Channel 的概念,你可以分別在不同的 Namespace 下去監聽或者發起事件。
更詳細可參考官方文件 Namespace
Room
再來我們來說說 Room,和 Namespace 都一樣可以做到分流,但他的加入方式不同,Room 提供幾個基本的 API,其中加入方式為 **join()**,當然也可以離開房間 **leave()**。
- 我們嘗試把使用者加入到某間房間,修改一下我們原本
/bin/www
的程式碼
1 | // /bin/www |
上面我們模擬一個情境,在前端透過點擊參加聊天室後,透過 Socket.io 發送事件 join,並且帶著房間(Room) id 及 使用者名稱,房間資訊在一開始登入時會傳給使用者。
接著透過 join() 這個方式把該事件的發送使用者(socket)加入名為變數 id 的 Room。緊接著透過 io.to(id).emit()
取代原本的 io.emit()
,這樣事件只會發送給該房間(Room)的使用者(訂閱者)。
更詳細可參考官方文件 Room
結合迸出新滋味
這時候聰明的你會想到,那我不就可以先用 Namespace 做出不同的頻道(伺服器的概念),裡面再丟好幾個小房間。沒錯!你可以這麼做,而這也是 Socket.io 的最佳應用。
如果用上圖來說明,應該會更清楚,如果你沒在玩遊戲,或者不清楚,也可以在參考下圖。
Conclusion & 結論
沒想到最近在工作上一忙,一晃眼又是兩個月沒有更新部落格了,趁這次在工作上摸新技術時,筆記了一下所學,其中一個就是 Socket.io,其實好久之前就一直想摸,雖然這篇文章沒有講得很詳細,但其實 Socket.io 真的滿好入門的,只要掌握其中幾個 API,像是 on()
、emit()
、of()
、to()
、join()
、leave()
…等等,其中最前面兩個也才是最常用的,大概了解了運作流程就可以自己做很多好玩的應用了。
前端專案裡面有包著 dockerfile,剛好最近工作上也有接觸到,但如果沒有研究的話,其實可以先跳過,這部分不影響程式碼進行;最後這邊會再附上 Source Code 的網址,大家都可以 colone 下來玩看看。
最後的最後放上一張手寫筆記結束今天這一回合,晚安各位!
參考網站
官方文件 - Socket.io
segmentfault - 你知道socket.io中connect事件和connection事件的区别吗?
Creative Coding TW - 互動程式創作台灣站 - 用 Socket.io 做一個即時聊天室吧!(直播筆記)