
一、要解決什么問題做一個在線 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