(二十)ATP应用测试平台——websocket实现微服务版在线客服聊天室实战案例

2023-05-16

前言

在前面的博客内容中我们介绍了如何使用websocket实现一个网页版的在线客服聊天室,众所周知,由于websocket是一个长连接,要和服务端保持会话连接,所以其本身并不适用于微服务环境,在微服务环境中,有可能A、B俩个客户端连接到不同的服务A、B中,这样就没法保证A、B俩个客户端完成聊天的功能,因为会话不在同一台服务器上,A、B无法感知到对方发送的消息,为了解决websocket单机的这个痛点,我们引入消息中间键RocketMQ的广播机制,实现消息的转发,从而实现微服务版的websocke聊天室功能。其架构如下:

本节内容使用的主要技术包含springboot、redis、rocketmq、vue等,关于中间键的搭建本节内容不在展开,请关注作者的往期博客内容。 

正文

  • 引入websocket、redis和rocketmq的pom依赖

①核心pom依赖

 <!--           rocketmq-->
<dependency>
	<groupId>org.apache.rocketmq</groupId>
	<artifactId>rocketmq-spring-boot-starter</artifactId>
	<version>2.2.2</version>
</dependency>
<!--           websocket-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--           redis-->
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
	<version>2.4.3</version>
</dependency>

 PS:可以按需引入自己需要的依赖,作者这里只列出核心的pom依赖

  • 配置application.yml

①配置文件

server:
  port: 8888
spring:
  #数据源配置
  datasource:
    dynamic:
      primary: master #设置默认的数据源或者数据源组,默认值即为master
      strict: false #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候会抛出异常,不启动则使用默认数据源.
      datasource:
        master:
          url: jdbc:mysql://192.168.56.10:3306/atp
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver # 3.2.0开始支持SPI可省略此配置
  profiles:
    active: dev
  servlet:
    multipart:
      max-file-size: 52428800
      max-request-size: 52428800

  #redis配置
  redis:
    #redisson配置
    redisson:
      file: classpath:redisson.yaml
    #默认数据分区
    database: 0
    #redis集群节点配置
    cluster:
      nodes:
        - 192.168.56.10:6379
        - 192.168.56.10:6380
        - 192.168.56.10.6381
      max-redirects: 3
    #超时时间
    timeout: 10000
    #哨兵节点配置
    sentinel:
      master: mymaster
      nodes:
        - "192.168.56.10:26379"
        - "192.168.56.10:26380"
        - "192.168.56.10:26381"
    #redis密码
    password: root
    #redis 客户端工具
    lettuce:
      pool:
        # 连接池最大连接数(使用负值表示没有限制) 默认为8
        max-active: 8
        # 连接池中的最小空闲连接 默认为 0
        min-idle: 1
        # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1
        max-wait: 1000
        # 连接池中的最大空闲连接 默认为8
        max-idle: 8
  session:
    store-type: redis
    redis:
      flush-mode: on_save
      namespace: spring:session:atp
  thymeleaf:
    cache: false

#mybatisplus配置
mybatis-plus:
  mapper-locations: classpath*:/mapper/*/*Mapper.xml
  type-aliases-package: com.yundi.atp.platform.module.*.entity
  configuration:
    map-underscore-to-camel-case: true
  global-config:
    db-config:
      id-type: assign_id

#rocketmq配置
rocketmq:
  #注册地址
  name-server: 192.168.56.10:9876;192.168.56.10:9877
  producer:
    #生产者组名称
    group: atp-producer
    #命名空间
    namespace: atp
    #异步消息发送失败重试次数,默认是2
    retry-times-when-send-async-failed: 2
    #发送消息超时时间,默认2000ms
    send-message-timeout: 2000
    #消息的最大长度:默认1024 * 1024 * 4(默认4M)
    max-message-size: 40000000
    #压缩消息阈值,超过4k就压缩
    compress-message-body-threshold: 4096
    #是否发送失败,重试另外的broker
    retry-next-server: false
    #是否启用消息追踪
    enable-msg-trace: false
    #默认追踪的主题
    customized-trace-topic: RMQ_SYS_TRACE_TOPIC
    #消息发送失败重试的次数
    retry-times-when-send-failed: 2

  • 创建websocket服务配置WebSocketConfig.java

package com.yundi.atp.platform.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

@Configuration
public class WebSocketConfig {
    /**
     * 注入ServerEndpointExporter,
     * 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
     */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

  
  • 创建微服务版websocket服务

package com.yundi.atp.platform.websocket;

import com.alibaba.fastjson.JSON;
import com.yundi.atp.platform.common.Constant;
import com.yundi.atp.platform.enums.MessageType;
import com.yundi.atp.platform.module.test.entity.ChatMsg;
import com.yundi.atp.platform.module.test.service.ChatMsgService;
import com.yundi.atp.platform.rocketmq.RocketConstant;
import com.yundi.atp.platform.rocketmq.RocketProducer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;


@Slf4j
@Component
@ServerEndpoint(Constant.WEBSOCKET_MQ_URL + "{userName}")
public class WebSocketMqServer {
    /**
     * 会话session
     */
    private Session session;

    /**
     * socket连接
     */
    private static CopyOnWriteArraySet<WebSocketMqServer> webSockets = new CopyOnWriteArraySet<>();

    /**
     * 会话连接池
     */
    private static Map<String, Session> sessionPool = new ConcurrentHashMap<>();

    /**
     * 消息持久化
     */
    private static ChatMsgService chatMsgService;
    /**
     * redis
     */
    private static RedisTemplate redisTemplate;

    /**
     * RocketMQ消息工具类
     */
    private static RocketProducer rocketProducer;

