SpringBoot:整合websocket实现消息推送

SpringBoot:整合websocket实现消息推送

惯例先上pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.fufu</groupId>
<artifactId>springbootdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>springbootdemo</name>
<description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- springboot-aop包,AOP切面注解,Aspectd等相关注解 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>

<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>

<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.6.1</version>
</dependency>

<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.6.1</version>
</dependency>

<!-- 分页插件 -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
</dependency>
<!-- shiro core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.3.2</version>
</dependency>
<!-- shiro权限控制框架 -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 引入Spring Boot 内嵌的Tomcat对jsp的解析包-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
</dependency>

<!-- servlet 依赖的jar包start-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>

<!-- jsp 依赖的jar包start-->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
</dependency>

<!-- jstl标签 依赖的jar包start-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>

<!--redis数据库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--spring2.0集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>

<!-- 提供API给Java程式对Microsoft Office格式档案读和写的功能 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.13</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.13</version>
</dependency>

<!-- 上传文件需要的依赖 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>

<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.7</version>
</plugin>
</plugins>
</build>
</project>

新增

1
2
3
4
5
6
7
8
9
10
11
<!-- websocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

application.yml这边新增thymeleaf配置,注意该配置适合SpringBoot2.0以上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Spring configuration
spring:
thymeleaf:
#开启模板缓存(默认值:true)
cache: false
#Check that the template exists before rendering it.
check-template: true
#检查模板位置是否正确(默认值:true)
check-template-location: true
#开启MVC Thymeleaf视图解析(默认值:true)
enabled: true
#模板编码
encoding: UTF-8
#要被排除在解析之外的视图名称列表,用逗号分隔
spring.thymeleaf.excluded-view-names:
#要运用于模板之上的模板模式。另见StandardTemplate-ModeHandlers(默认值:HTML5)
mode: HTML5
#在构建URL时添加到视图名称前的前缀(默认值:classpath:/templates/)
prefix: classpath:/templates/
#在构建URL时添加到视图名称后的后缀(默认值:.html)
suffix: .html
#Thymeleaf模板解析器在解析器链中的顺序。默认情况下,它排第一位。顺序从1开始,
#只有在定义了额外的TemplateResolver Bean时才需要设置这个属性。
template-resolver-order:
#可解析的视图名称列表,用逗号分隔
spring.thymeleaf.view-names:

websocket新增配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.fufu.config.websocket;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
* 开启WebSocket支持
*/
@Configuration
public class WebSocketConfig {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
package com.fufu.config.websocket;

import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;
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 org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@ServerEndpoint("/websocket/{sid}")
@Component
public class WebSocketServer {

private final static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
//静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static int onlineCount = 0;
//concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<WebSocketServer>();

//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;

//接收sid
private String sid="";
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session,@PathParam("sid") String sid) {
this.session = session;
webSocketSet.add(this); //加入set中
addOnlineCount(); //在线数加1
log.info("有新窗口开始监听:"+sid+",当前在线人数为" + getOnlineCount());
this.sid=sid;
try {
sendMessage("连接成功");
} catch (IOException e) {
log.error("websocket IO异常");
}
}

/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}

/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自窗口"+sid+"的信息:"+message);
//群发消息
for (WebSocketServer item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}


/**
* 群发自定义消息
* */
public static void sendInfo(String message,@PathParam("sid") String sid) throws IOException {
log.info("推送消息到窗口"+sid+",推送内容:"+message);
for (WebSocketServer item : webSocketSet) {
try {
//这里可以设定只推送给这个sid的,为null则全部推送
if(sid==null) {
item.sendMessage(message);
}else if(item.sid.equals(sid)){
item.sendMessage(message);
}
} catch (IOException e) {
continue;
}
}
}

public static synchronized int getOnlineCount() {
return onlineCount;
}

public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}

public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}

shiroFilter放开websocket

1
2
filterChainDefinitionMap.put("/websocket/**", "anon");
filterChainDefinitionMap.put("/socket/**", "anon");

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.fufu.controller;

import com.fufu.config.websocket.WebSocketServer;
import com.fufu.tools.JsonUtil;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Controller;
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.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import java.io.IOException;

@Controller
@RequestMapping("/websocket")
public class MessagePushController {
//页面请求
@GetMapping("/requestPage")
public String websocket() {
return "websocket";
}
//推送数据接口
@ResponseBody
@RequestMapping("/socket/push/{cid}")
public String pushToWeb(@PathVariable String cid, String message) {
try {
WebSocketServer.sendInfo(message,cid);
} catch (IOException e) {
e.printStackTrace();
return JsonUtil.getInstance().putData("ret", -1).putData("msg", cid+"#"+e.getMessage()).pushData();
}
return JsonUtil.getInstance().putData("ret", 1).putData("msg", "推送成功cid为:"+cid).pushData();
}

@Scheduled(cron = "0/5 * * * * ?")
public void autoPushToWeb() {
try {
WebSocketServer.sendInfo("我是一条5秒一次向web推送的消息~","20");
} catch (IOException e) {
e.printStackTrace();
}
}
}

html测试websocket

