WebSocket 聊天室项目(结合Socket.IO)

WebSocket实战项目(结合Socket.IO

  • [1. 前言(为什么使用 Scoket.IO)](#1. 前言(为什么使用 Scoket.IO))
  • [2. 创建 WebScoket 聊天室服务器](#2. 创建 WebScoket 聊天室服务器)
  • [3. 创建 WebScoket 聊天室客户端(Vue3为例)](#3. 创建 WebScoket 聊天室客户端(Vue3为例))
  • [4. 优化](#4. 优化)

1. 前言(为什么使用 Scoket.IO

对 Webcoket 不了解的同学,可以查看我之前的一篇文章《第24章 网络请求与远程资源(下)》。本篇文章承接上篇文章,主要针对相关库 Socket.IO 进行介绍。

虽然,WebSocket 的 API 已经足够简单,只需要掌握onopen、onmessage、onclose 几个基础事件,基本就能完成开发。但是在实际应用中仍然会有许多麻烦,比如你需要区分数据的类型和给谁发送的问题。

http 是请求-响应模型,通过接口的路径可以解决请求和响应数据类型的场景问题。

但是 WebScoket 是全双工通信,你需要通过自行书写类型和判断用户才能解决这个问题,相对麻烦,所以使用成熟的库 Socket.IO 就会方便许多。

接下来,我们将要使用这个库进行一个 WebSocket 聊天室的创作。

2. 创建 WebScoket 聊天室服务器

通常这一步交给后端来做,我们简单实现一遍,作为了解,同时配合下一步的聊天室客户端。

(1)初始化 server 项目:

shell 复制代码
mkdir chat-server
cd chat-server
npm init --y

(2)安装 socket.io

shell 复制代码
pnpm add socket.io

(3)创建 chat.js(聊天室服务器文件):

javascript 复制代码
const { Server } = require("socket.io");

const history = []; // 存储聊天记录 { name: string, content: string, date: number }
let nextId = 1; // 下一个用户的id
const users = new Set(); // 存储当前在线用户数组

const io = new Server({
  path: "/",
  cors: {
    origin: "*",
    methods: ["GET", "POST"],
  },
});

io.on("connection", (socket) => {
  // 分配用户名
  const username = `游客${nextId++}`;
  users.add(username);
  // 广播所有用户(包括自己)
  io.emit("$updateUser", [...users]);
  // 发送聊天记录
  socket.emit("$history", history);
  // 告知用户名
  socket.emit("$name", username);

  // 监听聊天记录
  socket.on("$message", (content) => {
    const msg = {
      name: username,
      content,
      date: Date.now(),
    };
    history.push(msg);
    // 广播消息,通知所有用户
    socket.broadcast.emit("$message", msg);
  });
});

io.listen(9528);
console.log(`Socket.io 聊天室已启动,端口号:9528`);

(4)创建 index.js:

javascript 复制代码
require("./chat");

方便后续做扩展,如果需要创建其他服务器,创建同级js文件,一同引入即可。

(5)修改package.json,加入启动命令:

javascript 复制代码
{
  "name": "chat-server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "serve": "node index.js",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "socket.io": "^4.8.3"
  }
}

(6)终端启动。

shell 复制代码
pnpm serve

3. 创建 WebScoket 聊天室客户端(Vue3为例)

(1)创建 vue3 项目 chat-client

shell 复制代码
npm create vue@latest

根据提示创建 vue3 项目 chat-client。

(2)下载需要的npm包

shell 复制代码
pnpm add sass
pnpm add socket.io-client
pnpm add moment

(3)创建聊天室组件 components/ChatRoom.vue:

javascript 复制代码
<template>
  <div class="container">
    <div class="users">
      <p>聊天室成员</p>
      <ul>
        <li v-for="user in users" :key="user">{{ user }}</li>
      </ul>
    </div>
    <div class="main">
      <div class="content-area" ref="info">
        <div class="item" :class="{ mine: me === h.name }" v-for="(h, i) in history" :key="i">
          <div class="name">{{ h.name }}</div>
          <div class="content">{{ h.content }}</div>
          <div class="date">{{ formatDate(h.date) }}</div>
        </div>
      </div>
      <div class="input-area form">
        <textarea v-model="value" @keydown.enter="handleEnter"></textarea>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import moment from 'moment'
moment.locale('zh-cn')
const { users, history, me } = defineProps({
  users: {
    type: Array,
    default: () => ['Alice', 'Bob', 'Charlie'],
  },
  history: {
    type: Array,
    default: () => [],
  },
  me: {
    type: String,
    required: true,
  },
})

const emit = defineEmits(['chat'])
const value = ref('')

const formatDate = (date) => {
  return moment(date).fromNow().replace(/\s/g, '')
}

const handleEnter = () => {
  const v = value.value.trim()
  if (v) {
    value.value = ''
    emit('chat', { name: me, content: v, date: Date.now() })
  }
}
</script>

<style lang="scss" scoped>
.container {
  background: #fff;
  width: 665px;
  height: 522px;
  display: flex;
  border-radius: 5px;
  box-shadow: -2px 2px 2px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}
.users {
  width: 150px;
  border-right: 1px solid #ccc;
  line-height: 30px;
  overflow: auto;
  flex: 0 0 auto;
}
.users p {
  text-align: center;
  border-bottom: 1px solid #ccc;
}
.users li {
  padding: 0 10px;
  font-size: 12px;
  border-bottom: 1px solid #ccc;
  background: #f1f1f1;
}
.users li:nth-child(2n) {
  background: #fff;
}

.main {
  flex: 1 1 auto;
  background: #f1f1f1;
  display: flex;
  flex-direction: column;
}
.content-area {
  height: 400px;
  padding: 1em;
  overflow: auto;
  border-bottom: 1px solid #ccc;
  font-size: 14px;
  line-height: 1.5;
  flex: 0 0 auto;
  scroll-behavior: smooth;
}
.item {
  float: left;
  max-width: 70%;
  clear: both;
  margin-bottom: 1em;
}
.name {
  font-size: 12px;
  color: #666;
}
.date {
  color: #bbb;
  font-size: 12px;
  text-align: right;
}
.content {
  background: #fff;
  border-radius: 5px;
  padding: 10px;
  margin: 5px 0;
}
.mine.item {
  float: right;
}
.mine .content {
  background: #a9e97a;
}
.mine .name {
  text-align: right;
}
.mine .date {
  text-align: left;
}
.form {
  flex: 1 1 auto;
}
.form textarea {
  width: 100%;
  height: 100%;
  resize: none;
  border: none;
  outline: none;
  padding: 20px;
}
</style>

(4)使用聊天室组件,并且引入socket.io,连接聊天服务器。views/HomeView.vue:

javascript 复制代码
<template>
  <div class="hone-container">
    <ChatRoom :me="me" :users="users" :history="history" @chat="handleChat" />
  </div>
</template>

<script setup>
import ChatRoom from '@/components/ChatRoom.vue'
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { io } from 'socket.io-client'
const me = ref('')
const users = ref([])
const history = ref([])
const socket = io('ws://localhost:9528') // 连接,握手

onMounted(() => {
  socket.on('$updateUser', (newUsers) => {
    users.value = newUsers
  })
  socket.on('$name', (name) => {
    me.value = name
  })
  socket.on('$history', (newHistory) => {
    history.value = newHistory
  })
  socket.on('$message', (message) => {
    history.value.push(message)
  })
})

onBeforeUnmount(() => {
  socket.disconnect() // 断开连接
})

const handleChat = (msg) => {
  history.value.push(msg)
  socket.emit('$message', msg.content)
}
</script>

<style lang="scss" scoped>
.hone-container {
  width: 100wh;
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}
</style>

打开不同的页签,或者浏览器,就能够使用不同游客身份进行对话了


4. 优化

上面只是一个十分简化的例子。真正的项目中,我们需要考虑的还有很多。比如:

(1)服务器通常需要存储用户的详细信息,并且使用用户的唯一标识(比如用户id)作为数组进行存储,而非不断创建游客;

(2)websocket 断线后需要进行重连;

(3)聊天数据需要存储在数据库中。

...

相关文章 《第24章 网络请求与远程资源(下)

相关推荐
无籽西瓜a2 小时前
WebSocket详解含图解:协议特性、握手流程
网络·后端·websocket·网络协议·http
2501_921649492 小时前
外汇实时汇率 API | 24 小时 架构设计与实战指南
大数据·python·websocket·金融·restful
honor_zhang1 天前
Spring Boot集成Websocket服务以及连接时需要注意的问题
spring boot·后端·websocket
REDcker1 天前
libevent、libev 与 libuv:对比、演进与实现原理
linux·c++·后端·编程·c·高并发·服务端
Aurora4191 天前
Compose的事件分发
客户端
小钻风33661 天前
在 Spring Boot 项目中使用 WebSocket 实现实时通信
websocket
Sean‘1 天前
Rancher 日志无法显示?WebSocket 代理配置是罪魁祸首
websocket·网络协议·rancher
曲幽2 天前
FastAPI实战:WebSocket vs Socket.IO,这回真给我整明白了!
python·websocket·nginx·socket·fastapi·web·async·socketio
Densen20142 天前
发布blazor应用到Linux, 使用nginx作为WebSocket代理
linux·websocket·nginx