    @Autowired
    public void setWebSocketServer(ChatMsgService chatMsgService,
                                   RedisTemplate redisTemplate,
                                   RocketProducer rocketProducer) {
        WebSocketMqServer.chatMsgService = chatMsgService;
        WebSocketMqServer.redisTemplate = redisTemplate;
        WebSocketMqServer.rocketProducer = rocketProducer;
    }

    @OnOpen
    public void onOpen(Session session, @PathParam(value = "userName") String userName) {
        //1.将用户添加到在线用户列表中
        if (!Constant.SUPER_ADMIN.equals(userName)) {
            redisTemplate.opsForSet().add("online", userName);
        }

        //2.保存会话连接
        this.session = session;
        webSockets.add(this);
        sessionPool.put(userName, session);
        Set online = redisTemplate.opsForSet().members("online");
        log.info("【websocket消息】有新的连接,总在线人数为:" + online.size());

        //3.创建消息
        WebSocketMqMsg webSocketMqMsg = new WebSocketMqMsg();
        //消息类型
        webSocketMqMsg.setKey(MessageType.MESSAGE_OPEN.getCode());
        //在线人数
        webSocketMqMsg.setOnlineList(online);
        //全部人数
        webSocketMqMsg.setUserList(chatMsgService.getUserList());

        //4.消息异步发送到RocketMQ
        rocketProducer.sendAsyncMsg(RocketConstant.ROCKET_TOPIC, RocketConstant.ROCKET_TAG_CHAT, UUID.randomUUID().toString(), JSON.toJSONString(webSocketMqMsg));
    }

    @OnClose
    public void onClose(@PathParam(value = "userName") String userName) {
        //1.更新在线用户列表
        redisTemplate.opsForSet().remove("online", userName);

        //2.清除会话连接
        webSockets.remove(this);
        sessionPool.remove(userName);
        Set online = redisTemplate.opsForSet().members("online");
        log.info("【websocket消息】连接断开,总在线人数为:" + online.size());

        //3.创建消息
        WebSocketMqMsg webSocketMqMsg = new WebSocketMqMsg();
        webSocketMqMsg.setKey(MessageType.MESSAGE_CLOSE.getCode());
        webSocketMqMsg.setOnlineList(online);
        webSocketMqMsg.setUserList(chatMsgService.getUserList());

        //4.消息异步发送到RocketMQ
        rocketProducer.sendAsyncMsg(RocketConstant.ROCKET_TOPIC, RocketConstant.ROCKET_TAG_CHAT, UUID.randomUUID().toString(), JSON.toJSONString(webSocketMqMsg));
    }

    @OnMessage
    public void onMessage(String message) {
        //1.持久化消息内容
        ChatMsg chatMsg = JSON.parseObject(message, ChatMsg.class);
        chatMsgService.save(chatMsg);

        //2.创建消息
        WebSocketMqMsg webSocketMqMsg = new WebSocketMqMsg();
        webSocketMqMsg.setKey(MessageType.MESSAGE_SEND.getCode());
        webSocketMqMsg.setData(chatMsg);

        //3.消息异步发送到RocketMQ
        rocketProducer.sendAsyncMsg(RocketConstant.ROCKET_TOPIC, RocketConstant.ROCKET_TAG_CHAT, UUID.randomUUID().toString(), JSON.toJSONString(webSocketMqMsg));
    }

