Ssemitter服务端主动推送消息

背景

  原本系统中有一个功能,输入服务代码的git地址以及分支,可以扫描代码中使用的所有组件。使用的是process类,执行maven命令。
  但是可能出现错误:1. 没有远端仓库权限,2. 或者项目本身不是maven项目,3. 或者命令执行错误等一系列问题。
  由于扫描任务为异步进行,扫描过程中无法判断任务进行的状况。所以想要在扫描过程中展示后台日志,查看扫描状态。

工具调查

  1. websocket 具体使用查看上一篇文章:https://blog.csdn.net/wanghl_/article/details/135696429
  2. Ssemitter

遇到问题

websocket已经实现基本功能,但为什么要换Ssemitter呢?
   从资源消耗的角度来看,WebSocket和SSE在某些方面存在一些差异。

  1. 建立连接开销:WebSocket在建立连接时需要进行握手,这涉及到发送额外的HTTP请求和响应,这可能会在一开始时产生一些额外的开销。而对于SSE,只需要进行一次普通的HTTP请求,因此在建立连接时开销相对较小。

  2. 保持连接开销:WebSocket是一种持久连接,可以长时间保持连接状态。这意味着WebSocket服务器需要维护与每个客户端的连接,这可能会占用服务器的内存和其他资源。而SSE是基于单向的HTTP请求响应模型,每次响应完成后就会关闭连接,这样服务器上不需要维持与客户端的持久连接,因此相对来说资源消耗较低。

  3. 数据传输开销:WebSocket和SSE都可以实现实时双向通信,但WebSocket倾向于在双向传输方面更高效。WebSocket使用了自定义的二进制数据帧协议,可以实现低延迟和高效率的双向数据传输。相比之下,SSE使用了纯文本的传输格式,不支持双向传输,且数据只能从服务器传输到客户端。

   最重要的原因是:根据查阅资料,WebSocket无法携带自定义请求头。我的接口有经过拦截,其中必须携带一些除token以外的自定义请求头。这使我不得不更换为Ssemitter。

Ssemitter详细学习,可参看https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html,作者写的很清晰

实现代码

Controller

@RestController
@RequestMapping("/sse")
public class LogSseController {

    @Resource
    private SseService sseService;
    @ApiOperation(value = " 建立连接")
    @GetMapping(value = "/sseConnect/{clientId}", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
    public SseEmitterUTF8 sseSonnect(@PathVariable("clientId") @ApiParam("客户端 id") String clientId) {
        SseEmitterUTF8 sseEmitter = sseService.getConn(clientId);
        CompletableFuture.runAsync(() -> {
            try {
                sseService.send(clientId);
            } catch (Exception e) {
                throw new BusinessException("推送数据异常");
            }
        });
        return sseEmitter;
    }

//    @ApiOperation("发送消息")
//    @GetMapping("/pushLog/{clientId}")
//    public Result<String> pushLog(@PathVariable("clientId") @ApiParam("客户端 id") String clientId) {
//        sseService.send(clientId);
//        return Result.success("推送数据成功");
//    }

    @GetMapping("closeConn/{clientId}")
    @ApiOperation(value = " 关闭连接")
    public Result<String> closeConn(@PathVariable("clientId") @ApiParam("客户端 id") String clientId) {
        sseService.closeConn(clientId);
        return Result.success("连接已关闭");
    }
}

sevice

@Slf4j
@Service
public class SseService {

    private static final Map<String, SseEmitterUTF8> SSE_CACHE = new ConcurrentHashMap<>();

    public SseEmitterUTF8 getConn(@NotBlank String clientId) {
        SseEmitterUTF8 sseEmitter = SSE_CACHE.get(clientId);

        if (sseEmitter != null) {
            return sseEmitter;
        } else {
            // 设置连接超时时间,需要配合配置项 spring.mvc.async.request-timeout: 600000 一起使用
            SseEmitterUTF8 emitter = new SseEmitterUTF8(600_000L);
            // 注册超时回调,超时后触发
            emitter.onTimeout(() -> {
                log.info("连接已超时,正准备关闭,clientId = {}", clientId);
                SSE_CACHE.remove(clientId);
            });
            // 注册完成回调,调用 emitter.complete() 触发
            emitter.onCompletion(() -> {
                log.info("连接已关闭,正准备释放,clientId = {}", clientId);
                SSE_CACHE.remove(clientId);
                log.info("连接已释放,clientId = {}", clientId);
            });
            // 注册异常回调,调用 emitter.completeWithError() 触发
            emitter.onError(throwable -> {
                log.error("连接已异常,正准备关闭,clientId = {}", clientId, throwable);
                SSE_CACHE.remove(clientId);
            });

            SSE_CACHE.put(clientId, emitter);

            return emitter;
        }
    }

    /**
     * 模拟类似于 chatGPT 的流式推送回答
     *
     * @param clientId 客户端 id
     * @throws IOException 异常
     */
    public void send(@NotBlank String clientId){

        //日志文件路径,获取最新的
        String filePath = "/home/logs/" + new SimpleDateFormat("yyyy-MM-dd").format(new Date()) + ".log";

        SseEmitterUTF8 emitter = SSE_CACHE.get(clientId);
        try {
            //指定文件指针到文件末尾(每次都从当前打开位置开始读取log)
            RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r");
            randomAccessFile.seek(randomAccessFile.length()-1);

            boolean keepWatching = true;
            String line;
            while(keepWatching){
                line = randomAccessFile.readLine();
                if(null != line){
                    //解决中文乱码问题
                    line = new String(line.getBytes(StandardCharsets.ISO_8859_1),StandardCharsets.UTF_8);
                    emitter.send(line + "<br/>", org.springframework.http.MediaType.TEXT_EVENT_STREAM);//APPLICATION_JSON
                    Thread.sleep(1000);
                }
            }
        }catch (Exception e) {
            //捕获但不处理
            e.printStackTrace();
        }
        // 结束推流
        emitter.complete();
        SSE_CACHE.remove(clientId);
    }

    //关闭连接
    public void closeConn(@NotBlank String clientId) {
        SseEmitterUTF8 sseEmitter = SSE_CACHE.get(clientId);
        if (sseEmitter != null) {
            sseEmitter.complete();
            SSE_CACHE.remove(clientId);
        }
    }
}

自定义返回类,重写SseEmitter

public class SseEmitterUTF8 extends SseEmitter {

    public SseEmitterUTF8(Long timeout) {
        super(timeout);
    }

    @Override
    protected void extendResponse(ServerHttpResponse outputMessage) {
        super.extendResponse(outputMessage);

        HttpHeaders headers = outputMessage.getHeaders();
        headers.setContentType( new MediaType("text", "event-stream", Charset.forName("UTF-8")));
    }
}
自定义html测试


<html>
<script src="https://code.jquery.com/jquery-3.0.0.min.js"></script>
<script th:inline="javascript">
    const eventSource = new EventSource('http://localhost:1919/sse/sseConnect/122');
    eventSource.onmessage = function(event) {
        if (event.data) {
            //日志内容
            let $loggingText = $("#loggingText");
            $loggingText.append(event.data);
            //是否开启自动底部
            if (window.loggingAutoBottom) {
                //滚动条自动到最底部
                $loggingText.scrollTop($loggingText[0].scrollHeight);
            }
        }
      // 处理接收到的消息
      console.log(event.data);
    };
</script>
  <body>
      <div id = "loggingText">
          
      </div>
  </body>
</html>
  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值