基于WebSocket的简易聊天室的基本实现梳理

一,前言

目前在很多网站为了实现推送技术所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。

然而,这种通信模型有一个弊端: HTTP协议无法实现服务器主动向客户端发起消息。这种单向请求的特点注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数web 应用程序将通过频繁的异步AJAX请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP连接始终打开)。因此在这种情况下WebSocket应运而生。

二,WebSocket介绍

WebSocket协议是基于TCP的一种新的网络协议,是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。它实现了浏览器与服务器全双工(full-duplex)通信--允许服务器主动发送信息给客户端,使得客户端和服务器之间的数据交换变得更加简单。弥补了Http协议在持久通信能力上的不足。

WebSocket通信协议于2011年被IETF定为标准RFC6455,并被RFC7936所补充规范。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

当你获取 Web Socket 连接后,你可以通过 send() 方法来向服务器发送数据,并通过onmessage 事件来接收服务器返回的数据。

1.1实现原理

在实现WebSocket连线过程中,需要通过浏览器发出WebSocket连线请求,然后服务器发出回应,这个过程通常称为"握手" 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:

  1. Header:互相沟通的Header是很小的-大概只有 2 Bytes

  2. Server Push:服务器的推送,服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。

1.2 WebSocket协议

该协议有两部分:握手和数据传输。握手是基于http协议的。

一个典型的WebSocket握手请求如下:

客户端请求

GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

来自服务器的握手看起来像如下形式:

HTTP/1.1 101 switching Protocols
Upgrade: websocket
Connection: upgrade
Sec-websocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+XOo=
Sec-websocket-Extensions: permessage-def1ate

字段说明:

头名称 说明
Connection 必须设置 Upgrade,表示客户端希望连接升级。
Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。
sec-websocket-version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。
sec-websocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。
sec-websocket-Extensions 协议扩展类型
Origin 字段是可选的,通常用来表示在浏览器中发起此 Websocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。

WebSocket 连接的过程是:

首先,客户端发起http请求,经过3次握手后,建立起TCP连接;http 请求里存放 WebSocket 支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;

然后,服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;

最后,客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。

三,WebSocket在B/S端的实现基础

2.1客户端(浏览器)实现

2.1.1 WebSocket对象

实现webSockets的 web浏览器将通过webSocket对象公开所有必需的客户端功能(主要指支持 Html5的浏览器)。以下API用于创建websocket对象:

  var ws = new websocket(ur1);

参数url格式说明: ws : / /ip地址:端口号/资源名称

2.1.2 WebSocket事件

事件 事件处理程序 描述
open websocket对象.onopen 连接建立时触发
message websocket对象.onmessage 客户端接收服务端数据时触发
error websocket对象.onerror 通信发生错误时触发
close websocket对象.onclose 连接关闭时触发

2.1.3WebSocket方法

WebSocket对象的相关方法:

方法 描述
send() 使用连接发送数据,WebSocket.send() 方法将需要通过 WebSocket 链接传输至服务器的数据排入队列,并根据所需要传输的 data bytes 的大小来增加bufferedAmount的值。若数据无法传输(例如数据需要缓存而缓冲区已满)时,套接字会自行关闭。
close() WebSocket.close() 方法关闭 WebSocket 连接或连接尝试(如果有的话)。如果连接已经关闭,则此方法不执行任何操作。

2.1.4客户端实例

WebSocket 协议本质上是一个基于 TCP 的协议。

为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。基于js和html的基础实现如下:

<!DOCTYPE HTML>
<html>
   <head>
   <meta charset="utf-8">
   <title>测试</title>
    
      <script type="text/javascript">
         function WebSocketTest()
         {
            if ("WebSocket" in window)
            {
               alert("您的浏览器支持 WebSocket!");
               
               // 打开一个 web socket
               var ws = new WebSocket("ws://localhost:8099/chat");
                
               ws.onopen = function()
               {
                  // Web Socket 已连接上,使用 send() 方法发送数据
                  ws.send("发送数据");
                  alert("数据发送中...");
               };
                
               ws.onmessage = function (evt) 
               { 
                  var received_msg = evt.data;
                  alert("数据已接收...");
               };
                
               ws.onclose = function()
               { 
                  // 关闭 websocket
                  alert("连接已关闭..."); 
               };
            }
            
            else
            {
               // 浏览器不支持 WebSocket
               alert("您的浏览器不支持 WebSocket!");
            }
         }
      </script>
        
   </head>
   <body>
   
      <div id="sse">
         <a href="javascript:WebSocketTest()">运行 WebSocket</a>
      </div>
   </body>
