Websocket 聊天室
第三方库:Socket.IO
Socket.IO 是一个用于实现实时双向通信的库,通常用于构建需要实时交互的 web 应用程序。它建立在 WebSocket 协议之上,但比 WebSocket 提供了更高级的功能和更好的兼容性。
主要特性
- 实时双向通信:支持客户端和服务器之间的实时消息交换。
- 自动重连:连接断开后,Socket.IO 会自动尝试重新连接。
- 事件驱动架构:使用事件的方式处理通信,支持自定义事件,使得开发更加直观和灵活。
- 跨平台兼容性:即使在不支持 WebSocket 的环境中,Socket.IO 也能通过轮询等其他技术进行通信。
- 命名空间(Namespaces):允许通过命名空间将不同的通信逻辑隔离开来,便于管理和扩展。
- 房间(Rooms):可以将客户端分配到特定的房间,便于进行组播、广播等操作。
使用场景
- 即时通讯应用:如聊天软件、客服系统。
- 协同编辑:实时同步文档或表格的编辑状态。
- 多人在线游戏:同步游戏状态和玩家动作。
- 实时数据更新:如股票、天气等实时信息推送。
- 实时通知和警报系统。
服务端:Node.js + Express
客户端:Vue3 + Vite
快速上手
客户端需要安装 sokcet.io-client 这个库,安装完成后需要在 main.js 注册使用这个库
js
// main.js
// 创建一个 socket 客户端实例
const socket = io("http://localhost:3000", {
// 这里是在配置客户端与服务器端建立连接的优先级列表
// 1. 第一优先级使用 websocket
// 2. 第二优先级使用 polling(长轮询)
// 3. 第三优先级使用 flashsocket
transports: ["websocket", "polling", "flashsocket"],
});
// 将 socket 实例挂载到 app.config.globalProperties 上
app.config.globalProperties.$socket = socket;
实战--聊天室
客户端
js
import "./assets/main.css";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { io } from "socket.io-client";
const app = createApp(App);
app.use(router);
// 创建一个 socket 客户端实例
const socket = io("http://localhost:3000", {
// 这里是在配置客户端与服务器端建立连接的优先级列表
// 1. 第一优先级使用 websocket
// 2. 第二优先级使用 polling(长轮询)
// 3. 第三优先级使用 flashsocket
transports: ["websocket", "polling", "flashsocket"],
});
// 将 socket 实例挂载到 app.config.globalProperties 上
app.config.globalProperties.$socket = socket;
app.mount("#app");
js
import { createRouter, createWebHistory } from "vue-router";
import Login from "@/views/Login.vue";
import Chat from "@/views/Chat.vue";
const routes = [
{
path: "/",
name: "login",
component: Login,
},
{
// 动态路由:后面会跟上用户名
path: "/chat/:username",
name: "chat",
component: Chat,
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
vue
<template>
<div class="login-container">
<h1>欢迎来到聊天室</h1>
<p>请输入你在聊天室的昵称</p>
<input
v-model="username"
type="text"
name="username"
id="username"
placeholder="请输入用户名"
class="input"
/>
<button @click="login" class="button">进入聊天室</button>
</div>
</template>
<script setup>
import { ref, getCurrentInstance, onMounted } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const username = ref("");
const { proxy } = getCurrentInstance();
const socket = proxy.$socket; // 获取到了 socket 实例
const login = () => {
if (!username.value) {
alert("请输入用户名");
return;
}
// 通过 socket 来触发一个登录事件
socket.emit("login", username.value);
};
onMounted(() => {
// 在组件进行挂载的时候,需要监听 login 事件
socket.on("login", (data) => {
// 跳转到 chat 页面
router.push({
name: "chat",
params: {
username: data.username,
},
});
});
});
</script>
<style scoped>
.login-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background-color: #f0f2f5;
text-align: center;
}
h1 {
margin-bottom: 20px;
color: #333;
}
p {
margin-bottom: 10px;
color: #555;
}
.input {
padding: 10px;
width: 80%;
max-width: 300px;
margin-bottom: 20px;
border: 1px solid #ccc;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.button {
padding: 10px 20px;
color: white;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #0056b3;
}
</style>
vue
<template>
<div class="chat-container">
<header>
<h1>聊天室</h1>
<p>【{{ username }}】进入了聊天室</p>
<p>当前聊天室人数:{{ count }}</p>
</header>
<!-- 显示聊天的信息 -->
<div class="chat-content" ref="chatContainer">
<div
v-for="(item, index) in content"
:key="index"
:class="getMessageClass(item.type)"
>
{{ item.msg }}
</div>
</div>
<div class="chat-input">
<input
v-model="msg"
type="text"
name="msg"
id="msg"
placeholder="输入消息..."
@keyup.enter="sendMsg"
/>
<button @click="sendMsg" class="send-button">发送</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, getCurrentInstance, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
const router = useRouter(); // 获取到路由实例
const route = useRoute(); // 获取到当前路由
const username = ref("");
const count = ref(0);
const msg = ref("");
const content = ref([]); // 存放聊天内容
const chatContainer = ref(null);
const { proxy } = getCurrentInstance();
const socket = proxy.$socket;
const sendMsg = () => {
if (!msg.value) {
alert("请输入消息");
return;
}
socket.emit("msg", {
username: username.value,
msg: msg.value,
});
msg.value = "";
};
const getMessageClass = (type) => {
if (type === 1) return "rightBox";
if (type === 2) return "leftBox";
return "centerBox";
};
const scrollBottom = () => {
nextTick(() => {
// 获取到 chatContainer 的 DOM 元素
const container = chatContainer.value;
if (container) {
container.scrollTo({
top: container.scrollHeight, // 滚动到底部
behavior: "smooth",
});
}
});
};
onMounted(() => {
// 在组件挂载的时候,就需要做一些处理,以及监听一些事件
// 1. 需要做的一些处理:拿到用户名和聊天室的人数
if (route.params.username) {
// 获取用户名
username.value = route.params.username;
// 通过 socket 来触发一个 count 事件获取在线用户数量
socket.emit("count");
} else {
router.replace("/");
}
socket.on("count", (data) => {
count.value = data;
});
socket.on("msg", (data) => {
if (data.type) {
count.value = data.count;
content.value.push({
type: 3,
msg:
data.type === "loginIn"
? `【${data.username}】进入了聊天室`
: `【${data.username}】离开了聊天室`,
});
} else {
// 说明就是普通的聊天消息
// 将聊天的消息推入到 content 数组中
if (data.username === username.value) {
// 当前处于发布消息的用户
// type 为 1 代表自己发送的消息
// type 为 2 代表其他用户发送的消息
// 这个 type 主要是为了处理样式
content.value.push({
type: 1,
msg: data.msg,
});
} else {
// 其他用户收到消息
content.value.push({
type: 2,
msg: `【${data.username}】说:${data.msg}`,
});
}
}
scrollBottom();
});
});
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f0f2f5;
}
header {
text-align: center;
padding: 20px;
background-color: #007bff;
color: white;
}
.chat-content {
flex: 1;
overflow-y: auto;
padding: 20px;
background-color: #fff;
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
.rightBox,
.leftBox,
.centerBox {
max-width: 70%;
margin: 10px 0;
padding: 10px;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.rightBox {
background-color: #d1e7dd;
margin-left: auto;
}
.leftBox {
background-color: #f8d7da;
margin-right: auto;
}
.centerBox {
text-align: center;
color: #6c757d;
margin: 10px auto;
}
.chat-input {
display: flex;
padding: 10px;
background-color: #f0f2f5;
border-top: 1px solid #ccc;
}
.chat-input input {
flex: 1;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
margin-right: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.send-button {
padding: 10px 20px;
color: white;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.send-button:hover {
background-color: #0056b3;
}
</style>
服务端
js
const createError = require("http-errors");
const express = require("express");
const app = express();
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404));
});
// error handler
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get("env") === "development" ? err : {};
// render the error page
res.status(err.status || 500);
res.render("error");
});
module.exports = app;
js
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require("../app");
const debug = require("debug")("server:server");
const http = require("http");
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || "3000");
app.set("port", port);
/**
* Create HTTP server.
*/
// 创建一个服务器
const server = http.createServer(app);
// 引入 Socket.IO 库,并立即调用以初始化 Socket.IO 服务器
// 传入的参数是一个 HTTP 服务器实例 `server`,将 Socket.IO 服务器绑定到这个 HTTP 服务器上
const io = require("socket.io")(server, {
// 配置 CORS(跨域资源共享)选项,以允许前端应用与后端进行跨域通信
cors: {
// 允许的前端地址,即可以与该 Socket.IO 服务器进行通信的来源
// 这里设置为 "http://localhost:5173",这通常是前端应用在开发环境中的地址
origin: "http://localhost:5173",
// 允许的 HTTP 方法,用于控制前端可以使用哪些方法与服务器进行通信
methods: ["GET", "POST"],
// 允许的 HTTP 头,用于控制前端在请求时可以使用哪些请求头
// 这里允许 "Content-Type" 头,以支持发送不同格式的数据
allowedHeaders: ["Content-Type"],
// 是否允许发送凭证(如 cookies 或 HTTP 认证信息)到服务器
// 设为 true,表示允许发送凭证,这在某些需要认证的情况下非常有用
credentials: true,
},
});
let count = 0; // 保存当前在线用户数量
// 监听连接事件
// 当客户端和服务器建立Websocket连接后,会触发 connection 事件
io.on("connection", function (socket) {
let username = ""; // 保存当前用户的用户名
// 新用户登录
socket.on("login", (data) => {
count += 1; // 在线用户数量加1
username = data; // 保存当前用户的用户名
// 触发当前用户的 login 事件,将当前用户的用户名和在线用户数量发送给客户端
socket.emit("login", {
username: data,
count,
});
// 向其他用户广播一条消息
socket.broadcast.emit("msg", {
username: data,
count,
type: "loginIn",
});
});
socket.on("count", () => {
// 触发客户端的 count 事件,将当前在线用户数量发送给客户端
socket.emit("count", count);
});
socket.on("msg", (data) => {
if (data.username) {
// 要做的事情:
// 1. 将这条消息原封不动的发给当前用户
socket.emit("msg", data);
// 2. 将这条消息广播给所有用户
socket.broadcast.emit("msg", data);
}
});
// 当前的用户离开
socket.on("disconnect", () => {
if (username) {
count -= 1; // 在线用户数量减1
// 向其他用户广播一条消息
socket.broadcast.emit("msg", {
username,
count,
type: "loginOut",
});
}
});
});
/**
* 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() {
const addr = server.address();
const bind = typeof addr === "string" ? "pipe " + addr : "port " + addr.port;
debug("Listening on " + bind);
console.log(`服务器已启动,监听${port}端口...`);
}
-EOF-