websocket sse http轮询

15 篇文章 0 订阅

适用场景:
http轮询:单向http请求,且http连接没有复用。客户端主动发起的轮询
sse:半双工,后端持续向客户端推送数据。典型的如评测结果的推送,因为需要评测的中间过程,且要能展示最终结果,所以后端主动多次推送和结束
websocket:全双工,客户端和后端可以持续相互推送数据。如运行代码,输入时需要客户端将内容推送给后端,执行结果需要推送给客户端。

三者区分:https://blog.gdccwxx.com/http/what-is-sse/
https://www.bookstack.cn/read/webapi-tutorial/spilt.1.docs-websocket.md
sse使用实例:
https://www.pomit.cn/p/3795471065500161
https://blog.csdn.net/qq_39321886/article/details/103746883


websocket的使用实例:
https://blog.csdn.net/AbstractLiu/article/details/120032317

websocket的浏览器模拟器:可以安装插件,便于研发人员测试websocket
插件名称:smart websocket client
链接: https://pan.baidu.com/s/1pS9-rFrkfhXUaIz4te0rgQ  密码: 4goc

下面是写的比较好的三者的区分:

url: https://blog.gdccwxx.com/http/what-is-sse/

🎓 背景

浏览器和服务端交互过程中,会有服务端向浏览器通信的场景。例如:服务端异步处理信息,处理成功后向浏览器推送。

但并不是所有的后台服务都建立了 websocket 通道,因此常用做法是浏览器定时查询,轮询后台数据。

从请求的角度来看,轮询多余了浏览器向后台服务发起握手发送数据包的过程,因此并不简洁、优雅。

那有没有既不需要 websocket 通道,又不用轮询这么 “low” 的方法呢?本文介绍的 SSE (server-site events) 就足够简洁和优雅。

🤔️ SSE 是啥

SSE 全称是 Server-sent events(服务器发送事件),是服务器向客户端推送数据的一种方式。

SSE 的本质是通过 HTTP 请求,不断发送 流信息(streaming),使得服务器向客户端推送信息。类似于视频流。

他不是一次性的数据包,而是会一直等着服务端的推送。因此客户端不会关闭连接,等着服务端的不断推送。这样就实现了服务端向客户端的推送。

🆚 SSE VS Websocket

Websocket 是双向通信(全双工),浏览器 <-> 服务端相互通信,更强大也更灵活。

SSE 是单向通信(半双工),浏览器 <- 服务端,本质是下载信息。

对比优点缺点
Websocket1. 全双工,功能更强大1.较为复杂,服务端需要重新支持
2.断线重连需要额外部署
SSE1.协议轻量,支持 HTTP 的服务端就支持
2.方便默认支持断线重连
3.支持自定义数据类型
1.半双工,不够灵活

两者各有特点,适合不同场所

💡 SSE 的使用和可能的坑

