使用 Spring Websocket 托管 Overflow 默认的WebSocket实现
-
适用于只使用overflow-core开发bot,并且使用spring自带的IoC能力的一套ws解决方案。可以避免早期overflow版本中反向websocket存在的各种问题。
- 定义handler
class WSHandler : WebSocketHandler, IAdapter { override val scope = CoroutineScope(Dispatchers.IO) + SupervisorJob() override val logger: Logger = LoggerFactory.getLogger(ActionHandler::class.java) override val actionHandler: ActionHandler = ActionHandler(scope.coroutineContext[Job], logger) init { top.mrxiaom.overflow.internal.Overflow.setup() log.info("Youmu WebSocket 已接管默认Overflow Bot Handler,WSBufferSize: ${System.getProperty("org.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE")}") } override fun afterConnectionEstablished(session: WebSocketSession) { //在这里进行鉴权 scope.launch { val botImpl = Bot( SpringDelegatedWebSocket(session), BotConfig(), actionHandler ) val versionInfo = botImpl.getVersionInfo() if (botImpl.onebotVersion == 12) { session.close(CloseStatus.PROTOCOL_ERROR.withReason("不支持的onebot版本:12")) return@launch } net.mamoe.mirai.Bot.getInstanceOrNull(botImpl.id)?.getOriginChannel() ?.close(CloseStatus.NORMAL.withReason("当前连接下线")) val bot = with(BotWrapper) { val result = runCatching { botImpl.wrap( configuration = BotConfiguration { botLoggerSupplier = { LoggerFactory.getLogger("Bot#${botImpl.id}").asMiraiLogger() } networkLoggerSupplier = { LoggerFactory.getLogger("Net#${botImpl.id}").asMiraiLogger() } } ) } if (result.isFailure) { session.close(CloseStatus.PROTOCOL_ERROR.withReason("无法实例化bot")) return@launch } result.getOrThrow() } log.info("Bot ${bot.id} 已连接,协议版本信息:$versionInfo") net.mamoe.mirai.Bot._instances[botImpl.id] = bot } } override fun handleMessage(session: WebSocketSession, message: WebSocketMessage<*>) { if (message is TextMessage) { val text = message.payload onReceiveMessage(text) } } override fun handleTransportError(session: WebSocketSession, exception: Throwable) { } @OptIn(MiraiInternalApi::class) override fun afterConnectionClosed(session: WebSocketSession, closeStatus: CloseStatus) { val bot = net.mamoe.mirai.Bot.instances.find { it.getOriginChannel() == session } if (bot !== null) { bot as BotWrapper bot.eventDispatcher.broadcastAsync(BotOfflineEvent.Dropped(bot, cause = RuntimeException("连接断开"))) net.mamoe.mirai.Bot._instances.remove(bot.id) log.info("${bot.id}断开连接") } } override fun supportsPartialMessages(): Boolean = false }
- 定义ws session的wrapper。overflow中未使用的方法均可以使用TODO替代。
class SpringDelegatedWebSocket(val delegated: WebSocketSession) : WebSocket { override fun close(p0: Int, p1: String?) { delegated.close(CloseStatus(p0, p1)) } override fun close(p0: Int) { delegated.close(CloseStatus(p0)) } override fun close() { delegated.close() } override fun closeConnection(p0: Int, p1: String?) { delegated.close(CloseStatus(p0, p1)) } override fun send(p0: String?) { delegated.sendMessage(TextMessage(p0!!)) } override fun send(p0: ByteBuffer?) { delegated.sendMessage(BinaryMessage(p0!!)) } override fun send(p0: ByteArray?) { delegated.sendMessage(BinaryMessage(p0!!)) } override fun sendFrame(p0: Framedata?) { TODO("Not yet implemented") } override fun sendFrame(p0: MutableCollection<Framedata>?) { TODO("Not yet implemented") } override fun sendPing() { delegated.sendMessage(PingMessage()) } override fun sendFragmentedFrame(p0: Opcode?, p1: ByteBuffer?, p2: Boolean) { TODO("Not yet implemented") } override fun hasBufferedData(): Boolean { TODO("Not yet implemented") } override fun getRemoteSocketAddress(): InetSocketAddress = delegated.remoteAddress!! override fun getLocalSocketAddress(): InetSocketAddress = delegated.localAddress!! override fun isOpen(): Boolean = delegated.isOpen override fun isClosing(): Boolean = !isOpen override fun isFlushAndClose(): Boolean { TODO("Not yet implemented") } override fun isClosed(): Boolean = !isOpen override fun getDraft(): Draft { TODO("Not yet implemented") } override fun getReadyState(): ReadyState { TODO("Not yet implemented") } override fun getResourceDescriptor(): String { TODO("Not yet implemented") } override fun <T : Any?> setAttachment(p0: T) { TODO("Not yet implemented") } override fun <T : Any?> getAttachment(): T { TODO("Not yet implemented") } override fun hasSSLSupport(): Boolean { TODO("Not yet implemented") } override fun getSSLSession(): SSLSession { TODO("Not yet implemented") } override fun getProtocol(): IProtocol { TODO("Not yet implemented") } }
- 最后自行注册即可
@Configuration @EnableWebSocket class WSConfig:WebSocketConfigurer { override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { registry.addHandler( WSHandler(), "bot" ) } }
- 补一个上文中出现的工具类
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") package top.kagg886.youmu.backend.socket import net.mamoe.mirai.Bot import org.springframework.web.socket.WebSocketSession import top.kagg886.youmu.bot.internal.spring.SpringDelegatedWebSocket import top.mrxiaom.overflow.internal.contact.BotWrapper import kotlin.reflect.KProperty1 import kotlin.reflect.full.memberProperties import kotlin.reflect.jvm.isAccessible fun Bot.getOriginChannel(): WebSocketSession { val prop = BotWrapper::class.memberProperties.first { it.name == "implBot" } as KProperty1<BotWrapper, cn.evolvefield.onebot.client.core.Bot> prop.isAccessible = true val wrapper = prop.get(this as BotWrapper).channel return (wrapper as SpringDelegatedWebSocket).delegated }
最后附赠一套使用此方案部署在公网上的反向websocket bot:
wss://youmu.kagg886.top/bot
接入教程见此:部署bot
-
补充:
- 由于网络延迟原因,可能需要增大overflow对onebot api响应的等待时间,在启动类的main方法添加属性以增大等待时间:
System.getProperties().setProperty("overflow.timeout",1.minutes.inWholeMilliseconds.toString())
- 由于Spring Websocket分包问题,需要增大Tomcat默认处理ws的缓冲区,推荐将发送图片大小压缩到2M以内,同时在启动类添加属性:
System.getProperties().setProperty("org.apache.tomcat.websocket.DEFAULT_BUFFER_SIZE","5242880")