在我们以往的软件或者网站使用中,都有遇到过这种情况,莫名的弹出广告或者通知!而在我们的业务系统中,有的时候也需要群发通知公告的方式去告知网站用户一些信息,那么这种功能是怎么实现的呢,本文将使用springboot+webSocket来实现这类功能,当然也有其他方式来实现 长连接/websocket/SSE等主流服务器推送技术比较
使用Intellij IDEA 快速创建一个springboot + webSocket项目
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> 复制代码
@ServerEndpoint
这个注解。这个注解是 Javaee 标准里的注解,tomcat7以上已经对其进行了实现,如果是用传统方法使用tomcat发布的项目,只要在pom文件中引入 javaee 标准即可使用。 <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> 复制代码
首先要注入 ServerEndpointExporter 类,这个bean会自动注册使用了 @ServerEndpoint
注解声明的Websocket endpoint。要注意,如果使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为 它(ServerEndpointExporter) 将由容器自己提供和管理。
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } } 复制代码
接下来就是写websocket的具体实现类,很简单,直接上代码:
package com.example.websocket.controller; import com.example.websocket.service.BulletinService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import java.util.Date; import java.util.concurrent.CopyOnWriteArraySet; /** * @ServerEndpoint 该注解用来指定一个URI,客户端可以通过这个URI来连接到WebSocket。 * 类似Servlet的注解mapping。无需在web.xml中配置。 * configurator = SpringConfigurator.class是为了使该类可以通过Spring注入。 * @Author jiangpeng */ @ServerEndpoint(value = "/webSocket/bulletin") @Component public class BulletinWebSocket { private static final Logger LOGGER = LoggerFactory.getLogger(BulletinWebSocket.class); private static ApplicationContext applicationContext; public static void setApplicationContext(ApplicationContext context) { applicationContext = context; } public BulletinWebSocket() { LOGGER.info("BulletinWebSocket init "); } // concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 private static CopyOnWriteArraySet<BulletinWebSocket> BULLETIN_WEBSOCKETS = new CopyOnWriteArraySet<BulletinWebSocket>(); // 与某个客户端的连接会话,需要通过它来给客户端发送数据 private Session session; /** * 连接建立成功调用的方法 * */ @OnOpen public void onOpen(Session session) throws IOException { this.session = session; // 加入set中 BULLETIN_WEBSOCKETS.add(this); // 新登录用户广播通知 this.session.getBasicRemote().sendText(applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date()); LOGGER.info("有新连接加入{}!当前在线人数为{}", session, getOnlineCount()); } @OnClose public void onClose() { BULLETIN_WEBSOCKETS.remove(this); LOGGER.info("有一连接关闭!当前在线人数为{}", getOnlineCount()); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 * @param session 可选的参数 */ @OnMessage public void onMessage(String message, Session session) { LOGGER.info("来自客户端的信息:{}", message); } @OnError public void onError(Session session, Throwable error) { LOGGER.error("发生错误:{}", session.toString()); error.printStackTrace(); } /** * 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。 * 因为使用了Scheduled定时任务,所以方法不是有参数 * @throws Exception */ @Scheduled(cron = "0/2 * * * * ?") public void sendMessage() throws IOException { // 所有在线用户广播通知 BULLETIN_WEBSOCKETS.forEach(socket -> { try { socket.session.getBasicRemote().sendText("定时:"+applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date()); } catch (IOException e) { e.printStackTrace(); } }); } public static synchronized int getOnlineCount() { return BULLETIN_WEBSOCKETS.size(); } } 复制代码
使用springboot的唯一区别是要添加 @Component
注解,而使用独立容器不用,是因为容器自己管理websocket的,但在springboot中连容器都是spring管理的。
虽然 @Component
默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来 private static CopyOnWriteArraySet<BulletinWebSocket> BULLETIN_WEBSOCKETS = new CopyOnWriteArraySet<BulletinWebSocket>();
。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>static</h1> <div id="msg" class="panel-body"> </div> <input id="text" type="text"/> <button onclick="send()">发送</button> </body> <script src="https://cdn.bootcss.com/web-socket-js/1.0.0/web_socket.js"></script> <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script type="text/javascript"> var websocket = null; //判断当前浏览器是否支持WebSocket if ('WebSocket' in window) { websocket = new WebSocket("ws://127.0.0.1:8080/webSocket/bulletin"); } else { alert("对不起!你的浏览器不支持webSocket") } //连接发生错误的回调方法 websocket.onerror = function () { setMessageInnerHTML("error"); }; //连接成功建立的回调方法 websocket.onopen = function (event) { setMessageInnerHTML("加入连接"); }; //接收到消息的回调方法 websocket.onmessage = function (event) { setMessageInnerHTML(event.data); }; //连接关闭的回调方法 websocket.onclose = function () { setMessageInnerHTML("断开连接"); }; //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接, // 防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { var is = confirm("确定关闭窗口?"); if (is) { websocket.close(); } }; //将消息显示在网页上 function setMessageInnerHTML(innerHTML) { $("#msg").append(innerHTML + "<br/>") }; //关闭连接 function closeWebSocket() { websocket.close(); } ![](https://user-gold-cdn.xitu.io/2019/2/21/1690f655083376d7?w=721&h=457&f=gif&s=31053) //发送消息 function send() { var message = $("#text").val(); websocket.send(message); $("#text").val(""); } </script> </html> 复制代码
GITHUB源码地址 《===
CREATE TABLE `bulletin` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号id', `title` varchar(50) COLLATE utf8_bin NOT NULL COMMENT '标题', `content` varchar(1000) COLLATE utf8_bin NOT NULL COMMENT '内容', `user_type` tinyint(1) NOT NULL COMMENT '通告对象类型 1:单个用户 2:多个用户 3:全部用户', `user_roles` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '通告对象角色', `user_depts` varchar(20) COLLATE utf8_bin DEFAULT NULL COMMENT '通告对象部门', `type` tinyint(1) DEFAULT NULL COMMENT '通告类型 1:系统升级', `publish_time` datetime DEFAULT NULL COMMENT '发布时间', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 0:待发布 1:已发布 2:撤销 ', `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', `created_by` int(11) NOT NULL COMMENT '创建人', `updated_by` int(11) NOT NULL COMMENT '修改人', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='通告表'; 复制代码
CREATE TABLE `bulletin_user` ( `bulletin_id` int(11) NOT NULL COMMENT '通告编号id', `user_id` int(11) NOT NULL COMMENT '用户id', `is_read` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否阅读 0否 1是', `created_at` datetime NOT NULL COMMENT '创建时间', `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`bulletin_id`,`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='用户通告标记表'; 复制代码
以上的功能实现居然可以参考上面 BulletinWebSocket.java 中的这几块代码
/** * 连接建立成功调用的方法 * */ @OnOpen public void onOpen(Session session) throws IOException { this.session = session; // 加入set中 BULLETIN_WEBSOCKETS.add(this); // 新登录用户广播通知 this.session.getBasicRemote().sendText(applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date()); LOGGER.info("有新连接加入{}!当前在线人数为{}", session, getOnlineCount()); } 复制代码
public void sendMessage() throws IOException { // 所有在线用户广播通知 BULLETIN_WEBSOCKETS.forEach(socket -> { try { socket.session.getBasicRemote().sendText("定时:"+applicationContext.getBean(BulletinService.class).getBulletin()+"-"+new Date()); } catch (IOException e) { e.printStackTrace(); } }); } 复制代码
SpringBoot 部署与Spring部署都有一些差别,但现在用Srpingboot的公司多,SpringBoot创建项目快,所以使用该方式来讲解,有一个问题就是开发WebSocket时发现无法通过@Autowired注入bean,一直为空。怎么解决呢?
其实不是不能注入,是已经注入了,但是客户端每建立一个链接就会创建一个对象,这个对象没有任何的bean注入操作,下面贴下实践
接下来
解决办法就是springboot的启动类注入一个static的对象
最后在 WebSocket endpoint 类添加相应的静态对象,并添加 set方法
接着如果那里要使用Spring管理在Bean的话,就可以使用这种方式使用applicationContext.getBean(BulletinService.class)