浏览器和服务端 提供双工异步通信,相互发送数据
通过一个socket来实现通信,直接使用WebSocket开发特别繁琐,我们用它的子协议 STOMP。
使用一个基于帧的格式来定义消息。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
application.properties
logging.level.org.springframework.web= DEBUG
@SpringBootApplication
public class Ch76Application {
public static void main(String[] args) {
SpringApplication.run(Ch76Application.class, args);
}
}
webMvcConfig
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/ws").setViewName("/ws");
registry.addViewController("/login").setViewName("/login");
registry.addViewController("/chat").setViewName("/chat");
}
}
广播式
即 服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器
STOMP 协议来传输基于代理message braker的消息,支持@MessageMapping
WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpointWisely").withSockJS(); //js new SockJS('/endpointWisely'); 的路径
registry.addEndpoint("/endpointChat").withSockJS();//1
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue","/topic"); //2 topic 广播式。 queue 点对点式
}
}
EnableWebSocketMessageBroker 开启webSocke
集成 AbstractWebSocketMessageBrokerConfigurer 进行配置
定义浏览器 向 服务器 发送的对象
public class WiselyMessage {
private String name;
public String getName(){
return name;
}
}
定义 服务端 发送给 浏览器的
public class WiselyResponse {
private String responseMessage;
public WiselyResponse(String responseMessage){
this.responseMessage = responseMessage;
}
public String getResponseMessage(){
return responseMessage;
}
}
控制器
@Controller
public class WsController {
@MessageMapping("/welcome")//相当于RequestMapping,浏览器 向 服务器发送
@SendTo("/topic/getResponse") //服务器有消息时,对订阅了@SendTo中的路径的浏览器发送消息
public WiselyResponse say(WiselyMessage message) throws Exception {
Thread.sleep(3000);
return new WiselyResponse("Welcome, " + message.getName() + "!");
}
@Autowired
private SimpMessagingTemplate messagingTemplate;//1 向浏览器 发送消息
@MessageMapping("/chat")
public void handleChat(Principal principal, String msg) { //2 principal 包含当前用户的信息。
if (principal.getName().equals("wyf")) {//3 如果发送人是 wyf 则发给 wisely
messagingTemplate.convertAndSendToUser
("wisely","/queue/notifications", principal.getName() + "-send:"+ msg);
} else {
messagingTemplate.convertAndSendToUser //向用户发送消息,第一个参数接收消息的用户。浏览器订阅的地址。消息本身。
("wyf","/queue/notifications", principal.getName() + "-send:"+ msg);
}
}
}
公用js
stomp.min.js STOMP协议的客户端脚本
sockjs.min.js SockJS 的客户端脚本
JS 放在 src/main/resources/static/jquery.js 下
演示页面
src/main/resources/templates/ws.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>Spring Boot+WebSocket+广播式</title>
</head>
<body onload="disconnect()"> //加在的时候关闭
<noscript><h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">连接</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">断开连接</button>
</div>
<div id="conversationDiv">
<label>输入你的名字</label><input type="text" id="name" />
<button id="sendName" onclick="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('/endpointWisely'); //1 连接 SockJs 的 endpoint
stompClient = Stomp.over(socket); //使用 stomp 子协议
stompClient.connect({}, function(frame) { //连接webSocket服务端,
setConnected(true);
console.log('Connected: ' + frame);
//stompClient 订阅/topic/getResponse,@SendTo 定义
stompClient.subscribe('/topic/getResponse', function(respnose){ //2
showResponse(JSON.parse(respnose.body).responseMessage);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = $('#name').val();
//3 向 /welcome 发送消息 。与: @MessageMapping 对应
stompClient.send("/welcome", {}, JSON.stringify({ 'name': name }));
}
function showResponse(message) {
var response = $("#response");
response.html(message);
}
</script>
</body>
</html>
http://localhost:8080/ws
一个浏览器 发送消息,其他浏览器也能收到
连接参数:
>>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000
连接成功的返回
<<< CONNECTED
version:1.1
heart-beat:0,0
user-name:wyf
订阅目标为:
>>> SUBSCRIBE
id:sub-0
destination:/topic/getResponse
向目标发送为:
>>> SEND
destination:/welcome
content-length:18
{"name":"huahaha"}
从目标 接受为的格式为
<<< MESSAGE
destination:/topic/getResponse
content-type:application/json;charset=UTF-8
subscription:sub-0
message-id:sbt2akvw-7
content-length:39
{"responseMessage":"Welcome, huahaha!"}
点对点式
消息有谁推送,消息有谁接收
引入security
spring-boot-starter-security
配置security
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/","/login").permitAll()//1根路径和/login路径不拦截
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") //2登陆页面
.defaultSuccessUrl("/chat") //3登陆成功转向该页面
.permitAll()
.and()
.logout()
.permitAll();
}
//4 使用内存存储,分配两个用户,
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("wyf").password("wyf").roles("USER")
.and()
.withUser("wisely").password("wisely").roles("USER");
}
//5忽略静态资源的拦截
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/static/**");
}
}
配置websocket(见上面)
控制器(见上面)
登录页面
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>
聊天页面
<!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="wiselyForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit"/>
</form>
<script th:inline="javascript">
$('#wiselyForm').submit(function(e){
e.preventDefault();
var text = $('#wiselyForm').find('textarea[name="text"]').val();
sendSpittle(text);
});
var sock = new SockJS("/endpointChat"); //1 endpoint
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function(frame) {
stomp.subscribe("/user/queue/notifications", handleNotification);
//2 订阅 /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>