</html>

2.2 服务端实现

Tomcat的7.0.5 版本开始支持webSocket,并且实现了Java WebSocket规范(JSR356)。

Java websocket应用由一系列的websocketEndpoint组成。Endpoint是一个java对象代表websocket链接的一端,对于服务端,我们可以视为处理具体websocket消息的接口,就像Servlet之与http请求一样。

我们可以通过两种方式定义Endpoint:

  • 第一种是编程式,即继承类javax.websocket.Endpoint并实现其方法。

  • 第二种是注解式,即定义一个服务组件,并添@serverEndpoint相关注解。

Endpoint实例在webSocket握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在Endpoint接口中明确定义了与其生命周期相关的方法,规范实现者确保生命周期的各个阶段调用实例的相关方法。生命周期方法如下:

方法 含义描述 注解
onClose 当会话关闭时调用。 onclose
onOpen 当开启一个新的会话时调用,该方法是客户端与服务端握手成功后调用的方法。 onopen
onError 当连接过程中异常时调用。 onError

服务端如何接收客户端发送的数据呢?

通过为 session添加MessageHandler消息处理器来接收消息,当采用注解方式定义Endpoint时,我们还可以通过@onMessage 注解指定接收消息的方法。

服务端如何推送数据给客户端呢?

发送消息则由RemoteEndpoint 完成,其实例由session维护,根据使用情况,我们可以通过session.getBasicRemote获取同步消息发送的实例,然后调用其sendXxx ()方法就可以发送消息,可以通过session. getAsyncRemote 获取异步消息发送实例。

服务端代码示例:

//该注解用来指定一个URI,客户端可以通过这个URI来连接到WebSocket。类似Servlet的注解mapping。无需在web.xml中配置。
  @ServerEndpoint("/webSocket/{id}")
  @Component("webSocket")
public class WebSocket {
 
              private static Logger logger = Logger.getLogger(WebSocket.class);
      //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
              private static int onlineCount = 0;
      //与某个客户端的连接会话,需要通过它来给客户端发送数据
              private Session session;
      //concurrent包的线程安全Map,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
             private static ConcurrentMap<String, WebSocket> webSocketMap = new ConcurrentHashMap<>();
     private static ConcurrentMap<String, WebSocket> webSocketMapAdmin = new ConcurrentHashMap<>();
 
      public Session getSession() {
         return session;
     }
 
      public static WebSocket getWebSocket(String id) {
         return webSocketMap.get(id);
     }
 
      /**
       * 连接建立成功调用的方法
       *
       * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
       */
              @OnOpen
    public void onOpen(Session session, @PathParam("id") String id) {
          this.session = session;
          //String sessionId = session.getId();
          webSocketMap.put(id, this);     //加入map中
          if (id.contains("admin")) {// 后台登陆用户,加入list
                  webSocketMapAdmin.put(id, this);
              }
          addOnlineCount();           //在线数加1
          System.out.println("有新连接加入!当前在线人数为" + getOnlineCount());
      }
 
              /**
        * 连接关闭调用的方法
        */
              @OnClose
      public void onClose(@PathParam("id") String id) {
         webSocketMap.remove(id);  //从map中删除
         webSocketMapAdmin.remove(id);
         subOnlineCount();           //在线数减1
         System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
     }
 
      /**
       * 收到客户端消息后调用的方法
       *
       * @param message 客户端发送过来的消息
       * @param session 可选的参数
       */
             @OnMessage
      public static void onMessage(String message, Session session) {
                 //群发消息
                 if (webSocketMapAdmin.size() > 0) {
                for (WebSocket item : webSocketMapAdmin.values()) {
                        try {
                                //System.out.println(item.session.getId());
                                item.session.getBasicRemote().sendText(message);
                            } catch (IOException e) {
                                logger.error("IO异常");
                                continue;
                            }
                            }
                    }
        
             }
     /**
        * 发生错误时调用
        *
        * @param session
        * @param error
        */
             @OnError
      public void onError(Session session, Throwable error) {
                 //System.out.println("发生错误");
                 logger.error("发生错误");
             }
}

