理解几个知识点
websocket.jpg
- 订阅地址 :如stompEndpointRegistry.addEndpoint("/endpointWechat") 或使用@ServerEndpoint创建
- 容许跨域
- 是否开启SockJS支持
- 推送:声明SimpMessagingTemplate ,调用convertAndSend方法(或者使用@SendTo和@SendToUser注解)
- 一对一发送消息
- Principal:身份验证和授权
- stomp协议:STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。
- WebSocket(stomp服务端)
WebSocketSession 发送的消息类型(sendMessage)
前端涉及js
- jquery
- sockJs :
- stomp.min.js(stomp客户端)
- STOMP Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互
js 前端链接websocket的方式
STOMP 链接成功、失败、主动断开等参阅
STOMP 客户端 API 整理:https://blog.csdn.net/jqsad/article/details/77745379
html页面,连接stomp服务端,订阅/topic/myTop的消息(订阅topic或myTop)
方式1:
var socket = new SockJS('/endpointWechat');
方式2
//前台 js 中 new SockJS 的时候,一起是3个参数项的,另外2个参数项可以传递目前是哪个用户的标识,这样就可以在后台区分出来连接是哪个用户建立的
var socket = new SockJS(url, undefined, {protocols_whitelist: ['websocket']});
方式3:直接携带参数
var socket = new SockJS('/endpointWechat'+ '?token='+str_token); //'?token=token8888'
Android端的链接方式
SpringBoot 使用的websocket 协议,不是标准的websocket协议,使用的是名称叫做STOMP的协议。
要想与js方式调用:stompClient.send("/sendServer", {}, JSON.stringify({ 'name': message }));,需要Android采用STOMP方式调用
更多细节参考
stomp协议 官方:http://stomp.github.io/
csdn 大神博客:http://blog.csdn.net/chszs/article/details/5200554
iteye 大神博客 http://diaocow.iteye.com/blog/1725186 (务必看一下,了解协议的一些使用)
SpringBoot webSocket 发送广播、点对点消息,Android接收
WebSocket可以应用于即时通信等场景,比如现在直播很火热,直播中的弹幕也可以使用WebSocket去实现。
WebSocket的协议内容可以见 The WebSocket Protocol,讲得最全面的官方说明。简单介绍可以见维基百科WebSocket
在Android客户端,一般可以使用下面的库完成WebSocket通信
- okhttp,一般人我不告诉他okhttp还可以用来进行WebSocket通信
- Java-WebSocket,纯java实现的WebSocket客户端和服务端实现
Android最佳实践——深入浅出WebSocket协议:https://blog.csdn.net/blueangle17/article/details/80701152
springboot 的工程
image.png
pom.xml引入
<!--添加websocket依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--添加alibaba 的fastjson引用-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.46</version>
</dependency>
编写MyHandShakeInterceptor类(主要作用用于对握手前检查合法性)
// 初始化对象,拦截握手,发生在链接之前
@Component
public class MyHandShakeInterceptor implements HandshakeInterceptor {
private static final Logger log = LoggerFactory.getLogger(MyHandShakeInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
//http://localhost:8080/endpointWechat/948/dtdzvrrs/websocket?token=token8888
//js调用:
// var host="http://localhost:8080";
// var socket = new SockJS(host+'/endpointWechat' + '?token=token8888');
log.info("this.getClass().getCanonicalName() = {},在这里决定是否允许链接,http协议转换websoket协议进行前, 握手前Url = {}",this.getClass().getCanonicalName(),request.getURI());
//System.out.println(this.getClass().getCanonicalName() + " 在这里决定是否允许链接,http协议转换websoket协议进行前, 握手前"+request.getURI() );
// http协议转换websoket协议进行前,可以在这里通过session信息判断用户登录是否合法
//request.getURI().getPath(); // /endpointWechat/896/mdoqjqia/websocket
//request.getURI().getHost();//localhost
//request.getURI().string ; // http://localhost:8080/endpointWechat/896/mdoqjqia/websocket?token=token8888
//request.getURI().getQuery() ; // token=token8888
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpRequest = servletServerHttpRequest.getServletRequest();
String myToken = httpRequest.getParameter("token");
if (null != myToken && !StringUtils.isEmpty(myToken)){
WebSocketSession webSocketSession = SocketManager.get(myToken);
if (webSocketSession != null){
log.info("token = {},已经在建立链接列表,不允许重复链接",myToken);
}else {
return true;
}
}
}
return false; //不允许建立链接
//return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
//握手成功后,
System.out.println(this.getClass().getCanonicalName() + "握手成功后...");
}
}
不允许创建连接的情况
成功连接的情况
成功连接的情况
配置类WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
private MyHandShakeInterceptor myHandShakeInterceptor;
@Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
//以 /endpointWechat端点,客户端就可以通过这个端点来进行连接
stompEndpointRegistry.addEndpoint("/endpointWechat").setAllowedOrigins("*")
.withSockJS();//开启SockJS支持
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic","/user"); //客户端订阅服务的前缀
registry.setUserDestinationPrefix("/user"); //开启一对一发送消息
}
}
控制器WebScoketController
/**
* 创建人:牵手生活
* 创建时间:2019-01-14 17:17
*/
@Controller
public class WebSocketController {
@Autowired
private SimpMessagingTemplate simpMessagingTemplate; //声明SimpMessagingTemplate (或者使用@SendTo和@SendToUser注解),SimpMessagingTemplate可以在需要用到推送的地方如Controller,service,Component等地方
private static final Logger log = LoggerFactory.getLogger(WebSocketController.class);
// 收到消息记数
private AtomicInteger count = new AtomicInteger(0);
/**
* @MessageMapping 指定要接收消息的地址,类似@RequestMapping。除了注解到方法上,也可以注解到类上
* @MessageMapping("/receive") 对应html中的 stompClient.send("/app/receive", {}, JSON.stringify({ 'name': name }));
* 多出来的“/app"是WebSocKetConfig中定义的,如不定义,则HTML中对应改为stompClient.send("/receive")
* @SendTo默认 消息将被发送到与传入消息相同的目的地
* 消息的返回值是通过{@link org.springframework.messaging.converter.MessageConverter}进行转换
* @SendTo("/topic/getResponse") 指定订阅路径,对应HTML中的stompClient.subscribe('/topic/getResponse', ……)
* 意味将信息推送给所有订阅了"/topic/getResponse"的用户
* @param requestMessage
* @return
*/
@MessageMapping("/receive")
@SendTo("/topic/getResponse") //topic是广播全局通讯
public ResponseMessage receive(RequestMessage requestMessage){
log.info("receive message = {}" , JSONObject.toJSONString(requestMessage));
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setResponseMessage("响应消息WebSocketController receive [" + count.incrementAndGet() + "] records:"+JSONObject.toJSONString(requestMessage));
return responseMessage;
}
/**
* 客户端发消息,服务端接收
*
* @param requestMessage
*/
// 相当于RequestMapping
@MessageMapping("/sendServer")
public void sendServer(RequestMessage requestMessage) {
log.info("sendServer 客服端发送的,不需要发回给客户端message:{}", JSONObject.toJSONString(requestMessage));
}
@MessageMapping("/sendServer_str")
public void sendServer_str(String message) {
log.info("sendServer 客服端发送的,不需要发回给客户端message:{}", message);
}
/**
* 客户端发消息,大家都接收,相当于直播说话
*
* @param message
* @return
*/
@MessageMapping("/sendAllUser_str")
@SendTo("/topic/sendTopic_str")
public String sendAllUser_str(String message) {
// 也可以采用template方式
return "服务的处理后的:"+message;
}
@MessageMapping("/sendAllUser")
@SendTo("/topic/sendTopic")
public ResponseMessage sendAllUser(RequestMessage requestMessage) {
log.info("sendTopic 请求message = {}" , JSONObject.toJSONString(requestMessage));
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setResponseMessage(JSONObject.toJSONString(requestMessage));
return responseMessage;
}
/**
* 点对点用户聊天,这边需要注意,由于前端传过来json数据,所以使用@RequestBody
* 这边需要前端开通var socket = new SockJS(host+'/myUrl' + '?token=token8888'); token为指定name
* @param map
*/
@MessageMapping("/sendMyUser")
public void sendMyUser(@RequestBody Map<String, String> map) {
log.info("sendMyUser 请求 map = {}", map);
WebSocketSession webSocketSession = SocketManager.get(map.get("name"));
if (webSocketSession != null) {
log.info("sendMyUser sessionId = {}", webSocketSession.getId());
//生成IJSONResult对象的data数据
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setResponseMessage("响应消息WebSocketController sendMyUser records:"+map.get("message"));
simpMessagingTemplate.convertAndSendToUser(map.get("name"), "/queue/sendUser", IJSONResult.ok(responseMessage));
//simpMessagingTemplate.convertAndSendToUser(map.get("name"), "/queue/sendUser", JSONObject.toJSONString(responseMessage)); //ok
}
}
@MessageMapping("/sendMyUser_obj")
//@SendToUser("/user/queue/sendUser_obj") //添加看看
public ResponseMessage sendMyUser_obj(RequestMessage requestMessage) {
log.info("sendMyUser message = {}" , JSONObject.toJSONString(requestMessage));
ResponseMessage responseMessage = new ResponseMessage();
responseMessage.setResponseMessage("响应消息WebSocketController sendMyUser [" + count.incrementAndGet() + "] records:"+JSONObject.toJSONString(requestMessage));
return responseMessage;
}
//http://localhost:8080//wechatTask/websocket/index 转发到页面
@RequestMapping(value="/wechatTask/websocket/index")
public String websocketIndex(HttpServletRequest req){
log.info("websocketIndex接口的 req.getRemoteHost(){}" , req.getRemoteHost());
return "websocket/simple/websocket-index";
}
}
添加thymeleaf的模板-websocket-index.html
说明
客户端可以通过使用Stomp.js和sockjs-client连接
var socket = new SockJS('/endpointWechat'+ '?token='+str_token);
或
socket连接对象也可通过WebSocket(不通过SockJS)连接
var socket=new WebSocket('/endpointWechat'+ '?token='+str_token);
stompClient.connect()方法签名:
client.connect(headers, connectCallback, errorCallback);
websocket-index.html如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Spring Boot+WebSocket例子</title>
<script src="https://cdn.bootcss.com/sockjs-client/1.3.0/sockjs.min.js"></script>
<script src="https://cdn.bootcss.com/stomp.js/2.3.3/stomp.min.js"></script>
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
<!--引入jqurey库-->
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<body onload="disconnect()" >
<div>
<p> 创建链接:var socket = new SockJS(host+'/myUrl' + '?token=token8888')</p>
<p> token 是你上面输入的token 或username</p>
<p>点击链接,如果链接成功,则可以与后台通过websocket进行通信 </p>
<p>输入你要发送的内容(这里是你的名字--以后改为json对象)</p>
<p>点击发送,会把输入的内容通过websocket发送到后台</p>
<p>查看log 更多细节查看chrome的开发者选项中的控制台</p>
</div>
<div >
<label >输入你新建链接是的token或name(用于点对点通信)?</label>
<input type="text" id="mytoken" value="wxid_on8oksh88zo22" placeholder="Your token/name here...如:4567"></input>
</div>
<div >
<label id = "state-info" >***连接状态-未连接***</label>
</div>
</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="message" value="fdsfs"/>
<button id="sendName" onclick="sendName();">发送</button>
<button id="sendServer" onclick="sendServer();">发送sendServer</button>
<button id="sendTopic" onclick="sendTopic();">发送sendTopic</button>
<button id="sendMyUser" onclick="sendMyUser();">发送点对点</button>
<!--用于显示通过websocket的响应数据-->
<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() {
//判断是否输入token
var val = $('#mytoken').val();
var str_token = val.replace(/(^\s*)|(\s*$)/g, '');//去除空格;
if (str_token == '' || str_token == undefined || str_token == null){
alert('建立链接前,需要输入token');
return
}
// websocket的连接地址,此值等于WebSocketMessageBrokerConfigurer中registry.addEndpoint("/endpointWechat").withSockJS()配置的地址
//var socket = new SockJS('/endpointWechat'); //
//建立连接对象(还未发起连接)
var socket = new SockJS('/endpointWechat'+ '?token='+str_token); //'?token=token8888'
//利用Stomp协议创建socket客户端
stompClient = Stomp.over(socket);
/**
* 调用stompClient中的connect方法来连接服务端,
* 连接成功之后调用setConnected方法,该隐藏的隐藏,该显示的显示
*/
stompClient.connect({},
function(frame) {
setConnected(true);
console.log('连接成功Connected: ' + frame);
document.getElementById("state-info").innerHTML = "***连接成功***";
// 客户端订阅消息的目的地址:此值WebSocketController中被@SendTo("/topic/getResponse")注解的里配置的值
stompClient.subscribe('/topic/getResponse', function(respnose){ //2
showResponse(JSON.parse(respnose.body).responseMessage);
});
//订阅queue===接收广播的
stompClient.subscribe('/topic/sendTopic', function(response) {
showResponse(JSON.parse(response.body).responseMessage);
//showResponse((respnose.body).responseMessage);//string返回的处理
});
//接收发个单个人的===点对点===????????为什么会收不到
var mytoken = $('#mytoken').val();
stompClient.subscribe('/user/queue/sendUser', function(response) { // '/user/queue/sendUser'===/'+mytoken+'
//showResponse(JSON.parse(response.body).responseMessage);
showResponseBody(response.body);//直接显示response.body
});
},
function errorCallBack (error) {
// 连接失败时(服务器响应 ERROR 帧)的回调方法
document.getElementById("state-info").innerHTML = "***连接失败***";
console.log('连接失败【' + error + '】');
}
);//end for connected
}
function disconnect() {
if (stompClient != null) {
stompClient.disconnect();
}
setConnected(false);
document.getElementById("state-info").innerHTML = "***连接未连接***";
console.log("Disconnected");
}
//后台采用@SendTo("/topic/getResponse")注解
function sendName() {
var message = $('#message').val();
// 客户端消息发送的目的:服务端使用WebSocketController中@MessageMapping("/receive")注解的方法来处理发送过来的消息
stompClient.send("/receive", {}, JSON.stringify({ 'name': message }));
}
//后台采用@SendTo("/topic/getResponse")注解
function sendName() {
var message = $('#message').val();
// 客户端消息发送的目的:服务端使用WebSocketController中@MessageMapping("/receive")注解的方法来处理发送过来的消息
stompClient.send("/receive", {}, JSON.stringify({ 'name': message }));
}
function sendTopic() { //发公告===类似发群公告
var message = $('#message').val();
// 客户端消息发送的目的:服务端使用WebSocketController中@MessageMapping("/receive")注解的方法来处理发送过来的消息
stompClient.send("/sendAllUser", {}, JSON.stringify({ 'name': message }));
}
function sendServer() { //发送到服务端,server不返回数据
var message = $('#message').val();
// 客户端消息发送的目的:服务端使用WebSocketController中@MessageMapping("/receive")注解的方法来处理发送过来的消息
stompClient.send("/sendServer", {}, JSON.stringify({ 'name': message }));
}
function sendAllUser() { //发公告===类直接发到群中,其实跟公告一样
var message = $('#message').val();
// 客户端消息发送的目的:服务端使用WebSocketController中@MessageMapping("/receive")注解的方法来处理发送过来的消息
stompClient.send("/sendAllUser", {}, JSON.stringify({ 'name': message }));
}
//发送点对点通信---后台对应的采用的是simpMessagingTemplate.convertAndSendToUser
function sendMyUser() {
var message = $('#message').val();
var mytoken = $('#mytoken').val();
// 客户端消息发送的目的:服务端使用WebSocketController中@MessageMapping("/receive")注解的方法来处理发送过来的消息
//stompClient.send("/sendMyUser", {}, JSON.stringify({ 'name': message }));
stompClient.send("/sendMyUser", {}, JSON.stringify({name:mytoken,message:message}));
}
//显示消息
function showResponse(message) {
var response = $("#response");
response.html("按ResponseMessage对象返回的responseMessage字段数据:\""+message + "<br\>" + response.html());
}
function showResponseBody(response_body) {
var response = $("#response");
response.html("按websocket api 返回格式的response.body数据:"+response_body + "<br\>" + response.html());
}
</script>
</body>
</html>
通过controller转发到html访问websocket
http://localhost:8080//wechatTask/websocket/index
websocket链接情况
添加Socket链接的管理器SocketManager
image.pngpublic class SocketManager {
private static final Logger log = LoggerFactory.getLogger(SocketManager.class);
private static ConcurrentHashMap<String, WebSocketSession> manager = new ConcurrentHashMap<String, WebSocketSession>();
public static void add(String key, WebSocketSession webSocketSession) {
log.info("新添加webSocket连接 {} ", key);
manager.put(key, webSocketSession);
}
public static void remove(String key) {
log.info("移除webSocket连接 {} ", key);
manager.remove(key);
}
public static WebSocketSession get(String key) {
log.info("获取webSocket连接 {}", key);
return manager.get(key);
}
public static int connectedCount(){
return manager.size();
}
}
添加WebSocketDecoratorFactory(用于管理websocket的链接与断开)
/**
* 服务端和客户端在进行握手时会被执行
*/
@Component
public class WebSocketDecoratorFactory implements WebSocketHandlerDecoratorFactory {
private static final Logger log = LoggerFactory.getLogger(WebSocketDecoratorFactory.class);
@Override
public WebSocketHandler decorate(WebSocketHandler handler) {
return new WebSocketHandlerDecorator(handler) {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("有人连接啦 sessionId = {}", session.getId()+"链接数量"+SocketManager.connectedCount()+"****连接数一直为0则是校验了taken");
Principal principal = session.getPrincipal();
if (principal != null) {
log.info("key = {} 存入", principal.getName());
// 身份校验成功,缓存socket连接
SocketManager.add(principal.getName(), session);
}
super.afterConnectionEstablished(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("有人退出连接啦 sessionId = {}", session.getId()+SocketManager.connectedCount());
Principal principal = session.getPrincipal();
if (principal != null) {
// 身份校验成功,移除socket连接
SocketManager.remove(principal.getName());
}
super.afterConnectionClosed(session, closeStatus);
}
};
}
}
WebSocketConfig中注入WebSocketDecoratorFactory,并重写configureWebSocketTransport方法
image.png java spring 后台日志 2个浏览器连接后收到不同的信息
关于websocket的文章收录
JMeter测试WebSocket的经验总结
springboot集成websocket需要的都在这里
Spring Boot系列十六 WebSocket简介和spring boot集成简单消息代理
使用spring boot +WebSocket实现(后台主动)消息推送-@ServerEndpoint创立websocket endpoint--实现onOpen、onError、onClose
//socket = new WebSocket("ws://localhost:9094/starManager/websocket/张三");
var socket;
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
//socket = new WebSocket("ws://localhost:9094/starManager/websocket/张三")
socket = new WebSocket("ws://localhost:9094/starManager/websocket");
//打开事件
socket.onopen = function() {
console.log("Socket 已打开");
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
//发现消息进入 调后台获取
getCallingList();
};
//关闭事件
socket.onclose = function() {
console.log("Socket已关闭");
};
//发生了错误事件
socket.onerror = function() {
alert("Socket发生了错误");
}
$(window).unload(function(){
socket.close();
});
// $("#btnSend").click(function() {
// socket.send("这是来自客户端的消息" + location.href + new Date());
// });
//
// $("#btnClose").click(function() {
// socket.close();
// });
}
SpringBoot使用WebSocket--SimpMessagingTemplate
声明SimpMessagingTemplate (或者使用@SendTo和@SendToUser注解)
在需要用到推送的地方如Controller,service,Component等地方声明SimpMessagingTemplate
当需要向客户端推送消息时,调用convertAndSend方法,即可推送消息,此处“/topic/send”可随意设置,所有前端订阅该url的客户端都可以收到推送的消息。
messagingTemplate.convertAndSend("/topic/send", result);
springboot+websocket,一篇足够了--管理Socket的类-SocketManager
springboot+websocket,一篇足够了--管理Socket的类-SocketManager -简书
Spring-boot2 WebFlux WebSockit实现-实现心跳
websocket消息推送实现
[java+websocket实现网页聊天室-今日头条](https://www.toutiao.com/a6700358112806699528)
spring websocket之sockjs超简单现实
客户端接收服务端消息推送sockjs-client的使用--new SockJS(url, _reserved, options)
利用Spring_Boot WebSocKet实现一个推送的小Demo--全局推送&点对点推动-spring-boot-starter-security
Spring Boot通信之STOMP协议:后台不发送心跳的问题
spring boot集成WebSocket实时输出日志到web页面--采用阻塞队列
Spring消息之STOMP--留意参数Principal principal
什么是stomp?spring-boot websocket stomp服务构建-@MessageMapping参数详情
网友评论