热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

SpringChapter4WebSocket胡乱翻译(二)

书接上文,SpringChapter4WebSocket胡乱翻译(一)4.4.4.消息流一旦暴露了STOMP端点,Spring应用程序就成为连接客户端的STOMP代理。本节介绍服务

书接上文,Spring Chapter4 WebSocket 胡乱翻译 (一)


4.4.4. 消息流

一旦暴露了STOMP端点,Spring应用程序就成为连接客户端的STOMP代理。 本节介绍服务器端的消息流。

Spring-messaging模块包含对源自Spring Integration的消息传递应用程序的基础支持,后来被提取并整合到Spring Framework中,以便在许多Spring项目和应用程序场景中得到更广泛的使用。 下面列出了一些可用的消息传递抽象:



  • Message - 包含标头和内容的消息的简单表示。

  • MessageHandler - 处理消息的合同。

  • MessageChannel - 发送消息的合同,该消息允许生成者和使用者之间的松散耦合。

  • SubscribableChannel - MessageChannel和MessageHandler订阅者。

  • ExecutorSubscribableChannel - 使用Executor传递消息的SubscribableChannel。

@EnableWebSocketMessageBroker使用上面的组件来实现消息的工作流。

下图示意了spring自带的简单消息代理:

“clientInboundChannel” - 用于传递从WebSocket客户端收到的消息。
“clientOutboundChannel” - 用于将服务器消息发送到WebSocket客户端。
“brokerChannel” - 用于从服务器端的应用程序代码向消息代理发送消息。

 

下图显示了Spring使用外部的代理,比如ActiveMQ:

当从WebSocket连接接收消息时,它们被解码为STOMP帧,然后变为Spring消息表示,并发送到“clientInboundChannel”以进行进一步处理。 例如,目标头以“/ app”开头的STOMP消息可以被路由到带注释的控制器中的@MessageMapping方法,而“/ topic”和“/ queue”消息可以直接路由到消息代理。

处理来自客户端的STOMP消息的带注释的@Controller可以通过“brokerChannel”向消息代理发送消息,并且代理将通过“clientOutboundChannel”将消息广播给匹配的订户。 相同的控制器也可以响应HTTP请求执行相同的操作,因此客户端可以执行HTTP POST,然后@PostMapping方法可以向消息代理发送消息以向订阅的客户端广播。

