在上一篇文章( www.zifangsky.cn/1355.html )中我介绍了在Spring项目中使用WebSocket的几种实现方式。但是,上篇文章中只介绍了服务端采用广播模式给所有客户端发送消息,然而我们有时需要服务端给指定用户的客户端发送消息(比如:发送Web通知、实时打印用户任务的日志、两个用户点对点聊天等)。
关于服务端如何给指定用户的客户端发送消息,一般可以通过以下三种方案来实现:
方案一 :WebSocket使用“Java提供的@ServerEndpoint注解”实现或者使用“Spring低层级API”实现,在建立连接时从 HttpSession 中获取用户登录后的用户名,然后把“ 用户名+该WebSocket连接 ”存储到 ConcurrentHashMap 。给指定用户发送消息,只需要 根据接收者的用户名获取对方已经建立的WebSocket连接,接着给他发送消息即可 。
方案二 :在页面的监听路径前面动态添加当前登录的“ 用户ID/用户名 ”,这样给指定用户发送消息,只需要发送广播消息到监听了前面那个路径的客户端即可。
方案三 :这种方案类似于方案一。使用Spring的高级API实现WebSocket,然后自定义 HandshakeHandler 类并重写 determineUser 方法,其目的是为了在建立连接时使用用户登录后的用户名作为此次WebSocket的凭证,最后我们就可以使用 messagingTemplate.convertAndSendToUser 方法给指定用户发送消息了。
使用SimpMessagingTemplate发送消息
使用 org.springframework.messaging.simp.SimpMessagingTemplate 类可以在服务端的任意地方给客户端发送消息。此外,在我们配置Spring支持STOMP后 SimpMessagingTemplate 类就会被自动装配到Spring的上下文中,因此我们只需要在想要使用的地方使用 @Autowired 注解注入SimpMessagingTemplate即可使用。
需要说明的是, SimpMessagingTemplate 类有两个重要的方法,它们分别是:
public void convertAndSend(D destination, Object payload) :给监听了路径 destination 的所有客户端发送消息 payload
public void convertAndSendToUser(String user, String destination, Object payload) :给监听了路径 destination 的用户 user 发送消息 payload
一个简单示例:
package cn.zifangsky.stompwebsocket.controller;
import cn.zifangsky.stompwebsocket.model.websocket.Greeting;
import cn.zifangsky.stompwebsocket.model.websocket.HelloMessage;
import cn.zifangsky.stompwebsocket.service.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
/**
* 测试{@link org.springframework.messaging.simp.SimpMessagingTemplate}类的基本用法
* @author zifangsky
* @date 2018/10/10
* @since 1.0.0
*/
@Controller
@RequestMapping(("/wsTemplate"))
public class MessageTemplateController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private SimpMessagingTemplate messagingTemplate;
@Autowired
private SimpUserRegistry userRegistry;
@Resource(name = "redisServiceImpl")
private RedisService redisService;
/**
* 简单测试SimpMessagingTemplate的用法
*/
@PostMapping("/greeting")
@ResponseBody
public String greeting(@RequestBody Greeting greeting) {
this.messagingTemplate.convertAndSend("/topic/greeting", new HelloMessage("Hello," + greeting.getName() + "!"));
return "ok";
}
}
复制代码
很显然,这里发送的地址是上篇文章中最后那个示例监听的地址,在客户端页面建立连接后,我们使用 Postman 请求一下上面这个方法,效果如下:
然后我们可以发现页面中也收到消息了:
向指定用户发送WebSocket消息并处理对方不在线的情况
给指定用户发送消息:
如果接收者在线,则直接发送消息;
否则将消息存储到redis,等用户上线后主动拉取未读消息。
(1)自定义HandshakeInterceptor,用于禁止未登录用户连接WebSocket:
package cn.zifangsky.stompwebsocket.interceptor.websocket;
import cn.zifangsky.stompwebsocket.common.Constants;
import cn.zifangsky.stompwebsocket.common.SpringContextUtils;
import cn.zifangsky.stompwebsocket.model.User;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpSession;
import java.text.MessageFormat;
import java.util.Map;
/**
* 自定义{@link org.springframework.web.socket.server.HandshakeInterceptor},实现“需要登录才允许连接WebSocket”
*
* @author zifangsky
* @date 2018/10/