在resources下的templates新建websocket.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>websocket测试</title>
<meta name="robots" content="all" />
<link rel="shortcut icon" href="/static/img/favicon.ico">
<meta name="google-site-verification" content="tW0wHpS5SAbFhl5Nt9o2G1uXV-G4YIHClYuX5lr1-ZE" />
<script src="https://libs.cdnjs.net/jquery/1.9.1/jquery.min.js"></script>
<link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css">
<link rel="stylesheet" href="/static/css/offcanvas.css">
<link rel="alternate" hreflang="en"
href="http://coolaf.com/en" />
<link rel="alternate" hreflang="zh"
href="http://coolaf.com/zh" />
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
</head>
<body>
<div class="container" style='width:1300px'>
<div class="row row-offcanvas row-offcanvas-right main-contain">
<div class="col-xs-12 col-sm-8">
<style>
.newmessage{
width:100%;
}
.bubble{
background-color:lightgreen
position: relative;
max-width: 240px;
word-wrap: break-word;
text-align: left;
margin-left: 16px;
margin-right: 16px;
border-radius: 9px;
}

.bubble:after{
position: absolute;
border: 4.8px solid transparent;
content: " ";
top: 20px;
}
</style>
<h2></h2>
<div style="">
<div>
<input type='text' value='ws://123.207.167.163:9010/ajaxchattest' class="form-control" style='width:390px;display:inline'id='wsaddr'/>
<div class="btn-group" role="group" aria-label="...">
<button type="button" class="btn btn-default" onclick='addsocket();'>连接</button>
<button type="button" class="btn btn-default" onclick='closesocket();'>断开</button>
<button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button>
<button type="button" class="btn btn-default" onclick='$("#wsaddr").val("ws://localhost:9010/ajaxchattest")'>示例</button>
</div>
<div style='margin-top:10px;margin-button:10px'>
<pre>本工具主要是为了测试服务端websocket功能是否完善可用而开发,主要是利用html5 的websocket去连接服务端的websocket,因此,
无论你是内网还是外网都可使用!服务端只是实现了接受和发送,这里只是测试而已!</pre>
</div>
<div id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin-left:0px"></div>
</div>
<div class="row" >
<div class="col-lg-6">
<div class="input-group" style=''>
<input type="text"id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);">
<span class="input-group-btn"><button class="btn btn-default" type="button" onclick="doSend();">发送</button></span>
</div>
</div>
</div>
</div>
<script language="javascript"type="text/javascript">
function formatDate(now) {
var year=now.getFullYear();
var month=now.getMonth()+1;
var date=now.getDate();
var hour=now.getHours();
var minute=now.getMinutes();
var second=now.getSeconds();
return year+"-"+(month=month<10?("0"+month):month)+"-"+(date=date<10?("0"+date):date)+" "+(hour=hour<10?("0"+hour):hour)+":"+(minute=minute<10?("0"+minute):minute)+":"+(second=second<10?("0"+second):second);
}
var output;
var websocket;
function init() {
output = document.getElementById("output");
testWebSocket();
}

function addsocket() {
var wsaddr = $("#wsaddr").val();
if (wsaddr=='') {
alert("请填写websocket的地址");
return false;
}
StartWebSocket(wsaddr);
}

function closesocket() {
websocket.close();
}

function StartWebSocket(wsUri) {
websocket = new WebSocket(wsUri);
websocket.onopen = function(evt) { onOpen(evt) };
websocket.onclose = function(evt) { onClose(evt) };
websocket.onmessage = function(evt) { onMessage(evt) };
websocket.onerror = function(evt) { onError(evt) }; }

function onOpen(evt) {
writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");
}
function onClose(evt) {
writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");
websocket.close();
}
function onMessage(evt) {
writeToScreen('<span style="color:blue">服务端回应&nbsp;'+formatDate(new Date())+'</span><br/><span class="bubble">'+ evt.data+'</span>');
}
function onError(evt) {
writeToScreen('<span style="color: red;">发生错误:</span> '+ evt.data);
}
function doSend() {
var message=$("#message").val();
if (message=='') {
alert("请先填写发送信息");
$("#message").focus();
return false;
}
if (typeof websocket==="undefined"){
alert("websocket还没有连接,或者连接失败,请检测");
return false;
}
if (websocket.readyState==3) {
alert("websocket已经关闭,请重新连接");
return false;
}
console.log(websocket);
$("#message").val('');
writeToScreen('<span style="color:green">你发送的信息&nbsp;'+formatDate(new Date())+'</span><br/>'+ message);
websocket.send(message);
}

function writeToScreen(message) {
var div = "<div class='newmessage'>"+message+"</div>";
var d = $("#output");
var d=d[0];
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
$("#output").append(div);
if (doScroll) {
d.scrollTop = d.scrollHeight - d.clientHeight;
}
}
function en(event){
var evt=evt?evt:(window.event?window.event:null);
if (evt.keyCode==13){
doSend()
}
}
</script>
</div>
</div>
</div>
</body>
</html>

效果图

浏览器访问 http://127.0.0.1:8080/websocket/requestPage

"图片描述"

源码:https://github.com/qq1028951741/springbootdemo or 右上角github进去,springbootdemo项目,如果对您有帮助,麻烦点下star,谢谢


人生两苦:想要却不得,拥有却失去。 –褚禄山
珍惜当下,与君共勉~


本文标题:SpringBoot:整合websocket实现消息推送

文章作者:fufua

发布时间:2018年11月27日 - 19:43:13

最后更新:2018年11月27日 - 19:46:35

原始链接:https://qq1028951741.github.io/2018/11/27/SpringBoot:整合websocket实现消息推送/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

this is end, thank you for reading