websoket+springboot+jquery.websocket-0.0.1.js+nginx+springcloud网关整合+websocket重连机制
前段时间在项目中引进websocket的时候,在网上查找资料,发现各种资料都比较零散,所以就写了这篇博客,将自己查找的资料以及写的代码整理出来,这也是自己的第一篇博客。这篇博客里,有些东西自己已经尝试了,有些还没有,但是给出了我自己查找的资料链接,大家可以参考。
1.什么是websocket
简单来说,websocket是用来解决长连接问题,以及客户端和服务器之间相互通信的问题的。这两个问题都是http协议不能很好解决的。具体区别可以查看这篇文章,感觉讲的比较好。
2.springboot+websocket
maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
WebSocketConfig
@Configuration
public class WebConfig {
//开启WebSocket支持
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
WebSocketServer
这段代码也是在网上参考其他教程,然后稍微改动的。代码中有一些自定义的实体,可以忽略。包含了websocket发送消息对象,和注入bean,后面会讲到。
import com.gcx.common.entities.Message; //自定义的消息实体
import com.gcx.common.entities.MessageType; //自定义的消息类型
import com.gcx.common.entities.UserOfflineException; //自定义的用户不在线异常
import com.gcx.common.entities.WsMessage;//自定义的用来发送消息的对象,后面会讲到
import com.gcx.message.service.MessageService; //注入的bean,后面会讲到
/**
* @author gcx
* @data 2019年3月20日 下午2:59:19
* 类说明
*/
@ServerEndpoint(value="/ws/{userId}", encoders= {ServerEncoder.class})
@Component
public class WebSocketServer {
private final static Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
public static MessageService msgService;
@Autowired
public void setMsgComponent(MessageService msgService) {
WebSocketServer.msgService = msgService;
}
//静态变量,用来记录当前在线连接数。
private static int onlineCount = 0;
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
//新:使用map对象,便于根据userId来获取对应的WebSocket
private static ConcurrentHashMap<String, WebSocketServer> websocketMap = new ConcurrentHashMap<>();
//接收sid
private String userId="";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
boolean exist = false;
//判断连接是否已经存在
if(null!=websocketMap.get(userId)) {
logger.info("该连接已经存在");
exist = true;
}
this.session = session;
websocketMap.put(userId,this);
logger.info("websocketList->"+JSON.toJSONString(websocketMap));
//webSocketSet.add(this); //加入set中
//如果连接不存在,则连接数+1
if(!exist) {
addOnlineCount(); //在线数加1
}
logger.info("有新窗口开始监听:"+userId+",当前在线人数为" + getOnlineCount());
this.userId=userId;
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if(websocketMap.get(this.userId)!=null){
websocketMap.remove(this.userId);
//webSocketSet.remove(this); //从set中删除
subOnlineCount(); //在线数减1
logger.info("有一连接关闭!当前在线人数为" + getOnlineCount());
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*
*/
@OnMessage
public void onMessage(String message, Session session) {
logger.info("收到来自窗口"+userId+"的信息:"+message);
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
logger.error("发生错误");
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
/** @content 向某个用户推送消息
* @param type 消息类型
* @param
* @return
*
* @author gcx
* @date 2019年4月12日
*/
public void sendMsg2User(String type, Message msg) throws IOException, EncodeException, UserOfflineException {
WsMessage<Message> wsMessage = new WsMessage<>(type, msg);
WebSocketServer ws = websocketMap.get(msg.getTarget());
if(null==ws) {
throw new UserOfflineException("用户不在线", msg.getTarget());
}
ws.session.getBasicRemote().sendObject(wsMessage);
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static synchronized Enumeration<String> getOnlineUserList(){
return websocketMap.keys();
}
public static synchronized KeySetView<String, WebSocketServer> getOnlineUserList2(){
return websocketMap.keySet();
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
ServerEncoder 支持返回对象
用于支持websocket返回对象。其中WsMessage是我自定义的消息对象,大家可以定义自己的消息对象。另外,在对对象进行json序列化时,由于考虑到springboot本身自带了jackson,所以使用了jackson进行对象序列化,大家也可以换成fastjson。
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gcx.common.entities.WsMessage;
/**
* @author gcx
* @param <T>
* @data 2019年4月4日 下午6:46:23
* 类说明
*/
public class ServerEncoder<T> implements Encoder.Text<WsMessage<T>> {
@Override
public void destroy() {
// TODO Auto-generated method stub
}
@Override
public void init(EndpointConfig arg0) {
// TODO Auto-generated method stub
}
@Override
public String encode(WsMessage<T> wsMessage) throws EncodeException {
//将java对象转换为json字符串
// return JSON.toJSONString(wsMessage);
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(wsMessage);
} catch (JsonProcessingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
return null;
}
}
}
bean注入问题
WebSocketServer中是不能直接使用下面的方法注入bean的。
@Autowired
private MessageService msgService;
本质原因:spring管理的都是单例(singleton),和 websocket (多对象)相冲突。
详细原因:spring是单例管理模式,在springboot项目启动自动初始化websocket(非用户连接),会自动为其注入service,即,第一次初始化websocket的时候,会注入成功。但是每次客户端发起websocket连接,都会重新初始化出一个新的websocket对象,这时矛盾出现了:spring 管理的都是单例,不会给第二个 websocket 对象注入 service,所以导致只要是用户连接创建的 websocket 对象,都不能再注入了。
解决方法:很简单,主要是将service声明成静态变量。
//这里使用静态,让 service 属于类
private static MessageService msgService;
@Autowired
public void setMsgComponent(MessageService msgService) {
WebSocketServer.msgService = msgService;
}
此处主要参考的博客:
3.客户端jquery.websocket
jquery.websocket是对websocket进行了一定的封装,我觉得最大的改变就是,其对消息类型的区分。具体不细讲了,直接上代码:
function initWS(uid) {
// var socketURL = 'ws://127.0.0.1:10013/ws/'+uid;
var socketURL = 'ws://localhost:881/ws/' + uid;
websocket = $.websocket(socketURL, {
open: function () {
// when the socket opens
// alert("open");
},
close: function (e) {
// when the socket closes
console.log('websocket 断开: ' + e.code + ' ' + e.reason + ' ' + e.wasClean);
console.log(e);
},
//收到服务端推送的消息处理
events: {
'radio': function (event) {
// console.info($.parseJSON(event.data));
$("#msg").empty().text(JSON.stringify(event.data));
// alert(event.data);
// console.info(event.data);
},
'remind': function (event) {
var data = event.t;
var type = 'warning';
switch (data.status) {
case -1: type = 'error'; break;
case 0: type = 'warning'; break;
case 1: type = 'success'; break;
case 2: type = 'info'; break;
default: break;
}
addToast("系统消息", data.content, type)
// $("#msg").empty().text(JSON.stringify(event.data));
// alert(event.data);
// console.info(event.data);
// console.info($.parseJSON(event.data));
}
//... more custom type of message
}
});
}
从上面可以看出,jquery.websocket是对接受到的消息按照类别分别进行不同的处理。这需要后台服务定义相应的对象。
4.ws协议的反向代理配置
现在服务的反向代理一般都是用nginx或者apache,这里主要讲一下nginx的配置。
主要是最后两行的配置。配置结束,当你开开心心的使用的时候,会发现,如果在60s内没有消息交互的话,websocket就断开了。因为配置中有个默认参数proxy_read_timeout的值是60s,要是想延长wesocket的连接时间,可以将这个参数调大。但是这不是根本的解决办法,要想解决这个问题,就需要进行心跳检测,后面会讲到。
location ^~/ws {
proxy_pass http://127.0.0.1:10013/ws/;
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
5.springcloud网关整合websocket
我的springcloud网关使用的是zuul1,zuul1是开源的,但是不支持长连接,据说zuul2支持长连接,但是开源遥遥无期。
我主要是参照下面这篇文章中的“自定义zuul过滤器”进行尝试,但是不知道为什么失败了。具体代码,我就不贴出来了。
最后想出来一个临时解决方案,就是将websocket服务的端口单独通过nginx代理出来,但是这不是好的解决方案。
将来准备使用spring自己开源的gateway网关。之前在网上看到,说就是因为zuul2开源总是跳票,随意spring忍无可忍就推出了gateway,不知是真是假。gateway支持长连接,听说性能也别zuul1强很多。
可以看看下面两篇文章了解一下
https://my.oschina.net/javaroad/blog/3048030
https://www.jianshu.com/p/76d2da1d0dd7
6.websock的重连机制
websocket连接断开的情况很多,其中的两种情况为:
- 上文提到的nginx反向代理默认60s无消息,就断开;
- 网络不稳定断开后,ws也会断开,当电脑自动重新连接网络时,ws并没有自动重连;
给大家分享一篇分析websocket断开原因查找的文章
因为考虑到我自己的业务需求,具体的解决方案我还没有尝试,但是这里把我找的的解决方案分享给大家:
思考:
之所以没有按照上面的教程进行尝试,主要是考虑到我的业务实际情况。
因为我是通过用户唯一标识符来建立连接。当一个用户打开多个websocket的页面的时候,每个新的websocket页面都会使用用户唯一标识符建立一个新的websocket连接,就会把旧的连接挤掉。这样子就造成,原来的页面无法接受到后台服务推送的消息的问题。
考虑过使用用户唯一标识符和每个页面生成的随机数组成的字符串来建立websocket连接,但是这样子后台服务向某个用户推送消息时,需要遍历所有的websocket连接,对效率又有影响,所以就纠结了。
大家有什么好的解决方案,希望可以留言告知,不胜感激!