3

简单介绍

    WebSocket是为浏览器和服务端提供双工艺部通信功能一种工具,即浏览器可以先服务端发送消息,服务端也可以先浏览器发送消息。现在支持Websocket的浏览器有  IE10+,Crome13+,FileFox6+。

WebSocket子协议

    WebSocket只是一个消息传递的体系结构,没有指定任何的消息传递协议。与HTTP协议不同的是,WebSocket只是一个应用层的协议,它非常简单,并不能理解传入的消息,也不能对消息进行路由或处理,因此WebSocket协议只是一个应用层的协议,其上需要一个框架来理解和处理消息。

    Spring框架提供了对使用STOMP子协议的支持。STOMP,全称Streaming Text Orientated Message Protol,流文本定向协议。STOMP是一个简单的消息传递协议,是一种为MOM(Message Orientated  Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供了一个可操作的连接格式,允许STOMP客户端与任意代理(Broker)进行交互,类似于OpenWire(一种二进制协议)。

    

Spring Boot的WebSocket实现

     SpringBoot对内嵌的Tomcat(7或者8)、Jetty9和Undertow使用了WebSocket提供了支持。

广播式

广播式即服务端有消息时,会将消息发送到所有连接了当前endpoint的浏览器。

配置WebSocket

需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过集成AbstractWebSocketMessageBrokerConfigurer类,重写其方法来配置WebSocket。
        

package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;

/**
 * Created by lenovo on 2017/3/15.
 */
@Configuration
@EnableWebSocketMessageBroker //通过@EnableWebSocketMessageBroker 注解凯旗使用STOMP协议来传输基于代理(message broker)的消息
//这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{


    @Override
    public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
        stompEndpointRegistry.addEndpoint("/endpoint").withSockJS();//注册STOMP协议的节点,映射指定的URL,并指定使用SockJS协议
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {//配置消息代码(Message Broker)
        registry.enableSimpleBroker("/topic");//广播式应配置一个/topic消息代理
    }
}

消息的接收器、发送器和控制器

package com.example.model;

/**
 * Created by lenovo on 2017/3/15.
 */
public class MessageSender {
    private String msg;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public MessageSender(String msg) {
        this.msg = msg;
    }
}
package com.example.model;

import java.io.Serializable;

/**
 * Created by lenovo on 2017/3/15.
 */
public class MessageAcceptor implements Serializable{

    private String msg;

    public String getMsg() {
        return msg;
    }

}
package com.example.websocket;

import com.example.model.MessageAcceptor;
import com.example.model.MessageSender;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Created by lenovo on 2017/3/15.
 */
@Controller
public class TestWeb {

    @MessageMapping(value = "/message/test")//当浏览器向服务端发送请求时,通过@MessageMapping映射的地址,类似于@RequestMapping
    @SendTo(value = "/topic/response")//当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息
    public MessageSender say(MessageAcceptor acceptor){
        return new MessageSender("HELLO!"+acceptor.getMsg());
    }

    @RequestMapping("index")
    public String index(){
        return "index";
    }
}

准备 WebSocket需要的前端文件。

下载stomp.min.js和sockjs.min.js文件,并放在static下,然后在templates下新建index.html页面

stomp的API参考链接:https://segmentfault.com/a/11...

<!DOCTYPE html>
<html xmlns="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>
<div>
    <button onclick="connect()" id="connect">连接</button>
    <button onclick="disconnect()" id="disconnect">断开</button>
</div>
<div>
    <input type="text" id="name"/>
    <button id="send">发送</button>
</div>
<div id="msg">

</div>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{sockjs.min.js}"></script>
<script>
    var stompClient = null;

    function connect() {
        var socket = new SockJS("/endpoint");//连接SockJS的endpoint名称为"/endpoint"
        stompClient = Stomp.over(socket);//使用STOMP子协议的WebSocket客户端
        stompClient.connect({}, function (frame) {//连接WebSocket服务端
            stompClient.subscribe("/topic/response", function (msg) {//通过stopmClient.subscribe订阅"/topic/response"目标发送的消息,这个路径是在控制器的@SendTo中定义的
                console.log(msg);
                var msgDom = document.getElementById("msg");
                var html = msgDom.innerHTML;
                msgDom.innerHTML = html + "\n" + msg.body;
            });
        });

    }

    function disconnect() {
        if(stompClient!=null){
            stompClient.disconnect();
        }
    }

    function send() {
        var name = document.getElementById("name").value;
        stompClient.send("/message/test", {}, JSON.stringify({//通过stompClient.send向"/message/test"目标发送消息,这个在控制器的@MessageMapping中定义的。
            'msg': name
        }));
    }

    document.getElementById("send").onclick = send;
</script>
</body>
</html>

上述代码都已经准备好了,那么一起来看一看运行效果

如预期一样,在连接了WebSocket的客户端发送消息时,其它同样连接了WebSocket的客户端浏览器也收到了消息,没有连接WebSocket的客户端则没有收到消息

