您的位置:首页 > 理论基础 > 计算机网络

websocket网络编程实战 - 用原生SOCKET协议实现在线群聊聊天室和一对一单聊天室

2020-07-11 00:48 597 查看

前言

上篇文章我们用STOMP子协议实现了在线群聊和一对一聊天室等功能,本篇我们继续WebSocket这个话题,这次我们换个实现维度:用原生的WebSocket来实现,看看这两者在实现上的差别有多大。


实战WebSocket的要点

一、WebSocket重要属性

属性

备注

Socket.readyState

只读属性 readyState 表示连接状态,可以是以下值:

0 - 表示连接尚未建立。

1 - 表示连接已建立,可以进行通信。

2 - 表示连接正在进行关闭。

3 - 表示连接已经关闭或者连接不能打开。

Socket.bufferedAmount

只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

二、WebSocket核心事件

事件

事件处理程序

备注

open

Socket.onopen

连接建立时触发

message

Socket.onmessage

客户端接收服务端数据时触发

error

Socket.onerror

通信发生错误时触发

close

Socket.onclose

连接关闭时触发

三、WebSocket核心方法

方法

备注

Socket.send()

使用连接发送数据

Socket.close()

连接关闭


代码设计实现

一、服务端部分

/**
* @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);
       }
   }
}

二、客户端部分

<!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();
   });
});

结果验证

一、群聊效果

二、单聊效果


总结

从实现角度原生HTML5的WebSocket在客户端比STOMP协议的实现方式要简洁清晰一些,不要额外依赖第三放的组件或插件;而服务器端的实现比STOMP协议实现上略为复杂一点(需要对客户端Session进行管理)。从功能维度讲,原生WebSocket不只支持文本数据传输,同时也支持对象和二进制流等传输方式,功能更强大;而STOMP只支持文本消息。从通信效率上看,STOMP协议的实现服务器端延迟更少(实现更简单高效)。这两种方式,我们可根据项目的具体业务场景选择使用。后面我们将看看Netty在实现这类通信实时性要求较高场景的表现,请继续关注!





内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