jx3dev / jx3dev-php
Official-style PHP SDK for the jx3dev API. HTTP and WebSocket channels, zero runtime dependencies.
Requires
- php: ^8.1
- ext-curl: *
- ext-json: *
- ext-openssl: *
Requires (Dev)
- phpunit/phpunit: ^10.0
Suggests
- ext-swoole: 在 Swoole 协程环境下自动启用专职读协程调度,避免多协程争抢同一条 WebSocket
Provides
None
Conflicts
None
Replaces
None
README
jx3dev 接口服务的 PHP SDK。HTTP 与 WebSocket 双通道,零运行时依赖——只用
ext-curl、ext-json、ext-openssl,不引入任何框架。
use Jx3Dev\Jx3Dev; $client = Jx3Dev::make('jx3dev::3f9a1c7e…'); $status = $client->getTokenStatus(); echo $status->tokenLevel(), ' / 剩余 ', $status->callsQuota(), ' 次', PHP_EOL;
安装
composer require jx3dev/jx3dev-php
要求 PHP 8.1+。
初始化
令牌只在这里接收一次,之后 HTTP 与 WebSocket 两条通道都会自动携带。只有令牌是必填的:
$client = Jx3Dev::make('jx3dev::3f9a1c7e…');
请求前缀默认 /api/,绝大多数接口都在这里,不用配。少数在别的前缀下的接口见切换前缀。
要调的话,常用的几个(全部选项见下方折叠表):
$client = Jx3Dev::make('jx3dev::3f9a1c7e…', options: [ 'timeout' => 30.0, // 单次请求超时,默认 10 秒 'locale' => 'en', // 只影响 msg,默认不发送由服务端定 'auto_reconnect' => true, // WebSocket 断线重连,默认关闭 ]); // 不携带令牌 $anonymous = Jx3Dev::make('');
令牌是否有效由服务端判定。无效或不存在的令牌不会导致连接失败,问题要到第一次调接口时才以权限错误的形式暴露。
服务地址不传则连官方生产环境(Config::DEFAULT_BASE_URI)。测试环境或私有部署必须显式传入第二个参数——**忘了传会静默连到生产
**,这是全包唯一一处带环境含义的默认值:
$client = Jx3Dev::make($token, 'https://your-test-host');
令牌请从环境变量或配置文件读取。不要硬编码在代码里,示例代码里也不要出现真令牌。
$client = Jx3Dev::make(getenv('JX3DEV_TOKEN') ?: '');
全部可选项
| 键 | 默认 | 说明 |
|---|---|---|
prefix |
/api/ |
HTTP 请求前缀 |
ws_uri |
由 base_uri 推导 |
WebSocket 完整地址,必须 ws:// 或 wss:// 开头 |
ws_path |
/ws/ |
推导时拼接的路径 |
timeout |
10.0 |
单次请求 / 单次应答等待的超时秒数 |
connect_timeout |
5.0 |
建立连接的超时秒数 |
locale |
不发送 | zh_CN 或 en |
verify_tls |
true |
是否校验 TLS 证书 |
ping_interval |
120.0 |
WebSocket 保活间隔,0 表示不自动发 |
auto_reconnect |
false |
是否自动重连 |
reconnect_on_kick |
false |
收到 event 1001 后是否仍然重连 |
backoff_base / backoff_max / backoff_factor / backoff_jitter |
1.0 / 60.0 / 2.0 / 0.2 |
重连退避 |
max_reconnect_attempts |
0(不限) |
连续失败多少次后放弃 |
ws_token_in_query |
false |
令牌改放握手 query,见下方警告 |
user_agent |
jx3dev-php/{版本},见 Config::VERSION |
服务端识别客户端版本的依据,一般不用改 |
拼错的键会直接报错,不会静默生效为默认值。
ws_token_in_query会把令牌写进 URL。 代价是它随后会出现在反向代理和服务端的访问日志 里,而日志的留存和权限通常不按密钥对待。只有在反代确实会剥离自定义请求头、拿不到Authorization时才打开它,并且要清楚日志里从此有一份明文令牌。
HTTP 调用
统一走 POST + JSON + op 参数式,令牌走 Authorization: Bearer 请求头。
// 接口名用文档里的下划线原名 $response = $client->call('get_token_status'); $response->ok(); // code === 0 $response->code(); // 错误码 $response->msg(); // 服务端提示语 $response->data(); // 业务数据数组,无返回体时为 null $response->get('token_level'); // 取 data 里的字段,支持 'a.b.c' 点号路径 $response->extra(); // request_id / request_ts 等 // 带参数 $client->call('some_endpoint', ['name' => '张三', 'page' => 2]); // 已封装的接口返回结构化对象 $status = $client->getTokenStatus();
op / token / locale 由 SDK 填充,业务参数里出现同名键会直接报错,不会被静默覆盖。
切换前缀
默认前缀是 /api/,在初始化时定。若文档里某个接口另有前缀,单次调用可以临时切:
$client->call('get_token_status'); // 默认 /api/ $client->http('/other/api/')->call('some_op'); // 文档里另有前缀的接口
按前缀缓存通道,全部共用同一个传输实例——切前缀不会另开连接,默认前缀也不受影响。
前缀必须以斜杠开头和结尾,否则拼出的请求地址不正确。SDK 会直接报错而不是悄悄替你补——静默修正会让人以为自己写对了。
错误处理
服务端给的 msg / code / extra 一个不落地交给你,SDK 不吞、不加工、不静默重试。
use Jx3Dev\Exception\{ApiException, RateLimitException, TransportException, TimeoutException}; try { $status = $client->getTokenStatus(); } catch (RateLimitException $e) { // 限流。SDK 不替你等待,退避多久由你决定 sleep($e->retryAfter()); } catch (ApiException $e) { // SDK 不内置码表——具体码的含义对照官方接口文档,别指望常量 echo $e->errorCode(), ' ', $e->getMessage(), PHP_EOL; print_r($e->extra()); } catch (TimeoutException $e) { // 请求超时,或 WebSocket 上没等到对应 echo 的应答 } catch (TransportException $e) { // 连不上、TLS 失败、应答不是合法 JSON —— 压根没拿到业务应答 }
不想用异常控制流就换 tryCall(),业务失败不抛,自己看 code:
$response = $client->http()->tryCall('get_token_status'); if (!$response->ok()) { echo $response->code(), ' ', $response->msg(), PHP_EOL; }
异常层次:
Jx3DevException
├── ApiException 服务端返回非 0 的 code
│ └── RateLimitException 20004,带 retryAfter()
└── TransportException 没拿到业务应答
├── TimeoutException
└── ConnectionClosedException
用法错误不在这棵树里。 非法的配置项、非法的接口名或 scope,抛的是 SPL 的 InvalidArgumentException ——
那是代码写错了,不是调用失败,不该和「服务端拒了」「网断了」一起兜住。所以 catch (Jx3DevException) 捕不到它,这是故意的:这类错误应该在开发期就炸出来。
WebSocket 连接
一条长连接同时干两件事:收事件和调接口。
$ws = $client->ws(); $ws->connect(); // 保活:服务端判定的是「客户端发出的数据」,超过 600 秒没发过东西就会被关闭。 // ping_interval 到点会自动发,也可以手动发 $ws->ping(); $ws->close();
握手时读一次令牌记进连接,之后每一帧都不用再带。令牌无效不会握手失败——服务端会把你按游客接进来。
事件订阅
三步缺一不可:登记回调 → connect() → run()。只登记回调收不到任何东西——没有东西在读 socket。顺序也别反,协程模式下
connect() 一返回读协程就开工了,之后再登记可能漏掉最早的几帧。
use Jx3Dev\Protocol\{EventFrame, EventCode}; // 订阅指定事件号 $sub = $ws->on(2999, function (EventFrame $event) { echo $event->event(), ' ', json_encode($event->data()), PHP_EOL; }); // 订阅全部事件 $ws->onAny(fn(EventFrame $e) => error_log('event ' . $e->event())); // 状态快照,每 30 秒一帧。可以当「连接还活着」的探针,但不能替代保活 $ws->on(EventCode::STATUS, function (EventFrame $e) { echo '已连接 ', $e->get('connect_secs'), ' 秒', PHP_EOL; }); // 被服务端断开 $ws->on(EventCode::KICKED, function (EventFrame $e) { error_log('被断开:' . $e->kickReason()); }); $sub->cancel(); // 取消订阅 // 连接生命周期 $ws->onOpen(fn() => error_log('connected')); $ws->onClose(fn($e) => error_log('closed: ' . $e->closeCode())); $ws->onError(fn($t) => error_log((string) $t)); // 不注册则回调里的异常原样抛出 // 持续收帧。不传参数就一直跑,传秒数则到点返回 $ws->run();
单 handler:
on()每个事件号、onAny各只保留一个——再次注册会顶掉前一个(后注册的生效)并发一条E_USER_WARNING提示。因此别把on()/onAny放进onOpen回调——重连会反复触发、每次都替换并告警;订阅在connect()前登记一次即可,handler 跨重连保留。
通过 WebSocket 调接口
$response = $ws->call('get_token_status'); $status = $ws->getTokenStatus(); // 指定 scope。它是第三个参数,只改它就用命名参数,别去凑空数组 $response = $ws->call('some_action', scope: 'other_scope'); $echo = $ws->send('some_action', ['page' => 2], 'other_scope'); // 连发多帧再统一取回 $a = $ws->send('endpoint_a'); $b = $ws->send('endpoint_b', ['page' => 2]); $ws->await($a); $ws->await($b);
scope 是帧里原样带上的字段,call() / tryCall() / send() 三个方法都有,默认 default,取值含义见接口文档。限
1–32 位 [A-Za-z0-9_],不合规直接抛 InvalidArgumentException —— 不会静默发出去等服务端报错。
echo 由 SDK 自动分配(同一连接内不重复),应答严格按 echo 对号,绝不按到达次序推断——应答不保证按发送顺序返回。等待应答期间事件回调照常触发,不会积压。
WebSocket 通道不认次数配额,只认时间订阅:没有生效中的时间订阅会拿到
10121。
收帧调度(协程环境必读)
一条 socket 只能有一个执行流在读——两个协程同时 fread 会把帧撕开,症状是随机的 JSON 解析失败。SDK 按运行环境自动选调度策略:
| 环境 | 策略 | 谁在读 socket |
|---|---|---|
| 普通 PHP CLI | InlineLoop |
没有并发,等应答的调用自己边等边读 |
| Swoole 协程 | CoroutineLoop |
connect() 时起一个专职读协程独占 socket,其余协程只挂在各自的等待通道上 |
所以在 Swoole 下可以放心这么写:
// 协程 A:后台收事件 go(function () use ($ws) { $ws->on(2999, fn($e) => handle($e)); $ws->run(); }); // 协程 B:同时调接口,不会和读协程抢 socket go(function () use ($ws) { $status = $ws->getTokenStatus(); });
出站写入也做了串行化:两个协程同时 call() 时,send 走一把互斥,不会撞上「socket 已被另一个协程占用」。
想显式指定策略(比如在协程里强制用阻塞模式):
$client = new Jx3Dev($config, loop: new InlineLoop()); $client->ws()->loop(); // 确认当前用的是哪个
自定义调度实现 Jx3Dev\Ws\Loop\LoopInterface 即可。
协程模式下务必注册
onError()。 事件回调抛出的异常会打断读协程,之后run()才会把它抛给你——注册了错误处理器就不会中断收帧。
断开与重连
先看关闭码,再决定重不重连。
| 关闭码 | 怎么回事 | SDK 行为 |
|---|---|---|
1008 |
服务端主动断开,原因在断开前那帧 event 1001 的 data.reason 里 |
默认不重连 |
| 其他 | 网络或服务端异常 | 开了 auto_reconnect 才退避重连 |
$client = Jx3Dev::make($token, $base, [ 'auto_reconnect' => true, // 默认关闭 'reconnect_on_kick' => false, // 另设参数,默认关闭 'backoff_base' => 1.0, 'backoff_max' => 60.0, 'backoff_factor' => 2.0, 'backoff_jitter' => 0.2, // ±20% 抖动,避免所有客户端同时打回服务端 ]);
reconnect_on_kick 单独设一个参数、且默认关闭,是因为 1008 意味着服务端已经判定这条连接不该再存在(比如同一令牌在别处建立了连接),无脑重连只会两边互踢。
换掉底层传输
默认实现零依赖且是阻塞式的。在 Swoole / Workerman 这类常驻协程环境里,实现对应接口注入即可,SDK 其余部分不用动。
use Jx3Dev\{Jx3Dev, Config}; $client = new Jx3Dev( new Config(token: $token, baseUri: $base), httpTransport: new MyGuzzleTransport(), // Jx3Dev\Contract\HttpTransport wsTransport: new MySwooleWsTransport(), // Jx3Dev\Contract\WebSocketTransport );
接口覆盖
| 通道 | 覆盖 |
|---|---|
| HTTP | 全部接口 |
| WebSocket | 事件订阅 + 接口调用 |
每个接口都能调。是否类型化按需决定——值得封装的手写成真方法、返回对象(如 getTokenStatus() →
TokenStatus);其余用驼峰方法(@method 注解 + __call 转发,IDE 按通道补全)或直接 call() 传原名,返回泛用 ApiResponse(->get('a.b') / ->data() 取值):
$status = $client->getTokenStatus(); // 已封装,返回类型化对象 $data = $client->someOp($params)->data(); // 驼峰方法 → call('some_op') $data = $client->call('some_op', $params)->data(); // 或直接 call() 传下划线原名
接口名一律保留文档里的下划线原名,不做改写。
常驻/协程环境注意
- HTTP 每次请求独占一个 curl 句柄。 不要改成复用——
SWOOLE_HOOK_ALL下curl_exec会让出协程,共用句柄会让后来者的curl_setopt_array覆盖掉正在执行的请求,表现为偶发的「响应对不上请求」。 - 一个
WsChannel只持有一条 socket。 多协程共用同一个WsChannel是支持的(见收帧调度)。但要同时 开两条连接(比如两个令牌各连一条),得建两个Jx3Dev实例——ws()每个实例只缓存一个WsChannel。 - 连着的时候不要再调
connect()。 旧 socket 会被顶掉但不会关闭,服务端留一条僵尸连接,第一条连接上未回的call()也等不到回执了。断线重连由tick()自己判断,不用你手动补一次connect()。 Jx3Dev实例可以做容器单例。 HTTP 通道无共享可变状态;WS 通道的并发安全由调度策略保证。
测试
composer test
另有一个不依赖 composer、不联网的自检脚本,用于换环境时快速确认(尤其是没装 PHPUnit 的机器):
php verify.php
它覆盖 PHPUnit 用例之外的两处:真 socket 组帧(stream_socket_pair),以及协程调度(装了 ext-swoole 才跑,没装则跳过并提示)。
常驻环境走的是协程路径,换机器部署前值得跑一次。
覆盖 get_token_status 的成功与各类失败路径、WebSocket 连接的成功与失败路径、echo 乱序对号、事件订阅与取消、被踢后不重连。