四,WebSocket在聊天室功能中的实现

3.1服务端代码

这里以前后端分离的SpringBoot项目集成聊天功能为例,首先需要在SpringBoot的项目中导入WebSocket依赖。

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

然后需要创建ServerEndpointExporter并注入Spring容器,如果想在使用内嵌容器的Spring Boot应用中使用@ServerEndpoint,我们需要声明一个单独的ServerEndpointExporter的Bean

@Bean
public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter();
}

该bean将使用底层的WebSocket容器注册任何被@ServerEndpoint注解的beans。

创建一个WebSocket服务,并通过@ServerEndpoint指明该类作为服务端点,直接构建请求接口即可,并在其中完成消息处理的业务。

package com.yy.util;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author young
 * Date 2023/6/7 12:34
 * Description: WebSocket服务
 */
@Component
@ServerEndpoint("/chat/{nickname}")
public class WebSocketServer {

    Logger log = LoggerFactory.getLogger(WebSocketServer.class);
    protected static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>(10);
    
    @OnOpen
    public void onOpen(Session session, @PathParam("nickname") String username) {
        SESSION_MAP.put(username, session);
        log.info("有新用户加入---》{},当前在线人数为:{}", username, SESSION_MAP.size());
        JSONObject result = new JSONObject();
        JSONArray array = new JSONArray();
        result.set("users", array);
        SESSION_MAP.keySet().forEach(s -> {
            JSONObject object = new JSONObject();
            object.set("nickname", s);
            array.add(object);
        });
        log.info("存入的对象集合信息{}", Arrays.toString(result.values().toArray()));
        // 后台发送消息给所有的客户端
        sendAllMessage(JSONUtil.toJsonStr(result));  
    }

    /**     
     * 收到客户端消息后调用的方法   
     * 后台收到客户端发送过来的消息    
     * onMessage 是一个消息的中转站  
     * 接受 浏览器端 socket.send 发送过来的 json数据  
     * @param message 客户端发送过来的消息   
     */
    @OnMessage
    public void onMessage(String message, Session session, @PathParam("nickname") String username) {
        log.info("服务端收到用户{}的消息{}", username, message);
        JSONObject obj = JSONUtil.parseObj(message);
        // to表示发送给哪个用户
        String toUser = obj.getStr("to");
        // 发送的消息文本  hello  
        String text = obj.getStr("text");
        // 根据 to用户名来获取 session,再通过session发送消息文本
        Session toSession = SESSION_MAP.get(toUser);
        if (ObjectUtil.isNotEmpty(toSession)) {
            JSONObject object = new JSONObject();
            object.set("from", username);
            object.set("text", text);
            this.sendMsg(object.toString(), toSession);
            log.info("发送给{}的消息---》{}", toUser, object);
        } else {
            log.info("消息发送失败,未找到用户{}", toUser);
        }
    }
    @OnClose
    public void onClose(Session session, @PathParam("username") String username) {
        SESSION_MAP.remove(username);
        log.info("有一个链接关闭,移除{}的用户session,当前在线人数为:{}", username, SESSION_MAP.size());
    }
    @OnError
    public void onError(Session session, Throwable throwable) {
        log.info("消息发生错误");
        throwable.printStackTrace();
    }

    private void sendMsg(String message, Session session) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (Exception e) {
            log.error("服务端发送消息失败", e);
        }
    }

    private void sendAllMessage(String message) {
        SESSION_MAP.values().forEach(s -> {
            try {
                s.getBasicRemote().sendText(message);
            } catch (IOException e) {
                log.error("服务端发送消息失败", e);
            }
        });
    }
}

此时一个简易的服务端就完成了。客户端发送消息时绑定该接口就能完成消息交互了。

3.2客户端代码

客户端这块主要在前端处理当前用户与远程用户聊天时,通过创建WebSocekt实例与服务端交互实现消息的组装传递与转发,从而达到用户相互聊天的功能。这里页面主要通过Vue和ElementPlus完成。大致前端代码如下:

