|
4 | 4 |
|
5 | 5 | use Swoole\Coroutine\Http\Client as SwooleClient;
|
6 | 6 | use Swoole\WebSocket\Frame;
|
| 7 | +use Swoole\Coroutine; |
| 8 | + |
| 9 | +use function Swoole\Coroutine\run as Co; |
7 | 10 |
|
8 | 11 | class Client
|
9 | 12 | {
|
@@ -50,62 +53,95 @@ public function __construct(string $url, array $options = [])
|
50 | 53 | $this->timeout = $options['timeout'] ?? 30;
|
51 | 54 | }
|
52 | 55 |
|
53 |
| - public function connect(): void |
| 56 | + /** |
| 57 | + * Ensures code runs in a coroutine context |
| 58 | + * @param callable $callback Function to run in coroutine |
| 59 | + * @return mixed Result of the callback |
| 60 | + * @throws \Throwable If the callback throws an exception |
| 61 | + */ |
| 62 | + private function ensureCoroutine(callable $callback): mixed |
54 | 63 | {
|
55 |
| - $this->client = new SwooleClient($this->host, $this->port, $this->port === 443); |
56 |
| - $this->client->set([ |
57 |
| - 'timeout' => $this->timeout, |
58 |
| - 'websocket_compression' => true, |
59 |
| - 'max_frame_size' => 32 * 1024 * 1024, // 32MB max frame size |
60 |
| - ]); |
61 |
| - |
62 |
| - if (!empty($this->headers)) { |
63 |
| - $this->client->setHeaders($this->headers); |
64 |
| - } |
| 64 | + if (Coroutine::getCid() === -1) { |
| 65 | + $result = null; |
| 66 | + $exception = null; |
| 67 | + |
| 68 | + Co(function () use ($callback, &$result, &$exception) { |
| 69 | + try { |
| 70 | + $result = $callback(); |
| 71 | + } catch (\Throwable $e) { |
| 72 | + $exception = $e; |
| 73 | + } |
| 74 | + }); |
65 | 75 |
|
66 |
| - $success = $this->client->upgrade($this->path); |
| 76 | + if ($exception !== null) { |
| 77 | + throw $exception; |
| 78 | + } |
67 | 79 |
|
68 |
| - if (!$success) { |
69 |
| - $error = new \RuntimeException( |
70 |
| - "WebSocket connection failed: {$this->client->errCode} - {$this->client->errMsg}" |
71 |
| - ); |
72 |
| - $this->emit('error', $error); |
73 |
| - throw $error; |
| 80 | + return $result; |
74 | 81 | }
|
| 82 | + return $callback(); |
| 83 | + } |
75 | 84 |
|
76 |
| - $this->connected = true; |
77 |
| - $this->emit('open'); |
| 85 | + public function connect(): void |
| 86 | + { |
| 87 | + $this->ensureCoroutine(function () { |
| 88 | + $this->client = new SwooleClient($this->host, $this->port, $this->port === 443); |
| 89 | + $this->client->set([ |
| 90 | + 'timeout' => $this->timeout, |
| 91 | + 'websocket_compression' => true, |
| 92 | + 'max_frame_size' => 32 * 1024 * 1024, // 32MB max frame size |
| 93 | + ]); |
| 94 | + |
| 95 | + if (!empty($this->headers)) { |
| 96 | + $this->client->setHeaders($this->headers); |
| 97 | + } |
| 98 | + |
| 99 | + $success = $this->client->upgrade($this->path); |
| 100 | + |
| 101 | + if (!$success) { |
| 102 | + $error = new \RuntimeException( |
| 103 | + "WebSocket connection failed: {$this->client->errCode} - {$this->client->errMsg}" |
| 104 | + ); |
| 105 | + $this->emit('error', $error); |
| 106 | + throw $error; |
| 107 | + } |
| 108 | + |
| 109 | + $this->connected = true; |
| 110 | + $this->emit('open'); |
| 111 | + }); |
78 | 112 | }
|
79 | 113 |
|
80 | 114 | public function listen(): void
|
81 | 115 | {
|
82 |
| - while ($this->connected) { |
83 |
| - try { |
84 |
| - $frame = $this->client->recv($this->timeout); |
85 |
| - |
86 |
| - if ($frame === false) { |
87 |
| - if ($this->client->errCode === SWOOLE_ERROR_CLIENT_NO_CONNECTION) { |
88 |
| - $this->handleClose(); |
89 |
| - break; |
| 116 | + $this->ensureCoroutine(function () { |
| 117 | + while ($this->connected) { |
| 118 | + try { |
| 119 | + $frame = $this->client->recv($this->timeout); |
| 120 | + |
| 121 | + if ($frame === false) { |
| 122 | + if ($this->client->errCode === SWOOLE_ERROR_CLIENT_NO_CONNECTION) { |
| 123 | + $this->handleClose(); |
| 124 | + break; |
| 125 | + } |
| 126 | + throw new \RuntimeException( |
| 127 | + "Failed to receive data: {$this->client->errCode} - {$this->client->errMsg}" |
| 128 | + ); |
90 | 129 | }
|
91 |
| - throw new \RuntimeException( |
92 |
| - "Failed to receive data: {$this->client->errCode} - {$this->client->errMsg}" |
93 |
| - ); |
94 |
| - } |
95 | 130 |
|
96 |
| - if ($frame === "") { |
97 |
| - continue; |
98 |
| - } |
| 131 | + if ($frame === "") { |
| 132 | + continue; |
| 133 | + } |
99 | 134 |
|
100 |
| - if ($frame instanceof Frame) { |
101 |
| - $this->handleFrame($frame); |
| 135 | + if ($frame instanceof Frame) { |
| 136 | + $this->handleFrame($frame); |
| 137 | + } |
| 138 | + } catch (\Throwable $e) { |
| 139 | + $this->emit('error', $e); |
| 140 | + $this->handleClose(); |
| 141 | + break; |
102 | 142 | }
|
103 |
| - } catch (\Throwable $e) { |
104 |
| - $this->emit('error', $e); |
105 |
| - $this->handleClose(); |
106 |
| - break; |
107 | 143 | }
|
108 |
| - } |
| 144 | + }); |
109 | 145 | }
|
110 | 146 |
|
111 | 147 | private function handleFrame(Frame $frame): void
|
@@ -138,18 +174,25 @@ private function handleClose(): void
|
138 | 174 |
|
139 | 175 | public function send(string $data): void
|
140 | 176 | {
|
141 |
| - if (!$this->connected) { |
142 |
| - throw new \RuntimeException('Not connected to WebSocket server'); |
143 |
| - } |
| 177 | + try { |
| 178 | + $this->ensureCoroutine(function () use ($data) { |
| 179 | + if (!$this->connected) { |
| 180 | + throw new \RuntimeException('Not connected to WebSocket server'); |
| 181 | + } |
144 | 182 |
|
145 |
| - $success = $this->client->push($data); |
| 183 | + $success = $this->client->push($data); |
146 | 184 |
|
147 |
| - if ($success === false) { |
148 |
| - $error = new \RuntimeException( |
149 |
| - "Failed to send data: {$this->client->errCode} - {$this->client->errMsg}" |
150 |
| - ); |
151 |
| - $this->emit('error', $error); |
152 |
| - throw $error; |
| 185 | + if ($success === false) { |
| 186 | + $error = new \RuntimeException( |
| 187 | + "Failed to send data: {$this->client->errCode} - {$this->client->errMsg}" |
| 188 | + ); |
| 189 | + $this->emit('error', $error); |
| 190 | + throw $error; |
| 191 | + } |
| 192 | + }); |
| 193 | + } catch (\Throwable $e) { |
| 194 | + $this->emit('error', $e); |
| 195 | + throw $e; |
153 | 196 | }
|
154 | 197 | }
|
155 | 198 |
|
@@ -223,43 +266,61 @@ private function emit(string $event, mixed $data = null): void
|
223 | 266 |
|
224 | 267 | public function receive(): ?string
|
225 | 268 | {
|
226 |
| - if (!$this->connected) { |
227 |
| - throw new \RuntimeException('Not connected to WebSocket server'); |
228 |
| - } |
| 269 | + /** @var string|null */ |
| 270 | + return $this->ensureCoroutine(function (): ?string { |
| 271 | + if (!$this->connected) { |
| 272 | + throw new \RuntimeException('Not connected to WebSocket server'); |
| 273 | + } |
229 | 274 |
|
230 |
| - $frame = $this->client->recv($this->timeout); |
| 275 | + $frame = $this->client->recv($this->timeout); |
231 | 276 |
|
232 |
| - if ($frame === false) { |
233 |
| - if ($this->client->errCode === SWOOLE_ERROR_CLIENT_NO_CONNECTION) { |
234 |
| - $this->handleClose(); |
| 277 | + if ($frame === false) { |
| 278 | + if ($this->client->errCode === SWOOLE_ERROR_CLIENT_NO_CONNECTION) { |
| 279 | + $this->handleClose(); |
| 280 | + return null; |
| 281 | + } |
| 282 | + throw new \RuntimeException( |
| 283 | + "Failed to receive data: {$this->client->errCode} - {$this->client->errMsg}" |
| 284 | + ); |
| 285 | + } |
| 286 | + |
| 287 | + if ($frame === "") { |
235 | 288 | return null;
|
236 | 289 | }
|
237 |
| - throw new \RuntimeException( |
238 |
| - "Failed to receive data: {$this->client->errCode} - {$this->client->errMsg}" |
239 |
| - ); |
240 |
| - } |
241 | 290 |
|
242 |
| - if ($frame === "") { |
| 291 | + if ($frame instanceof Frame) { |
| 292 | + switch ($frame->opcode) { |
| 293 | + case WEBSOCKET_OPCODE_TEXT: |
| 294 | + return $frame->data; |
| 295 | + case WEBSOCKET_OPCODE_CLOSE: |
| 296 | + $this->handleClose(); |
| 297 | + return null; |
| 298 | + case WEBSOCKET_OPCODE_PING: |
| 299 | + $this->emit('ping', $frame->data); |
| 300 | + $this->client->push('', WEBSOCKET_OPCODE_PONG); |
| 301 | + return null; |
| 302 | + case WEBSOCKET_OPCODE_PONG: |
| 303 | + $this->emit('pong', $frame->data); |
| 304 | + return null; |
| 305 | + } |
| 306 | + } |
| 307 | + |
243 | 308 | return null;
|
244 |
| - } |
| 309 | + }); |
| 310 | + } |
245 | 311 |
|
246 |
| - if ($frame instanceof Frame) { |
247 |
| - switch ($frame->opcode) { |
248 |
| - case WEBSOCKET_OPCODE_TEXT: |
249 |
| - return $frame->data; |
250 |
| - case WEBSOCKET_OPCODE_CLOSE: |
251 |
| - $this->handleClose(); |
252 |
| - return null; |
253 |
| - case WEBSOCKET_OPCODE_PING: |
254 |
| - $this->emit('ping', $frame->data); |
255 |
| - $this->client->push('', WEBSOCKET_OPCODE_PONG); |
256 |
| - return null; |
257 |
| - case WEBSOCKET_OPCODE_PONG: |
258 |
| - $this->emit('pong', $frame->data); |
259 |
| - return null; |
| 312 | + /** |
| 313 | + * Check if there is an incoming message available without consuming it |
| 314 | + */ |
| 315 | + public function hasIncomingMessage(): bool |
| 316 | + { |
| 317 | + return (bool) $this->ensureCoroutine(function (): bool { |
| 318 | + if (!$this->connected) { |
| 319 | + return false; |
260 | 320 | }
|
261 |
| - } |
262 | 321 |
|
263 |
| - return null; |
| 322 | + // Small timeout to check if there is an incoming message |
| 323 | + return $this->client->recv(0.001) !== false; |
| 324 | + }); |
264 | 325 | }
|
265 | 326 | }
|
0 commit comments