工作两三年,才实现一个简单的聊天室功能

想必各位同学在学习编程的时候就听说过聊天室项目,最近项目中也使用到了webSocket,但是对webSocket还不是很了解。所以自己在空余的时间实现了一个简单的聊天室功能来加深对webSocket的理解。

webSocket基础内容

WebSocket是一种在单个TCP连接上进行全双工通信的协议。 (没接触的时候一直以为webSocket和udp协议一样,原来websocket是tcp协议的)

WebSocket建立连接的方式通常有两种:

  1. 一种是基于WebSocket协议的连接方式。在这种方式下,客户端和服务器通过WebSocket协议进行协商,确定协议版本、握手等过程,最终建立连接。这个过程通常是由客户端发起的,客户端通过WebSocket API中的WebSocket.connect()方法来连接服务器。一旦连接建立,客户端和服务器就可以通过WebSocket的发送和接收机制进行数据传输。
  2. 另一种是基于HTTP协议的连接方式。在这种方式下,客户端通过HTTP请求与WebSocket服务端协商升级协议。具体来说,客户端会发送一个包含特殊头部信息的HTTP请求给服务器,服务器会根据这个请求进行协议升级。协议升级完成后,后续的数据交换则遵照WebSocket的协议进行。这种方式的连接建立是基于HTTP的握手通道的,因此与WebSocket协议方式的连接建立有所不同。

简单的说就是建立webSocket连接可以使用ws协议和http协议,但是通过http协议的方式需要包含特殊的请求头,请求头的信息包含:

  • "Upgrade": 这个头部字段的值设置为"websocket",表示客户端希望将连接升级为WebSocket协议。
  • "Connection": 这个头部字段的值设置为"Upgrade",表示希望保持长连接,而不是完成一次性的请求-响应后就关闭连接。
  • "Sec-WebSocket-Version": 这个头部字段指定了WebSocket协议的版本,例如"13"表示WebSocket协议的版本13。
  • "Sec-WebSocket-Key": 这个头部字段的值是一个随机生成的字符串,用于在连接升级过程中进行安全性验证。服务器会根据这个值生成一个响应头部,来确认连接升级的合法性。

结合代码理解

js 复制代码
// 创建WebSocket对象 
var ws = new WebSocket('ws://example.com/ws'); 
// 连接建立后的处理函数 
ws.onopen = function(event) { 
    console.log('WebSocket connected'); // 在这里可以发送消息给服务器 
}; 
// 接收到服务器消息的处理函数 
ws.onmessage = function(event) { 
    console.log('Received message:', event.data); // 在这里可以处理接收到的消息 
}; 
// 连接关闭后的处理函数 
ws.onclose = function(event) { 
    console.log('WebSocket disconnected:', event.code, event.reason); 
}; 
// 连接错误的处理函数 
ws.onerror = function(error) { 
    console.error('WebSocket error:', error); 
}; 
// 发送消息给服务器的处理函数 
function sendMessage() { 
    var message = document.getElementById('message').value; 
    ws.send(message); 
}

以上是基于WebSocket协议的连接方式

js 复制代码
// 生成一个随机的Sec-WebSocket-Key 
const randomKey = Math.random().toString(36).substring(7); 
// 将Sec-WebSocket-Key进行Base64编码 
const encodedKey = btoa(randomKey); 
// 构造请求头信息 
const headers = { 
    'Sec-WebSocket-Version': '13',
    'Sec-WebSocket-Key': encodedKey,
    'Sec-WebSocket-Protocol': 'chat' 
}; 
// 发起POST请求,建立Websocket连接 
fetch('wss://example.com/ws', { 
        method: 'POST', 
        headers: headers, 
        body: '' // 请求正文为空 
    })
    .then(response => response.text()) // 获取响应文本 
    .then(data => { 
        if (data === 'Connection established') { 
            console.log('Websocket connection established'); 
            // 在连接建立后,使用WebSocket的发送和接收机制进行数据传输 
            // ... 
        } else { 
            console.log('Websocket connection failed:', data);
        } 
    }) 
    .catch(error => console.error('Error:', error));

首先生成了一个随机的Sec-WebSocket-Key,然后将其进行Base64编码。接下来,构造了一个包含请求头信息的对象,其中指定了Websocket协议的版本、Sec-WebSocket-Key和子协议。然后,使用Fetch API发起了一个POST请求,将请求发送到服务器,其中请求头信息和请求正文为空。

在获取到响应后,通过response.text()方法获取响应文本。如果响应文本为"Connection established",表示Websocket连接已经建立。在这个示例中,我们在控制台输出了一条消息。在实际应用中,你可以根据具体的需求,在连接建立后使用WebSocket的发送和接收机制进行数据传输。 (原来除了使用html5 的 websocket Api之外,使用普通的请求方式也是可以连接websocket的,当然需要加上这些特殊的请求头)

聊天室简单版代码