<template>
  <div class="ml20" style="padding: 10px; margin-bottom: 50px;">
    <el-row>
      <el-col :span="4">
        <el-card style="width: 300px; height: 300px; color: #333">
          <div style="padding-bottom: 10px; border-bottom: 1px solid #ccc">
            在线用户<span style="font-size: 12px">(点击聊天气泡开始聊天)</span>
          </div>
          <div style="padding: 10px 0" v-for="user in users" :key="user.username">
            <span style="padding-bottom: 10px; border-bottom: 1px solid #48a6f3">{
   
   { user.nickname }}</span>
            <el-icon color="#409EFC" class="no-inherit" style="margin-left: 10px;text-align: center; font-size: 16px; cursor: pointer"
                     @click="chatUser = user.nickname">
              <ChatLineRound />
            </el-icon>
            <span style="font-size: 12px;color: limegreen; margin-left: 5px" v-if="user.nickname === chatUser">聊天中...</span>
          </div>
        </el-card>
      </el-col>
      <el-col :span="20">
        <div
            style="width: 800px; margin: 0 auto; background-color: white;                    border-radius: 5px; box-shadow: 0 0 10px #ccc">
          <div style="text-align: center; line-height: 50px;">
            Web聊天室({
   
   { chatUser }})
          </div>
          <div style="height: 300px; 
          overflow:auto;
          border-top: 1px solid #ccc" 
          v-html="content">
          </div>
          <div style="height: 200px">
            <textarea v-model="text"
                      placeholder="在此输入信息……"
                      style="height: 120px;
                      width: -webkit-fill-available; 
                      padding: 20px; 
                      border: none; 
                      background-color: #f7f7fa;
                      border-top: 1px solid #ccc;   
                      border-bottom: 1px solid #ccc;
                      outline: none"
                      @keyup.enter="send"
                       >
            </textarea>
            <div style="text-align: right; padding-right: 10px">
              <el-button type="primary" size="small" @click="send">发送</el-button>
            </div>
          </div>
        </div>
      </el-col>
    </el-row>
    <div class="fixed3">
      <a href="#"><img src="../../assets/kefu.png" style="border:5px solid #0f99e9;border-radius: 20%;" alt=""/></a>
    </div>
  </div>
</template>

