單體Webscoket
- springboot版本: 2.1.1.RELEASE
- jdk: 1.8
示例代碼
- WebsocketServer
@ServerEndpoint("/client/{userName}")
@Component
@Slf4j
public class WebSocketServer {
/**
* 靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
*/
private Session session;
/**
* 接收userId
*/
private String userName = "";
/**
* @Description: 連接建立成功調用的方法,成功建立之後,將用戶的userName 存儲到redis
* @params: [session, userId]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnOpen
public void onOpen(Session session, @PathParam("userName") String userName) {
this.session = session;
this.userName = userName;
webSocketMap.put(userName, this);
addOnlineCount();
log.info("用戶連接:" + userName + ",當前在線人數爲:" + getOnlineCount());
}
/**
* @Description: 連接關閉調用的方法
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userName)) {
webSocketMap.remove(userName);
//從set中刪除
subOnlineCount();
}
log.info("用戶退出:" + userName + ",當前在線人數爲:" + getOnlineCount());
}
/**
* @Description: 收到客戶端消息後調用的方法, 調用API接口 發送消息到
* @params: [message, session]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnMessage
public void onMessage(String message, @PathParam("userName") String userName) {
log.info("用戶消息:" + userName + ",報文:" + message);
if (StringUtils.isNotBlank(message)) {
try {
//解析發送的報文
JSONObject jsonObject = JSON.parseObject(message);
//追加發送人(防止串改)
jsonObject.put("sender", this.userName);
String receiver = jsonObject.getString("receiver");
//傳送給對應toUserId用戶的websocket
if (StringUtils.isNotBlank(receiver) && webSocketMap.containsKey(receiver)) {
webSocketMap.get(receiver).session.getBasicRemote().sendText(jsonObject.toJSONString());
} else {
log.error("用戶:" + receiver + "不在該服務器上");
//否則不在這個服務器上,發送到mysql或者redis
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 發佈websocket消息
* 消息格式: { "sender": "u2","receiver": "u1","msg": "hello world","createTime":"2021-10-12 11:12:11"}
*
* @param dto
* @return
*/
public static void sendWebsocketMessage(ChatMsg dto) {
if (dto != null) {
if (StringUtils.isNotBlank(dto.getReceiver()) && webSocketMap.containsKey(dto.getReceiver())) {
String json = JSON.toJSONString(dto);
try {
webSocketMap.get(dto.getReceiver()).session.getBasicRemote().sendText(json);
} catch (IOException e) {
log.error("消息發送異常:{}", e.toString());
}
} else {
log.error("用戶:" + dto.getReceiver() + ",不在線!");
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用戶錯誤:" + this.userName + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* @Description: 獲取在線人數
* @params: []
* @return: int
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* @Description: 在線人數+1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
/**
* @Description: 在線人數-1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
- WebSocketConfig
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
- 前端代碼
var socket;
var userName;
establishConnection()
/***建立連接*/
function establishConnection() {
userName = $("#sender").val();
if (userName == '' || userName == null) {
alert("請輸入發送者");
return;
}
//實現化WebSocket對象,指定要連接的服務器地址與端口 建立連接
var socketUrl = "" + window.location.protocol + "//" + window.location.host + "/client/" + userName;
socketUrl = socketUrl.replace("https", "ws").replace("http", "ws");
if (socket != null) {
socket.close();
socket = null;
}
socket = new WebSocket(socketUrl);
//打開事件
socket.onopen = function () {
console.log("開始建立鏈接....")
};
//關閉事件
socket.onclose = function () {
console.log("websocket已關閉");
};
//發生了錯誤事件
socket.onerror = function () {
console.log("websocket發生了錯誤");
};
/**
* 接收消息
* @param msg
*/
socket.onmessage = function (msg) {
msg = JSON.parse(msg.data);
console.log(msg);
if (msg.msg != '連接成功') {
$("#msgDiv").append('<p class="other">用戶名:' + msg.sender + '</p><p class="chat">' + msg.msg + '</p>');
}
};
}
/**
* 發送消息
*/
function sendMessage() {
var msg = $("#msg").val();
if (msg == '' || msg == null) {
alert("消息內容不能爲空");
return;
}
var receiver = $("#receiver").val();
if (receiver == '' || receiver == null) {
alert("接收人不能爲空");
return;
}
var msgObj = {
"receiver": receiver,
"msg": msg
};
$("#msgDiv").append('<p class="user">用戶名:' + userName + '</p><p class="chat">' + msg + '</p>');
try{
socket.send(JSON.stringify(msgObj));
$("#msg").val('');
}catch (e) {
alert("服務器內部錯誤");
}
}
-
測試效果
-
問題
如果兩個客戶端連接不在同一個服務器上,會出現什麼問題?
結果就是如下所示:
如何解決多臺客戶端連接在不同服務器,互相發送消息問題!
分佈式WebSocket 解決
方案一 Redis消息訂閱與發佈
描述:
客戶端A 和客戶端B 都訂閱同一個Topic ,後臺Websocket收到消息後,將消息發送至Redis中,同時服務端會監聽該渠道內的消息,監聽到消息後,會將消息推送至對應的客戶端。
示例代碼
- application.yml
主要是Redis配置
server:
port: 8082
spring:
thymeleaf:
#模板的模式,支持 HTML, XML TEXT JAVASCRIPT
mode: HTML5
#編碼 可不用配置
encoding: UTF-8
#內容類別,可不用配置
content-type: text/html
#開發配置爲false,避免修改模板還要重啓服務器
cache: false
# #配置模板路徑,默認是templates,可以不用配置
prefix: classpath:/templates
suffix: .html
#Redis配置
redis:
host: localhost
port: 6379
password: 123456
timeout: 5000
- RedisSubscriberConfig.java
/**
* @Description 消息訂閱配置類
* @Author wxl
* @Date 2020/3/31 13:54
*/
@Configuration
public class RedisSubscriberConfig {
/**
* 消息監聽適配器,注入接受消息方法
*
* @param receiver
* @return
*/
@Bean
public MessageListenerAdapter messageListenerAdapter(ChatMessageListener receiver) {
return new MessageListenerAdapter(receiver);
}
/**
* 創建消息監聽容器
*
* @param redisConnectionFactory
* @param messageListenerAdapter
* @return
*/
@Bean
public RedisMessageListenerContainer getRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, MessageListenerAdapter messageListenerAdapter) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.addMessageListener(messageListenerAdapter, new PatternTopic(TOPIC_CUSTOMER));
return redisMessageListenerContainer;
}
}
- RedisUtil.java
@Component
public class RedisUtil {
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 發佈
*
* @param key
*/
public void publish(String key, String value) {
stringRedisTemplate.convertAndSend(key, value);
}
}
- ChatMessageListener.java
/**
* @Description 集羣聊天消息監聽器
* @Author wxl
* @Date 2020/3/29 15:07
*/
@Slf4j
@Component
public class ChatMessageListener implements MessageListener {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
RedisSerializer<String> valueSerializer = redisTemplate.getStringSerializer();
String value = valueSerializer.deserialize(message.getBody());
ChatMsg dto = null;
if (StringUtils.isNotBlank(value)) {
try {
dto = JacksonUtil.json2pojo(value, ChatMsg.class);
} catch (Exception e) {
e.printStackTrace();
log.error("消息格式轉換異常:{}", e.toString());
}
log.info("監聽集羣websocket消息--- {}", value);
WebSocketServer.sendWebsocketMessage(dto);
}
}
}
- WebSocketServer
@ServerEndpoint("/client/{userName}")
@Component
@Slf4j
public class WebSocketServer {
/**
* 靜態變量,用來記錄當前在線連接數。應該把它設計成線程安全的。
*/
private static int onlineCount = 0;
/**
* concurrent包的線程安全Set,用來存放每個客戶端對應的MyWebSocket對象。
*/
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**
* 與某個客戶端的連接會話,需要通過它來給客戶端發送數據
*/
private Session session;
/**
* 不能使用@AutoWire原因:發現注入不了redis,redis注入失敗 可能是因爲實例化的先後順序吧,WebSocket先實例化了, 但是@Autowire是會觸發getBean操作
* 因爲@ServerEndpoint不支持注入,所以使用SpringUtils獲取IOC實例
*/
private RedisUtil redisUtil = SpringUtils.getBean(RedisUtil.class);
/**
* 接收userId
*/
private String userName = "";
/**
* @Description: 連接建立成功調用的方法,成功建立之後,將用戶的userName 存儲到redis
* @params: [session, userId]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnOpen
public void onOpen(Session session, @PathParam("userName") String userName) {
this.session = session;
this.userName = userName;
webSocketMap.put(userName, this);
addOnlineCount();
log.info("用戶連接:" + userName + ",當前在線人數爲:" + getOnlineCount());
}
/**
* @Description: 連接關閉調用的方法
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userName)) {
webSocketMap.remove(userName);
//從set中刪除
subOnlineCount();
}
log.info("用戶退出:" + userName + ",當前在線人數爲:" + getOnlineCount());
}
/**
* @Description: 收到客戶端消息後調用的方法, 調用API接口 發送消息到
* @params: [message, session]
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:13 PM
*/
@OnMessage
public void onMessage(String message, @PathParam("userName") String userName) {
log.info("用戶消息:" + userName + ",報文:" + message);
if (StringUtils.isNotBlank(message)) {
try {
//解析發送的報文
JSONObject jsonObject = JSON.parseObject(message);
//追加發送人(防止串改)
jsonObject.put("sender", this.userName);
//傳送給對應toUserId用戶的websocket
redisUtil.publish(TOPIC_CUSTOMER,jsonObject.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 發佈websocket消息
* 消息格式: { "sender": "u2","receiver": "u1","msg": "hello world","createTime":"2021-10-12 11:12:11"}
*
* @param dto
* @return
*/
public static void sendWebsocketMessage(ChatMsg dto) {
if (dto != null) {
if (StringUtils.isNotBlank(dto.getReceiver()) && webSocketMap.containsKey(dto.getReceiver())) {
String json = JSON.toJSONString(dto);
try {
webSocketMap.get(dto.getReceiver()).session.getBasicRemote().sendText(json);
} catch (IOException e) {
log.error("消息發送異常:{}", e.toString());
}
} else {
log.error("用戶:" + dto.getReceiver() + ",不在次服務器上!");
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用戶錯誤:" + this.userName + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* @Description: 獲取在線人數
* @params: []
* @return: int
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* @Description: 在線人數+1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
/**
* @Description: 在線人數-1
* @params: []
* @return: void
* @Author: wangxianlin
* @Date: 2020/5/9 9:09 PM
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
-
測試效果
方案二 RabbitMq
暫未完成,敬請期待!