Browse Source

🚀 集成SSE

master
wangxiang 3 years ago
parent
commit
45d8cb483a
  1. 2
      kicc-platform/kicc-platform-api/kicc-common-api/src/main/java/com/cloud/kicc/commonbiz/api/entity/SseSignalContainer.java
  2. 32
      kicc-platform/kicc-platform-biz/kicc-common-biz/src/main/java/com/cloud/kicc/commonbiz/controller/MapLogisticSseController.java
  3. 13
      kicc-platform/kicc-platform-biz/kicc-common-biz/src/main/java/com/cloud/kicc/commonbiz/service/IMapLogisticSseService.java
  4. 93
      kicc-platform/kicc-platform-biz/kicc-common-biz/src/main/java/com/cloud/kicc/commonbiz/service/impl/MapLogisticSseServiceImpl.java

2
kicc-platform/kicc-platform-api/kicc-common-api/src/main/java/com/cloud/kicc/commonbiz/api/entity/SseSignalContainer.java

@ -20,6 +20,8 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
@AllArgsConstructor @AllArgsConstructor
public class SseSignalContainer { public class SseSignalContainer {
private String clientId;
private String userId; private String userId;
private SseEmitter sseEmitter; private SseEmitter sseEmitter;

32
kicc-platform/kicc-platform-biz/kicc-common-biz/src/main/java/com/cloud/kicc/commonbiz/controller/MapLogisticSseController.java

@ -1,11 +1,20 @@
package com.cloud.kicc.commonbiz.controller; package com.cloud.kicc.commonbiz.controller;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.StrUtil;
import com.cloud.kicc.common.core.api.R; import com.cloud.kicc.common.core.api.R;
import com.cloud.kicc.common.core.constant.AppConstants; import com.cloud.kicc.common.core.constant.AppConstants;
import com.cloud.kicc.common.security.annotation.Inner;
import com.cloud.kicc.commonbiz.service.IMapLogisticSseService; import com.cloud.kicc.commonbiz.service.IMapLogisticSseService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -29,9 +38,20 @@ public class MapLogisticSseController {
private final IMapLogisticSseService iMapLogisticSseService; private final IMapLogisticSseService iMapLogisticSseService;
@GetMapping(value = "/subscribe", produces = {MediaType.TEXT_EVENT_STREAM_VALUE}) private final TokenStore tokenStore;
public SseEmitter subscribe() {
return iMapLogisticSseService.SseSubscribe(); private final OAuth2ClientContext oAuth2ClientContext;
@Inner(false)
@GetMapping(value = "/subscribe")
public SseEmitter subscribe(String accessToken, String clientId) {
OAuth2Authentication oAuth2Authentication = tokenStore.readAuthentication(accessToken);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(oAuth2Authentication);
DefaultOAuth2AccessToken defaultOAuth2AccessToken = new DefaultOAuth2AccessToken(accessToken);
oAuth2ClientContext.setAccessToken(defaultOAuth2AccessToken);
SecurityContextHolder.setContext(context);
return iMapLogisticSseService.SseSubscribe(clientId);
} }
@GetMapping("/sendMessage") @GetMapping("/sendMessage")
@ -46,9 +66,9 @@ public class MapLogisticSseController {
return R.ok(); return R.ok();
} }
@GetMapping("/remove/{userId:\\w+}") @GetMapping("/disconnect")
public R remove(@PathVariable String userId) { public R disconnect(String clientId) {
iMapLogisticSseService.remove(userId); iMapLogisticSseService.disconnect(clientId);
return R.ok(); return R.ok();
} }

13
kicc-platform/kicc-platform-biz/kicc-common-biz/src/main/java/com/cloud/kicc/commonbiz/service/IMapLogisticSseService.java

@ -15,9 +15,10 @@ public interface IMapLogisticSseService {
/** /**
* 采用长轮询订阅 * 采用长轮询订阅
* @param clientId 客户端唯一Id
* @return SseEmitter * @return SseEmitter
*/ */
SseEmitter SseSubscribe(); SseEmitter SseSubscribe(String clientId);
/** /**
* sse发送消息 * sse发送消息
@ -42,15 +43,9 @@ public interface IMapLogisticSseService {
/** /**
* 断开当前用户连接 * 断开当前用户连接
* @param clientId 客户端唯一Id
* @return void * @return void
*/ */
void disconnect(); void disconnect(String clientId);
/**
* 移除指定用户客户端
* @param userId 指定用户Id
* @return void
*/
void remove(String userId);
} }

93
kicc-platform/kicc-platform-biz/kicc-common-biz/src/main/java/com/cloud/kicc/commonbiz/service/impl/MapLogisticSseServiceImpl.java

@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ConcurrentHashSet; import cn.hutool.core.collection.ConcurrentHashSet;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.cloud.kicc.common.core.exception.CommonException; import com.cloud.kicc.common.core.exception.CommonException;
import com.cloud.kicc.common.core.util.BaseUtil;
import com.cloud.kicc.common.data.entity.KiccUser; import com.cloud.kicc.common.data.entity.KiccUser;
import com.cloud.kicc.common.security.util.SecurityUtils; import com.cloud.kicc.common.security.util.SecurityUtils;
import com.cloud.kicc.commonbiz.api.entity.SseSignalContainer; import com.cloud.kicc.commonbiz.api.entity.SseSignalContainer;
@ -17,6 +18,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException; import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Iterator;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -42,23 +44,20 @@ public class MapLogisticSseServiceImpl implements IMapLogisticSseService {
*/ */
@XxlJob("doHeartbeat") @XxlJob("doHeartbeat")
public void doHeartbeat() { public void doHeartbeat() {
sseSignalContainers.forEach(item -> { doMaintenance();
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 @Override
@SneakyThrows @SneakyThrows
public SseEmitter SseSubscribe() { public SseEmitter SseSubscribe(String clientId) {
KiccUser kiccUser = getUser(); KiccUser kiccUser = getUser();
// 超时时间设置为20秒 // 设置超时时间为1小时
SseEmitter sseEmitter = new SseEmitter(20000L); SseEmitter sseEmitter = new SseEmitter(3600_000L);
if(sseSignalContainers.stream()
.filter(item -> StrUtil.equals(item.getClientId(),clientId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()))
.collect(Collectors.toSet()).isEmpty()) {
SseSignalContainer sseSignalContainer =new SseSignalContainer( SseSignalContainer sseSignalContainer =new SseSignalContainer(
clientId,
kiccUser.getId(), kiccUser.getId(),
sseEmitter, sseEmitter,
kiccUser.getTenantId() kiccUser.getTenantId()
@ -66,19 +65,20 @@ public class MapLogisticSseServiceImpl implements IMapLogisticSseService {
// 设置如果网络出错前端请求的重试时间为1s // 设置如果网络出错前端请求的重试时间为1s
sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("创建通道连接成功")); sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("创建通道连接成功"));
sseSignalContainers.add(sseSignalContainer); sseSignalContainers.add(sseSignalContainer);
log.info("当前建立的用户Id为:{}", kiccUser.getId()); log.info("clientId:{},建立的用户Id为:{}", clientId, kiccUser.getId());
sseEmitter.onTimeout(() -> { sseEmitter.onTimeout(() -> {
log.info("当前用户Id为:{}的SSE长轮询已经超时,正在删除当前的建立通道对象", kiccUser.getId()); log.info("clientId:{},用户Id为:{},的SSE长轮询已经超时,正在删除当前的建立通道对象", clientId, kiccUser.getId());
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); sseSignalContainers.removeIf(item -> StrUtil.equals(item.getClientId(), clientId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()));
}); });
sseEmitter.onCompletion(() -> { sseEmitter.onCompletion(() -> {
log.info("当前用户Id为:{}的SSE长轮询已经返回响应关闭,正在删除当前的建立通道对象", kiccUser.getId()); log.info("clientId:{},用户Id为:{}的SSE长轮询已经返回响应关闭,正在删除当前的建立通道对象", clientId, kiccUser.getId());
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); sseSignalContainers.removeIf(item -> StrUtil.equals(item.getClientId(), clientId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()));
}); });
sseEmitter.onError(e -> { sseEmitter.onError(e -> {
log.info("当前用户Id为:{}的SSE长轮询出现异常,正在删除当前的建立通道对象,错误信息{}", kiccUser.getId(), e.getLocalizedMessage()); log.info("clientId:{},当前用户Id为:{}的SSE长轮询出现异常,正在删除当前的建立通道对象,错误信息{}", clientId, kiccUser.getId(), e.getLocalizedMessage());
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); sseSignalContainers.removeIf(item -> StrUtil.equals(item.getClientId(), clientId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()));
}); });
} else new CommonException("客户端id重复,请重新设置确保唯一");
return sseEmitter; return sseEmitter;
} }
@ -87,57 +87,58 @@ public class MapLogisticSseServiceImpl implements IMapLogisticSseService {
public void sendMessage(String userId, String json) { public void sendMessage(String userId, String json) {
KiccUser kiccUser = getUser(); KiccUser kiccUser = getUser();
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream()
.filter(item -> StrUtil.equals(item.getUserId(), userId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); .filter(item -> StrUtil.equals(item.getUserId(), userId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()))
if (CollectionUtil.isNotEmpty(sendSseSignalContainers)) { .collect(Collectors.toSet());
SseSignalContainer sendSseSignalContainer = sendSseSignalContainers.stream().findFirst().get(); Iterator<SseSignalContainer> it = sendSseSignalContainers.iterator();
sendSseSignalContainer.getSseEmitter().send(json); while (it.hasNext()) {
SseSignalContainer item = it.next();
item.getSseEmitter().send(json);
} }
} }
@Override @Override
@SneakyThrows
public void sendTenantMessage(String json) { public void sendTenantMessage(String json) {
KiccUser kiccUser = getUser(); KiccUser kiccUser = getUser();
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream()
.filter(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); .filter(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()))
sendSseSignalContainers.forEach(item -> { .collect(Collectors.toSet());
try { Iterator<SseSignalContainer> it = sendSseSignalContainers.iterator();
while (it.hasNext()) {
SseSignalContainer item = it.next();
item.getSseEmitter().send(json); item.getSseEmitter().send(json);
} catch (IOException e) {
throw new RuntimeException(e);
} }
});
} }
@Override @Override
public void disconnect() { public void disconnect(String clientId) {
KiccUser kiccUser = getUser(); KiccUser kiccUser = getUser();
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream()
.filter(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); .filter(item -> StrUtil.equals(item.getClientId(), clientId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()))
if (CollectionUtil.isNotEmpty(sendSseSignalContainers)) { .collect(Collectors.toSet());
SseSignalContainer sendSseSignalContainer = sendSseSignalContainers.stream().findFirst().get(); sendSseSignalContainers.forEach(item -> item.getSseEmitter().complete());
sendSseSignalContainer.getSseEmitter().complete(); sseSignalContainers.removeIf(sseSignalContainer -> StrUtil.equals(sseSignalContainer.getClientId(), clientId) && StrUtil.equals(sseSignalContainer.getTenantId(), kiccUser.getTenantId()));
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), kiccUser.getId()) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()));
}
} }
@Override @Override
public void disconnectTenant() { public void disconnectTenant() {
KiccUser kiccUser = SecurityUtils.getUser(); KiccUser kiccUser = getUser();
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream()
.filter(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); .filter(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()))
.collect(Collectors.toSet());
sendSseSignalContainers.forEach(item -> item.getSseEmitter().complete()); sendSseSignalContainers.forEach(item -> item.getSseEmitter().complete());
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); sseSignalContainers.removeIf(item -> StrUtil.equals(item.getTenantId(), kiccUser.getTenantId()));
} }
@Override /**
public void remove(String userId) { * 执行心跳维护,避免 sse 膨胀容量问题
KiccUser kiccUser = getUser(); */
Set<SseSignalContainer> sendSseSignalContainers = sseSignalContainers.stream() @SneakyThrows
.filter(item -> StrUtil.equals(item.getUserId(), userId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())).collect(Collectors.toSet()); private void doMaintenance() {
if (CollectionUtil.isNotEmpty(sendSseSignalContainers)) { Iterator<SseSignalContainer> it = sseSignalContainers.iterator();
SseSignalContainer sendSseSignalContainer = sendSseSignalContainers.stream().findFirst().get(); while (it.hasNext()) {
sendSseSignalContainer.getSseEmitter().complete(); SseSignalContainer item = it.next();
sseSignalContainers.removeIf(item -> StrUtil.equals(item.getUserId(), userId) && StrUtil.equals(item.getTenantId(), kiccUser.getTenantId())); item.getSseEmitter().send(SseEmitter.event().comment("保持心跳" + LocalDateTime.now()).reconnectTime(1000));
} }
} }

Loading…
Cancel
Save