看完了代码和代码的运行效果,我们再来看一看WebSocket运行中STOMP的帧

连接STOMP服务端帧:CONNECT↵accept-version:1.1,1.0↵heart-beat:10000,10000 

连接STOMP服务端成功帧:CONNECTED↵version:1.1↵heart-beat:0,0 

订阅目标/topic/response:SUBSCRIBE↵id:sub-0↵destination:/topic/response 

向目标/message/test发送消息:SEND↵destination:/message/test↵content-length:16↵↵{"msg":"测试"} 

从目标/topic/response接收到消息:MESSAGE↵destination:/topic/response↵content-type:application/json;charset=UTF-8↵subscription:sub-0↵message-id:hstpp2xl-0↵content-length:22↵↵{"msg":"HELLO!测试"} 

点对点式

广播式有自己的应用场景,但是广播式不能解决我们我们一个常见的问题,即消息由谁发送,由谁接受的问题。

1.在进行点对点传递消息的时候,必然发生在两个用户之间的行为,那么就需要添加用户相关的内容,在这里先完成一个简单的登陆。

首先添加Spring Security的starter pom:    

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

2.然后进行spring security的简单配置

  

package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
 * Created by lenovo on 2017/3/17.
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 权限管理配置构造器
     *
     * @param auth 权限管理
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置了两个用户和对应的密码,并且申明了他们的角色
        auth.inMemoryAuthentication().withUser("muxiao").password("123456").roles("USER")
                .and().withUser("hahaha").password("123456").roles("USER");//在内存中分别配置两个用户muxiao和hahaha
    }

    /**
     * Web安全配置
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        //静态资源不做安全校验
        web.ignoring().antMatchers("/resources/static/**");///resources/static/目录下的静态资源,不拦截
    }

    /**
     * 配置http安全
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //简单的配置运行点对点所需要的登陆权限
        http.authorizeRequests()
                .antMatchers("/","login").permitAll()//设置Spring Security对/和/login路径不拦截
                .anyRequest().authenticated()
                .and().formLogin().loginPage("/login")//设置登录页面访问的路径为/login
                .defaultSuccessUrl("/chat").permitAll()//登陆成功后转向chat页面
                .and().logout().permitAll();
    }
}
        然后在TestWeb中增加一个MessageMapping接口:

  

  @Autowired private SimpMessagingTemplate messagingTemplate;//spring实现的一个发送模板类 

    @MessageMapping("/chat")
    public void handlerChat(Principal principal, String msg) {//springmvc中可以直接在参数中获得pricipal,pricipal中包含当前永不的信息
        if (principal.getName().equalsIgnoreCase("muxiao")) {
            messagingTemplate.convertAndSendToUser("hahaha","/queue/notice",principal.getName()+":"+msg);
        } else {
            messagingTemplate.convertAndSendToUser("muxiao","/queue/notice",principal.getName()+":"+msg);
            //通过messaginTemplate.converAndSendTiUser向用户发送消息,第一次参数是接受信息的用户,第二个是浏览器订阅的地址,第三个是消息本身
        }
    }

3.同时定义需要的页面访问路径:

package com.example.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

/**
 * Created by lenovo on 2017/3/17.
 */
@Configuration
public class WebViewConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/chat").setViewName("/chat");
        registry.addViewController("/login").setViewName("/login");
    }
}

4.我们已经准备好了所需要的后台,这时候就开始实现我们需要的功能的前端编写了。

首先,实现登陆页面,在浏览器中访问除过"/","/index"之外的其它页面,都会来到login页面以进行登陆,即下面的页面:

<!DOCTYPE html>
<html xmlns="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
</head>
<body>
<form th:action="@{/login}" method="post">
    <input type="text" name="username"/>
    <input type="password" name="password"/>
    <input type="submit"/>
</form>
</body>
</html>
              输入我们在内存中指定的用户名和密码,登陆进入chat页面

<!DOCTYPE html>
<html xmlns="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>Title</title>
</head>
<body>
<textarea id="content"></textarea>
<input type="button" value="发送" onclick="send()"/>
<div id="out">

</div>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script>
    var sock = new SockJS("/endpointOneToOne");//连接"endpointOneToOne"
    var stomp = Stomp.over(sock);
    stomp.connect({},function(frame){
        //订阅/user/queue/notice发送的消息,这里与在控制器的messagingTemplate.convertAndSendToUser中定义的订阅地址保持一致。
        //这里多一个/user,并且这个/user是必须的,使用了/user才会发送消息到指定的用户
        stomp.subscribe("/user/queue/notice",function(message){//
            var out = document.getElementById("out");
            var html = out.innerHTML;
            out.innerHTML = html +"<br />"+message.body;
        })
    });

    function send(){
        var content = document.getElementById("content").value;
        stomp.send("/chat",{},content);
    }
</script>
</body>
</html>

同时在两个浏览器上面,用在内存中指定的两个用户登陆,这样两个用户就可以互相发送消息了,延时效果如下:

           图片描述 


海角七号
11 声望2 粉丝

喵~~~