前言
javascript
上篇文章咱們用STOMP子協議實現了在線羣聊和一對一聊天室等功能,本篇咱們繼續WebSocket這個話題,此次咱們換個實現維度:用原生的WebSocket來實現,看看這二者在實現上的差異有多大。css
實戰WebSocket的要點html
1、WebSocket重要屬性java
屬性jquery |
備註web |
Socket.readyState緩存 |
只讀屬性 readyState 表示鏈接狀態,能夠是如下值:安全 0 - 表示鏈接還沒有創建。服務器 1 - 表示鏈接已創建,能夠進行通訊。session 2 - 表示鏈接正在進行關閉。 3 - 表示鏈接已經關閉或者鏈接不能打開。 |
Socket.bufferedAmount |
只讀屬性 bufferedAmount 已被 send() 放入正在隊列中等待傳輸,可是尚未發出的 UTF-8 文本字節數。 |
2、WebSocket核心事件
事件 |
事件處理程序 |
備註 |
open |
Socket.onopen |
鏈接創建時觸發 |
message |
Socket.onmessage |
客戶端接收服務端數據時觸發 |
error |
Socket.onerror |
通訊發生錯誤時觸發 |
close |
Socket.onclose |
鏈接關閉時觸發 |
3、WebSocket核心方法
方法 |
備註 |
Socket.send() |
使用鏈接發送數據 |
Socket.close() |
鏈接關閉 |
代碼設計實現
1、服務端部分
/**
* @author andychen https://blog.51cto.com/14815984
* @description:WebSocket配置
*/
@Configuration
public class WebSocketConfig {
/**
* 註冊並開啓WebSocket
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
/**
* @author andychen https://blog.51cto.com/14815984
* @description:WebSocket通訊業務類
*/
@ServerEndpoint("/ws/server")
@Component
public class WebSocketController {
private static final Logger log = LoggerFactory.getLogger(WebSocketController.class);
/**
* 服務端鏈接計數器
*/
private static final AtomicInteger counter = new AtomicInteger(0);
/**
* 定義客戶端會話安全容器
* 緩存客戶端會話對象(正式環境,這裏能夠直接作分佈式緩存)
*/
private static final CopyOnWriteArraySet<Session> sessionContainer = new CopyOnWriteArraySet<>();
/**
* 定義客戶端會話和用戶身份映射安全容器
*/
private static final Map<String,String> sessionMap = new ConcurrentHashMap<>();
/**
* 消息分隔字符竄
*/
private static final String MSG_SPLIT_STR = "@#@";
/**
* 消息角色
*/
private static final String[] MSG_ROLES = {"sender","recevier"};
/**
* WebSocket鏈接打開事件
* @param session 客戶端鏈接會話
*/
@OnOpen
public void open(Session session){
//緩存會話
sessionContainer.add(session);
//會話Id
String sessionId = session.getId();
if(!sessionMap.containsKey(sessionId)){
String receiver = this.getRecevier(session);
boolean isMass = (null == receiver);
//消息用戶:羣聊爲發送者,單聊時爲發送者和接收者
String usrInfo = parseMsgParameter(session, MSG_ROLES[0]);
if(isMass){
sessionMap.put(sessionId, usrInfo);
}else{
usrInfo += MSG_SPLIT_STR+receiver;
sessionMap.put(usrInfo, sessionId);
}
//發送新用戶加入消息
if(isMass){
sendMass("系統消息"+MSG_SPLIT_STR+"用戶["+usrInfo+"]加入羣聊");
}
log.info("會話[{}]加入,當前鏈接數爲:{}", sessionId, counter.incrementAndGet());
}
}
/**
* 接收客戶端消息事件
* @param message 文本消息(也支持對象、二進制Buffer)
* @param session 客戶端鏈接會話
*/
@OnMessage
public void accept(String message, Session session){
String sender = null;
String sessionId = session.getId();
String sessionId2 = null;
String msg =null;
String recevier = getRecevier(session);
if(null == recevier){
msg = sessionMap.get(sessionId)+MSG_SPLIT_STR+message;
sendMass(msg);
}else{
sender = parseMsgParameter(session, MSG_ROLES[0]);
msg = sender+MSG_SPLIT_STR+message;
//發送者sessionId
sessionId = sender+MSG_SPLIT_STR+recevier;
sessionId = sessionMap.get(sessionId);
//接收者sessionId
sessionId2 = recevier+MSG_SPLIT_STR+sender;
sessionId2 = sessionMap.get(sessionId2);
sendSingle(sessionId, sessionId2, msg);
}
log.info("已接收客戶端[{}]消息:{},請求地址:{}", sessionId, message, session.getRequestURI().toString());
}
/**
* 鏈接關閉事件
* @param session 客戶端鏈接會話
*/
@OnClose
public void close(Session session){
String sessionId = session.getId();
sessionContainer.remove(session);
String recevier =getRecevier(session);
if(null == recevier){
//羣聊發送退羣消息
String sender = sessionMap.get(sessionId);
sessionMap.remove(sessionId);
sendMass("系統消息"+MSG_SPLIT_STR+"用戶["+sender+"]退出羣聊");
}else{
sessionId = parseMsgParameter(session, MSG_ROLES[0])+MSG_SPLIT_STR+recevier;
sessionId = sessionMap.get(sessionId);
sessionMap.remove(sessionId);
}
log.info("會話[{}]關閉鏈接,當前鏈接數爲:{}", sessionId, counter.decrementAndGet());
}
/**
* 鏈接發生錯誤事件
* @param session 客戶端鏈接會話
* @param error 錯誤對象
*/
@OnError
public void error(Session session, Throwable error){
log.error("鏈接發生錯誤:{}, \n\n客戶端會話ID[{}],請求地址:{}", error.getMessage(),
session.getId(), session.getRequestURI().toString());
error.printStackTrace();
}
/**
* 是否單聊
* @param session 客戶會話id
* @return
*/
private String getRecevier(Session session){
return parseMsgParameter(session, MSG_ROLES[1]);
}
/**
* 解析消息參數
* @param session 客戶端會話
* @param name 參數名稱
* @return
*/
private static String parseMsgParameter(Session session, String name){
//獲取會話中包含的參數信息
Map<String, List<String>> params = session.getRequestParameterMap();
if(params.containsKey(name)){
return params.get(name).get(0);
}
return null;
}
/**
* 發送消息
* @param session 客戶端會話
* @param msg 消息內容
*/
private static boolean send(Session session, String msg){
try {
//異步轉發文本消息(也可發送消息對象,二進制流等)
session.getAsyncRemote().sendText(msg);
return true;
} catch (Exception e) {
log.error("消息發送失敗:{}", e.getMessage());
e.printStackTrace();
}
return false;
}
/**
* 羣發消息
* @param msg 消息內容
*/
private static void sendMass(String msg){
for (Session session : sessionContainer){
if(session.isOpen()){
//發送
send(session, msg);
}
}
}
/**
* 發送聊消息
* @param senderSid 發送者會話id
* @param recevSid 接收者會話id
* @param msg 消息內容
*/
private static void sendSingle(String senderSid, String recevSid, String msg){
String id = null;
int count = 0;
for (Session s : sessionContainer) {
id = s.getId();
if (senderSid.equals(id)) {
count++;
send(s, msg);
}
if (recevSid.equals(id)) {
count++;
send(s, msg);
}
if(2 == count){break;}
}
if(2 > count){
log.warn("未找到指定會話[ID: {}或{}]", senderSid, recevSid);
}
}
}
2、客戶端部分
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="aplus-terminal" content="1">
<meta name="apple-mobile-web-app-title" content="">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<meta name="format-detection" content="telephone=no, address=no">
<title>WebSocket在線聊天室</title>
<link rel="stylesheet" th:href="@{/css/chatroom.css}" type="text/css"/>
</head>
<body>
<div>
<div class="window_frame">
<span><e style="font-weight: bold;">選擇你的網名:</e>
<select id="selectSender">
<option value="">請選擇..</option>
<option value="zhangsan">zhangsan</option>
<option value="lisi">lisi</option>
<option value="wangwu">wangwu</option>
<option value="zhaoliu">zhaoliu</option>
<option value="chenqi">chenqi</option>
<option value="qianba">qianba</option>
</select>
<e style="font-weight: bold;">羣聊:</e>
</span>
<div class="chatWindow">
<section class="chatRecord">
<div id="mass_div" class="mobile-page"></div>
</section>
<section class="sendWindow">
<textarea name="txtContent" id="txtContent" class="send_box"></textarea>
<input type="button" id="btnSend" value="發送" class="send_btn"/>
</section>
</div>
</div>
<div class="window_frame">
<span><e style="font-weight: bold;">選擇聊天的對象:</e>
<select id="selectRecevier">
<option value="">請選擇..</option>
<option value="zhangsan">zhangsan</option>
<option value="lisi">lisi</option>
<option value="wangwu">wangwu</option>
<option value="zhaoliu">zhaoliu</option>
<option value="chenqi">chenqi</option>
<option value="qianba">qianba</option>
</select>
<e style="font-weight: bold;">單聊:</e>
</span>
<div class="chatWindow">
<section class="chatRecord">
<div id="single_div" class="mobile-page"></div>
</section>
<section class="sendWindow">
<textarea name="txtContent2" id="txtContent2" class="send_box"></textarea>
<input type="button" id="btnSend2" value="發送" class="send_btn"/>
</section>
</div>
</div>
</div>
<script type="text/javascript" th:src="@{/js/jquery-1.9.1.min.js}"></script>
<script type="text/javascript" th:src="@{/js/wschatroom.js}"></script>
</body>
</html>
/**
* WS-WebSocket在線聊天室類
* 負責實現羣聊和單聊相關的聊天業務
*/
WsChatRoom = {
socket: null,
sys_msg_tag:'系統消息',
msg_split_str:'@#@',//消息分隔
isMass: true //是否羣發
};
/**
* 選擇發送者
*/
WsChatRoom.selectSender = function () {
let sender = $("#selectSender").val();
if("" === sender){
alert("請選擇你的聊天身份!");
return;
}
WsChatRoom.switchUser(sender);
};
/**
* 選擇接收者
*/
WsChatRoom.selectRecevier = function () {
let sender = $("#selectSender").val();
if("" === sender){
alert("請選擇你的聊天身份!");
return;
}
let recevier = $("#selectRecevier").val();
if("" === recevier){
alert("請選擇對方的聊天身份!");
return;
}
WsChatRoom.switchUser(sender, recevier);
};
/**
* 切換用戶
*/
WsChatRoom.switchUser = function (sender, recevier) {
//先關閉以前鏈接
WsChatRoom.close();
//鏈接服務器端
let url = "ws://localhost:8089/ws/server?sender="+sender;
if(recevier && null !== recevier && "" !== recevier){
url += ("&recevier="+recevier);
WsChatRoom.isMass = false;
}else{
WsChatRoom.isMass = true;
}
WsChatRoom.socket = new WebSocket(url);
//打開鏈接事件
WsChatRoom.socket.onopen = function (data) {
console.log("Socket鏈接已創建");
}
//接收消息事件
WsChatRoom.socket.onmessage = function (msg) {
let aData = msg.data.split(WsChatRoom.msg_split_str);
let sender = aData[0];
let content = aData[1];
let container = $("#mass_div");
let current = $("#selectSender").val();
if(!WsChatRoom.isMass){
container = $("#single_div");
}
//當前用戶發的消息 WsChatRoom.isMass &&
if(current === sender && WsChatRoom.sys_msg_tag !== sender){
container.append("<div class='user-group'>" +
" <div class='user-msg'>" +
" <span class='user-reply'>"+content+"</span>" +
" <i class='triangle-user'></i>" +
" </div><span style='padding-top:10px;'>" +sender+
" </span></div>");
}
else{
//系統消息
if(WsChatRoom.sys_msg_tag === sender){
$("#mass_div").append(" <div class='admin-group'><span class='msg_head'>"+
sender+
"</span><div class='admin-msg'>"+
" <i class='triangle-sys'></i>"+
" <span class='sys-reply'>"+content+"</span>"+
"</div>"+
"</div>");
}else{
container.append(" <div class='admin-group'><span class='msg_head'>"+
sender+
"</span><div class='admin-msg'>"+
" <i class='triangle-admin'></i>"+
" <span class='admin-reply'>"+content+"</span>"+
"</div>"+
"</div>");
}
}
}
//關閉鏈接事件
WsChatRoom.socket.onclose = function (data) {
console.log("Socket鏈接已關閉");
}
//鏈接異常事件
WsChatRoom.socket.onerror = function (e) {
console.log("Socket鏈接出錯:"+e);
}
};
/**
* 發送消息
*/
WsChatRoom.send = function () {
let sender = $("#selectSender").val();
if("" === sender){
alert("請選擇你的聊天身份!");
return;
}
let content = "";
if(WsChatRoom.isMass){
content = $("#txtContent").val().trim();
}else{
content = $("#txtContent2").val().trim();
}
if("" === content){
alert("發送的消息不能爲空!");
return;
}
if(!WsChatRoom.isMass && "" === $("#selectRecevier").val()){
alert("請選擇對方的聊天身份!");
return;
}
//發送消息
WsChatRoom.socket.send(content);
if(WsChatRoom.isMass){
$("#txtContent").val("");
}else{
$("#txtContent2").val("");
}
};
/**
* 關閉鏈接
*/
WsChatRoom.close = function(){
if(null != WsChatRoom.socket){
WsChatRoom.socket.close();
console.log("鏈接已關閉");
}
}
/**
* 窗口關閉時,關閉鏈接
*/
window.onload = function(){
WsChatRoom.close();
}
/**
* 頁面加載完畢事件
*/
$(function () {
//註冊事件
$("#selectSender").change(function () {
WsChatRoom.selectSender();
});
$("#selectRecevier").change(function () {
WsChatRoom.selectRecevier();
});
$("#btnSend").click(function () {
//發送時爲單聊,這裏須要切換
if(!WsChatRoom.isMass){
WsChatRoom.selectSender();
}
WsChatRoom.send();
});
$("#btnSend2").click(function () {
//發送時爲羣聊,這裏須要切換
if(WsChatRoom.isMass){
WsChatRoom.selectRecevier();
}
WsChatRoom.send();
});
});
結果驗證
1、羣聊效果
2、單聊效果
總結
從實現角度原生HTML5的WebSocket在客戶端比STOMP協議的實現方式要簡潔清晰一些,不要額外依賴第三放的組件或插件;而服務器端的實現比STOMP協議實現上略爲複雜一點(須要對客戶端Session進行管理)。從功能維度講,原生WebSocket不僅支持文本數據傳輸,同時也支持對象和二進制流等傳輸方式,功能更強大;而STOMP只支持文本消息。從通訊效率上看,STOMP協議的實現服務器端延遲更少(實現更簡單高效)。這兩種方式,咱們可根據項目的具體業務場景選擇使用。後面咱們將看看Netty在實現這類通訊實時性要求較高場景的表現,請繼續關注!