<script>
import {mixin} from "../../mixins/index";
import { ChatLineRound} from "@element-plus/icons-vue";
let socket;
export default {
  name: "ChatHome",
  mixins:[mixin],
  components:{
    ChatLineRound
  },
  data() {
    return {
      circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png',
      user: localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {},
      isCollapse: false,
      users: [],
      chatUser: '',
      text: "",
      messages: [],
      content: ''
    }
  },
  created() {
    this.init()
  },
  methods: {
    send() {
      if (!this.chatUser) {
        this.$message({type: 'warning', message: "请选择聊天对象"})
        return;
      }
      if (!this.text) {
        this.$message({type: 'warning', message: "请输入内容"})
      } else {
        if (typeof (WebSocket) == "undefined") {
          console.log("您的浏览器不支持WebSocket");
        } else {
          console.log("您的浏览器支持WebSocket");
          // 组装待发送的消息 json          
          // {"from": "张三", "to": "李四", "text": "聊天文本"}          
          let message = {from: this.user.nickname, to: this.chatUser, text: this.text}
          socket.send(JSON.stringify(message));
          // 将组装好的json发送给服务端,由服务端进行转发          
          this.messages.push({user: this.user.nickname, text: this.text})
          // 构建消息内容,本人消息       
          this.createContent(null, this.user.nickname, this.text)
          this.text = '';
        }
      }
    },
    createContent(remoteUser, nowUser, text) {
      // 这个方法是用来将 json的聊天消息数据转换成 html的。   
      let html
// 当前用户消息     
      if (nowUser) {
        // nowUser 表示是否显示当前用户发送的聊天消息,绿色气泡   
        html = "<div class=\"el-row\" style=\"padding: 5px 0;\">\n" +
            "  <div class=\"el-col el-col-22\" style=\"text-align: right;margin-top: auto;margin-bottom: auto; padding-right: 10px\">\n" 
            + "    <div class=\"tip left\" style=\" width: auto;\n" +
            "  height: 30px;\n" +
            "  background: #48a6f3;\n" +
            "  padding: 5px 20px;\n" +
            "  margin: 4px;\n" +
            "  line-height: 30px;\n" +
            "  font-size: 14px;\n" +
            "  border-radius: 10px;\n" +
            "  margin-left: 10px;\n" +
            "  position: relative;\n" +
            "  float: right;\">"
            + text 
            + "</div>\n" 
            + "  </div>\n" 
            + "  <div class=\"el-col el-col-2\" style=\"text-align: left;margin-top: auto;padding-left: 10px;\">\n" 
            + "  <span class=\"el-avatar el-avatar--circle\" style=\"height: 40px; width: 40px; line-height: 40px;\">\n" 
            + "    <img :src=\"http://img.mp.itc.cn/upload/20161123/2fb03a6584f24901acc5e02d19ece787_th.jpeg\" style=\"object-fit: cover;\">\n" 
            + "  </span>\n" + "  </div>\n" + "</div>";
      } else if (remoteUser) {
        // remoteUser表示远程用户聊天消息,蓝色的气泡 
        html = "<div class=\"el-row\" style=\"padding: 5px 0;\">\n" 
            + "  <div class=\"el-col el-col-2\" style=\"text-align: right;margin-top: auto;\">\n" 
            + "  <span class=\"el-avatar el-avatar--circle\" style=\"height: 40px; width: 40px; line-height: 40px;\">\n" 
            + "    <img src=\"http://img.mp.itc.cn/upload/20161123/2fb03a6584f24901acc5e02d19ece787_th.jpeg\" style=\"object-fit: cover;\">\n" 
            + "  </span>\n"
            + "  </div>\n" 
            + "  <div class=\"el-col el-col-22\" style=\"text-align: left; padding-left: 10px;margin-top: auto;margin-bottom: auto;\">\n"
            + "    <div class=\"tip right\" style=\"width: auto;\n" +
            "  height: 30px;\n" +
            "  background: #eeeeee;\n" +
            "  padding: 5px 20px;\n" +
            "  margin: 4px;\n" +
            "  line-height: 30px;\n" +
            "  font-size: 14px;\n" +
            "  border-radius: 10px;\n" +
            "  margin-left: 10px;\n" +
            "  position: relative;\n" +
            "  float: left;\">"
            + text 
            + "</div>\n" 
            + "  </div>\n"
            + "</div>";
      }
      console.log(html)
      this.content += html;
    },
    init() {
      let nickname = this.user.nickname;
      let _this = this;
      if (typeof (WebSocket) == "undefined") {
        console.log("您的浏览器不支持WebSocket");
      } else {
        console.log("您的浏览器支持WebSocket");
        let socketUrl = "ws://localhost:8084/chat/" + nickname;
        if (socket != null) {
          socket.close();
          socket = null;
        }
        // 开启一个websocket服务   
        socket = new WebSocket(socketUrl);
        //打开事件      
        socket.onopen = function () {
          console.log("websocket已打开");
        };
        //  浏览器端收消息,获得从服务端发送过来的文本消息 
        socket.onmessage = function (message) {
          console.log("收到数据====" + message.data)
          let chatData = JSON.parse(message.data)
         // 对收到的json数据进行解析, 类似这样的: {"users": [{"username": "张三"},{ "username": "李四"}]}  
          if (chatData.users) {
            // 获取在线人员信息,并且排除自身,自己不会出现在自己的聊天列表里
            _this.users = chatData.users.filter(user => user.nickname !== nickname)
          } else {
            // 如果服务器端发送过来的json数据 不包含 users 这个key,那么发送过来的就是聊天文本json数据            
            // {"from": "张三", "text": "hello"}     
            if (chatData.from === _this.chatUser) {
              _this.messages.push(chatData)
            // 构建消息内容          
              _this.createContent(chatData.from, null, chatData.text)
            }
          }
        };
        //关闭事件    
        socket.onclose = function () {
          console.log("websocket已关闭");
        };
        //发生了错误事件    
        socket.onerror = function () {
          console.log("websocket发生了错误");
        }
      }
    }
  }
}
</script>