    /**
     * 广播消息
     */
    public void sendAllMessage(String message) {
        for (WebSocketMqServer webSocket : webSockets) {
            log.info("【websocket消息】广播消息:" + message);
            try {
                Session session = webSocket.session;
                if (session != null && session.isOpen()) {
                    webSocket.session.getAsyncRemote().sendText(message);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 单点消息
     *
     * @param userName
     * @param message
     */
    public void sendOneMessage(String userName, String message) {
        log.info("【websocket消息】单点消息:" + message);
        Session session = sessionPool.get(userName);
        if (session != null && session.isOpen()) {
            try {
                session.getAsyncRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


}

ps:这里我们将会话的消息先推送给消息中间键RocketMQ,然后将消息通过广播的形式分发给每一台服务器去消费,如何能消费成功 ,就将消息推送给对应的客户端

  • 常量定义

package com.yundi.atp.platform.common;


public class Constant {
    /**
     * zookeeper分布式锁根路径
     */
    public final static String LOCK_ROOT_PATH = "/zookeeper/lock/";

    /**
     * websocket协议
     */
    public final static String WEBSOCKET_PROTOCOL = "ws://";

    /**
     * 单机版聊天室
     */
    public final static String WEBSOCKET_SINGLE_URL = "/websocket/chat/";


    /**
     * 微服务版聊天室
     */
    public final static String WEBSOCKET_MQ_URL = "/websocket/mq/chat/";

    /**
     * 超级管理员
     */
    public final static String SUPER_ADMIN = "super_admin";
}

  • 自定义消息类型:根据不同消息内容处理不同的消息业务逻辑
package com.yundi.atp.platform.enums;


public enum MessageType {
    MESSAGE_OPEN(1, "开启连接"),
    MESSAGE_CLOSE(2, "断开连接"),
    MESSAGE_SEND(3, "发送消息"),
    MESSAGE_RE_OPEN(4, "异地登录下线通知");

    private Integer code;

    private String msg;

    MessageType(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

​​​​​​​

  • RocketMQ消息发送的工具类 

package com.yundi.atp.platform.rocketmq;

import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;


@Component
@Slf4j
public class RocketProducer {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 发送同步消息:消息响应后发送下一条消息
     *
     * @param topic 消息主题
     * @param tag   消息tag
     * @param key   业务号
     * @param data  消息内容
     */
    public void sendSyncMsg(String topic, String tag, String key, String data) {
        //消息
        Message message = MessageBuilder.withPayload(data).setHeader(RocketMQHeaders.KEYS, key).build();
        //主题
        String destination = topic + ":" + tag;
        SendResult sendResult = rocketMQTemplate.syncSend(destination, message);
        log.info("【RocketMQ】发送同步消息:{}", sendResult);
    }

    /**
     * 发送异步消息:异步回调通知消息发送的状况
     *
     * @param topic 消息主题
     * @param tag   消息tag
     * @param key   业务号
     * @param data  消息内容
     */
    public void sendAsyncMsg(String topic, String tag, String key, String data) {
        //消息
        Message message = MessageBuilder.withPayload(data).setHeader(RocketMQHeaders.KEYS, key).build();
        //主题
        String destination = topic + ":" + tag;
        rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("【RocketMQ】发送异步消息:{}", sendResult);
            }

            @Override
            public void onException(Throwable e) {
                log.info("【RocketMQ】发送异步消息异常:{}", e.getMessage());
            }
        });
    }


    /**
     * 发送单向消息:消息发送后无响应,可靠性差,效率高
     *
     * @param topic 消息主题
     * @param tag   消息tag
     * @param key   业务号
     * @param data  消息内容
     */
    public void sendOneWayMsg(String topic, String tag, String key, String data) {
        //消息
        Message message = MessageBuilder.withPayload(data).setHeader(RocketMQHeaders.KEYS, key).build();
        //主题
        String destination = topic + ":" + tag;
        rocketMQTemplate.sendOneWay(destination, message);
    }


    /**
     * 同步延迟消息
     *
     * @param topic      主题
     * @param tag        标签
     * @param key        业务号
     * @param data       消息体
     * @param timeout    发送消息的过期时间
     * @param delayLevel 延迟等级-----固定等级:1到18分别对应1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     */
    public void sendSyncDelayMsg(String topic, String tag, String key, String data, long timeout, int delayLevel) {
        //消息
        Message message = MessageBuilder.withPayload(data).setHeader(RocketMQHeaders.KEYS, key).build();
        //主题
        String destination = topic + ":" + tag;
        SendResult sendResult = rocketMQTemplate.syncSend(destination, message, timeout, delayLevel);
        log.info("【RocketMQ】发送同步延迟消息:{}", sendResult);
    }


    /**
     * 异步延迟消息
     *
     * @param topic      主题
     * @param tag        标签
     * @param key        业务号
     * @param data       消息体
     * @param timeout    发送消息的过期时间
     * @param delayLevel 延迟等级-----固定等级:1到18分别对应1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     */
    public void sendAsyncDelayMsg(String topic, String tag, String key, String data, long timeout, int delayLevel) {
        //消息
        Message message = MessageBuilder.withPayload(data).setHeader(RocketMQHeaders.KEYS, key).build();
        //主题
        String destination = topic + ":" + tag;
        rocketMQTemplate.asyncSend(destination, message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("【RocketMQ】发送异步延迟消息:{}", sendResult);
            }

            @Override
            public void onException(Throwable e) {
                log.info("【RocketMQ】发送异步延迟消息异常:{}", e.getMessage());
            }
        }, timeout, delayLevel);
    }


    /**
     * 同步顺序消息
     *
     * @param topic 主题
     * @param tag   标签
     * @param key   业务号
     * @param data  消息体
     */
    public void sendSyncOrderlyMsg(String topic, String tag, String key, String data) {
        //消息
        Message message = MessageBuilder.withPayload(data).setHeader(RocketMQHeaders.KEYS, key).build();
        //主题
        String destination = topic + ":" + tag;
        SendResult sendResult = rocketMQTemplate.syncSendOrderly(destination, message, key);
        log.info("【RocketMQ】发送同步顺序消息:{}", sendResult);
    }


    /**
     * 异步顺序消息
     *
     * @param topic 主题
     * @param tag   标签
     * @param key   业务号
     * @param data  消息体
     */
    public void sendAsyncOrderlyMsg(String topic, String tag, String key, String data) {
        //消息
        Message message = MessageBuilder.withPayload(data).setHeader(RocketMQHeaders.KEYS, key).build();
        //主题
        String destination = topic + ":" + tag;
        rocketMQTemplate.asyncSendOrderly(destination, message, key, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info("【RocketMQ】发送异步顺序消息:{}", sendResult);
            }

            @Override
            public void onException(Throwable e) {
                log.info("【RocketMQ】发送异步顺序消息异常:{}", e.getMessage());
            }
        });
    }


    /**
     * 分布式事务消息
     *
     * @param topic 主题
     * @param tag   标签
     * @param key   业务号
     * @param data  消息体
     */
    public void sendTransactionMsg(String topic, String tag, String key, String data) {
        //消息
        Message message = MessageBuilder.withPayload(data)
                .setHeader(RocketMQHeaders.KEYS, key)
                .setHeader(RocketMQHeaders.TRANSACTION_ID, key)
                .build();
        //主题
        String destination = topic + ":" + tag;
        TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction(destination, message, null);
        if (transactionSendResult.getLocalTransactionState().equals(LocalTransactionState.COMMIT_MESSAGE) &&
                transactionSendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
            log.info("分布式事物消息发送成功");

        }
        log.info("分布式事物消息发送结果:{}", transactionSendResult);
    }

}

  • websocket服务的连接地址获取及历史消息获取 
package com.yundi.atp.platform.module.test.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yundi.atp.platform.common.Result;
import com.yundi.atp.platform.module.test.entity.ChatMsg;
import com.yundi.atp.platform.module.test.service.ChatMsgService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

@Api(tags = "聊天室接口-mq版")
@RestController
@RequestMapping("/test/mq/chatMsg")
public class ChatMsgMqController {
    @Autowired
    private ChatMsgService chatMsgService;

    @ApiOperation(value = "获取聊天室地址")
    @GetMapping(value = "/getWebSocketAddress/{username}")
    public Result getWebSocketAddress(HttpServletRequest request, @PathVariable(value = "username") String username) throws UnknownHostException {
        String address = "ws://" + InetAddress.getLocalHost().getHostAddress() + ":" + request.getServerPort() + request.getContextPath() + "/websocket/mq/chat/" + username;
        return Result.success(address);
    }

    @ApiOperation(value = "获取历史聊天记录")
    @GetMapping(value = "/getHistoryChat/{username}")
    public Result getWebSocketAddress(@PathVariable(value = "username") String username) {
        List<ChatMsg> list = chatMsgService.list(new QueryWrapper<ChatMsg>()
                .and(wrapper -> wrapper.eq("sender", username).or().eq("receiver", username))
                .orderByDesc("create_time"));
        List<ChatMsg> collect = list.stream().sorted(Comparator.comparing(ChatMsg::getCreateTime)).collect(Collectors.toList());
        return Result.success(collect);
    }

    @ApiOperation(value = "获取用户列表")
    @GetMapping(value = "/getUserList")
    public Result getUserList() {
        List<String> userList = chatMsgService.getUserList();
        return Result.success(userList);
    }

}

​​​​​​​

  • 消息的广播分发
package com.yundi.atp.platform.websocket;

import com.alibaba.fastjson.JSON;
import com.yundi.atp.platform.common.Constant;
import com.yundi.atp.platform.enums.MessageType;
import com.yundi.atp.platform.module.test.entity.ChatMsg;
import com.yundi.atp.platform.rocketmq.RocketConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;


@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = RocketConstant.ROCKET_CONSUMER_CHAT_GROUP,
        topic = RocketConstant.ROCKET_TOPIC,
        selectorExpression = RocketConstant.ROCKET_TAG_CHAT,
        namespace = RocketConstant.ROCKET_NAMESPACE,
        messageModel = MessageModel.BROADCASTING)
public class WebSocketMqConsumer implements RocketMQListener<String> {
    @Autowired
    WebSocketMqServer webSocketMqServer;

