本篇文章主要介绍websocket的两种通信,广播式和点对点的通信。
一、广播式通讯
类似广播一样,只要发出,订阅的人便可以接收到
前端发出消息,通过SockJS连接
代码示例
1、pom.xml引入jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2、WebSocketConfig.java 配置WebSocket
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;
/**
* @description: 配置WebSocket
* 注释@EnableWebSocketMessageBroker开始使用STOMP协议来传输基于代理(message broker)的消息
*
* @author: Shenshuaihu
* @version: 1.0
* @data: 2019-07-12 16:39
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
/**
* 注册STOMP协议的节点(endpoint),并映射的对应的URL。
* 注册一个STOMP的endpoint,并指定使用SickJS协议
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpointSSH").withSockJS();
registry.addEndpoint("/endpointChat").withSockJS();
}
/**
* 配置消息代理(Message Broker)
* 广播式应配置一个/topic 消息代理
* 点对点配置 /queue
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue","/topic");
}
}
3、两个发送和接收消息的实体 ElijahMessage.java ElijahResponse.java
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @description: 用于服务器想向浏览器发生消息
*
* @author: Shenshuaihu
* @version: 1.0
* @data: 2019-07-12 17:10
*/
@AllArgsConstructor
@Getter
public class ElijahResponse {
private String responseMessage;
}
import lombok.Getter;
/**
* @description: 用于接收服务器发送的消息
*
* @author: Shenshuaihu
* @version: 1.0
* @data: 2019-07-12 17:09
*/
@Getter
public class ElijahMessage {
private String name;
}
4、WsController.java WebSocket 控制器
import com.ch7.domain.ElijahMessage;
import com.ch7.domain.ElijahResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import java.security.Principal;
/**
* @description: WebSocket 控制器
*
* @author: Shenshuaihu
* @version: 1.0
* @data: 2019-07-12 17:15
*/
@Controller
@Slf4j
public class WsController {
/**
* 通过SimpMessagingTemplate 向浏览器发生消息
*/
@Autowired
private SimpMessagingTemplate messagingTemplate;
/**
* 当浏览器向服务端发生请求时,通过@MessageMapping映射/welcome这个地址
* 注解@MessageMapping使用方法与@RequestMapping相似
* @param message
* @return
* @throws Exception
*/
@MessageMapping("welcome")
@SendTo("/topic/getResponse")
public ElijahResponse say(ElijahMessage message) throws Exception {
Thread.sleep(3000);
return new ElijahResponse("Welcome, " + message.getName() + "!");
}
/**
* 点对点聊天
*
* @param principal 包含当前用户的信息
* @param msg
*/
@MessageMapping("/chat")
public void handleChar(Principal principal, String msg) {
// 判断发生给谁
if (principal.getName().equals("ssh")) {
// 发生消息给用户 接收消息的用户、浏览器订阅地址和消息内容
messagingTemplate.convertAndSendToUser("elijah",
"/queue/notifications",
principal.getName() + "-send: " + msg);
} else {
messagingTemplate.convertAndSendToUser("ssh",
"/queue/notifications",
principal.getName() + "-send: " + msg);
}
}
}
5、ws.html 发送消息和接收消息的页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>spring-boot-WebSocket-广播式</title>
<link rel="stylesheet" type="text/css" value="">
</head>
<body onload="disconnect()">
<noscript>
<h2 style="color: #ff0000">貌似你的浏览器不支持websocket</h2>
</noscript>
<div>
<h3>WebSocket</h3>
</div>
<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 src="/static/js/sockjs.min.js" charset="utf-8"></script>-->
<!--<script src="/static/js/stomp.min.js" charset="utf-8"></script>-->
<script th:src="@{/static/js/jquery.min.js}"></script>
<script th:src="@{/static/js/sockjs.min.js}"></script>
<script th:src="@{/static/js/stomp.min.js}"></script>
<script type="application/javascript">
var stompClient = null;
function setConnected(connected) {
console.log('Connected status: ' + connected);
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !disconnect;
document.getElementById("conversationDiv").style.visibility= (connected ? 'visible' : 'hidden');
// $("#conversationDiv").style.visibility = (connected ? 'visible' : 'hidden');
$('response').html();
}
/**
* 打开连接
*/
function connect() {
// 1、连接SockJS的endpoint
var socket = new SockJS('/endpointSSH');
// 2、使用STOMP 子协议的WebSocket客户端
stompClient = Stomp.over(socket);
// 3、连接websockst服务端
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
// 4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义
stompClient.subscribe('/topic/getResponse', function (respose) {
showResponse(JSON.parse(respose.body).responseMessage);
});
});
}
/**
* 关闭连接
*/
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendName() {
var name = $('#name').val();
// 5、通过 stompClient.send 向/welcome 目标发送消息 服务端在@MessageMapping中定义的
stompClient.send("/welcome", {}, JSON.stringify({'name': name}));
}
function showResponse(message) {
var response = $("#response");
response.html(message);
}
</script>
</body>
</html>
展示结果:
websocket-广播式.png在之前学习过socket连接对象也可通过WebSocket(不通过SockJS)连接
var socket = new WebSocket(url);
https://www.jianshu.com/p/bd0667b270ca
目前的是通过sockjs来
1、连接SockJS的endpoint
2、使用STOMP 子协议的WebSocket客户端
3、连接websockst服务端
4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义
5、通过 stompClient.send 向/welcome 目标发送消息 服务端在@MessageMapping中定义的
STOMP帧由命令,一个或多个头信息、一个空行及负载(文本或字节)所组成;
其中可用的COMMAND 包括:
CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT;
数据执行流程
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)/welcome 发生消息的格式为:
SEND destination:/welcome content-length:17
{"name":"elijah"}
从目标(destination)/topic/getResponse接收的格式为:
<<< MESSAGE destination:/topic/getResponse content-type:application/json;charset=UTF-8 subscription:sub-0 message-id:5nd0pfjf-73 content-length:38
{"responseMessage":"Welcome, elijah!"}
二、点对点式通信:
点对点多用于聊天室,一对一的通信,这里是基础的登录(springsecurity)到聊天室然后进行两天
代码示例
相关登录配置
1、pom.xml 引入jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、WebSecurityConfig.java 鉴权配置
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;
/**
* @description: 登录时的鉴权配置
*
* @author: Shenshuaihu
* @version: 1.0
* @data: 2019-07-15 18:31
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// /和login不拦截
.antMatchers("/", "login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 页面访问路径
.loginPage("/login")
// 登录成功转向/char
.defaultSuccessUrl("/chat")
.permitAll()
.and()
.logout()
.permitAll();
}
/**
* 内存中分配两个用户
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("elijah")
.password(new BCryptPasswordEncoder().encode("elijah"))
.roles("USER")
.and()
.withUser("ssh")
.password(new BCryptPasswordEncoder().encode("ssh"))
.roles("USER");
}
/**
* 静态资源不拦截
* @param web
* @throws Exception
*/
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resource/static/**");
}
}
3、login.html 登录页面
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>spring-boot-login</title>
<link rel="stylesheet" type="text/css" value="">
</head>
<body>
<div>
<h3>登录</h3>
</div>
<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>
聊天代码
1、相关配置
WebSocketConfig.java
WsController.java
方法在上面广播式代码里
两个页面也需要配置
2、char.html 聊天窗口
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>spring-boot-WebSocket-点对点式-home</title>
<link rel="stylesheet" type="text/css" value="">
</head>
<body>
<p>聊天室</p>
<form id="elijahForm">
<textarea rows="4" cols="60" name="text"></textarea>
<input type="submit">
</form>
<script th:src="@{/static/js/jquery.min.js}"></script>
<script th:src="@{/static/js/sockjs.min.js}"></script>
<script th:src="@{/static/js/stomp.min.js}"></script>
<script type="application/javascript">
$('#elijahForm').submit(function (e) {
e.preventDefault();
var text = $('#elijahForm').find('textarea[name="text"]').val();
sendSpittle(text);
});
// 1、连接SockJS的endpoint
var socket = new SockJS('/endpointChat');
// 2、使用STOMP 子协议的WebSocket客户端
stomp = Stomp.over(socket);
// 3、连接websockst服务端 //默认的和STOMP端点连接
stomp.connect("guest", "guest", function (frame) {
// 4、 通过stomp.subscribe订阅/topic/getResponse目标(destination)发生的消息,后端在@SendTo定义
stomp.subscribe("/user/queue/notifications", function (message) {
debugger;
var content = message.body;
var obj = JSON.parse(content);
console.log("admin用户特定的消息1:" + obj.message)
console.log("收到一条新消息:" + JSON.parse(respose.message).responseMessage)
$('#output').append("<b>Received: " + message.body + "</b><br/>")
});
});
function sendSpittle(text) {
stomp.send("/chat", {}, text);
}
$('#stop').click(function () {
socket.close();
})
</script>
<div id="output"></div>
</body>
</html>
展示结果:
websocket-点对点式.png三、其他说明:
1、基本概念:
STOMP:
STOMP(Simple Text-Orientated Messaging Protocol) 面向消息的简单文本协议
如何理解 STOMP 与 WebSocket 的关系: 1) HTTP协议解决了 web 浏览器发起请求以及 web 服务器响应请求的细节,假设 HTTP 协议 并不存在,只能使用 TCP 套接字来 编写 web 应用,你可能认为这是一件疯狂的事情;
-
直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用,因为没有高层协议,就需要我们定义应用间所发送消息的语义,还需要确保连接的两端都能遵循这些语义;
-
同 HTTP 在 TCP 套接字上添加请求-响应模型层一样,STOMP 在 WebSocket 之上提供了一个基于帧的线路格式层,用来定义消息语义;
2、所遇到的坑:
使用springsecurity时在内容中设置密码没有处理会报错
There is no PasswordEncoder mapped for the id "null"
是高版本的security所导致
.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("elijah")
.password(new BCryptPasswordEncoder().encode("elijah"))
.roles("USER")
参考文档:
https://blog.csdn.net/jqsad/article/details/77745379
https://www.jianshu.com/p/bd0667b270ca
参考书籍汪云飞 SpringBoot实战
网友评论