从理论到综合实例,终于弄懂了双工通讯协议websocket!

Spring STOMP-带注解的控制器 应用程序可以使用带注解的类来处理来自客户端的消息。这些类可以声明带和注释注解的方法,能处理对应destination的消息。注解支持方法和类级别。在类级别,的配置会在所有方法上共享。默认情况下,映射值是Ant风格的路径模式(例如/thing*/thing/**),也支持模板变量(例如,这些值可以通过带注解的方法参数引用。应用程序也可以将destination的映射格式切换到点分隔的形式,如中解释的一样。 阅读详情
  • 西格玛的博客

  • http://lrwinx.github.io/

本篇文章以 websocket 的原理和落地为核心,来叙述 websocket 的使用,以及相关应用场景。

http 与 websocket

如我们所了解,http 连接为一次请求一次响应 (request->response),必须为同步调用方式。
而 websocket 为一次连接以后,会建立 tcp 连接,后续客户端与服务器交互为全双工方式的交互方式,客户端可以发送消息到服务端,服务端也可将消息发送给客户端。

http,websocket

此图来源于 Websocket 协议的学习、调研和实现, 如有侵权问题,告知后,删除。

根据上图,我们大致可以了解到 http 与 websocket 之间的区别和不同。

为什么要使用 websocket

那么了解 http 与 websocket 之间的不同以后,我们为什么要使用 websocket 呢?他的应用场景是什么呢?

我找到了一个比较符合 websocket 使用场景的描述

“The best fit for WebSocket is in web applications where the client and server need to exchange events at high frequency and with low latency.”
翻译: 在客户端与服务器端交互的 web 应用中,websocket 最适合在高频率低延迟的场景下,进行事件的交换和处理

此段来源于 spring websocket 的官方文档

了解以上知识后,我举出几个比较常见的场景:

  1. 游戏中的数据传输

  2. 股票 K 线图数据

  3. 客服系统

根据如上所述,各个系统都来使用 websocket 不是更好吗?

其实并不是,websocket 建立连接之后,后边交互都由 tcp 协议进行交互,故开发的复杂度会较高。当然 websocket 通讯,本身要考虑的事情要比 HTTP 协议的通讯考虑的更多.

所以如果不是有特殊要求 (即 应用不是” 高频率低延迟”的要求), 需要优先考虑 HTTP 协议是否可以满足。

比如新闻系统,新闻的数据晚上 10 分钟 - 30 分钟,是可以接受的,那么就可以采用 HTTP 的方式进行轮询 (polling) 操作调用 REST 接口。

当然有时我们建立了 websocket 通讯,并且希望通过 HTTP 提供的 REST 接口推送给某客户端,此时需要考虑 REST 接口接受数据传送给 websocket 中,进行广播式的通讯方式。

至此,我已经讲述了三种交互方式的使用场景:

  1. websocket 独立使用场景

  2. HTTP 独立使用场景

  3. HTTP 中转 websocket 使用场景

websocket

websocket 为一次 HTTP 握手后,后续通讯为 tcp 协议的通讯方式。

当然,和 HTTP 一样,websocket 也有一些约定的通讯方式,http 通讯方式为 http 开头的方式, e.g. http://xxx.com/path ,websocket 通讯方式则为 ws 开头的方式, e.g. ws://xxx.com/path

SSL:

  1. HTTP: https://xxx.com/path

  2. WEBSOCKET: wss://xxx.com/path

websocket 通讯

此图来源于 WebSocket 教程, 如有侵权问题,告知后,删除。

SockJS

正如我们所知, websocket 协议虽然已经被制定,当时还有很多版本的浏览器或浏览器厂商还没有支持的很好。

所以, SockJS, 可以理解为是 websocket 的一个备选方案。

那它如何规定备选方案的呢?

它大概支持这样几个方案:

  1. Websockets

  2. Streaming

  3. Polling

当然,开启并使用 SockJS 后,它会优先选用 websocket 协议作为传输协议,如果浏览器不支持 websocket 协议,则会在其他方案中,选择一个较好的协议进行通讯。

看一下目前浏览器的支持情况:

Supported transports, by browser

此图来源于 github: sockjs-client

所以,如果使用 SockJS 进行通讯,它将在使用上保持一致,底层由它自己去选择相应的协议。

可以认为 SockJS 是 websocket 通讯层上的上层协议。

底层对于开发者来说是透明的。

STOMP

STOMP 中文为: 面向消息的简单文本协议

websocket 定义了两种传输信息类型: 文本信息 和 二进制信息 (text and binary).

类型虽然被确定,但是他们的传输体是没有规定的。

当然你可以自己来写传输体,来规定传输内容。(当然,这样的复杂度是很高的)

所以, 需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议, 即交互中的高级协议来定义交互信息。

STOMP 本身可以支持流类型的网络传输协议: websocket 协议和 tcp 协议

它的格式为:

COMMAND
header1:value1
header2:value2
Body^@
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@

当然 STOMP 已经应用于很多消息代理中,作为一个传输协议的规定,如: RabbitMQ, ActiveMQ

我们皆可以用 STOMP 和这类 MQ 进行消息交互.

除了 STOMP 相关的代理外,实际上还提供了一个 stomp.js, 用于浏览器客户端使用 STOMP 消息协议传输的 js 库。

让我们很方便的使用 stomp.js 进行与 STOMP 协议相关的代理进行交互.

正如我们所知,如果 websocket 内容传输信息使用 STOMP 来进行交互,websocket 也很好的于消息代理器进行交互 (如: RabbitMQ, ActiveMQ)

这样就很好的提供了消息代理的集成方案。

总结,使用 STOMP 的优点如下:

  1. 不需要自建一套自定义的消息格式

  2. 现有 stomp.js 客户端 (浏览器中使用) 可以直接使用

  3. 能路由信息到指定消息地点

  4. 可以直接使用成熟的 STOMP 代理进行广播 如: RabbitMQ, ActiveMQ

后端技术方案选型

websocket 服务端选型: spring websocket

支持 SockJS, 开启 SockJS 后,可应对不同浏览器的通讯支持
支持 STOMP 传输协议,可无缝对接 STOMP 协议下的消息代理器 (如: RabbitMQ, ActiveMQ)

前端技术方案选型

前端选型: stomp.js,sockjs.js

后端开启 SOMP 和 SockJS 支持后,前对应有对应的 js 库进行支持.
所以选用此两个库.

总结

上述所用技术,是这样的逻辑:

  1. 开启 socktJS:
    如果有浏览器不支持 websocket 协议,可以在其他两种协议中进行选择,但是对于应用层来讲,使用起来是一样的。
    这是为了支持浏览器不支持 websocket 协议的一种备选方案

  2. 使用 STOMP:
    使用 STOMP 进行交互,前端可以使用 stomp.js 类库进行交互,消息一 STOMP 协议格式进行传输,这样就规定了消息传输格式。
    消息进入后端以后,可以将消息与实现 STOMP 格式的代理器进行整合。
    这是为了消息统一管理,进行机器扩容时,可进行负载均衡部署

  3. 使用 spring websocket:
    使用 spring websocket, 是因为他提供了 STOMP 的传输自协议的同时,还提供了 StockJS 的支持。
    当然,除此之外,spring websocket 还提供了权限整合的功能,还有自带天生与 spring 家族等相关框架进行无缝整合。

应用背景

2016 年,在公司与同事一起讨论和开发了公司内部的客服系统,由于前端技能的不足,很多通讯方面的问题,无法亲自调试前端来解决问题。

因为公司技术架构体系以前后端分离为主,故前端无法协助后端调试,后端无法协助前端调试

在加上 websocket 为公司刚启用的协议,了解的人不多,导致前后端调试问题重重。

一年后的今天,我打算将前端重温,自己来调试一下前后端,来发掘一下之前联调的问题.

当然,前端,我只是考虑 stomp.js 和 sockt.js 的使用。

代码阶段设计

角色

  1. 客服

  2. 客户

登录用户状态

  1. 上线

  2. 下线

分配策略

  1. 用户登陆后,应该根据用户角色进行分配

关系保存策略

  1. 应该提供关系型保存策略: 考虑内存式策略 (可用于测试),redis 式策略
    备注: 优先应该考虑实现 Inmemory 策略,用于测试,让关系保存策略与存储平台无关

通讯层设计

  1. 归类 topic 的广播设计 (通讯方式: 1-n)

  2. 归类 queue 的单点设计 (通讯方式: 1-1)

代码实现

角色

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
public enum Role {
  CUSTOMER_SERVICE,
  CUSTOMER;
  public static boolean isCustomer(User user) {
      Collection<GrantedAuthority> authorities = user.getAuthorities();
      SimpleGrantedAuthority customerGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + Role.CUSTOMER.name());
      return authorities.contains(customerGrantedAuthority);
  }
  public static boolean isCustomerService(User user) {
      Collection<GrantedAuthority> authorities = user.getAuthorities();
      SimpleGrantedAuthority customerServiceGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + Role.CUSTOMER_SERVICE.name());
      return authorities.contains(customerServiceGrantedAuthority);
  }
}

代码中 User 对象,为安全对象,即 spring 中 org.springframework.security.core.userdetails.User,为 UserDetails 的实现类。
User 对象中,保存了用户授权后的很多基础权限信息,和用户信息。
如下:

public interface UserDetails extends Serializable {
  Collection<? extends GrantedAuthority> getAuthorities();
  String getPassword();
  String getUsername();
  boolean isAccountNonExpired();
  boolean isAccountNonLocked();
  boolean isCredentialsNonExpired();
  boolean isEnabled();
}

方法 #isCustomer 和 #isCustomerService 用来判断用户当前是否是顾客或者是客服。

登录用户状态

public interface StatesManager {
    enum StatesManagerEnum{
        ON_LINE,
        OFF_LINE
    }
    void changeState(User user , StatesManagerEnum statesManagerEnum);
    StatesManagerEnum currentState(User user);
}

设计登录状态时,应存在登录状态管理相关的状态管理器,此管理器只负责更改用户状态和获取用户状态相关操作。
并不涉及其他关联逻辑,这样的代码划分,更有助于面向接口编程的扩展性

分配策略

public interface DistributionUsers {
  void distribution(User user);
}

分配角色接口设计,只关注传入的用户,并不关注此用户是客服或者用户,具体需要如何去做,由具体的分配策略来决定。

关系保存策略

public interface RelationHandler {
  void saveRelation(User customerService,User customer);
  List<User> listCustomers(User customerService);
  void deleteRelation(User customerService,User customer);
  void saveCustomerService(User customerService);
  List<User> listCustomerService();
  User getCustomerService(User customer);
  boolean exist(User user);
  User availableNextCustomerService();
}

关系保存策略,亦是只关注关系保存相关,并不在乎于保存到哪个存储介质中。
实现类由 Inmemory 还是 redis 还是 mysql, 它并不专注。
但是,此处需要注意,对于这种关系保存策略,开发测试时,并不涉及高可用,可将 Inmemory 先做出来用于测试。
开发功能同时,相关同事再来开发其他介质存储的策略,性能测试以及 UAT 相关测试时,应切换为此介质存储的策略再进行测试。

用户综合管理

对于不同功能的实现策略,由各个功能自己来实现,在使用上,我们仅仅根据接口编程即可。
所以,要将上述所有功能封装成一个工具类进行使用,这就是所谓的 设计模式: 门面模式

@Component
public class UserManagerFacade {
    @Autowired
    private DistributionUsers distributionUsers;
    @Autowired
    private StatesManager statesManager;
    @Autowired
    private RelationHandler relationHandler;
    public void login(User user) {
        if (roleSemanticsMistiness(user)) {
            throw new SessionAuthenticationException("角色语义不清晰");
        }
        distributionUsers.distribution(user);
        statesManager.changeState(user, StatesManager.StatesManagerEnum.ON_LINE);
    }
    private boolean roleSemanticsMistiness(User user) {
        Collection<GrantedAuthority> authorities = user.getAuthorities();
        SimpleGrantedAuthority customerGrantedAuthority = new SimpleGrantedAuthority("ROLE_"+Role.CUSTOMER.name());
        SimpleGrantedAuthority customerServiceGrantedAuthority = new SimpleGrantedAuthority("ROLE_"+Role.CUSTOMER_SERVICE.name());
        if (authorities.contains(customerGrantedAuthority)
                && authorities.contains(customerServiceGrantedAuthority)) {
            return true;
        }
        return false;
    }
    public void logout(User user){
        statesManager.changeState(user, StatesManager.StatesManagerEnum.OFF_LINE);
    }
    public User getCustomerService(User user){
        return relationHandler.getCustomerService(user);
    }
    public List<User> listCustomers(User user){
        return relationHandler.listCustomers(user);
    }
    public StatesManager.StatesManagerEnum getStates(User user){
        return statesManager.currentState(user);
    }
}

UserManagerFacade 中注入三个相关的功能接口:

@Autowired
private DistributionUsers distributionUsers;
@Autowired
private StatesManager statesManager;
@Autowired
private RelationHandler relationHandler;

可提供:

  1. 登录 (#login)

  2. 登出 (#logout)

  3. 获取对应客服 (#getCustomerService)

  4. 获取对应用户列表 (#listCustomers)

  5. 当前用户登录状态 (#getStates)

这样的设计,可保证对于用户关系的管理都由 UserManagerFacade 来决定
其他内部的操作类,对于使用者来说,并不关心,对开发来讲,不同功能的策略都是透明的。

通讯层设计 - 登录,授权

spring websocket 虽然并没有要求 connect 时,必须授权,因为连接以后,会分发给客户端 websocket 的 session id,来区分客户端的不同。
但是对于大多数应用来讲,登录授权以后,进行 websocket 连接是最合理的,我们可以进行权限的分配,和权限相关的管理。

我模拟例子中,使用的是 spring security 的 Inmemory 的相关配置:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.inMemoryAuthentication().withUser("admin").password("admin").roles(Role.CUSTOMER_SERVICE.name());
      auth.inMemoryAuthentication().withUser("admin1").password("admin").roles(Role.CUSTOMER_SERVICE.name());
      auth.inMemoryAuthentication().withUser("user").password("user").roles(Role.CUSTOMER.name());
      auth.inMemoryAuthentication().withUser("user1").password("user").roles(Role.CUSTOMER.name());
      auth.inMemoryAuthentication().withUser("user2").password("user").roles(Role.CUSTOMER.name());
      auth.inMemoryAuthentication().withUser("user3").password("user").roles(Role.CUSTOMER.name());
  }
  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http.csrf().disable()
              .formLogin()
              .and()
              .authorizeRequests()
              .anyRequest()
              .authenticated();
  }
}

相对较为简单,创建 2 个客户,4 个普通用户。
当认证管理器认证后,会将认证后的合法认证安全对象 user(即 认证后的 token) 放入 STOMP 的 header 中.
此例中,认证管理认证之后,认证的 token 为 org.springframework.security.authentication.UsernamePasswordAuthenticationToken,
此 token 认证后,将放入 websocket 的 header 中。(即 后边会谈到的安全对象 java.security.Principal)

通讯层设计 - websocket 配置

@Order(Ordered.HIGHEST_PRECEDENCE + 99)
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/portfolio").withSockJS();
    }
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic", "/queue");
    }
}

此配置中,有几点需进行讲解:
其中端点”portfolio”, 用于 socktJs 进行 websocket 连接时使用,只用于建立连接。
“/topic”, “/queue”, 则为 STOMP 的语义约束,topic 语义为 1-n(广播机制),queue 语义为 1-1(单点机制)
“app”, 此为应用级别的映射终点前缀,这样说有些晦涩,一会看一下示例将会清晰很多。

通讯层设计 - 创建连接

用于连接 spring websocket 的端点为 portfolio,它可用于连接,看一下具体实现:

<script src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.min.js"></script>
<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>
<script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
var socket = new SockJS("/portfolio");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
   showGreeting("登录用户: " + frame.headers["user-name"]);
});

这样便建立了连接。后续的其他操作就可以通过 stompClient 句柄进行使用了。

通讯层设计 - spring websocket 消息模型

见模型图:

message-flow-simple-broker

此图来源 spring-websocket 官方文档

可以看出对于同一定于目标都为:/topic/broadcast, 它的发送渠道为两种:/app/broadcast 和 / topic/broadcast

如果为 / topic/broadcast, 直接可将消息体发送给定于目标(/topic/broadcast)。

如果是 / app/broadcast,它将消息对应在 MessageHandler 方法中进行处理,处理后的结果发放到 broker channel 中,最后再讲消息体发送给目标 (/topic/broadcast)

当然,这里边所说的 app 前缀就是刚才我们在 websocket 配置中的前缀.

看一个例子:

前端订阅:

stompClient.subscribe('/topic/broadcast', function(greeting){
    showGreeting(greeting.body);
});

后端服务:

@Controller
public class ChatWebSocket extends AbstractWebSocket{
 @MessageMapping("broadcast")
 public String broadcast(@Payload @Validated Message message, Principal principal) {
     return "发送人: " + principal.getName() + " 内容: " + message.toString();
 }
}
@Data
public class Message {
    @NotNull(message = "标题不能为空")
    private String title;
    private String content;
}

前端发送:

function sendBroadcast() {
    stompClient.send("/app/broadcast",{},JSON.stringify({'content':'message content'}));
}

这种发送将消息发送给后端带有 @MessageMapping 注解的方法,然后组合完数据以后,在推送给订阅 / topic/broadcast 的前端

function sendBroadcast() {
    stompClient.send("/topic/broadcast",{},JSON.stringify({'content':'message content'}));
}

这种发送直接将消息发送给订阅 / topic/broadcast 的前端,并不通过注解方法进行流转。

我相信上述这个理解已经解释清楚了 spring websocket 的消息模型图

通讯层设计 - @MessageMapping

带有这个注解的 @Controller 下的方法,正是对应 websocket 中的中转数据的处理方法。
那么这个注解下的方法究竟可以获取哪些数据,其中有什么原理呢?

方法说明

我大概说一下:
Message,@Payload,@Header,@Headers,MessageHeaders,MessageHeaderAccessor, SimpMessageHeaderAccessor,StompHeaderAccessor
以上这些都是获取消息头,消息体,或整个消息的基本对象模型。

@DestinationVariable
这个注解用于动态监听路径,很想 rest 中的 @PathVariable:

e.g.:

@MessageMapping("/queue/chat/{uid}")
public void chat(@Payload @Validated Message message, @DestinationVariable("uid") String uid, Principal principal) {
    String msg = "发送人: " + principal.getName() + " chat ";
    simpMessagingTemplate.convertAndSendToUser(uid,"/queue/chat",msg);
}

java.security.Principal

这个对象我需要重点说一下。
他则是 spring security 认证之后,产生的 Token 对象, 即本例中的 UsernamePasswordAuthenticationToken.

UsernamePasswordAuthenticationToken 类图

不难发现 UsernamePasswordAuthenticationToken 是 Principal 的一个实现.

可以将 Principal 直接转成授权后的 token, 进行操作:

UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) principal;

正如前边设计章节所说,整个用户设计都是对 org.springframework.security.core.userdetails.User 进行操作,那如何拿到 User 对象呢。
很简单, 如下:

UsernamePasswordAuthenticationToken user = (UsernamePasswordAuthenticationToken) principal;
User user = (User) user.getPrincipal()

通讯层设计 - 1-1 && 1-n

1-n topic:
此方式,上述消息模型章节已经讲过,此处不再赘述

1-1 queue:
客服 - 用户沟通为 1-1 用户交互的案例

前端:

stompClient.subscribe('/user/queue/chat',function(greeting){
    showGreeting(greeting.body);
});

后端:

@MessageMapping("/queue/chat/{uid}")
public void chat(@Payload @Validated Message message, @DestinationVariable("uid") String uid, Principal principal) {
    String msg = "发送人: " + principal.getName() + " chat ";
    simpMessagingTemplate.convertAndSendToUser(uid,"/queue/chat",msg);
}

发送端:

function chat(uid) {
    stompClient.send("/app/queue/chat/"+uid,{},JSON.stringify({'title':'hello','content':'message content'}));
}

上述的转化,看上去没有 topic 那样 1-n 的广播要流畅,因为代码中采用约定的方式进行开发,当然这是由 spring 约定的。

约定转化的处理器为 UserDestinationMessageHandler。

大概的语义逻辑如下:

“An application can send messages targeting a specific user, and Spring’s STOMP support recognizes destinations prefixed with “/user/“ for this purpose. For example, a client might subscribe to the destination “/user/queue/position-updates”. This destination will be handled by the UserDestinationMessageHandler and transformed into a destination unique to the user session, e.g. “/queue/position-updates-user123”. This provides the convenience of subscribing to a generically named destination while at the same time ensuring no collisions with other users subscribing to the same destination so that each user can receive unique stock position updates.”

大致的意思是说: 如果是客户端订阅了 / user/queue/position-updates, 将由 UserDestinationMessageHandler 转化为一个基于用户会话的订阅地址, 比如 / queue/position-updates-user123,然后可以进行通讯。

例子中,我们可以把 uid 当成用户的会话,因为用户 1-1 通讯是通过 spring security 授权的,所以我们可以把会话当做授权后的 token.
如登录用户 token 为: UsernamePasswordAuthenticationToken newToken = new UsernamePasswordAuthenticationToken(“admin”,”user”);
且这个 token 是合法的,那么 / user/queue/chat 订阅则为 / queue/chat-admin

发送时,如果通过 / user/admin/queue/chat, 则不通过 @MessageMapping 直接进行推送。
如果通过 / app/queue/chat/admin, 则将消息由 @MessageMapping 注解处理,最终发送给 / user/admin/queue/chat 终点

追踪代码 simpMessagingTemplate.convertAndSendToUser:

@Override
public void convertAndSendToUser(String user, String destination, Object payload, Map<String, Object> headers,
    MessagePostProcessor postProcessor) throws MessagingException {
  Assert.notNull(user, "User must not be null");
  user = StringUtils.replace(user, "/", "%2F");
  super.convertAndSend(this.destinationPrefix + user + destination, payload, headers, postProcessor);
}

说明最后的路径依然是 / user/admin/queue/chat 终点.

通讯层设计 - @SubscribeMapping

@SubscribeMapping 注解可以完成订阅即返回的功能。
这个很像 HTTP 的 request-response,但不同的是 HTTP 的请求和响应是同步的,每次请求必须得到响应。
而 @SubscribeMapping 则是异步的。意思是说:当订阅时,直到回应可响应时在进行处理。

通讯层设计 - 异常处理

@MessageMapping 是支持 jsr 303 校验的,它支持 @Validated 注解,可抛出错误异常, 如下:

@MessageMapping("broadcast")
public String broadcast(@Payload @Validated Message message, Principal principal) {
    return "发送人: " + principal.getName() + " 内容: " + message.toString();
}

那异常如何处理呢

@MessageExceptionHandler, 它可以进行消息层的异常处理

@MessageExceptionHandler
@SendToUser(value = "/queue/error",broadcast = false)
public String handleException(MethodArgumentNotValidException methodArgumentNotValidException) {
    BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
    if (!bindingResult.hasErrors()) {
        return "未知错误";
    }
    List<FieldError> allErrors = bindingResult.getFieldErrors();
    return "jsr 303 错误: " + allErrors.iterator().next().getDefaultMessage();
}

其中 @SendToUser,是指只将消息发送给当前用户,当然,当前用户需要订阅 / user/queue/error 地址。
注解中 broadcast, 则表明消息不进行多会话的传播 (有可能一个用户登录 3 个浏览器,有三个会话),如果此 broadcast=false,则只传给当前会话,不进行其他会话传播

本文从 websocket 的原理和协议,以及内容相关协议等不同维度进行了详细介绍。

最终以一个应用场景为例,从项目的结构设计,以及代码策略设计,设计模式等不同方面展示了 websocket 的通讯功能在项目中的使用。

如何实现某一功能其实并不重要,重要的是得了解理论,深入理论之后,再进行开发。

望此篇文章对你理解 websocket 有所帮助。

(完)

【推荐阅读】

优秀!高级Java都这样优雅处理空值

SpringBoot 微信点餐开源系统

在 Spring Boot 中,如何干掉 if else

基于 token 的多平台身份认证架构设计

好文!必须点赞

Spring STOMP @SubscribeMapping 的使用 标记在方法上,用于将客户端以 “/app” 开头的订阅路由到对应的方法上。默认情况下,返回值会通过clientOutboundChannel直接返回(发送)给客户端,不经过broker,是一次性的。用户可以通过 @SendTo 或 @SendToUser 去修改这一行为,也就是将返回值发送个broker,然后广播出去。一般用于初始化数据。如登入聊天室后,初始化在线人员列表和历史消息等。不是真正的订阅。 客户端: 结果: 官方文档 https://docs.spring.io/spring-framew 阅读详情

相关推荐

【SpringBoot笔记40】SpringBoot中@SendTo、@SendToUser、@MessageMapping、@SubscribeMapping注解介绍及其使用

SendToUser注解中,默认只会处理以【/user】开头的消息地址,因为@SendToUser注解标记的方法,是会被【UserDestinationMessageHandler】代理器处理的,当然,我们也可以自定义处理地址的前缀,这要在WebSocket配置类中设置。@MessageMapping注解是SpringBoot中提供的,它的作用是:当客户端向某个地址发送消息时候,此时会根据@MessageMapping注解映射的地址进行匹配,找到对应的映射地址之后,就会执行对应的方法。

✅ Java 开发工程师 1484

【websocket三】 @MessageMapping 和 @SubscribeMapping 注解区别

客户端可以有多种不同的方式,发送消息给server,包括 SUBSCRIBE和 SEND. @SubscribeMapping("/topic/topic1") 标注的方法,只会处理SUBSCRIBE发送的消息。 @MessageMapping("/topic/topic1") 标注的方法,只会处理SEND发送的消息。 在客户端和server建立连接后,涉及到很多不同形式的消息转发,这个可以参考R...

kouryoushine的博客 9751

PHP实现WebSocket实时通信完整实例详解

逐行解析- 第9行:提供对象级存储,避免引用问题。- 第17行:resourceId是Ratchet分配的唯一整数ID,便于追踪连接。- 第22行:send()方法自动处理UTF-8编码与WebSocket帧封装。- 第28行:detach()防止内存泄漏。- 第34行:close()主动终止异常连接,释放资源。随着系统复杂度上升,原始文本消息已无法满足需求,必须引入自定义协议来支持认证、权限控制、命令执行等功能。

weixin_42509720的博客 1114

java subscribe_java-使用Spring @SubscribeMapping获取当前用户

我在严重依赖WebSocket进行数据交换的Spring Boot应用程序中使用@SubscribeMapping.该应用程序由Spring Security保护.客户端,我在WebSocket上使用Stomp:this.socket = new WebSocket(this.socketUrl);this.stompClient = Stomp.over(this.socket);this.st...

weixin_29355377的博客 1473

从USB 2.0到USB 3.x:Synopsys SVT USB VIP配置避坑与接口选择指南

本文详细解析了从USB 2.0到USB 3.x的Synopsys SVT USB VIP配置要点,涵盖协议版本选择、物理接口匹配及UVM层次化架构实现。重点介绍了layering sequence的配置技巧和常见问题解决方案,帮助验证工程师高效避坑并优化验证流程。

weixin_42653691的博客 238

别再花屏了!手把手教你用STM32CubeIDE搞定LVGL 8.3移植(附完整源码)

本文详细介绍了如何在STM32CubeIDE环境下成功移植LVGL 8.3,解决常见的花屏问题。从环境配置、显示驱动优化到编译警告处理,提供完整的实战方案和源码示例,帮助开发者快速实现流畅的嵌入式GUI界面。特别针对STM32平台的DMA2D加速和内存管理进行了深度优化。

weixin_33739646的博客 242

js websocket同步等待_从理论到综合实例,终于弄懂了双工通讯协议websocket!

小Hub领读:websocket是双工通讯协议,STOMP是简易文本协议,让传输内容更加简单,socktJS是为了解决浏览器不支持websocket协议切换到其他协议解决问题。如果大家看了这篇文章不懂的话,我可以自己写篇文章介绍给大家,带git地址的。西格玛的博客http://lrwinx.github.io/本篇文章以 websocket 的原理和落地为核心,来叙述 websocket...

weixin_28881989的博客 1339

pgpool-II踩坑日记两则

最近结合pgpool-II进行postgreSQL高可用的部署,过程非常煎熬,作为新手,公司也没有前辈熟悉postgreSQL,只能自己摸索。好在已经搞明白了,把困扰我很长时间的两个问题分享一下。 1. show pool_nodes 第一个问题就是 show pool_nodes 的使用,照着教程建好后,在哪里执行这个指令呢?我在pg服务器上怎么执行这个指令都显示 ERROR: unrecognized configuration parameter "pool_nodes",一度怀疑人生。翻阅了很多资料

zhaizhai6_6的博客 4540

websocket区分不同的用户_从理论到综合实例,终于弄懂了双工通讯协议websocket!...

西格玛的博客http://lrwinx.github.io/本篇文章以 websocket 的原理和落地为核心,来叙述 websocket 的使用,以及相关应用场景。http 与 websocket如我们所了解,http 连接为一次请求一次响应 (request->response),必须为同步调用方式。而 websocket 为一次连接以后,会建立 tcp 连接,后续客户端与服务...

weixin_39579726的博客 1770

GD32F303驱动WS2812B灯带:用TIMER+PWM+DMA解放CPU,实现流畅灯效(附完整代码)

本文详细介绍了基于GD32F303的TIMER+PWM+DMA全自动控制方案,用于高效驱动WS2812B灯带。通过硬件外设协同工作,该方案不仅解放了CPU资源,还能实现复杂的灯光效果,如渐变、流水等。文章提供了完整的代码实现和性能优化技巧,适合嵌入式开发者参考。

weixin_42561143的博客 379

[基于STM32底盘控制与ROS上层导航小车制作] 第一节 stm32电机驱动与编码器读取反馈

系列文章目录 第一节stm32电机驱动与编码器读取反馈 第二节 stm32电机pid控制 第三节 stm32线速度标定 第四节 stm32添加mpu6050得到angle角度 第五节 实现STM32与ubuntu系统下的ROS串口DMA通信,传输底盘速度等信息 第六节 ROS计算和发布里程计 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 系列文章目录 前言 1.pwm与电机正反转 2.编码器初始化 二、pwm控制与编码器使用 1.pwm使电机 .

LanLanLan1927的博客 1万+

从理论到实例:双工通讯协议 Websocket

作者 | 西格玛的博客链接 | http://lrwinx.github.io/本篇文章以 websocket 的原理和落地为核心,来叙述 websocket 的使用,以及相关应用场景。...

公众号:Java后端 330

如何使用Dify 搭建一个用自然语言查询数据库的 Agent?

这样就会发布完毕。

AI Agent 首席体验官 1698

websocket通讯协议详解

Websocket通讯协议详解

哂笑年少 1427

YOLOv8改进 | 融合篇,YOLOv8改进主干网络为MobileNetV3+CA注意机制+添加小目标检测层(全网独家首发,实现极限涨点)

小目标检测难点众多,导致很多算法对小目标的检测效果远不如大中型目标。影响算法性能的主要原因如下:第一,小目标分辨率低、信息量不足,导致神经网络提取到的有效特征较少。第二,小目标在图像中所占的区域较小,易受背景干扰,这对算法的定位性能要求较高。第三,小物体标注困难,训练数据有限,导致模型泛化能力差。例如多尺度学习、无锚机制和生成对抗学习等方法,都能提高小目标检测的准确性和鲁棒性。为了改善小目标漏检现象严重的问题,我在 YOLOv8 中增加了 P2 检测头,相当于有四个检测头,使网络能检测到更小的目标。

在职AI算法工程师,擅长计算机视觉,YOLO目标检测、分割等,擅长web、pyqt界面可视化,好内容持续更新中,来这里跟大家一起学习,共同进步 2857

RAG测评实战代码

本文主要介绍了RAG测评实战代码,希望对使用RAG的同学们有所帮助。 文章目录 1. 前言 2. 实战代码 2.1 实战代码一 2.2 实战代码二

herosunly的博客 1万+

HTTP之SSE技术

SSE简介 SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。 严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开,SSE 使用的就是这种原理。 SSE 是 Server-Sent Events 的简称, 是一种服务器端到客户端(浏览器)的单项消息推送。 相比于 WebSocket,SSE 简单不少,服务器端和客户端工做量都要小不少、简单不少,同时实现的功能也有局限。 SS

yzpbright的博客 2万+

SAP B1中文培訓教材_SDK

公司内部培训资料,对开发工具 ,DIAPI, UIAPI,Add-ON有详细解说

源码角度,分析OkHttp实现WebSocket | 握手/保活/数据处理...

wolfcstech | 作者 承香墨影 | 校对 https://www.wolfcstech.com/2017/02/23/OkHttp实现分析之Websocket | 原文 前段时间写了篇文章,讲解了如何使用 OkHttp 实现 WebSocket 通信,但是只停留在一些使用和理论上,分享出来各项数据还不错。本来想继续写一篇 OkHttp 关于 WebSocke...

AndroidAlvin的博客 492

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

python server client_怎么用Python做Server-sent Events Client

SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开,SSE 使用的就是这种原理。一、SSE 能做什么?理论上, SSE 和 WebSocket 做的是同一件事情。当你需要用新数据局部更新网络应用时,SSE 可以做到...

weixin_39552204的博客 1038

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

SSE技术详解:使用 HTTP 做服务端数据推送应用的技术

SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。   严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开,SSE 使用的就是这种原理。 一、SSE 能做什么?   理论上, SSE 和 WebSocket 做的是同一件事情。当你需要用新数据局部更新网络应用时,...

ygunoil的博客 182

8种流行网络协议(非常详细)零基础入门到精通,收藏这一篇就够了

压箱底的好资料,全面地介绍网络安全的基础理论,包括逆向、八层网络防御、汇编语言、白帽子web安全、密码学、网络安全协议等,将基础理论和主流工具的应用实践紧密结合,有利于读者理解各种主流工具背后的实现机制。采用QUIC协议作为传输层协议:相比TCP+TLS的组合,QUIC可以减少连接建立的时延,提高数据传输的效率。定义:HTTPS是以安全为目标的HTTP通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。定义:HTTP/3是一种新的网络协议,旨在提供更快的互联网连接速度和更高的安全性。

QXXXD的博客 818

从零打造3D打印无线遥控机器人:机电分离设计与ESP32实践

在嵌入式系统与机器人开发领域,机电分离设计是一种核心架构思想,它将机械执行机构与电子控制系统解耦,通过明确的接口协议进行交互。其原理在于将复杂的机器人系统划分为独立的机械模块和电子模块,机械部分专注于运动实现与结构承载,电子部分则负责感知、决策与信号控制。这种设计的技术价值在于大幅提升了系统的可维护性、可扩展性与并行开发效率,使得开发者可以独立优化机械结构或升级控制算法。在应用场景上,机电分离架构广泛应用于教育机器人、智能小车、开源硬件平台以及个人创客项目中,尤其适合与3D打印技术结合,快速迭代机械原型。本

weixin_33862993的博客 399

应用重连时间_SSE技术详解:使用 HTTP 做服务端数据推送应用的技术

 SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。  严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开,SSE 使用的就是这种原理。一、SSE 能做什么?  理论上, SSE 和 WebSocket 做的是同一件事情。当你需要用新数据局部更新网络应用...

weixin_35318150的博客 1189

比server酱更好用的推送_怎么用Python做Server-sent Events Client

SSE ( Server-sent Events )是 WebSocket 的一种轻量代替方案,使用 HTTP 协议。  严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开,SSE 使用的就是这种原理。一、SSE 能做什么?  理论上, SSE 和 WebSocket 做的是同一件事情。当你需要用新数据局部更新网络应用时...

weixin_39898854的博客 494
上一篇: iPhone11 128G 抱回家!
下一篇: Spring Boot 这么火,常用注解和原理都给你整理好了!
Java思维导图
博客等级 码龄10年 306粉丝 71原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值