Completed
Push — master ( 1f28ab...083bb9 )
by y
01:17
created

WebSocketClient   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 241
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 24
lcom 1
cbo 5
dl 0
loc 241
rs 10
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A close() 0 13 3
A getFrameHandler() 0 3 1
A getServer() 0 3 1
A getState() 0 3 1
A isNegotiating() 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 21 6
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 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 WebSocketServer
103
     */
104
    public function getServer (): WebSocketServer {
105
        return $this->server;
106
    }
107
108
    /**
109
     * @return int
110
     */
111
    public function getState (): int {
112
        return $this->state;
113
    }
114
115
    final public function isNegotiating (): bool {
116
        return $this->state === self::STATE_HANDSHAKE;
117
    }
118
119
    /**
120
     * @return bool
121
     */
122
    final public function isOk (): bool {
123
        return $this->state === self::STATE_OK;
124
    }
125
126
    /**
127
     * Called when a complete `BINARY` payload is received from the peer.
128
     *
129
     * Throws by default.
130
     *
131
     * @param string $binary
132
     * @throws WebSocketError
133
     */
134
    public function onBinary (string $binary): void {
135
        unset($binary);
136
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle binary data.");
137
    }
138
139
    /**
140
     * Called when a `CLOSE` frame is received from the peer.
141
     *
142
     * @param int $code
143
     * @param string $reason
144
     */
145
    public function onClose (int $code, string $reason): void {
146
        unset($code, $reason);
147
        $this->close();
148
    }
149
150
    /**
151
     * WebSockets do not use the out-of-band channel.
152
     *
153
     * The RFC says the connection must be dropped if any unsupported activity occurs.
154
     *
155
     * Closes the connection with a protocol-error frame.
156
     */
157
    final public function onOutOfBand (): void {
158
        $this->close(Frame::CLOSE_PROTOCOL_ERROR, "Received out-of-band data.");
159
    }
160
161
    /**
162
     * Called when a `PING` is received from the peer.
163
     *
164
     * Automatically PONGs back the payload back by default.
165
     *
166
     * @param string $message
167
     */
168
    public function onPing (string $message): void {
169
        $this->frameHandler->writePong($message);
170
    }
171
172
    /**
173
     * Called when a `PONG` is received from the peer.
174
     *
175
     * Does nothing by default.
176
     *
177
     * @param string $message
178
     */
179
    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...
180
        // stub
181
    }
182
183
    /**
184
     * Delegates the read-channel to handlers.
185
     *
186
     * @throws WebSocketError
187
     * @throws Throwable
188
     */
189
    public function onReadable (): void {
190
        try {
191
            if ($this->isNegotiating()) {
192
                if ($this->handshake->onReadable()) {
193
                    $this->state = self::STATE_OK;
194
                    $this->onStateOk();
195
                }
196
            }
197
            elseif ($this->isOk()) {
198
                $this->frameHandler->onReadable();
199
            }
200
        }
201
        catch (WebSocketError $e) {
202
            $this->close($e->getCode(), $e->getMessage());
203
            throw $e;
204
        }
205
        catch (Throwable $e) {
206
            $this->close(Frame::CLOSE_INTERNAL_ERROR);
207
            throw $e;
208
        }
209
    }
210
211
    /**
212
     * Called when the initial connection handshake succeeds and frame I/O can occur.
213
     *
214
     * Does nothing by default.
215
     *
216
     * If you have negotiated an extension during {@link Handshake},
217
     * claim the RSV bits here via {@link FrameReader::setRsv()}
218
     */
219
    protected function onStateOk (): void {
220
        // stub
221
    }
222
223
    /**
224
     * Called when a complete `TEXT` payload is received from the peer.
225
     *
226
     * Throws by default.
227
     *
228
     * @param string $text
229
     * @throws WebSocketError
230
     */
231
    public function onText (string $text): void {
232
        unset($text);
233
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle text.");
234
    }
235
236
    /**
237
     * Forwards to the {@link FrameHandler}
238
     *
239
     * @param string $binary
240
     */
241
    public function writeBinary (string $binary): void {
242
        $this->frameHandler->writeBinary($binary);
243
    }
244
245
    /**
246
     * Forwards to the {@link FrameHandler}
247
     *
248
     * @param string $text
249
     */
250
    public function writeText (string $text): void {
251
        $this->frameHandler->writeText($text);
252
    }
253
254
}