<style scoped>
.tip {
  color: white;
  text-align: center;
  border-radius: 10px;
  font-family: sans-serif;
  padding: 10px;
  width: auto;
  display: inline-block !important;
  display: inline;
}
.fixed3{
  position: absolute;
  right: 14px;
  top: 400px;
}
.right {
  background-color: deepskyblue;
}

.left {
  background-color: forestgreen;
}
</style>

然后就可以进行简单测试了。

3.3聊天功能测试

打开两个不同的浏览器,并登录两个用户进入到聊天室界面。这里需要注意的是:因为笔者这里用户使用的是相同的WebSocket连接,因此在同一个浏览器中登录多个用户,可能会出现信息被覆盖的问题。

因为WebSocket是基于长连接的通信协议,而同一个浏览器内的WebSocket连接是共享的,多个用户在同一个浏览器中使用相同的WebSocket连接进行通信,导致消息会被混淆。所以需要在两个不同浏览器中才看得到实现效果。

如果你的用户采用了不同的连接方式,或者建立不同的独立的WebSocket连接,就没这回事了。

测试效果如下:A用户聊天界面:

 B用户聊天界面如下:

 至此一个简单聊天室就实现了,但是这里面的消息记录都是与原生html拼接的,因此头像,用户名并没有与用户真实信息连同展示,并且还存在许多不足之处:

首先,因为用的都是一个WebSocket连接,因此用户只有在不同浏览器中登录才能实现该功能,否则就会出现信息覆盖。

其次,当用户数>2时,聊天区消息记录并没有及时清除,也就是说,该模式更像一个群聊。一对一与一对多的聊天功能实现可参考如下方式:

实现一对一聊天功能:

  • 当一个客户端建立连接时,服务器为每个客户端分配一个唯一的标识符。

  • 当客户端发送消息时,服务器将消息标识符和消息内容保存下来,并通过WebSocket服务器将消息发送给特定的目标客户端。

实现一对多聊天功能:

  • 当一个客户端建立连接时,服务器为每个客户端分配一个唯一的标识符。

  • 当客户端发送消息时,服务器将消息标识符和消息内容保存下来,并通过WebSocket服务器将消息发送给所有连接的客户端。

另外,聊天消息并未持久化,也就是说,当页面重新打开时消息记录就被清空了,因为WebSockset断连了,相应的记录也就没有了。如果需要消息记录保存的话,仍然需要数据库参与其中。

最后,消息记录采用html拼接形式展示出来的,因此无论是聊天用户头像还是昵称都并未与用户本身信息绑定,比较呆板。

留给大家一些思考~

五,总结

WebSocket 是为了在 web 应用上进行双通道通信而产生的协议,相比于轮询HTTP请求的方式,WebSocket 有节省服务器资源,效率高等优点。

WebSocket 中 Sec-WebSocket-Key 的生成算法是拼接服务端和客户端生成的字符串,进行SHA1哈希算法,再用base64编码。

WebSocket 协议握手是依靠 HTTP 协议的,依靠于 HTTP 响应101进行协议升级转换。

它的优缺点在于:

  • 优点:WebSocket协议一旦建议后,互相沟通所消耗的请求头是很小的服务器可以向客户端推送消息了

  • 缺点:少部分浏览器不支持,浏览器支持的程度与方式有区别(IE10)

应用场景如下:

  • 即时聊天通信

  • 多玩家游戏

  • 在线协同编辑/编辑

  • 实时数据流的拉取与推送

  • 体育/游戏实况

  • 实时地图位置

  • 即时Web应用程序:即时Web应用程序使用一个Web套接字在客户端显示数据,这些数据由后端服务器连续发送。在WebSocket中,数据被连续推送/传输到已经打开的同一连接中,这就是为什么WebSocket更快并提高了应用程序性能的原因。

  • 游戏应用程序:在游戏应用程序中,你可能会注意到,服务器会持续接收数据,而不会刷新用户界面。屏幕上的用户界面会自动刷新,而且不需要建立新的连接,因此在WebSocket游戏应用程序中非常有帮助。

  • 聊天应用程序:聊天应用程序仅使用WebSocket建立一次连接,便能在订阅户之间交换,发布和广播消息。它重复使用相同的WebSocket连接,用于发送和接收消息以及一对一的消息传输。

猜你喜欢

转载自blog.csdn.net/qq_42263280/article/details/131463604