主题
socket 的应用
一、websocket
1、websocket 概述
1.1 websocket 是什么?
- WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。
1.2 为什么要用 websocket?
HTTP 协议有一个缺陷:通信只能由客户端单向发起,这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询"(每隔一段时候,就发出一个询问,了解服务器有没有新的信息)。最典型的场景就是聊天室。 轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。
1.3 wensocket 有什么用?
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
1.4 特点
WebSocket 协议在 2008 年诞生,2011 年成为国际标准。所有浏览器都已经支持了。 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种
其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信(只要服务端支持 socket 通信即可)。
(6)协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。
- 例如:ws://example.com:80/some/path
2、websocket 的使用
2.1 服务端 websocket(Nodejs)
要在 nodejs 中使用 websocket,有很多种方案,比如:nodejs-websocket,socket.io,这里先介绍 nodejs-websocket 这个第三方库。 nodejs-websocket 的 github 地址:https://github.com/sitegui/nodejs-websocket
2.1.1、安装 nodejs-websocket 模块
bash
npm install nodejs-websocket --save
2.1.2、编写 nodejs 服务端代码
js
const ws = require("nodejs-websocket");
ws.createServer((conn) => {
conn.on("text", (str) => {
console.log("接收到的String:" + str);
conn.sendText(`客户端,这是你给我发的消息: 【${str}】,我收到了`);
});
conn.on("close", (code, reason) => {
console.log("连接关闭:", code, reason);
});
conn.on("error", (err) => {
console.log("wobsocket错误", err);
});
}).listen(3000, () => {
console.log("socket已连接,正在监听客户端消息.....");
});
2.1.3、nodejs-websocket 的 API
这里仅仅标注了很少一部分,建议参考如下地址:
服务端 API 使用请参考nodejs-websocket 文档 API 中文介绍参考:https://www.jianshu.com/p/f0baf93a3795
const ws = require("nodejs-websocket");
ws:引入 nodejs-websocket 后的主要对象
ws 上面的 方法 | 描述 |
---|---|
ws.createServer([options], [callback]) | 创建并返回一个 server 对象 |
ws.connect(URL, [options], [callback]) | 创建一个 connect 对象,一般由客户端链接服务端 websocket 服务时创建 |
ws.setBinaryFragmentation(bytes) | 设置传输二进制文件的最小尺寸,默认 512kb |
setMaxBufferLength | 设置传输二进制文件的最大尺寸,默认 2M |
Server:通过 ws.createServer 创建
方法 | 描述 |
---|---|
server.listen(port, [host], [callback]) | 传入端口和主机地址后,开启一个 websocket 服务 |
server.close([callback]) | 关闭 websocket 服务 |
server.connections | 返回包含所有 connection 的数组,可以用来广播所有消息 |
js
// 服务端广播
function broadcast(server, msg) {
server.connections.forEach(function (conn) {
conn.sendText(msg);
});
}
2.2 客户端 websocket
参考 MDN 由于所有浏览器都已经支持了 websocket,所以客户端一般情况下没必要使用 websocket 的库,直接使用浏览器提供的对象即可实现 websocket 链接。
2.2.1、客户端的 API
(1)创建 websocket 实例 WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。
js
var ws = new WebSocket(url[, protocols]);
解释:
- url: 指定连接的 URL
- protocol: 可选的,指定了可接受的子协议
- 执行此语句之后,客户端就会与服务器进行连接
例如:
javascript
var ws = new WebSocket("ws://127.0.0.1:3000");
实例对象的所有属性和方法清单,https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
(2)websocket 实例上的属性
实例上的属性通过 websocket 实例 ws 直接调用,例如:ws.readyState
一些常用属性如下:
websocket属性 | 取值 | 取值描述 | 其他 |
---|---|---|---|
ws.readyState | 0 | 表示正在连接 | 等价与 WebSocket.CONNECTING |
1 | 表示连接成功,可以通信了 | 等价与 WebSocket.OPEN | |
2 | 表示连接正在关闭 | 等价与 WebSocket.CLOSING | |
3 | 表示连接已经关闭,或者打开连接失败 | 等价与 WebSocket.CLOSED | |
ws.bufferedAmount | 只读属性 | 未发送至服务器的字节数。 | 已被 send() 放入正在队列中等待传输, 但是还没有发出的 UTF-8 文本字节数。 |
一些常用方法
ws 是 Websocket 的实例化对象
方法 | 描述 |
---|---|
ws.send() | 用于向服务器发送数据 |
ws.close() | 用于关闭当前链接。 |
一些常用事件
以下事件是 Websocket 的实例化对象 ws 拥有的事件 使用 addEventListener() 或 直接使用 on 事件绑定方式,例如:ws.onopen=fn、ws.onmessage=fn、..., 如果要指定多个回调函数,请用
ws.addEventListener('message',fn)
事件 | 描述 |
---|---|
open | 连接建立时触发 |
message | 客户端接收服务端数据时触发 |
error | 通信发生错误时触发 |
close | 连接关闭时触发 |
2.2.2、客户端示例代码
js
if (window.WebSocket) {
//当设备支持websocket时,开始建立客户端链接
var ws = new WebSocket("ws://127.0.0.1:3000");
// 连接建立时触发
ws.onopen = function (e) {
console.log("连接服务器成功");
ws.send("今天天气真好"); //主动向服务端发起消息
};
// 接收到服务端数据时触发
ws.onmessage = function (e) {
//在这里,e表示服务端发送而来的内容
console.info("服务端说==>:", e.data);
};
//当链接被服务端关闭时触发
ws.onclose = function (e) {
console.log("服务器关闭");
};
// 当服务端通信发生错误时触发
ws.onerror = function () {
console.log("连接出错");
};
} else console.info("你的设备不支持websocket!");
2.3 演示结果
3、websocket 实现简单聊天室
聊天室思路:
- 客户端发送数据到服务器,然后服务器遍历所有的聊天室中的连接,将消息广播出去,聊天室内所有客户端接收服务端发来的消息,更新到页面上,呈现广播文本。
- 一对一聊天更简单,服务端不需要广播遍历了,只需要将消息转发给特定的用户即可。
3.1 客户端
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Websocket</title>
</head>
<body>
<h1>Chat Room</h1>
<input type="text" id="sendTxt">
<button id="sendBtn">发送</button>
<script>
// 用于更新消息到页面上
function showMessage(str, type) {
var div = document.createElement("div");
div.innerHTML = str;
if(type == "enter") {
div.style.color = "blue";
} else if(type == "leave") {
div.style.color = "red";
}
document.body.appendChild(div);
}
// 创建websocket实例
var websocket = new WebSocket("ws://127.0.0.1:3003/");
// Socket.onopen事件 连接建立时触发
websocket.onopen = function() {
console.log("websocket open");
document.getElementById("sendBtn").onclick = function() {
var txt = document.getElementById("sendTxt").value;
if(txt) {
websocket.send(txt);
}
}
}
// 连接关闭时触发
websocket.onclose = function() {
console.log('websocket close');
}
// 客户端接收服务端数据时触发 e是MessageEvent
websocket.onmessage = function(e) {
console.log(e.data);
var mes = JSON.parse(e.data);
showMessage(mes.data, mes.type);
}
</script>
</body>
</html>
3.2 服务端
js
// 引入express(用于承载html的静态资源服务)
const express = require("express");
const app = express();
// 引入服务端核心的websocket模块
const ws = require("nodejs-websocket");
// 托管客户端html静态资源
app.use(express.static("./static"));
// websocket端口
const port = 3003;
const hostname = "127.0.0.1";
// http服务器监听80端口
app.listen(80, () => {
console.log("服务器启动成功,prot:" + 80);
});
// 客户端计数器
var clientCount = 0;
// 创建一个server对象, ws是引入nodejs-websocket后的主要对象
var server = ws
.createServer(function (conn) {
console.log("New connection");
clientCount++; // 有用户连接进来,计数器+1
conn.nickname = "user" + clientCount; // 每个用户的昵称不同
/*
服务器发送给客户端数据用sendText()必须是字符串,我们可以使用
JSON.stringify()方法将JavaScript对象转换为字符串
*/
var mes = {};
mes.user = conn.nickname;
mes.type = "enter";
mes.data = conn.nickname + " comes in";
broadcast(JSON.stringify(mes)); // 刚播xxx用户线
// 收到文本时触发,str是收到的文本字符串
conn.on("text", function (str) {
console.log("Received " + str);
var mes = {};
mes.user = conn.nickname;
mes.type = "message";
mes.data = conn.nickname + " says: " + str;
broadcast(JSON.stringify(mes)); // 把客户端收到的消息广播给每一位用户
});
// 连接关闭时触发
conn.on("close", function (code, reason) {
console.log("Connection closed");
var mes = {};
mes.user = conn.nickname;
mes.type = "leave";
mes.data = conn.nickname + " left";
broadcast(JSON.stringify(mes)); // 广播xxx用户下线
});
// 发生错误时触发,如果握手无效,也会发出响应
conn.on("error", function (err) {
console.log("handle err");
console.log(err);
});
})
.listen(port); // ws服务监听端口
console.log(
"websocket server listening on port: " + port + ", on hostname: " + hostname
);
// server.connections返回包含所有connection的数组,可以用来广播所有消息
// 服务端广播
function broadcast(msg) {
server.connections.forEach(function (conn) {
conn.sendText(msg);
});
}
3.3 效果展示
二、Socket.io
Socket.IO 是一个库,可以在浏览器和服务器之间实现实时、双向和基于事件的通信。
1、Socket.io 与 websocket 的关系
- (1)WebSocket 是 HTML5 最新提出的规范,虽然主流浏览器都已经支持,但仍然可能有不兼容的情况,为了兼容所有浏览器,给程序员提供一致的编程体验,SocketIO 将 WebSocket、AJAX 和其它的通信方式全部封装成了统一的通信接口,
- (2)也就是说,我们在使用 SocketIO 时,不用担心兼容问题,底层会自动选用最佳的通信方式。
- (3)因此说,WebSocket 是 SocketIO 的一个子集。
- (4)Socket.io 会自动根据浏览器从 WebSocket、AJAX 长轮询、Iframe 流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达 IE5.5,应该可以满足绝大部分需求了。
2、socket.io 的组成
- (1)服务端
- 用于集成(或挂载)到 Node.JS HTTP 服务器:socket.io
- (2)客户端
- 加载到浏览器中的客户端:socket.io-client
开发环境下, socket.io 会自动提供客户端。 当我们在生产环境下使用时,我们就应该单独的使用 socket 客户端模块了,而不是共用服务端的 socket 模块。
3、scoket.io 的官方文档
英文文档:https://socket.io/get-started/chat/(推荐) 中文文档:https://www.w3cschool.cn/socket/socket-1olq2egc.html
4、socket.io 的使用
4.1 服务端基本使用
服务端以 express 为例
(1)创建服务端 express 项目,安装 socket.io
js
mkdir scoket-demo
cd ./scoket-demo
npm init -y
npm i express socket.io -S
(2)编写 socke.io 服务端代码
js
var express = require("express");
var app = express();
var http = require("http");
//将express模块实例作为回调构建http模块实例
var server = http.Server(app);
// 通过传入 server (HTTP 服务器) 对象初始化了 socket.io 的一个实例
var io = require("socket.io")(server); // 这句话写上时候,socket.io就会把客户端(js文件)挂载到服务器
server.listen(81);
app.get("/", function (req, res) {
// 浏览器访问http://127.0.0.1:81,将看到页面index.html
res.sendFile(__dirname + "/index.html");
});
/*
监听 connection 事件来接收 sockets
(前端每次执行一次io()方法就就会发起一次socket请求)
*/
io.on("connection", function (socket) {
// 只要进入这个回调函数,就说明客户端已经连接进来了
// 给客户端发送消息
socket.emit("news", { hello: "world" });
socket.on("my other event", function (data) {
console.log("客户端发来的消息:", data);
});
//如果是断开socket请求,就会触发下面的代码
socket.on("disconnect", function () {
console.log("连接断开了...");
});
});
const io = new Server(server, {});
这句话写上,socketio 就已经把客户端挂在到服务器, 验证: 访问http://127.0.0.1:3000/socket.io/socket.io.js 但是只能用于开发环境,生产环境需要单独下载引入
4.2 客户端基本使用
开发环境下, socket.io 会自动提供客户端。省去了下载
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>Document</title>
</head>
<body>
<!-- 这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。 -->
<script src="/socket.io/socket.io.js"></script>
<script>
// var socket = io.connect('http://localhost:81'); // 要解决跨域
// 请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。
var socket = io(); // 写上这一句,我们的客户端就已经脸上服务器了
// 监听服务器推送的消息
socket.on("news", function (data) {
console.log("服务器推送的消息:", data);
socket.emit("my other event", { my: "hello 服务器" });
});
</script>
</body>
</html>
var socket = io();
写上这一句,我们的客户端就已经脸上服务器了 验证:访问http://127.0.0.1:3000 >
4.3 运行效果
5、socket.io 常用的 API
5.1 创建服务(node 服务)
5.1.0 基础 API
下面两种方式都可以创建服务
js
const io = require('socket.io')(httpServer[, options]);
或者
js
const Server = require('socket.io');
const io = new Server(httpServer[, options]);
(1)、new Server(httpServer[, options])
:基于 http 服务器创建 scoket (2)、new Server(port[, options])
:创建独立的 socket (3)、new Server(options)
基础 API 参数详解 更多 API 建议参考英文官网,中文的翻译出来有些出入:https://socket.io/docs/v4/server-api/
参数 | 说明 | - |
---|---|---|
httpServer (http.Server) | 要绑定的服务器 | - |
port (Number) | 要侦听的端口 | |
options (Object) | path (String) | 捕获路径的名称(默认:"/socket.io" )前后端需要一致(可理解为路由) |
serveClient (Boolean) | 是否提供客户端文件(默认:true ) | |
adapter (适配器) | 适配器使用 参考:socket.io-adapter,配合redis使用的 | |
origins (String) | 允许的originins(默认:* ) | |
parser (Parser) | 解析器使用。默认为Parser socket.io附带的实例 | |
pingTimeout (Number) | 有多少ms没有乒乓包考虑连接close(60000) | |
pingInterval (Number) | 发送新的ping packet(25000)之前有多少ms | |
transports (Array <String> ) | 传输以允许连接到(['polling', 'websocket']) |
注意:
- 1、
pingTimeout
和pingInterval
两个参数将在客户端知道服务器不再可用之前影响延迟- 例如,如果底层 TCP 连接由于网络问题而未正确关闭,则 pingTimeout + pingInterval 在获取 disconnect 事件之前,客户端可能必须等待最多 ms 。
- 2、
transports
参数顺序很重要。默认情况下,首先建立长轮询连接,然后如果可能,升级到 WebSocket。使用['websocket']方法如果无法打开 WebSocket 连接,则不会有后备。
5.1.1 创建独立的 socket 服务(有自己的端口)
js
const { Server } = require("socket.io");
const io = new Server({
/* options */
});
io.on("connection", (socket) => {
// ...
});
io.listen(3000);
您还可以将端口作为第一个参数传递
js
const { Server } = require("socket.io");
const io = new Server(3000, {
/* options */
});
io.on("connection", (socket) => {
console.log(".......");
});
5.1.2 使用 http 服务器
不会创建单独的 ws 服务,而是使用 http 服务承载 ws 服务
- (1)原生 nodejs
js
// 原生nodejs服务
const { createServer } = require("http");
const { Server } = require("socket.io");
const httpServer = createServer();
const io = new Server(httpServer, {
/* options */
});
io.on("connection", (socket) => {
// ...
});
httpServer.listen(3000);
- (2)Express
js
// Express服务
const express = require("express");
const { createServer } = require("http");
const { Server } = require("socket.io");
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
/* options */
});
io.on("connection", (socket) => {
// ...
});
httpServer.listen(3000);
- (3)Koa
js
// Koa服务
const Koa = require("koa");
const { createServer } = require("http");
const { Server } = require("socket.io");
const app = new Koa();
const httpServer = createServer(app.callback());
const io = new Server(httpServer, {
/* options */
});
io.on("connection", (socket) => {
// ...
});
httpServer.listen(3000);
5.2 发送消息emit
方法
语法:
socket.emit(eventName [,... args] [,ack])
返回返回 Socket 描述:将一个事件发送到由字符串名称标识的 socket。可以包括任何其他参数。支持所有可序列化的数据结构,包括 Buffer。 客户端服务端方法都一样
5.2.1 服务端发送消息
js
const { Server } = require("socket.io");
const io = new Server({ /* options */ });
io.on("connection", (socket) => {
// 发送给当前客户端
socket.emit('news', 'can you hear me?', 1, 2, 'abc');
// 发送给所有客户端,除了发送者
socket.broadcast.emit('broadcast', 'hello friends!');
// 发送给同在 'game' 房间的所有客户端,除了发送者
socket.to('game').emit('nice game', "let's play a game");
// 发送给同在 'game1' 或 'game2' 房间的所有客户端,除了发送者
socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");
// 发送给同在 'game' 房间的所有客户端,包括发送者
io.in('game').emit('big-announcement', 'the game will start soon');
// 发送给同在 'myNamespace' 命名空间下的所有客户端,包括发送者
io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');
// 发送给指定 socketid 的客户端(私密消息)
socket.to(<socketid>).emit('hey', 'I just met you');
// 包含回执的消息
socket.emit('question', 'do you think so?', function (answer) {});
// 不压缩,直接发送
socket.compress(false).emit('uncompressed', "that's rough");
// 如果客户端还不能接收消息,那么消息可能丢失
socket.volatile.emit('maybe', 'do you really need it?');
// 发送给当前 node 实例下的所有客户端(在使用多个 node 实例的情况下)
io.local.emit('hi', 'my lovely babies');
});
io.listen(3000);
5.2.2 客户端发送消息
html
<body>
<script src="/socket.io/socket.io.js"></script>
<!-- 或者 生产环境 -->
<script
src="https://cdn.socket.io/4.4.0/socket.io.min.js"
integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj"
crossorigin="anonymous"
></script>
<script>
// 这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。
//请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。
window.onload = function () {
var socket = io("http://localhost:3000");
var form = document.querySelector("form");
var val = document.querySelector("#m");
form.onsubmit = function () {
socket.emit("news", val.value);
val.value = "";
return false;
};
};
</script>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
</body>
5.3 接收消息 on
方法
语法:
socket.on(flag,callback)
flag 是事件的名称,前后端需一致 客户端服务端方法都一样
5.3.1 服务端接收消息
js
const { Server } = require("socket.io");
const io = new Server({
/* options */
});
io.on("connection", (socket) => {
// socket监听客户端推送的消息
socket.on("news", function (msg) {
console.log("message: " + msg);
});
});
io.listen(3000);
5.3.2 客户端接收消息
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>
<script src="/socket.io/socket.io.js"></script>
<!-- 或者 生产环境-->
<script
src="https://cdn.socket.io/4.4.0/socket.io.min.js"
integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj"
crossorigin="anonymous"
></script>
<script>
// 这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。
//请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。
window.onload = function () {
var socket = io("http://localhost:3000");
var form = document.querySelector("form");
var val = document.querySelector("#m");
form.onsubmit = function () {
//接收服务器发来的消息
socket.on("news", function (data) {
console.log(data);
});
};
};
</script>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
</body>
</html>
5.4 文件的发送与接收
5.4.1 发送文件的两种方式
- 方式一:使用 ajax 形式将文件发送到服务端,服务端进行保存后,将地址转发给客户端,然后对应客户端就能显示我们的图片了。
- 方式二:使用 H5 的新 api【FileReader】读取文件(图片),然后转换为 base64 编码的形式进行发送到客户端,由于是 base64 字符串的形式,所以服务端收到后就可以直接发送到对应客户端。
这里介绍【方式二】,以图片为例
5.4.2 客户端发送图片
js
let Imginput = document.getElementById("...");
let file = Imginput.files[0]; //得到该图片
let fileReader = new FileReader(); //创建一个FileReader对象,进行下一步的操作
fileReader.readAsDataURL(file); //通过readAsDataURL读取图片
fileReader.onload = function () {
//读取完毕会自动触发,读取结果保存在result中
let data = { img: this.result };
socket.emit("sendImg", data);
};
5.4.3 服务端接收与转发
js
socket.on("sendImg", (data) => {
data.id = socket.id;
io.emit("receiveImg", data);
});
5.4.4 客户端接收
js
socket.on("receiveImg", (data) => {
let img = data.img; // 接收到的图片
});
5.5 表情包的发送与接收
对于表情,我们当做文件进行发送就可以了,为了更快速的发送表情,提高性能,我们可以创建一个表情包,里面有很多表情图片。
每个客户端的本地都具有相关的表情包,然后每个表情有相关的对应代码,我们只需要把对应代码发送给别人即可,然后好友收到后会在本地解析此代码作为表情图片进行显示。
如果要将表情混在文本中间发送的话,我们需要写一些逻辑进行判断处理
6、聊天室的实现
6.1 单人聊天
思路:
- 1、当用户连接上服务器时,就把此用户保存在一个对象中,以用户名为键(用户名值不应该给汉字,除非做转码处理),以 socket.id 为值。
- 2、然后用户 a 要给用户 b 发消息时,在后端通过 b 的用户名找到 b 的 socket 的 id, 从而将用户 a 的消息发给 b. b 发消息给 a 也是一样的方式。
在实现私人聊天时应该注意如下事项
- 1、各种变量属性等命名时尽量给上一个独特的后缀,以避免和其他名字冲突从而出现 bug.
- 2、在这里使用 socket.io 返回数据时,名字尽量不要出现 username,否则可能会出现异常。在使用 socket.io 做事件监听时,事件名不要写成 connect,会发生冲突。
- 3、对象的键不应该给汉字,否则在很多时候都会出现异常。
6.2 群聊
效果
码云仓库地址
https://gitee.com/li_jia_hong123/scoket.io-express
目录结构
服务端核心代码
- 服务端核心代码 index.js
js
const express = require("express");
const { Server } = require("socket.io");
const path = require("path");
const http = require("http");
const { userInRoom, userLevelRoom, getRoomUser } = require("./utils");
// 实例化express应用
const app = express();
// 声明一个端口
const PORT = process.env.SERVER_PORT || 3000;
// 托管静态资源
app.use(express.static(path.join(__dirname, "public")));
// 要将socket.io挂载到服务器,需要用到原生http模块创建服务,并将请求交给express处理
const server = http.createServer(app);
// 初始化scoket.io, 这句话写上,socketio就已经把客户端挂在到服务器,
// 验证: 访问http://127.0.0.1:3000/socket.io/socket.io.js
const io = new Server(server, {});
//1. 监听连接
io.on("connection", (socket) => {
/*
进到这个回调函数,说明连接已经建立了
并且,没个连接进来的客户端,都会有一个唯一的客户端标识ID,用来区分同名的用户,获取这个ID只需 socket.id 即可获取
*/
var id = socket.id; // 每一个连接唯一标识
// 2. 监听用户加入房间
socket.on("inRoom", ({ name, room }) => {
// 3. 将用户加入房间
socket.join(room);
// 4. 将加入进来的用户存起来
userInRoom({ id, name, room });
// 5. 广播 将该房间内用户列表推送给当前房间内所有人,注意to(room)就限了只会给当前房间内的用户推送消息
// 这里用的是全局的io对象 io.emit而不是socket.emit来发送消息,因为socket只会发送给某个人
io.to(room).emit("getusers", getRoomUser(room));
// 广播 欢迎xxx进入房间
io.to(room).emit("systemMsg", { name, room });
// 将进入房间的用户的Id返回给用户自己
socket.to(room).emit("ownInfo", { id, name, room });
// 监听客户端发来的消息
socket.on("chatMsg", (msg) => {
// 将用户发来的消息广播出去(广播给房间内所有用户)
io.to(room).emit("chatMsg", msg);
});
// 断开连接时候会触发(比如:客服端离开)
socket.on("disconnect", () => {
// 用户掉线或离开,将用户从服务器用户列表删除
userLevelRoom(id);
// 给该房间内用户推送房前房间内所有用户列表
socket.leave(room);
io.to(room).emit("getusers", getRoomUser(room));
});
});
});
// 监听端口
server.listen(PORT, "0.0.0.0", () => {
console.info("服务启动成功,运行端口:" + PORT);
});
- 服务端工具方法 utils.js
js
let users = []; // 用于存储用户
//用户加入房间的方法
function userInRoom(userObj) {
users.push(userObj);
return users;
}
// 用户离开房间的方法
function userLevelRoom(id) {
let index = users.findIndex((item) => item.id === id);
if (index !== -1) {
users.splice(index, 1);
}
return users;
}
// 获取某个房间的所有用户
function getRoomUser(room) {
return users.filter((item) => item.room === room);
}
module.exports = {
userInRoom,
userLevelRoom,
getRoomUser,
};
- 用的一些依赖包 package.json
json
{
"name": "scoket-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "nodemon ./index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.1",
"socket.io": "^4.4.0"
},
"devDependencies": {
"nodemon": "^2.0.15"
}
}
客户端代码
客户端思路:
客户端用了 bootstrap 布局页面,做了简单的适配
在登陆页面,填入姓名和选择房间,登录后,将姓名和房间放到 URL 上,跳转房间 room.html 页面,在 room.html 页面取到姓名和房间,连接 socket 后,首先 emit 给 socket 服务器端,服务端会记录人员,并加入 room,就可以聊天了, 主要方法 emit 和 on
登陆页面 index.html
html
<!-- login.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>
<!-- 引入bootstrap样式 -->
<!-- CSS only -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous"
/>
<link rel="stylesheet" href="index.css" />
</head>
<body class="bg-light d-flex justify-content-center">
<div class="login-container shadow-lg bg-body rounded mt-5">
<div
class="login-head p-3 text-center bg-success bg-gradient text-light"
>
聊一聊
</div>
<div class="login-body p-3 border-bottom">
<form action="#" id="loginForm">
<div class="mb-3">
<label for="name" class="form-label">姓 名</label>
<input
type="text"
class="form-control"
id="name"
placeholder="请输入姓名"
/>
</div>
<div class="mb-3">
<label for="room" class="form-label">房 间</label>
<select class="form-select" id="room">
<option value="" selected>请选择房间</option>
<option value="Javascript">Javascript</option>
<option value="Html">Html</option>
<option value="Css">Css</option>
</select>
</div>
<div class="d-grid gap-2 mt-5">
<button type="submit" class="btn btn-success">登 录</button>
</div>
</form>
</div>
</div>
</body>
<!-- 引入bootstrap的js -->
<!-- JavaScript Bundle with Popper -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"
></script>
<script>
var loginForm = document.getElementById("loginForm");
loginForm.addEventListener("submit", (e) => {
e.preventDefault();
let name = document.getElementById("name").value;
let room = document.getElementById("room").value;
if (!name.trim()) {
alert("姓名必填!");
return;
}
if (!room) {
alert("请选择房间!");
return;
}
window.location.replace(
`/room.html?name=${encodeURI(name)}&room=${encodeURI(room)}`,
"_self"
);
});
</script>
</html>
- 聊天室页面 room.html
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>
<!-- 引入bootstrap样式 -->
<!-- CSS only -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
crossorigin="anonymous"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"
/>
<link rel="stylesheet" href="index.css" />
</head>
<body class="bg-light d-flex justify-content-center">
<div class="container room mt-5 card">
<div class="row">
<div
class="d-flex justify-content-between aline-item-center py-2 bg-secondary text-light"
>
<div class="left">聊一聊</div>
<a
href="javascript:;"
class="right text-light text-decoration-none"
id="outPage"
>
退出<i class="bi bi-arrow-right-short"></i>
</a>
</div>
</div>
<div class="row">
<div
class="room-list col-lg-3 col-md-3 col-sm-12 col-xs-12 d-flex flex-column"
>
<div class="fs-4 py-2 border-bottom">
<span id="roomName"></span>
<span style="font-size:13px;"
>在线人数( <span id="inlineCount">100</span> )</span
>
</div>
<div class="overflow-auto">
<ul class="flex-grow-1 mb-2" id="userList"></ul>
</div>
</div>
<div class="col-lg-9 col-md-9 col-sm-12 col-xs-12 new-list-container">
<div class="welcome bg-success text-white bg-opacity-75">
欢迎张三加入房间
</div>
<div class="overflow-auto news-content" id="msg-container">
<ul id="msgContent"></ul>
</div>
<div class="row">
<div class="input-group mb-2">
<input
type="text"
class="form-control"
placeholder="请输入内容"
id="inputContent"
/>
<button
class="btn btn-outline-secondary"
type="button"
id="submitBtn"
>
<i class="bi bi-send"></i> 发 送
</button>
</div>
</div>
</div>
</div>
</div>
</body>
<!-- 引入客户端js -->
<script src="/socket.io/socket.io.js"></script>
<!-- 引入bootstrap的js -->
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
crossorigin="anonymous"
></script>
<!-- moment -->
<script src="http://cdn.staticfile.org/moment.js/2.24.0/moment-with-locales.js"></script>
<!-- 引入自己的js -->
<script src="/main.js"></script>
</html>
- 聊天室 js 文件 main.js
js
// 写上这一句话,客户端就已经连上服务器了,验证:http://127.0.0.1:3000/room.html,查看network,已经发送了一个ws请求了
const socket = io();
const inputContent = document.getElementById("inputContent");
const submitBtn = document.getElementById("submitBtn");
const msgContainer = document.getElementById("msg-container");
const msgContent = document.getElementById("msgContent");
const inlineD = document.getElementById("inlineCount");
const userListD = document.getElementById("userList");
const roomNameD = document.getElementById("roomName");
const outPageD = document.getElementById("outPage");
// 获取URL里面的姓名和房间
let queryString = window.location.search;
queryString = queryString.slice(1);
let obj = queryString.split("&").reduce((pre, cur) => {
let [key, value] = cur.split("=");
pre[key] = decodeURI(value);
return pre;
}, {});
// 进入房间前将姓名和房间号发给服务器
socket.emit("inRoom", obj);
// 监听服务器返回的用户列表
socket.on("getusers", (users) => {
inlineD.innerHTML = users.length || 0;
renderUserList(users);
});
// 监听系统消息: xxx进入房间
socket.on("systemMsg", (user) => {
roomNameD.innerText = user.room;
let welcomeD = document.querySelector(".welcome");
welcomeD.innerHTML = `欢迎【${user.name}】加入房间`;
welcomeD.style.display = "block";
setTimeout(() => {
welcomeD.style.display = "none";
}, 600);
});
socket.on("ownInfo", (own) => {
window.sessionStorage.setItem("userId", own.id);
});
// 接收用户消息
socket.on("chatMsg", (msg) => {
renderMsg(msg);
});
// 发消息
submitBtn.addEventListener("click", sendMsg);
inputContent.addEventListener("keyup", (e) => {
var event = e || window.event;
if (event.key === "Enter" || event.Code === "Enter" || event.keyCode === 13) {
sendMsg();
}
});
function sendMsg() {
let msg = inputContent.value;
if (!msg.trim()) {
alert("请输入内容");
return;
}
// 发消息
const { name, room } = obj;
let id = window.sessionStorage.getItem("userId");
socket.emit("chatMsg", { id, name, room, msg, time: Date.now() });
inputContent.value = "";
}
// 渲染消息message
function renderMsg(message) {
let userid = window.sessionStorage.getItem("userId");
console.log(userid, message);
const { id, name, room, msg, time } = message;
if (userid === id) {
// 我自己
msgContent.innerHTML += `
<li class="news-item d-flex flex-column align-items-end">
<div class="w-75">
<div class="time pt-1 ml-1 text-end mx-2">
<span class="d-inline-block mx-2">${moment(time).format(
"YYYY-MM-DD HH:mm:ss"
)}</span><span class="text-danger">我</span>
</div>
<p class="text-end">
<span
class="text-start mx-2 bg-body mt-2 text-break shadow-lg d-inline-block p-2 rounded"
style="background-color:#9eea6a!important;">
${msg}
</span>
</p>
</div>
</li>
`;
} else {
// 别人
msgContent.innerHTML += `
<li class="news-item">
<div class="w-75">
<div class="time pt-1 ml-1 mx-2">
<span class="d-inline-block me-2"><span class="text-danger">${name}</span></span></span><span>${moment(
time
).format("YYYY-MM-DD HH:mm:ss")}</span>
</div>
<p class="bg-body mt-2 mx-2 shadow-lg text-break d-inline-block p-2 rounded">${msg}</p>
</div>
</li>
`;
}
msgContainer.scrollTo(0, msgContent.scrollHeight);
}
// 渲染用户列表
function renderUserList(users) {
let str = "";
users.forEach((item) => (str += `<li class="p-1">${item.name}</li>`));
userListD.innerHTML = str;
}
// 用户退出
outPageD.addEventListener("click", (e) => {
e.preventDefault();
window.location.replace("/index.html", "_self");
});
- 全局样式文件 index.css
css
ul,
li {
padding: 0;
margin: 0;
list-style: none;
}
.new-list-container {
position: relative;
}
.new-list-container .welcome {
position: absolute;
top: 15px;
left: 50%;
transform: translateX(-50%);
font-size: 13px;
border-radius: 5px;
padding: 5px;
display: none;
}
.login-container {
width: 450px;
justify-content: space-between;
}
.room-list {
border-bottom: 1px solid #ccc;
}
.news-content {
height: 70vh;
}
.time {
font-size: 12px;
}
@media (max-width: 575px) {
.login-container {
width: 90%;
}
}
@media (min-width: 992px) {
.room-list {
border-right: 1px solid #ccc;
border-bottom: none;
}
}
7、参考资料
https://socket.io/get-started/chat/ (英文)
https://www.w3cschool.cn/socket/socket-ulbj2eii.html (中文)
其他:
https://blogzl.com/pub/7-nodejs/Nodejs实践教程/8. SocketIO实践教程.html#客户端-1