WebSocket是为浏览器和服务端提供双工艺部通讯功能一种工具,即浏览器能够先服务端发送消息,服务端也能够先浏览器发送消息。如今支持Websocket的浏览器有 IE10+,Crome13+,FileFox6+。html
WebSocket只是一个消息传递的体系结构,没有指定任何的消息传递协议。与HTTP协议不一样的是,WebSocket只是一个应用层的协议,它很是简单,并不能理解传入的消息,也不能对消息进行路由或处理,所以WebSocket协议只是一个应用层的协议,其上须要一个框架来理解和处理消息。前端
Spring框架提供了对使用STOMP子协议的支持。STOMP,全称Streaming Text Orientated Message Protol,流文本定向协议。STOMP是一个简单的消息传递协议,是一种为MOM(Message Orientated Middleware,面向消息的中间件)设计的简单文本协议。STOMP提供了一个可操做的链接格式,容许STOMP客户端与任意代理(Broker)进行交互,相似于OpenWire(一种二进制协议)。java
SpringBoot对内嵌的Tomcat(7或者8)、Jetty9和Undertow使用了WebSocket提供了支持。web
广播式即服务端有消息时,会将消息发送到全部链接了当前endpoint的浏览器。spring
须要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并经过集成AbstractWebSocketMessageBrokerConfigurer类,重写其方法来配置WebSocket。
json
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消息代理 } }
消息的接收器、发送器和控制器segmentfault
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"; } }
下载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服务端帧: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>
同时在两个浏览器上面,用在内存中指定的两个用户登录,这样两个用户就能够互相发送消息了,延时效果以下: