您当前的位置:首页 > 电脑百科 > 程序开发 > 框架

SpringBoot使用WebSocket实现即时消息

时间:2023-08-14 14:23:30  来源:  作者:Springboot实战案例锦集
当以jar包形式运行时需要配置该bean,暴露我们配置的@ServerEndpoint;当我们以war独立Tomcat运行时不能配置该bean。这里有个g-messages.js文件是我写的一个工具类,用来做连接及心跳检查用的。

环境:SpringBoot2.4.12.


依赖

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

定义消息类型

  • 抽象消息对象
public class AbstractMessage {
  /**
   *   消息类型
   */
  protected String type ;
  
  /**
   *   消息内容
   */
  protected String content ;
  /**
   *   消息日期
   */
  protected String date ;
}

消息对象子类

1、Ping检查消息

public class PingMessage extends AbstractMessage {
  public PingMessage() {}
  public PingMessage(String type) {
    this.type = type ;
  }
}

2、系统消息

public class SystemMessage extends AbstractMessage {
  public SystemMessage() {}
  public SystemMessage(String type, String content) {
    this.type = type ;
    this.content = content ;
  }
}

3、点对点消息

public class PersonMessage extends AbstractMessage {
  private String fromName ;
  private String toName ;
}

消息类型定义

public enum MessageType {
  
  /**
   *   系统消息 0000;心跳检查消息 0001;点对点消息2001
   */
  SYSTEM("0000"), PING("0001"), PERSON("2001") ;
  
  private String type ;
  
  private MessageType(String type) {
    this.type = type ;
  }


  public String getType() {
    return type;
  }


  public void setType(String type) {
    this.type = type;
  }
  
}

WebSocket服务端点

该类作用就是定义客户端连接的地址

@ServerEndpoint(value = "/message/{username}", 
  encoders = {WsMessageEncoder.class},
  decoders = {WsMessageDecoder.class},
  subprotocols = {"gmsg"},
  configurator = MessageConfigurator.class)  
@Component  
public class GMessageListener {  
  
    public static ConcurrentMap<String, UserSession> sessions = new ConcurrentHashMap<>();
    private static Logger logger = LoggerFactory.getLogger(GMessageListener.class) ;
  
    private String username ;
    
    @OnOpen  
    public void onOpen(Session session, EndpointConfig config, @PathParam("username") String username){
      UserSession userSession = new UserSession(session.getId(), username, session) ;
      this.username = username ;
      sessions.put(username, userSession) ;
      logger.info("【{}】用户进入, 当前连接数:{}", username, sessions.size()) ; 
    }  
  
    @OnClose  
    public void onClose(Session session, CloseReason reason){  
      UserSession userSession = sessions.remove(this.username) ;
      if (userSession != null) {
        logger.info("用户【{}】, 断开连接, 当前连接数:{}", username, sessions.size()) ;
      }
    }
    
    @OnMessage
    public void pongMessage(Session session, PongMessage message) {
      ByteBuffer buffer = message.getApplicationData() ;
      logger.debug("接受到Pong帧【这是由浏览器发送】:" + buffer.toString());
    }
    
    @OnMessage
    public void onMessage(Session session, AbstractMessage message) {
      if (message instanceof PingMessage) {
        logger.debug("这里是ping消息");
        return ;
      }
      if (message instanceof PersonMessage) {
        PersonMessage personMessage = (PersonMessage) message ;
        if (this.username.equals(personMessage.getToName())) {
          logger.info("【{}】收到消息:{}", this.username, personMessage.getContent());
        } else {
          UserSession userSession = sessions.get(personMessage.getToName()) ;
          if (userSession != null) {
            try {
            userSession.getSession().getAsyncRemote().sendText(new ObjectMapper().writeValueAsString(message)) ;
          } catch (JsonProcessingException e) {
            e.printStackTrace();
          }
          }
        }
        return ;
      }
      if (message instanceof SystemMessage) {
        logger.info("接受到消息类型为【系统消息】") ; 
        return ;
      }
    }
    
    @OnError
    public void onError(Session session, Throwable error) {
      logger.error(error.getMessage()) ;
    }
}

WsMessageEncoder.JAVA
该类的主要作用是,当发送的消息是对象时,该如何转换

public class WsMessageEncoder implements Encoder.Text<AbstractMessage> {
  private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class) ;
  @Override
  public void init(EndpointConfig endpointConfig) {
  }
  @Override
  public void destroy() {
  }
  @Override
  public String encode(AbstractMessage tm) throws EncodeException {
    String message = null ;
    try {
      message = new ObjectMapper().writeValueAsString(tm);
    } catch (JsonProcessingException e) {
      logger.error("JSON处理错误:{}", e) ;
    }
    return message;
  }
}

