Spring Boot实战(七)Spring Boot 的 Web 开发 7.6 WebSocket

7.6.1 什么是WebSocket
WebSocket为浏览器和服务端提供了双工异步通信的功能,即浏览器可以向服务端发送消息,服务端也可以向浏览器发送消息。WebSocket需浏览器的支持,如IE10+、Chrome 13+、Firefox 6+,这对我们现在的浏览器来说都不是问题。
WebSocket是通过一个socket来实现双工异步通信能力的。但是直接使用WebSocket(或者SockJS:WebSocket协议的模拟,增加了当浏览器不支持WebSocket的时候的兼容支持)协议开发程序显得特别烦琐,我们会使用它的子协议STOMP,它是一个更高级别的协议,STOMP协议使用一个基于帧(frame)的格式来定义消息,与HTTP的request和response类似(具有类似于@RequestMapping的@MessageMapping),我们会 后面实战内容中观察STOMP的帧。
7.6.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

7.6.3 实战
1.准备
新建Spring Boot项目,选择Thymeleaf和WebSocket依赖。
2.广播式
广播式即服务端有消息时,会将消息发送给所有连接了当前endpoint的浏览器。
(1)配置WebSocket,需要在配置类上使用@EnableWebSocketMessageBroker开启WebSocket支持,并通过继承AbstractWebSocketMessageBrokerConfigurer类,重写其方法来配置WebSocket。
代码如下:

package com.wisely;

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;

@Configuration
@EnableWebSocketMessageBroker  //通过@EnableWebSocketMessageBroker注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样。
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

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

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

	
}

(2)浏览器5向服务端发送的消息用此类接受:

package com.wisely.domain;

public class AdminMessage {
	private String name;

	public String getName() {
		return name;
	}
	
	

}

(3)服务端向浏览器发送的此类的消息:

package com.wisely.domain;

public class AdminResponse {
	private String responseMessage;
	
	public AdminResponse(String responseMessage) {
		this.responseMessage=responseMessage;
	}

	public String getResponseMessage() {
		return responseMessage;
	}

}

(4)演示控制器,代码如下:

package com.wisely.web;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;

import com.wisely.domain.AdminMessage;
import com.wisely.domain.AdminResponse;

@Controller
public class AdController {
	@MessageMapping("/welcome")   // 当浏览器向服务端发送请求时,通过@MessageMapping映射/welcome这个地址,类似一@RequestMapping
	@SendTo("/topic/getResponse")  //当服务端有消息时,会对订阅了@SendTo中的路径的浏览器发送消息。
	public AdminResponse say(AdminMessage adminMessage) throws InterruptedException {
		Thread.sleep(3000);
		return new AdminResponse("Welcome,"+adminMessage.getName()+"!");
	}
}

(5)添加脚本。将stomp.min.js(STOMP协议的客户端脚本)、sockjs.min.js(SockJS的客户端脚本)以及jQuery放置在src/main/resources/static下。
(6)演示页面。在src/main/resource/templates下新建ws.html,代码如下:

