Chrome Pointer

2023年12月8日 星期五

使用WebSocket結合node.js實作一個線上聊天室,只需要3個檔案就完成

參考文章

💓使用MongoDB結合Express和Fetch來實作RESTful API簡單網站(包括用戶Post, Get, Update, Delete等功能)

💓基礎>>使用WebSocket結合node.js實作一個線上聊天室,只需要3個檔案就完成

💓進階>>使用WebSocket結合node.js實作一個線上聊天室(進階功能:即時時間動態顯示,表情符號emoji等)

💓超進階>>使用WebSocket結合vue.js實作一個程式碼簡單明瞭的線上聊天室


參考外部文章

💓淺談 WebSocket 協定:實作一個簡單的即時聊天室吧!


- Root Directory
  ├── front.js
  ├── server.js
  └── index.html

server.js
const express = require('express');
const http = require('http');
const WebSocket = require('ws');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// 提供靜態資源(HTML 檔案)
app.use(express.static('.'));

wss.on('connection', function connection(ws) {
  console.log('有新的 WebSocket 連接');

  ws.on('message', function incoming(message) {
    const data = JSON.parse(message);
    //message = message.toString('utf-8')
    console.log('server接收到Name:'+data.name+" ; "+'server接收到訊息:'+data.message);

    // 廣播訊息給所有客戶端
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {  //加了client !== ws && 代表訊息不傳給自己
        console.log("(沒JSON.parse)-message: ", message );
        console.log("(有JSON.parse)-data: ", data );
        client.send(JSON.stringify(data)); //重點!! 這裡需要再把data轉成JSON檔案一次,不然front.js前端拿不到
      }
    });
  });
});

// 啟動伺服器監聽指定的埠口
const PORT = process.env.PORT || 3000;
server.listen(PORT, function() {
  console.log(`伺服器正在監聽埠口 ${PORT}`);
});


👄😉 

Q: 為何需要require('http')和http.createServer(app)呢?

A: 在之前的程式碼中,使用 const express = require('express'); 和 const app = express(); 

是用來建立一個 Express 應用程式。Express 是一個 Node.js 的 Web 應用程式框架,

它提供了簡潔的 API 讓開發者能夠輕鬆建立基於 Node.js 的 Web 伺服器。


在一般的情況下,如果只需要建立一個基本的 Web 伺服器,並不需要額外地使用 WebSocket 

或其他更高級的通訊協議。因此,在一開始只使用 Express 時,

僅需使用 const express = require('express'); 和 const app = express(); 

就足夠建立一個基本的 HTTP 伺服器,可以處理網頁請求、路由、中間件等等。


但是,當你想要在同一個伺服器上實現 WebSocket 功能時,

Express 本身無法直接處理 WebSocket 的通訊。所以,為了在同一個伺服器上實現 WebSocket 功能,

需要使用 Node.js 原生的 HTTP 模組來建立底層的 HTTP 伺服器,以便與 WebSocket 進行整合,


這也是為什麼在原先的程式碼中引入了 const http = require('http'); 

並且使用 http.createServer(app) 的原因。

這樣做可以將 Express 應用程式集成到 Node.js 的 HTTP 伺服器中,

以便在同一伺服器上處理HTTP 請求和 WebSocket 連接。


>>簡單來說,當你需要在同一個伺服器上實現基於 HTTP 和 WebSocket 的通訊時,

就需要使用到 Node.js 的 HTTP 模組來建立底層的 HTTP 伺服器,

然後將 Express 應用程式整合到該伺服器中,以支援這兩種不同的通訊方式。


👄😉 

wss.on 和 ws.on 是用來設置事件監聽器(event listener)的方法,但它們應用在不同的對象上。


1. wss 是 WebSocket Server 的實例,在伺服器端用於監聽客戶端與伺服器建立的 WebSocket 連接。

2. ws 則是單一 WebSocket 連接的實例,它代表伺服器與單一客戶端之間的一個連接。


具體來說:

1. wss.on(event, callback) 用於在 WebSocket Server 實例上設置事件監聽器,當特定事件發生時

(例如 'connection'),就會執行對應的回調函式(callback)。

這是用來管理伺服器上多個 WebSocket 連接的事件。


2. ws.on(event, callback) 則是在單一 WebSocket 連接實例上設置事件監聽器,

用於處理特定 WebSocket 連接的事件,

例如當單一連接收到訊息時觸發 'message' 事件,或者當連接關閉時觸發 'close' 事件。


>>簡單來說來說,wss.on 用於 WebSocket 伺服器端,用來管理多個連接的事件處理,

而 ws.on 則是在每個單一的 WebSocket 連接上用來處理該連接的事件。



👄😉 

