# WebSocket
# Websocket 是什么?
WebSocket 是一种在客户端
与服务器
之间保持TCP
长连接的网络协议,这样它们就可以随时进行信息交换。
虽然任何客户端或服务器上的应用都可以使用 WebSocket,但原则上还是指浏览器
与服务器
之间使用。通过 WebSocket,服务器可以直接向客户端发送数据,而无须客户端周期性的请求服务器,以动态更新数据内容。
# 客户端 API
WebSocket
对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的 API。
使用WebSocket()
构造函数来构造一个WebSocket
。
# 构造函数
WebSocket(urlp[, protocols]) 返回一个 WebSocket 实例对象
# 常量
Constant | Value |
---|---|
CONNECTING | 0 |
OPEN | 1 |
CLOSING | 2 |
CLOSED | 3 |
# 属性
实例属性 | |
---|---|
binaryType | bufferedAmount |
extensions | protocol |
readyState | url |
# 方法
实例方法 | |
---|---|
close([code[, reason]]) | |
send(data) |
# 事件
实例事件 | |
---|---|
close | |
error | |
message | |
open |
# 服务端 API
服务端使用 Node.js 举例
# 构造函数
WebSocket.Server(options, [callback])
const WebSocket = require("ws");
const wss = new WebSocket.Server({ server });
配置参数
options | |
---|---|
host | String 服务绑定的主机名 |
port | Number 服务绑定的端口 |
backlog | Number 等待连接队列的最大长度 |
server | 一个预先创建好的 HTTP/S 服务 |
verifyClient | Function 用于对客户端进行验证的钩子函数 |
handleProtocols | FunctionWebSocket 子协议的监听函数 |
path | String 允许连接匹配的路径 |
noServer | Boolean 允许无服务器模式 |
clientTracking | Booleans 是否跟踪客户端 |
maxPayload | Number 设置每个数据包的大小 |
# 内置事件
内置事件 | |
---|---|
close | 当服务关闭时 |
connection | 当握手完成时 |
error | 当服务出错时 |
headers | 在响应头作为握手的一部分写入套接字之前 |
listening | 当绑定基础服务完成时 |
# 内置方法
server.close([callback]) 关闭 WebSocket 服务
# 内置属性
server.clients 存储所有连接的客户端的集合
# 实例
实例和 Web 端基本一致
# 连接状态
Constant | Value |
---|---|
CONNECTING | 0 |
OPEN | 1 |
CLOSING | 2 |
CLOSED | 3 |
# 实例事件
实例事件 | |
---|---|
close | error |
message | open |
ping | pong |
# 实例属性
实例属性 | |
---|---|
binaryType | |
bufferedAmount | |
readyState |
# 实例方法
实例方法 | |
---|---|
close([code[,reason]]) | 关闭连接 |
send(data[, options,callback]) | 发送消息 |
terminate() | 强制关闭 |
# 和 Node.js 通信
这里本着学习的目的,我们将使用 node 的网络模块net
来实现 WebScoket 的
- 握手 协议升级
- 数据帧的解析和封装
- 房间管理
# 数据包解析和封装
// utils.js
const crypto = require("crypto");
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
// 数据头解析和握手
function handshake(sock, data) {
let str = data.toString();
// 1.每行数据切开": "
let lines = str.split("\r\n");
// 2.舍去第一行和最后两行
lines = lines.slice(1, lines.length - 2);
let headers = {};
lines.forEach((line) => {
let [key, val] = line.split(": ");
headers[key.toLowerCase()] = val;
});
if (headers["upgrade"] != "websocket") {
console.log("其他协议", headers["upgrade"]);
sock.end();
} else if (headers["sec-websocket-version"] != "13") {
console.log("版本不对", headers["sec-websocket-version"]);
sock.end();
} else {
let key1 = headers["sec-websocket-key"];
let key2 = crypto
.createHash("sha1")
.update(key1 + GUID)
.digest("base64");
sock.write("HTTP/1.1 101 Switching Protocol\r\n");
sock.write("Connection: Upgrade\r\n");
sock.write("Upgrade: websocket\r\n");
sock.write("Sec-WebSocket-Accept: " + key2 + "\r\n");
sock.write("\r\n");
}
console.log("handshake");
}
// 数据帧解析函数
function decodeDataFrame(data) {
let start = 0;
let frame = {
// 解析前两个字节的基本数据
FIN: data[start] >> 7,
Opcode: data[start++] & 0x0f,
Mask: data[start] >> 7,
PayloadLen: data[start++] & 0x7f,
MaskingKey: "",
PayloadData: null
};
// 处理特殊长度126和127
if (frame.PayloadLen === 126) {
frame.PayloadLen = (data[start++] << 8) + data[start++]; //16位Extended payload length
}
if (frame.PayloadLen === 127) {
frame.PayloadLen = 0;
for (let i = 7; i >= 0; i--) {
frame.PayloadLen += data[start++] << (i * 8); //64位Extended payload length
}
}
// 判断是否使用掩码
if (frame.Mask) {
// 获取掩码实体
frame.MaskingKey = [data[start++], data[start++], data[start++], data[start++]];
// 对数据和掩码做异或运算
frame.PayloadData = data.slice(start, start + frame.PayloadLen).map((byte, index) => {
return byte ^ frame.MaskingKey[index % 4];
});
} else {
// 否则的话 直接使用数据
frame.PayloadData = data.slice(start, start + frame.PayloadLen);
}
// 数组转换成缓冲区来使用
let buf = Buffer.from(frame.PayloadData);
// 如果有必要则把缓冲区转换成字符串来使用
if (frame.Opcode === 1) {
buf = buf.toString();
}
// 设置上数据部分
frame.PayloadData = buf;
// 返回数据帧
return frame;
}
// 数据帧编码函数
function encodeDataFrame(data) {
let arrs = [];
let PayloadData = data.PayloadData ? Buffer.from(data.PayloadData) : null;
let PayloadLen = PayloadData ? PayloadData.length : null;
// 处理第一个字节
arrs.push((data.FIN << 7) + data.Opcode);
// 处理第二个字节,判断它的长度并放入相应的位置
if (PayloadLen < 126) {
arrs.push(PayloadLen);
} else if (PayloadLen < 65536) {
// push 126 和 length的高八位和低八位
arrs.push(126, PayloadLen >> 8, PayloadLen & 0xff);
} else {
arrs.push(127);
for (let i = 7; i >= 0; i--) {
arrs.push(PayloadLen & ((0xff << (i * 8)) >> (i & 8)));
}
}
// 返回头部分和数据部分的合并缓冲区
let frame = PayloadData ? Buffer.concat([Buffer.from(arrs), PayloadData]) : Buffer.from(arrs);
return frame;
}
function distributionID(sockList, sock) {
let ran = String(Math.random() * 100);
let flag = sockList.every((item) => {
return item.id != ran;
});
// 生成的id为唯一值
if (flag) {
// 分配id,将当前连接的sock添加进sockList
sock.id = ran;
sockList.push(sock);
console.log("sockList.length", sockList.length);
return sockList;
} else {
// 否则重新生成id
return distributionID(sock);
}
}
function createRoom(Rooms, sock, currentRoom) {
let flag = Object.keys(Rooms).every((room) => {
return room != currentRoom;
});
if (flag) {
console.log(sock.id, "创建了房间", currentRoom);
Rooms[currentRoom] = [];
Rooms[currentRoom].push(sock);
} else {
console.log(sock.id, "加入了房间", currentRoom);
Rooms[currentRoom].push(sock);
}
console.log("当前房间列表", [...Object.keys(Rooms)]);
}
function leaveRoom(Rooms, sock) {
Object.keys(Rooms).forEach((roomName) => {
let roomIndex = -1;
Rooms[roomName].forEach((client, index) => {
if (sock.id == client.id) {
roomIndex = index;
return;
}
});
// 找到所在房间和索引
if (roomIndex >= 0) {
console.log(sock.id, "离开了房间", roomName);
Rooms[roomName].splice(roomIndex, 1);
}
// 如果房间内没人,销毁此房间
if (Rooms[roomName].length == 0 && roomName != "default") {
console.log("销毁房间", roomName);
delete Rooms[roomName];
}
console.log("当前房间列表", [...Object.keys(Rooms)]);
});
}
function getIPAdress() {
const interfaces = require("os").networkInterfaces();
for (var devName in interfaces) {
var iface = interfaces[devName];
for (var i = 0; i < iface.length; i++) {
var alias = iface[i];
if (alias.family === "IPv4" && alias.address !== "127.0.0.1" && !alias.internal) {
return alias.address;
}
}
}
}
module.exports = {
handshake,
decodeDataFrame,
encodeDataFrame,
distributionID,
createRoom,
leaveRoom,
getIPAdress
};
# 后端业务逻辑
// app.js
const port = 2020;
const net = require("net"); //TCP 原生Socket套接字
const utils = require("./utils"); // 引入握手解析工具方法
// 定义sock总列表
let sockList = [];
// 定义房间对象
let Rooms = { default: [] };
// 创建wsServer服务
let wsServer = net.createServer((sock) => {
// 定义$emit方法(单数据帧发送)
sock.$emit = function (name, ...args) {
let sendFrame = {
FIN: 1,
Opcode: 1,
PayloadData: JSON.stringify({ name, data: [...args] })
};
this.write(utils.encodeDataFrame(sendFrame));
};
// 第一次数据是握手,此过程每个sock只有一次
sock.once("data", (data) => {
utils.handshake(sock, data);
// 分配sock.id
sockList = utils.distributionID(sockList, sock);
// 真正的数据在在这里处理
sock.on("data", (data) => {
let reciveFrame = utils.decodeDataFrame(data);
// 客户端断开连接
if (reciveFrame.Opcode == 0x8) {
// 服务端也断开连接
sock.end();
} else {
let message = JSON.parse(reciveFrame.PayloadData);
// 消息分类处理
if (message.name == "createRoom") {
let currentRoom = message.data;
utils.createRoom(Rooms, sock, currentRoom);
sock.$emit("success", currentRoom);
}
if (message.name == "leaveRoom") {
utils.leaveRoom(Rooms, sock);
sock.$emit("success", "");
}
if (message.name == "chat") {
chat(message.data);
}
if (message.name == "broadcast") {
broadcast(message.data);
}
}
});
});
sock.on("end", () => {
console.log("客户端断开", sock.id);
let n = sockList.indexOf(sock);
// 在sockList列表中则删除此连接
if (n != -1) {
sockList.splice(n, 1);
}
console.log("sockList.length", sockList.length);
utils.leaveRoom(Rooms, sock);
});
sock.on("error", () => {
console.log("客户端错误");
});
});
wsServer.listen(port);
// 房间内发送
function chat(data) {
console.log("chat", data);
let message = data[0];
let roomName = data[1];
Rooms[roomName].forEach((s) => {
s.$emit("chat", message);
});
}
// 全平台广播
function broadcast(data) {
let message = data[0];
console.log("boardcast", data);
sockList.forEach((s) => {
s.$emit("broadcast", message);
});
}
console.log("ws://" + utils.getIPAdress() + ":" + port);
# 前端业务逻辑
vue create chat-client
// main.js
import Vue from "vue";
import App from "./App.vue";
// 引入全局样式
import "./styles/common.css";
// 定义全局变量
// 开发环境
if (process.env.NODE_ENV === "development") {
Vue.prototype.$wsServer = "ws://localhost:2020";
} else {
// 部署环境
Vue.prototype.$wsServer = "ws://media.gausszhou.top/ws";
}
// 全局配置
Vue.config.productionTip = false;
// new Vue 配置路由,状态,渲染组件,挂载组件
new Vue({
render: (h) => h(App)
}).$mount("#app");
<!-- App.vue -->
<template>
<div id="app">
<div class="demo">
<h1>Learn WebSocket</h1>
<div class="item-box display-flex">
<chat-vue class="item"></chat-vue>
<chat-vue class="item"></chat-vue>
</div>
<div class="item-box display-flex">
<chat-vue class="item"></chat-vue>
<chat-vue class="item"></chat-vue>
</div>
</div>
</div>
</template>
<script>
import chatVue from "./components/WebSocketChat";
export default {
components: {
chatVue
}
};
</script>
// components/WebSocketChat.vue
<template>
<div>
<div>
<div :class="{ green: isLinked }">{{ tips }}</div>
<li>
请求地址
<input v-model="$wsServer" size="mini" />
<button type="primary" size="mini" @click="initSocket()">发起连接</button>
<button type="danger" size="mini" @click="closeSocket()">断开连接</button>
</li>
<li>
创建房间
<input v-model="RoomId" size="mini" />
<button type="primary" size="mini" @click="createRoom()">创建或加入房间</button>
</li>
<li>
当前房间
<input v-model="currentRoomId" disabled size="mini" />
<button type="warning" size="mini" @click="leaveRoom()">离开当前的房间</button>
</li>
</div>
<template>
<div class="box">
<li v-for="(msg, index) in messageList" :key="index">{{ msg }}</li>
</div>
<textarea class="message" v-model="message"></textarea>
<div>
<button size="mini" type="success" v-if="currentRoomId" @click="chat()">房间内聊天</button>
<button size="mini" v-if="currentRoomId" @click="broadcast()">全平台广播</button>
</div>
</template>
</div>
</template>
<script>
export default {
data() {
return {
// socket相关
tips: "Hello World",
socket: null,
RoomId: "default",
currentRoomId: "",
message: "",
messageList: [],
isLinked: false
};
},
created() {
this.initSocket();
},
methods: {
// 初始化socket
initSocket() {
if (!this.socket || this.socket.readyState == 3) {
console.log(this.$wsServer);
this.socket = new WebSocket(this.$wsServer);
console.log("----new WebSocket");
}
// 封装自定义消息(需要和后端约定)
this.socket.emit = (name, ...args) => {
let data = JSON.stringify({ name, data: [...args] });
this.socket.send(data);
};
// 绑定四个基本事件
// open
this.socket.onopen = () => {
this.tips = "连接成功,现在可以接收到广播";
this.isLinked = true;
};
// onmessage(通过定义不同的name字段来区分不同的的消息)
this.socket.onmessage = (res) => {
let data = JSON.parse(res.data);
// 房间操作成功
if (data.name == "success") {
this.currentRoomId = data.data[0][0];
if (this.currentRoomId) {
this.tips = "当前房间:" + this.currentRoomId;
} else {
this.messageList = [];
this.tips = "已经离开房间,但是仍能接收广播";
}
}
// 接受消息
if (data.name == "chat") {
let message = data.data[0];
this.messageList.push(message);
}
if (data.name == "broadcast") {
let message = data.data[0];
this.messageList.push(message);
}
};
// onerror
this.socket.onerror = (err) => {
console.log(err);
};
// onclose
this.socket.onclose = () => {
this.tips = "断开连接";
this.isLinked = false;
};
},
// 关闭socket
closeSocket() {
this.socket.close();
},
// 创建房间
createRoom() {
let room = this.RoomId;
if (room == "" || room == null) {
this.tips = "请输入房间号";
} else {
if (this.socket) {
if (this.currentRoomId) this.leaveRoom();
this.socket.emit("createRoom", room);
} else {
this.tips = "请连接聊天室";
}
}
},
// 离开房间
leaveRoom() {
let room = this.currentRoomId;
if (room) {
this.socket.emit("leaveRoom", room);
}
},
// 房间内聊天
chat() {
if (this.message) {
this.socket.emit("chat", this.message, this.currentRoomId);
this.message = "";
}
},
// 广播
broadcast() {
this.socket.emit("broadcast", this.message);
this.message = "";
}
},
beforeDestroy() {
if (this.socket) {
this.socket.close();
}
}
};
</script>