SpringBoot2.0集成WebSocket实现后台向前端推送信息

SpringBoot2.0集成WebSocket实现后台向前端推送信息
⽬录
什么是WebSocket?
为什么需要 WebSocket?
什么是WebSocket?
WebSocket协议是基于TCP的⼀种新的⽹络协议。它实现了浏览器与服务器全双⼯(full-duplex)通信——允许服务器主动发送信息给客户端
为什么需要 WebSocket?
初次接触 WebSocket 的⼈,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另⼀个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有⼀个缺陷:通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息。
举例来说,我们想要查询当前的排队情况,只能是页⾯轮询向服务器发出请求,服务器返回查询结果。轮询的效率低,⾮常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此WebSocket
就是这样发明的。前⾔
2020-10-20教程补充:
补充关于@Component和@ServerEndpoint关于是否单例模式等的解答,感谢⼤家热⼼提问和研究。
Vue版本的websocket连接⽅法
2020-01-05教程补充:
整合了IM相关的优化
优化开启/关闭连接的处理
上传到开源项⽬spring-cloud-study-websocket,⽅便⼤家下载代码。
感谢⼤家的⽀持和留⾔,14W访问量是满满的动⼒!接下来还会有websocket+redis集优化篇针对多
ws服务器做简单优化处理,敬请期待!
话不多说,马上进⼊⼲货时刻。
maven依赖
SpringBoot2.0对WebSocket的⽀持简直太棒了,直接就有包可以引⼊
import t.annotation.Bean;
import t.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 开启WebSocket⽀持
* @author zhengkai.blog.csdn
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketConfig
启⽤WebSocket的⽀持也是很简单,⼏句代码搞定
import t.annotation.Bean;import t.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;/** * 开启WebSocket⽀持
* @author zhengkai.blog.csdn */@C
这就是重点了,核⼼都在这⾥。
因为WebSocket是类似客户端服务端的形式(采⽤ws协议),那么这⾥的WebSocketServer其实就相当于⼀个ws协议的Controller
直接@ServerEndpoint("/imserver/{userId}")、@Component启⽤即可,然后在⾥⾯实现@OnOpen开启连接,@onClose关闭连接,@onMessage接收消息等⽅法。
新建⼀个ConcurrentHashMap webSocketMap ⽤于接收当前userId的WebSocket,⽅便IM之间对userId进⾏推送消息。单机版实现到这⾥就可以。
集版(多个ws节点)还需要借助mysql或者redis等进⾏处理,改造对应的sendMessage⽅法即可。
package com.softdev.fig;
import java.io.IOException;
import urrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apachemons.lang.StringUtils;
import org.springframework.stereotype.Component;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
/**
* @author zhengkai.blog.csdn
*/
@ServerEndpoint("/imserver/{userId}")
@Component
public class WebSocketServer {
static Log (WebSocketServer.class);
/**静态变量,⽤来记录当前在线连接数。应该把它设计成线程安全的。*/
private static int onlineCount = 0;
/
**concurrent包的线程安全Set,⽤来存放每个客户端对应的MyWebSocket对象。*/
private static ConcurrentHashMap<String,WebSocketServer> webSocketMap = new ConcurrentHashMap<>();
/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
private Session session;
/**接收userId*/
private String userId="";
/**
* 连接建⽴成功调⽤的⽅法*/
@OnOpen
public void onOpen(Session session,@PathParam("userId") String userId) {
this.session = session;
this.userId=userId;
ainsKey(userId)){
webSocketMap.put(userId,this);
//加⼊set中
}else{
webSocketMap.put(userId,this);
//加⼊set中
addOnlineCount();
//在线数加1
}
log.info("⽤户连接:"+userId+",当前在线⼈数为:" + getOnlineCount());
try {
sendMessage("连接成功");
} catch (IOException e) {
<("⽤户:"+userId+",⽹络异常");
}
}
/**
* 连接关闭调⽤的⽅法
*/
@OnClose
public void onClose() {
ainsKey(userId)){
//从set中删除
subOnlineCount();
}
log.info("⽤户退出:"+userId+",当前在线⼈数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调⽤的⽅法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("⽤户消息:"+userId+",报⽂:"+message);
//可以发消息
//消息保存到数据库、redis
if(StringUtils.isNotBlank(message)){
try {
//解析发送的报⽂
JSONObject jsonObject = JSON.parseObject(message);
//追加发送⼈(防⽌串改)
jsonObject.put("fromUserId",this.userId);
String String("toUserId");
//传送给对应toUserId⽤户的websocket
if(StringUtils.isNotBlank(toUserId)&&ainsKey(toUserId)){
<(toUserId).JSONString());
}else{
<("请求的userId:"+toUserId+"不在该服务器上");
//否则不在这个服务器上,发送到mysql或者redis
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
<("⽤户错误:"+this.userId+",原因:"+Message());
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
BasicRemote().sendText(message);
}
/**
* 发送⾃定义消息
* */
public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {
log.info("发送消息到:"+userId+",报⽂:"+message);
if(StringUtils.isNotBlank(userId)&&ainsKey(userId)){
<(userId).sendMessage(message);
}else{
<("⽤户"+userId+",不在线!");
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
}
public static synchronized void subOnlineCount() {
}
}
消息推送
⾄于推送新信息,可以再⾃⼰的Controller写个⽅法调⽤WebSocketServer.sendInfo();即可
import com.softdev.fig.WebSocketServer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;
import java.io.IOException;
/**
* WebSocketController
* @author zhengkai.blog.csdn
*/
@RestController
public class DemoController {
@GetMapping("index")
public ResponseEntity<String> index(){
return ResponseEntity.ok("请求成功");
}
@GetMapping("page")
public ModelAndView page(){
return new ModelAndView("websocket");
}
@RequestMapping("/push/{toUserId}")
public ResponseEntity<String> pushToWeb(String message, @PathVariable String toUserId) throws IOException {
WebSocketServer.sendInfo(message,toUserId);
return ResponseEntity.ok("MSG SEND SUCCESS");
}
}
页⾯发起
页⾯⽤js代码调⽤websocket,当然,太古⽼的浏览器是不⾏的,⼀般新的浏览器或者⾕歌浏览器是没问题的。还有⼀点,记得协议是ws的,如果使⽤了⼀些路径类,可以replace(“http”,“ws”)来替换协议。<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>websocket通讯</title>
</head>
<script src="cdn.bootcss/jquery/3.3.1/jquery.js"></script>
<script>
var socket;
function openSocket() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不⽀持WebSocket");
}else{
console.log("您的浏览器⽀持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端⼝建⽴连接
//等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
//var socketUrl="${tPath}/im/"+$("#userId").val();
var socketUrl="localhost:9999/demo/imserver/"+$("#userId").val();
place("https","ws").replace("http","ws");
console.log(socketUrl);
if(socket!=null){
socket.close();
socket=null;
}
socket = new WebSocket(socketUrl);
//打开事件
console.log("websocket已打开");
/
/socket.send("这是来⾃客户端的消息" + location.href + new Date());
};
//获得消息事件
console.log(msg.data);
//发现消息进⼊开始处理前端触发逻辑
};
//关闭事件
console.log("websocket已关闭");
};
/
/发⽣了错误事件
console.log("websocket发⽣了错误");
}
}
}
function sendMessage() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不⽀持WebSocket");
}else {
console.log("您的浏览器⽀持WebSocket");
console.log('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
socket.send('{"toUserId":"'+$("#toUserId").val()+'","contentText":"'+$("#contentText").val()+'"}');
}
}
</script>
<body>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="10"></div>
<p>【toUserId】:<div><input id="toUserId" name="toUserId" type="text" value="20"></div>
<p>【toUserId】:<div><input id="contentText" name="contentText" type="text" value="hello websocket"></div>
<p>【操作】:<div><a onclick="openSocket()">开启socket</a></div>
<p>【操作】:<div><a onclick="sendMessage()">发送消息</a></div>
</body>
</html>
运⾏效果
v20200105,加⼊开源项⽬spring-cloud-study-websocket,更新运⾏效果,更⽅便理解。
v1.1的效果,刚刚修复了⽇志,并且⽀持指定监听某个端⼝,代码已经全部更新,现在是这样的效果打开两个页⾯,按F12调出控控制台查看测试效果:
页⾯参数
localhost:9999/demo/page fromUserId=10,toUserId=20
localhost:9999/demo/page fromUserId=20,toUserId=10
分别开启socket,再发送消息
2. 向前端推送数据:苟仲武
通过调⽤push api,可以向指定的userId推送信息,当然报⽂这⾥乱写,建议规定好格式。
后续
针对简单IM的业务场景,进⾏了⼀些优化,可以看后续的⽂章v20201005已整合)覆膜胶
主要变动是CopyOnWriteArraySet改为ConcurrentHashMap,保证多线程安全同时⽅便利⽤(userId)进⾏推送到指定端⼝。
相⽐之前的Set,Set遍历是费事且⿇烦的事情,⽽Map的get是简单便捷的,当WebSocket数量⼤的时候,这个⼩⼩的消耗就会聚少成多,影响体验,所以需要优化。在IM的场景下,指定userId进⾏推送消息更加⽅便。
Websocker注⼊Bean问题
关于这个问题,可以看最新发表的这篇⽂章,在参考和研究了⽹上⼀些攻略后,项⽬已经通过该⽅法注⼊成功,⼤家可以参考。
netty-websocket-spring-boot-starter
Springboot2构建基于Netty的⾼性能Websocket服务器(netty-websocket-spring-boot-starter)
只需要换个starter即可实现⾼性能websocket,赶紧使⽤吧
Springboot2+Netty+Websocket
,使⽤官⽅的netty-all的包,⽐原⽣的websocket更加稳定更加⾼性能,同等配置情况下可以handle更多的连接。
塔吊防碰撞
代码样式全部已经更正,也⽀持websocket连接url带参数功能,另外也感谢⼤家的阅读和评论,⼀起进步,谢谢!~~
ServerEndpointExporter错误
org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘serverEndpointExporter' defined in class path resource [com/xxx/WebSocketConfig.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
感谢@来了⽼弟⼉的反馈:
双柱汽车举升机如果tomcat部署⼀直报这个错,请移除WebSocketConfig中@Bean ServerEndpointExporter的注⼊。
ServerEndpointExporter是由Spring官⽅提供的标准实现,⽤于扫描ServerEndpointConfig配置类和@ServerEndpoint注解实例。使⽤规则也很简单:
如果使⽤默认的嵌⼊式容器⽐如Tomcat则必须⼿⼯在上下⽂提供ServerEndpointExporter。如果使⽤外部容器部署war包,则不需要提供提供ServerEndpointExporter,因为此时SpringBoot默认将扫描服务端的⾏为交给外部容器处理,所以线上部署的时候要把WebSocketConfig中这段注⼊bean的代码注掉。正式项⽬的前端WebSocket框架 GoEasy
感谢kkatrina的补充,正式的项⽬中,⼀般是⽤第三⽅websocket框架来做,稳定性、实时性有保证的多,也会包括⼀些⼼跳、重连机制。
@Component和@ServerEndpoint关于是否单例模式,能否使⽤static Map等⼀些问题的解答
看到⼤家都在热⼼的讨论关于是否单例模式这个问题,请⼤家相信⾃⼰的直接,如果websocket是单例模式,还怎么服务这么多session呢。
websocket是原型模式,@ServerEndpoint每次建⽴双向通信的时候都会创建⼀个实例,区别于spring的单例模式。Spring的@Component默认是单例模式,请注意,默认⽽已,是可以被改变的。
这⾥的@Component仅仅为了⽀持@Autowired依赖注⼊使⽤,如果不加则不能注⼊任何东西,为了⽅便。
什么是prototype 原型模式?基本就是你需要从A的实例得到⼀份与A内容相同,但是⼜互不⼲扰的实例B的话,就需要使⽤原型模式。关于在原型模式下使⽤static 的webSocketMap,请注意这是ConcurrentHashMap ,也就是线程安全/线程同步的,⽽且已经是静态变量作为全局调⽤,这种情况下是ok的,或者⼤家如果有顾虑或者更好的想法的化,可以进⾏改进。
例如使⽤⼀个中间类来接收和存放session。为什么每次都@OnOpen都要检查ainsKey(userId),⾸先了为了代码强壮性考虑,假设代码以及机制没有问题,那么肯定这个逻辑是废的对吧。
但是实际使⽤的时候发现偶尔会出现重连失败或者其他原因导致之前的session还存在,这⾥就做了⼀个清除旧session,迎接新session的功能。
Vue版本的websocket连接
感谢**@GzrStudy**的贡献,供⼤家参考。
<script>
export default {
data() {
return {
socket:null,
Item("ms_uuid"),
toUserId:'2',
content:'3'
}
},
methods: {
openSocket() {
if (typeof WebSocket == "undefined") {
console.log("您的浏览器不⽀持WebSocket");
} else {
console.log("您的浏览器⽀持WebSocket");
//实现化WebSocket对象,指定要连接的服务器地址与端⼝建⽴连接
//等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
//var socketUrl="${tPath}/im/"+$("#userId").val();
var socketUrl =施万细胞红蓝图
"localhost:8081/imserver/" + this.userId;
socketUrl = place("https", "ws").replace("http", "ws");
console.log(socketUrl);
if (this.socket != null) {
this.socket.close();
this.socket = null;
}
this.socket = new WebSocket(socketUrl);
//打开事件
this.socket = new WebSocket(socketUrl);
//打开事件
pen = function() {
console.log("websocket已打开");
//socket.send("这是来⾃客户端的消息" + location.href + new Date());
};cn-m
//获得消息事件
ssage = function(msg) {
console.log(msg.data);
//发现消息进⼊开始处理前端触发逻辑
};
//关闭事件
lose = function() {
console.log("websocket已关闭");
};
//发⽣了错误事件
r = function() {
console.log("websocket发⽣了错误");
};

本文发布于:2024-09-23 04:29:39,感谢您对本站的认可!

本文链接:https://www.17tex.com/tex/1/267009.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:消息   连接   模式   服务器   客户端
留言与评论(共有 0 条评论)
   
验证码:
Copyright ©2019-2024 Comsenz Inc.Powered by © 易纺专利技术学习网 豫ICP备2022007602号 豫公网安备41160202000603 站长QQ:729038198 关于我们 投诉建议