這裡是在 WebSocket 伺服器 (wss) 上的所有客戶端 (wss.clients) 上遍歷進行操作。對於每個客戶端,程式碼檢查以下條件:


client !== ws: 確保當前訊息不發送給原始發送訊息的那個客戶端。

client.readyState === WebSocket.OPEN: 確保客戶端的連接狀態是開啟的

(即 WebSocket.OPEN 狀態),這樣才能透過 WebSocket 連接發送訊息。

如果滿足這兩個條件,就會將 message 透過 client.send(message) 發送給該客戶端。


Q: wss.clients是?

A: 當使用 wss.on('connection', function connection(ws) {...}) 設置新連接時的事件監聽器時,

當有新的 WebSocket 客戶端連接到伺服器時,這個 wss.clients 屬性會自動更新,

將新的客戶端 WebSocket 實例加入到這個集合中。


透過 wss.clients 屬性,你可以迭代這個集合,例如使用 forEach 方法,來遍歷所有的連接客戶端,

進而對所有連接做相應的操作,例如廣播訊息給所有客戶端。

這種用法允許伺服器與所有連接的客戶端進行交互動作。


👄😉 

client.readyState 是 WebSocket 物件的屬性,用來表示該 WebSocket 連接的當前狀態。它可以是以下幾個值之一:


- WebSocket.CONNECTING (0):連接尚未建立。

- WebSocket.OPEN (1):連接已建立且可以通訊。

- WebSocket.CLOSING (2):連接正在關閉。

- WebSocket.CLOSED (3):連接已經關閉或無法建立。


WebSocket.OPEN 是 WebSocket 物件的常數,代表 WebSocket 連接已建立且可以通訊的狀態值,它的值是整數 1。

client.readyState 屬性代表 WebSocket 連接的當前狀態,可能的值是 0、1、2 和 3,

分別對應著 WebSocket.CONNECTING、WebSocket.OPEN、WebSocket.CLOSING 和 WebSocket.CLOSED。每個值對應著不同的連接狀態,可以用來判斷連接目前的狀態


在以上程式碼片段中,client.readyState === WebSocket.OPEN 這個條件語句檢查的是該 WebSocket 連接是否處於可以通訊的狀態,如果是的話,表示該連接目前是開啟的狀態,可以進行資料傳輸。


Q: 如果您發現這段程式碼中的訊息沒有被傳送到任何一個客戶端,請確保:

A: 您有多個客戶端已經連接到伺服器。是否加了client !== ws && 代表訊息不傳給自己。

