前言
记录学习历程,在学习笔记中有描述不正确的地方,欢迎小伙伴们评论指正
概念
WebSocket协议是基于TCP的一种新的网络协议,它实现了客户端与服务器全双工(full-duplex)通信,允许服务器主动发送信息到客户端。
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket通信协议于2011年被IETF定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C定为标准。
WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
运用场景:弹幕、网页聊天系统、实时监控、股票行情推送等。
SocketJS
1、是一个JavaScript库,提供一个类似WebSocket的对象
2、提供一个连贯的跨浏览器的JavaScriptAPI,在浏览器和web服务器之间创建一个低延迟、全双工、跨域的通信通道。
3、在底层SockJS首先尝试使用本地WebSocket,若失败,它可以使用各种浏览器特定的协议,并通过类似WebSocket的抽象方式呈现它们
4、SockJS旨在适用于所有现代浏览器和不支持WebSocket协议的环境
StompJS
它定义了可互操作的连接格式,以便任何可用的STOMP客户端都可以与任何STOMP消息代理进行通信,以在语言和平台之间提供简单而广泛的消息互操作性。简而言之、是一个简单的面向文本的消息传递协议。
WebSocket划分
单播(Unicast):点对点、私信
广播(Broadcast):所有人群进行的公告、发布订阅
多播/组播(Multicast):特定人群进行的群聊、发布订阅
实例
SpringBoot非注解方式(WebSocket、SockJS、Stomp)
创建Spring Boot项目,并在pom.xml注解中添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.3</version>
</dependency>
创建WebSocket配置类并实现WebSocketMessageBrokerConfigurer,并使用注解@EnableWebSocketMessageBroker
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
/**
* 注册websocket端点(基站)
* 发布或订阅消息时,需要先连接此端点
* setAllowedOrigins("*") 可有可无;*表示允许其他域进行访问
* 注:若使用setAllowedOrigins报错的情况下将setAllowedOrigins换成setAllowedOriginPatterns
* withSockJS 表示开启sockjs支持
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
//registry.addEndpoint("oneToOne").setAllowedOrigins("*").withSockJS();//点对点端点
registry.addEndpoint("oneToOne").setAllowedOriginPatterns("*").withSockJS();
registry.addEndpoint("broadcast").withSockJS();//广播端点
}
/**
* 配置消息代理【中介转发的意思】
* enableSimpleBroker:服务端推送给客户端路径的前缀
* setApplicationDestinationPrefixes:客户端发送数据到服务端路径的前缀
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic", "/user");
registry.setApplicationDestinationPrefixes("/info");
}
}
在Controller中使用@SendTo注解方式实现
@RestController
public class WebSocketController {
@MessageMapping("/getInfo")
@SendTo("/topic/get")
public GetOutMessage getInfo(GetInMessage in){
return new GetOutMessage(in.getContext());
}
}
在static文件下创建html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Parent</title>
<script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected){
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
$("#response").html();
}
function connect() {
var socket = new SockJS("/broadcast");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/get', function(response){
var response1 = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(response.body));
response1.appendChild(p);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendTo() {
var name = document.getElementById('name').value;
var context = document.getElementById('context').value;
console.info(name);
stompClient.send("/info/getInfo", {}, JSON.stringify({ 'name': name, 'context': context }));
}
</script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">Connect</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div id="conversationDiv">
<labal>消息来源</labal><input type="text" id="name" />
<labal>内容</labal><input type="text" id="context" />
<button id="send" onclick="sendTo();">Send</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Parent Child</title>
<script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected){
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
$("#response").html();
}
function connect() {
var socket = new SockJS("/broadcast");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/get', function(response){
var response1 = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(response.body));
response1.appendChild(p);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
</script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">Connect</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div id="conversationDiv">
<p id="response"></p>
</div>
</div>
</body>
</html>
在Controller中使用SimpMessagingTemplate方式实现。
注:在单播订阅时,convertAndSendToUser和convertAndSend在前端订阅的时候地址拼接的方式不同。代码中有注释说明
@RestController
public class WebSocketController {
@Autowired
private SimpMessagingTemplate template;
@MessageMapping("/oneToBroadcast")
public void oneToBroadcast(GetInMessage in) throws Exception{
in.setFrom(in.getName());
in.setTo("全体成员");
template.convertAndSend("/topic/getResponse", in.toString());
}
@MessageMapping("/toOne")
public void toOne(GetInMessage in) throws Exception{
//template.convertAndSendToUser(in.getTo(),"/message",in.toString());
//或者使用convertAndSend类实现点对点的私信传输
template.convertAndSend("/user/message/"+in.getTo(),new GetInMessage(in.getFrom()+"发送的信息"+in.getContext()));
}
@MessageMapping("/getInfo")
@SendTo("/topic/get")
public GetOutMessage getInfo(GetInMessage in){
return new GetOutMessage(in.getContext());
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Broadcast</title>
<script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected){
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
$("#response").html();
}
function connect() {
var socket = new SockJS("/broadcast");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/getResponse', function(response){
var response1 = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(response.body));
response1.appendChild(p);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function sendTo() {
var name = document.getElementById('name').value;
var context = document.getElementById('context').value;
console.info(name);
stompClient.send("/info/oneToBroadcast", {}, JSON.stringify({ 'name': name, 'context': context }));
}
</script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<button id="connect" onclick="connect();">Connect</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div id="conversationDiv">
<labal>消息来源</labal><input type="text" id="name" />
<labal>内容</labal><input type="text" id="context" />
<button id="send" onclick="sendTo();">Send</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ToOne</title>
<script src="/webjars/jquery/3.6.0/jquery.min.js"></script>
<script src="/webjars/sockjs-client/1.0.2/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected){
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
$("#response").html();
}
function connect() {
var socket = new SockJS("/oneToOne");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
//convertAndSendToUser的路径拼接方式
stompClient.subscribe('/user/'+document.getElementById('from').value+'/message', function(response){
//convertAndSend的路劲拼接方式
//stompClient.subscribe('/user/message/'+document.getElementById('from').value, function(response){
var response1 = document.getElementById('response');
var p = document.createElement('p');
p.style.wordWrap = 'break-word';
p.appendChild(document.createTextNode(response.body));
response1.appendChild(p);
});
});
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
console.log("断开连接");
}
function sendTo() {
console.log("发送信息");
var from = document.getElementById('from').value;
var to = document.getElementById('to').value;
var name = document.getElementById('name').value;
var context = document.getElementById('context').value;
console.info(name+"发送信息"+context);
stompClient.send("/info/toOne", {}, JSON.stringify({ 'from': from, 'to': to, 'name': name, 'context': context}));
}
</script>
</head>
<body onload="disconnect()">
<noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable
Javascript and reload this page!</h2></noscript>
<div>
<div>
<labal>用户</labal><input type="text" id="from" />
<labal>发送对象</labal><input type="text" id="to" />
<button id="connect" onclick="connect();">Connect</button>
<button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
<div id="conversationDiv">
<labal>标题</labal><input type="text" id="name" />
<labal>发送内容</labal><input type="text" id="context" />
<button id="send" onclick="sendTo();">Send</button>
<p id="response"></p>
</div>
</div>
</body>
</html>
当前实例可将SimpMessagingTemplate做一步提取。
@Service
public class WebSocketService {
@Autowired
private SimpMessagingTemplate template;
public void broadcast(GetInMessage inMessage){
template.convertAndSend("/topic/getResponse", inMessage.toString());
}
public void unicast(GetInMessage inMessage){
template.convertAndSendToUser(inMessage.getTo(),"/message",inMessage.toString());
//或者使用convertAndSend类实现点对点的私信传输
//template.convertAndSend("/user/message/"+in.getTo(),new GetInMessage(in.getFrom()+"发送的信息"+in.getContext()));
}
}
@RestController
public class WebSocketController {
@Autowired
private WebSocketService webSocketService;
@MessageMapping("/oneToBroadcast")
public void oneToBroadcast(GetInMessage in) throws Exception{
in.setFrom(in.getName());
in.setTo("全体成员");
webSocketService.broadcast(in);
}
@MessageMapping("/toOne")
public void toOne(GetInMessage in) throws Exception{
webSocketService.unicast(in);
}
@MessageMapping("/getInfo")
@SendTo("/topic/get")
public GetOutMessage getInfo(GetInMessage in){
return new GetOutMessage(in.getContext());
}
}
小结
@SendTo和SimpMessagingTemplate的区别?
SendTo不通用,固定发送给指定的订阅者。
SimpMessagingTemplate灵活,支持多种发送方式。
setAllowedOrigins("*")造成的异常
java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
解决方案在代码中有注释:就是将setAllowedOrigins换成setAllowedOriginPatterns完美解决。
事件监听器
监听器事件类型
SessionSubscribeEvent:订阅事件
SessionUnsubscribeEvent:取消订阅事件
SessionConnectEvent:建立连接事件
SessionDisconnectEvent:断开连接事件
注:
1、监听器类需要实现接口ApplicationListener<T>,T表示监听事件类型
2、在监听器类上加注解@Component
@Component
public class SubscribeEvenListener implements ApplicationListener<SessionSubscribeEvent> {
@Override
public void onApplicationEvent(SessionSubscribeEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("SubscribeEvenListener 订阅监听事件 类型:"+accessor.getCommand());
}
}
@Component
public class ConnectEventListener implements ApplicationListener<SessionConnectEvent> {
@Override
public void onApplicationEvent(SessionConnectEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
System.out.println("ConnectEventListener 连接监听器 类型:"+accessor.getCommand());
}
}
StompHeaderAccessor:简单消息传递协议中处理消息头的基类,通过该类可以获取到消息类型、会话ID等信息。
拦截器
WebSocket结合Spring Boot的拦截器:HandshakeInterceptor握手拦截器。
http握手拦截器,可以通过这个类的方法获取到request、reponse。
拦截器需要在websocket配置文件中启用:
.addInterceptors(new HttpSessionHandshakeInterceptor())
public class WebSocketInterceptor implements HandshakeInterceptor { //extends HttpSessionHandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
System.out.println("WebSocketInterceptor ------ beforeHandshake");
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
System.out.println("WebSocketInterceptor ------ afterHandshake");
}
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("oneToOne").addInterceptors(new HttpSessionHandshakeInterceptor()).setAllowedOriginPatterns("*").withSockJS();
registry.addEndpoint("broadcast").addInterceptors(new HttpSessionHandshakeInterceptor()).withSockJS();//广播端点
}
到这里...不得不说!!!这个拦截器
不能用!不能用!!不能用!!!
执行不进去!没找出来啥原因。来个小伙伴指点下哈
Channel拦截器
ChannelInterceptorAdapter频道拦截器适配器结合HandshakeInterceptor实现上线下线功能。
Channel拦截器和HTTP握手拦截器有所不同;握手拦截器对应HTTP请求,可以拿到request参数,包括sessionId之类的。而channel拦截器只是连接一个Channel(频道),类似管道,消息通过这些管道做一些操作。
实现
只看执行顺序。创建频道拦截器并实现ChannelInterceptor,并重写需要的方法。一般常用一下三个。
package learn.websocket.learnwebsocket.interceptor;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
/**
* @title: WebSocketChannelInterceptor
* @Description 频道拦截器
* @Date: 2021/12/8 16:13
* @Version 1.0
*/
public class WebSocketChannelInterceptor implements ChannelInterceptor {
/**
* 发送消息之前调用此方法
* @param message
* @param channel
* @return
*/
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
System.out.println("WebSocketChannelInterceptor ------ preSend");
return ChannelInterceptor.super.preSend(message, channel);
}
/**
* 调用发送消息时立即执行此方法
* @param message
* @param channel
* @param sent
*/
@Override
public void postSend(Message<?> message, MessageChannel channel, boolean sent) {
System.out.println("WebSocketChannelInterceptor ------ postSend");
//业务Code...
}
/**
* 无论是发送成功或者是异常之后都会调用此方法,一般用于资源释放
* @param message
* @param channel
* @param sent
* @param ex
*/
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel, boolean sent, Exception ex) {
System.out.println("WebSocketChannelInterceptor ------ afterSendCompletion");
}
}
在WebSocketConfig配置文件中添加 configureClientInboundChannel 和 configureClientOutboundChannel 方法。
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new WebSocketChannelInterceptor());
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.interceptors(new WebSocketChannelInterceptor());
}
Over,查看执行顺序...
注解方式
Spring Boot + WebSocket 自己给自己发送信息。直接上代码
package learn.websocket.annotation.learnwebsocketannotation.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @title: WebSocketConfig
* @Description 开启WebSocket支持
* @Date: 2021/12/8 9:16
* @Version 1.0
*/
@Configuration
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,@Bean会自动注册使用@ServerEndpoint注解声明的websocket端点endpoint
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
package learn.websocket.annotation.learnwebsocketannotation.utils;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @title: WebSocketUtils
* @Description 自己对自己发送消息
* @Date: 2021/12/8 9:27
* @Version 1.0
*/
@Component
@ServerEndpoint("/ws/test")
public class WebSocketUtils {
private static AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 连接建立成功调用的方法
* @param session
*/
@OnOpen
public void onOpen(Session session){
atomicInteger.incrementAndGet();
System.out.println("建立连接成功 onOpen 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
}
/**
* 连接关闭调用的方法
* @param session
*/
@OnClose
public void onClose(Session session){
atomicInteger.incrementAndGet();
System.out.println("连接关闭成功 onOpen 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
}
/**
* 接收到客户端消息后调用的方法
* @param msg 客户端发送的消息
* @param session
*/
@OnMessage
public void onMessage(String msg, Session session) throws IOException {
atomicInteger.incrementAndGet();
System.out.println("接收到客户端发送的消息 onMessage:"+ msg+" 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
session.getBasicRemote().sendText(msg);
}
/**
* 出现错误时调用的方法
* @param session
* @param throwable
*/
@OnError
public void onError(Session session, Throwable throwable){
atomicInteger.incrementAndGet();
System.out.println("错误 onError 线程数量:"+atomicInteger.get()+" sessionID:"+session.getId());
throwable.printStackTrace();
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Annotation Broadcast Demo</title>
<script type="text/javascript">
var websocket = null;
if('WebSocket' in window){
websocket=new WebSocket("ws://localhost:6005/ws/test");
}else{
console.log("Not Found WebSocket");
}
//建立连接回调函数
websocket.onopen=function (event){
setMessage("建立连接");
}
//关闭连接回调函数
websocket.onclose=function (event){
setMessage("关闭连接");
}
//发送消息回调函数
websocket.onmessage=function (event){
setMessage("发送消息:"+ event.data);
}
//出现错误回调函数
websocket.onerror=function (event){
setMessage("错误");
}
function setMessage(innerHTML) {
document.getElementById('div_msg').innerHTML += innerHTML + '<br/>';
}
function sendto(){
var _txtMsg=document.getElementById("txtMsg").value;
console.log(_txtMsg);
websocket.send(_txtMsg);
}
function closeto(){
websocket.close();
}
</script>
</head>
<body>
<input type="text" id="txtMsg" />
<button onclick="sendto()">发送消息</button>
<button onclick="closeto()">断开连接</button>
<div id="div_msg"></div>
</body>
</html>
Over...