目前系统中对sse的使用梳理:
第一种方式:直接使用sse
具体代码实例:
    @GetMapping("/progress/{exerciseId}/sse")
    @ApiOperation(value = "评测进度[SSE]")
    public SseEmitter getProgressBySse(
            @ApiParam("Bearer Token") @RequestHeader(value = "Authorization", required = false)
                    String token,
            @ApiParam("C++习题ID") @PathVariable
                    String exerciseId) {

        SseEmitter emitter = new SseEmitter();

        taskExecutor.execute(() -> {
            try {
                var isNotFinished = true;
                int mid = 0;
                do {
                    var progress = judgeBiz.askCppProgress(exerciseId);
                    isNotFinished = !progress.isFinished();

                    emitter.send(SseEmitter.event()
                            .id(++mid + "")
                            .reconnectTime(2000)
                            .data(progress));

                } while (isNotFinished && delay());

                sendCloseEvent(emitter, ++mid);

            } catch (IOException e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }


    private boolean delay() {
        try {
            Thread.sleep(800);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return true;
    }

    private void sendCloseEvent(SseEmitter emitter, int messageId) throws IOException {
        emitter.send(SseEmitter.event()
                .id(messageId + "")
                .name("close")
                .data("done"));
        emitter.complete();
    }

关于上面使用到的taskExecutor,是启动是配置的线程池资源,如下:
@Slf4j
@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    @Bean("taskExecutor")
    public AsyncTaskExecutor getAsyncExecutor() {
        log.debug("Creating Async Task Executor");
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int core = Runtime.getRuntime().availableProcessors();
        executor.setCorePoolSize(core);
        executor.setMaxPoolSize(core * 2 + 1);
        executor.setQueueCapacity(100);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(60);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("monkey-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new SimpleAsyncUncaughtExceptionHandler();
    }

    @Bean
    public WebMvcConfigurer webMvcConfigurerConfigurer(@Qualifier("taskExecutor") AsyncTaskExecutor taskExecutor,
                                                       CallableProcessingInterceptor interceptor) {
       ......
    }

    @Bean
    public CallableProcessingInterceptor callableProcessingInterceptor() {
            ......
    }
}

但是如果评测出现问题(即isNotFinished一直为false),会进一步出现sse一直不关闭的问题,主要有两个优化方向:
1)所有业务中评测结果查询时增加最大的刷新次数限制,0.8s刷新一次,允许最大调用20次 sse连接最长持续时间为16s,避免非正常场景下sse一直不断联,浪费cpu和线程资源
2)竞赛和竞赛题库业务中执行sse线程池的核心线程数从core*1调整为core*4,最大线程数:core*8,充分利用cpu资源,提升响应速度和用户体验。


第二种方式,使用了reactor,具体代码如下:
    @ApiOperation(value = "查询教师提示[SSE][学生]", notes = "学生端")
    @PreAuthorize("hasRole('STUDENT')")
    @GetMapping("/sse/{courseId}/{lessonId}/{bundleId}/{practiceId}")
    public Flux<ServerSentEvent<String>> getTips(
            @ApiParam("Bearer Token") @RequestParam String token,
            @ApiIgnore @AuthenticationPrincipal final UserDto userDto,
            @ApiParam(value = "课程ID", required = true) @PathVariable String courseId,
            @ApiParam(value = "讲次ID", required = true) @PathVariable String lessonId,
            @ApiParam(value = "题组ID", required = true) @PathVariable String bundleId,
            @ApiParam(value = "练习题ID", required = true) @PathVariable String practiceId) {
        //sse接口请求量比较大,链路上的日志打印需要精简
        log.info("学生端查询教师提示[SSE] 用户-studentId: {}", userDto.getStudentId());
        return Flux.interval(Duration.ofSeconds(Constants.LONG_POLLING_TIME_UNIT))
                .map(seq -> Tuples.of(seq, JSON.toJSONString(teacherTipBiz.getTips(courseId, lessonId, bundleId, practiceId, userDto))))
                .map(data -> ServerSentEvent.<String>builder()
                        .id(Long.toString(data.getT1()))
                        .data(data.getT2())
                        .build());
    }
具体场景包括:
1)学生端查询教师提示[SSE] /sse/{courseId}/{lessonId}/{bundleId}/{practiceId}
2)学生查询题目的收发题装填 /sse/bundles/{bundleId}/published
3)学生端查询习题列表 /lesson/s-practices/sse

说明:此时使用默认线程池是Schedulers.parallel(),Schedulers.parallel()线程池是Reactor提供的默认线程池,它会在需要时自动创建和管理线程。在使用Flux或Mono时,如果没有指定要使用的线程池,则会默认使用Schedulers.parallel()线程池。默认情况下,Schedulers.parallel()线程池的线程数等于可用的CPU核心数。

也可以不使用默认的线程池,具体示例代码如下:
要声明Schedulers.elastic()线程池,您可以使用Schedulers类的静态方法elastic()。以下是一个示例:

```
import reactor.core.scheduler.Schedulers;

// 创建一个Flux并在Schedulers.elastic()线程池中执行
Flux.range(1, 10)
    .publishOn(Schedulers.elastic())
    .map(i -> i * 2)
    .subscribe(System.out::println);
```

在这个例子中,我们使用publishOn()方法将Flux的执行切换到Schedulers.elastic()线程池中。Schedulers.elastic()线程池会根据需要自动创建和回收线程,以适应不同的负载情况。

请注意,Schedulers.elastic()线程池的线程数是动态调整的,因此您不需要手动指定线程数。如果您需要更精细的控制,可以使用Schedulers.newElastic()方法创建一个自定义的elastic线程池,并指定最小和最大线程数等参数。例如:

```
import reactor.core.scheduler.Schedulers;

// 创建一个自定义的elastic线程池
Scheduler customElasticScheduler = Schedulers.newElastic("custom-elastic", 10, 50);

// 创建一个Flux并在自定义的elastic线程池中执行
Flux.range(1, 10)
    .publishOn(customElasticScheduler)
    .map(i -> i * 2)
    .subscribe(System.out::println);
```

在这个例子中,我们使用Schedulers.newElastic()方法创建了一个名为"custom-elastic"的自定义elastic线程池,并指定了最小线程数为10,最大线程数为50。然后,我们使用publishOn()方法将Flux的执行切换到自定义的elastic线程池中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值