1、websocket
WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。
应用场景:通知通告、弹幕,网页聊天系统,实时监控,股票行情推送等
sockjs:
1、是一个浏览器JavaScript库,提供了一个类似WebSocket的对象。
2、提供了一个连贯的跨浏览器的JavaScriptAPI,在浏览器和Web服务器之间创建了一个低延迟,全双工,跨域的通信通道
3、在底层SockJS首先尝试使用本地WebSocket。如果失败了,它可以使用各种浏览器特定的传输协议,并通过类似WebSocket的抽象方式呈现它们
4、SockJS旨在适用于所有现代浏览器和不支持WebSocket协议的环境。
stompjs:面向文本的消息传递协议。
STOMP与WebSocket 的关系:
1、HTTP协议解决了web浏览器发起请求以及web服务器响应请求的细节,假设HTTP协议不存在,
只能使用TCP套接字来编写web应用,你可能认为这是一件疯狂的事情;
2、直接使用WebSocket(SockJS)就很类似于使用TCP套接字来编写web应用,因为没有高层协议,
就需要我们定义应用间发送消息的语义,还需要确保连接的两端都能遵循这些语义;
3、同HTTP在TCP套接字上添加请求-响应模型层一样,STOMP在WebSocket之上提供了一个基于帧的
线路格式层,用来定义消息语义.
2、websocket广播、单播、组播
单播(Unicast):
点对点,私信私聊
广播(Broadcast)(所有人):
游戏公告,发布订阅
多播也称为组播:
特定的群体消息通信
3、广播基本Demo
webjars:
1、方便统一管理
2、主要解决前端框架版本不一致,文件混乱等问题
3、把前端资源,打包成jar包,借助maven工具进行管理
<!-- webSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator</artifactId>
</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>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>3.3.7</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.1.0</version>
</dependency>
<!-- webSockte-->
---不是前后分离的Demo HTML Demo暂不放了 核心代码就这些
---前端使用sockJs+stompJs
//定义一个Stopm客户端
var stompClient = null;
//连接后端websocket
function connect() {
var socket = new SockJS('/endpoint-websocket'); //连接上端点(基站)
stompClient = Stomp.over(socket); //用stomp进行包装,规范协议
stompClient.connect({}, function (frame) {
//订阅后端消息主题 '/topic/game_chat' function是后端推送消息前端接受回调
stompClient.subscribe('/topic/game_chat', function (result) {
console.info(result)
});
});
}
//断开连接
function disconnect() {
if (stompClient !== null) {
stompClient.disconnect();
}
}
//前端向后端发送消息
function sendName() {
var message = "消息测试";
stompClient.send("/app/v1/chat", {}, JSON.stringify({'content':message}));
}
------后端代码 原生编码
创建接收与发送module
public class InMessage {
private String fromName;
private String toName;
private String content;
private Date time;
..........
..........
public class OutMessage {
private String fromName;
private String content;
private Date time = new Date( );
..........
..........
创建消息controller
@Controller
public class GameV1Controller {
/**
* @MessageMapping路径映射 针对于websocket消息映射
* @SendTo 发送给前端一个topic主题
* @param inMessage
* @return
*/
@MessageMapping("/v1/chat")
@SendTo("/topic/game_chat")
public OutMessage onVOneMessage(InMessage inMessage){
return new OutMessage(inMessage.getContent());
}
}
创建webSocket配置文件
package com.lmr.config;
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;
/**
* Created by ldd on 2019/12/30.
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
//注册端点(基站) 任何发布订阅消息时吗,需要连接此端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* /endpoint-websocket:基站名称
* setAllowedOrigins( "*" ):非必须 *表示允许其他域进行连接
* withSockJS:表示支持sockJs连接
*/
registry.addEndpoint( "/endpoint-websocket" ).setAllowedOrigins( "*" ).withSockJS();
}
/**
* 配置消息代理(中介)
* enableSimpleBroker:服务端推送给客户端的路径前缀
* setApplicationDestinationPrefixes:客户端发送给服务端的路径前缀
*
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker( "/topic" );
registry.setApplicationDestinationPrefixes( "/app" );
}
}
4、@SendTo注解和SimpMessagingTemplate的区别
1、SendTo 不通用,固定发送给指定的订阅者
2、SimpMessagingTemplate 灵活,支持多种发送方式
-----使用SimpMessagingTemplate模板推送消息
----前端只需要修改sendName方法
function sendName() {
var objMessage = {
toName:"/topic/game_chat",
content:$("#content").val()
}
stompClient.send("/app/v2/chat", {}, JSON.stringify(objMessage));
}
-----后端服务器代码编写
创建WebSocketService
package com.lmr.service;
import com.lmr.module.InMessage;
import com.lmr.module.OutMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
/**
* Created by ldd on 2019/12/31.
*/
@Service
public class WebSocketService {
//注入消息模板
@Autowired
private SimpMessagingTemplate messagingTemplate;
public void sendTopicMessage(InMessage inMessage){
messagingTemplate.convertAndSend( inMessage.getToName(),new OutMessage( inMessage.getContent() +"Template") );
}
}
创建V2 Controller
package com.lmr.controller.v2;
import com.lmr.module.InMessage;
import com.lmr.service.WebSocketService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.stereotype.Controller;
/**
* Created by ldd on 2019/12/30.
*/
@Controller
public class GameV2Controller {
@Autowired
private WebSocketService webSocketService;
/**
* @MessageMapping路径映射 针对于websocket消息映射
* @param inMessage
* @return
*/
@MessageMapping("/v2/chat")
public void onVOneMessage(InMessage inMessage){
webSocketService.sendTopicMessage( inMessage );
}
}
5、websocket 4类的监听器介绍和使用
注意点:
1、需要监听器类需要实现接口ApplicationListener<T> T表示事件类型,下列几种都是对应的websocket事件类型
websocket模块监听器类型:
SessionSubscribeEvent 订阅事件
SessionUnsubscribeEvent 取消订阅事件
SessionDisconnectEvent 断开连接事件
SessionConnectEvent 建立连接事件
@Component
public class SessionConnectEventListener implements ApplicationListener<SessionConnectEvent>{
/**
* 连接事件监听器
* @param sessionConnectEvent
*/
@Override
public void onApplicationEvent(SessionConnectEvent sessionConnectEvent) {
StompHeaderAccessor wrap = StompHeaderAccessor.wrap( sessionConnectEvent.getMessage() );
System.out.println("连接事件监听器"+wrap.getMessage());
}
}
@Component
public class SessionDisconnectEventListener implements ApplicationListener<SessionDisconnectEvent>{
/**
* 断开连接事件监听器
* @param sessionDisconnectEvent
*/
@Override
public void onApplicationEvent(SessionDisconnectEvent sessionDisconnectEvent) {
StompHeaderAccessor wrap = StompHeaderAccessor.wrap( sessionDisconnectEvent.getMessage() );
System.out.println("断开连接事件监听器"+wrap.getMessage());
}
}
@Component
public class SessionSubscribeEventListener implements ApplicationListener<SessionSubscribeEvent>{
/**
* 订阅事件监听器
* @param sessionSubscribeEvent
*/
@Override
public void onApplicationEvent(SessionSubscribeEvent sessionSubscribeEvent) {
StompHeaderAccessor wrap = StompHeaderAccessor.wrap( sessionSubscribeEvent.getMessage() );
System.out.println("订阅事件监听器"+wrap.getMessage());
}
}
@Component
public class SessionUnsubscribeEventListener implements ApplicationListener<SessionUnsubscribeEvent>{
/**
* 取消订阅事件监听器
* @param sessionUnsubscribeEvent
*/
@Override
public void onApplicationEvent(SessionUnsubscribeEvent sessionUnsubscribeEvent) {
StompHeaderAccessor wrap = StompHeaderAccessor.wrap( sessionUnsubscribeEvent.getMessage() );
System.out.println("取消订阅事件监听器"+wrap.getMessage());
}
}
6、websocket结合SpringBoot相关拦截器使用
拦截器介绍,springBoot结合websocket相关拦截器使用,握手拦截器的开发和使用
HandshakeInterceptor
实现步骤:
1、编写一个类,实现一个接口HandshakeInterceptor;
2、在websocket配置里面启用.addInterceptors(new 自定义类())
注意:
1、Http握手拦截器,前端连接基站才会触发
2、前端推送后台消息@MessageMapping 不会触发握手拦截器
package com.lmr.intecepter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;
/**
* Created by ldd on 2020/1/2.
*
*
* Http握手拦截器,前端连接基站才会触发
* 前端推送后台消息@MessageMapping 不会触发握手拦截器
*/
@Component
public class HttpHandShakeIntecepter implements HandshakeInterceptor{
@Override
public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest)serverHttpRequest;
HttpServletRequest request = servletRequest.getServletRequest();
HttpSession session = request.getSession();
System.out.println(session.getId());
Cookie[] cookies = request.getCookies();
for(int i=0;i<cookies.length;i++){
System.out.println("cookies-name:"+cookies[i].getName()+"cookies-value:"+cookies[i].getValue());
}
System.out.println("webSocket前置拦截器");
return true;
}
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, @Nullable Exception e) {
System.out.println("webSocket后置拦截器");
}
}
package com.lmr.config;
import com.lmr.intecepter.HttpHandShakeIntecepter;
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;
/**
* Created by ldd on 2019/12/30.
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
//注册端点(基站) 任何发布订阅消息时吗,需要连接此端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* /endpoint-websocket:基站名称
* setAllowedOrigins( "*" ):非必须 *表示允许其他域进行连接
* withSockJS:表示支持sockJs连接
*/
registry.addEndpoint( "/endpoint-websocket" ).addInterceptors( new HttpHandShakeIntecepter() ).setAllowedOrigins( "*" ).withSockJS();
}
/**
* 配置消息代理(中介)
* enableSimpleBroker:服务端推送给客户端的路径前缀
* setApplicationDestinationPrefixes:客户端发送给服务端的路径前缀
*
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker( "/topic","/chat" );
registry.setApplicationDestinationPrefixes( "/app" );
}
}
7、channel频道拦截器使用
使用方法:
1、ChannelInterceptorAdapter 频道拦截器适配器,具体实现的接口是ChannelIntecepter
2、需要ChannelInterceptorAdapter子类重写override对应的方法,实现自己的逻辑,
主要是public void postSend(Message<?> message, MessageChannel channel, boolean sent)
3、ChannelInterceptorAdapter子类需要在配置Websocket的配置里面加入
4、在配置类里面加入
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors( new SocketChannelIntecepter());
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.interceptors( new SocketChannelIntecepter());
}
package com.lmr.intecepter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* Created by ldd on 2020/1/2.
* 频道拦截器
*/
@Component
public class SocketChannelIntecepter extends ChannelInterceptorAdapter {
/**
* 在消息被实际发送到频道之前调用
*/
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
System.out.println( "消息被实际发送到频道之前调用->preSend" );
return super.preSend( message, channel );
}
/**
* 发送消息调用后立即调用
*/
@Override
public void postSend(Message<?> message, MessageChannel channel,
boolean sent) {
System.out.println( "发送消息调用后立即调用->postSend" );
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap( message );//消息头访问器
if (headerAccessor.getCommand() == null) return;// 避免非stomp消息类型,例如心跳检测
Map<String, Object> objectMap = headerAccessor.getSessionAttributes();
if(objectMap == null || objectMap.size() <= 0) return;
String sessionId = objectMap.get( "sessionId" ).toString();
System.out.println( "SocketChannelIntecepter -> sessionId = " + sessionId );
switch (headerAccessor.getCommand()) {
case CONNECT:
System.out.println("连接成功");
break;
case DISCONNECT:
System.out.println("断开连接");
break;
case SUBSCRIBE:
System.out.println("订阅消息");
break;
case UNSUBSCRIBE:
System.out.println("取消订阅");
break;
default:
break;
}
}
/**
* 在完成发送之后进行调用,不管是否有异常发生,一般用于资源清理
*/
@Override
public void afterSendCompletion(Message<?> message, MessageChannel channel,
boolean sent, Exception ex) {
System.out.println( "完成发送之后进行调用->afterSendCompletion" );
super.afterSendCompletion( message, channel, sent, ex );
}
}
package com.lmr.config;
import com.lmr.intecepter.HttpHandShakeIntecepter;
import com.lmr.intecepter.SocketChannelIntecepter;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
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;
/**
* Created by ldd on 2019/12/30.
*/
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
//注册端点(基站) 任何发布订阅消息时吗,需要连接此端点
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
/**
* /endpoint-websocket:基站名称
* setAllowedOrigins( "*" ):非必须 *表示允许其他域进行连接
* withSockJS:表示支持sockJs连接
*/
registry.addEndpoint( "/endpoint-websocket" ).addInterceptors( new HttpHandShakeIntecepter() ).setAllowedOrigins( "*" ).withSockJS();
}
/**
* 配置消息代理(中介)
* enableSimpleBroker:服务端推送给客户端的路径前缀
* setApplicationDestinationPrefixes:客户端发送给服务端的路径前缀
*
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker( "/topic","/chat" );
registry.setApplicationDestinationPrefixes( "/app" );
}
//配置客户端进入通道
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors( new SocketChannelIntecepter() );
}
//配置客户端退出通道
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.interceptors( new SocketChannelIntecepter() );
}
}
----连接
C54EF41FACA3D49B02C3948434A69EF8
cookies-name:JSESSIONIDcookies-value:C54EF41FACA3D49B02C3948434A69EF8
webSocket前置拦截器
webSocket后置拦截器
消息被实际发送到频道之前调用->preSend
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion
连接事件监听器null
消息被实际发送到频道之前调用->preSend
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion
消息被实际发送到频道之前调用->preSend
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion
订阅事件监听器null
---发送消息
消息被实际发送到频道之前调用->preSend
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion
接收消息1111
消息被实际发送到频道之前调用->preSend
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion
---断开连接
消息被实际发送到频道之前调用->preSend
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion
消息被实际发送到频道之前调用->preSend
断开连接事件监听器null
发送消息调用后立即调用->postSend
消息被实际发送到频道之前调用->preSend
完成发送之后进行调用->afterSendCompletion
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion
消息被实际发送到频道之前调用->preSend
发送消息调用后立即调用->postSend
完成发送之后进行调用->afterSendCompletion