本文旨在帮助未掌握此技能的小白扫清障碍,快速搭建websocket消息推送服务,高手请绕行。谢谢!
首先,笔者的写作背景也是一名刚刚打通websocket消息推送服务的小白。在连续几日的搜集资料下,最终在没有找到一个完整的解决方案的情况下。摸索出正确的结果,倍感不易的同时,希望能够记录下自己心路历程的同时真正帮助到那些正在此处挣扎的道友。
笔者此前参考了众多资料之后,最终采用的是@Bean注册ServerEndpointExporter,并注解@ServerEndpoint的方式,简单直接。参考前辈长乐忘忧的个人博客:
http://www.cnblogs.com/bianzy/p/5822426.html#3688125
然而还是在实现的过程中出现一系列问题,下面会详细描述,具体实现步骤如下。
首先介绍使用SpringBoot内置Tomcat的配置方式,我们按照流程编辑序号如下:
1、配置SpringBootMaven环境
pom文件中,需要引入spring-boot-starter-websocket的资源包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
2、配置ServerEndpointExporter的组件注册。
这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3、实现类的编码。
此处代码原型依然来源于参考文档,后经笔者调整完善,修改内容如下:
1>完善了统计连接人数,每次关闭会话时都会无限-1的bug。
此bug导致所有异常连接的关闭都会将在线人数-1。某些情况下是@onOpen执行没有成功的,但却会执行@onClose,于是会造成在线人数统计异常。
2>添加了对业务登录用户和会话用户的绑定。
根据参考文档源码,可操作连接成功的会话用户,即websocket session。但是实际开发中,我们操作的是当前业务用户,即数据库user table中的用户。因此,此处需要将业务用户与会话用户绑定,从而实现1对1或多对多的灵活自由消息交互。
实现方式是:在@ServerEndpoint注解时,value值可设定路径参数。而在连接成功,即@onOpen执行时可接收此参数,接收方式为@PathParam。通过此参数即可将业务用户与会话用户绑定。
3>基于业务特点以及实用/安全性等考虑,笔者对代码做出一个原则性限制,即一个业务用户同时只能连接成功一个会话用户。若读者不需要此限制,则自行修改代码即可。
4>添加了几个工具方法:
(1)getcurrentWenSocket(Integer fuserid)根据当前登录的业务用户,获取其对应的会话用户的websocket对象。方便于“系统–>用户”的面对点定点消息推送。
(2)sendMessage(Integer fuserid,String message) 给指定用户发送消息,即集成了(1)方法,可直接调用此方法给当前业务用户推送消息,可用于发送私信。
(3)sendMessageList(List fuseridList, String message) 给指定业务用户列表发送消息,即集成了方法(2),此方法可给指定群体推送同一条消息,可用于发送区域性公告。
(4)sendMessageAll(String message) 给所有在线用户发送消息。此方法可用于发送全网公告。
代码如下:
package com.iking.tms.util;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
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.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* websocket
* @author hufx
* @version 1.0
* @date 2017年6月2日上午10:27:40
*/
@ServerEndpoint(value = "/websocket/{fuserid}")
@Component
public class WebSocket {
private static Logger log = LoggerFactory.getLogger(WebSocket.class);
private static int onlineCount = 0; //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<WebSocket>(); //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
private Session session; //与某个客户端的连接会话,需要通过它来给客户端发送数据
private Integer fuserid; //保存当前登录用户ID
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "fuserid") Integer fuserid) {
try {
this.session = session; //设置当前session
this.fuserid = fuserid;
WebSocket _this = getcurrentWenSocket(this.fuserid); //当前登录用户校验 每个用户同时只能连接一次
if(_this != null){
sendMessage("您已有连接信息,不能重复连接 !");
return;
}
webSocketSet.add(this); //将当前websocket加入set中
addOnlineCount(); //在线数加1
sendMessage("连接成功!");
System.out.println("有一新连接!当前在线人数为" + getOnlineCount());
} catch (IOException e) {
System.out.println("连接异常!");
log.error("websocket连接异常 : 登录人ID = " + this.fuserid +" , Exception = " + e.getMessage());
}
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
boolean b = webSocketSet.remove(this); //从set中删除
if(b && getOnlineCount() > 0){
subOnlineCount(); //在线数减1
}
System.out.println("有一连接关闭!当前在线人数为" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
try {
WebSocket _this = null;
for (WebSocket item : webSocketSet) {
if(item.session.getId() == session.getId()){
_this = item;
}
}
if(_this == null){
this.sendMessage("未连接不能发送消息!");
return;
}
System.out.println("来自客户端的消息:" + message);
this.sendMessage("来自服务端的消息: <已读> " + message);
} catch (IOException e) {
System.out.println("发送消息异常!");
log.error("websocket发送消息异常 : 登录人ID = " + this.fuserid +" , Exception = " + e.getMessage());
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误!");
log.error("websocket发生错误 : 登录人ID = " + this.fuserid);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized void addOnlineCount() {
WebSocket.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocket.onlineCount--;
}
/**
* 根据当前登录用户ID获取他的websocket对象
* @param fuserid 用户ID
* @return
* MyWebSocket
* @author hufx
* @date 2017年6月2日上午10:35:32
*/
public static WebSocket getcurrentWenSocket(Integer fuserid){
if(fuserid == null || fuserid < 1 || webSocketSet == null || webSocketSet.size() < 1){
return null;
}
Iterator<WebSocket> iterator = webSocketSet.iterator();
while (iterator.hasNext()) {
WebSocket _this = iterator.next();
if(_this.fuserid == fuserid){
return _this;
}
}
return null;
}
/**
* 给当前用户发消息(单条)
* @param message 消息
* @throws IOException
* void
* @author hufx
* @date 2017年6月1日下午2:05:36
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
//this.session.getAsyncRemote().sendText(message);
}
/**
* 给指定用户发指定消息(单人单条)
* @param fuserid 用户ID
* @param message 消息
* void
* @author hufx
* @date 2017年6月2日上午11:13:26
*/
public static void sendMessage(Integer fuserid,String message){
try {
if(fuserid == null || fuserid < 1 || StringUtils.isBlank(message)){
return;
}
WebSocket _this = getcurrentWenSocket(fuserid);
if(_this == null){
return;
}
_this.sendMessage(message);
} catch (IOException e) {
System.out.println("发送消息异常!");
}
}
/**
* 给指定人群发消息(单条)
* @param fuseridList 用户ID列表
* @param message 消息
* void
* @author hufx
* @date 2017年6月2日上午11:25:29
*/
public static void sendMessageList(List<Integer> fuseridList, String message){
try{
if(fuseridList == null || fuseridList.size() < 1 || StringUtils.isBlank(message)){
return;
}
for (Integer fuserid : fuseridList) {
WebSocket _this = getcurrentWenSocket(fuserid);
if(_this == null){
continue;
}
_this.sendMessage(message);
}
}catch(Exception e){
System.out.println("发送消息异常!");
log.error("websocket发送消息异常 : 登录人ID = " + fuseridList.toString() +" , Exception = " + e.getMessage());
}
}
/**
* 给所有在线用户发消息(单条)
* @param message 消息
* @throws IOException
* void
* @author hufx
* @date 2017年6月2日上午11:11:05
*/
public static void sendMessageAll(String message) {
try {
if(webSocketSet == null || webSocketSet.size() < 1 || StringUtils.isBlank(message)){
return;
}
for (WebSocket item : webSocketSet) {
item.sendMessage(message);
}
} catch (IOException e) {
System.out.println("发送消息异常!");
log.error("websocket发送消息异常 : Exception = " + e.getMessage());
}
}
}
4、前段页面代码
此代码为参考文档源码。
<!DOCTYPE HTML>
<html>
<head>
<title>My WebSocket</title>
</head>
<body>
Welcome<br/>
<input id="text" type="text" /><button onclick="send()">Send</button> <button onclick="closeWebSocket()">Close</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
websocket = new WebSocket("ws://localhost:8084/websocket");
}
else{
alert('Not support websocket')
}
//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//连接成功建立的回调方法
websocket.onopen = function(event){
setMessageInnerHTML("open");
}
//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭连接
function closeWebSocket(){
websocket.close();
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
</script>
</html>
截至此时,SpringBoot内核Tomcat启动的 websocket已经配置完成,可以投入测试。
测试方式:
1>右键项目run as Java Applacation 启动。
2>编辑前端页面文件,将new WebSocket(“ws://localhost:8084/websocket/1”);中的路径参数修改到
本地保存即可。
3>双击使用浏览器打开此页面,见到下图即表明连接成功。
4>连接成功之后即可发送消息内容给后台。后台Conlose控制台即可见到此连接用户信息。
接下来会讲述maven build 打包时所遇到的问题以及外部Tomcat的websocket 配置调整。
在SpringBoot Run As 启动方式测试成功之后,最终是要将项目部署到服务器Web容器中去的。那么,上述代码在部署以及打包时会遇到一系列问题。具体解释如下:
5、初次打包部署
对上述上述代码进行maven build 打包操作,我们以war包为例。具体打包操作此处不做阐述。
上述代码打包不会报错。
但是在部署时,基本上都会遇到一个错误:
中文解释是 “非法堆栈异常”,描述是 “@ServerEndpoint 组件注册失败”。
Application startup failed
java.lang.IllegalStateException: Failed to register @ServerEndpoint class: class com.iking.tms.util.WebSocket
究其原因,苦思不得其解。在调查了大量资料后,得出如下结论:
1>、tomcat7以下的版本(包含tomcat7部分版本),均不支持websocket。而@ServerEndpoint为javax.websocket包下的。此包在tomcat7后期版本及后续版本中,tomcat的lib目录下可见。名称为:“websocket-api.jar”。
2>、出现此问题的原因。
此时要从SpringBoot的特性说起,我们知道SpringBoot Run As 可以快速启动项目,且能够即时刷新。其原因是SpringBoot拥有一个内置的Tomcat,此Tomcat的版本可在pom.xml中指定。每次我们使用SpringBoot Run As启动项目时,我们的web容器即就是这个内置的Tomcat。此刻web容器连同项目本身都是由Spring进行代理。而当我们将项目打成war包,部署在服务器上的某个Tomcat下时。此刻我们的项目将会交由这个Tomcat去管理。因为外部Tomcat的优先级高于Spring内置Tomcat。问题就在这里。当我们在IDE内使用 SpringBoot Run As去启动时,Spring会帮我们找到内置Tomcat lib中的javax.websocket包加载使用。所以项目正常运行。而当我们将打好的war包放在外部Tomcat上进行启动时。Tomcat管理器根据之前的Javax.websocket包的路径找不到对应的ServerEndpoint类资源文件,因此自然会注册失败。
3>、解决方案。
为了解决此问题,我们可以手动将此包引入项目。
有两种引入方式:
1>>pom.xml 引入
2>>.jar文件直接引入
6、解决问题。
两种解决方案均可,但推荐使用.jar文件直接copy进项目build path即可。
这里重点说明pom文件引入
代码如下:
pom做如下配置:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1-b07</version>
</dependency>
之后再打包。打包时可能会出现“javax.servlet-api不存在”的错误。出现此错误时,我们需要将此jar的路径手动引入项目。引入方式如下:
参考文档:
http://www.cnblogs.com/xiaosiyuan/p/6894766.html
当然,在方便项目迁移,引入是自然是要从Tomcat下找到此jar包保存在项目目录下的。因此。推荐使用jar文件直接build path的方式。
7、再次打包。
此时问题已基本解决,打包成功!
部署!
但是会惊奇的发现,部署时仍会报Failed to register @ServerEndpoint。
梳理后发现原因如下。
8、最终问题得解。
上面有简述过当我们使用外部Tomcat时,项目的管理权将会由Spring交接至Tomcat。
而Tomcat7及后续版本是对websocket直接支持的,且我们所使用的jar包也是tomcat提供的。
但是我们在WebSocketConfig中将ServerEndpointExporter指定给Spring管理。而部署后ServerEndpoint是需要Tomcat直接管理才能生效的。所以此时即就是此包的管理权交接失败,那肯定不能成功了。最后我们需要将WebSocketConfig中的bean配置注释掉。然后再打包上传部署测试。一切正常!
至此,此问题完全解决!
以上为笔者解决SpringBoot实现websocket消息推送的全部步骤、问题、bug、解决方案以及笔者个人的见解。
文中如有错误,还望不吝指正!
Q:980420579 Q群:697819474