#!/usr/bin/env python3 """SSE 通道演示脚本:证明"服务器主动推、客户端用 HTTP 发"这一对方向都通。 它同时是一个"外部客户端"的样板——完全不知道游戏内部结构,只知道协议: 连上 /api/stream → 收 welcome(含状态)→ 用 HTTP 发一个动作 → 收 state 推送 只用标准库(urllib 流式读长连接),所以任何有 Python 的环境都能跑。 用法: python tools/sse_demo.py --url http://127.0.0.1:8765 --farm demo --seconds 8 """ from __future__ import annotations import argparse import json import os import sys import time import urllib.parse import urllib.request sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from farm import net def stream_url(endpoint: str, farm: str, token: str | None) -> str: base = endpoint if "://" in endpoint else "http://" + endpoint query = {"farm": farm, "name": "sse-demo"} if token: query["token"] = token return f"{base.rstrip('/')}/api/stream?{urllib.parse.urlencode(query)}" def action_url(endpoint: str, farm: str, token: str | None) -> str: base = endpoint if "://" in endpoint else "http://" + endpoint query = {"farm": farm} if token: query["token"] = token return f"{base.rstrip('/')}/api/actions?{urllib.parse.urlencode(query)}" def post_action(url: str, action: str, params: dict | None = None) -> dict: """用普通 HTTP POST 提交动作——上行不占用实时通道。""" body = json.dumps({"action": action, "params": params or {}}).encode("utf-8") request = urllib.request.Request( url, data=body, method="POST", headers={"Content-Type": "application/json"} ) with net.open_url(request, timeout=8.0) as response: return json.loads(response.read().decode("utf-8")) def run(endpoint: str, farm: str, token: str | None, seconds: float) -> int: url = stream_url(endpoint, farm, token) print(f"连接 {url}") request = urllib.request.Request(url, headers={"Accept": "text/event-stream"}) counts: dict[str, int] = {} acted = False deadline = time.time() + seconds # 读超时必须比服务器心跳(15 秒)宽裕,否则空闲时会被自己的超时打断 with net.open_url(request, timeout=45.0) as response: while True: try: raw_line = response.readline() except (TimeoutError, OSError) as exc: print(f"(读超时:{exc})") break if not raw_line: print("(服务器关闭了长连接)") break line = raw_line.decode("utf-8", errors="replace").strip() if not line or line.startswith(":"): continue if not line.startswith("data:"): continue message = json.loads(line[len("data:") :].strip()) kind = message.get("type", "?") counts[kind] = counts.get(kind, 0) + 1 if kind == "welcome": state = message.get("state", {}) print( f"← welcome:牧场 {message.get('farm')} · 第 {state.get('day')} 天 · " f"金币 {state.get('gold')} · 候选动作 {len(message.get('actions', []))} 个" ) # welcome 里就带着当前合法动作,挑第一个执行, # 验证"两条通道改的是同一个引擎" candidates = message.get("actions") or [] if candidates: pick = candidates[0] reply = post_action( action_url(endpoint, farm, token), pick["action"], pick.get("params") or {}, ) acted = True print( f"→ POST /api/actions {pick['action']} " f"{pick.get('params') or {}}:{reply.get('message')}" ) elif kind == "state": snapshot = message.get("state", {}) print( f"← state(seq={message.get('seq')}):第 {snapshot.get('day')} 天 · " f"金币 {snapshot.get('gold')} · " f"在线 {message.get('online')}" ) if acted: # 收到了动作触发的推送,两条路都验证完了 break elif kind == "day": print(f"← day:进入第 {message.get('day')} 天,事件 {message.get('events')}") elif kind == "notice": print(f"← notice:{message.get('text')}") else: print(f"← {kind}") if time.time() >= deadline: break print(f"\n消息统计:{counts}") if counts.get("welcome", 0) < 1: print("✗ 没有收到 welcome,SSE 长连接可能没建立") return 1 if counts.get("state", 0) < 1: print("✗ 动作之后没有收到 state 推送(引擎与推送线程之间可能断了)") return 1 print("✓ SSE 通道正常:状态会主动下行,动作经由 HTTP 上行") return 0 def main() -> int: parser = argparse.ArgumentParser(description="SSE 通道演示") parser.add_argument("--url", default="http://127.0.0.1:8765") parser.add_argument("--farm", default="sse-demo") parser.add_argument("--token", default=None) parser.add_argument("--seconds", type=float, default=8.0) args = parser.parse_args() return run(args.url, args.farm, args.token, args.seconds) if __name__ == "__main__": sys.exit(main())