Completed
Push — master ( 9fb81f...0a022c )
by y
01:28
created

WebSocketClient   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 257
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 6
dl 0
loc 257
rs 10
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 7 1
A close() 0 13 3
A getFrameHandler() 0 3 1
A getHandshake() 0 3 1
A getServer() 0 3 1
A getState() 0 3 1
A isOk() 0 3 1
A onBinary() 0 4 1
A onClose() 0 4 1
A onOutOfBand() 0 3 1
A onPing() 0 3 1
A onPong() 0 3 1
B onReadable() 0 31 9
A onStateOk() 0 3 1
A onText() 0 4 1
A writeBinary() 0 3 1
A writeText() 0 3 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 FrameReader
38
     */
39
    protected $frameReader;
40
41
    /**
42
     * @var HandShake
43
     */
44
    protected $handshake;
45
46
    /**
47
     * @var WebSocketServer
48
     */
49
    protected $server;
50
51
    /**
52
     * @var int
53
     */
54
    protected $state = self::STATE_HANDSHAKE;
55
56
    /**
57
     * @param resource $resource
58
     * @param WebSocketServer $server
59
     */
60
    public function __construct ($resource, WebSocketServer $server) {
61
        parent::__construct($resource);
62
        $this->server = $server;
63
        $this->handshake = new HandShake($this);
64
        $this->frameReader = new FrameReader($this);
65
        $this->frameHandler = new FrameHandler($this);
66
    }
67
68
    /**
69
     * Closes, optionally with a code and reason sent to the peer.
70
     *
71
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
72
     * > The application MUST NOT send any more data frames after sending a
73
     * > Close frame.
74
     * >
75
     * > After both sending and receiving a Close message, an endpoint
76
     * > considers the WebSocket connection closed and MUST close the
77
     * > underlying TCP connection.
78
     *
79
     * https://tools.ietf.org/html/rfc6455#section-7.4.2
80
     * > Status codes in the range 0-999 are not used.
81
     *
82
     * @param int|null $code Sent to the peer if >= 1000
83
     * @param string $reason Sent to the peer, if code is >= 1000
84
     * @return $this
85
     */
86
    public function close (int $code = null, string $reason = '') {
87
        try {
88
            if ($code >= 1000 and $this->isOk()) {
89
                $this->frameHandler->writeClose($code, $reason);
90
            }
91
        }
92
        finally {
93
            $this->server->remove($this);
94
            parent::close();
95
            $this->state = self::STATE_CLOSED;
96
        }
97
        return $this;
98
    }
99
100
    /**
101
     * @return FrameHandler
102
     */
103
    public function getFrameHandler (): FrameHandler {
104
        return $this->frameHandler;
105
    }
106
107
    /**
108
     * @return HandShake
109
     */
110
    public function getHandshake (): HandShake {
111
        return $this->handshake;
112
    }
113
114
    /**
115
     * @return WebSocketServer
116
     */
117
    public function getServer (): WebSocketServer {
118
        return $this->server;
119
    }
120
121
    /**
122
     * @return int
123
     */
124
    public function getState (): int {
125
        return $this->state;
126
    }
127
128
    /**
129
     * @return bool
130
     */
131
    final public function isOk (): bool {
132
        return $this->state === self::STATE_OK;
133
    }
134
135
    /**
136
     * Called when a complete `BINARY` payload is received from the peer.
137
     *
138
     * Throws by default.
139
     *
140
     * @param string $binary
141
     * @throws WebSocketError
142
     */
143
    public function onBinary (string $binary): void {
144
        unset($binary);
145
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle binary data.");
146
    }
147
148
    /**
149
     * Called when a `CLOSE` frame is received from the peer.
150
     *
151
     * @param int $code
152
     * @param string $reason
153
     */
154
    public function onClose (int $code, string $reason): void {
155
        unset($code, $reason);
156
        $this->close();
157
    }
158
159
    /**
160
     * WebSockets do not use the out-of-band channel.
161
     *
162
     * The RFC says the connection must be dropped if any unsupported activity occurs.
163
     *
164
     * Closes the connection with a protocol-error frame.
165
     */
166
    final public function onOutOfBand (): void {
167
        $this->close(Frame::CLOSE_PROTOCOL_ERROR, "Received out-of-band data.");
168
    }
169
170
    /**
171
     * Called when a `PING` is received from the peer.
172
     *
173
     * Automatically PONGs back the payload back by default.
174
     *
175
     * @param string $message
176
     */
177
    public function onPing (string $message): void {
178
        $this->frameHandler->writePong($message);
179
    }
180
181
    /**
182
     * Called when a `PONG` is received from the peer.
183
     *
184
     * Does nothing by default.
185
     *
186
     * @param string $message
187
     */
188
    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...
189
        // stub
190
    }
191
192
    /**
193
     * Delegates the read-channel to handlers.
194
     *
195
     * @throws WebSocketError
196
     * @throws Throwable
197
     */
198
    public function onReadable (): void {
199
        if (!strlen($this->recv(1, MSG_PEEK))) { // peer has shut down writing, or closed.
200
            $this->close();
201
            return;
202
        }
203
        try {
204
            switch ($this->state) {
205
                case self::STATE_HANDSHAKE:
206
                    if ($this->handshake->negotiate()) {
207
                        $this->state = self::STATE_OK;
208
                        $this->onStateOk();
209
                    }
210
                    return;
211
                case self::STATE_OK:
212
                    foreach ($this->frameReader->getFrames() as $frame) {
213
                        $this->frameHandler->onFrame($frame);
214
                    }
215
                    return;
216
                case self::STATE_CLOSED:
217
                    return;
218
            }
219
        }
220
        catch (WebSocketError $e) {
221
            $this->close($e->getCode(), $e->getMessage());
222
            throw $e;
223
        }
224
        catch (Throwable $e) {
225
            $this->close(Frame::CLOSE_INTERNAL_ERROR);
226
            throw $e;
227
        }
228
    }
229
230
    /**
231
     * Called when the initial connection handshake succeeds and frame I/O can occur.
232
     *
233
     * Does nothing by default.
234
     */
235
    protected function onStateOk (): void {
236
        // stub
237
    }
238
239
    /**
240
     * Called when a complete `TEXT` payload is received from the peer.
241
     *
242
     * Throws by default.
243
     *
244
     * @param string $text
245
     * @throws WebSocketError
246
     */
247
    public function onText (string $text): void {
248
        unset($text);
249
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle text.");
250
    }
251
252
    /**
253
     * Forwards to the {@link FrameHandler}
254
     *
255
     * @param string $binary
256
     */
257
    public function writeBinary (string $binary): void {
258
        $this->frameHandler->writeBinary($binary);
259
    }
260
261
    /**
262
     * Forwards to the {@link FrameHandler}
263
     *
264
     * @param string $text
265
     */
266
    public function writeText (string $text): void {
267
        $this->frameHandler->writeText($text);
268
    }
269
270
}