Passed
Push — master ( a91d54...875978 )
by y
02:08
created

WebSocketClient::getFrameReader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Helix\Socket\WebSocket;
4
5
use Exception;
6
use Helix\Socket\ReactiveInterface;
7
use Helix\Socket\StreamClient;
8
9
/**
10
 * A WebSocket client.
11
 *
12
 * https://tools.ietf.org/html/rfc6455
13
 */
14
class WebSocketClient extends StreamClient implements ReactiveInterface {
15
16
    const STATE_HANDSHAKE = 0;
17
    const STATE_OK = 1;
18
    const STATE_CLOSE = 2;
19
20
    /**
21
     * @var FrameHandler
22
     */
23
    protected $frameHandler;
24
25
    /**
26
     * @var FrameReader
27
     */
28
    protected $frameReader;
29
30
    /**
31
     * @var HandShake
32
     */
33
    protected $handshake;
34
35
    /**
36
     * @var MessageHandler
37
     */
38
    protected $messageHandler;
39
40
    /**
41
     * @var WebSocketServer
42
     */
43
    protected $server;
44
45
    /**
46
     * @var int
47
     */
48
    protected $state = self::STATE_HANDSHAKE;
49
50
    /**
51
     * @param $resource
52
     * @param WebSocketServer $server
53
     */
54
    public function __construct ($resource, WebSocketServer $server) {
55
        parent::__construct($resource);
56
        $this->server = $server;
57
    }
58
59
    /**
60
     * Closes, optionally with a code and reason sent to the peer.
61
     *
62
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
63
     * > The application MUST NOT send any more data frames after sending a
64
     * > Close frame.
65
     * >
66
     * > After both sending and receiving a Close message, an endpoint
67
     * > considers the WebSocket connection closed and MUST close the
68
     * > underlying TCP connection.
69
     *
70
     * https://tools.ietf.org/html/rfc6455#section-7.4.2
71
     * > Status codes in the range 0-999 are not used.
72
     *
73
     * @param int|null $code Only used if `>= 1000`
74
     * @param string $reason
75
     * @return StreamClient|void
76
     */
77
    public function close (int $code = null, string $reason = '') {
78
        try {
79
            if ($code >= 1000 and $this->state === self::STATE_OK) {
80
                $payload = pack('n', $code) . $reason;
81
                $this->getFrameHandler()->write($payload, Frame::OP_CLOSE);
82
                $this->shutdown(self::CH_WRITE);
83
            }
84
        }
85
        finally {
86
            $this->state = self::STATE_CLOSE;
87
            $this->server->remove($this);
88
            parent::close();
89
        }
90
    }
91
92
    /**
93
     * @return FrameHandler
94
     */
95
    public function getFrameHandler () {
96
        return $this->frameHandler ?? $this->frameHandler = new FrameHandler($this);
97
    }
98
99
    /**
100
     * @return FrameReader
101
     */
102
    public function getFrameReader () {
103
        return $this->frameReader ?? $this->frameReader = new FrameReader($this);
104
    }
105
106
    /**
107
     * @return HandShake
108
     */
109
    public function getHandshake () {
110
        return $this->handshake ?? $this->handshake = new HandShake($this);
111
    }
112
113
    /**
114
     * @return MessageHandler
115
     */
116
    public function getMessageHandler () {
117
        return $this->messageHandler ?? $this->messageHandler = new MessageHandler($this);
118
    }
119
120
    /**
121
     * @return WebSocketServer
122
     */
123
    public function getServer () {
124
        return $this->server;
125
    }
126
127
    /**
128
     * @return int
129
     */
130
    public function getState (): int {
131
        return $this->state;
132
    }
133
134
    /**
135
     * @return bool
136
     */
137
    final public function isOk (): bool {
138
        return $this->state === self::STATE_OK;
139
    }
140
141
    /**
142
     * WebSockets do not use the out-of-band channel.
143
     *
144
     * The RFC says the connection must be dropped if any unsupported activity occurs.
145
     */
146
    final public function onOutOfBand (): void {
147
        $this->close(Frame::CLOSE_PROTOCOL_ERROR, "Received out-of-band data.");
148
    }
149
150
    /**
151
     * Delegates received data to handlers.
152
     *
153
     * @throws Exception
154
     */
155
    public function onReadable (): void {
156
        if (!strlen($this->recv(1, MSG_PEEK))) { // peer has shut down writing, or closed.
157
            $this->close();
158
            return;
159
        }
160
        try {
161
            switch ($this->state) {
162
                case self::STATE_HANDSHAKE:
163
                    if ($this->getHandshake()->negotiate()) {
164
                        $this->state = self::STATE_OK;
165
                        $this->onStateOk();
166
                    }
167
                    return;
168
                case self::STATE_OK:
169
                    $frameHandler = $this->getFrameHandler();
170
                    foreach ($this->getFrameReader()->recvAll() as $frame) {
171
                        $frameHandler->onFrame($frame);
172
                    }
173
                    return;
174
                case self::STATE_CLOSE:
175
                    return;
176
            }
177
        }
178
        catch (WebSocketError $e) {
179
            $this->close($e->getCode(), $e->getMessage());
180
            throw $e;
181
        }
182
        catch (Exception $e) {
183
            $this->close(Frame::CLOSE_INTERNAL_ERROR);
184
            throw $e;
185
        }
186
    }
187
188
    /**
189
     * Stub.
190
     */
191
    protected function onStateOk (): void {
192
193
    }
194
195
    /**
196
     * @param FrameHandler $frameHandler
197
     * @return $this
198
     */
199
    public function setFrameHandler (FrameHandler $frameHandler) {
200
        $this->frameHandler = $frameHandler;
201
        return $this;
202
    }
203
204
    /**
205
     * @param FrameReader $frameReader
206
     * @return $this
207
     */
208
    public function setFrameReader (FrameReader $frameReader) {
209
        $this->frameReader = $frameReader;
210
        return $this;
211
    }
212
213
    /**
214
     * @param MessageHandler $messageHandler
215
     * @return $this
216
     */
217
    public function setMessageHandler (MessageHandler $messageHandler) {
218
        $this->messageHandler = $messageHandler;
219
        return $this;
220
    }
221
222
}