Vue项目配置WebSocket连接 ws、wss 两种方式
1. 写作背景
项目使用的是ruoyi的前后端分离框架
- 项目需要使用到 websocket , 在本地使用 ws 连接方式是没问题 , 但是服务器上边使用的是nginx + ssl 证书 https域名访问的方式部署的
使用普通的 ws 连接是不可以成功的 需要使用 wss的方式
2. 晒出代码
2.1 前端 vue.config.js 的代码
-
这里target: 里边指向的都是后端server的地址 16000是我后端服务的端口 , 我这里websocket服务和普通的业务项目用的都是一个项目 所以都是16000端口
devServer: {
host: '0.0.0.0',
port: port,
open: true,
proxy: {
// detail: https://cli.vuejs.org/config/#devserver-proxy
// 正常的 http 请求代理
[process.env.VUE_APP_BASE_API]: {
target: `http://localhost:16000`,
changeOrigin: true,
pathRewrite: {
['^' + process.env.VUE_APP_BASE_API]: ''
}
},
// websocket ws 的代理路由配置
[process.env.VUE_APP_WEBSOCKET_API]: {
target: `ws://localhost:16000`,
changeOrigin: true,
ws: true,
pathRewrite: {
['^' + process.env.VUE_APP_WEBSOCKET_API]: ''
}
},
// websocket wss 的代理路由配置
[process.env.VUE_APP_WSS_WEBSOCKET_API]: {
target: `wss://域名:16000`,
changeOrigin: true,
ws: true,
pathRewrite: {
['^' + process.env.VUE_APP_WSS_WEBSOCKET_API]: ''
}
}
},
disableHostCheck: true
},
2.2 Vue项目路由配置代码
-
为什么要配置两个地址呢? , 因为在本次测试的时候使用的是普通的ws方式连接 所以为了方便切换就写了两个websocket代理路由
- .env.development 文件和 .env.production 文件都加上这两行代码即可
// WebSocket地址
VUE_APP_WEBSOCKET_API = '/websocket-api'
// WebSocket wss 地址
VUE_APP_WSS_WEBSOCKET_API = '/wss-websocket-api'
3.3 服务器Nginx配置
server {
add_header X-Frame-Options ALLOWALL;
listen 8681 ssl;
server_name 域名; #需要将yourdomain.com替换成证书绑定的域名。
root ..\html;
index index.html index.htm;
ssl_certificate pem文件地址; #需要将cert-file-name.pem替换成已上传的证书文件的名称。
ssl_certificate_key文件地址; #需要将cert-file-name.key替换成已上传的证书私钥文件的名称。
ssl_session_timeout 6m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; #表示使用的TLS协议的类型。
ssl_prefer_server_ciphers on;
#charset koi8-r;
access_log logs/host.access.log main;
#默认目录
location / {
root C:/xxx/dist;
index index.html;
try_files $uri $uri/ @router;
}
location @router {
rewrite ^.*$ /index.html last;
}
#vue二级目录代理
location /admin {
alias /root/www/admin;
index index.html;
try_files $uri $uri/ /index.html last;
}
location /prod-api {
rewrite ^/prod-api/(.*)$ /$1 break;
proxy_pass http://localhost:16000;
proxy_set_header Host $host;
add_header X-Frame-Options ALLOWALL;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header authorization $http_authorization;
}
# websocket wss 连接方式的路由代理配置
location /wss-websocket-api {
rewrite ^/wss-websocket-api/(.*)$ /$1 break;
proxy_pass http://localhost:16000; #通过配置端口指向部署websocker的项目
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
3. 使用方式
3.1 前端代码
// 当前浏览器Location对象
const nowLocation = window.location;
// 协议 => http、https
const protocol = nowLocation.protocol;
// hostName => ip
const hostName = nowLocation.hostname;
// host => ip:port
const host = nowLocation.host;
// websocket api 地址
// 这个判断就是根据当前项目环境 自动确定使用 ws 还是 wss 的路由地址
const websocket_pattern = (hostName == '域名') ? 'wss-websocket-api' : 'websocket-api';
// websocket 请求地址前缀
const webSocketApiUrl = ((protocol.startsWith('https')) ? 'wss://' : 'ws://') + host + '/' + websocket_pattern;
// 当前WebSocket的请求地址前缀,
// /websocket/template-push/ 就是我后端配置的websocket端点地址
let REQUEST_WEBSOCKET_URL_PREFIX = webSocketApiUrl + '/websocket/template-push/';
// 当前的WwebSocket对象
let CURRENT_SOCKET = null;
// 当前请求WebSocket的指令代码
let CURRENT_INDICATE_CODE = null;
let ENABLE_CONFIG = {
WEBSOCKET_PUSH_VIDEO_ENABLE: true,
}
/**
* 1. 初始化WebSocket连接对象
* @param {*} clientKey 当前客户端Key
*/
function openWebSocket(clientKey) {
if (CURRENT_SOCKET != null) {
CURRENT_SOCKET.close();
CURRENT_SOCKET = null;
}
CURRENT_SOCKET = new WebSocket(REQUEST_WEBSOCKET_URL_PREFIX + clientKey);
CURRENT_SOCKET.onopen = event => {
console.log('连接Socket');
};
// 从服务器接受到信息时的回调函数
CURRENT_SOCKET.onmessage = event => {
console.log('收到服务器响应 , 响应数据信息==>' , event.data);
};
CURRENT_SOCKET.onclose = event => {
console.log('关闭Socket连接!');
};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = () => {
CURRENT_SOCKET.close();
CURRENT_SOCKET = null;
};
}
function getWebSocketConnection() {
return CURRENT_SOCKET;
}
- 前端websocket向后端发送数据使用方式
let sendData = {};
getWebSocketConnection().send(JSON.stringify(sendData));
3.2 后端代码
package com.ruoyi.web.controller.websocket;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.websocket.WebSocketClientIndicate;
import com.ruoyi.websocket.WebSocketRequest;
import com.ruoyi.websocket.WebSocketTemplateSession;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
/**
* --->
*
* @author xqh , 987072248@qq.com
* @data 2023-11-02 15:46:51
*/
@Component
@ServerEndpoint("/websocket/template-push/{clientKey}")
@RequiredArgsConstructor
public class WebSocketTemplateInfoPushServer {
/**
* 统计在线人数 线程安全的计数器 比原子更新类 效率更高 更专业
*/
private final static LongAdder ONLINE_ADDER = new LongAdder();
/**
* 客户端 连接会话存储Map , 每个客户端对应一个唯一Id , 在当前端点中 唯一Id为Session Id
*/
private final static Map<String, WebSocketTemplateSession> SESSION_MAP = new ConcurrentHashMap<>();
/**
* 通过 clientKey 反查 sessionId , key为clientKey , value 为sessionId
*/
private final static Map<String, String> CLIENT_KEY_SESSION_STORE_MAP = new ConcurrentHashMap<>();
private static final Logger WEBSOCKET_TEMPLATE_PUSH_LOGGER = LoggerFactory.getLogger(WebSocketTemplateInfoPushServer.class);
/**
* 1. 有新的连接访问当前 websocket 地址
*
* @param session 当前客户端的服务器对象 session
* @param clientCode 客户端设备唯一code码
*/
@OnOpen
public void doConnectionSocket(Session session, @PathParam("clientKey") String clientCode) {
// 前端异常 通过抓包发送 则直接关闭当前创建的session对象
if (StringUtils.isEmpty(clientCode)) {
try {
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "参数不合法,已关闭当前连接!"));
} catch (IOException e) {
WEBSOCKET_TEMPLATE_PUSH_LOGGER.error(e.getMessage());
}
}
// 正常则建立连接 存储数据 并返回连接成功
else {
String sessionId = session.getId();
SESSION_MAP.put(sessionId, new WebSocketTemplateSession(session,clientCode));
CLIENT_KEY_SESSION_STORE_MAP.put(sessionId,clientCode);
ONLINE_ADDER.increment();
WEBSOCKET_TEMPLATE_PUSH_LOGGER.info("WebSocket-连接成功,此刻连接设备码为: [{}] , 此刻在线的连接数为:[{}]", clientCode, ONLINE_ADDER.sum());
}
}
/**
* 2. 关闭当前websocket连接
* @param session
*/
@OnClose
public void doCloseSocket(Session session) {
try {
String sessionId = session.getId();
WebSocketTemplateSession doCloseSession = SESSION_MAP.get(sessionId);
doCloseSession.getSession().close();
// 清除当前关联的Session信息
SESSION_MAP.remove(sessionId);
CLIENT_KEY_SESSION_STORE_MAP.remove(sessionId);
ONLINE_ADDER.decrement();
WEBSOCKET_TEMPLATE_PUSH_LOGGER.info("WebSocket-连接关闭,此刻在线的连接数为:[{}]", ONLINE_ADDER.sum());
} catch (IOException e) {
WEBSOCKET_TEMPLATE_PUSH_LOGGER.error(e.getMessage());
}
}
/**
* 3. 接收客户端主动发送的消息数据
* @param session 当前会话
* @param jsonMessage 客户端发送的JSON数据
*/
@OnMessage
public void receiveMessage(Session session , String jsonMessage){
try {
// 收到前端发送的信息
} catch (JSONException jsonException){
WEBSOCKET_TEMPLATE_PUSH_LOGGER.error("JSON格式有误,异常信息->[{}]" , jsonException.getMessage());
} catch (Exception e){
WEBSOCKET_TEMPLATE_PUSH_LOGGER.error("接收信息接口失败,异常信息->[{}]" , e.getMessage());
}
}
}
- WebSocketTemplateSession
@Data
@AllArgsConstructor
public class WebSocketTemplateSession {
private Session session;
private String clientKey;
}
4. 测试使用
-
本地的 ws 方式
-
服务器的 wss 方式