一、WebSocket简介
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
原理:在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:
- 1. Header:互相沟通的Header是很小的-大概只有 2 Bytes
- 2. Server Push:服务器的推送,服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。
二、实现消息推送
1、添加websocke的t依赖
<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>
2、webSocket配置,添加@EnableWebSocketMessageBroker注解开启TOMP协议
package com.example.demo.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* 路径:com.example.demo.websocket.config
* 类名:
* 功能:webSocket配置
* 备注:@EnableWebSocketMessageBroker注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思;
* 创建人:typ
* 创建时间:2018/10/18 10:15
* 修改人:
* 修改备注:
* 修改时间:
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 方法名:
* 功能:《用一句话描述一下》
* 描述:注册STOMP协议的节点,并指定映射的URL
* 创建人:typ
* 创建时间:2018/10/18 10:21
* 修改人:
* 修改描述:
* 修改时间:
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册STOMP协议节点,同时指定使用SockJS协议
registry.addEndpoint("/endpointSang").withSockJS();
}
/**
* 方法名:
* 功能:《用一句话描述一下》
* 描述:配置消息代理,由于我们是实现推送功能,这里的消息代理是/topic
* 创建人:typ
* 创建时间:2018/10/18 10:22
* 修改人:
* 修改描述:
* 修改时间:
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
}
}
3、接受消息的实体
package com.example.demo.websocket.entity;
/**
* 路径:com.example.demo.websocket.entity
* 类名:
* 功能:接受消息的实体
* 备注:
* 创建人:typ
* 创建时间:2018/10/18 11:03
* 修改人:
* 修改备注:
* 修改时间:
*/
public class RequestMessage {
private String name;
public String getName() {
return name;
}
}
4、响应消息的实体
package com.example.demo.websocket.entity;
/**
* 路径:com.example.demo.websocket.entity
* 类名:
* 功能:响应消息的实体
* 备注:
* 创建人:typ
* 创建时间:2018/10/18 11:03
* 修改人:
* 修改备注:
* 修改时间:
*/
public class ResponseMessage {
private String responseMessage;
public ResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
public String getResponseMessage() {
return responseMessage;
}
}
5、配置viewController
package com.example.demo.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* 路径:com.example.demo.websocket.config
* 类名:
* 功能:《用一句描述一下》
* 备注:为ws.html提供路径映射
* 创建人:typ
* 创建时间:2018/10/18 10:34
* 修改人:
* 修改备注:
* 修改时间:
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/ws").setViewName("/ws");
}
}
6、创建Controller,添加@SendTo注解表示当服务器有消息需要推送的时候,会对订阅了SendTo中路径的浏览器发送消息。
package com.example.demo.websocket.controller;
import com.example.demo.websocket.entity.RequestMessage;
import com.example.demo.websocket.entity.ResponseMessage;
import lombok.extern.slf4j.Slf4j;
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.RequestBody;
/**
* 路径:com.example.demo.websocket.controller
* 类名:
* 功能:《用一句描述一下》
* 备注:
* 创建人:typ
* 创建时间:2018/10/18 10:26
* 修改人:
* 修改备注:
* 修改时间:
*/
@Slf4j
@Controller
public class WsController {
/**
* 方法名:
* 功能:《用一句话描述一下》
* 描述:@MessageMapping注解和我们之前使用的@RequestMapping类似;
* @SendTo注解表示当服务器有消息需要推送的时候,会对订阅了@SendTo中路径的浏览器发送消息。
* 创建人:typ
* 创建时间:2018/10/18 10:28
* 修改人:
* 修改描述:
* 修改时间:
*/
@MessageMapping("/welcome")
@SendTo("/topic/getResponse")
public ResponseMessage say(@RequestBody RequestMessage message){
log.info("消息:{}",message.getName());
String mag = "welcom," + message.getName() + "!";
return new ResponseMessage(mag);
}
}
7、测试页面
引入js脚本文件,分别是STOMP协议的客户端脚本stomp.js、SockJS的客户端脚本sock.js以及jQuery,放到src/main/resources/static/js目录下。
在src/main/resources/templates目录下新建一个ws.html页面,如下:
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>WebSocket</title>
<script th:src="@{js/sockjs.min.js}"></script>
<script th:src="@{js/stomp.js}"></script>
<script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #e80b0a;">浏览器不支持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 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('/endpointSang');
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected:' + frame);
stompClient.subscribe('/topic/getResponse', function (response) {
showResponse(JSON.parse(response.body).responseMessage);
})
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log('Disconnected');
}
function sendName() {
var name = $('#name').val();
console.log('name:' + name);
stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
}
function showResponse(message) {
$("#response").html(message);
}
</script>
</body>
</html>
启动工程测试,打开两个浏览器访问:http://localhost:8081/ws
三、实现聊天功能
1、使用SpringSecurity管理权限,添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、WebSocket配置文件
package com.example.demo.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* 路径:com.example.demo.websocket.config
* 类名:
* 功能:webSocket配置
* 备注:@EnableWebSocketMessageBroker注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思;
* 创建人:typ
* 创建时间:2018/10/18 11:48
* 修改人:
* 修改备注:
* 修改时间:
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//注册STOMP协议节点,同时指定使用SockJS协议
registry.addEndpoint("/endpointChat").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue");
}
}
3、页面映射配置文件
package com.example.demo.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
/**
* 路径:com.example.demo.websocket.config
* 类名:
* 功能:《用一句描述一下》
* 备注:
* 创建人:typ
* 创建时间:2018/10/18 11:52
* 修改人:
* 修改备注:
* 修改时间:
*/
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter{
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("/login");
registry.addViewController("/chat").setViewName("/chat");
}
}
4、SpringSecurity配置文件,添加@EnableWebSecurity注解,设置登录用户及密码
package com.example.demo.websocket.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;
/**
* 路径:com.example.demo.websocket.config
* 类名:
* 功能:《用一句描述一下》
* 备注:
* 创建人:typ
* 创建时间:2018/10/18 11:53
* 修改人:
* 修改备注:
* 修改时间:
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
/**
* 方法名:
* 功能:先设置拦截规则,设置默认登录页面以及登录成功后的跳转页面
* 描述:
* 创建人:typ
* 创建时间:2018/10/18 21:07
* 修改人:
* 修改描述:
* 修改时间:
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
//设置拦截器
.antMatchers("/")
.permitAll()
.anyRequest()
.authenticated()
.and()
//开启默认登录页面
.formLogin()
//默认登录页面
.loginPage("/login")
//默认登录成功跳转页面
.defaultSuccessUrl("/chat")
.permitAll()
.and()
//设置注销
.logout()
.permitAll();
}
/**
* 方法名:
* 功能:定义两个用户,设置用户名、用户密码、用户角色等信息。
* 描述:
* 创建人:typ
* 创建时间:2018/10/18 21:07
* 修改人:
* 修改描述:
* 修改时间:
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.passwordEncoder(new PasswordEncoderConfig())
.withUser("admin").password("admin").roles("USER")
.and()
.withUser("root").password("root").roles("USER");
}
/**
* 方法名:
* 功能:设置静态资源不被拦截。
* 描述:
* 创建人:typ
* 创建时间:2018/10/18 21:08
* 修改人:
* 修改描述:
* 修改时间:
*/
@Override
public void configure(WebSecurity web) throws Exception {
//设置不拦截规则
web.ignoring().antMatchers("/resources/static/**");
}
}
5、提供一个PasswordEncorderConfig的实例,实现PasswordEncorder接口
在springboot2.0.3以上版本中会报错:
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
package com.example.demo.websocket.config;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 路径:com.example.demo.websocket.config
* 类名:
* 功能:提供一个PasswordEncorder的实例,否则后台会报错误
* 备注:
* 创建人:typ
* 创建时间:2018/10/18 11:48
* 修改人:
* 修改备注:
* 修改时间:
*/
public class PasswordEncoderConfig implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
}
6、控制器controller配置文件
package com.example.demo.websocket.controller;
import lombok.extern.slf4j.Slf4j;
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;
/**
* 路径:com.example.demo.websocket.controller
* 类名:
* 功能:webSocket实现聊天室
* 备注:
* 创建人:typ
* 创建时间:2018/10/18 11:48
* 修改人:
* 修改备注:
* 修改时间:
*/
@Slf4j
@Controller
public class WsController {
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 方法名:
* 功能:《用一句话描述一下》
* 描述:
* 创建人:typ
* 创建时间:2018/10/18 12:04
* 修改人:
* 修改描述:
* 修改时间:
*/
@MessageMapping("/chat")
public void handleChat(Principal principal, String msg) {
log.info("name:{} ,msg:{},",principal.getName(),msg);
if (principal.getName().equals("admin")) {
messagingTemplate.convertAndSendToUser("admin", "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
}else{
messagingTemplate.convertAndSendToUser("root", "/queue/notifications", principal.getName() + "给您发来了消息:" + msg);
}
}
}
7、创建登录和聊天页面,在src/main/resources/templates目录下新建login.html和chat.html文件
stomp中的connect方法用来连接服务端,连接成功之后注册监听,在注册监听的时候,注册的地址/user/queue/notifications比WebSocket配置文件中的多了一个/user,这个/user是必不可少的,使用了它消息才会点对点传送。 收到消息后在handleNotification方法中处理,实际上就是把收到的内容添加到id为output的div中。
依赖的静态js文件消息推送中的一致,具体HTML代码如下:
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-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>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<title>聊天室</title>
<script th:src="@{js/sockjs.min.js}"></script>
<script th:src="@{js/stomp.js}"></script>
<script th:src="@{js/jquery-3.1.1.js}"></script>
</head>
<body>
<p>聊天室</p>
<form id="sangForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit" value="发送"/>
</form>
<script th:inline="javascript">
$("#sangForm").submit(function (e) {
e.preventDefault();
var textArea = $("#sangForm").find('textarea[name="text"]');
var text = textArea.val();
sendSpittle(text);
textArea.val('');
});
var sock = new SockJS("/endpointChat");
var stomp = Stomp.over(sock);
stomp.connect('guest','guest',function (frame) {
stomp.subscribe("/user/queue/notifications", handleNotification);
});
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>
启动工程测试,在不同的两个浏览器中用不同的用户登录,效果入下:
下载源码:https://download.youkuaiyun.com/download/typ1805/10730574