前言
最近在做一个点餐项目,其中,用户下单之后需要主动推送到商家去,所以就用到了webSocket编程。后端是springboot,前端是uniapp,运行方式是用外部tomcat容器运行。实现目标是用户点餐之后主动推送到商家,推送失败时缓存数据,连接成功之后再次推送。
查看了许多文章,大部分文章都没有做连接异常的处理,部分文章加入了前端的心跳检测,但是都没有对后端发送失败的情形进行处理。也遇到很多坑,花了许多时间,在此记录下。
注:不使用文件直接启动项目而使用外置tomcat容器原因有二
其一:项目最后需要和前端项目部署到同一台服务器上面,如果以jar方式启动,最终和tomcat部署的前端项目就会是不同端口。而我申请的是免费的lls证书,不可以设置二级域名,这样就无法进行部署。
其二:以文件方式启动,存在一点问题,会一直进onError方法,也没有找到原因(如果大家解决了这个问题,欢迎分享学习)
实现思路
前端
- 页面加载的时候,建立webSocket连接
- 连接建立之后,初始化webSocket环境,绑定webSocket相关方法
- 连接打开之后,添加连接检测机制,15s检测一次连接是否正常
- 连接失败,则进行重连,重复步骤2
- webSocket异常关闭和出错的时候,进行重连,重复步骤2
后端
- 新增订单时调用主动推送消息的方法
- 若推送失败,则把数据缓存到redis
- 待前端重连成功之后,把redis的数据取出来重新发送
具体实现
添加依赖
- 首先添加websocket依赖
<!--webSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 排除内置tomcat
<!--排除内置的tomcat-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
- 编译时需加入的依赖
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<scope>provided</scope>
</dependency>
详细实现
以下前端代码使用了uniapp的api,如果你使用的不是uniapp,请大家自行修改下代码。
- 页面初始化onLoad时调用connectSocket方法
connectSocket() {
let _this = this
//g_webSocketUrl 域名+项目名
//g_header.value.user_id 用户id
uni.connectSocket({
url: _this.g_webSocketUrl + '/webSocket/addOrder/' + _this.g_header.value.user_id,
success() {
//初始化socket环境 绑定相关的方法
_this.initSocketEvent()
}
})
},
- initSocketEvent中绑定相关方法(步骤3-6中的方法)
- 连接打开,获取后端缓存的订单(第一次加载时直接进行查询,不需要推送),加入存活检测机制
uni.onSocketOpen(function(res) {
console.log('WebSocket连接打开!')
//获取断线期间的新订单
_this.getNewOrder()
//连接存活检测
_this.keepAliveCheck()
})
- 收到后端发送的消息,判断是不是存活检测的测试数据
uni.onSocketMessage(function(res) {
if (res.data == 'pong') {
_this.keepAliveFlag = true
console.log('webSocket连接正常')
} else {
console.log('WebSocket收到消息!')
_this.onSocketMessageSucc(JSON.parse(res.data))
}
})
- 连接关闭,主动调用关闭连接方法,15s后进行重连,重复步骤1
uni.onSocketClose(function(res) {
console.log('WebSocket连接关闭')
console.log(res)
uni.closeSocket()
clearInterval(_this.clock)
setTimeout(() => {
_this.connectSocket()
}, 15000)
})
- 连接出错,主动调用关闭连接方法,15s后进行重连,重复步骤1
uni.onSocketError(function(res) {
console.log('WebSocket连接错误')
console.log(res)
clearInterval(_this.clock)
setTimeout(() => {
_this.connectSocket()
}, 15000)
})
- 连接打开5s之后获取缓存的新订单
前端代码:
getNewOrder() {
let _this = this
setTimeout(() => {
uni.request({
url: _this.g_domain + '/api/order/' + 'getNewOrderList',
method: 'GET',
header: _this.g_header.value,
success(res) {
if (res.statusCode == 200 && res.data.status == 200) {} else {
let msg = typeof(res.data) == 'string' ? res.data : res.data.msg
_this.$api.msg(msg)
}
},
fail() {
_this.$api.msg('数据请求失败,请稍后重试!')
}
})
}, 5000)
},
后端代码:
@GetMapping(value = "/getNewOrderList")
public ResultData getNewOrderList(@RequestAttribute(ConstantUtils.CURRENT_USER) UserRedisBO userInfo) {
if (userInfo == null || StringUtils.isBlank(userInfo.getUserId())) {
return ResultData.errorException("未知错误,请稍后重试");
}
//连接成功之后取出缓存在redis中的订单进行推送
String userId = userInfo.getUserId();
String key = ConstantUtils.ORDER_PUSH_ + userId.toUpperCase();
if (redisTemplate.hasKey(key)) {
List<String> orders = redisTemplate.opsForList().range(key, 0, -1);
redisTemplate.delete(key);
if (orders != null) {
for (String order : orders) {
try {
AddOrderSocket.sendInfo(order, userId);
if (!AddOrderSocket.sendInfo(order, userId)) {
//推送出错 缓存到redis
redisTemplate.opsForList().rightPushAll(ConstantUtils.ORDER_PUSH_ + userId.toUpperCase(), order);
}
} catch (IOException e) {
log.error("用户:" + userId + ",网络异常!!!!!!");
redisTemplate.opsForList().rightPushAll(ConstantUtils.ORDER_PUSH_ + userId.toUpperCase(), order);
}
}
}
}
return ResultData.ok();
}
- 存活检测机制,发送消息’ping’给后端,10s内收到回复’pong’,则连接正常,否则进行重连(和步骤4联系起来看)
keepAliveCheck() {
let _this = this
_this.clock = setInterval(() => {
console.log('webSocket心跳检测')
uni.sendSocketMessage({
data: 'ping',
success() {
_this.keepAliveFlag = false
setTimeout(() => {
if (!_this.keepAliveFlag) {
clearInterval(_this.clock)
uni.closeSocket()
console.log('webSocket准备重连')
_this.connectSocket()
}
}, 10000)
}
})
}, 15000)
},
- 用户下单时推送消息
try {
if(!AddOrderSocket.sendInfo(msg, user.getUserId())){
//推送出错 缓存到redis
redisTemplate.opsForList().rightPushAll(ConstantUtils.ORDER_PUSH_+user.getUserId().toUpperCase(),msg);
}
} catch (IOException e) {
e.printStackTrace();
//推送发送异常,则把数据保存下来 下次连接之后继续推送
//缓存到redis
redisTemplate.opsForList().rightPushAll(ConstantUtils.ORDER_PUSH_+user.getUserId().toUpperCase(),msg);
}
相关源码
代码中涉及到的部分类未给出,如有需要的留言联系我。
AddOrderSocket.java
/**
* 新订单推送socket
*
* @author: pansong
* @date: 2020-06-01 11:25
**/
@Slf4j
@ServerEndpoint("/webSocket/addOrder/{userId}")
@Component
public class AddOrderSocket {
/**
* session 与客户端的连接对话,需要通过其给客户端发送消息
* userId 用户唯一标识
* webSocketMap 存放已连接的客户端信息——ConcurrentHashMap是线程安全的
*/
private Session session;
private String userId;
private static ConcurrentMap<String, AddOrderSocket> webSocketMap = new ConcurrentHashMap<>();
/***
* 连接建立
*
* @param session 客户端对话
* @param userId 用户id
* @return: void
* @author: pansong
* @date: 2020/6/11 23:45
**/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
webSocketMap.put(userId, this);
log.info(userId + ":连接,在线人数:" + webSocketMap.size());
// this.sendMsg("pong");
}
/***
* 连接关闭
*
* @param
* @return: void
* @author: pansong
* @date: 2020/6/11 23:45
**/
@OnClose
public void onClose() {
if (this.userId != null && webSocketMap.containsKey(this.userId)) {
webSocketMap.remove(this.userId);
}
log.info(this.userId + ":退出,在线人数:" + webSocketMap.size());
}
/***
* 连接出错
*
* @param throwable
* @return: void
* @author: pansong
* @date: 2020/6/11 23:46
**/
@OnError
public void onError(Throwable throwable) {
log.error(this.userId + ":错误,原因:" + throwable.getMessage());
if (webSocketMap.containsKey(this.userId)) {
webSocketMap.remove(this.userId);
}
}
/***
* 推送消息到前端
*
* @param msg 消息内容
* @return: void
* @author: pansong
* @date: 2020/6/11 23:46
**/
public Boolean sendMsg(String msg) {
try {
this.session.getBasicRemote().sendText(msg);
} catch (IOException e) {
log.error(e.getMessage());
e.printStackTrace();
return false;
}
return true;
}
/**
* 收到消息
*
* @param msg 消息内容
* @return: void
* @author: pansong
* @date: 2020/6/11 23:48
**/
@OnMessage
public void onMessage(String msg) {
if (StringUtils.isNotBlank(msg)) {
if("ping".equals(msg)){
this.sendMsg("pong");
}
else{
log.info(this.userId + ":消息,报文:" + msg);
//解析报文
JSONObject jsonObject = JSON.parseObject(msg);
//追加发送人 范篡改
jsonObject.put("sendUserId", this.userId);
String receiveUserId = jsonObject.getString("receiveUserId");
//存在receiveUserId 单发
if (StringUtils.isNotBlank(receiveUserId)) {
if (webSocketMap.containsKey(receiveUserId)) {
webSocketMap.get(receiveUserId).sendMsg(jsonObject.toJSONString());
}
} else {//不存在 则群发
webSocketMap.forEach((k, v) -> {
v.sendMsg(msg);
});
}
}
}
}
/**
* 自定义推送消息
*
* @param msg 消息内容
* @param userId 用户id
* @return: java.lang.Boolean
* @author: pansong
* @date: 2020/6/11 23:49
**/
public static Boolean sendInfo(String msg, String userId) throws IOException {
if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
webSocketMap.get(userId).sendMsg(msg);
log.info(userId+":发送,报文:"+msg);
return true;
} else {
log.error(userId + ":掉线");
return false;
}
}
}
main.js
export default {
data() {
return {
keepAliveFlag: true,
clock: -1,
}
},
onLoad() {
this.connectSocket()
},
methods: {
connectSocket() {
let _this = this
uni.connectSocket({
url: _this.g_webSocketUrl + '/webSocket/addOrder/' + _this.g_header.value.user_id,
success() {
//初始化socket环境 绑定相关的方法
_this.initSocketEvent()
}
})
},
initSocketEvent() {
let _this = this
uni.onSocketOpen(function(res) {
console.log('WebSocket连接打开!')
//获取断线期间的新订单
_this.getNewOrder()
//连接存活检测
_this.keepAliveCheck()
})
uni.onSocketMessage(function(res) {
if (res.data == 'pong') {
_this.keepAliveFlag = true
console.log('webSocket连接正常')
} else {
console.log('WebSocket收到消息!')
_this.onSocketMessageSucc(JSON.parse(res.data))
}
})
uni.onSocketClose(function(res) {
console.log('WebSocket连接关闭')
console.log(res)
uni.closeSocket()
clearInterval(_this.clock)
setTimeout(() => {
_this.connectSocket()
}, 15000)
})
uni.onSocketError(function(res) {
console.log('WebSocket连接错误')
console.log(res)
clearInterval(_this.clock)
setTimeout(() => {
_this.connectSocket()
}, 15000)
})
},
getNewOrder() {
let _this = this
setTimeout(() => {
uni.request({
url: _this.g_domain + '/api/order/' + 'getNewOrderList',
method: 'GET',
header: _this.g_header.value,
success(res) {
if (res.statusCode == 200 && res.data.status == 200) {} else {
let msg = typeof(res.data) == 'string' ? res.data : res.data.msg
_this.$api.msg(msg)
}
},
fail() {
_this.$api.msg('数据请求失败,请稍后重试!')
}
})
}, 5000)
},
keepAliveCheck() {
let _this = this
_this.clock = setInterval(() => {
console.log('webSocket心跳检测')
uni.sendSocketMessage({
data: 'ping',
success() {
_this.keepAliveFlag = false
setTimeout(() => {
if (!_this.keepAliveFlag) {
clearInterval(_this.clock)
uni.closeSocket()
console.log('webSocket准备重连')
_this.connectSocket()
}
}, 10000)
}
})
}, 15000)
},
onSocketMessageSucc(resData) {
//处理数据
},
}
}