分散式下的WebSocket解決方案
WebSocket單體應用介紹
在介紹分散式叢集之前,我們先來看一下王子的WebSocket程式碼實現,先來看java後端程式碼如下:
複製程式碼
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/webSocket/{key}")
public class WebSocket {
private static int onlineCount = 0;
/**
* 儲存連線的客戶端
/
private static Map<String, WebSocket> clients = new ConcurrentHashMap<String, WebSocket>();
private Session session;
/*
* 傳送的目標科室code
*/
private String key;
@OnOpen
public void onOpen(@PathParam("key") String key, Session session) throws IOException {
this.key = key;
this.session = session;
if (!clients.containsKey(key)) {
addOnlineCount();
}
clients.put(key, this);
Log.info(key+"已連線訊息服務!");
}
@OnClose
public void onClose() throws IOException {
clients.remove(key);
subOnlineCount();
}
@OnMessage
public void onMessage(String message) throws IOException {
if(message.equals("ping")){
return ;
}
JSONObject jsonTo = JSON.parseObject(message);
String mes = (String) jsonTo.get("message");
if (!jsonTo.get("to").equals("All")){
sendMessageTo(mes, jsonTo.get("to").toString());
}else{
sendMessageAll(mes);
}
}
@OnError
public void onError(Session session, Throwable error) {
error.printStackTrace();
}
private void sendMessageTo(String message, String To) throws IOException {
for (WebSocket item : clients.values()) {
if (item.key.contains(To) )
item.session.getAsyncRemote().sendText(message);
}
}
private void sendMessageAll(String message) throws IOException {
for (WebSocket item : clients.values()) {
item.session.getAsyncRemote().sendText(message);
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocket.onlineCount--;
}
public static synchronized Map<String, WebSocket> getClients() {
return clients;
}
}
複製程式碼
示例程式碼中並沒有使用Spring,用的是原生的java web編寫的,簡單和大家介紹一下里面的方法。
onOpen:在客戶端與WebSocket服務連線時觸發方法執行
onClose:在客戶端與WebSocket連線斷開的時候觸發執行
onMessage:在接收到客戶端傳送的訊息時觸發執行
onError:在發生錯誤時觸發執行
可以看到,在onMessage方法中,我們直接根據客戶端傳送的訊息,進行訊息的轉發功能,這樣在單體訊息服務中是沒有問題的。
再來看一下js程式碼
複製程式碼
var host = document.location.host;
// 獲得當前登入科室
var deptCodes='${sessionScope.$UserContext.departmentID}';
deptCodes=deptCodes.replace(/[\[|\]|\s]+/g, "");
var key = '${sessionScope.$UserContext.userID}'+deptCodes;
var lockReconnect = false; //避免ws重複連線
var ws = null; // 判斷當前瀏覽器是否支援WebSocket
var wsUrl = 'ws://' + host + '/webSocket/'+ key;
createWebSocket(wsUrl); //連線ws
function createWebSocket(url) {
try{
if('WebSocket' in window){
ws = new WebSocket(url);
}else if('MozWebSocket' in window){
ws = new MozWebSocket(url);
}else{
layer.alert("您的瀏覽器不支援websocket協議,建議使用新版谷歌、火狐等瀏覽器,請勿使用IE10以下瀏覽器,360瀏覽器請使用極速模式,不要使用相容模式!");
}
initEventHandle();
}catch(e){
reconnect(url);
console.log(e);
}
}
function initEventHandle() {
ws.onclose = function () {
reconnect(wsUrl);
console.log("llws連線關閉!"+new Date().toUTCString());
};
ws.onerror = function () {
reconnect(wsUrl);
console.log("llws連線錯誤!");
};
ws.onopen = function () {
heartCheck.reset().start(); //心跳檢測重置
console.log("llws連線成功!"+new Date().toUTCString());
};
ws.onmessage = function (event) { //如果獲取到訊息,心跳檢測重置
heartCheck.reset().start(); //拿到任何訊息都說明當前連線是正常的//接收到訊息實際業務處理
…
};
}
// 監聽視窗關閉事件,當視窗關閉時,主動去關閉websocket連線,防止連線還沒斷開就關閉視窗,server端會拋異常。
window.onbeforeunload = function() {
ws.close();
}
function reconnect(url) {
if(lockReconnect) return;
lockReconnect = true;
setTimeout(function () { //沒連線上會一直重連,設定延遲避免請求過多
createWebSocket(url);
lockReconnect = false;
}, 2000);
}
//心跳檢測
var heartCheck = {
timeout: 300000, //5分鐘發一次心跳
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
//這裡傳送一個心跳,後端收到後,返回一個心跳訊息,
//onmessage拿到返回的心跳就說明連線正常
ws.send("ping");
console.log("ping!")
self.serverTimeoutObj = setTimeout(function(){//如果超過一定時間還沒重置,說明後端主動斷開了
ws.close(); //如果onclose會執行reconnect,我們執行ws.close()就行了.如果直接執行reconnect 會觸發onclose導致重連兩次
}, self.timeout)
}, this.timeout)
}
}
複製程式碼
js部分使用的是原生H5編寫的,如果為了更好的相容瀏覽器,也可以使用SockJS,有興趣小夥伴們可以自行百度。
接下來我們就手動的優化程式碼,實現WebSocket對分散式架構的支援。
解決方案的思考
現在我們已經瞭解單體應用下的程式碼結構,也清楚了WebSocket在分散式環境下面臨的問題,那麼是時候思考一下如何能夠解決這個問題了。
我們先來看一看發生這個問題的根本原因是什麼。
簡單思考一下就能明白,單體應用下只有一臺伺服器,所有的客戶端連線的都是這一臺訊息伺服器,所以當釋出訊息者傳送訊息時,所有的客戶端其實已經全部與這臺伺服器建立了連線,直接群發訊息就可以了。
換成分散式系統後,假如我們有兩臺訊息伺服器,那麼客戶端通過Nginx負載均衡後,就會有一部分連線到其中一臺伺服器,另一部分連線到另一臺伺服器,所以釋出訊息者傳送訊息時,只會傳送到其中的一臺伺服器上,而這臺訊息伺服器就可以執行群發操作,但問題是,另一臺伺服器並不知道這件事,也就無法傳送訊息了。
現在我們知道了根本原因是生產訊息時,只有一臺訊息伺服器能夠感知到,所以我們只要讓另一臺訊息伺服器也能感知到就可以了,這樣感知到之後,它就可以群發訊息給連線到它上邊的客戶端了。
那麼什麼方法可以實現這種功能呢,王子很快想到了引入訊息中介軟體,並使用它的釋出訂閱模式來通知所有訊息伺服器就可以了。
引入RabbitMQ解決分散式下的WebSocket問題
在訊息中介軟體的選擇上,王子選擇了RabbitMQ,原因是它的搭建比較簡單,功能也很強大,而且我們只是用到它群發訊息的功能。
RabbitMQ有一個廣播模式(fanout),我們使用的就是這種模式。
首先我們寫一個RabbitMQ的連線類:
複製程式碼
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMQUtil {
private static Connection connection;
/**
* 與rabbitmq建立連線
* @return
*/
public static Connection getConnection() {
if (connection != null&&connection.isOpen()) {
return connection;
}
ConnectionFactory factory = new ConnectionFactory();
factory.setVirtualHost("/");
factory.setHost("192.168.220.110"); // 用的是虛擬IP地址
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
try {
connection = factory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return connection;
}
}
複製程式碼
這個類沒什麼說的,就是獲取MQ連線的一個工廠類。
然後按照我們的思路,就是每次伺服器啟動的時候,都會建立一個MQ的消費者監聽MQ的訊息,王子這裡測試使用的是Servlet的監聽器,如下:
複製程式碼
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class InitListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
WebSocket.init();
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
複製程式碼
記得要在Web.xml中配置監聽器資訊
複製程式碼
<?xml version="1.0" encoding="UTF-8"?>
InitListener
複製程式碼
WebSocket中增加init方法,作為MQ消費者部分
複製程式碼
public static void init() {
try {
Connection connection = RabbitMQUtil.getConnection();
Channel channel = connection.createChannel();
//交換機宣告(引數為:交換機名稱;交換機型別)
channel.exchangeDeclare(“fanoutLogs”,BuiltinExchangeType.FANOUT);
//獲取一個臨時佇列
String queueName = channel.queueDeclare().getQueue();
//佇列與交換機繫結(引數為:佇列名稱;交換機名稱;routingKey忽略)
channel.queueBind(queueName,“fanoutLogs”,"");
//這裡重寫了DefaultConsumer的handleDelivery方法,因為傳送的時候對訊息進行了getByte(),在這裡要重新組裝成String
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
super.handleDelivery(consumerTag, envelope, properties, body);
String message = new String(body,"UTF-8");
System.out.println(message);
//這裡可以使用WebSocket通過訊息內容傳送訊息給對應的客戶端
}
};
//宣告佇列中被消費掉的訊息(引數為:佇列名稱;訊息是否自動確認;consumer主體)
channel.basicConsume(queueName,true,consumer);
//這裡不能關閉連線,呼叫了消費方法後,消費者會一直連線著rabbitMQ等待消費
} catch (IOException e) {
e.printStackTrace();
}
}
複製程式碼
同時在接收到訊息時,不是直接通過WebSocket傳送訊息給對應客戶端,而是傳送訊息給MQ,這樣如果訊息伺服器有多個,就都會從MQ中獲得訊息,之後通過獲取的訊息內容再使用WebSocket推送給對應的客戶端就可以了。
WebSocket的onMessage方法增加內容如下:
複製程式碼
try {
//嘗試獲取一個連線
Connection connection = RabbitMQUtil.getConnection();
//嘗試建立一個channel
Channel channel = connection.createChannel();
//宣告交換機(引數為:交換機名稱; 交換機型別,廣播模式)
channel.exchangeDeclare(“fanoutLogs”, BuiltinExchangeType.FANOUT);
//訊息釋出(引數為:交換機名稱; routingKey,忽略。在廣播模式中,生產者宣告交換機的名稱和型別即可)
channel.basicPublish(“fanoutLogs”,"", null,msg.getBytes(“UTF-8”));
System.out.println(“釋出訊息”);
channel.close();
} catch (IOException |TimeoutException e) {
e.printStackTrace();
}
亞馬遜測評 www.yisuping.com
相關文章
- 聊聊分散式下的WebSocket解決方案分散式Web
- 分散式鎖的解決方案分散式
- 分散式事務解決方案分散式
- Redis 分散式鎖解決方案Redis分散式
- Redis分散式鎖解決方案Redis分散式
- SAP HANA分散式解決方案分散式
- 常用的分散式事務解決方案分散式
- Spring Cloud 微服務架構下的 WebSocket 解決方案SpringCloud微服務架構Web
- SpringCloud 分散式事務解決方案SpringGCCloud分散式
- 分散式互斥的高效容錯解決方案分散式
- 分散式事務解決方案(五)【TCC型方案】分散式
- 微服務架構下分散式事務解決方案-hoop(一)微服務架構分散式OOP
- 分散式事務解決方案彙總分散式
- 杉巖分散式儲存解決方案分散式
- MSSQL server分散式事務解決方案SQLServer分散式
- 分散式事務解決方案--GTS(二)分散式
- 分散式事務解決方案--GTS(一)分散式
- Java微服務下的分散式事務介紹及其解決方案2Java微服務分散式
- 基於RocketMq的分散式事務解決方案MQ分散式
- 你必須瞭解的分散式事務解決方案分散式
- 分散式 ID 解決方案之美團 Leaf分散式
- 分散式事務解決方案(一)【介紹】分散式
- 聊聊Seata分散式解決方案AT模式的實現原理分散式模式
- 分散式ID生成器的解決方案總結分散式
- 最常用的分散式ID解決方案,你知道幾個分散式
- 生成分散式唯一ID的幾種解決方案分散式
- 五大分散式場景解決方案分散式
- 分散式事務解決方案(四)【最大努力通知】分散式
- 分散式WebSocket架構分散式Web架構
- .NET開源的處理分散式事務的解決方案分散式
- 【分散式鎖的演化】常用鎖的種類以及解決方案分散式
- 分散式事務的理解和常見解決方案彙總分散式
- 架構師必備的那些分散式事務解決方案!!架構分散式
- 打造基於 PostgreSQL/openGauss 的分散式資料庫解決方案SQL分散式資料庫
- 剛柔並濟的開源分散式事務解決方案分散式
- 常用的分散式事務解決方案介紹有多少種?分散式
- 分散式事務(2)---強一致性分散式事務解決方案分散式
- 杉巖海量圖片分散式儲存解決方案分散式