SpringBoot+WebSocket集成

什么是WebSocket?

为什么需要 WebSocket?

前言

maven依赖

WebSocketConfig

WebSocketServer

消息推送

页面发起

运行效果

后续

Websocker注入Bean问题

netty-websocket-spring-boot-starter

Springboot2+Netty+Websocket

ServerEndpointExporter错误

正式项目的前端WebSocket框架 GoEasy

`@Component`和`@ServerEndpoint`关于是否单例模式,能否使用static Map等一些问题的解答

Vue版本的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的支持简直太棒了,直接就有包可以引入

org.springframework.boot

spring-boot-starter-websocket

1

2

3

4

WebSocketConfig

启用WebSocket的支持也是很简单,几句代码搞定

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**

开启WebSocket支持

@author zhengkai.blog.csdn.net

*/

@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

WebSocketServer

这就是重点了,核心都在这里。

因为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.system.demo.config;

import java.io.IOException;

import java.util.concurrent.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.apache.commons.lang.StringUtils;

import org.springframework.stereotype.Component;

import cn.hutool.log.Log;

import cn.hutool.log.LogFactory;

/**

@author zhengkai.blog.csdn.net

*/

@ServerEndpoint("/imserver/{userId}")

@Component

public class WebSocketServer {

static Log log=LogFactory.get(WebSocketServer.class);

/**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/

private static int onlineCount = 0;

/**concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。*/

private static ConcurrentHashMapwebSocketMap = 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;

if(webSocketMap.containsKey(userId)){

webSocketMap.remove(userId);

webSocketMap.put(userId,this);

//加入set中

}else{

webSocketMap.put(userId,this);

//加入set中

addOnlineCount();

//在线数加1

}

log.info("用户连接:"+userId+",当前在线人数为:" + getOnlineCount());

try {

sendMessage("连接成功");

} catch (IOException e) {

log.error("用户:"+userId+",网络异常!!!!!!");

}

}

/**

* 连接关闭调用的方法

*/

@OnClose

public void onClose() {

if(webSocketMap.containsKey(userId)){

webSocketMap.remove(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 toUserId=jsonObject.getString("toUserId");

//传送给对应toUserId用户的websocket

if(StringUtils.isNotBlank(toUserId)&&webSocketMap.containsKey(toUserId)){

webSocketMap.get(toUserId).sendMessage(jsonObject.toJSONString());

}else{

log.error("请求的userId:"+toUserId+"不在该服务器上");

//否则不在这个服务器上,发送到mysql或者redis

}

}catch (Exception e){

e.printStackTrace();

}

}

}

/**

*

* @param session

* @param error

*/

@OnError

public void onError(Session session, Throwable error) {

log.error("用户错误:"+this.userId+",原因:"+error.getMessage());

error.printStackTrace();

}

/**

* 实现服务器主动推送

*/

public void sendMessage(String message) throws IOException {

this.session.getBasicRemote().sendText(message);

}

/**

* 发送自定义消息

* */

public static void sendInfo(String message,@PathParam("userId") String userId) throws IOException {

log.info("发送消息到:"+userId+",报文:"+message);

if(StringUtils.isNotBlank(userId)&&webSocketMap.containsKey(userId)){

webSocketMap.get(userId).sendMessage(message);

}else{

log.error("用户"+userId+",不在线!");

}

}

public static synchronized int getOnlineCount() {

return onlineCount;

}

public static synchronized void addOnlineCount() {

WebSocketServer.onlineCount++;

}

public static synchronized void subOnlineCount() {

WebSocketServer.onlineCount--;

}

}

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

消息推送

至于推送新信息,可以再自己的Controller写个方法调用WebSocketServer.sendInfo();即可

import com.softdev.system.demo.config.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.net

*/

@RestController

public class DemoController {

@GetMapping("index")

public ResponseEntityindex(){

return ResponseEntity.ok("请求成功");

}

@GetMapping("page")

public ModelAndView page(){

return new ModelAndView("websocket");

}

@RequestMapping("/push/{toUserId}")

public ResponseEntitypushToWeb(String message, @PathVariable String toUserId) throws IOException {

WebSocketServer.sendInfo(message,toUserId);

return ResponseEntity.ok("MSG SEND SUCCESS");

}

}

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

页面发起

页面用js代码调用websocket,当然,太古老的浏览器是不行的,一般新的浏览器或者谷歌浏览器是没问题的。还有一点,记得协议是ws的,如果使用了一些路径类,可以replace(“http”,“ws”)来替换协议。

websocket通讯

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="${request.contextPath}/im/"+$("#userId").val();

var socketUrl="http://localhost:9999/demo/imserver/"+$("#userId").val();

socketUrl=socketUrl.replace("https","ws").replace("http","ws");

console.log(socketUrl);

if(socket!=null){

socket.close();

socket=null;

}