WsMessageDecoder.java类
该类的作用是,当接收到消息时如何转换成对象。

public class WsMessageDecoder implements  Decoder.Text<AbstractMessage> {


  private static Logger logger = LoggerFactory.getLogger(WsMessageDecoder.class) ;
  private static Set<String> msgTypes = new HashSet<>() ;
  
  static {
    msgTypes.add(MessageType.PING.getType()) ;
    msgTypes.add(MessageType.SYSTEM.getType()) ;
    msgTypes.add(MessageType.PERSON.getType()) ;
  }
  @Override
  @SuppressWarnings("unchecked")
  public AbstractMessage decode(String s) throws DecodeException {
    AbstractMessage message = null ;
    try {
      ObjectMapper mapper = new ObjectMapper() ;
      Map<String,String> map = mapper.readValue(s, Map.class) ;
      String type = map.get("type") ;
      switch(type) {
        case "0000":
          message = mapper.readValue(s, SystemMessage.class) ;
          break;
        case "0001":
          message = mapper.readValue(s, PingMessage.class) ;
          break;
        case "2001":
          message = mapper.readValue(s, PersonMessage.class) ;
          break;
      }
    } catch (JsonProcessingException e) {
      logger.error("JSON处理错误:{}", e) ;
    }
    return message ;
  }


  // 该方法判断消息是否可以被解码(转换)
  @Override
  @SuppressWarnings("unchecked")
  public boolean willDecode(String s) {
    Map<String, String> map = new HashMap<>() ;
    try {
      map = new ObjectMapper().readValue(s, Map.class);
    } catch (JsonProcessingException e) {
      e.printStackTrace();
    }
    logger.debug("检查消息:【" + s + "】是否可以解码") ;
    String type = map.get("type") ;
    if (StringUtils.isEmpty(type) || !msgTypes.contAIns(type)) {
      return false ;
    }
    return true ;
  }
  @Override
  public void init(EndpointConfig endpointConfig) {
  }
  @Override
  public void destroy() {
  }
}

MessageConfigurator.java类
该类的作用是配置服务端点,比如配置握手信息

public class MessageConfigurator extends ServerEndpointConfig.Configurator {
  private static Logger logger = LoggerFactory.getLogger(MessageConfigurator.class) ;
  @Override
  public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
    logger.debug("握手请求头信息:" + request.getHeaders());
    logger.debug("握手响应头信息:" + response.getHeaders());
    super.modifyHandshake(sec, request, response);
  }  
}

WebSocke配置类

@Configuration
public class WebSocketConfig {
  
  @Bean
    public ServerEndpointExporter serverEndpointExporter (){  
        return new ServerEndpointExporter();  
    }  
  
}
  • 当以jar包形式运行时需要配置该bean,暴露我们配置的@ServerEndpoint;当我们以war独立tomcat运行时不能配置该bean。

前端页面

<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <meta name="Author" content="">
  <meta name="Keywords" content="">
  <meta name="Description" content="">
  <script src="g-messages.js?v=1"></script>
  <title>WebSocket</title>
  <style type="text/css">
</style>
  <script>
  let gm = null ;
  let username = null ;
  function ListenerMsg({url, protocols = ['gmsg'], options = {}}) {
    if (!url){ 
      throw new Error("未知服务地址") ;
    }
    gm = new window.__GM({
      url: url,
      protocols: protocols
    }) ;
    gm.open(options) ;
  }
  ListenerMsg.init = (user) => {
    if (!user) {
      alert("未知的当前登录人") ;
      return ;
    }
    let url = `ws://localhost:8080/message/${user}` ;
    let msg = document.querySelector("#msg")
    ListenerMsg({url, options: {
      onmessage (e) {
        let data = JSON.parse(e.data) ;
        let li = document.createElement("li") ;
        li.innerHTML = "【" + data.fromName + "】对你说:" + data.content ;
        msg.appendChild(li) ;
      }
    }}) ;
  }
  function enter() {
    username = document.querySelector("#nick").value ;
    ListenerMsg.init(username) ;
    document.querySelector("#chat").style.display = "block" ;
    document.querySelector("#enter").style.display = "none" ;
    document.querySelector("#cu").innerText = username ;
  }
  function send() {
    let a = document.querySelector("#toname") ;
    let b = document.querySelector("#content") ;
    let toName = a.value ;
    let content = b.value ;
    gm.sendMessage({type: "2001", content, fromName: username, toName}) ;
    a.value = '' ;
    b.value = '' ;
  }