// 廣播訊息給所有客戶端
    wss.clients.forEach(function each(client) {
      if (client !== ws && client.readyState === WebSocket.OPEN) {  
        //加了client !== ws && 代表訊息不傳給自己


如果想要訊息也傳給自己的話,請把client !== ws && 拿掉。

可以看到message有『送出』和『接收』到。

if (client.readyState === WebSocket.OPEN)
可以看到message只有『送出』,沒有『接收』到。

if (client !== ws && client.readyState === WebSocket.OPEN)
在cmd內的資訊如下,可以看出有JSON.parse和沒有JSON.parse的差別。

有新的 WebSocket 連接
server接收到Name:可愛寶 ; server接收到訊息:ㄎㄎ
(沒JSON.parse)-message:  <Buffer 7b 22 6e 61 6d 65 22 3a 22 e5 8f af e6 84 9b e5 af b6 22 2c 22 6d 65 73 73 61 67 65 22 3a 22 e3 84 8e e3 84 8e 22 7d>
(有JSON.parse)-data:  { name: '可愛寶', message: 'ㄎㄎ' }



當要廣播訊息給所有客戶端時,需要再把檔案給JSON.stringify轉成JSON。

client.send(JSON.stringify(data));
//重點!! 這裡需要再把data轉成JSON檔案一次,不然front.js前端拿不到


front.js
// 建立 WebSocket 連接
const socket = new WebSocket('ws://localhost:3000'); // 根據您的伺服器位址和埠號設定

// 連接成功時的事件處理
socket.onopen = function(event) {
  console.log('已連接至 WebSocket 伺服器');
};

// 接收到訊息時的事件處理
socket.onmessage = function(event) {
    // console.log('接收到伺服器訊息:', event.data);
    // 解析接收到的 JSON 資料
    console.log('only event接收到伺服器訊息:', event);
    console.log('before JSON.parse接收到伺服器訊息:', event.data);
    const data = JSON.parse(event.data);
    console.log('JSON.parse接收到伺服器訊息:', data.name+": "+data.message);

    // 將訊息顯示在畫面上
    const messageBox = document.getElementById('messageBox');
    messageBox.innerHTML += `<p>${data.name} : ${data.message}</p>`;
};

// 關閉連接時的事件處理
socket.onclose = function(event) {
  console.log('WebSocket 連接已關閉');
};

// 送出訊息函數
document.getElementById('text_btn').addEventListener('click', () =>{
    const inputName = document.getElementById('inputName').value;
    const inputMessage = document.getElementById('inputMessage').value;

    // 組合名稱和訊息成為一個物件,然後將其轉換成 JSON 格式的字串
    const data = JSON.stringify({ name: inputName, message: inputMessage });

    socket.send(data);
    document.getElementById('inputMessage').value = ''; // 清空輸入框
})


👄😉 

在JavaScript中,當您使用new關鍵字來創建新的物件時,您需要提供一個類(class)

或者構造函數(constructor function)。

const socket = new WebSocket('ws://localhost:3000'); // 根據您的伺服器位址和埠號設定

在程式碼中,WebSocket是瀏覽器提供的內建類,用於建立WebSocket連接。

當使用new WebSocket('ws://localhost:3000')時,JavaScript會知道WebSocket是要創建的物件類別,

並使用指定的URL來建立到該URL的WebSocket連接。



const mongoose = require('mongoose');
// 定義模型 Schema
const Schema = mongoose.Schema;
const userSchema = new Schema({})

對於Schema,它不是JavaScript的內建類或構造函數。Schema來自於Mongoose這個庫(Library),

它是Mongoose庫提供的類(class)或者構造函數(constructor function)。

因此,當需要使用Schema時,您必須首先透過mongoose.Schema來訪問和建立它,

以便使用它來定義資料庫中的模型結構。


JavaScript本身提供了一些內建的物件或類(如Array、Object等),

可以直接使用它們來創建新的物件。

而對於像Mongoose這樣的庫所提供的類(如Schema),需要通過該庫提供的方式來訪問和使用它。



延伸閱讀>>MongoDB有使用到schema


👄😉 
1. socket.onopen 事件:當 WebSocket 連接成功建立時觸發。

2. socket.onmessage事件:當接收到從伺服器發送來的訊息時觸發。同樣地,

3. socket.onclose 事件:當 WebSocket 連接關閉時觸發。

4. socket.send() :是用於向 WebSocket 伺服器發送資料的方法。

這些事件屬於 WebSocket 物件的屬性,並且它們具有特定的名稱,不是任意命名的。

這些名稱都是固定的,在使用 WebSocket 時,您可以依賴這些名稱來處理不同的事件。 


例如,onopen 事件是用來處理 WebSocket 連接成功時的情況,

onmessage 事件用來處理接收到訊息的情況,onclose 事件用來處理連接關閉的情況。


當 socket.send() 被呼叫時,資料將會透過 WebSocket 連接被傳送到伺服器端,

您可以在後端的 WebSocket 伺服器程式中處理這些資料。




👄😉 

可以看出在網頁的console中,event, event.data, JSON.parse(event.data)的差別。

藉由JSON.parse過後的data呼叫的data.name和data.message訊息為最乾淨的。

// 接收到訊息時的事件處理
socket.onmessage = function(event) {
    // console.log('接收到伺服器訊息:', event.data);
    // 解析接收到的 JSON 資料
    console.log('only event接收到伺服器訊息:', event);
    console.log('before JSON.parse接收到伺服器訊息:', event.data);
    const data = JSON.parse(event.data);
    console.log('JSON.parse接收到伺服器訊息:', data.name+": "+data.message);
};



index.html

<!DOCTYPE html>
<html>
<head>
  <title>WebSocket Example</title>
</head>
<body>

<h1>WebSocket Example</h1>

<!-- 顯示訊息的區域 -->
<div id="messageBox"></div>

<!-- 輸入框與送出按鈕 -->
Name<input type="text" id="inputName" placeholder="輸入Name..."></br>
Text<input type="text" id="inputMessage" placeholder="輸入訊息...">
<button id="text_btn">送出</button>

<script src="front.js"></script>
</body>
</html>

實作講解

訊息的接收和送出可以透過network下方的Type為websocket的地方確認。

假如有兩個人分別為「拉拉」和「蓮花」,

他們都連進了聊天室裡面http://127.0.0.1:3000/,並且開始互相傳訊息給對方,結果如下。

                                Data
{"name":"拉拉","message":"想吃拉麵@_@"}                    
{"name":"蓮花","message":"嗨嗨"}                                 
{"name":"拉拉","message":"嗨 你想吃拉麵嗎QAQ"}        
{"name":"蓮花","message":"好挖!!"}       
                       


沒有留言:

張貼留言

喜歡我的文章嗎? 喜歡的話可以留言回應我喔! ^^