客户端代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>聊天室</title>
  <style>
    * {
      box-sizing: border-box;
      overflow: hidden;
    }
    html, body {
      padding: 0;
      margin: 0;
    }
    .config {
      position: absolute;
      z-index: 100;
      width: 100%;
      height: 100%;
      background: #40E0D0;  /* fallback for old browsers */
      background: -webkit-linear-gradient(to right, #FF0080, #FF8C00, #40E0D0);  /* Chrome 10-25, Safari 5.1-6 */
      background: linear-gradient(to right, #FF0080, #FF8C00, #40E0D0); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .config--box {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      background-color: #f5f5f5;
      border: 1px solid #ccc;
      border-radius: 5px;
      padding: 20px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    .config--title {
      font-size: 24px;
      font-weight: bold;
      margin-bottom: 20px;
    }

    .config--ipt {
      display: flex;
      align-items: center;
      justify-content: center;
      margin-bottom: 20px;
    }

    .config--ipt input {
      border: none;
      border-radius: 5px;
      padding: 10px;
      font-size: 16px;
      outline: none;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    .config--btn button {
      border: none;
      border-radius: 5px;
      padding: 10px 20px;
      font-size: 16px;
      font-weight: bold;
      color: #fff;
      background-color: #007bff;
      cursor: pointer;
      outline: none;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
    }

    .config--btn button:hover {
      background-color: #0069d9;
    }

    .index {
      display: flex;
      flex-direction: column;
      height: 100vh;
      background-color: #f5f5f5;
      padding: 20px;
    }

    #index #messageBox {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      justify-content: flex-start;
      height: calc(100vh - 200px);
      overflow-y: auto;
    }

    #index #messageBox div {
      width: 100%;
      border-radius: 5px;
      padding: 10px;
      margin-bottom: 10px;
    }

    #index #messageBox div span {
      font-size: 12px;
    }

    #index #messageBox div p {
      margin: 0;
    }

    #messageBox {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      justify-content: flex-start;
      height: 400px;
      overflow-y: auto;
    }

    .center {
      text-align: center;
      font-size: 12px;
      color: #666;
    }

    .left {
      display: flex;
      flex-direction: column;
      align-items: flex-start;
      justify-content: flex-start;
    }

    .left .bubble {
      margin-bottom: 10px;
      border-radius: 20px;
      padding: 10px 15px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      background-color: #fff;
      color: #333;
    } 
    .left span {
      margin-left: 10px;
    }

    .right {
      display: flex;
      flex-direction: column;
      align-items: flex-end;
    }

    .right .bubble { 
      margin-bottom: 10px;
      border-radius: 20px;
      padding: 10px 15px;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      background-color: #def1fc;
      color: #333;
    }

    .right span {
      margin-right: 10px;
    }

    .operation {
      position: relative;
    }

    #index textarea {
      border: none;
      border-radius: 5px;
      padding: 10px;
      font-size: 16px;
      outline: none;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      margin-right: 10px;
      flex-grow: 1;
      width: 100%;
    }

    #index button {
      border: none;
      border-radius: 5px;
      padding: 10px 20px;
      font-size: 16px;
      font-weight: bold;
      color: #fff;
      background-color: #007bff;
      cursor: pointer;
      outline: none;
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      position: absolute;
      right: 20px;
      top: 20px;
    }

    #index button:hover {
      background-color: #0069d9;
    }
  </style>
</head>
<body>
  <div id="config" class="config">
    <div class="config--box">
      <div class="config--btn">
        <button id="connectBtn">进入聊天室</button>
      </div>
    </div>
  </div>
  <div id="index" class="index">
    <div id="messageBox">
    </div>
    <div class="operation">
      <textarea id="chat" cols="30" rows="10" placeholder="发送消息"></textarea>
      <button id="sendBtn">发送</button>
    </div>
  </div>
  <script>
    let ws = null;
    const list = [];

    let identifier = '';

    const connectBtn = document.getElementById('connectBtn');
    const config = document.getElementById('config');
    const sendBtn = document.getElementById('sendBtn');

    const outPut = (obj) => {
      list.push({
        key: list.length,
        ...obj,
      })
      let str = ''
      list.forEach(item => {
        switch (item.type) {
          case 'me':
            str += `<div class="right">
                      <span>我</span>
                      <p class="bubble">${item.data}</p>
                    </div>`
            break;
          
          case 'people': 
            
            str += `<div class="left">
                      <span>
                        ${item.ip}
                      </span>
                      <p class="bubble">${item.data}</p>
                    </div>`
            break

          case 'system':
            str += `<div class="center">${item.data}</div>`
            break;

          default:
            break;
        }
      });
      document.getElementById('messageBox').innerHTML = str;
    }

    function send(obj) {
      const sendMsg = JSON.stringify(obj)
      if(ws) {
        ws.send(sendMsg)
      }
    }

    connectBtn.addEventListener('click', () => {
      if(!ws) {
        ws = new WebSocket('ws://你的后端ip地址:3000/ws');
        ws.onopen = function (params) {
          outPut({
            type: 'system',
            data: '我加入聊天',
          })
        };
        ws.onmessage = function (e) {
          const message = JSON.parse(e.data)
          if(message.type === 'init') {
            identifier = message.identifier
          } else {
            outPut({
              ...message,
              type: message.identifier === identifier ? 'me' : 'people'
            })
          }
        };
        ws.onclose = function(evt) {
          outPut({
            type: 'system',
            data: '聊天室关闭'
          })
        };
        ws.onerror = function (evt) {
          outPut({
            type: 'system',
            data: '连接失败'
          });
        };
      } else {
        outPut({
          type: 'system',
          data: '加入聊天'
        })
      }
      config.style.display = 'none';
    })

    sendBtn.addEventListener('click', () => {
      const msg = document.getElementById('chat').value;
      if(!msg && msg === '') return
      send({
        identifier,
        data: msg,
      });
      document.getElementById('chat').value = '';
    });

    document.addEventListener("keydown", function(event) {  
      if (event.key === "Enter") {  
        const msg = document.getElementById('chat').value;
        if(!msg && msg === '') return
        send({
          identifier,
          data: msg,
        });
          document.getElementById('chat').value = '';
        }  
    });
  </script>