</script>
 </head>
 <body>
  <div id="enter">
    <input id="nick"/><button type="button" onclick="enter()">进入</button>
  </div>
  <hr/>
  <div id="chat" style="display:none;">
    当前用户:<b id="cu"></b><br/>
    用户:<input id="toname" name="toname"/><br/><br/>
    内容:<textarea id="content" rows="3" cols="22"></textarea><br/>
    <button type="button" onclick="send()">发送</button>
  </div>
  <div>
    <ul id="msg">
    </ul>
  </div>
 </body>
</html>

这里有个g-messages.js文件是我写的一个工具类,用来做连接及心跳检查用的。

到此所有的代码完毕,接下来测试

测试

打开两个标签页,以不同的用户进入。

图片

 

图片

 

输入对方用户名发送消息

图片

 

图片

 

成功了,简单的websocket。我们生产环境还就这么完的,8g内存跑了6w的用户。



Tags:SpringBoot   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
详解基于SpringBoot的WebSocket应用开发
在现代Web应用中,实时交互和数据推送的需求日益增长。WebSocket协议作为一种全双工通信协议,允许服务端与客户端之间建立持久性的连接,实现实时、双向的数据传输,极大地提升了用...【详细内容】
2024-01-30  Search: SpringBoot  点击:(10)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19  Search: SpringBoot  点击:(86)  评论:(0)  加入收藏
SpringBoot3+Vue3 开发高并发秒杀抢购系统
开发高并发秒杀抢购系统:使用SpringBoot3+Vue3的实践之旅随着互联网技术的发展,电商行业对秒杀抢购系统的需求越来越高。为了满足这种高并发、高流量的场景,我们决定使用Spring...【详细内容】
2024-01-14  Search: SpringBoot  点击:(90)  评论:(0)  加入收藏
公司用了六年的 SpringBoot 项目部署方案,稳得一批!
本篇和大家分享的是springboot打包并结合shell脚本命令部署,重点在分享一个shell程序启动工具,希望能便利工作。 profiles指定不同环境的配置 maven-assembly-plugin打发布压...【详细内容】
2024-01-10  Search: SpringBoot  点击:(163)  评论:(0)  加入收藏
简易版的SpringBoot是如何实现的!!!
SpringBoot作为目前最流行的框架之一,同时是每个程序员必须掌握的知识,其提供了丰富的功能模块和开箱即用的特性,极大地提高了开发效率和降低了学习成本,使得开发人员能够更专注...【详细内容】
2023-12-29  Search: SpringBoot  点击:(132)  评论:(0)  加入收藏
用 SpringBoot+Redis 解决海量重复提交问题
前言 一:搭建redis的服务Api 二:自定义注解AutoIdempotent 三:token创建和检验 四:拦截器的配置 五:测试用例 六:总结前言:在实际的开发项目中,一个对外暴露的接口往往会面临很多...【详细内容】
2023-12-20  Search: SpringBoot  点击:(53)  评论:(0)  加入收藏
SpringBoot中如何优雅地个性化定制Jackson
当使用 JSON 格式时,Spring Boot 将使用ObjectMapper实例来序列化响应和反序列化请求。在本教程中,我们将了解配置序列化和反序列化选项的最常用方法。一、默认配置默认情况下...【详细内容】
2023-12-20  Search: SpringBoot  点击:(132)  评论:(0)  加入收藏
springboot-如何集成Validation进行参数校验
一、步骤概览 二、步骤说明1.引入依赖包在 pom.xml 文件中引入 validation 组件,它提供了在 Spring Boot 应用程序中进行参数校验的支持。<!-- WEB 程序依赖包 --><dependen...【详细内容】
2023-12-13  Search: SpringBoot  点击:(156)  评论:(0)  加入收藏
优雅的springboot参数校验,你学会了吗?
前言在后端的接口开发过程,实际上每一个接口都或多或少有不同规则的参数校验,有一些是基础校验,如非空校验、长度校验、大小校验、格式校验;也有一些校验是业务校验,如学号不能重...【详细内容】
2023-11-29  Search: SpringBoot  点击:(199)  评论:(0)  加入收藏
Springboot扩展点之BeanDefinitionRegistryPostProcessor,你学会了吗?
前言通过这篇文章来大家分享一下,另外一个Springboot的扩展点BeanDefinitionRegistryPostProcessor,一般称这类扩展点为容器级后置处理器,另外一类是Bean级的后置处理器;容器级...【详细内容】
2023-11-27  Search: SpringBoot  点击:(174)  评论:(0)  加入收藏
▌简易百科推荐
Web Components实践:如何搭建一个框架无关的AI组件库
一、让人又爱又恨的Web ComponentsWeb Components是一种用于构建可重用的Web元素的技术。它允许开发者创建自定义的HTML元素,这些元素可以在不同的Web应用程序中重复使用,并且...【详细内容】
2024-04-03  京东云开发者    Tags:Web Components   点击:(8)  评论:(0)  加入收藏
Kubernetes 集群 CPU 使用率只有 13% :这下大家该知道如何省钱了
作者 | THE STACK译者 | 刘雅梦策划 | Tina根据 CAST AI 对 4000 个 Kubernetes 集群的分析,Kubernetes 集群通常只使用 13% 的 CPU 和平均 20% 的内存,这表明存在严重的过度...【详细内容】
2024-03-08  InfoQ    Tags:Kubernetes   点击:(12)  评论:(0)  加入收藏
Spring Security:保障应用安全的利器
SpringSecurity作为一个功能强大的安全框架,为Java应用程序提供了全面的安全保障,包括认证、授权、防护和集成等方面。本文将介绍SpringSecurity在这些方面的特性和优势,以及它...【详细内容】
2024-02-27  风舞凋零叶    Tags:Spring Security   点击:(53)  评论:(0)  加入收藏
五大跨平台桌面应用开发框架:Electron、Tauri、Flutter等
一、什么是跨平台桌面应用开发框架跨平台桌面应用开发框架是一种工具或框架,它允许开发者使用一种统一的代码库或语言来创建能够在多个操作系统上运行的桌面应用程序。传统上...【详细内容】
2024-02-26  贝格前端工场    Tags:框架   点击:(47)  评论:(0)  加入收藏
Spring Security权限控制框架使用指南
在常用的后台管理系统中,通常都会有访问权限控制的需求,用于限制不同人员对于接口的访问能力,如果用户不具备指定的权限,则不能访问某些接口。本文将用 waynboot-mall 项目举例...【详细内容】
2024-02-19  程序员wayn  微信公众号  Tags:Spring   点击:(39)  评论:(0)  加入收藏
开发者的Kubernetes懒人指南
你可以将本文作为开发者快速了解 Kubernetes 的指南。从基础知识到更高级的主题,如 Helm Chart,以及所有这些如何影响你作为开发者。译自Kubernetes for Lazy Developers。作...【详细内容】
2024-02-01  云云众生s  微信公众号  Tags:Kubernetes   点击:(50)  评论:(0)  加入收藏
链世界:一种简单而有效的人类行为Agent模型强化学习框架
强化学习是一种机器学习的方法,它通过让智能体(Agent)与环境交互,从而学习如何选择最优的行动来最大化累积的奖励。强化学习在许多领域都有广泛的应用,例如游戏、机器人、自动驾...【详细内容】
2024-01-30  大噬元兽  微信公众号  Tags:框架   点击:(67)  评论:(0)  加入收藏
Spring实现Kafka重试Topic,真的太香了
概述Kafka的强大功能之一是每个分区都有一个Consumer的偏移值。该偏移值是消费者将读取的下一条消息的值。可以自动或手动增加该值。如果我们由于错误而无法处理消息并想重...【详细内容】
2024-01-26  HELLO程序员  微信公众号  Tags:Spring   点击:(84)  评论:(0)  加入收藏
SpringBoot如何实现缓存预热?
缓存预热是指在 Spring Boot 项目启动时,预先将数据加载到缓存系统(如 Redis)中的一种机制。那么问题来了,在 Spring Boot 项目启动之后,在什么时候?在哪里可以将数据加载到缓存系...【详细内容】
2024-01-19   Java中文社群  微信公众号  Tags:SpringBoot   点击:(86)  评论:(0)  加入收藏
花 15 分钟把 Express.js 搞明白,全栈没有那么难
Express 是老牌的 Node.js 框架,以简单和轻量著称,几行代码就可以启动一个 HTTP 服务器。市面上主流的 Node.js 框架,如 Egg.js、Nest.js 等都与 Express 息息相关。Express 框...【详细内容】
2024-01-16  程序员成功  微信公众号  Tags:Express.js   点击:(86)  评论:(0)  加入收藏
站内最新
站内热门
站内头条