在原有springmvc上使用webflux实现websocket长连接推送

目录

1.背景:

2.客户端:

2.1 连接到服务端:

2.2 断线重连:

2.3 接收消息:

3 服务端:


1.背景:

对老系统进行改造,希望使用websocket长连接异步推送日志(跨云),特此记录,如果有问题可以在评论区提出

2.客户端:

依赖的包:

<!-- webflux -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>

2.1 连接到服务端:

public void connectWebsocketServer() {
        String clientId = UUID.fastUUID().toString();
        String url = config.getUrl() + clientId;
        URI uri = URI.create(url);
        log.info("connect websocket server ... {}", url);
        WebSocketClient client = new StandardWebSocketClient();
        Integer nextConnectTime = config.getNextConnectTime();
        try {
            client.execute(uri, getHttpHeaders(), session -> {
                        this.handler.setSession(session);
                        Mono<Void> input = session.receive().doOnNext(this.handler::onMessage)
                                .doOnError(throwable -> log.error("receive message happen error:" + throwable))
                                .doOnComplete(() -> log.info("session will be closed, client is end of receive message")).then();
                        Mono<Void> output = session.send(Flux.create(this.handler::setSink))
                                .timeout(Duration.of(config.getMaxSessionIdleTimeout(), ChronoUnit.SECONDS))
//重试
                                .retry(config.getRetryCount())
                                .doOnSuccess(v -> log.info("session will be closed, client is end of send message"));
                        return Mono.zip(input, output).then().doFinally(signalType -> reconnect(nextConnectTime, false));
                    }).onTerminateDetach().doOnError(throwable -> {
                        log.error("happen error, :" + throwable);
                        reconnect(nextConnectTime, true);
                    })
                    .subscribe(aVoid -> {
                    });
        } catch (Throwable th) {
            log.error("websocket client error: ", th);
            try {
                handler.getSession().close();
                this.handler.setSessionIsClose(true);
            } catch (Exception ignore) {

            }
            reconnect(nextConnectTime, true);
        }
    }

踩坑:上述代码中使用了StandardWebSocketClient作为websocket客户端,是为了兼容tomcat,

session对应的实现类其实就是StandardWebSocketSession,但是当前的webflux (springboot 2.3.2)很坑,不能判断websocket的session是否关闭,并且也获取不到原生的session对象,源码如下:

 这里是范型T 并不能看出是什么实际是什么对象,于是我继续往下翻源码,翻到源码这里:

 可以看出 这里实际上是tomcat中的Session接口,继续看Session接口源码发现有isOpen这个属性 :

到此可以使用反射提升权限获取该属性:

 /**
     * 通过反射获取tomcat的session
     *
     * @param standardWebSocketSession
     * @return
     */
    private Session getSession(StandardWebSocketSession standardWebSocketSession) {
//省略了反射的代码 其实就是获取父类属性,并且提升权限
        return ReflectorUtils.getSuperClazzFiled(StandardWebSocketSession.class, "delegate", standardWebSocketSession);
    }

2.2 断线重连:

 /**
     * 断线重连
     */
    private void reconnect(Integer next, Boolean isErrorReconnect) {
        //如果是程序关闭 不需要重连
        if (!IS_OPEN) {
            return;
        }
        //如果发生错误,可能是服务端重启,稍后进行断线重连
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(next));
        // 断线重连
        connectWebsocketServer();
    }

2.3 接收消息:

发送:

/**
     * 发送
     *
     * @param infos
     */
    public void send(List<MetaLogInfo> infos) {
        if (sink == null || session == null) {
            return;
        }
        LogTransportData data = new LogTransportData();
        data.setInfos(infos);
        Long messageId = ID.nextId();
        data.setMessageId(messageId);
        data.setCurrentTime(System.currentTimeMillis());
        byte[] dataForByte = ProtostuffSerializer.serialize(data);
        log.debug("send message:" + messageId);
        sink.next(session.binaryMessage((dataBufferFactory) -> dataBufferFactory.wrap(dataForByte)));
        //sink.next(session.textMessage(data.toJSONString()));
        LogData ld = new LogData();
        BeanCopier.copyProperties(data, ld);
        sendBeforePutLocalCache(dataForByte, ld);
    }

这边使用了Protostuff方式序列化为二进制发送打包的消息,减少传输报文的体积。使用了本地缓存暂存消息,待得到服务端ack之后清除缓存。同时也使用定时线程延时定期清除漏了ack(可能因为服务端主动断线或者服务端重启)的消息缓存。