    @Override
    public void onMessage(String message) {
        log.info("聊天室消息:{}", message);
        //1.解析消息
        WebSocketMqMsg webSocketMqMsg = JSON.parseObject(message, WebSocketMqMsg.class);

        //2.根据消息类型解析消息

        // 建立连接消息
        if (webSocketMqMsg.getKey().equals(MessageType.MESSAGE_OPEN.getCode())) {
            webSocketMqServer.sendOneMessage(Constant.SUPER_ADMIN, message);
        }

        // 关闭连接消息
        if (webSocketMqMsg.getKey().equals(MessageType.MESSAGE_CLOSE.getCode())) {
            webSocketMqServer.sendOneMessage(Constant.SUPER_ADMIN, message);
        }

        // 发送消息
        if (webSocketMqMsg.getKey().equals(MessageType.MESSAGE_SEND.getCode())) {
            ChatMsg data = webSocketMqMsg.getData();
            webSocketMqServer.sendOneMessage(data.getSender(), message);
            webSocketMqServer.sendOneMessage(data.getReceiver(), message);
        }
    }
}

  • 消息主题定义
package com.yundi.atp.platform.rocketmq;


public class RocketConstant {
    /**
     * 消费者组
     */
    public final static String ROCKET_CONSUMER_GROUP = "atp-consumer";
    /**
     * 聊天室消费者组
     */
    public final static String ROCKET_CONSUMER_CHAT_GROUP = "atp-chat-consumer";
    /**
     * 主题
     */
    public final static String ROCKET_TOPIC = "atp";
    /**
     * tag
     */
    public final static String ROCKET_TAG = "app";
    /**
     * 聊天室tag
     */
    public final static String ROCKET_TAG_CHAT = "chat";
    /**
     * 名称空间
     */
    public final static String ROCKET_NAMESPACE = "atp";

}

