WebSocket多服务负载均衡下怎么进行交互
前言
平时开发我们websocket都是单服务测试,在实际部署的项目中用到了负载均衡部署多台tomcat服务;
那么多台服务直接的websocket服务又怎么实现互通呢?
案例
如两台服务器8081/8082,因为nginx代理的原因,A客户端连上了8081,B客户端连上了8082,那么两个客户端连接的websocket服务都是不同的,A客户端怎么给B客户端发送消息呢?
解决
使用redis缓存?本人试过了,由于websocket的session无法存储到redis中,这种方式不大可行;
于是使用如下方案实现了互通
代码
代码都是复制过去就可以用的,可能有些引入不一样而已
java端websocket服务
java
package com.webSocket;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
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 org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component // SpringBoot打包成war需要注释,不然会报错(tomcat下不需要这个注解)
@ServerEndpoint("/webSocket/{uuid}")
public class WebSocket {
/**
* 记录当前在线的客户端
*/
public static List<Map<String, WebSocket>> list = new ArrayList<>();
/**
* 记录当前在线连接数
*/
private static AtomicInteger screenCount = new AtomicInteger(0);
public Session session;
public String uuid;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(@PathParam("uuid") String uuid, Session session) {
if (!StrUtil.isNotEmpty(uuid)) {
sendError("uuid不能为空!", session);
return;
}
this.uuid = uuid;
this.session = session;
Map<String, WebSocket> map = new HashMap<>();
map.put(uuid, this);
list.add(map);
screenCount.incrementAndGet();
log.info("有新连接加入:{},当前在线数量为:{}", uuid, screenCount.get());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
for (Map<String, WebSocket> map : list) {
for (String key : map.keySet()) {
if (map.get(key).equals(this)) {
list.remove(map);
}
}
}
if (screenCount.get() > 0) {
screenCount.decrementAndGet();
}
log.info("有一连接关闭:{},当前在线数量为:{}", session.getId(), screenCount.get());
}
/**
* 异常情况下回调
*/
public static void sendError(String str, Session toSession) {
try {
toSession.getBasicRemote().sendText(JSONObject.toJSONString(new ResVo(-1, false, str, null)));
toSession.close();
} catch (IOException e) {
log.error("关闭连接异常:{}", e);
} catch (Exception e) {
log.error("服务端发送消息给客户端失败:{}", e);
}
}
/**
* 服务端发送消息给客户端
*/
public static void sendMessage(ResVo resVo, Session toSession) {
try {
toSession.getBasicRemote().sendText(JSONObject.toJSONString(resVo));
} catch (Exception e) {
log.error("服务端发送消息给客户端失败");
try {
toSession.close();
} catch (IOException ioException) {
log.error("关闭失败");
}
}
}
/**
* 接收到指令-给所有type类型的连接发送消息
*
* @param message
*/
@OnMessage
public static void onMessage(String message) {
try {
/*解析消息为JSON格式*/
net.sf.json.JSONObject jsonTo = net.sf.json.JSONObject.fromObject(message);
Object mes = (Object) jsonTo.get("message");
String uuid = (String) jsonTo.get("To");
if (uuid != null) {
for (Map<String, WebSocket> map : list) {
if (map.get(uuid) != null) {
sendMessage(new ResVo(0, true, null, mes), map.get(uuid).session);
}
}
}
// 如果有值了,说明另一个服务已经处理过了
if (!jsonTo.has("ws_state") || jsonTo.get("ws_state") == null) {
jsonTo.put("ws_state", "服务交互");
WsClicent.webSocketClient.onSend(jsonTo.toString());
}
} catch (Exception e) {
log.error("接收消息处理异常!ERROR=[{}]", e.getMessage());
}
}
}
调用推送
java
// 【消息推送】
net.sf.json.JSONObject jo = new net.sf.json.JSONObject();
// map就是要推送的数据
jo.put(ServiceConstant.message, map);
// 推送给某某指定的项目页面;前缀一样的是指某个页面,多加了个id区别指定某个项目
jo.put("To", "attendance_"+id);
WebSocket.onMessage(jo.toString());
java端websocket客户端
这一步就关键了,就是用于推送给两台服务直接进行交互的
继承WebSocketClient的原因很简单,就是它本身自带有重连,但是太频繁了,所以继承后这个重连机制就取消了,我们就可以自己写一个想要的时间重连
注:发布的时候8081端口的tomcat就要把路径改成8082的,8082端口的tomcat就要把路径改成8081的(路径要自己定义另一个服务的ws地址)
java
package com.webSocket;
import lombok.extern.slf4j.Slf4j;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.enums.ReadyState;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
/**
* websocket客户端(用于多tomcat服务交互)
* @author
*/
@Slf4j
public class WsClicent extends WebSocketClient{
/** 重连次数*/
private static int reconnect_count = 10;
/** 重连:用于判断*/
private static boolean reconnect_state = true;
/** 连接地址*/
private static String connect_url = "http://localhost:8081/projectName/webSocket/8081";
/** websocket客户端对象*/
public static WsClicent webSocketClient;
/** 构造*/
public WsClicent(URI serverUri, Draft protocolDraft) {
super(serverUri, protocolDraft);
}
/**
* 服务端发送消息给客户端
*/
public static WsClicent init() {
try {
webSocketClient = new WsClicent(new URI(connect_url), new Draft_6455());
webSocketClient.connect();
return webSocketClient;
} catch (Exception e) {
log.info("【websocket-客户端异常】:", e);
}
return null;
}
/**
* 连接服务端时触发
*/
@Override
public void onOpen(ServerHandshake handshakedata) {
log.info("[websocket-客户端和服务器连接成功]");
reconnect_count = 10;
}
/**
* 收到服务端消息时触发
*/
@Override
public void onMessage(String message) {
log.info("[websocket-客户端收到消息={}]", message);
WebSocket.onMessage(message); // 这里就是服务直接的交互了
}
/**
* 和服务端断开连接时触发
*/
@Override
public void onClose(int code, String reason, boolean remote) {
log.info("[websocket-客户端退出/关闭连接]");
reconnect_state = true;
reConnect();
}
/**
* 连接异常时触发
*/
@Override
public void onError(Exception ex) {
log.info("[websocket-客户端和服务器连接发生错误={}]", ex.getMessage());
}
/**
* 发送消息给另一个服务端
*/
public void onSend(String msg) {
webSocketClient.send(msg);
}
/**
* 断开重连
*/
public static void reConnect() {
if(reconnect_state && !webSocketClient.getReadyState().equals(ReadyState.OPEN)) {
reconnect_state = false;
log.info("【websocket-调用重连线程】:{}", reconnect_count);
new Thread(() -> {
try {
// 如果5秒执行一次10次后(50秒)都无法连接成功,则后面30秒重连一次
if(reconnect_count > 0) {
reconnect_count --;
Thread.sleep(1000 * 5);
}else {
Thread.sleep(1000 * 30);
}
init();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
前端websocket.js实现
在js中使用websocket连接,并包含了心跳、重连、处理...
javascript
/**
* websocket消息推送
*/
var websocket = null;
var host = document.location.host;
var judgeTemp = 0;
var loadTemp = 0;
var heatInterval;
var refreshPage;
var createWSInter;
var ws_value = 88; // 某某项目的ID
/**
* -------------------------------------------------------------------------------------------------------
* 根据不同的http协议使用不同的websocket连接方式
* @returns
*/
function getWSURL(){
var urlPath = window.document.location.href;
if(urlPath.indexOf("https:") != -1){
return 'wss://' + host + '/projectName/webSocket/attendance_'+ws_value;
}else{
return 'ws://' + host + '/projectName/webSocket/attendance_'+ws_value;
}
}
// 初始化调用
createWebSocket();
/**
* -------------------------------------------------------------------------------------------------------
* 创建websock连接
* @returns
*/
function createWebSocket(){
// 刷新页面操作(根据自己需求使用,我这里使用在了考勤方面,所以避免失效什么的才写的)
if(!refreshPage){
refreshPage = setInterval(function(){
var date = Date.parse(new Date());
var maxTime = parseInt((date - mqttDate) / 1000);
if(maxTime > (60 * 60 * 2)){ // 两小时未接收到ws的消息就刷新页面
window.location.reload();
}
}, 1000 * 60 * 10); // 十分钟执行一次
}
try{
// 判断当前浏览器是否支持WebSocket
if('WebSocket' in window) {
var wsUrl = getWSURL();
websocket = new WebSocket(wsUrl);
initEventHandle();
} else {
alert('当前浏览器 Not support websocket,消息推送将无法使用')
}
}catch(err){
loadTemp ++;
if(loadTemp == 60){ // 等待60个十秒连接失败后刷新页面
window.location.reload();
}
if(!createWSInter){
createWSInter = setInterval(function(){
createWebSocket();// 由于网络问题可能无法连接(10秒连接秒后重新连接)
}, 1000 * 10)
}
console.log(err)
console.log("连接错误计数:"+createWebSocket, ' ---- ', err.message);
}
}
/**
* -------------------------------------------------------------------------------------------------------
* 回调处理
* @returns
*/
function initEventHandle(){
//连接发生错误的回调方法
websocket.onerror = function() {
judgeTemp = 2;
};
//连接成功建立的回调方法
websocket.onopen = function() {
console.log("成功连接---"+judgeTemp);
clearInterval(createWSInter);// 连接成功关闭重连
heatInterval = setInterval(function () { // 制造心跳
if (websocket.readyState == 1) {
websocket.send(JSON.stringify({
uuid: ws_value
}))
}
}, 1000*60);
}
//接收到消息的回调方法
websocket.onmessage = function(event) {
console.log("成功接收---"+event.data);
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function() {
console.log("关闭连接---"+judgeTemp);
clearInterval(heatInterval);// 关闭心跳
if(judgeTemp != 1){// 非正常关闭就给它重连
createWebSocket();
}
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function() {
judgeTemp = 1;
websocket.close(); // 关闭WebSocket连接
}
}
/**
* -------------------------------------------------------------------------------------------------------
* 接收消息
*/
var mqttDate = Date.parse(new Date());
function setMessageInnerHTML(message) {
var date = Date.parse(new Date());
var maxTime = parseInt((date - mqttDate) / 1000);
if(maxTime > 5){// 相差>5秒,执行一次(一般消息推送太频繁才限制,不然直接获取message解析成json即可)
mqttDate = date;
refreshData();
}
}
// 刷新数据
function refreshData(){
// 调用某某函数获取后台数据
}
问题解决方案
tomcat重启后,怎么保证之前的通讯?
解决方式一:客户端实现重连,如:js中的websocket进行重连机制。
多tomcat服务下,不同客户端连接不同tomcat下的websocket,怎么进行通讯?
解决方式一:创建一个单独websocket服务,然后tomcat服务以客户端方式推送给ws服务,通过ws服务对js客户端进行反馈,以ws单独昨晚媒介进行客户端直接通讯。
解决方式二:在服务端创建一个java客户端,接收到js客户端消息后WS服务处理完成并标记,再通过本服务的java客户端推送给另一个WS服务,另一个WS服务接收处理后发现已被标记,则不需要再给另一个服务推送,完成闭环。
解决方式三:基本跟方式二一样,但是,使用消息队列(如RabbitMQ分组)作为两个服务之间的客户端,每次其中一个ws服务接收到js客户端消息后,处理完并对mq进行消息设置,让mq对另一个服务进行消息通知,然后另一个服务通过这个消息通知就可以对当前连接的客户端进行消息推送