接收:

 /**
     * 接收消息
     */
    public void onMessage(WebSocketMessage webSocketMessage) {
        switch (webSocketMessage.getType()) {
            case PING:
                break;
            case TEXT:
                try {
                    //当前接口只接收ack消息
                    String message = webSocketMessage.getPayloadAsText();
                    if (!StringUtils.hasText(message)) {
                        return;
                    }
                    long messageId = Long.parseLong(message);
                    log.debug("receive messages id:" + messageId);
                    Map<LogData, byte[]> cacheMap = getLocalLogDataCache().asMap();
                    cacheMap.forEach((k, v) -> {
                        if (messageId == k.getMessageId()) {
                            cacheMap.remove(k);
                        }
                    });
                } catch (Throwable th) {
                    log.error("receive message happened error: ", th);
                }
                break;
            case BINARY:
                //webSocketMessage.getPayload().read();
            default:
        }
    }

获取服务端日志保存成功返回的ack,并清除消息缓存

3 服务端:

如果原先服务是springmvc的话,首先需要改变上下文类型,否则webflux不生效,我亲测是可以兼容原来的代码的:

 SpringApplication application = new SpringApplication(ChLogApplication.class);
 application.setWebApplicationType(WebApplicationType.REACTIVE);
 application.run(args);

服务端代码发送和接收和客户端是如出一辙的,这里省略,剩下需要增加处理器到对应路径:

@Bean
    public HandlerMapping webSocketMapping(LogWebsocketServerHandler handler) {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/log/sync", handler);
        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
        mapping.setUrlMap(map);
        return mapping;
    }

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }

同时需要一个定时线程清理过期session,获取session是否关闭的方式与客户端一致:

private class DelayClearSessionTask implements Runnable {
        //处理非正常关闭的session
        @Override
        public void run() {
            try {
                for (String sessionId : SESSION_MAP.keySet()) {
                    WebSocketSession session = SESSION_MAP.get(sessionId).getSession();
                    if (session instanceof StandardWebSocketSession) {
                        StandardWebSocketSession standardWebSocketSession = (StandardWebSocketSession) session;
                        Session delegate = getSession(standardWebSocketSession);
                        if (!delegate.isOpen()) {
                            log.warn("user id: {} session: {} is closed,must clear ", sessionId, session.getId());
                            SESSION_MAP.remove(sessionId);
                            standardWebSocketSession.close();
                        }
                    }
                }

               
            } catch (Throwable th) {
                log.error("task will be shutdown ...", th);
            }

        }
    }

以上就是使用webflux实现websocket长连接日志推送的全部内容了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring MVC可以很方便地集成WebSocket,以下是实现WebSocket的步骤: 1.添加依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>${spring.version}</version> </dependency> ``` 2.配置WebSocketSpring MVC的配置文件中添加以下配置: ``` @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new WebSocketHandler(), "/websocket").setAllowedOrigins("*"); } } ``` 其中,WebSocketHandler是自定义的一个处理器类,"/websocket"是WebSocket的访问路径,setAllowedOrigins("*")表示允许跨域访问。 3.编写WebSocket处理器 编写一个WebSocket处理器类,实现WebSocketHandler接口,并重写以下方法: - afterConnectionEstablished:当WebSocket连接建立成功时调用该方法。 - handleMessage:当接收到客户端发送的WebSocket消息时调用该方法。 - handleTransportError:当WebSocket通信发生错误时调用该方法。 - afterConnectionClosed:当WebSocket连接关闭时调用该方法。 例如: ``` public class WebSocketHandler implements WebSocketHandler { @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { System.out.println("WebSocket连接建立成功"); } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { System.out.println("接收到消息:" + message.getPayload()); session.sendMessage(new TextMessage("收到消息:" + message.getPayload())); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { System.out.println("WebSocket通信发生错误"); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { System.out.println("WebSocket连接已关闭:" + closeStatus); } @Override public boolean supportsPartialMessages() { return false; } } ``` 4.使用WebSocket 在前端页面中,使用JavaScript创建WebSocket对象,并指定WebSocket的访问路径: ``` var socket = new WebSocket("ws://localhost:8080/websocket"); ``` 然后,可以通过WebSocket对象发送和接收消息: ``` // 发送消息 socket.send("Hello, WebSocket!"); // 接收消息 socket.onmessage = function(event) { console.log("接收到消息:" + event.data); }; ``` 这样,就可以利用Spring MVC实现WebSocket了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值