6 changed files with 303 additions and 0 deletions
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
package com.cloud.kicc.commonbiz.api.entity; |
||||
|
||||
import lombok.AllArgsConstructor; |
||||
import lombok.Data; |
||||
import lombok.EqualsAndHashCode; |
||||
import lombok.experimental.Accessors; |
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
||||
|
||||
/** |
||||
*<p> |
||||
* SSE 信号量容器 |
||||
*</p> |
||||
* |
||||
* @Author: entfrm开发团队-王翔 |
||||
* @since: 2022/9/15 |
||||
*/ |
||||
@Data |
||||
@EqualsAndHashCode(callSuper = false) |
||||
@Accessors(chain = true) |
||||
@AllArgsConstructor |
||||
public class SseSignalContainer { |
||||
|
||||
private String userId; |
||||
|
||||
private SseEmitter sseEmitter; |
||||
|
||||
protected String tenantId; |
||||
|
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
package com.cloud.kicc.commonbiz.controller; |
||||
|
||||
import com.cloud.kicc.common.core.api.R; |
||||
import com.cloud.kicc.common.core.constant.AppConstants; |
||||
import com.cloud.kicc.commonbiz.service.IMapLogisticSseService; |
||||
import io.swagger.annotations.Api; |
||||
import lombok.RequiredArgsConstructor; |
||||
import org.springframework.http.MediaType; |
||||
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.mvc.method.annotation.SseEmitter; |
||||
|
||||
/** |
||||
*<p> |
||||
* 物流地图SSE长连接 控制层 |
||||
* 长轮询: https://developer.aliyun.com/article/933937#slide-5
|
||||
*</p> |
||||
* |
||||
* @Author: entfrm开发团队-王翔 |
||||
* @since: 2022/9/14 |
||||
*/ |
||||
@RestController |
||||
@RequestMapping(AppConstants.APP_COMMON + "/mapLogisticSse") |
||||
@RequiredArgsConstructor |
||||
@Api(tags = "地图物流SSE控制接口") |
||||
public class MapLogisticSseController { |
||||
|
||||
private final IMapLogisticSseService iMapLogisticSseService; |
||||
|
||||
@GetMapping(value = "/subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE}) |
||||
public SseEmitter subscribe() { |
||||
return iMapLogisticSseService.SseSubscribe(); |
||||
} |
||||
|
||||
@GetMapping("/sendMessage") |
||||
public R sendMessage(String userId, String json) { |
||||
iMapLogisticSseService.sendMessage(userId, json); |
||||
return R.ok(); |
||||
} |
||||
|
||||
@GetMapping("/sendTenantMessage") |
||||
public R sendTenantMessage(String json) { |
||||
iMapLogisticSseService.sendTenantMessage(json); |
||||
return R.ok(); |
||||
} |
||||
|
||||
@GetMapping("/remove/{userId:\\w+}") |
||||
public R remove(@PathVariable String userId) { |
||||
iMapLogisticSseService.remove(userId); |
||||
return R.ok(); |
||||
} |
||||
|
||||
@GetMapping("/disconnectTenant") |
||||
public R disconnectTenant() { |
||||
iMapLogisticSseService.disconnectTenant(); |
||||
return R.ok(); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
package com.cloud.kicc.commonbiz.service; |
||||
|
||||
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
||||
|
||||
/** |
||||
*<p> |
||||
* 物流地图SSE长连接 服务层 |
||||
*</p> |
||||
* |
||||
* @Author: entfrm开发团队-王翔 |
||||
* @since: 2022/9/14 |
||||
*/ |
||||
public interface IMapLogisticSseService { |
||||
|
||||
/** |
||||
* 采用长轮询订阅 |
||||
* @return SseEmitter |
||||
*/ |
||||
SseEmitter SseSubscribe(); |
||||
|
||||
/** |
||||
* sse发送消息 |
||||
* @param userId 用户Id |
||||
* @param json 发送数据包 |
||||
* @return void |
||||
*/ |
||||
void sendMessage(String userId, String json); |
||||
|
||||
/** |
||||
* sse发送当前租户下所有在线客户端消息 |
||||
* @param json 发送数据包 |
||||
* @return void |
||||
*/ |
||||
void sendTenantMessage(String json); |
||||
|
||||
/** |
||||
* 断开当前用户租户下所有客户端连接 |
||||
* @return void |
||||
*/ |
||||
void disconnectTenant(); |
||||
|
||||
/** |
||||
* 断开当前用户连接 |
||||
* @return void |
||||
*/ |
||||
void disconnect(); |
||||
|
||||
/** |
||||
* 移除指定用户客户端 |
||||
* @param userId 指定用户Id |
||||
* @return void |
||||
*/ |
||||
void remove(String userId); |
||||
|
||||
} |
@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
|
||||
package com.cloud.kicc.commonbiz.service.impl; |
||||
|
||||
import cn.hutool.core.collection.CollectionUtil; |
||||
import cn.hutool.core.collection.ConcurrentHashSet; |
||||
import cn.hutool.core.util.StrUtil; |
||||
import com.cloud.kicc.common.core.exception.CommonException; |
||||
import com.cloud.kicc.common.data.entity.KiccUser; |
||||
import com.cloud.kicc.common.security.util.SecurityUtils; |
||||
import com.cloud.kicc.commonbiz.api.entity.SseSignalContainer; |
||||
import com.cloud.kicc.commonbiz.service.IMapLogisticSseService; |
||||
import com.xxl.job.core.handler.annotation.XxlJob; |
||||
import lombok.RequiredArgsConstructor; |
||||
import lombok.SneakyThrows; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.stereotype.Service; |
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
||||
|
||||
import java.io.IOException; |
||||
import java.time.LocalDateTime; |
||||
import java.util.Optional; |
||||
import java.util.Set; |
||||
import java.util.stream.Collectors; |
||||
|
||||
/** |
||||
* <p> |
||||
* 地图核心主任务表 服务实现类 |
||||
* </p> |
||||
* |
||||
* @author entfrm开发团队-王翔 |
||||
* @since 2022-07-22 |
||||
*/ |
||||
@Slf4j |
||||
@Service |
||||
@RequiredArgsConstructor |
||||
public class MapLogisticSseServiceImpl implements IMapLogisticSseService { |
||||
|
||||
/** 存储 sse 上下文容器 */ |
||||
private static Set<SseSignalContainer> sseSignalContainers = new ConcurrentHashSet(); |
||||
|
||||
/** |
||||
* 间隔10秒发送心跳包,检查客户端是否关闭 |
||||
*/ |
||||
@XxlJob("doHeartbeat") |
||||
public void doHeartbeat() { |
||||
sseSignalContainers.forEach(item -> { |
||||
try { |
||||
item.getSseEmitter().send(SseEmitter.event().comment("保持心跳 " + LocalDateTime.now()).reconnectTime(1000)); |
||||
} catch (IOException e) { |
||||
log.debug("当前用户Id为:{}发送心跳包失败,正在删除当前的建立通道对象", item.getUserId()); |
||||
sseSignalContainers.removeIf(sseSignalContainer -> StrUtil.equals(item.getUserId(), sseSignalContainer.getUserId()) && StrUtil.equals(item.getTenantId(), sseSignalContainer.getTenantId())); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Override |
||||
@SneakyThrows |
||||
public SseEmitter SseSubscribe() { |
||||
KiccUser kiccUser = getUser(); |
||||
// 超时时间设置为20秒
|
||||
SseEmitter sseEmitter = new SseEmitter(20000L); |
||||
SseSignalContainer sseSignalContainer =new SseSignalContainer( |
||||
kiccUser.getId(), |
||||
sseEmitter, |
||||
kiccUser.getTenantId() |
||||
); |
||||
// 设置如果网络出错前端请求的重试时间为1s
|
||||
sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("创建通道连接成功")); |
||||
sseSignalContainers.add(sseSignalContainer); |
||||
log.info("当前建立的用户Id为:{}", kiccUser.getId()); |
||||
sseEmitter.onTimeout(() -> { |
||||
log.info("当前用户Id为:{}的SSE长轮询已经超时,正在删除当前的建立通道对象", kiccUser.getId()); |
||||
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); |
||||
}); |
||||
sseEmitter.onCompletion(() -> { |
||||
log.info("当前用户Id为:{}的SSE长轮询已经返回响应关闭,正在删除当前的建立通道对象", kiccUser.getId()); |
||||
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); |
||||
}); |
||||
sseEmitter.onError(e -> { |
||||
log.info("当前用户Id为:{}的SSE长轮询出现异常,正在删除当前的建立通道对象,错误信息{}", kiccUser.getId(), e.getLocalizedMessage()); |
||||
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); |
||||
}); |
||||
return sseEmitter; |
||||
} |
||||
|
||||
@Override |
||||
@SneakyThrows |
||||
public void sendMessage(String userId, String json) { |
||||
KiccUser kiccUser = getUser(); |
||||
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() |
||||
.filter(item -> StrUtil.equals(item.getUserId(), userId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); |
||||
if (CollectionUtil.isNotEmpty(sendSseSignalContainers)) { |
||||
SseSignalContainer sendSseSignalContainer = sendSseSignalContainers.stream().findFirst().get(); |
||||
sendSseSignalContainer.getSseEmitter().send(json); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void sendTenantMessage(String json) { |
||||
KiccUser kiccUser = getUser(); |
||||
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() |
||||
.filter(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); |
||||
sendSseSignalContainers.forEach(item -> { |
||||
try { |
||||
item.getSseEmitter().send(json); |
||||
} catch (IOException e) { |
||||
throw new RuntimeException(e); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
@Override |
||||
public void disconnect() { |
||||
KiccUser kiccUser = getUser(); |
||||
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() |
||||
.filter(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); |
||||
if (CollectionUtil.isNotEmpty(sendSseSignalContainers)) { |
||||
SseSignalContainer sendSseSignalContainer = sendSseSignalContainers.stream().findFirst().get(); |
||||
sendSseSignalContainer.getSseEmitter().complete(); |
||||
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void disconnectTenant() { |
||||
KiccUser kiccUser = SecurityUtils.getUser(); |
||||
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() |
||||
.filter(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); |
||||
sendSseSignalContainers.forEach(item -> item.getSseEmitter().complete()); |
||||
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); |
||||
} |
||||
|
||||
@Override |
||||
public void remove(String userId) { |
||||
KiccUser kiccUser = getUser(); |
||||
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() |
||||
.filter(item -> StrUtil.equals(item.getUserId(), userId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); |
||||
if (CollectionUtil.isNotEmpty(sendSseSignalContainers)) { |
||||
SseSignalContainer sendSseSignalContainer = sendSseSignalContainers.stream().findFirst().get(); |
||||
sendSseSignalContainer.getSseEmitter().complete(); |
||||
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), userId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); |
||||
} |
||||
} |
||||
|
||||
private KiccUser getUser() { |
||||
KiccUser kiccUser = SecurityUtils.getUser(); |
||||
Optional.ofNullable(kiccUser).orElseThrow(() -> new CommonException("当前用户登录,请先登录后重试!")); |
||||
return kiccUser; |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue