Completed
Push — master ( 0a022c...1f28ab )
by y
01:37
created

WebSocketClient::onBinary()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
1
<?php
2
3
namespace Helix\Socket\WebSocket;
4
5
use Helix\Socket\ReactiveInterface;
6
use Helix\Socket\StreamClient;
7
use Throwable;
8
9
/**
10
 * Wraps a WebSocket peer.
11
 *
12
 * @see https://tools.ietf.org/html/rfc6455
13
 */
14
class WebSocketClient extends StreamClient implements ReactiveInterface {
15
16
    /**
17
     * The peer has connected but hasn't negotiated a session yet.
18
     */
19
    const STATE_HANDSHAKE = 0;
20
21
    /**
22
     * The session is active and the client can perform frame I/O with the peer.
23
     */
24
    const STATE_OK = 1;
25
26
    /**
27
     * The peer has disconnected.
28
     */
29
    const STATE_CLOSED = 2;
30
31
    /**
32
     * @var FrameHandler
33
     */
34
    protected $frameHandler;
35
36
    /**
37
     * @var Handshake
38
     */
39
    protected $handshake;
40
41
    /**
42
     * @var WebSocketServer
43
     */
44
    protected $server;
45
46
    /**
47
     * @var int
48
     */
49
    protected $state = self::STATE_HANDSHAKE;
50
51
    /**
52
     * @param resource $resource
53
     * @param WebSocketServer $server
54
     */
55
    public function __construct ($resource, WebSocketServer $server) {
56
        parent::__construct($resource);
57
        $this->server = $server;
58
        $this->handshake = new Handshake($this);
59
        $this->frameHandler = new FrameHandler($this);
60
    }
61
62
    /**
63
     * Closes, optionally with a code and reason sent to the peer.
64
     *
65
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
66
     * > The application MUST NOT send any more data frames after sending a
67
     * > Close frame.
68
     * >
69
     * > After both sending and receiving a Close message, an endpoint
70
     * > considers the WebSocket connection closed and MUST close the
71
     * > underlying TCP connection.
72
     *
73
     * https://tools.ietf.org/html/rfc6455#section-7.4.2
74
     * > Status codes in the range 0-999 are not used.
75
     *
76
     * @param int|null $code Sent to the peer if >= 1000
77
     * @param string $reason Sent to the peer, if code is >= 1000
78
     * @return $this
79
     */
80
    public function close (int $code = null, string $reason = '') {
81
        try {
82
            if ($code >= 1000 and $this->isOk()) {
83
                $this->frameHandler->writeClose($code, $reason);
84
            }
85
        }
86
        finally {
87
            $this->server->remove($this);
88
            parent::close();
89
            $this->state = self::STATE_CLOSED;
90
        }
91
        return $this;
92
    }
93
94
    /**
95
     * @return FrameHandler
96
     */
97
    public function getFrameHandler (): FrameHandler {
98
        return $this->frameHandler;
99
    }
100
101
    /**
102
     * @return Handshake
103
     */
104
    public function getHandshake (): Handshake {
105
        return $this->handshake;
106
    }
107
108
    /**
109
     * @return WebSocketServer
110
     */
111
    public function getServer (): WebSocketServer {
112
        return $this->server;
113
    }
114
115
    /**
116
     * @return int
117
     */
118
    public function getState (): int {
119
        return $this->state;
120
    }
121
122
    final public function isNegotiating (): bool {
123
        return $this->state === self::STATE_HANDSHAKE;
124
    }
125
126
    /**
127
     * @return bool
128
     */
129
    final public function isOk (): bool {
130
        return $this->state === self::STATE_OK;
131
    }
132
133
    /**
134
     * Called when a complete `BINARY` payload is received from the peer.
135
     *
136
     * Throws by default.
137
     *
138
     * @param string $binary
139
     * @throws WebSocketError
140
     */
141
    public function onBinary (string $binary): void {
142
        unset($binary);
143
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle binary data.");
144
    }
145
146
    /**
147
     * Called when a `CLOSE` frame is received from the peer.
148
     *
149
     * @param int $code
150
     * @param string $reason
151
     */
152
    public function onClose (int $code, string $reason): void {
153
        unset($code, $reason);
154
        $this->close();
155
    }
156
157
    /**
158
     * WebSockets do not use the out-of-band channel.
159
     *
160
     * The RFC says the connection must be dropped if any unsupported activity occurs.
161
     *
162
     * Closes the connection with a protocol-error frame.
163
     */
164
    final public function onOutOfBand (): void {
165
        $this->close(Frame::CLOSE_PROTOCOL_ERROR, "Received out-of-band data.");
166
    }
167
168
    /**
169
     * Called when a `PING` is received from the peer.
170
     *
171
     * Automatically PONGs back the payload back by default.
172
     *
173
     * @param string $message
174
     */
175
    public function onPing (string $message): void {
176
        $this->frameHandler->writePong($message);
177
    }
178
179
    /**
180
     * Called when a `PONG` is received from the peer.
181
     *
182
     * Does nothing by default.
183
     *
184
     * @param string $message
185
     */
186
    public function onPong (string $message): void {
0 ignored issues
show
Unused Code introduced by
The parameter $message is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
187
        // stub
188
    }
189
190
    /**
191
     * Delegates the read-channel to handlers.
192
     *
193
     * @throws WebSocketError
194
     * @throws Throwable
195
     */
196
    public function onReadable (): void {
197
        try {
198
            if ($this->isNegotiating()) {
199
                if ($this->handshake->onReadable()) {
200
                    $this->state = self::STATE_OK;
201
                    $this->onStateOk();
202
                }
203
            }
204
            elseif ($this->isOk()) {
205
                $this->frameHandler->onReadable();
206
            }
207
        }
208
        catch (WebSocketError $e) {
209
            $this->close($e->getCode(), $e->getMessage());
210
            throw $e;
211
        }
212
        catch (Throwable $e) {
213
            $this->close(Frame::CLOSE_INTERNAL_ERROR);
214
            throw $e;
215
        }
216
    }
217
218
    /**
219
     * Called when the initial connection handshake succeeds and frame I/O can occur.
220
     *
221
     * Does nothing by default.
222
     *
223
     * If you have negotiated an extension during {@link Handshake},
224
     * claim the RSV bits here via {@link FrameReader::setRsv()}
225
     */
226
    protected function onStateOk (): void {
227
        // stub
228
    }
229
230
    /**
231
     * Called when a complete `TEXT` payload is received from the peer.
232
     *
233
     * Throws by default.
234
     *
235
     * @param string $text
236
     * @throws WebSocketError
237
     */
238
    public function onText (string $text): void {
239
        unset($text);
240
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle text.");
241
    }
242
243
    /**
244
     * Forwards to the {@link FrameHandler}
245
     *
246
     * @param string $binary
247
     */
248
    public function writeBinary (string $binary): void {
249
        $this->frameHandler->writeBinary($binary);
250
    }
251
252
    /**
253
     * Forwards to the {@link FrameHandler}
254
     *
255
     * @param string $text
256
     */
257
    public function writeText (string $text): void {
258
        $this->frameHandler->writeText($text);
259
    }
260
261
}