Completed
Push — master ( b537f9...9fb81f )
by y
01:17
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 Exception;
6
use Helix\Socket\ReactiveInterface;
7
use Helix\Socket\StreamClient;
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
    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 WebSocketServer
37
     */
38
    protected $server;
39
40
    /**
41
     * @var int
42
     */
43
    protected $state = self::STATE_HANDSHAKE;
44
45
    /**
46
     * @param resource $resource
47
     * @param WebSocketServer $server
48
     */
49
    public function __construct ($resource, WebSocketServer $server) {
50
        parent::__construct($resource);
51
        $this->server = $server;
52
    }
53
54
    /**
55
     * Closes, optionally with a code and reason sent to the peer.
56
     *
57
     * https://tools.ietf.org/html/rfc6455#section-5.5.1
58
     * > The application MUST NOT send any more data frames after sending a
59
     * > Close frame.
60
     * >
61
     * > After both sending and receiving a Close message, an endpoint
62
     * > considers the WebSocket connection closed and MUST close the
63
     * > underlying TCP connection.
64
     *
65
     * https://tools.ietf.org/html/rfc6455#section-7.4.2
66
     * > Status codes in the range 0-999 are not used.
67
     *
68
     * @param int|null $code Only used if `>= 1000`
69
     * @param string $reason
70
     * @return $this
71
     */
72
    public function close (int $code = null, string $reason = '') {
73
        try {
74
            if ($code >= 1000 and $this->state === self::STATE_OK) {
75
                $this->getFrameHandler()->writeClose($code, $reason);
76
                $this->shutdown(self::CH_WRITE);
77
            }
78
        }
79
        finally {
80
            $this->state = self::STATE_CLOSE;
81
            $this->server->remove($this);
82
            return parent::close();
83
        }
84
    }
85
86
    /**
87
     * @return FrameHandler
88
     */
89
    public function getFrameHandler (): FrameHandler {
90
        return $this->frameHandler ?? $this->frameHandler = new FrameHandler($this);
91
    }
92
93
    /**
94
     * @return FrameReader
95
     */
96
    public function getFrameReader (): FrameReader {
97
        return $this->frameReader ?? $this->frameReader = new FrameReader($this);
98
    }
99
100
    /**
101
     * @return HandShake
102
     */
103
    public function getHandshake (): HandShake {
104
        return $this->handshake ?? $this->handshake = new HandShake($this);
105
    }
106
107
    /**
108
     * @return WebSocketServer
109
     */
110
    public function getServer (): WebSocketServer {
111
        return $this->server;
112
    }
113
114
    /**
115
     * @return int
116
     */
117
    public function getState (): int {
118
        return $this->state;
119
    }
120
121
    /**
122
     * @return bool
123
     */
124
    final public function isOk (): bool {
125
        return $this->state === self::STATE_OK;
126
    }
127
128
    /**
129
     * Called when a complete binary payload is received.
130
     *
131
     * @param string $binary
132
     */
133
    public function onBinary (string $binary): void {
134
        unset($binary);
135
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle binary data.");
136
    }
137
138
    /**
139
     * WebSockets do not use the out-of-band channel.
140
     *
141
     * The RFC says the connection must be dropped if any unsupported activity occurs.
142
     */
143
    final public function onOutOfBand (): void {
144
        $this->close(Frame::CLOSE_PROTOCOL_ERROR, "Received out-of-band data.");
145
    }
146
147
    /**
148
     * Delegates received data to handlers.
149
     *
150
     * @throws Exception
151
     */
152
    public function onReadable (): void {
153
        if (!strlen($this->recv(1, MSG_PEEK))) { // peer has shut down writing, or closed.
154
            $this->close();
155
            return;
156
        }
157
        try {
158
            switch ($this->state) {
159
                case self::STATE_HANDSHAKE:
160
                    if ($this->getHandshake()->negotiate()) {
161
                        $this->state = self::STATE_OK;
162
                        $this->onStateOk();
163
                    }
164
                    return;
165
                case self::STATE_OK:
166
                    $frameHandler = $this->getFrameHandler();
167
                    foreach ($this->getFrameReader()->getFrames() as $frame) {
168
                        $frameHandler->onFrame($frame);
169
                    }
170
                    return;
171
                case self::STATE_CLOSE:
172
                    return;
173
            }
174
        }
175
        catch (WebSocketError $e) {
176
            $this->close($e->getCode(), $e->getMessage());
177
            throw $e;
178
        }
179
        catch (Exception $e) {
180
            $this->close(Frame::CLOSE_INTERNAL_ERROR);
181
            throw $e;
182
        }
183
    }
184
185
    /**
186
     * Called when the initial connection handshake succeeds and frame I/O can occur.
187
     */
188
    protected function onStateOk (): void {
189
        // stub
190
    }
191
192
    /**
193
     * Called when a complete text payload is received.
194
     *
195
     * @param string $text
196
     */
197
    public function onText (string $text): void {
198
        unset($text);
199
        throw new WebSocketError(Frame::CLOSE_UNHANDLED_DATA, "I don't handle text.");
200
    }
201
202
}