  • 客户端代码
<template>
  <div class="container">
    <el-card class="box-card">
      <div slot="header">
        <el-row type="flex">
          <el-col :span="1" style="margin: 15px 10px;">
            <img alt="ATP客服" src="@/assets/logo.png" style="width:40px;height:40px;"/>
          </el-col>
          <el-col :span="3" style="line-height: 74px;margin-left: 10px;">
            <span style="display: inline-block;color: white;">ATP客服</span>
          </el-col>
          <el-col :span="20" v-if="username==='super_admin'">
            <h5 style="color: #83ccd2;padding: 0;text-align: right;margin: 50px 20px 0 0;">当前在线人数:{{ online }}</h5>
          </el-col>
          <el-col :span="20" v-else>
            <h5 style="color: #83ccd2;padding: 0 0 2px 0;text-align: right;margin: 50px 20px 0 0;font-size: 18px;">
              {{ username }}</h5>
          </el-col>
        </el-row>
      </div>
      <div class="content" ref="content">
        <el-row type="flex">
          <el-col :span="6" style="background: #eee;min-height: 600px;" v-if="username==='super_admin'">
            <el-tabs v-model="activeName" @tab-click="handleClick" style="width: 190px;margin: 0 2px;">
              <el-tab-pane label="在线用户" name="online">
                <div v-for="item in friend" :key="item" @click="switchUser(item)" :class="item===active?'mark':''">
                  <el-badge :is-dot=msgNotify.includes(item) class="item" type="success">
                    <li style="list-style-type:none;padding: 5px 8px;cursor: pointer;"
                        class="active">
                      {{ item }}
                    </li>
                  </el-badge>
                  <el-divider></el-divider>
                </div>
              </el-tab-pane>
              <el-tab-pane label="全部用户" name="all">
                <div v-for="item in userList" :key="item" @click="switchUser(item)" :class="item===active?'mark':''">
                  <el-badge :is-dot=msgNotify.includes(item) class="item" type="success">
                    <li style="list-style-type:none;padding: 5px 8px;cursor: pointer;"
                        :class="friend.includes(item)?'active':''">
                      {{ item }}
                    </li>
                  </el-badge>
                  <el-divider></el-divider>
                </div>
              </el-tab-pane>
            </el-tabs>
          </el-col>
          <el-col :span="18" v-if="username==='super_admin'">
            <div v-for="item in chatMsgList">
              <el-row type="flex" style="margin-bottom: 20px;" v-if="username===item.sender">
                <el-col :span="2">
                  <img alt="ATP客服" src="@/assets/logo.png" style="width:30px;height:30px;margin: 10px 0px 0px 20px;"/>
                </el-col>
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;margin-left: 5px;opacity: 0.2;">
                    <el-col :span="7"><span style="padding-left: 20px;">{{ item.sender }}</span></el-col>
                    <el-col :span="7"><span>{{ item.createTime | dataFormat('yyyy-MM-dd HH:mm') }}</span></el-col>
                  </el-row>
                  <el-row>
                    <el-col :span="14" style="margin-left: 8px;margin-top: 5px;">
                      <el-card style="padding: 8px 5px;">
                        {{ item.msg }}
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
              </el-row>
              <el-row type="flex" style="margin-bottom: 20px;" v-else justify="end">
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;margin-right: 5px;opacity: 0.2;" justify="end">
                    <el-col :span="6"><span>{{ item.sender }}</span></el-col>
                    <el-col :span="7"><span>{{ item.createTime | dataFormat('yyyy-MM-dd HH:mm') }}</span></el-col>
                  </el-row>
                  <el-row type="flex" justify="end" style="margin-right: 8px;margin-top: 5px;">
                    <el-col :span="14" style="margin-right: 8px;">
                      <el-card style="padding: 8px 5px;">
                        {{ item.msg }}
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
                <el-col :span="2">
                  <el-avatar shape="square" size="medium" style="float: right;margin: 10px 20px 0px 0px;">客户</el-avatar>
                </el-col>
              </el-row>
            </div>
          </el-col>
          <el-col :span="24" v-else>
            <div v-for="item in chatMsgList">
              <el-row type="flex" style="margin-bottom: 20px;" v-if="username===item.sender">
                <el-col :span="2">
                  <el-avatar shape="square" size="medium" style="float: right;margin: 10px 20px 0px 0px;">客户</el-avatar>
                </el-col>
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;opacity: 0.2;margin-left: 20px;">
                    <el-col :span="7"><span style="padding-left: 5px;">{{ item.sender }}</span></el-col>
                    <el-col :span="7"><span>{{ item.createTime | dataFormat('yyyy-MM-dd HH:mm') }}</span></el-col>
                  </el-row>
                  <el-row>
                    <el-col :span="14">
                      <el-card style="padding: 8px 5px;">
                        {{ item.msg }}
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
              </el-row>
              <el-row type="flex" style="margin-bottom: 20px;" v-else justify="end">
                <el-col :span="22">
                  <el-row type="flex" style="margin-top: 10px;margin-right: 5px;opacity: 0.2;" justify="end">
                    <el-col :span="6"><span>{{ item.sender }}</span></el-col>
                    <el-col :span="7"><span>{{ item.createTime | dataFormat('yyyy-MM-dd HH:mm') }}</span></el-col>
                  </el-row>
                  <el-row type="flex" justify="end" style="margin-top: 5px;">
                    <el-col :span="14">
                      <el-card style="padding: 8px 5px;">
                        {{ item.msg }}
                      </el-card>
                    </el-col>
                  </el-row>
                </el-col>
                <el-col :span="2">
                  <img alt="ATP客服" src="@/assets/logo.png" style="width:30px;height:30px;margin: 10px 0px 0px 20px;"/>
                </el-col>
              </el-row>
            </div>
          </el-col>
        </el-row>
      </div>
      <div class="operate" v-if="username==='super_admin'">
        <el-input
            type="textarea"
            :autosize="{ minRows: 3, maxRows: 3}"
            placeholder="您好!这里是ATP客服部,我是客服小美,很高兴为您服务!"
            v-model="msg">
        </el-input>
        <el-button type="success" size="mini" style="float: right;margin-top: 5px;" @click="sendMsg"
                   :disabled="!(msg && active)">
          发送
        </el-button>
      </div>
      <div class="operate" v-else>
        <el-input
            type="textarea"
            :autosize="{ minRows: 3, maxRows: 3}"
            placeholder="您好!这里是ATP客服部,我是客服小美,很高兴为您服务!"
            v-model="msg">
        </el-input>
        <el-button type="success" size="mini" style="float: right;margin-top: 5px;" @click="sendMsg" :disabled="!msg">
          发送
        </el-button>
      </div>
    </el-card>
  </div>
</template>

<script>
export default {
  name: "ClientMqChat",
  data() {
    return {
      msg: '',
      chatMsgList: [],
      username: sessionStorage.getItem("username"),
      friend: [],
      online: 0,
      active: '',
      receiver: 'super_admin',
      userList: [],
      activeName: 'online',
      msgNotify:[],
    }
  },
  created() {
    this.getWebSocketAddress();
  },
  methods: {
    //tab切换
    handleClick(tab, event) {
      const _this = this;
      if (tab.name === 'online') {
        if (!_this.active) {
          if (_this.online > 0) {
            _this.active = _this.friend[0];
            _this.activeName = 'online';
            _this.receiver = _this.active;
            _this.getHistoryChat(_this.receiver);
          } else {
            if (_this.userList.length > 0) {
              _this.active = _this.userList[0];
              _this.activeName = 'all';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
            }
          }
        }
      }
      if (tab.name === 'all') {
        if (!_this.active) {
          if (_this.online > 0) {
            _this.active = _this.friend[0];
            _this.activeName = 'online';
            _this.receiver = _this.active;
            _this.getHistoryChat(_this.receiver);
          } else {
            if (_this.userList.length > 0) {
              _this.active = _this.userList[0];
              _this.activeName = 'all';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
            }
          }
        }
      }
    },
    //切换用户
    switchUser(data) {
      if (this.active === data) {
        return;
      }
      this.active = data;
      this.receiver = data;
      //获取历史聊天记录
      this.getHistoryChat(this.receiver);
      this.msgNotify = this.msgNotify.filter(item => item != this.active);
    },
    //获取历史聊天记录
    getHistoryChat(data) {
      this.$http.get('/test/mq/chatMsg/getHistoryChat/' + data).then(res => {
        if (res.data.code === 1) {
          this.chatMsgList = res.data.data;
          this.flushScroll();
        } else {
          this.$message.warning(res.data.msg);
        }
      }).catch(error => {
        this.$message.error(error);
      });
    },
    //获取websocket地址
    getWebSocketAddress() {
      this.$http.get('/test/mq/chatMsg/getWebSocketAddress/' + this.username).then(res => {
        if (res.data.code === 1) {
          if ('WebSocket' in window) {
            this.websocket = new WebSocket(res.data.data);
            this.initWebSocket();
            if (this.username != 'super_admin') {
              this.getHistoryChat(this.username);
            }
          } else {
            this.$message.warning('当前浏览器不支持websocket创建!');
          }
        } else {
          this.$message.warning(res.data.msg);
        }
      }).catch(error => {
        this.$message.error(error);
      });
    },
    //初始化websocket
    initWebSocket() {
      const _this = this;
      _this.websocket.onerror = function (event) {
        _this.$message.error('服务端连接错误!');
      }
      _this.websocket.onopen = function (event) {
        _this.$message.success("连接成功!");
      }
      _this.websocket.onmessage = function (event) {
        let res = JSON.parse(event.data);
        if (res.key === 1) {
          _this.userList = res.userList;
          _this.friend = res.onlineList;
          _this.online = _this.friend.length;
          if (!_this.active) {
            if (_this.online > 0) {
              _this.active = _this.friend[0];
              _this.activeName = 'online';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
            } else {
              if (_this.userList.length > 0) {
                _this.active = _this.userList[0];
                _this.activeName = 'all';
                _this.receiver = _this.active;
                _this.getHistoryChat(_this.receiver);
              }
            }
          }
        }
        if (res.key === 2) {
          _this.userList = res.userList;
          _this.friend = res.onlineList;
          _this.online = _this.friend.length;
          if (!_this.active) {
            if (_this.online > 0) {
              _this.active = _this.friend[0];
              _this.activeName = 'online';
              _this.receiver = _this.active;
              _this.getHistoryChat(_this.receiver);
            } else {
              if (_this.userList.length > 0) {
                _this.active = _this.userList[0];
                _this.activeName = 'all';
                _this.receiver = _this.active;
                _this.getHistoryChat(_this.receiver);
              }
            }
          }
        }
        if (res.key === 3) {
          if (_this.username === res.data.sender) {
            _this.chatMsgList.push(res.data);
            _this.flushScroll();
          } else {
            if (res.data.sender === 'super_admin') {
              _this.chatMsgList.push(res.data);
              _this.flushScroll();
            } else {
              if (res.data.sender === _this.active) {
                _this.chatMsgList.push(res.data);
                _this.flushScroll();
              } else {
                //发送其它用户处理
                _this.msgNotify.push(res.data.sender);
              }
            }
          }
        }
      }
      _this.websocket.onclose = function (event) {
        _this.$message.warning('服务端已关闭!');
      }
    },
    //发送消息
    sendMsg() {
      if (this.msg.trim().length === 0) {
        this.$message.warning('不能发送空消息!');
        return;
      }
      let chatMsg = {};
      chatMsg.msg = this.msg;
      chatMsg.sender = this.username;
      chatMsg.createTime = new Date();
      chatMsg.receiver = this.receiver;
      this.websocket.send(JSON.stringify(chatMsg));
      this.msg = '';
      this.flushScroll();
    },
    //刷新滚动条
    flushScroll() {
      let content = this.$refs.content;
      setTimeout(() => {
        content.scrollTop = content.scrollHeight;
      }, 100);
    },
  }
}
</script>

<style scoped lang="scss">
.container {
  padding-top: 50px;

  .box-card {
    margin: auto;
    width: 800px;
    height: 800px;
    max-height: 900px;

    ::v-deep .el-card__header {
      background: #867ba9 !important;
      border-bottom: none;
      padding: 0;
    }

    ::v-deep .el-card__body {
      padding: 0px !important;
      position: relative;

      .content {
        height: 600px;
        background: #ddd;
        overflow-y: auto;

        .el-divider--horizontal {
          margin: 0;
        }

        .active {
          color: #0080ff;
        }

        .mark {
          background: #deb068;
        }

        .item {
          margin-top: 10px;
          margin-right: 10px;
        }
      }

      .operate {
        padding: 5px 15px;
      }
    }
  }
}
</style>

