Screego 1.12.6 REST API 与 WebSocket 信令详解
给出 Screego 1.12.6 对外接口完整清单与行为语义:5 条 REST、/stream 信令 9 入 9 出事件、认证与 Cookie 会话语义、CLOSE 帧错误处理与 Python 标准库集成示例,全部实测交互记录照抄
Screego 1.12.6 REST API 与 WebSocket 信令详解
目标:面向二次开发与运维集成,给出 Screego 1.12.6 对外接口的完整清单与行为语义——5 条 REST 接口、1 条 WebSocket 信令通道(9 种入站事件、9 种出站消息)、认证机制与错误处理,全部以真实服务的实测交互记录为准,查询日期 2026-10-27。
一、环境信息
- 被测服务:Screego 1.12.6(部署形态见《Screego 1.12.6 Docker Compose 部署指南》,HTTP 端口映射为
8130) - 测试位置:与服务器同机(
127.0.0.1:8130) - 管理账号:
admin/q1w2e3r4(SCREEGO_AUTH_MODE=turn,即房主需登录、加入者可匿名) - 接口清单取证:官方源码
router/router.go(v1.12.6),与线上实测行为一一对应
二、接口全景
Screego 对外只有一个 HTTP 入口,REST 与 WebSocket 共用端口:
| 方法 | 路径 | 类型 | 认证 | 职责 |
|---|---|---|---|---|
| GET | /health | REST | 无 | 健康检查,返回在线连接数 |
| GET | /config | REST | 无(带 Cookie 返回登录态) | 前端启动配置:认证模式、当前用户、版本、随机房间名 |
| POST | /login | REST | 无 | 表单登录,成功签发会话 Cookie |
| POST | /logout | REST | Cookie | 注销当前浏览器会话 |
| GET | /metrics | REST | Basic Auth | Prometheus 指标(SCREEGO_PROMETHEUS=true 时注册) |
| GET | /stream | WebSocket | Cookie(房主操作时需要) | 信令通道:建房/加入/共享/ICE 协商全部走这里 |
三个对集成方至关重要的全局行为(实测 + 源码双确认):
- 除上表外没有其他 REST 接口。 不存在「REST 创建房间」之类的能力——一切房间操作都通过
/stream信令完成。 - 未知路径返回 200 空响应体(内部按 404 记日志但不体现到响应上)。做集成判断时必须校验响应体而非状态码,探活请用
/health。 - 错误响应统一为 JSON
{"message":"..."},无错误码字段,靠 HTTP 状态码(401/500)或 WebSocket CLOSE 帧区分。
三、认证机制
3.1 登录与 Cookie 会话
登录是 form 表单提交(字段 user/pass),不是 JSON——发 JSON 请求不会报错,但会得到 200 空响应体(命中上面的未知路径怪癖之前的行为差异:/login 路由存在但取不到表单值,认证失败)。
curl -s -i -X POST http://127.0.0.1:8130/login -d "user=admin&pass=q1w2e3r4"
实测输出:
HTTP/1.1 200 OK
Set-Cookie: user=MTc5MzA2MDc4N3xEWDhFQVFMX2dBQUJFQUVRQUFBal80QUFBUVp6ZEhKcGJtY01CZ0FFZFhObGNnWnpkSEpwYm1jTUJ3QUZZV1J0YVc0PXyuMAAP5G8hTpYoe-L86U4xwjF7YxPEN0uKLYIbP7vdRQ==
Date: Tue, 27 Oct 2026 00:26:27 GMT
Content-Length: 28
Content-Type: text/plain; charset=utf-8
{"message":"authenticated"}
会话就是签名 Cookie(名为 user),无服务端会话存储;签名密钥为 SCREEGO_SECRET(未固定则重启全失效,见 B2 篇排障 8.2)。口令错误返回 401:
HTTP/1.1 401 Unauthorized
Date: Tue, 27 Oct 2026 00:26:27 GMT
Content-Length: 37
Content-Type: text/plain; charset=utf-8
{"message":"could not authenticate"}
3.2 登出语义(集成方必读)
POST /logout 的实现是给浏览器下发一个新的空会话 Cookie,浏览器用新 Cookie 替换旧 Cookie 后即恢复匿名态:
HTTP/1.1 200 OK
Set-Cookie: user=MTc5MzA2MDc4N3xEWDhFQVFMX2dBQUJFQUVRQUFBRV80QUFBQT09fLwfnV5YzG7Y5QQGdDTSgiJTWQ1m5MYN2jKs934UXXnV
Date: Tue, 27 Oct 2026 00:26:27 GMT
Content-Length: 0
用新 Cookie 访问 /config,已恢复匿名(user 回到随机访客名):
{"authMode":"turn","user":"guest","loggedIn":false,"version":"1.12.6","roomName":"dynamic-salmon-duck","closeRoomWhenOwnerLeaves":true}
安全注意:旧 Cookie 并未在服务端吊销(会话无服务端状态,没有黑名单机制),泄露的旧 Cookie 在其生命周期内依然可用。服务的 Cookie 生命周期默认为浏览器会话级,可用 SCREEGO_SESSION_TIMEOUT_SECONDS 收紧;怀疑泄露时轮换 SCREEGO_SECRET(旧 Cookie 全部失效)。
3.3 登录态读取
GET /config 带 Cookie 与不带 Cookie 的差别就是登录态回显。带有效 Cookie 时:
{"authMode":"turn","user":"admin","loggedIn":true,"version":"1.12.6","roomName":"able-tan-rabbit","closeRoomWhenOwnerLeaves":true}
四、WebSocket 信令通道
4.1 握手
GET /stream,标准 WebSocket 升级。实测(Python 标准库构造握手,无第三方依赖):
[handshake] HTTP/1.1 101 Switching Protocols
[handshake] Sec-WebSocket-Accept: /HkkvMZ4QWNGP0ntenCLQVa104Y=
登录 Cookie 直接随握手请求带上(Cookie: user=...),服务端在握手阶段完成身份识别;匿名连接同样允许握手成功,只有需要身份的操作才会被拒。
4.2 封包格式
- 全部为 JSON 文本帧,统一信封:
{"type":"<事件名>","payload":{...}} - 发送二进制帧会被直接断开:CLOSE 码 1003,原因
unsupported binary message type(源码ws/client.go) - 不可识别的
type同样断开:CLOSE 码 1003,原因malformed message: cannot handle foo - 服务端会周期性发 ping 帧,客户端必须回 pong(所有标准 WebSocket 库自动处理)
4.3 事件清单(源码取证)
入站事件(客户端 → 服务端,9 种,源码 ws/event_*.go):
| 事件 | payload 字段 | 语义 |
|---|---|---|
create | id、mode、closeOnOwnerLeave、username、joinIfExist | 创建房间。mode 取 stun/local/turn;turn 模式的房间要求创建者已登录 |
join | id、username | 加入房间。加入即与房间内所有正在共享的用户建立会话 |
share | (空对象) | 房主/成员开始屏幕共享,向房间内其他成员发起 WebRTC 会话 |
stopshare | (空对象) | 停止共享,对应成员收到 endshare |
hostoffer | sid、value | 共享方 SDP Offer(value 为 {type,sdp} 对象) |
clientanswer | sid、value | 观看方 SDP Answer |
hostice | sid、value | 共享方 ICE 候选(value 为候选描述对象) |
clientice | sid、value | 观看方 ICE 候选 |
name | name | 匿名用户改显示名 |
出站消息(服务端 → 客户端,9 种,源码 ws/outgoing/messages.go):
| 消息 | payload 字段 | 语义 |
|---|---|---|
room | id、users[] | 房间成员表广播(成员增减、共享状态变化都会触发全员推送) |
hostsession | id、peer、iceServers[] | 分配给共享方的一个会话:会话 id、观看方 id、可用 ICE 服务器 |
clientsession | id、peer、iceServers[] | 分配给观看方的对称会话 |
hostoffer / hostice / clientanswer / clientice | sid、value | SDP/ICE 转发(与入站同名、方向相反) |
endshare | sid | 共享结束/会话拆除 |
closewriter | code、reason | 服务端主动断开前的告知 |
users[] 中每个成员的字段:id(连接级 xid)、name(登录用户名或随机名)、streaming(是否共享中)、you(是否本连接)、owner(是否房主)。
4.4 完整实测对话:建房 → 匿名加入 → 发起共享
以下为一次真实交互的逐帧记录(两连接:owner 已登录、joiner 匿名)。
owner 发送 create:
{"type": "create", "payload": {"id": "d5-api-test", "mode": "stun", "closeOnOwnerLeave": true, "username": ""}}
owner 收到首个 room 广播:
{"type":"room","payload":{"id":"d5-api-test","mode":"","users":[{"id":"dbfuvcpi6cp000849lng","name":"admin","streaming":false,"you":true,"owner":true}]}}
joiner 发送 join(匿名,username 留空):
{"type": "join", "payload": {"id": "d5-api-test"}}
joiner 收到的 room(第二个成员 Charming Mouse 是服务端生成的随机访客名):
{"type":"room","payload":{"id":"d5-api-test","mode":"","users":[{"id":"dbfuvcpi6cp000849lng","name":"admin","streaming":false,"you":false,"owner":true},{"id":"dbfuvcpi6cp000849lo0","name":"Charming Mouse","streaming":false,"you":true,"owner":false}]}}
owner 同时收到成员变化广播:
{"type":"room","payload":{"id":"d5-api-test","mode":"","users":[{"id":"dbfuvcpi6cp000849lng","name":"admin","streaming":false,"you":true,"owner":true},{"id":"dbfuvcpi6cp000849lo0","name":"Charming Mouse","streaming":false,"you":false,"owner":false}]}}
owner 发送 share,双方各收到一条会话分配消息。注意 iceServers 的 urls 直接由 SCREEGO_EXTERNAL_IP 生成——这正是「EXTERNAL_IP 配错则无法穿透」在信令层的直接体现:
{"type":"hostsession","payload":{"id":"dbfuvcpi6cp000849log","peer":"dbfuvcpi6cp000849lo0","iceServers":[{"urls":["stun:192.168.146.131:3478"],"credential":"","username":""}]}}
{"type":"clientsession","payload":{"id":"dbfuvcpi6cp000849log","peer":"dbfuvcpi6cp000849lng","iceServers":[{"urls":["stun:192.168.146.131:3478"],"credential":"","username":""}]}}
后续协商即对称转发:owner 逐会话发 hostoffer → joiner 回 clientanswer → 双方互发 hostice/clientice,然后媒体流在浏览器间直连(或经 TURN 中继),服务端不接触媒体数据。
4.5 错误处理实测
错误统一以 WebSocket CLOSE 帧送达,无 JSON 错误消息。三种典型:
未登录创建 turn 模式房间(SCREEGO_AUTH_MODE=turn 下建房本不需要登录,但 mode=turn 需要):
<< CLOSE code=1000 reason="you need to login"
未知事件类型:
<< CLOSE code=1003 reason="malformed message: cannot handle foo"
加入不存在的房间:
<< CLOSE code=1000 reason="room with id no-such-room does not exist"
其余实测可触发的同类错误原因:room with id <id> does already exist(重复创建)、cannot join room, you are already in one(单连接同时只属于一个房间)。集成方重连后必须重新 create/join,连接与房间绑定。
五、Python 标准库集成示例
不引第三方库的完整可用客户端骨架(握手 + 掩码发送 + 帧解析 + CLOSE 处理):
#!/usr/bin/env python3
"""Minimal WebSocket signaling client for Screego /stream (stdlib only)."""
import socket, base64, os, struct, json
HOST, PORT = '127.0.0.1', 8130
def handshake(cookie=None):
key = base64.b64encode(os.urandom(16)).decode()
lines = ['GET /stream HTTP/1.1', f'Host: {HOST}:{PORT}', 'Upgrade: websocket',
'Connection: Upgrade', f'Sec-WebSocket-Key: {key}', 'Sec-WebSocket-Version: 13']
if cookie:
lines.append(f'Cookie: {cookie}')
s = socket.create_connection((HOST, PORT))
s.sendall(('\r\n'.join(lines) + '\r\n\r\n').encode())
resp = b''
while b'\r\n\r\n' not in resp:
resp += s.recv(4096)
status = resp.split(b'\r\n', 1)[0].decode()
assert '101' in status, status
return s
def send_text(s, obj):
data = json.dumps(obj).encode()
mask = os.urandom(4) # 客户端帧必须掩码
header = bytes([0x81]) # FIN + text
n = len(data)
if n < 126:
header += bytes([0x80 | n])
elif n < 65536:
header += bytes([0x80 | 126]) + struct.pack('!H', n)
else:
header += bytes([0x80 | 127]) + struct.pack('!Q', n)
s.sendall(header + mask + bytes(b ^ mask[i % 4] for i, b in enumerate(data)))
def recv_msg(s, timeout=5):
s.settimeout(timeout)
def rd(n):
buf = b''
while len(buf) < n:
c = s.recv(n - len(buf))
if not c:
raise ConnectionError('closed')
buf += c
return buf
b1, b2 = rd(2)
op, ln = b1 & 0x0F, b2 & 0x7F
if ln == 126:
ln = struct.unpack('!H', rd(2))[0]
elif ln == 127:
ln = struct.unpack('!Q', rd(8))[0]
payload = rd(ln)
if op == 1:
return json.loads(payload)
if op == 8: # CLOSE 帧:错误都从这里拿
code = struct.unpack('!H', payload[:2])[0]
raise ConnectionError(f'closed {code}: {payload[2:].decode()}')
if op == 9: # ping -> pong
s.sendall(bytes([0x8A, 0x80]) + os.urandom(4))
return recv_msg(s, timeout)
return recv_msg(s, timeout)
s = handshake('user=<登录后拿到的 Cookie 值>')
send_text(s, {'type': 'create', 'payload': {'id': 'my-room', 'mode': 'stun',
'closeOnOwnerLeave': True, 'username': ''}})
print(json.dumps(recv_msg(s), ensure_ascii=False))
要点归纳:
- 客户端发出的每帧必须掩码(RFC 6455),否则握手后的第一帧就会被断开。
- 错误都在 CLOSE 帧里:
code=1000是业务拒绝(需登录、房间不存在等),code=1003是封包违规(未知 type、二进制帧)。 - 服务端 ping 周期较长,长连接集成必须正确回 pong,否则按超时断开。
- Cookie 从
POST /login的Set-Cookie头解析(值到第一个;之前)。
六、语义细节与常见坑
| # | 坑点 | 说明 |
|---|---|---|
| 1 | 登录发 JSON 拿不到结果 | /login 只读 form 表单;发 JSON 会得到 401 或空响应。curl 用 -d "user=..&pass=.." |
| 2 | 房间不是 REST 资源 | 没有创建/列举/删除房间的 HTTP 接口;房间存在于内存、生命周期随连接 |
| 3 | room 广播没有 mode 字段 | 房间模式是服务端内部字段,广播只含 id 与 users[],实测回显 "mode":"" 即此原因 |
| 4 | 未知路径 200 空体 | 判断接口存在性/集成探测必须看响应体;探活用 /health(C1 篇 5.2 同源怪癖) |
| 5 | 单连接单房间 | 一个 WebSocket 连接同时只能属于一个房间,重复 create/join 报 already in one |
| 6 | 房间无落盘 | 服务重启全部房间消失(无数据库);长会话集成需处理重连重入 |
| 7 | 匿名用户随机名 | username 留空时服务端生成「形容词-动物」式随机名(如 Charming Mouse),可用 name 事件修改 |
| 8 | 房间名是随机短语 | 前端「快速创建」用的房间名(/config 的 roomName,如 kind-fuchsia-cow)由服务端生成,自建房间 id 则完全自定义 |
| 9 | /metrics 不是探活点 | 未开启 SCREEGO_PROMETHEUS 时该路径 200 空体;且开启后要 Basic Auth(C1 篇 5.2) |
| 10 | ICE 服务器串着 EXTERNAL_IP | hostsession/clientsession 里的 iceServers.urls 由 SCREEGO_EXTERNAL_IP 拼出,改 IP 后立即影响新会话 |
七、参考链接
- 《Screego 1.12.6 介绍与选型指南》:https://www.quzuo.net/document/XzOmvpIQfDim
- 《Ubuntu 22.04 安装 Screego 1.12.6 指南》:https://www.quzuo.net/document/IPfdAp2GZwAK
- 《Screego 1.12.6 Docker Compose 部署指南》:https://www.quzuo.net/document/xTY6t7RtGvqo
- 《Screego 1.12.6 配置参数详解》:https://www.quzuo.net/document/YIZZNUyL2MxY
- GitHub 仓库(源码
router/、ws/、auth/):https://github.com/screego/server