{%hackmd theme-dark %} ###### tags: `nodejs` `express` `websocket` # 如何實作一個簡單的websockt聊天室 本篇程式碼github [websocketPrictice](https://github.com/a121515222/websocketPriceict) ## 1.使用express generator建立資料 請參考[express generator](https://www.npmjs.com/package/express-generator) 建立完成之後 1. 在public資料夾創建websocket.html 2. 在routes資料夾創建websocket.js 3. 安裝[uuid套件](https://www.npmjs.com/package/uuid) 4. 安裝[ws套件](https://www.npmjs.com/package/ws#server-broadcast) ![image](https://hackmd.io/_uploads/HypwseIZC.png) ## 2.前端準備 開啟websocket.html 把以下的code複製貼上 ``` 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> </head> <body> <ul id="list"></ul> <input type="text" id="message"> <button type="button" id="sendBtn">送出</button> <p>uuid: <span id="uuid"></span></p> <script> const host = 'ws://localhost:3000/ws' const inputMessage = document.querySelector('#message') const spanUUID = document.querySelector('#uuid') const ulList = document.querySelector('#list') const btnSend = document.querySelector('#sendBtn') // 建立一個WebSocket const ws = new WebSocket(host) // 開啟連線 ws.open = (res) =>{ console.log("oepe",res) } </script> </body> </html> ``` ## 3.後端準備 在websocket.js中先使用uudi與ws,並且export新建立的WebSocketServer ``` javascript= const WebSocket = require("ws"); const { v4: uuidv4 } = require("uuid"); // 在http server下建立一個WebSocketServer const wss1 = new WebSocket.WebSocketServer({ noServer: true }); wss1.on("connection", function connection(ws){ ws.on(error,console.error); console.log("連線成功") }) module.exports = wss1 ``` 在bin內的www.js,這個www.js可以看做是後端的入口,http server就是在這裡建立的 打開www.js後 1. 引用path這個套件 2. 引用wss1 3. 在建立server之後加入對"upgrade"的監聽,一旦監聽到前端的header有upgrade後連線就會從http升級成websocket 4. 監聽的code在下方第25行-35行,監聽到upgradec後判斷url是不是有/ws有的話建立連線,沒有的話則切斷連線,這一段code可以在ws的文件Multiple servers sharing a single HTTP/S server內找到 ``` javascript= #!/usr/bin/env node /** * Module dependencies. */ var app = require("../app"); var debug = require("debug")("cd:server"); var http = require("http"); const { parse } = require("url"); const wss1 = require("../routes/websocket"); /** * Get port from environment and store in Express. */ var port = normalizePort(process.env.PORT || "3000"); app.set("port", port); /** * Create HTTP server. */ var server = http.createServer(app); server.on("upgrade", function upgrade(request, socket, head) { const { pathname } = parse(request.url); if (pathname === "/ws") { wss1.handleUpgrade(request, socket, head, function done(ws) { wss1.emit("connection", ws, request); }); } else { socket.destroy(); } }); /** * Listen on provided port, on all network interfaces. */ server.listen(port); server.on("error", onError); server.on("listening", onListening); /** * Normalize a port into a number, string, or false. */ function normalizePort(val) { var port = parseInt(val, 10); if (isNaN(port)) { // named pipe return val; } if (port >= 0) { // port number return port; } return false; } /** * Event listener for HTTP server "error" event. */ function onError(error) { if (error.syscall !== "listen") { throw error; } var bind = typeof port === "string" ? "Pipe " + port : "Port " + port; // handle specific listen errors with friendly messages switch (error.code) { case "EACCES": console.error(bind + " requires elevated privileges"); process.exit(1); break; case "EADDRINUSE": console.error(bind + " is already in use"); process.exit(1); break; default: throw error; } } /** * Event listener for HTTP server "listening" event. */ function onListening() { var addr = server.address(); var bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port; debug("Listening on " + bind); } ``` ## 4.建立連線 1.先把後端專案運行起來 npm run start 2.到chrome中輸入localhost:3000/websocket.html,就可以把public的websocket.html打開,畫面如下![image](https://hackmd.io/_uploads/HyO4G-IbA.png) 3.後端的terminal會出現連線成功的字樣,如下圖![image](https://hackmd.io/_uploads/Byq9G-IZA.png) 看到上面代表連線建立成功 ## 5.傳送id與接收 ### 後端傳送id 接下來是要給客戶端的連線賦予id並且回傳回去。 websocket.js ``` javascript= const WebSocket = require("ws"); const { v4: uuidv4 } = require("uuid"); // 在http server下建立一個WebSocketServer const wss1 = new WebSocket.WebSocketServer({ noServer: true }); wss1.on("connection", function connection(ws){ ws.on(error,console.error); console.log("連線成功") // 產生id const id = uuidv4(); // 給websocket實例id 這個很重要不然會不知道會知道是誰傳什麼資料 ws.id = id; // 需要傳回去的內容一律為字串所以將Object轉成字串。 ws.sned(JSON.stringigy({context:"connect",id})) }) module.exports = wss1 ``` ### 前端接受id websocket.html 內的script為 ``` javascript= <script> const host = 'ws://localhost:3000/ws' const inputMessage = document.querySelector('#message') const spanUUID = document.querySelector('#uuid') const ulList = document.querySelector('#list') const btnSend = document.querySelector('#sendBtn') // 向後端建立ws連線 const ws = new WebSocket(host) ws.onopen = (res) => { console.log('open', res) } // 接收後端資料 ws.onmessage = (res)=>{ const data = JSON.parse(res.date) console.log('onmessage', data) spanUUID.innerHTML = data.id } </script> ``` 以上成功的話前端會看到以下的畫面 ![image](https://hackmd.io/_uploads/rJD1aGUbA.png) ## 6.接收前端傳資料 接下來前端發資料到後端 ### 前端發送資料 websocket.html 內的script為 這邊要特別注意的是要把憶起傳給後端,第23行 ``` javascript= <script> const host = 'ws://localhost:3000/ws' const inputMessage = document.querySelector('#message') const spanUUID = document.querySelector('#uuid') const ulList = document.querySelector('#list') const btnSend = document.querySelector('#sendBtn') // 向後端建立ws連線 const ws = new WebSocket(host) ws.onopen = (res) => { console.log('open', res) } // 接收後端給的id let id ="" ws.onmessage = (res)=>{ const data = JSON.parse(res.date) console.log('onmessage', data) spanUUID.innerHTML = data.id id = data.id } // 發送資料給後端 btnSend.addEventListener('click', () => { const message = inputMessage.value ws.send(JSON.stringify({context: 'message', message,id})) }) ``` ### 後端接受前端所傳資料 websocket.js ``` javascript= const WebSocket = require("ws"); const { v4: uuidv4 } = require("uuid"); // 在http server下建立一個WebSocketServer const wss1 = new WebSocket.WebSocketServer({ noServer: true }); wss1.on("connection", function connection(ws){ ws.on(error,console.error); console.log("連線成功") // 產生id const id = uuidv4(); // 給websocket實例id 這個很重要不然會不知道會知道是誰傳什麼資料 ws.id = id; // 需要傳回去的內容一律為字串所以將Object轉成字串。 ws.sned(JSON.stringigy({context:"connect",id})) //接收來自前端的資料 ws.on("message", function message(data)=>{ const {context,message,id} = JSON.parse(data); const newMessage = { context, message, id } console.log("get Message from client", newMessage) }); }) module.exports = wss1 ``` 由前端頁面發送abc後在後端terminal會看到以下畫面,代表後端有收到資料 ![image](https://hackmd.io/_uploads/rJ03-7UZC.png) ## 7.後端廣播收到的前端資料 這地的廣播發資料的人不會收到,所以需要靠id判斷 這邊會用到wss1.clients進行處理,簡單的來說就是把所有連到後端的client都檢查一遍id 與前端送來的message id不同就傳給client ### 後端廣播資料 ``` javascript= const WebSocket = require("ws"); const { v4: uuidv4 } = require("uuid"); // 在http server下建立一個WebSocketServer const wss1 = new WebSocket.WebSocketServer({ noServer: true }); wss1.on("connection", function connection(ws){ ws.on(error,console.error); console.log("連線成功") // 產生id const id = uuidv4(); // 給websocket實例id 這個很重要不然會不知道會知道是誰傳什麼資料 ws.id = id; // 需要傳回去的內容一律為字串所以將Object轉成字串。 ws.sned(JSON.stringigy({context:"connect",id})) //接收來自前端的資料 ws.on("message", function message(data)=>{ const {context,message,id} = JSON.parse(data); const newMessage = { context, message, id } }); // 如果是訊息,就廣播出去 if(context === "message"){ broadcast(newMessage); } }) const broadcast = (message)=>{ wss1.clients.forEach((client)=>{ if(client.readyState === Websocket.OPEN && client.id !== message.id){ client.send(JSON.stringify(message)) } }) } module.exports = wss1 ``` ### 前端接收廣播資料並渲染 websocket.html 內的script為 這邊要 接收資料的那部分程式碼 ``` javascript= <script> const host = 'ws://localhost:3000/ws' const inputMessage = document.querySelector('#message') const spanUUID = document.querySelector('#uuid') const ulList = document.querySelector('#list') const btnSend = document.querySelector('#sendBtn') // 向後端建立ws連線 const ws = new WebSocket(host) ws.onopen = (res) => { console.log('open', res) } // 根據context判斷內容是message還是id let id ="" const messageList = [] ws.onmessage = (res)=>{ const data = JSON.parse(res.date) if(data.context === 'message'){ messageList.push(data.message) ulList.innerHTML = messageList.map((message) => `<li>${message}</li>`).join('') } else{ spanUUID.innerHTML = data.id id = data.id } } // 發送資料給後端 btnSend.addEventListener('click', () => { const message = inputMessage.value ws.send(JSON.stringify({context: 'message', message,id})) }) ``` ### 最後測試 開兩個遊覽器或是其中一個使用無痕模式連線到都連線到localhost:3000/websocket.html 並且輸入不同的資料,如果沒問題的話會像下圖 ![image](https://hackmd.io/_uploads/HygSvQIbA.png) ## 參考資料 -[六角學院 WebSocket 實現 1 對 1、1 對多即時通訊](https://www.youtube.com/watch?v=1JH3tLhyzl8&t=738s) -[ws](https://www.npmjs.com/package/ws#server-broadcast)