  • ​​​​​​​ 启动前后端项目,分别使用客服账号和客户账号登录聊天室

  • 聊天消息

 

 

结语

至此,关于实现微服务的websocket聊天室到这里就结束了,下期见。。。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

(二十)ATP应用测试平台——websocket实现微服务版在线客服聊天室实战案例 的相关文章

  • 「leetcode」C++题解:239. 滑动窗口最大值,单调队列的经典题目

    更多精彩文章持续更新 xff0c 微信搜索 代码随想录 第一时间围观 xff0c 本文https github com youngyangyang04 TechCPP 已经收录 xff0c 里面有更多干货等着你 xff0c 欢迎Star x
  • BAT程序员总结的力扣刷题指南,已经在Github了!!刷题顺序,优质题解一网打尽!

    相信很多小伙伴刷题的时候面对力扣上近两千道题目 xff0c 感觉无从下手 xff01 我花费半年时间整理了Github学习项目 力扣刷题攻略 xff1a https github com youngyangyang04 leetcode m
  • 扩展卡尔曼滤波EKF与多传感器融合

    Extended Kalman Filter xff08 扩展卡尔曼滤波 xff09 是卡尔曼滤波的非线性版本 在状态转移方程确定的情况下 xff0c EKF已经成为了非线性系统状态估计的事实标准 本文将简要介绍EKF xff0c 并介绍其
  • 我把年终总结写成了年度回忆录(1)

