简单介绍
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>
同时在两个浏览器上面,用在内存中指定的两个用户登陆,这样两个用户就可以互相发送消息了,延时效果如下:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。