WebSocket
1 WebSocket是什么
WebSocket为浏览器和服务器提供双工异步通信(浏览器可以向服务器发送消息,服务器也可以向浏览器发送消息)。
WebSocket需要浏览器支持。
WebSocket是通过一个socket来实现双工异步通信能力的。
直接使用WebSocket过于麻烦,使用它的子协议STOMP,它是一个更高级别的协议,STOMP协议使用基于帧(frame)的格式来定义消息,与HTTP的request和response类似。
2 Spring Boot提供的自动配置
Spring Boot对内嵌的Tomcat(7、8)、Jetty9和Undertow使用WebSocket提供了支持。
配置源码存于org.springframework.boot.autoconfigure.websocket下.
Spring Boot为WebSocket提供的starter pom是spring-boot-starter-websocket.
3 实战
3.1 准备
新建Spring Boot项目,选择Thymeleaf和Websocket依赖
3.2广播式
广播式即服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器
(1)配置WebSocket,需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过集成AbstractWebSocketMessageBrokerConfigurer类,重写其方法来配置WebSocket.
package
com.hand;
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 lqy on 2017-11-28.
*/
@Configuration
@EnableWebSocketMessageBroker
//通过注解开启使用STOMP协议来传输基于代理(message broker)的消息
//这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
public class
WebSocketConfig
extends
AbstractWebSocketMessageBrokerConfigurer{
@Override
public void
registerStompEndpoints(StompEndpointRegistry registry){
//注册STOMP协议的节点(endpoint),并映射到指定URL
registry.addEndpoint(
"/endpointHand"
).withSockJS();
//注册一个STOMP的endpoint,并指定使用SockJS协议
}
@Override
public void
configureMessageBroker(MessageBrokerRegistry registry){
//配置消息代理(Message Broker)
registry.enableSimpleBroker(
"/topic"
);
//广播式应配置一个/topic消息代理
}
}
(2)浏览器向服务端发送的消息用此类接受
package
com.hand;
/**
* Created by lqy on 2017-11-28.
*/
public class
HandMessage {
private
String
name
;
public
String getName(){
return
name
;
}
}
(3)服务端向浏览器发送的此类的消息
package
com.hand;
/**
* Created by lqy on 2017-11-28.
*/
public class
HandResponse {
private
String
responseMessage
;
public
HandResponse(String responseMessage){
this
.
responseMessage
=responseMessage;
}
public
String getResponseMessage(){
return
responseMessage
;
}
}
(4)演示控制器:
package
com.hand;
import
org.springframework.messaging.handler.annotation.
MessageMapping
;
import
org.springframework.messaging.handler.annotation.
SendTo
;
import
org.springframework.stereotype.
Controller
;
/**
* Created by lqy on 2017-11-28.
*/
@Controller
public class
HandController {
@MessageMapping
(
"/welcome"
)
//当浏览器向服务端发送请求时,通过该注解映射/welcome这个地址,类似@RequestMapping
@SendTo
(
"/topic/getResponse"
)
//当服务端有消息时,会对订阅了@Sendto中的路径的浏览器发送消息
public
HandResponse say(HandMessage message)
throws
Exception{
Thread.
sleep
(
3000
);
return new
HandResponse(
"welcome,"
+message.getName()+
"!"
);
}
}
(5)添加脚本。
将stomp.min.js(STOMP协议的客户端脚本)、sockjs.min.js(SockJS的客户端脚本)以及jQuery放置在src/main/resources/static下。
(6)演示页面,在src/main/resources/templates下 新建hand.html
<!DOCTYPE html>
<
html
xmlns:
th
=
"http://www.thymeleaf.org"
>
<
head
>
<
meta
charset=
"UTF-8"
/>
<
title
>
Spring Boot+WebSocket+广播式
</
title
>
</
head
>
<
body
οnlοad=
"
disconnect
()
"
>
<
noscript
><
h2
style=
"
color
:
#ff0000
"
>
貌似你的浏览器不支持websocket
</
h2
></
noscript
>
<
div
>
<
div
>
<
button
id=
"connect"
οnclick=
"
connect
();
"
>
连接
</
button
>
<
button
id=
"disconnect"
disabled=
"disabled"
οnclick=
"
disconnect
();
"
>
断开连接
</
button
>
</
div
>
<
div
id=
"conversationDiv"
>
<
label
>
输入你的名字
</
label
><
input
type=
"text"
id=
"name"
/>
<
button
id=
"sendName"
οnclick=
"
sendName
();
"
>
发送
</
button
>
<
p
id=
"response"
></
p
>
</
div
>
</
div
>
<
script
th
:src=
"@{sockjs.min.js}"
></
script
>
<
script
th
:src=
"@{stomp.min.js}"
></
script
>
<
script
th
:src=
"@{jquery.js}"
></
script
>
<
script
type=
"text/javascript"
>
var
stompClient
=
null
;
function
setConnected
(connected) {
document
.
getElementById
(
'connect'
).
disabled
= connected;
document
.
getElementById
(
'disconnect'
).
disabled
= !connected;
document
.
getElementById
(
'conversationDiv'
).
style
.
visibility
= connected ?
'visible'
:
'hidden'
;
$
(
'#response'
).
html
();
}
function
connect
() {
var
socket
=
new
SockJS
(
'/endpointHand'
);
//连接SockJS的endpoint的名字为“/endpointHand”
stompClient
=
Stomp
.
over
(
socket
);
//使用STOMP子协议的WebSocker客户端
stompClient
.
connect
({},
function
(frame) {
//连接WebSocker服务端
setConnected
(
true
);
console
.
log
(
'Connected: '
+ frame);
stompClient
.
subscribe
(
'/topic/getResponse'
,
function
(respnose){
//通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送的消息,这个是在控制器的@Sento中定义的
showResponse
(
JSON
.
parse
(respnose.
body
).responseMessage);
});
});
}
function
disconnect
() {
if
(
stompClient
!=
null
) {
stompClient
.
disconnect
();
}
setConnected
(
false
);
console
.
log
(
"Disconnected"
);
}
function
sendName
() {
var
name
=
$
(
'#name'
).
val
();
//通过stompClient.send向/welcome目标(destination)发送消息,这个是在控制器的MessageMapping中定义的
stompClient
.
send
(
"/welcome"
, {},
JSON
.
stringify
({
'name'
:
name
}));
}
function
showResponse
(message) {
var
response
=
$
(
"#response"
);
response
.
html
(message);
}
</
script
>
</
body
>
</
html
>
(7)配置viewController,为Hand.html提供便捷的路径映射
package
com.hand;
import
org.springframework.context.annotation.
Configuration
;
import
org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Created by lqy on 2017-11-28.
*/
@Configuration
public class
WebmvcConfig
extends
WebMvcConfigurerAdapter{
@Override
public void
addViewControllers(ViewControllerRegistry registry){
registry.addViewController(
"/hand"
).setViewName(
"/Hand"
);
}
}
(8)运行程序。
预期效果:一个浏览器发送一个消息到服务端时,其他浏览器也能接收到从服务端发送的这个消息。
开启三个浏览器,访问http://localhost:8080/hand,分别连接服务器,然后再浏览器发送一条消息,其他浏览器接收
连接服务端格式:
连接成功的返回:
订阅目标
向目标发送消息的格式
从目标接收的格式
3.3 点对点式
一对一聊天室:
需要用户相关的内容,引入最简单的Spring Security相关内容
(1)添加Spring Security的starter pom;
<
dependency
>
<
groupId
>
org.springframework.boot
</
groupId
>
<
artifactId
>
spring-boot-starter-security
</
artifactId
>
</
dependency
>
(2)Spring Security的简单配置.
package
com.hand;
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 lqy on 2017-11-28.
*/
@Configuration
@EnableWebSecurity
public class
WebSecurityConfig
extends
WebSecurityConfigurerAdapter{
@Override
protected void
configure(HttpSecurity http)
throws
Exception{
http
.authorizeRequests()
.antMatchers(
"/"
,
"/login"
).permitAll()
//设置Spring Security对/和/"login"路径不拦截
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage(
"/login"
)
//设置Spring Security的登陆页面访问的路径为/login
.defaultSuccessUrl(
"/chat"
)
//登陆成功后转向/chat路径
.permitAll()
.and()
.logout()
.permitAll();
}
//在内存中分别配置两个用户lqy和lqy01,密码与用户名一致,角色是USER
@Override
protected void
configure(AuthenticationManagerBuilder auth)
throws
Exception{
auth
.inMemoryAuthentication()
.withUser(
"lqy"
).password(
"lqy"
).roles(
"USER"
)
.and()
.withUser(
"lqy01"
).password(
"lqy01"
).roles(
"USER"
);
}
///resources/static目录下的静态资源,Spring Security不拦截
@Override
public void
configure(WebSecurity web)
throws
Exception{
web.ignoring().antMatchers(
"/resources/static/**"
);
}
}
(3)WebSocket;
package
com.hand;
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 lqy on 2017-11-28.
*/
@Configuration
@EnableWebSocketMessageBroker
//通过注解开启使用STOMP协议来传输基于代理(message broker)的消息
//这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样
public class
WebSocketConfig
extends
AbstractWebSocketMessageBrokerConfigurer{
@Override
public void
registerStompEndpoints(StompEndpointRegistry registry){
//注册STOMP协议的节点(endpoint),并映射到指定URL
//registry.addEndpoint("/endpointHand").withSockJS();//注册一个STOMP的endpoint,并指定使用SockJS协议
registry.addEndpoint(
"/endpointChat"
).withSockJS();
//注册一个STOMP的endpoint,并指定使用SockJS协议
}
@Override
public void
configureMessageBroker(MessageBrokerRegistry registry){
//配置消息代理(Message Broker)
//registry.enableSimpleBroker("/topic");//广播式应配置一个/topic消息代理
registry.enableSimpleBroker(
"/queue"
,
"/topic"
);
//点对点式新增一个/queue消息代理
}
}
(4)控制器
package
com.hand;
import
org.springframework.beans.factory.annotation.
Autowired
;
import
org.springframework.messaging.handler.annotation.
MessageMapping
;
import
org.springframework.messaging.simp.SimpMessagingTemplate;
import
org.springframework.stereotype.
Controller
;
import
java.security.Principal;
/**
* Created by lqy on 2017-11-28.
*/
@Controller
public class
WebSecurityController {
@Autowired
private
SimpMessagingTemplate
messagingTemplate
;
//通过SimpMessagingTemplate向浏览器发送消息
@MessageMapping
(
"/chat"
)
public void
handleChat(Principal principal, String msg){
//在Spring MVC中,可以直接在参数中获取principal,principal中包含当前用户的信息
if
(principal.getName().equals(
"lqy"
)){
//这是一段硬编码,如果发送人是Lqy,则发送给lqy01,反之。。
//通过messagingTemplate.convertAndSendToUser向用户发送消息,第一个参数是接收消息的用户,第二个是浏览器订阅的地址,第三个是消息本身
messagingTemplate
.convertAndSendToUser(
"lqy01"
,
"/queue/notifications"
,principal.getName()+
"-send:"
+msg);
}
else
{
messagingTemplate
.convertAndSendToUser(
"lqy"
,
"/queue/notifications"
,principal.getName()+
"-send:"
+msg);
}
}
}
(5)登录页面,src/main/resources/templates下新建login.html
<!DOCTYPE
html
>
<
html
xmlns=
"http://www.w3.org/1999/xhtml"
xmlns:th=
"http://www.thymeleaf.org"
xmlns:
sec
=
"http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
>
<
meta
charset=
"UTF-8"
/>
<
head
>
<
title
>
登陆页面
</
title
>
</
head
>
<
body
>
<
div
th:if=
"${param.error}"
>
无效的账号和密码
</
div
>
<
div
th:if=
"${param.logout}"
>
你已注销
</
div
>
<
form
th:action=
"@{/login}"
method=
"post"
>
<
div
><
label
>
账号 :
<
input
type=
"text"
name=
"username"
/>
</
label
></
div
>
<
div
><
label
>
密码 :
<
input
type=
"password"
name=
"password"
/>
</
label
></
div
>
<
div
><
input
type=
"submit"
value=
"登陆"
/></
div
>
</
form
>
</
body
>
</
html
>
(6)聊天页面
<!DOCTYPE
html
>
<
html
xmlns:
th
=
"http://www.thymeleaf.org"
>
<
meta
charset=
"UTF-8"
/>
<
head
>
<
title
>
Home
</
title
>
<
script
th
:src=
"@{sockjs.min.js}"
></
script
>
<
script
th
:src=
"@{stomp.min.js}"
></
script
>
<
script
th
:src=
"@{jquery.js}"
></
script
>
</
head
>
<
body
>
<
p
>
聊天室
</
p
>
<
form
id=
"handForm"
>
<
textarea
rows=
"4"
cols=
"60"
name=
"text"
></
textarea
>
<
input
type=
"submit"
/>
</
form
>
<
script
th
:inline=
"javascript"
>
$
(
'#handForm'
).
submit
(
function
(e){
e.
preventDefault
();
var
text
=
$
(
'#handForm'
).
find
(
'
textarea
[name="text"]'
).
val
();
sendSpittle
(
text
);
});
var
sock
=
new
SockJS
(
"/endpointChat"
);
//连接endpoint名为endpointChat的endpoint
var
stomp
=
Stomp
.
over
(
sock
);
stomp
.
connect
(
'guest'
,
'guest'
,
function
(frame) {
stomp
.
subscribe
(
"/user/queue/notifications"
,
handleNotification
);
//订阅/user/queue/notifications发送的消息,这里与在控制器的messagingTemplate.convertAndSendToUser中定义的订阅地址保持一致。
//此处多一个/user,且是必须的,使用/user之后才能发送消息到指定用户
});
function
handleNotification
(message) {
$
(
'#output'
).
append
(
"<b>Received: "
+ message.
body
+
"</b><br/>"
)
}
function
sendSpittle
(text) {
stomp
.
send
(
"/chat"
, {}, text);
//3
}
$
(
'#stop'
).
click
(
function
() {
sock
.
close
()});
</
script
>
<
div
id=
"output"
></
div
>
</
body
>
</
html
>
(7)增加页面的viewController;
package
com.hand;
import
org.springframework.context.annotation.
Configuration
;
import
org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import
org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* Created by lqy on 2017-11-28.
*/
@Configuration
public class
WebmvcConfig
extends
WebMvcConfigurerAdapter{
@Override
public void
addViewControllers(ViewControllerRegistry registry){
//registry.addViewController("/hand").setViewName("/Hand");
registry.addViewController(
"/login"
).setViewName(
"/login"
);
registry.addViewController(
"/chat"
).setViewName(
"/chat"
);
}
}
(8)运行
预期效果:两个用户登录系统,可以互发消息。一个浏览器用户会话session是共享的,在浏览器设置两个独立的用户,从而实现用户会话session隔离。http://localhos:8080/login