websocket網絡編程實戰 - 用原生SOCKET協議實如今線羣聊聊天室和一對一單聊天室

前言
javascript

上篇文章咱們用STOMP子協議實現了在線羣聊和一對一聊天室等功能,本篇咱們繼續WebSocket這個話題,此次咱們換個實現維度:用原生的WebSocket來實現,看看這二者在實現上的差異有多大。css

image.png


實戰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、羣聊效果

image.png

image.png

2、單聊效果

image.png


總結

從實現角度原生HTML5的WebSocket在客戶端比STOMP協議的實現方式要簡潔清晰一些,不要額外依賴第三放的組件或插件;而服務器端的實現比STOMP協議實現上略爲複雜一點(須要對客戶端Session進行管理)。從功能維度講,原生WebSocket不僅支持文本數據傳輸,同時也支持對象和二進制流等傳輸方式,功能更強大;而STOMP只支持文本消息。從通訊效率上看,STOMP協議的實現服務器端延遲更少(實現更簡單高效)。這兩種方式,咱們可根據項目的具體業務場景選擇使用。後面咱們將看看Netty在實現這類通訊實時性要求較高場景的表現,請繼續關注!

相關文章
相關標籤/搜索