让我们通过一个简单的例子来追踪消息流。 鉴于以下服务器设置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app");
registry.enableSimpleBroker("/topic");
}
}
@Controller
public class GreetingController {
@MessageMapping("/greeting"){
public String handle(String greeting) {
return "[" + getTimestamp() + ": " + greeting;
}
}

1.客户端连接到"http://localhost:8080/portfolio",一旦建立了WebSocket连接,STOMP帧就会开始流动。
2.客户端发送带有目标头"/topic/greeting"的SUBSCRIBE帧。一旦接收并解码,该消息就被发送到“clientInboundChannel”,然后被路由到存储客户端订阅的消息代理。
3.客户端将SEND帧发送到"/app/greeting"。 "/app"前缀有助于将其路由到带注释的控制器。删除“/ app”前缀后,目标的剩余“/ greeting”部分将映射到GreetingController中的@MessageMapping方法。
4.从GreetingController返回的值变为Spring消息,其中消息内容基于返回值和默认目标头"/topic/greeting"(从输入目标派生,"/app"替换为"/topic" )。生成的消息将发送到"brokerChannel" 并由消息代理处理。
5.消息代理找到所有匹配的订户,并通过“clientOutboundChannel”向每个订户发送MESSAGE帧,消息被编码为STOMP帧并在WebSocket连接上发送。


4.4.5. Annotated Controllers

应用程序可以使用带@Controller注释的类来处理来自客户端的消息。 这些类可以声明@MessageMapping,@ SubscribeMapping和@ExceptionHandler方法,如下所述。


@MessageMapping

@MessageMapping注释可用于根据目标路由消息的方法。 它在方法级别和类型级别受支持。 在类型级别,@ MessessMapping用于表示控制器中所有方法的共享映射。

默认情况下,目标映射应为Ant样式,路径模式,例如, “/foo *”,“/foo/**”。 模式包括对模板变量的支持,例如 “/foo /{id}”,可以使用@DestinationVariable方法参数引用。

当@MessageMapping方法返回一个值时,默认情况下,该值通过配置的MessageConverter序列化为有效负载,然后作为消息发送到“brokerChannel”,从那里向用户广播。 出站消息的目的地与入站消息的目的地相同,但前缀为“/ topic”。

您可以使用@SendTo方法批注来自定义将消息内容发送到的目标。 @SendTo也可以在类级别使用,以共享发送消息的默认目标。 @SendToUser是仅向与消息关联的用户发送消息的变体。 有关详细信息,请参阅用户目标。

@MessageMapping方法的返回值可以用ListenableFuture,CompletableFuture或CompletionStage包装,以便异步生成消息内容。

作为从@MessageMapping方法返回消息内容的替代方法,您还可以使用SimpMessagingTemplate发送消息,这也是在封面下处理返回值的方式。 请参阅发送消息。


@SubscribeMapping

@SubscribeMapping类似于@MessageMapping,但仅将映射缩小为订阅消息。 它支持与@MessageMapping相同的方法参数。 但是对于返回值,默认情况下,消息通过“clientOutboundChannel”直接发送到客户端以响应订阅,而不是通过“brokerChannel”作为对匹配订阅的广播发送给代理。 添加@SendTo或@SendToUser会覆盖此行为并发送给代理。

什么时候有用? 假设应用程序控制器映射到“/app”时代理映射到“/topic”和“/queue”。 在此设置中,代理将所有订阅存储到旨在用于重复广播的“/topic”和“/queue”,并且不需要应用程序参与。 客户端还可以订阅一些“/app”目的地,并且控制器可以返回响应该订阅的值而不涉及代理,实际上是一次性的请求 - 回复交换,而无需再次存储或使用订阅。 一种情况是在启动时使用初始数据填充UI。

什么时候这没用? 不要尝试将代理和控制器映射到相同的目标前缀,除非您希望由于某种原因单独处理消息(包括订阅)。 入站消息是并行处理的。 无法保证代理或控制器是否将首先处理给定的消息。 如果在存储订阅并准备好广播时通知目标,则客户端应该在服务器支持时询问收据(简单代理不支持)。 例如,使用Java STOMP Client:

@Autowired
private TaskScheduler messageBrokerTaskScheduler;
// During initialization..
stompClient.setTaskScheduler(this.messageBrokerTaskScheduler);
// When subscribing..
StompHeaders headers = new StompHeaders();
headers.setDestination("/topic/...");
headers.setReceipt("r1");
FrameHandler handler = ...;
stompSession.subscribe(headers, handler).addReceiptTask(() -> {
// Subscription ready...
});

 

一个服务器端的选择是在brokerChannel上注册ExecutorChannelInterceptor,并实现在处理完消息(包括订阅)后调用的afterMessageHandled方法。


@MessageExceptionHandler

应用程序可以使用@MessageExceptionHandler方法来处理来自@MessageMapping方法的异常。 感兴趣的异常可以在注释本身中声明,或者如果要获取对异常实例的访问权限,则可以通过方法参数声明:

@Controller
public class MyController {
// ...
@MessageExceptionHandler
public ApplicationError handleException(MyException exception) {
// ...
return appError;
}
}

@MessageExceptionHandler方法支持灵活的方法签名,并支持与@MessageMapping方法相同的方法参数类型和返回值。

通常,@ MessessExceptionHandler方法在声明它们的@Controller类(或类层次结构)中应用。如果您希望这些方法在控制器之间全局应用更多,则可以在标有@ControllerAdvice的类中声明它们。 这与Spring MVC中的类似支持相当。


4.4.6. 发送消息

如果要从应用程序的任何部分向连接的客户端发送消息,该怎么办? 任何应用程序组件都可以向“brokerChannel”发送消息。 最简单的方法是注入一个SimpMessagingTemplate,并使用它来发送消息。 通常,应该很容易按类型注入,例如:

@Controller
public class GreetingController {
private SimpMessagingTemplate template;
@Autowired
public GreetingController(SimpMessagingTemplate template) {
this.template = template;
}
@RequestMapping(path = "/greetings", method = POST)
public void greet(String greeting) {
String text = "[" + getTimestamp() + "]:" + greeting;
this.template.convertAndSend("/topic/greetings", text);
}
}

但如果存在相同类型的另一个bean,它也可以通过其名称“brokerMessagingTemplate”进行限定。


4.4.7. Simple Broker

内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的连接客户端。 代理支持类似路径的目标,包括对Ant样式目标模式的订阅。


4.4.8. External Broker

简单的代理非常适合入门但仅支持STOMP命令的子集(例如,没有ack,收据等),依赖于简单的消息发送循环,并且不适合于群集。 作为替代方案,应用程序可以升级到使用功能齐全的消息代理。

检查STOMP文档以查找您选择的消息代理(例如RabbitMQ,ActiveMQ等),安装代理,并在启用STOMP支持的情况下运行它。 然后在Spring配置中启用STOMP代理中继而不是简单代理。

以下是启用功能齐全的代理的示例配置:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue");
registry.setApplicationDestinationPrefixes("/app");
}
}

 

上述配置中的“STOMP代理中继”是Spring MessageHandler,它通过将消息转发到外部消息代理来处理消息。 为此,它建立到代理的TCP连接,将所有消息转发给它,然后通过其WebSocket会话将从代理接收的所有消息转发给客户端。 从本质上讲,它充当“转发”,可以在两个方向上转发消息。

注意:请将io.projectreactor.ipc:reactor-netty和io.netty:netty-all dependencies添加到项目中以进行TCP连接管理。

此外,应用程序组件(例如,HTTP请求处理方法,业务服务等)也可以向代理中继发送消息,如发送消息中所述,以便向订阅的WebSocket客户端广播消息。

实际上,代理中继实现了健壮且可扩展的消息广播。


4.4.9. Connect to Broker

STOMP代理中继维护与代理的单个“系统”TCP连接。 此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。 您可以为此连接配置STOMP凭据,即STOMP帧登录和密码标头。 这在XML命名空间和Java配置中都显示为systemLogin / systemPasscode属性,默认值为guest / guest。

STOMP代理中继还为每个连接的WebSocket客户端创建单独的TCP连接。 您可以配置STOMP凭据以用于代表客户端创建的所有TCP连接。 它在XML命名空间和Java配置中作为clientLogin / clientPasscode属性公开,默认值为guest / guest。

提示:STOMP代理中继始终在每个CONNECT帧上设置登录和密码头,它代表客户端转发给代理。 因此,WebSocket客户端无需设置这些标头; 他们会被忽略。 正如身份验证部分所述,WebSocket客户端应该依赖HTTP身份验证来保护WebSocket端点并建立客户端身份。 

STOMP代理中继还通过“system”TCP连接向消息代理发送和接收心跳。 您可以配置发送和接收心跳的间隔(默认情况下每个10秒)。 如果与代理的连接丢失,代理中继将继续尝试每5秒重新连接一次,直到成功为止。

任何Spring bean都可以实现ApplicationListener ,以便在与代理的“系统”连接丢失并重新建立时接收通知。 例如,股票报价服务广播股票报价可以在没有活动的“system”连接时停止尝试发送消息。

默认情况下,STOMP代理中继始终连接,并在连接丢失时根据需要重新连接到同一主机和端口。 如果您希望提供多个地址,则在每次尝试连接时,您都可以配置地址供应商,而不是固定的主机和端口。 例如:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient());
registry.setApplicationDestinationPrefixes("/app");
}
private ReactorNettyTcpClient createTcpClient() {
Consumer> builderCOnsumer= builder -> {
builder.connectAddress(() -> {
// Select address to connect to ...
});
};
return new ReactorNettyTcpClient<>(builderConsumer, new StompReactorNettyCodec());
}
}

还可以使用virtualHost属性配置STOMP代理中继。 此属性的值将被设置为每个CONNECT帧的主机头,并且可能在例如云环境中有用,其中建立TCP连接的实际主机与提供基于云的STOMP服务的主机不同。


4.4.10 点作为分隔符

当消息路由到@MessageMapping方法时,它们与AntPathMatcher匹配,并且默认模式应使用斜杠“/”作为分隔符。 这是Web应用程序中的一个很好的约定,类似于HTTP URL。 但是,如果您更习惯于消息传递约定,则可以切换到使用点“.” 作为分隔符。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

// ...
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableStompBrokerRelay("/queue", "/topic");
registry.setApplicationDestinationPrefixes("/app");
}
}

之后,控制器可以使用点“.” 作为@MessageMapping方法中的分隔符:

@Controller
@MessageMapping("foo")
public class FooController {
@MessageMapping("bar.{baz}")
public void handleBaz(@DestinationVariable String baz) {
// ...
}
}

客户端现在可以向“/app/foo.bar.baz123”发送消息。

在上面的示例中,我们没有更改“代理中继”上的前缀,因为它们完全依赖于外部消息代理。 检查您正在使用的代理的STOMP文档页面,以查看它为目标标头支持的约定。

另一方面,“简单代理”确实依赖于配置的PathMatcher,因此如果您切换也将应用于代理的分隔符,并且将消息中的目标与订阅中的模式匹配。


4.4.11认证 (Authentication)

WebSocket消息传递会话中的每个STOMP都以HTTP请求开始 - 可以是升级到WebSockets的请求(即WebSocket握手),或者在SockJS回退一系列SockJS HTTP传输请求的情况下。

Web应用程序已经具有用于保护HTTP请求的身份验证和授权。 通常,用户通过Spring Security使用某种机制(例如登录页面,HTTP基本身份验证或其他)进行身份验证。 经过身份验证的用户的安全上下文保存在HTTP会话中,并与同一个基于COOKIE的会话中的后续请求相关联。

因此,对于WebSocket握手或SockJS HTTP传输请求,通常已经存在可通过HttpServletRequest#getUserPrincipal()访问的经过身份验证的用户。 Spring自动将该用户与为其创建的WebSocket或SockJS会话相关联,随后通过用户头与该会话上传输的所有STOMP消息相关联。

简而言之,为了安全性,典型的Web应用程序不需要做任何其他特殊的事情。 用户在HTTP请求级别进行身份验证,并通过基于COOKIE的HTTP会话维护安全上下文,然后将该会话与为该用户创建的WebSocket或SockJS会话相关联,并在流经应用程序的每个Message上标记用户标头。

请注意,STOMP协议在CONNECT帧上确实有“登录”和“密码”标头。 这些最初设计用于并且仍然需要例如用于TCP上的STOMP。 但是,对于STOMP over WebSocket,Spring默认忽略STOMP协议级别的授权标头,并假定用户已在HTTP传输级别进行了身份验证,并期望WebSocket或SockJS会话包含经过身份验证的用户。

提示:Spring Security提供WebSocket子协议授权,该授权使用ChannelInterceptor根据其中的用户头来授权消息。 此外,Spring Session还提供WebSocket集成,以确保在WebSocket会话仍处于活动状态时,用户HTTP会话不会过期。


4.4.12令牌认证

Spring Security OAuth支持基于令牌的安全性,包括JSON Web Token(JWT)。 这可以用作Web应用程序中的身份验证机制,包括STOMP over WebSocket交互,正如上一节所述,即通过基于COOKIE的会话维护身份。

同时,基于COOKIE的会话并不总是最适合,例如在压根不希望维护服务器端会话的应用程序中,或者在通常使用标头进行身份验证的手机应用程序中。

WebSocket协议RFC 6455“没有规定服务器在WebSocket握手期间可以对客户端进行身份验证的任何特定方式。” 实际上,浏览器客户端只能使用标准身份验证标头(即基本HTTP身份验证)或COOKIE,并且不能提供自定义标头。 同样,SockJS Javascript客户端没有提供使用SockJS传输请求发送HTTP标头的方法,请参阅sockjs-client问题196.相反,它确实允许发送可用于发送令牌但具有其自身缺点的查询参数,例如 因为令牌可能无意中使用服务器日志中的URL进行了记录。

提示:以上限制适用于基于浏览器的客户端,不适用于基于Spring Java的STOMP客户端,该客户端支持使用WebSocket和SockJS请求发送头文件

因此,希望避免使用COOKIE的应用程序可能无法在HTTP协议级别进行身份验证。 他们可能更喜欢在STOMP消息传递协议级别使用标头进行身份验证,而不是使用COOKIE。有两个简单的步骤可以做到这一点:



  1. 使用STOMP客户端在连接时传递身份验证标头。

  2. 使用ChannelInterceptor处理身份验证标头。

下面是注册自定义身份验证拦截器的示例服务器端配置。 请注意,拦截器只需要在CONNECT消息上进行身份验证并设置用户头。 Spring将记录并保存经过身份验证的用户,并将其与同一会话中的后续STOMP消息相关联:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new ChannelInterceptorAdapter() {
@Override
public Message preSend(Message message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
Authentication user = ...; // access authentication header(s)
accessor.setUser(user);
}
return message;
}
});
}
}

还要注意,当使用Spring Security的消息授权时,目前您需要确保在Spring Security之前申请认证ChannelInterceptor配置。 最好通过在自己的标记为@Order(Ordered.HIGHEST_PRECEDENCE + 99)的WebSocketMessageBrokerConfigurer实现中声明自定义拦截器来完成。


4.4.13 用户目的地

应用程序可以发送针对特定用户的消息,Spring的STOMP支持可识别以“/user/”为前缀的目标。 例如,客户端可能订阅目标“/user/queue/position-updates”。 该目的地将由UserDestinationMessageHandler处理并转换为用户会话唯一的目的地,例如,“/queue/position-updates-user123”。 这提供了订阅一般命名的目的地的便利性,同时确保不与订阅相同目的地的其他用户发生冲突,使得每个用户可以接收唯一的库存位置更新。

在发送方,消息可以发送到目的地,例如“/user/{username}/queue/position-updates”,然后由UserDestinationMessageHandler将其转换为一个或多个目的地,每个目的地对应用户会话。 这允许应用程序中的任何组件发送针对特定用户的消息,而不必知道除其名称和通用目标之外的任何内容。 通过注释和消息传递模板也支持这一点。

例如,消息处理方法可以向与通过@SendToUser注释处理的消息相关联的用户发送消息(在类级别上也支持共享公共目的地):

@Controller
public class PortfolioController {
@MessageMapping("/trade")
@SendToUser("/queue/position-updates")
public TradeResult executeTrade(Trade trade, Principal principal) {
// ...
return tradeResult;
}
}

如果用户有多个会话,则默认情况下,所有订阅给定目标的会话都是目标。 但是,有时可能需要仅定位发送正在处理的消息的会话。 这可以通过将broadcast属性设置为false来完成,例如:

@Controller
public class MyController {
@MessageMapping("/action")
public void handleAction() throws Exception {
// raise MyBusinessException here
}
@MessageExceptionHandler
@SendToUser(destinatiOns= "/queue/errors", broadcast = false)
public ApplicationError handleException(MyBusinessException exception) {
// ...
return appError;
}
}

虽然用户目的地通常意味着经过身份验证的用户,但并不严格要求。 与经过身份验证的用户无关的WebSocket会话可以订阅用户目标。 在这种情况下,@ SendToUser注释的行为与broadcast = false完全相同,即仅针对发送正在处理的消息的会话。

例如,通过注入由Java配置或XML命名空间创建的SimpMessagingTemplate,也可以从任何应用程序组件向用户目标发送消息(如果需要使用@Qualifier进行限定,则bean名称为“brokerMessagingTemplate”):

@Service
public class TradeServiceImpl implements TradeService {
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
// ...
public void afterTradeExecuted(Trade trade) {
this.messagingTemplate.convertAndSendToUser(trade.getUserName(), "/queue/position-updates", trade.getResult());
}
}

提示:将用户目标与外部消息代理一起使用时,请检查代理文档,了解如何管理非活动队列,以便在用户会话结束时删除所有唯一用户队列。 例如,当使用/exchange/amq.direct/position-updates等目标时,RabbitMQ会创建自动删除队列。 因此,在这种情况下,客户端可以订阅/user/exchange/amq.direct/position-updates。 同样,ActiveMQ具有用于清除非活动目标的配置选项。

在多应用程序服务器方案中,用户目标可能仍未解析,因为用户连接到不同的服务器。 在这种情况下,您可以配置目标以广播未解析的消息,以便其他服务器有机会尝试。 这可以通过Java config中的MessageBrokerRegistryuserDestinationBroadcast属性和XML中的message-broker元素的userdestination-broadcast属性来完成。


4.4.14消息顺序

来自代理的消息将发布到“clientOutboundChannel”,从那里将它们写入WebSocket会话。 由于通道由ThreadPoolExecutor支持,因此消息在不同的线程中处理,并且客户端接收的结果序列可能与发布的确切顺序不匹配。

如果这是一个问题,请启用以下标志:

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {
@Override
protected void configureMessageBroker(MessageBrokerRegistry registry) {
// ...
registry.setPreservePublishOrder(true);
}
}

设置标志后,同一客户端会话每次只发送一个消息发布到“clientOutboundChannel”,以便保证发布顺序。 请注意,这会导致额外性能开销,因此仅在需要时才启用它。


4.4.15. 事件

发布了几个ApplicationContext事件(如下所列),可以通过实现Spring的ApplicationListener接口来接收它们。



  • BrokerAvailabilityEvent

  • SessionConnectEvent

  • SessionConnectedEvent

  • SessionSubscribeEvent

  • SessionUnsubscribeEvent

  • SessionDisconnectEvent

注意:使用功能齐全的代理时,STOMP“代理中继”会自动重新连接“system”连接,以防代理暂时不可用。 但是,客户端连接不会自动重新连接。 假设启用了心跳,客户端通常会注意到代理在10秒内没有响应。 客户端需要实现自己的重新连接逻辑。


4.4.16拦截

事件提供STOMP连接生命周期的通知,而不是每个客户端消息的通知。 应用程序还可以注册ChannelInterceptor来拦截任何消息,以及处理链的任何部分。 例如,拦截来自客户端的入站消息:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.setInterceptors(new MyChannelInterceptor());
}
}

自定义的ChannelInterceptor可以使用StompHeaderAccessor或SimpMessageHeaderAccessor来访问有关消息的信息。

public class MyChannelInterceptor implements ChannelInterceptor {
@Override
public Message preSend(Message message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getStompCommand();
// ...
return message;
}
}

应用程序还可以实现ExecutorChannelInterceptor,它是ChannelInterceptor的子接口,在处理消息的线程中具有回调。 虽然为发送到通道的每个消息调用一次ChannelInterceptor,但ExecutorChannelInterceptor在订阅来自通道的消息的每个MessageHandler的线程中提供挂钩。

请注意,就像上面的SesionDisconnectEvent一样,可能已从客户端发送DISCONNECT消息,或者也可能在WebSocket会话关闭时自动生成。 在某些情况下,拦截器可能会在每个会话中多次拦截此消息。 对于多个断开连接事件,组件应该是幂等的。


4.4.17 STOMP客户端

Spring可以通过WebSocket客户端或者通过TCP客户端这2种方式提供STOMP。
要开始创建和配置WebSocketStompClient:

WebSocketClient webSocketClient = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient);
stompClient.setMessageConverter(new StringMessageConverter());
stompClient.setTaskScheduler(taskScheduler); // for heartbeats

在上面的示例中,StandardWebSocketClient可以替换为SockJsClient,因为它也是WebSocketClient的实现。 SockJsClient可以使用WebSocket或基于HTTP的传输作为后备。 有关更多详细信息,请参阅SockJsClient。

接下来建立连接并为STOMP会话提供处理程序:

String url = "ws://127.0.0.1:8080/endpoint";
StompSessionHandler sessiOnHandler= new MyStompSessionHandler();
stompClient.connect(url, sessionHandler);

当会话准备好使用时,会通知处理程序:

public class MyStompSessionHandler extends StompSessionHandlerAdapter {
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
// ...
}
}

建立会话后,可以发送任何消息内容,并使用配置的MessageConverter对其进行序列化:

session.send("/topic/foo", "payload");

您也可以订阅目的地。 订阅方法需要处理订阅消息的处理程序,并返回可用于取消订阅的订阅句柄。 对于每个收到的消息,处理程序可以指定消息内容应该反序列化的目标对象类型:

session.subscribe("/topic/foo", new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return String.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
// ...
}
});

要启用STOMP心跳,请使用TaskScheduler配置WebSocketStompClient,并可选择自定义心跳间隔,写入不活动10秒,导致发送心跳,10秒读取不活动,关闭连接。

STOMP协议还支持收据,其中客户端必须添加“收据”标头,服务器在处理发送或订阅后用RECEIPT帧响应。 为了支持这一点,StompSession提供了setAutoReceipt(boolean),它导致在每个后续发送或订阅时添加“收据”标头。 或者,您也可以手动将“收据”标题添加到StompHeaders。 发送和订阅都返回一个Receiptable实例,可用于注册接收成功和失败回调。 对于此功能,客户端必须配置TaskScheduler和收据到期前的时间(默认为15秒)。

请注意,StompSessionHandler本身是一个StompFrameHandler,它允许它处理ERROR帧以及处理消息的异常的handleException回调,以及包含ConnectionLostException的传输级错误的handleTransportError。


4.4.18. WebSocket Scope

每个WebSocket会话都有一个属性映射。 映射作为标头附加到入站客户端消息,并且可以从控制器方法访问,例如:

@Controller
public class MyController {
@MessageMapping("/action")
public void handle(SimpMessageHeaderAccessor headerAccessor) {
Map attrs = headerAccessor.getSessionAttributes();
// ...
}
}

也可以在websocket范围中声明一个Spring管理的bean。 WebSocket范围的bean可以注入控制器和“clientInboundChannel”上注册的任何通道拦截器。 这些通常是单例,比任何单独的WebSocket会话都更长寿。 因此,您需要为WebSocket范围的bean使用范围代理模式:

@Component
@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyBean {
@PostConstruct
public void init() {
// Invoked after dependencies injected
}
// ...
@PreDestroy
public void destroy() {
// Invoked when the WebSocket session ends
}
}
@Controller
public class MyController {
private final MyBean myBean;
@Autowired
public MyController(MyBean myBean) {
this.myBean = myBean;
}
@MessageMapping("/action")
public void handle() {
// this.myBean from the current WebSocket session
}
}

与任何自定义作用域一样,Spring在第一次从控制器访问时初始化一个新的MyBean实例,并将该实例存储在WebSocket会话属性中。 随后返回相同的实例,直到会话结束。 WebSocket范围的bean将调用所有Spring生命周期方法,如上面的示例所示。


4.4.19. Performance


4.4.20. Monitoring


4.4.21. Testing

 


推荐阅读
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 本文详细介绍了在Linux虚拟化部署中进行VLAN配置的方法。首先要确认Linux系统内核是否已经支持VLAN功能,然后配置物理网卡、子网卡和虚拟VLAN网卡的关系。接着介绍了在Linux配置VLAN Trunk的步骤,包括将物理网卡添加到VLAN、检查添加的VLAN虚拟网卡信息以及重启网络服务等。最后,通过验证连通性来确认配置是否成功。 ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 本文介绍了Java高并发程序设计中线程安全的概念与synchronized关键字的使用。通过一个计数器的例子,演示了多线程同时对变量进行累加操作时可能出现的问题。最终值会小于预期的原因是因为两个线程同时对变量进行写入时,其中一个线程的结果会覆盖另一个线程的结果。为了解决这个问题,可以使用synchronized关键字来保证线程安全。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了在iOS开发中使用UITextField实现字符限制的方法,包括利用代理方法和使用BNTextField-Limit库的实现策略。通过这些方法,开发者可以方便地限制UITextField的字符个数和输入规则。 ... [详细]
author-avatar
Mr_XieZhiQ
无表面兄弟,不编程!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有