spring boot2 (32)-WebSocket和stomp消息

WebSocket可实现浏览器和服务器之间的通信,如在线聊天,消息推送等,其基于tcp协议来传输数据。而stomp是一种更高级的协议,可以更加方便的实现WebSocket。

broker和客户端

客户端可以是任何语言,如js,php等,只须使用stomp协议来收发消息,broker可对消息进行处理或转发等。本篇将介绍以spring boot实现broker,以js实现客户端。

pom.xml

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

配置消息端点

spring boot会自动配置好broker,我们只需添加端点如/endpoint1,客户端通过端点建立连接。

@SpringBootConfiguration
@EnableWebSocketMessageBroker	//自动配置Broker
public class WebSocketConfig  implements WebSocketMessageBrokerConfigurer  {

    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint1").withSockJS();
    }

消息广播

java代码:每当请求/sent时向/topic1发送消息hello,world,所有订阅了/topic1的客户端都会收到这个消息。

    @Autowired
    private SimpMessagingTemplate messagingTemplate;
    
    @GetMapping("/sent")
    public void sent(HttpSession session) {
    	messagingTemplate.convertAndSend("/topic1", "hello,world"); 
    }
js代码:向/endpoint1端点建立连接,同时订阅/topic1的消息,并弹出窗口,显示消息。中间有个{}传参后面再讲。
<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">
var socket = new SockJS('/endpoint1'); 
var stompClient = Stomp.over(socket);
stompClient.connect({}, function() {
    stompClient.subscribe('/topic1', function(respnose){
    	alert(respnose.body);
    });
});
启动项目,首先打开js的页面即会建立连接,每当服务器收到/sent请求便会向topic1发消息,页面会自己弹出hello,world。

消息接收 

java代码:当客户端向/hello发消息时,在服务器控制台打印消息

    @MessageMapping("/hello")
    public void hello(String message) {
    	    System.out.println(message);
html代码:一个按钮,用来触发发消息事件
<button onclick="sendTest();">发消息</button>
js代码:加到前面js代码中,向/hello发消息,"test"会传给后台的message。这里也有个{}和connect中的{}是一样的作用,用来传递头部参数,后面会讲。
function sendTest() {
    stompClient.send("/hello", {}, "test");
}

权限拦截器

修改端点配置代码,设置拦截器。每当客户端连接端点时都会被拦截,在这里可以读取到session,进行验证。和mvc拦截器类似,参考第19篇。当连接建立以后,通过该连接收发消息就不需要再次验证了。

    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint1").withSockJS()
        .setInterceptors(new HandshakeInterceptor(){

			@Override
			public void afterHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2,
					Exception arg3) {
			}

			@Override
			public boolean beforeHandshake(ServerHttpRequest arg0, ServerHttpResponse arg1, WebSocketHandler arg2,
					Map<String, Object> arg3) throws Exception {
				//拿到session,自己完成验证代码
				HttpSession session = getSession(arg0);
				return true;
			}
			
			private HttpSession getSession(ServerHttpRequest request) {
				if (request instanceof ServletServerHttpRequest) {
					ServletServerHttpRequest serverRequest = (ServletServerHttpRequest) request;
					return serverRequest.getServletRequest().getSession();
				}
				return null;
			}
        }); 请求
    }

请求拦截器

上面客户端建立连接时,可以拿到session。而一但连接建立成功,就可以直接收发消息,但是无法拿到session或request。此时可以使用Principal来识别 每条消息的用户来源。

在前面的WebSocketConfig类中加入以下方法,客户端建立连接和收发消息时都会被它拦截,不影响上面的权限拦截器。这段代码意思是:当用户建立连接时,将前面说的{}中的参数username存入Principa。以后每次客户端发消息时,都可以从Principa读取到其username

   @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
    	//注册拦截器
        registration.interceptors(new ChannelInterceptorAdapter() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                //判断客户端发出的命令是不是CONNECT
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                	//这里的username参数就是从前面的{}中传过来的
                    Principal user =  new MyPrincipal(accessor.getNativeHeader("username").get(0));
                    //类似于设置session,在控制层获取该参数时,会调用下面的MyPrincipal.getName(),下面会讲
                    accessor.setUser(user);
                }
                //拦截处理完后转发message,如果不允许该消息,可以返回null
                return message;
            }
        });
    }
    //内部类实现Principal接口
	class MyPrincipal implements Principal{
		private String name;
		public MyPrincipal(String name){
			this.name=name;
		}
		@Override
		public String getName() {
			return name;
		}
	}
修改/hello接收消息方法的参数,以下的principal.getName()就是上面的MyPrincipal.getName(),可获取username参数值
    @MessageMapping("/hello")
	public void hello(Principal principal,String message) {
    	System.out.println(principal.getName());
修改js中的{}传参如下,对应拦截器中的accessor.getNativeHeader("username")
stompClient.connect({username:"tom"}, function() {
此时,客户端连接后,每次点击按钮向/hello发消息时,都会打印出其username,以此确认消息来源用户。

点对点消息

点对点即向特定用户发消息,上面已经确认消息来源,现在只需要确认特定的消息接收者。
修改hello方法,将tom发过来的message单独发给jerry,而不再是公共广播
    @MessageMapping("/hello")
	public void hello(Principal principal,String message) {
    	messagingTemplate.convertAndSendToUser("jerry","/topic1", principal.getName()+"say:"+message); 
修改js,注意固定语法/user/jerry,指只订阅user用户jerry的消息
	stompClient.subscribe('/user/jerry/topic1', function(respnose){
    	alert(respnose.body);
    });
此时,点击消息按钮时,将弹出来自tom的消息。这里发消息和收消息都在一个页面,出于方便我把username写死了,实际可以动态设置用户名,不同用户互发消息,不解释了。

猜你喜欢

转载自blog.csdn.net/wangb_java/article/details/80718401