    写在前面 这大概是我第一次正儿八经的写个年终总结 xff0c 其实 xff0c 更像是一篇很有意思的回忆录 去年元旦的情景我已经记不起来了 但这一年里 xff0c 却是有很多事情让我难以忘记 从去年寒假自己在出租屋里苦学的时光 xff0c
  • .mat文件后缀名消失

    情况说明 xff1a 下载了 mat文件后 xff0c 打开文件发现文件的后缀名缺失了 xff0c 并且文件类型变为Microsoft Access Table Shortcut类型 具体原因 xff1a 这是由于MATLAB和Access
  • lsusb命令

    在 Linux 中我们可以使用 lsusb 来列出 USB 设备和它的属性 xff0c lsusb 会显示驱动和内部连接到你系统的设备 直接在控制台输入 lsusb 即可 如果无法运行 lsusb xff0c 使用以下命令安装 xff08
  • 现代控制理论基础——卡尔曼滤波(kalman filtering)

    现代控制理论基础 卡尔曼滤波 xff08 kalman filtering xff09 什么是卡尔曼滤波 xff1f 在任何含有不确定信息的动态系统中使用卡尔曼滤波 xff0c 对系统下一步的走向做出有根据的预测 xff0c 对系统状态进行
  • C/C++中的'\0'

    在C C 43 43 语言中 xff0c 字符是按其所对应的ASCII码来存储的 xff0c 一个字符占一个字节 xff0c 而 0 就是ASCII码表中的第一个字符 xff0c ASCII码为00000000 xff0c 它被称为空字符
  • OpenCV 创建图像时,CV_8UC1,CV_32FC3,CV_32S等参数的含义

    形式 xff1a CV lt bit depth gt S U F C lt number of channels gt bit depth xff1a 比特数 代表8bite 16bites 32bites 64bites 举个例子吧 比
  • 解决apt-get update更新错误

    sudo apt get update出现解析错误 xff0c 如下 fkuner 64 data3 span class token function sudo span span class token function apt get
  • C++初阶:vector类

    vector 0 vector的介绍 vector是用数组实现的 可变长度的顺序容器 xff0c 本质是一种类模板 span class token keyword template span span class token operat
  • Git之分支创建策略

    分支类型 git上始终保持两个分支 xff0c master分支 develop分支 master分支主要用于发布时使用 xff0c 而develop分支主要用于开发使用 除了以上两个常驻分支外 xff0c 我们还可以适当分支出三种分支 x
  • ubuntu 设置pip源

    前言 在Ubuntu下我们一般使用pip工具去管理我们的Python包 但是在使用pip命令操作的时候一般都是使用的默认设置 xff0c 使用的是国外的镜像 xff0c 这就导致了我们在国内下载安装包的时候很慢 xff08 乌龟慢慢爬 xf
  • 27.串口通信实验源码讲解

    串口通信实验源码讲解 笔记基于正点原子官方视频 视频连接https www bilibili com video BV1Wx411d7wT p 61 71 amp spm id from 61 333 1007 top right bar
  • 国内快速下载keil的pack文件包

    问题 xff1a 国内keil官网下载pack文件包太慢 xff0c 网上很多网盘资源如果没有VIP也是很慢 解决方案 xff1a https www keil com dd2 pack 第一步 xff1a 首先去上面的keil官网找自己需
  • forensics - make virtual machine with E01[ewf] files on OSX ———— 电子取证 MAC OS平台仿真

    forensics make virtual machine with E01 ewf files on OSX 电子取证 MAC OS平台仿真1挂载库安装osxfuselibewf 2 虚拟机存储文件qemu 3 开始实验 amp amp
  • 如何从官网下载 Google Chrome 离线安装包

    Google Chrome 已经是许多人的默认浏览器 xff0c 但由于 你懂的 原因 xff0c 在线安装基本没有成功过 xff0c 他自己的自动更新也多数一直在加载中 xff0c 所以我们会到一些下载站下载安装包 xff0c 但我的多次
  • 腾讯资深3D游戏建模师你不知道的5个3DMAX细节

    首先我们要清楚的是行业划分 3DMAX的用途非常广泛 xff0c 所涉及的行业大致有 xff0c 园林景观 城市规划 建筑设计 室内设计 动漫设计 商业动画制作等 所以我们在入手学3DMAX软件时 xff0c 大家应该分清楚 xff0c 你
  • 通过GetProcessNameByProcessId得到进程路径

    写主防时 xff0c 为了拿到进程路径 xff0c 所以查询发现一种发现一种方式是通过PID xff0c 调用PsLookupProcessByProcessId ProcessId amp ProcessObj 拿到进程的EPROCESS
  • 10.Python修炼之路【14-链表】2018.05.11

    关键字 xff1a 单链表 双链表 循环单链表 循环双链表 一 链表 1 为什么需要链表 顺序表的构建需要预先知道数据大小来申请连续的存储空间 xff0c 而在进行扩充时又需要进行数据的搬迁 xff0c 所以使用起来并不是很灵活 链表结构可

随机推荐

  • 谈谈Linux内核的实时性优化

    1 实时系统的概念 1 1什么是实时操作系统 什么是实时操作系统 xff1f 接触过嵌入式的小伙伴可能会知道 xff0c 实时操作系统是指在嵌入式领域广泛应用的各类RTOS Real Time Operating System 其中最具代表
  • docker 命令详解(十八):port