socket = new WebSocket(socketUrl);

//打开事件

socket.onopen = function() {

console.log("websocket已打开");

//socket.send("这是来自客户端的消息" + location.href + new Date());

};

//获得消息事件

socket.onmessage = function(msg) {

console.log(msg.data);

//发现消息进入 开始处理前端触发逻辑

};

//关闭事件

socket.onclose = function() {

console.log("websocket已关闭");

};

//发生了错误事件

socket.onerror = function() {

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()+'"}');

}

}

【userId】:

【toUserId】:

【toUserId】:

【操作】:

【操作】:

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

运行效果

v20200105,加入开源项目spring-cloud-study-websocket,更新运行效果,更方便理解。

v1.1的效果,刚刚修复了日志,并且支持指定监听某个端口,代码已经全部更新,现在是这样的效果

打开两个页面,按F12调出控控制台查看测试效果:

页面 参数

http://localhost:9999/demo/page fromUserId=10,toUserId=20

http://localhost:9999/demo/page fromUserId=20,toUserId=10

分别开启socket,再发送消息

在这里插入图片描述

在这里插入图片描述

向前端推送数据:

http://localhost:9999/demo/push/10?message=123123

在这里插入图片描述

通过调用push api,可以向指定的userId推送信息,当然报文这里乱写,建议规定好格式。

后续

针对简单IM的业务场景,进行了一些优化,可以看后续的文章SpringBoot2+WebSocket之聊天应用实战(优化版本)(v20201005已整合)

主要变动是CopyOnWriteArraySet改为ConcurrentHashMap,保证多线程安全同时方便利用map.get(userId)进行推送到指定端口。

相比之前的Set,Set遍历是费事且麻烦的事情,而Map的get是简单便捷的,当WebSocket数量大的时候,这个小小的消耗就会聚少成多,影响体验,所以需要优化。在IM的场景下,指定userId进行推送消息更加方便。

Websocker注入Bean问题

关于这个问题,可以看最新发表的这篇文章,在参考和研究了网上一些攻略后,项目已经通过该方法注入成功,大家可以参考。

关于controller调用controller/service调用service/util调用service/websocket中autowired的解决方法

netty-websocket-spring-boot-starter

Springboot2构建基于Netty的高性能Websocket服务器(netty-websocket-spring-boot-starter)

只需要换个starter即可实现高性能websocket,赶紧使用吧

Springboot2+Netty+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框架来做,稳定性、实时性有保证的多,也会包括一些心跳、重连机制。

GoEasy专注于服务器与浏览器,浏览器与浏览器之间消息推送,完美兼容世界上的绝大多数浏览器,包括IE6, IE7之类的非常古老的浏览器。支持Uniapp,各种小程序,react,vue等所有主流Web前端技术。

GoEasy采用 发布/订阅 的消息模式,帮助您非常轻松的实现一对一,一对多的通信。

https://www.goeasy.io/cn/doc/

@Component和@ServerEndpoint关于是否单例模式,能否使用static Map等一些问题的解答

看到大家都在热心的讨论关于是否单例模式这个问题,请大家相信自己的直接,如果websocket是单例模式,还怎么服务这么多session呢。

websocket是原型模式,@ServerEndpoint每次建立双向通信的时候都会创建一个实例,区别于spring的单例模式。

Spring的@Component默认是单例模式,请注意,默认 而已,是可以被改变的。

这里的@Component仅仅为了支持@Autowired依赖注入使用,如果不加则不能注入任何东西,为了方便。

什么是prototype 原型模式? 基本就是你需要从A的实例得到一份与A内容相同,但是又互不干扰的实例B的话,就需要使用原型模式。

关于在原型模式下使用static 的webSocketMap,请注意这是ConcurrentHashMap ,也就是线程安全/线程同步的,而且已经是静态变量作为全局调用,这种情况下是ok的,或者大家如果有顾虑或者更好的想法的化,可以进行改进。 例如使用一个中间类来接收和存放session。

为什么每次都@OnOpen都要检查webSocketMap.containsKey(userId) ,首先了为了代码强壮性考虑,假设代码以及机制没有问题,那么肯定这个逻辑是废的对吧。但是实际使用的时候发现偶尔会出现重连失败或者其他原因导致之前的session还存在,这里就做了一个清除旧session,迎接新session的功能。

Vue版本的websocket连接

感谢@GzrStudy的贡献,供大家参考。

