WSserver/server.py
import asyncio
import json
import uuid
import websockets
clients = {}
client_to_browsers = {}
WS_MAX_SIZE = 100 * 1024 * 1024
async def send_json(ws, payload):
try:
await ws.send(json.dumps(payload))
except:
pass
def new_client_id():
return str(uuid.uuid4())
def add_browser_for_client(client_id, ws):
if not client_id:
return
if client_id not in client_to_browsers:
client_to_browsers[client_id] = set()
client_to_browsers[client_id].add(ws)
def remove_browser_ws(ws):
stale_ids = []
for cid, browser_set in list(client_to_browsers.items()):
if ws in browser_set:
browser_set.discard(ws)
if not browser_set:
stale_ids.append(cid)
for cid in stale_ids:
client_to_browsers.pop(cid, None)
async def cleanup_loop():
while True:
await asyncio.sleep(60)
for cid, ws in list(clients.items()):
if getattr(ws, "closed", False):
clients.pop(cid, None)
client_to_browsers.pop(cid, None)
for cid, browser_set in list(client_to_browsers.items()):
alive = set()
for browser_ws in browser_set:
if not getattr(browser_ws, "closed", False):
alive.add(browser_ws)
if alive:
client_to_browsers[cid] = alive
else:
client_to_browsers.pop(cid, None)
async def handle_client(ws, msg):
custom_client_id = (msg.get("id") or "").strip()
if not custom_client_id:
custom_client_id = new_client_id()
clients[custom_client_id] = ws
await send_json(ws, {
"type": "hello",
"id": custom_client_id
})
async for raw in ws:
try:
data = json.loads(raw)
except:
continue
msg_type = (data.get("type") or "").strip()
if msg_type == "keepalive":
await send_json(ws, {
"type": "keepalive_ack"
})
continue
target_browsers = list(client_to_browsers.get(custom_client_id, set()))
if not target_browsers:
continue
payload = dict(data)
payload["id"] = custom_client_id
for browser_ws in target_browsers:
await send_json(browser_ws, payload)
async def handle_browser(ws):
await send_json(ws, {
"type": "hello",
"message": "browser connected"
})
async for raw in ws:
try:
data = json.loads(raw)
except:
await send_json(ws, {
"type": "error",
"message": "invalid json"
})
continue
if data.get("type") != "cmd":
continue
target_id = (data.get("id") or "").strip()
target_ws = clients.get(target_id)
if not target_id:
await send_json(ws, {
"type": "error",
"message": "missing client id"
})
continue
if not target_ws:
await send_json(ws, {
"type": "error",
"message": f"client not found: {target_id}"
})
continue
add_browser_for_client(target_id, ws)
await send_json(target_ws, data)
async def handler(ws):
role = None
custom_client_id = None
try:
first = await ws.recv()
try:
msg = json.loads(first)
except:
await send_json(ws, {
"type": "error",
"message": "invalid json"
})
return
role = (msg.get("role") or "").strip().lower()
if role == "client":
custom_client_id = (msg.get("id") or "").strip()
await handle_client(ws, msg)
elif role == "browser":
await handle_browser(ws)
else:
await send_json(ws, {
"type": "error",
"message": "unknown role"
})
except websockets.ConnectionClosed:
pass
except Exception as e:
print("handler error:", e)
finally:
if role == "client":
if custom_client_id and clients.get(custom_client_id) is ws:
clients.pop(custom_client_id, None)
if custom_client_id:
client_to_browsers.pop(custom_client_id, None)
remove_browser_ws(ws)
async def main():
asyncio.create_task(cleanup_loop())
async with websockets.serve(
handler,
"0.0.0.0",
8888,
max_size=WS_MAX_SIZE,
ping_interval=20,
ping_timeout=20
):
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())