什么是websocket这里就不进行介绍了,有兴趣的可以自己百度,或许后面我也会发文章介绍。
主要演示一下代码的实现,红色标注部分 需要格外注意
1、 引入依赖websocket
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
2、首先在要创建一个WebSocket的实体类,用来接收一些数据(Session 和 用户名)
//这里我使用了Lombok的注解,如果没有添加这个依赖 可以创建get set方法
@Data
public class WebSocketClient {
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//连接的uri
private String uri;
}
创建websocket的配置文件
@Configuration
@EnableWebSocket
public class WebSocketConfig {
/**
* 注入ServerEndpointExporter,
* 这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocket的具体实现逻辑,重点
/**
* @desc: WebSocketService实现
* @author: LiuCh
* @since: 2021/8/16
*/
//ServerEncoder 是为了解决编码异常,如果不需要使用sendObject()方法,这个可以忽略,只写value即可
@ServerEndpoint(value = "/websocket/{userName}",encoders = {ServerEncoder.class})
@Component
public class WebSocketService {
private static final Logger log = LoggerFactory.getLogger(WebSocketService.class);
/**
* 静态变量,用来记录当前在线连接数
*/
private static int onlineCount = 0;
/**
* concurrent包的线程安全Set,用来存放每个客户端对应的WebSocketServer对象。
*/
private static ConcurrentHashMap<String, WebSocketClient> webSocketMap = new ConcurrentHashMap<>();
/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
private Session session;
/**接收userName 用来区别不同的用户*/
private String userName="";
/**
* 连接建立成功调用的方法 可根据自己的业务需求做不同的处理*/
@OnOpen
public void onOpen(Session session, @PathParam("userName") String userName) {
if(!webSocketMap.containsKey(userName))
{
addOnlineCount(); // 在线数 +1
}
this.session = session;
this.userName= userName;
WebSocketClient client = new WebSocketClient();
client.setSession(session);
client.setUri(session.getRequestURI().toString());
webSocketMap.put(userName, client);
// log.info("用户连接:"+userName+",当前在线人数为:" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if(webSocketMap.containsKey(userName)){
webSocketMap.remove(userName);
if(webSocketMap.size()>0)
{
//从set中删除
subOnlineCount();
}
}
log.info(userName+"用户退出,当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到用户消息:"+userName+",报文:"+message);
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:"+this.userName+",原因:"+error.getMessage());
error.printStackTrace();
}
/**
* 连接服务器成功后主动推送
*/
public void sendMessage(String message) throws IOException {
synchronized (session){
this.session.getBasicRemote().sendText(message);
}
}
/**
* 向指定客户端发送消息(字符串形式)
* @param userName
* @param message
*/
public static void sendMessage(String userName,String message){
try {
WebSocketClient webSocketClient = webSocketMap.get(userName);
if(webSocketClient!=null){
webSocketClient.getSession().getBasicRemote().sendText(message);
}
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
/**
* 向指定客户端发送消息(对象的形式)
* @param userName
* @param object
*/
public static void sendMessage(String userName,Object object){
try {
WebSocketClient webSocketClient = webSocketMap.get(userName);
if(webSocketClient!=null){
webSocketClient.getSession().getBasicRemote().sendObject(object);
}
} catch (IOException | EncodeException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocketService.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketService.onlineCount--;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
下面主要是为了解决通过服务端向客户端传递一个对象的问题,需要指定编码器,否则会抛出异常。
/**
* @desc: WebSocket编码器
* @author: LiuCh
* @since: 2021/8/18
*/
public class ServerEncoder implements Encoder.Text<HashMap> {
private static final Logger log = LoggerFactory.getLogger(ServerEncoder.class);
/**
* 这里的参数 hashMap 要和 Encoder.Text保持一致
* @param hashMap
* @return
* @throws EncodeException
*/
@Override
public String encode(HashMap hashMap) throws EncodeException {
/*
* 这里是重点,只需要返回Object序列化后的json字符串就行
* 你也可以使用gosn,fastJson来序列化。
* 这里我使用fastjson
*/
try {
return JSONObject.toJSONString(hashMap);
}catch (Exception e){
log.error("",e);
}
return null;
}
@Override
public void init(EndpointConfig endpointConfig) {
//可忽略
}
@Override
public void destroy() {
//可忽略
}
}
上面的Text中的HashMap就是我们需要传递的对象,我这里是通过map封装返回的结果集,如果你创建的是一个具体的对象,直接换成你的消息返回对象即可,注意Text和encode中的要保持一致。
完成这一步以后,我们可以通过在线的连接工具测试一下,检查一下我们服务端是否已经配置好
http://coolaf.com/tool/chattest
注意:如果是一个普通的没有拦截器的springboot项目,代码编写没有问题的情况下,这里是可以测通的。
如果代码中存在拦截器或者增加了一些安全框架,我们需要对访问的路径进行放行,采用的拦截方式不同,具体的放行方式会有差别,但是一定要将所请求的url放行,否则在测试时会出现连接失败的情况。
//WebSocket 请求放行
filterChainDefinitionMap.put("/websocket/**", "anon");
created() {
// 初始化websocket
this.initWebSocket();
},
destroyed() {
//销毁
this.websocketclose();
},
methods: {
initWebSocket() {
//建立websocket连接
if ("WebSocket" in window){
//连接服务端访问的url,我这里配置在了env中,就是上面在线测试工具中的地址,下面放了实例
let ws = process.env.VUE_APP_BASE_WEBSOCKET + this.$store.getters.user.acctLogin;
this.websock = new WebSocket(ws);
this.websock.onopen = this.websocketonopen;
this.websock.onerror = this.websocketonerror;
this.websock.onmessage = this.websocketonmessage;
this.websock.onclose = this.websocketclose;
}
},
websocketonopen: function () {
// console.log("连接成功",)
//可以通过send方法来向服务端推送消息,推送的消息在onMessage中可以打印出来查看并做一些业务处理。
//this.websock.send("向服务端推送的消息内容")
},
websocketonerror: function (e) {
// console.log("连接失败",)
},
websocketonmessage: function (e) {
// console.log("服务端消息的内容",e.data)
if (e){
JSON.parse(e.data);//这个是收到后端主动推送的json字符串转化为对象(必须保证服务端传递的是json字符串,否则会报错)
//你的业务处理...
}
},
websocketclose: function (e) {
// console.log("连接关闭",)
}
}
.env.development文件中增加的内容
VUE_APP_BASE_WEBSOCKET = 'ws://127.0.0.1:7773/websocket/'
到了上面的这一步,你在控制台中就可以看到连接建立的情况了。
4、服务端主动向客户端推送消息
WebSocketService.sendMessage("消息的内容"); //调用这个方法即可 这种是向所有的在线的推送消息 广播
WebSocketService.sendMessage("接收者的账号","消息的内容"); //向指定账号推送消息 单点
//这里消息的内容也可以封装为一个对象进行返回,使用对象时一定要指定编码器
注意
:websocket的连接不是一直持续的,是有时长的,超过一分钟连接就会关闭,因此我们需要引入心跳来保证连接的持续调用(每一次连接关闭时,调用重连方法,保证连接一直在线)
加入心跳机制后的代码
data(){
websock:null,
lockReconnect: false, //是否真正建立连接
timeout: 28 * 1000, //保持websocket的连接
timeoutObj: null, //心跳心跳倒计时
serverTimeoutObj: null, //心跳倒计时
timeoutnum: null, //断开 重连倒计时
......
},
method(){
initWebSocket() {
//建立websocket连接
if ("WebSocket" in window){
// let ws = Config.wsUrl + this.$store.getters.user.acctLogin;
let ws = process.env.VUE_APP_BASE_WEBSOCKET + this.$store.getters.user.acctLogin;
this.websock = new WebSocket(ws);
this.websock.onopen = this.websocketonopen;
this.websock.onerror = this.websocketonerror;
this.websock.onmessage = this.websocketonmessage;
this.websock.onclose = this.websocketclose;
}
},
//重新连接
reconnect() {
var that = this;
if (that.lockReconnect) {
return;
}
that.lockReconnect = true;
//没连接上会一直重连,设置延迟避免请求过多
that.timeoutnum && clearTimeout(that.timeoutnum);
that.timeoutnum = setTimeout(function () {
//新连接
that.initWebSocket();
that.lockReconnect = false;
}, 5000);
},
//重置心跳
reset() {
var that = this;
//清除时间
clearTimeout(that.timeoutObj);
clearTimeout(that.serverTimeoutObj);
//重启心跳
that.start();
},
//开启心跳
start() {
var self = this;
self.timeoutObj && clearTimeout(self.timeoutObj);
self.serverTimeoutObj && clearTimeout(self.serverTimeoutObj);
self.timeoutObj = setTimeout(function () {
//这里发送一个心跳,后端收到后,返回一个心跳消息,
if (self.websock.readyState == 1) {
//连接正常
} else {
//否则重连
self.reconnect();
}
self.serverTimeoutObj = setTimeout(function () {
//超时关闭
self.websock.close();
}, self.timeout);
}, self.timeout);
},
websocketonopen: function () {
// console.log("连接成功",)
},
websocketonerror: function (e) {
// console.log("连接失败",)
//重连
this.reconnect();
},
websocketonmessage: function (e) {//JSON.parse(e.data); //这个是收到后端主动推送的值
// console.log("服务端消息的内容",e.data)
if (e){
let parse = JSON.parse(e.data) //将json字符串转为对象
if (parse.type === 'add'){ //消息个数+1
this.total = this.total + 1
if (this.$route.path !== '/system_manager/system_manager_message'){
this.mess = this.$message({
showClose: true,
message: '你有一条新的系统通知待查看',
type: 'success',
center:true,
duration:0
});
}else {
vue.$emit('flush',true)
}
} else if(parse.type === 'del'){
this.total = this.total - 1
}
if (parse.type === 'message'){
//判断是外厂还是内厂操作 跳转的route不同
if(parse.maint === 'outSide'){ //外厂
this.jumpRouter = API.backFlowApproveOutSidePath
//如果当前页面就是将要跳转的页面,直接刷新当前页
if (this.$route.path === API.backFlowApproveOutSidePath){
vue.$emit('flush',true)
}
}else { //内厂
this.jumpRouter = API.backFlowApprovePath
//如果当前页面就是将要跳转的页面,直接刷新当前页
if (this.$route.path === API.backFlowApprovePath){
vue.$emit('flush',true)
}
}
let notification = document.getElementsByClassName('el-notification')
if (notification.length === 0){
let that = this
this.notifi = this.$notify({
title: '维保流程消息提醒',
message: parse.message,
offset: 100,
type:'success',
duration: 0,
onClick:function(){
that.$router.push({
path: that.jumpRouter
});
//关闭消息通知弹窗
that.notifi.close();
}
});
}
}
//收到服务器信息,心跳重置
this.reset();
}
},
websocketclose: function (e) {
// console.log("连接关闭",)
//重连
this.reconnect();
},
}
这里我是在前端进行配置的,通过心跳机制保持连接,如果使用ngin进行代理,也可以在nginx中配置,保证连接的不失效。
这个我没实际测试过,仅供参考!!!
#nginx websocket的默认连接超时时间是60s,我们需要进行修改
proxy_read_timeout 3600s; #现在改为一小时
proxy_connect_timeout 4s;
proxy_send_timeout 12s;
5、Nginx的配置
将springboot部署到服务器后,需要先使用在线连接工具进行测试,确保可以连接成功再进行下面的操作,连接不同一般为防火墙或过滤器问题,注意放行。
VUE_APP_BASE_WEBSOCKET = 'ws://127.0.0.1:7773/websocket/
开发环境下配置ws://127.0.0.1:7773/websocket/
即可。
部署环境直接使用前端的端口然后通过nginx反向代理访问即可,我这里直接使用前端url中的host部分。
实例代码(上面前端访问websocket的url地方改写):
推荐连接下根据环境变化更换配置,下面这种写法最优雅
//建立websocket连接
if ("WebSocket" in window){
let ws = ''
if (process.env.NODE_ENV === 'development'){
ws = process.env.VUE_APP_BASE_WEBSOCKET+ this.$store.getters.user.acctLogin;
}else{
ws = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}`+`/websocket/`+ this.$store.getters.user.acctLogin;
}
this.websock = new WebSocket(ws);
this.websock.onopen = this.websocketonopen;
this.websock.onerror = this.websocketonerror;
this.websock.onmessage = this.websocketonmessage;
this.websock.onclose = this.websocketclose;
}
},
.... 这里和上面一样,就不粘贴了
服务端访问的url : ws://127.0.0.1:7773/websocket/test
你的可能是: ws://127.0.0.1:7773/test
这个时候你下面的location 也要变化 :
/websocket/ =》 /
proxy_pass也需要改变http://127.0.0.1:7773/
在原有的监听的端口增加下面的配置
location /websocket/ { #请求的url
proxy_pass http://127.0.0.1:7773/websocket/;
proxy_set_header Host $host;
#websocket的配置,将http连接升级为websocket连接
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 90s; #超时时间 现在是一个小时,默认为60s,超时会断开
#由于vue中我已经加入了心跳机制 这个地方不写也可以
}
//proxy_http_version http的版本
配置完成的效果图如下图(打码的都是无关信息不影响整体配置 修改为你的即可,具体nginx的配置使用,可参考官方文档)
注意
:图中websocket拼错了,这里就没截新图不好意思,还有就是如果你的url是xxx:port/websocket/**
这里要写成 /websocket/ 不能漏掉后面的 /
,并且proxy_pass后面的可以写成7773:
或者写完整7773:/websocket/
注意最后面的 /
使用域名时的配置,和配置ip时一样,在配置域名的下面添加
location /websocket/ { #请求的url
proxy_pass http://127.0.0.1:7773/websocket/;
#websocket的配置,将http连接升级为websocket连接
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_read_timeout 90s; #超时时间 现在是一个小时,默认为60s,超时会断开
#由于vue中我已经加入了心跳机制 这个地方不写也可以
}
需要注意的是,使用域名时的请求时wss,使用ws是会被浏览器block 掉的。
配置域名,使用https 需要ca证书(文章现在太长了,这里就不写了怎么获取了)
server {
listen 7776 ssl;
server_name 你的域名; #你的域名
ssl on; #如果需要同事支持http和https 把这个注释掉
ssl_certificate /etc/nginx/ssl/xxx.crt; #证书的路径
ssl_certificate_key /etc/nginx/ssl/xxx.key; #证书的路径
ssl_session_timeout 20m;
ssl_verify_client off;
root /root/nginx/html/vehicle;
location /api {
proxy_set_header Host $host;
proxy_set_header X-Real-Ip $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass http://127.0.0.1:7773/;
}
location /websocket/ {
proxy_pass http://127.0.0.1:7773/websocket/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 90s;
}
}
配置完重启nginx./nginx -s reload
,到了这一步如果没有报错,就可以了,但是大部分的应该都会出现报错,提示的意思就是缺少一个ssl,我们进入到nginx的安装目录,每个人的安装位置都不同cd xxx/xxx/nginx/
输入nginx -V可以看到安装了哪些module
查看是否存在 --with-http_ssl_module
,上面如果没有--with-http_ssl_module
但是存在OpenSSL,直接使用命令行
./configure --with-http_ssl_module
如果没有安装OpenSSL,需要先安装OpenSSL
yum -y install openssl openssl-devel
然后执行
./configure --with-http_ssl_module
make
到了这一步不出现报错,重启nginx即可。
最后更新时间 2021-9-4,解决了之前的错误,之前nginx反向代理理解错误,今天配置的时候发现了问题,有问题可以留言或者私信