服务器如何向前端页面推送消息,后端向前端推送消息相关推荐

  1. 网易云信IM即时通讯聊天源码SDK 并发高轻松万人并发稳定不丢消息 后端PHP 前端 安卓Java

    优势:承载用户并发同时在线都在第三方网易云信高并发轻松承载万人并发. 开发语言:后端PHP 前端安卓Java 苹果 OC PC端C# 网易云信 IM UIKit是基于 NIM SDK(网易云信 IM ...

  2. 前端页面生成神器以及后端变量命名神器

    1.imgcook前端页面生成具体看官网上的教程 2.后端变量命名codelf 网址 访问可能有点慢 例如:我输入一个员工,下面就会出面对应的名称 鼠标放上去会有复制的选项,非常方便

  3. web开发中前端页面是如何跟后端服务器数据交互的

    后端服务器一般是指servlet容器,用于执行java源程序 常见的网页有html,htm,shtml,asp,aspx,php,jsp等格式 前两个常用于静态网页,后面几个常用于动态网页. 这里前端 ...

  4. html前端页面的基本骨架是,web前端入门到实战:css实现的骨架屏方案

    web前端入门到实战:css实现的骨架屏方案 发布时间:2020-08-04 01:32:03 来源:51CTO 阅读:152 作者:前端向南 优点 简单,不需要工程,不用puppeteer生成骨架d ...

  5. 前端如何提交数据给后端(包含前端和后端代码)

    前端使用Ajax提交Json数据给Spring Boot后端 如果你想提交表单数据到服务器,可以使用Ajax技术将表单数据通过HTTP POST请求到发送到服务器. 前端代码如下 <!DOCTY ...

  6. android极光推送判断消息,通过极光推送给Android所有用户发送推送消息

    https://www.cnblogs.com/yueguanguanyun/p/8485381.html 所需jar包,在maven中添加下列依赖: cn.jpush.api jpush-clien ...

  7. web前端页面性能优化SEO优化

    首先什么叫网站? 网站一般分为前端和后台.我们可以理解成后台是用来实现网站的功能的,比如:实现用户注册,用户能够为文章发表评论等等.而前端应该是属于功能的表现.并且影响用户访问体验的绝大部分来自前端页 ...

  8. Web项目中前端页面通过URL传中文或 # 特殊字符到后台出现乱码解决方案

    对于刚入行新手来说碰到URL传递中文数据到后台出现乱码或是传递包含特使符号'#'后端获取不到的情况是比较头疼的事,这里我就告诉你这两个的解决的方式: 1.URL传递中文 前端页面:中文字符串用 enc ...

  9. Spring+SpringMVC+MyBatis明日方舟版人员信息管理系统前端页面代码前后端交互+SSM框架 管理员登录 游客登录 普通用户登录 人员的增删改查 信息更新 图片上传 分页查询)

    Spring+SpringMVC+MyBatis明日方舟版人员信息管理系统前端页面代码(前后端交互+SSM框架 管理员登录 游客登录 普通用户登录 人员的增删改查 信息更新 图片上传 分页查询 修改密 ...

最新文章

  1. ORB-SLAM3中的ORB提取
  2. php批量请求url_php请求url的方法小结
  3. 面完字节跳动,才知道自己的数据结构与算法有多薄弱...
  4. 【repost】JavaScript Scoping and Hoisting
  5. less新手入门(四)—— Mixin Guards
  6. go-ethereum环境搭建及目录结构介绍
  7. 查看oracle连接客户端
  8. 计算机右键管理显示没权限,解决右键 选择打开方式提示没有权限
  9. 点击打开自己博客地址通过默认浏览器
  10. 为什么 web 开发人员需要迁移到. NET Core, 并使用 ASP.NET Core MVC 构建 web 和 API
  11. 生信分析过程中这些常见文件的格式以及查看方式你都知道吗?
  12. 在modelarts上部署backend为TensorFlow的keras模型
  13. 服务器压力测试系列二:服务器监控工具tsar安装
  14. Dynamics CRM2013 Server2012R2下IFD部署遇到There is already a listener on IP endpoint的解决方法...
  15. anroid adt离线下载地址(可自己选最新版本使用迅雷下载)
  16. 三菱plc分拣程序_三菱PLC实现电梯控制方案,含全套程序设计
  17. python mysql扩展,python-sql-faker:轻量级、易拓展的数据库智能填充开源库(Python实现版)...
  18. python中去除空格用什么函数_python中用什么函数去掉空格
  19. css兼容360浏览器极速,CSS hack 360浏览器 极速模式与兼容模式
  20. 差点被一个截图忽悠了,分析一个QQ空间钓鱼网站

热门文章

  1. CSS——引入阿里字体图标步骤
  2. 死锁定理与资源分配图化简法
  3. 创建一个图文并茂的调查
  4. Hexo之Next主题美化代码
  5. Vue3通过axios来读取本地json文件
  6. n与9n--char3
  7. icesword 是如何列出隐藏进程?
  8. SL651协议报文解析(一)
  9. A* 算法原理以及在二维环境地图中的应用 -- Python 代码实现
  10. Python网络爬虫入门(五)—— 巧用抓包,爬遍SCU玻璃杯事件所有神回复