超级简单的WebSocket的聊天应用
1.定义消息类型
2.定义WebSocket的实例类型
3.定义聊天消息model
4.定义Socket连接、发送消息、发送心跳类
5.定义发布订阅类,用于新消息来了立即发布接收到的消息到相关的页面
6.实现网页打开时,连接服务器;关闭页面时,断开socket连接
7.收到消息后,发送浏览器外的通知
1.YLGroupChatProtocol
是定义的聊天消息类型,不同的ID对应不同的消息
// 通信协议定义
class YLGroupChatProtocol {
constructor () {
this.create_room = 1001;
this.send_content = 1002;
this.send_qiniu_image = 1003;
this.send_file = 1004;
this.newUserBroadcast = 1005; // 新用户连接时,广播消息给所有的用户
this.heartbeat = 1006; // 心跳
}
}
2.这是 WebSocket
状态 ,按照官方描述的来定义的
WebSocket文档:https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState
class YLWebSocketReadyState {
constructor () {
this.CONNECTING = 0; // Socket has been created. The connection is not yet open.
this.OPEN = 1; // The connection is open and ready to communicate.
this.CLOSING = 2; // The connection is in the process of closing.
this.CLOSED = 3; // The connection is closed or couldn't be opened.
}
}
3.定义聊天消息消息实体model
class YLChatMessageModel {
constructor () {
this.protocol = "";
this.roomUUID = "";
this.sender = "";
this.content = "";
this.imagekey = ""; // 图片的七牛云key
this.uuid = ""; // 消息UUID
this.users = null; // 用户列表
this.client_ip = ""; // 用户真实IP
this.rawProtocol = new YLGroupChatProtocol();
}
setData (data) {
let jsonDict = JSON.parse(data);
this.protocol = jsonDict["protocol"];
if (this.protocol == this.rawProtocol.heartbeat) {
} else if (this.protocol == this.rawProtocol.send_content) {
this.roomUUID = jsonDict["roomUUID"];
this.client_ip = jsonDict["client_ip"];
this.sender = jsonDict["sender"];
this.content = jsonDict["content"];
this.uuid = jsonDict["uuid"];
} else if (this.protocol == this.rawProtocol.send_qiniu_image) {
this.roomUUID = jsonDict["roomUUID"];
this.client_ip = jsonDict["client_ip"];
this.sender = jsonDict["sender"];
this.content = jsonDict["content"];
this.imagekey = jsonDict["imagekey"];
this.uuid = jsonDict["uuid"];
} else if (this.protocol == this.rawProtocol.newUserBroadcast) {
this.roomUUID = jsonDict["roomUUID"];
this.client_ip = jsonDict["client_ip"];
this.users = jsonDict["users"];
}
}
}
4.定义Socket连接、发送消息、发送心跳类
'use strict';
// 群组聊天
class YLGroupChatWebSocket {
constructor () {
this.current_user = null; // 当前登录用户实例对象
this._group_ws = null;
this.heartbeat_enabled = false; // 心跳包是否启用
var ws_protocol = 'https:' == document.location.protocol ? "wss:": "ws:";
this._roomUUID = "yooulchat2019";
// 测试域名
let test_domains = [
"localhost:8080",
"localhost:9000",
];
let real_domain = "";
if (test_domains.indexOf(window.location.host) != -1) {
// 测试环境
real_domain = "ws://test.yourdomain.com";
} else {
// 线上环境
real_domain = "wss://yourdomain.com";
}
this._group_ws_url = real_domain + "/stream/ws/gchat/" + this._roomUUID;
// 临时的消息暂存
this.tmp_message_protocol = null;
this.tmp_message_content = null;
}
// 连接服务器
connect_to_server () {
if (!this.hasLogin()) {
console.log("请选登录!");
return;
}
this._group_ws = new WebSocket(this._group_ws_url);
this._group_ws.binaryType = "arraybuffer";
var that = this;
this._group_ws.onopen = function (event) {
console.log("成功连接到服务器!");
window.YLNotificationMessages.publish(window.YLNotificationMessageType.connected, event);
// 重新连接上服务器后,把消息发出去
if (that.tmp_message_protocol != null && that.tmp_message_content != null) {
that.send_content(that.tmp_message_protocol, that.tmp_message_content);
that.tmp_message_protocol = null;
that.tmp_message_content = null;
}
// 新加入连接的用户,发给服务器记录起来
let rawProtocol = new YLGroupChatProtocol();
that.send_content(rawProtocol.newUserBroadcast, "");
// 定期发送心跳包
that.heartbeat_enabled = true;
that.send_heartbeat_periodically();
}
this._group_ws.onmessage = function (event) {
// console.log("收到消息了", event)
let msgModel = new YLChatMessageModel();
msgModel.setData(event.data);
if (msgModel.protocol == new YLGroupChatProtocol().heartbeat) {
return;
}
// 发送消息到聊天的页面上显示
window.YLNotificationMessages.publish(window.YLNotificationMessageType.receive_messages, msgModel);
}
this._group_ws.onclose = function (event) {
console.log("与服务器已断开连接。", event);
this.heartbeat_enabled = false;
window.YLNotificationMessages.publish(window.YLNotificationMessageType.disconnected, event);
if (that.hasLogin() && window.location.pathname.indexOf("/chatbox") > -1) {
console.log("正在重新连接...");
that.connect_to_server();
}
}
this._group_ws.onerror = function (err) {
console.log(err);
this.heartbeat_enabled = false;
window.YLNotificationMessages.publish(window.YLNotificationMessageType.socket_error, err);
}
}
// 发送数据到服务器
send_content (protocolEnumVal, content, imagekey="") {
if (window.location.pathname.indexOf("/chatbox") == -1) {
return;
}
if (!this.hasLogin()) {
console.log("请选登录!");
return;
}
if (this._group_ws == null) {
this.connect_to_server();
// 暂存发送的消息,待重新连接上服务器后,再把消息发出去
this.tmp_message_protocol = protocolEnumVal;
this.tmp_message_content = content;
return;
}
if ((this._group_ws.readyState == window.YLWebSocketReadyState.CLOSING ||
this._group_ws.readyState == window.YLWebSocketReadyState.CLOSED)) {
if (confirm("您与全球用户聊天已失去连接,是否自动连接?")) {
this.connect_to_server();
// 暂存发送的消息,待重新连接上服务器后,再把消息发出去
this.tmp_message_protocol = protocolEnumVal;
this.tmp_message_content = content;
}
return;
}
// 当前登录用户的User对象(游客)
var token = window.localStorage.getItem("token");
if (token == undefined || token == null) {
token = window.sessionStorage.getItem("token_guest");
}
// 当前登录用户的User对象
var yooulUserStr = window.localStorage.getItem("$Yoouluser");
if (yooulUserStr == undefined || yooulUserStr == null) {
yooulUserStr = window.sessionStorage.getItem("$Yooulguest");
}
let data = {
"protocol" : protocolEnumVal,
"roomUUID" : this._roomUUID,
"token": token,
"content" : content,
"sender": JSON.parse(yooulUserStr)
}
let rawProtocol = new YLGroupChatProtocol();
if (protocolEnumVal == rawProtocol.send_qiniu_image && imagekey.length > 0) {
data["imagekey"] = imagekey;
}
let jsonData = JSON.stringify(data)
this._group_ws.send(jsonData);
}
// 关闭WebSocket链接
close_connection () {
if (this._group_ws != null) {
this._group_ws.close();
this._group_ws = null;
}
}
// 间隔30秒发送心跳包
send_heartbeat_periodically() {
// 当前登录用户的User对象
let self = this;
var myHeartBeatInterval = window.setInterval(() => {
if (!this.heartbeat_enabled) {
// 关闭计时器
window.clearInterval(myHeartBeatInterval);
return;
}
let data = {"protocol" : new YLGroupChatProtocol().heartbeat}
let jsonData = JSON.stringify(data)
self._group_ws.send(jsonData);
}, 30 * 1000);
}
getUUID () {
var d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now(); //use high-precision timer if available
}
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
hasLogin () {
var token = "Your token";
if (token != null) {
return true;
}
return false;
}
}
// 定义通知类型
class YLNotificationMessageType {
constructor () {
this.socket_error = -1; // socket发生错误
this.connected = 1; // 已连接到服务器
this.reconnect = 2; // 重新连接服务器
this.receive_messages = 3; // 接收消息
this.disconnected = 4; // 客户端已掉线
}
}
5.定义发布订阅类,用于新消息来了立即发布接收到的消息到相关的页面
// 消息通知
// 发布/订阅模式
class YLNotificationMessages {
constructor () {
// 事件对象:存放事件的订阅名字和回调
this.events = {};
}
// 订阅事件
subscribe (eventName, callback) {
if (!this.events[eventName]) {
// 一个名称可以有多个订阅回调事件,所以使用数组存储回调
this.events[eventName] = [callback];
} else {
// 如果该事件名称存在,则继续往该名称添加回调事件
this.events[eventName].push(callback);
}
}
// 发布事件
publish (eventName, ...args) {
this.events[eventName] && this.events[eventName].forEach(cb => cb(...args));
}
// 取消订阅事件
unsubscribe (eventName, callback) {
if (this.events[eventName]) {
// 找到该回调,并移除它
// this.events[eventName].filter(cb => cb != callback); // 不管用
var _events = [];
for (var i = 0; i < this.events[eventName].length; i++) {
if (this.events[eventName][i].toString()!=callback.toString()) {
_events.push(this.events[eventName][i]);
}
}
this.events[eventName] = _events;
}
}
// 取消订阅所有事件
unsubscribeAll(eventName) {
if (this.events[eventName]) {
this.events[eventName] = [];
}
}
}
// 注册到Window对象中
window["YLNotificationMessageType"] = new YLNotificationMessageType();
window["YLNotificationMessages"] = new YLNotificationMessages();
window["YLGroupChatProtocol"] = new YLGroupChatProtocol();
window["YLWebSocketReadyState"] = new YLWebSocketReadyState();
window["YLGroupChatWebSocket"] = new YLGroupChatWebSocket();
6.实现网页打开时,连接服务器;关闭页面时,断开socket连接
// 当页面关闭或强制刷新时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常
window.onbeforeunload = function() {
console.log("客户端连接已关闭");
if (isUserLoginWhenConnectingToWSServer()) {
window.YLGroupChatWebSocket.close_connection();
// 取消订阅事件,取消接收聊天消息
window.YLNotificationMessages.unsubscribe(window.YLNotificationMessageType.receive_messages, chatReceiveMessagesCallback);
}
}
window.onload = function () {
grantNotification();
// 仅打开聊天界面时才开启WebSocket聊天
// 等待页面加载完后再去连接WebSocket服务
if (isUserLoginWhenConnectingToWSServer()) {
window.YLGroupChatWebSocket.connect_to_server()
// 订阅事件,接收聊天消息
window.YLNotificationMessages.subscribe(window.YLNotificationMessageType.receive_messages, chatReceiveMessagesCallback);
}
}
// 连接WebSocket服务器时判断一下登录状态
function isUserLoginWhenConnectingToWSServer() {
var token = "You token";
if (token != null) {
return true;
}
return false;
}
// 订阅事件,接收聊天消息回调
function chatReceiveMessagesCallback(data) {
let rawProtocol = new YLGroupChatProtocol();
var yooulUserStr = window.localStorage.getItem("$Yoouluser");
let yooulUser = JSON.parse(yooulUserStr);
if (yooulUser.user_name == data.sender.user_name && yooulUser.user_id == data.sender.user_id) {
// 如果是自己的消息,则不通知
} else if (data.protocol == rawProtocol.send_content || data.protocol == rawProtocol.send_qiniu_image) {
if (window.location.pathname == "/chatbox") {
// 当用户在聊天页面时,不显示通知
return;
}
// 只通知收到别人发的消息
showBrowserNotification(data.sender.user_avatar, data.content);
}
}
7.收到消息后,发送浏览器外的通知
// 显示浏览器通知
function showBrowserNotification(msgAvatar, msgContent) {
console.log("用户设定的通知状态是:", Notification.permission);
if (Notification.permission !== 'granted') {
Notification.requestPermission();
} else {
var notification = new Notification('有人给你发送了消息', {
icon: msgAvatar,
body: msgContent,
});
notification.onclick = function() {
window.open('/chatbox');
};
}
}
// 授权显示通知
function grantNotification() {
if (Notification.permission !== 'granted') {
Notification.requestPermission();
}
}
完整代码:
https://gist.github.com/VictorZhang2014/3811b38aea41390039cbbe79d9dad0a3