</body>
</html>

服务端代码

js 复制代码
const WebSocket = require('ws');
const http = require('http');
const express = require('express');
const app = express();

// 创建一个HTTP服务器
const server = http.createServer(app);

// 存储每个客户端的标识  
const clients = new Map();

// 创建WebSocket服务器并将其附加到HTTP服务器
const wss = new WebSocket.Server({ noServer: true });

// 生成唯一的客户端标识符  
function generateClientId() {  
  return Math.random().toString(36).substring(7);  
}

server.on('upgrade', (request, socket, head) => {
    switch (request.url) {
        case '/ws':
            wss.handleUpgrade(request, socket, head, (ws) => {
                Socket = socket
                wss.emit('connection', ws, request);
            });
        break;
        default:
        break;
    }
});

// 监听WebSocket连接事件
wss.on('connection', (socket, request) => {
    // 获取客户端的唯一标识符  
    const clientId = generateClientId(); 
    // 将客户端标识符存储在映射中  
    clients.set(clientId, socket);
    
    socket.send(JSON.stringify({identifier: clientId, type: 'init'}));

    // 获取客户端的IP地址  
    const clientIP = request.connection.remoteAddress;  
    // 监听客户端发送的消息
    socket.on('message', async(message) => {
        // 解析消息中的标识符和数据  
        const { identifier, data, type } = JSON.parse(message);
        // 如果标识符不匹配,将消息广播给其他客户端  
        clients.forEach((client) => {  
            if (client.readyState === WebSocket.OPEN) {
              client.send(JSON.stringify({
                ip: clientIP,
                identifier,
                data,
              }));  
            }  
        });
    });

    wss.on('error', (message) => {
        console.log('报错了');
    });
    wss.on('close', (message) => {
        console.log('连接关闭:')
        // 移除客户端的标识符  
        clients.delete(clientId);
    })
    wss.on('open', (message) => {
        console.log('open', message)
     })
});

// 启动HTTP服务器
server.listen(3000, () => {
     console.log('HTTP服务器已启动,监听端口3000');
});

客户端使用vscode插件golive插件就可以用啦,服务端代码复制到index.js文件中然后node index一下就启动了,切换一下客户端接口地址你就可以在局域网中和其他同学聊起来了。

刚开始使用websocket时遇到的问题

  1. 最开始使用websocket的时候使用new webSocket()时候里面必须要使用 ws:// 或者 wss:// 不能使用 http:// 欢迎补充O(∩_∩)O哈哈~

总结

先这样,再这样,然后这样,接着这样,铛铛铛铛!就弄出来啦,你学废了吗😜

相关推荐
写bug的小屁孩2 小时前
websocket初始化
服务器·开发语言·网络·c++·websocket·网络协议·qt creator
清尘沐歌16 小时前
有什么好用的 WebSocket 测试工具吗?
websocket·网络协议·测试工具
清尘沐歌16 小时前
有什么好用的 WebSocket 调试工具吗?
网络·websocket·网络协议
专注VB编程开发20年19 小时前
一篇介绍 Websocket 和 Http 的很好的帖子
websocket·网络协议·http·聊天协议·传输文件
heilai41 天前
workerman的安装与使用
c++·websocket·http·php·phpstorm·visual studio code
wclass-zhengge2 天前
Netty篇(WebSocket)
网络·websocket·网络协议
猛男敲代码2 天前
SSE与WebSocket与MQTT
前端·javascript·websocket
IT大玩客2 天前
SpringBoot如何集成WebSocket
spring boot·后端·websocket
凡人的AI工具箱5 天前
15分钟学 Go 实战项目三 : 实时聊天室(学习WebSocket并发处理)
人工智能·后端·websocket·学习·golang
不喝水的鱼儿5 天前
【LuatOS】基于WebSocket的同步请求框架
网络·websocket·网络协议·go·luatos·lua5.4