    一 命令作用 列出指定的容器的端口映射 xff0c 或者查找将 PRIVATE PORT NAT 到面向公众的端口 二 命令语法 docker port OPTIONS CONTAINER PRIVATE PORT PROTO 三 使用示例
  • 在Ubuntu 20.04上面搭建嵌入式开发环境

    电脑系统盘出故障了 xff0c 重新安装了Ubunt20 04 xff08 之前用的是18 04 日常工作编译基于Rockchip和AM335x系列芯片的内核和U boot比较多 xff0c 所以先搭建它们的开发环境 包括并不限依赖的库和常
  • 自下而上和自上而下的注意力:不同的过程和重叠的神经系统 2014sci

    摘要 大脑在处理物理世界中任何时刻出现的所有感官刺激的能力是有限的 xff0c 相反 xff0c 它依赖于根据瞬间的偶然性集中神经资源的注意力的认知过程 注意可以分为两种不同的功能 自下而上的注意 xff0c 指的是单纯 由外部 驱动因素对
  • Python爬虫抓取基金数据

    Python做网络爬虫需要学习额外基本知识 xff1a 1 HTML 基金所需的数据都通过HTML网页的形式返回 xff0c 数据和HTML tag通过一定的规范组成渲染后的形成网页 了解HTML是为了有效地剥离数据 2 Python的正则
  • ASN1.c v2x开发记录

    一 工具安装及使用 Asn1c编解码器代码git xff1a https github com vlm asn1c 当前主线版本为0 9 29 发布最新版本为0 9 28 将文件解压后 xff0c 依次执行 xff08 1 xff09 te
  • 高德地图api开发记录

    1 高德地图api使用讲解 https blog csdn net Augenstern QXL article details 120488096 具体的使用可以参考高德官方提供的demo和参考手册 2 地图坐标问题 高德地图使用的地图坐
  • vsphere远程访问ESXI端口

    如果要让VM ESXI在外网供用户访问的话 xff0c 要在路由器上面设置两个端口443 902 其中443 端口 主要 负责 别名 讯息 的 传递 xff0c 而 902 端口 主要 负责 远端 控制台 画面 的 传递 vsphere版本
  • CentOS7 下源码安装MPlayer播放器

    最近学习了build源码安装软件 xff0c 老师布置的习题 xff0c 用所学过的知识安装mplayer播放器 通过上网我了解到在linux系统下 xff0c mplayer播放器十分强大好用 但是 xff0c 在安装的过程中遇到了很多问
  • centos7 安装 mariadb 及安装设置

    使用的是linode的centos7系统 xff0c 安装mysql发现已经默认的是mariadb 但是不管是使用linode官网说明还是百度搜索到的的根本安装方法无法安装成功 总是提示这一句 xff1a ERROR 2002 HY000
  • Win8.1和Centos 7双系统, 磁盘挂在问题,Unable to access “ *** Volume”

    在装好Centos7后 xff0c 打开Win8 1系统磁盘时 xff0c 会有如下提示 xff1a Unable to access 70 GB Volume Error mounting dev sda1 at run media yo
  • 编译原理 FIRST集和FOLLOW集的求法

    前几日纠结于编译原理的First 和 Follow集合的求法 xff0c 然后发现了一片不错的博文 xff0c 记于此 原文地址 xff1a http blog sina com cn s blog a1132bf901011ylj htm
  • 今天装gocoed 出现了%path没有设置,不能进行

    今天安装gocode xff0c 虽然设置了Path xff0c 但是还是因为没有设置好 path xff0c 而不能进行 最终我找到了一篇博文 xff0c 结局了这个问题 windows 用户需要 go get u ldflags H 6
  • Broadcom Corporation BCM43227,Linux下安装网卡驱动

    平时喜欢倒腾 xff0c 特别是电脑系统的安装方面 xff0c 我基本上安装过所有系统 xff0c 在我买电脑的2年时间里 但是 xff0c 一直有个问题困扰着我 xff0c 那就是在Ubuntu xff0c Linux Mint等系统下
  • 官网下载Google Chrome离线安装包

    一直在用Google Chrome浏览器 xff0c 不过安装过chrome 的朋友都知道 xff0c 从Google官方下载安装chrome xff0c 会先下载一个小的安装管理程序 xff0c 安装时再从google 下载所需的安装文件
  • VSCode安装go语言开发环境,go插件问题解决

    在安装go插件时 xff0c 会自动更新很多依赖库文件 xff0c 都是从Github更新下来 xff0c 但是因为Github的文件中 xff0c 多有应用go官网中的文件 xff0c 导致 xff0c 因为网络缘故 xff0c 不能直接
  • VNC连接Ubuntu 16.04桌面灰色的问题解决

    1 安装gnome apt get install no install recommends ubuntu desktop gnome panel gnome settings daemon metacity nautilus gnome
  • Linux用diff比较两个文件的差异

    功能 xff1a 比较两个文件的差异 xff0c 并把不同地方的信息显示出来 语法 xff1a diff options FILES FILES的格式 xff1a FILE1 FILE2 xff1a 源是一个文件 xff0c 目标也是文件
  • 基于Qt的GPS导航系统软件源代码

    博主按 xff1a 这是我当初为了学习C 43 43 而给自己定的一个项目 xff0c 基本上实现了 通过这个项目可以说基本上掌握了C 43 43 和Qt的编程 现在将源码贡献出来给初学Qt的同志一些参考 当然其中肯定有不少地方是有问题的
  • (二十)ATP应用测试平台——websocket实现微服务版在线客服聊天室实战案例

    前言 在前面的博客内容中我们介绍了如何使用websocket实现一个网页版的在线客服聊天室 xff0c 众所周知 xff0c 由于websocket是一个长连接 xff0c 要和服务端保持会话连接 xff0c 所以其本身并不适用于微服务环境