从零开始复现小智AI 05

从零开始复现小智AI 05

在上一次复现中我成功实现session鉴权、请求过滤,静态资源映射。本次复现将完成webscoket链接建立,以及部分功能初始化。在与设备通信时采用WebSocket的原因是WebSocket支持全双工的长连接,并且不会自动释放链接,请求头短,是稳定链接等有的,尤其不会长链接是与Http区分的主要之处。本次代码地址荣先海/xiaozhi-esp32-ddd - Gitee.com

代码编写

想要实现WebSocket链接需要在app层书写相关配置,并且在trigger层实现自定义的WebSocket处理器

WebSocketConfig编写

@Configuration
@EnableWebSocket
@Slf4j
public class WebSocketConfig implements WebSocketConfigurer {

    @Resource
    private MyWebSocketHandler webSocketHandler;

    // 定义为public static以便其他类可以访问
    public static final String WS_PATH = "/ws/xiaozhi/v1/";

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSocketHandler, WS_PATH)
                .setAllowedOrigins("*");

    }
}
}

WebSocketConfig是是配置,类这里的关键点就在于把自定义的WebSocket处理器和相应的请求地址相对应

MyWebSocketHandler代码编写

package cn.yangfanqihang.xiaozhi.trigger.websocket.handler;

import cn.yangfanqihang.xiaozhi.domain.communication.model.entity.DeviceEntity;
import cn.yangfanqihang.xiaozhi.domain.communication.model.entity.WebSocketSessionEntity;
import cn.yangfanqihang.xiaozhi.domain.communication.service.chatSession.ICommunicationService;
import cn.yangfanqihang.xiaozhi.types.common.Constants;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@Slf4j
public class MyWebSocketHandler extends AbstractWebSocketHandler {

    @Resource
    private ICommunicationService communicationService;

    // 会话存储
    private final ConcurrentHashMap<String, WebSocketSessionEntity> sessions = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        Map<String, String> headers = getHeadersFromSession(session);
        String deviceIdAuth = headers.get("device-id");
        log.info("设备:{}链接已建立",deviceIdAuth);
        if (deviceIdAuth == null || deviceIdAuth.isEmpty()) {
            log.error("设备ID为空");
            try {
                session.close(CloseStatus.BAD_DATA.withReason("设备ID为空"));
            } catch (IOException e) {
                log.error("关闭WebSocket连接失败", e);
            }
            return;
        }
        afterConnection(session,deviceIdAuth);

        log.info("WebSocket连接建立成功 - SessionId: {}, DeviceId: {}", session.getId(), deviceIdAuth);
    }


    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {

        WebSocketSessionEntity chatSession = sessions.get(session.getId());
        if (chatSession == null || !chatSession.isOpen()) {
            return;
        }

        DeviceEntity device = chatSession.getDevice();
        if (device != null) {
            Thread.startVirtualThread(() -> {
                try {

                    communicationService.update(DeviceEntity.builder()
                            .deviceId(device.getDeviceId())
                            .state(Constants.DEVICE_STATE_OFFLINE)
                            .lastLogin(new Date().toString())
                            .build());
                    log.info("WebSocket连接关闭 - SessionId: {}, DeviceId: {}", session.getId(), device.getDeviceId());
                } catch (Exception e) {
                    log.error("更新设备状态失败", e);
                }
            });
        }
    }



    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

        String payload = message.getPayload();

        log.info("收到信息:{}",payload);

        super.handleTextMessage(session, message);
    }

    public void afterConnection(WebSocketSession session, String deviceIdAuth) {
        String deviceId = deviceIdAuth;
        String sessionId = session.getId();
        // 注册会话和设备
        DeviceEntity device=registerSessionAndDevice(sessionId,deviceId, session);
        log.info("开始查询设备信息 - DeviceId: {}", deviceId);

        // 如果角色已绑定,则初始化其他内容
        if (!ObjectUtils.isEmpty(device) && device.getRoleId() != null) {
            //这里需要放在虚拟线程外
            //TODO 初始化全局函数管理,让每个会话都绑定自己的函数
            //TODO 建立会话上下文管理

            //TODO 异步处理语音转文字,文字转语音预热处理,预热全局函数

        }


    }





    public DeviceEntity registerSessionAndDevice(String sessionId,String deviceId, WebSocketSession session) {

        WebSocketSessionEntity chatSession =new WebSocketSessionEntity();
        chatSession.setSessionId(sessionId);
        chatSession.setLastActivityTime(Instant.now());

        sessions.put(sessionId, chatSession);
        log.info("会话已注册 - SessionId: {}  SessionType: {}", sessionId, chatSession.getClass().getSimpleName());
        DeviceEntity device = communicationService.queryDeviceById(deviceId);
        chatSession.setDevice(device);
        log.info("设备配置已注册 - SessionId: {}, DeviceId: {}", sessionId, deviceId);
        return device;
    }
    public void updateLastActivity(String sessionId) {
        WebSocketSessionEntity session = sessions.get(sessionId);
        if(session != null){
            session.setLastActivityTime(Instant.now());
        }
    }

    private Map<String, String> getHeadersFromSession(WebSocketSession session) {
        // 尝试从请求头获取设备ID
        String[] deviceKeys = {"device-id", "mac_address", "uuid", "Authorization"};

        Map<String, String> headers = new HashMap<>();

        for (String key : deviceKeys) {
            String value = session.getHandshakeHeaders().getFirst(key);
            if (value != null) {
                headers.put(key, value);
            }
        }
        // 尝试从URI参数中获取
        URI uri = session.getUri();
        if (uri != null) {
            String query = uri.getQuery();
            if (query != null) {
                for (String key : deviceKeys) {
                    String paramPattern = key + "=";
                    int startIdx = query.indexOf(paramPattern);
                    if (startIdx >= 0) {
                        startIdx += paramPattern.length();
                        int endIdx = query.indexOf('&', startIdx);
                        headers.put(key, endIdx >= 0 ? query.substring(startIdx, endIdx) : query.substring(startIdx));
                    }
                }
            }
        }
        return headers;
    }


}

MyWebSocketHandler与链接/ws/xiaozhi/v1/对应起来后,所有ws://127.0.0.1:8091/ws/xiaozhi/v1/开头的ws链接都会走自定义的处理器。这里面重写了的几个重要的方法,分别是afterConnectionEstablished,afterConnectionClosed,handleTextMessage,链接建立完成后会优先走afterConnectionEstablished,所有这里最适合对设备和会话进行初始化,例如根据diviceId查出设备信息初始化会话通用变量等等初始化操作。

复现05总结

本次复现内容不多,但是也是花了很长的时间,原因是之前我并没有详细了解我WebSocket的知识,同时还有更为重要的是从WebSocket开始就已经开始接触到小智的核心内容了,然而小智的源码我并没有掌握,我只是重构一点学一点,不断的去了解小智的代码。并且从这里开始有很多知识很难从网上查找了,如果不是小智这个项目我也不会知道WebSocket链接后需要进行这么多的初始化任务。

同时我发现源码中有很多的代码耦合度较高,我在后续的复现过程中可能需要对其进行深度的解耦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值