公司动态

从零实现浏览器端 Redis 在线查询:WebSocket 隧道

📅 2026/8/7 11:44:30
从零实现浏览器端 Redis 在线查询:WebSocket 隧道
一、要解决什么问题做一个在线 Redis 查询工具技术挑战分两层第一层浏览器怎么连 Redis浏览器 JS 只能发 HTTP/WebSocket不能直接建立 TCP 连接到 Redis 的 6379 端口。必须有一个中间层做协议转换。第二层用户 Redis 在内网怎么办公司内网的192.168.25.71:6379云端服务器根本访问不到。二、整体架构┌──────────┐ WebSocket ┌────────────────┐ WebSocket ┌──────────────┐ TCP:6379 ┌────────┐ │ 浏览器 │ ◄──────────► │ Spring Boot │ ◄──────────► │ Python Agent │ ◄────────► │ Redis │ │ Vue 2 │ │ (消息转发) │ │ (redis-py) │ │ (内网) │ └──────────┘ └────────────────┘ └──────────────┘ └────────┘浏览器WebSocket 客户端发 JSON 命令Spring Boot纯消息转发不解析 Redis 协议不存密码Agent收到 JSON → 调用 redis-py 执行 → 返回结果NginxWebSocket Upgrade 代理三、Spring Boot 端纯透明转发3.1 会话配对ComponentpublicclassSessionManager{// sessionId → { agentSession, browserSession }privatefinalConcurrentHashMapString,SessionPairpairsnewConcurrentHashMap();// Agent 连接 → 生成 8 位 sessionIdpublicStringregisterAgent(WebSocketSessionagentSession){StringsessionIdUUID.randomUUID().toString().replace(-,).substring(0,8);SessionPairpairnewSessionPair(sessionId);pair.agentSessionagentSession;pairs.put(sessionId,pair);returnsessionId;}// 浏览器连接 → 和 Agent 配对publicbooleanpairBrowser(StringsessionId,WebSocketSessionbrowserSession){SessionPairpairpairs.get(sessionId);if(pairnull)returnfalse;pair.browserSessionbrowserSession;returntrue;}// 消息转发Browser → AgentpublicvoidrelayToAgent(StringsessionId,Stringmessage){SessionPairpairpairs.get(sessionId);if(pair!nullpair.agentSession!nullpair.agentSession.isOpen()){pair.agentSession.sendMessage(newTextMessage(message));}}}3.2 两个 WebSocket 端点ConfigurationEnableWebSocketpublicclassWebSocketConfigimplementsWebSocketConfigurer{OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistryregistry){registry.addHandler(agentHandler,/ws/agent);// Agent 连这里registry.addHandler(browserHandler,/ws/browser/*);// 浏览器连这里}}AgentWebSocketHandler在 Agent 连接时生成 sessionId 返回之后收到的每条消息都 relay 给浏览器。BrowserWebSocketHandler从 URL 路径/ws/browser/{sessionId}提取 ID配对后收到的每条消息 relay 给 Agent。3.3 关键坑AuthFilter 拦截 WebSocket 握手Spring Boot 的Filter先于 WebSocket 处理器执行。ApiAuthFilter拦截了/ws/**握手阶段的 HTTP Upgrade 请求被 401 拦截。加一行白名单即可if(apiUrl.startsWith(/api/pub/)||apiUrl.startsWith(/ws/)){filterChain.doFilter(servletRequest,servletResponse);return;}四、Python Agent协议转换核心4.1 消息协议所有通信走 JSON清晰可调试// 连接 Redis浏览器 → Agent:{type:connect,host:192.168.25.71,port:6379,password:xxx,db:0}Agent → 浏览器:{type:connected,msg:192.168.25.71:6379 DB0 - PONG}// 执行命令浏览器 → Agent:{type:query,command:HGETALL user:1001}Agent → 浏览器:{type:result,result:{name:Alice,age:25},resultType:map}// 错误Agent → 浏览器:{type:error,message:Connection refused}4.2 Redis 连接与命令执行importredisasredis_libdefconnect_redis(ws,msg):globaldb dbredis_lib.Redis(hostmsg.get(host),portmsg.get(port,6379),passwordmsg.get(password)orNone,dbmsg.get(db,0),socket_connect_timeout5,socket_timeout5,decode_responsesTrue,protocol2,# ← 关键强制 RESP2兼容 Redis 6.0)db.ping()defquery_redis(ws,msg):partsshlex.split(msg[command])# HGETALL user:1001 → [HGETALL, user:1001]cmd,argsparts[0].upper(),parts[1:]ifcmdnotinREDIS_READ_CMDS:# 白名单校验returnsend_error(ws,fCommand {cmd} not allowed)resultdb.execute_command(cmd,*args)# 底层调用比反射更可靠send(ws,{type:result,result:display,resultType:rtype})4.3 兼容旧版 Redisredis-py8.x 默认用 RESP3 协议连接时先发HELLO 3协商。Redis 6.0 不支持这个命令直接报错unknown command HELLO, with args beginning with: 3解决办法连接时强制指定 RESP2。dbredis.Redis(...,protocol2)一行搞定兼容所有 Redis 版本。4.4 连接稳定性浏览器 ──ping/15s──► 服务器 ──relay──► Agent ← 应用层心跳 ▲ │ └── pong ────────────┘ ← 服务端 echo 回执 Agent 断开 → 保留 sessionId → 带旧 ID 重连 ← 浏览器无感恢复 浏览器断开 → 指数退避重连2s/4s/8s最多3次 ← 超过3次提示检查 AgentAgent 主循环用whileWebSocketApp每次重连带?sessionId旧IDwhileshould_run[0]:wsWebSocketApp(build_url(),...)ws.run_forever(ping_interval0)delaymin(delay*2,30)time.sleep(delay)五、前端Vue 2 实现5.1 双模式切换el-radio-groupv-modelmodeel-radio-buttonlabeldirect直连模式/el-radio-buttonel-radio-buttonlabelagentAgent 模式/el-radio-button/el-radio-group直连模式HTTP POST 给后端后端用 Jedis 直连 Redis同网段场景Agent 模式WebSocket 发到 AgentAgent 在本地执行后返回5.2 WebSocket 连接管理connectViaAgent(){constwsUrl${location.protocolhttps:?wss::ws:}//${location.host}/ws/browser/${this.conn.sessionId}this.wsnewWebSocket(wsUrl)this.ws.onopen(){this.startHeartbeat()// 15s 间隔 ping/pongthis.ws.send(JSON.stringify({type:connect,host,port,password,db}))}this.ws.onmessage(e){constmsgJSON.parse(e.data)if(msg.typeconnected){this.connectedtrue// 连接成功显示命令输入区}elseif(msg.typeresult){this.resultmsg// 查询结果停止 loadingthis.loadingfalse}}}5.3 结果智能渲染根据resultType自动选渲染方式!-- string --divv-ifresTypestringdivclassmeta{{ resSize }}/divprev-ifisJson{{ resJson }}/pre!-- JSON 自动格式化 --spanv-else{{ resStr }}/span!-- 长文本折叠 --buttonclickcopyResult复制/button/div!-- list --divv-else-ifresTypelistdivclassmeta{{ resList.length }} items/divdivv-for(v,i) in pagedList:class{stripe: i%2}span{{ i }}/spancode{{ v }}/code/divbuttonclickloadMore显示更多/button!-- 分页加载 --/div!-- map --divv-else-ifresTypemapinputv-modelfilterplaceholder筛选字段.../!-- 搜索过滤 --tabletrv-fork in filteredKeystdcode{{ k }}/code/tdtdcode{{ resMap[k] }}/code/td/tr/table/div!-- nil --divv-else-ifresStr(nil)classnil-tip(键不存在)/div每种类型的渲染方式不同string 展示字节数 折叠list 斑马纹 分批加载map 搜索框 隔行变色nil 独立卡片。六、部署架构6.1 Nginx WebSocket 代理location /ws/ { proxy_pass http://127.0.0.1:8000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }6.2 Agent 打包pipinstallpyinstaller pyinstaller--onefile--nameagent--iconagent.ico agent.py# dist/agent.exe12MB单文件可分发七、结果展示八、总结核心设计思路就三条协议转换下沉到 Agent服务器不碰 Redis 协议只做消息转发。Agent 用原生 redis-py 连接不受浏览器限制Agent 主动出站WebSocket 从内网往外连天然穿透 NAT/防火墙用户零网络配置在线体验https://onltool.site