<!DOCTYPE html>
<html xmlsn:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Spring Boot+WebSocket+广播式</title>
</head>
<body "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.min.js}"></script>
<script type="text/javascript">
	var stompClien = 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('/endpointAdmin');  //连接SockJS的endpoint名称为"/endpontAdmin"
		stompClient = Stomp.over(socket);   //使用STOMP子协议的WebSocket客户端。
		stompClient.connect({},function(frame){   //连接WebSocket服务器
			setConnected(true);
			console.log('Connected: '+ frame);
			stompClient.subscribe('/topic/getResponse',function(response){  //通过stompClient.subscribe订阅/topic/getResponse目标(destination)发送的消息,这个是在控制器的@SendTo中定义的。
				showResponse(JSON.parse(response.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,为ws.html提供便捷的路径映射:

package com.wisely;

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

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/ws").setViewName("/ws");
	}
	
	
}

(8)运行。我们预期的效果是:当一个浏览器发送一个消息到服务器时,其它浏览器也能接收到从服务端发送来的这个消息。
开启三个浏览器窗口,并访问http://localhost:8080/ws,分别连接服务器。然后在一个浏览器中发送一条消息,其它浏览器接收消息。
连接服务器,如图。
在这里插入图片描述
一个浏览器发送消息,如图
在这里插入图片描述
所有浏览器接收服务端发送的消息,如图
在这里插入图片描述
我们在Chrome浏览器(在Chrome下按F12调出)下观察一下控制台,如图
在这里插入图片描述
从上截图可以观察得出,连接服务端的格式为:

>>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000

连接成功的返回为:

<<< CONNECTED
version:1.1
heart-beat:0,0

订阅目标(destination)/topic/getResponse:

>>> SUBSCRIBE
id:sub-0
destination:/topic/getResponse

向目标(destination)/welcom发送消息的格式为:

>>> SEND
destination:/welcome
content-length:16

{"name":"admin"}

从目标(destination)/topic/getResponse接收的格式为:

<<< MESSAGE
destination:/topic/getResponse
content-type:application/json;charset=UTF-8
subscription:sub-0
message-id:03oflrlr-3
content-length:36

{"responseMessage":"Welcome,admin!"}

3.点对点式
广播式有自己的应用场景,但是广播式不能解决我们一个常见的场景,即消息由谁发送、由谁接收的问题。
本例中演示了一个简单的聊天室程序。例子中只有两个用户,互相发送消息给彼此,因需要用户相关的内容,所以先在这里引入最简单的Spring Security相关内容。
(1)添加Spring Security的 starter pom:

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

(2)Spring Security的简单配置。这里不对Spring Security做过多解释,只解释对本项目有帮助的部分:

package com.wisely;

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;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;

@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();
	}

	//在内存中分别配置两个用户lmz和admin,密码和用户名一致,角色是USER
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
			.withUser("lmz").password(new BCryptPasswordEncoder().encode("lmz")).roles("USER")
			.and()
			.withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("USER");
	}
	
	///resources/static目录下的静态资源,Spring Security不拦截
	@Override
	public void configure(WebSecurity web) throws Exception {
		web.ignoring().antMatchers("/resources/static/**");
	}
	

}

(3)配置WebSocket:

package com.wisely;

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;

@Configuration
@EnableWebSocketMessageBroker  //通过@EnableWebSocketMessageBroker注解开启使用STOMP协议来传输基于代理(message broker)的消息,这时控制器支持使用@MessageMapping,就像使用@RequestMapping一样。
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) { //注册STOMP协议的节点(endpoint),并映射的指定的URL。
		registry.addEndpoint("/endpointAdmin").withSockJS();  //注册一个STOMP的endpoint,并指定使用SockJS协议
		registry.addEndpoint("/endpointChat").withSockJS();  //注册一个名为/endpointChat的endpoint
	}

	@Override
	public void configureMessageBroker(MessageBrokerRegistry registry) {  //配置消息代理(Message Broker)
		registry.enableSimpleBroker("/queue","/topic");   // 点对点式应增加一个/queue消息代理 
	}

}

(4)控制器。在WsController内添加如下代码:

package com.wisely.web;

import java.security.Principal;

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;

@Controller
public class WsController {
	
	@Autowired
	private SimpMessagingTemplate messagingTemplate; //通过SimpMessagingTemplate向浏览器发送消息。
	
	@MessageMapping("/chat")
	public void handleChat(Principal principal,String msg) {  //在Spring MVC中,可以直接在参数中获得pricipal,pinciple中包含当前用户的信息。
		if(principal.getName().equals("lmz")) {
			messagingTemplate.convertAndSendToUser("admin", "/queue/notifications", principal.getName()+"-send:"+msg);
		}else {
			messagingTemplate.convertAndSendToUser("lmz", "/queue/notifications", principal.getName()+"-send:"+msg);
		}
	}

}


(5)登录页面。在 src/main/resources/templates 下新建login.html,代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlsn:th="http://www.thymeleaf.org"
		xmlns:sec="http://www.thymeleafe.org/thymmeleaf-extras-springsecurity3">
<head>
<meta charset="UTF-8">
<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)聊天页面。在 src/main/resources/templates下新建 chat.html,代码如下:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Home</title>
<script th:src="@{sockjs.min.js}"></script>
<script th:src="@{stomp.min.js}"></script>
<script th:src="@{jquery.min.js}"></script>
</head>
<body>
<p>
	聊天室
</p>

<form id="adminForm">
	<textarea tows="4" cols="60" name="text"></textarea>
	<input type="submit">
</form>

<script th:inline="javascript">
	$('#adminForm').submit(function(e){
		e.preventDefault();
		var text = $('#adminForm').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发送的消息,这里与在控制器的messaginTemplate.convertAndSendToUser中定义的订阅地址保持一致。这里多了一个/user,并且这个/user是必须的,使用了/user才会发送消息到指定用户。
	});
	
	function handleNotification(message){
		$('#output').append("<b>Received:"+message.body+"</b><br/>")
	}
	
	function sendSpittle(text){
		stomp.send("/chat",{},text);  
	}
	$('#stop').click(function() {sock.close()});
</script>

<div id="output"></div>
</body>
</html>

(7)增加页面的viewController:

	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/ws").setViewName("/ws");
		registry.addViewController("/login").setViewName("/login");
		registry.addViewController("/chat").setViewName("/chat");
	}

(8)运行。分别在两用户下的浏览器访问http://localhost:8080/login 并登录,如图,我这里一个是谷歌浏览器,一个是火狐。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_40929047/article/details/86662881