webSocket的概念在这里就不多阐述了,网上有很多大家可以自行搜索。本篇博客主要是实现webSocket在多服务器下的点对点推送实现
直接进入代码实现
import com.cjh.websocket.socket.vo.User;
import com.cjh.websocket.util.WebsocketMapUtil;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
import java.security.Principal;
import java.util.List;
import java.util.Map;
@Slf4j
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/user", "/message");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket")
.addInterceptors(myHandshakeInterceptor()) //添加 websocket握手拦截器
.setHandshakeHandler(myDefaultHandshakeHandler()) //添加 websocket握手处理器
.setAllowedOrigins("*")
.withSockJS();
}
/**
* WebSocket 握手拦截器
* 可做一些用户认证拦截处理
*/
private HandshakeInterceptor myHandshakeInterceptor() {
return new HandshakeInterceptor() {
/**
* websocket握手连接
* @return 返回是否同意握手
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
}
};
}
//WebSocket 握手处理器
private DefaultHandshakeHandler myDefaultHandshakeHandler() {
return new DefaultHandshakeHandler() {
@Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
//设置认证通过的用户到当前会话中
return (Principal) attributes.get("user");
}
};
}
/**
* 输入通道参数设置
*/
@Override
public void configureClientInboundChannel(ChannelRegistration channelRegistration) {
log.info("configureClientInboundChannel-start");
channelRegistration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
log.info("configureClientInboundChannel-accessor.getCommand():" + accessor.getCommand());
//判断前端socket发送过来操作
switch (accessor.getCommand()) {
case CONNECT: {
String authorization = accessor.getNativeHeader("Authorization").get(0);
User user = (User) JSONObject.toBean(JSONObject.fromObject(authorization), User.class);
log.info("configureClientInboundChannel-CONNECT:" + "user:" + user);
if (user == null) {
return null;
}
WebsocketMapUtil.putServerChannel(user);
//注册当前用户
accessor.setUser(new MyPrincipal(user));
return message;
}
case ABORT:
case DISCONNECT: {
MyPrincipal myPrincipal = (MyPrincipal) accessor.getHeader("simpUser");
log.info("configureClientInboundChannel-DISCONNECT:myPrincipal:" + myPrincipal.toString());
if (myPrincipal == null) {
return null;
}
WebsocketMapUtil.removeServerChannel(myPrincipal.user);
return message;
}
case SUBSCRIBE: {
return message;
}
default:
return null;
}
}
});
}
//重新实现Principal接口,主要重写getName方法。点对点推送就是采用这个getName方法获取用户名
class MyPrincipal implements Principal {
private User user;
public MyPrincipal(User user) {
this.user = user;
}
@Override
public String getName() {
return user.getId();
}
}
@Override
public boolean configureMessageConverters(List<MessageConverter> list) {
return false;
}
}
有几个方法需要进行解释:
1.@EnableWebSocketMessageBroker注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思。
2.registerStompEndpoints方法表示注册STOMP协议的节点,并指定映射的URL。
3.stompEndpointRegistry.addEndpoint("/webSocket").withSockJS();这一行代码用来注册STOMP协议节点,同时指定使用SockJS协议。 中间增加了两个方法用于webSocket握手拦截器和处理器的认证处理。
4.configureMessageBroker方法用来配置消息代理,由于我们是实现推送功能,这里的消息代理是/app。config.enableSimpleBroker("/topic", "/user/*");表示以/topic和/user/*开头的url地址进行交互通信。
@Slf4j
@Component
public class SubThread extends Thread {
@Resource
protected JedisPool jedisPool;
@Autowired
Subscriber subscriber;
final public static String serverChannel = "server_channel";
@Override
public void run() {
log.info(String.format("subscribe redis, channel %s, thread will be blocked", serverChannel));
try (Jedis jedis = jedisPool.getResource()) {
jedis.subscribe(subscriber, serverChannel);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
}
@Component
public class CommandLine implements CommandLineRunner{
@Autowired
SubThread subThread;
@Override
public void run(String... args) {
subThread.start();
}
}
启动程序时就启动一条线程用于监听redis消息。
@Component
@AllArgsConstructor
public class Subscriber extends JedisPubSub {
private final SimpMessageSendingOperations simpMessageSendingOperations;
private static final String DESTINATION = "/message";
@Override
//监听到消息进行处理
public void onMessage(String channel, String messageJson) {
BaseSubPubBean baseSubPubBean = null;
try {
baseSubPubBean = (BaseSubPubBean) SerializationUtil.deserializeToObject(messageJson);
} catch (Exception e) {
e.printStackTrace();
}
if (baseSubPubBean == null) {
return;
}
switch (baseSubPubBean.getType()) {
case LOGIN:
String message = (String) baseSubPubBean.getData();
if (WebsocketMapUtil.inServerChannel(baseSubPubBean.getId())) {
simpMessageSendingOperations.convertAndSendToUser(
baseSubPubBean.getId(),
DESTINATION,
message);
}
break;
default:
break;
}
System.out.println("信息发送成功");
}
@Override
public void onSubscribe(String channel, int subscribedChannels) {
System.out.println(String.format("subscribe redis channel success, channel %s, subscribedChannels %d",
channel, subscribedChannels));
}
@Override
public void onUnsubscribe(String channel, int subscribedChannels) {
System.out.println(String.format("unsubscribe redis channel, channel %s, subscribedChannels %d",
channel, subscribedChannels));
}
}
获取到监听的消息进行判断,webSocket建立连接的是否本服务器。如果是则进行单点数据返回,不是则跳过。
点对点数据发送,convertAndSendToUser方法就是用于发送给指定用户数据。
<!DOCTYPE html>
<html>
<head>
<title>websocket</title>
<script src="//cdn.bootcss.com/angular.js/1.5.6/angular.min.js"></script>
<script src="https://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
/*<![CDATA[*/
var stompClient = null;
var app = angular.module('app', []);
// 定义客户端的认证信息,按需求配置
app.controller('MainController', function($rootScope, $scope, $http) {
$scope.data = {
//用户id
id : '',
//用户名
name : '',
//连接状态
connected : false,
//消息
message : '',
rows : []
};
//连接
$scope.connect = function() {
var socket = new SockJS('/websocket');
var headers = {
Authorization:'{"id":"' + $scope.data.id + '"}'
}
stompClient = Stomp.over(socket);
stompClient.connect(headers, function(frame) {
// 注册推送时间回调
stompClient.subscribe('/user/message', function(r) {
$scope.data.time = '当前服务器时间:' + r.body;
$scope.data.connected = true;
$scope.$apply();
});
$scope.data.connected = true;
$scope.$apply();
});
};
$scope.disconnect = function() {
if (stompClient != null) {
stompClient.disconnect();
}
$scope.data.connected = false;
}
});
</script>
</head>
<body ng-app="app" ng-controller="MainController">
<h2>websocket</h2>
id:<input type="text" ng-model="data.id" placeholder="只能为纯数字..." />
<br/>
<label>WebSocket连接状态:</label>
<button type="button" ng-disabled="data.connected" ng-click="connect()">连接</button>
<button type="button" ng-click="disconnect()" ng-disabled="!data.connected">断开</button>
<br />
<br />
<div ng-show="data.connected">
<label>{{data.time}}</label> <br /> <br />
</div>
</body>
</html>
简单的前端实现,这里需要注意的是convertAndSendToUser发送的地址是 /message,而前端需要进行通讯的地址是 /user/message。指定用户发送需要在地址前面增加 /user 路径。
代码在git上可以下载运行访问 http://localhost:8099/地址输入自己指定的 id 连接后,调用http://localhost:8099/send?id=?进行发送redis消息测试。其中redis消息服务可以替换成现在流程的mq消息服务,这里主要给大家提供一个多服务器下webSocket单点发送数据的思路,再在此基础上进行改造。