Completed
Push — master ( 87e84e...f836de )
by y
01:21
created

WebSocketClient::setFrameHandler()   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 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 $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 $this
76
     */
77
    public function close (int $code = null, string $reason = '') {
78
        try {
79
            if ($code >= 1000 and $this->state === self::STATE_OK) {
80
                $this->getFrameHandler()->writeClose($code, $reason);
81
                $this->shutdown(self::CH_WRITE);
82
            }
83
        }
84
        finally {
85
            $this->state = self::STATE_CLOSE;
86
            $this->server->remove($this);
87
            return parent::close();
88
        }
89
    }
90
91
    /**
92
     * @return FrameHandler
93
     */
94
    public function getFrameHandler (): FrameHandler {
95
        return $this->frameHandler ?? $this->frameHandler = new FrameHandler($this);
96
    }
97
98
    /**
99
     * @return FrameReader
100
     */
101
    public function getFrameReader (): FrameReader {
102
        return $this->frameReader ?? $this->frameReader = new FrameReader($this);
103
    }
104
105
    /**
106
     * @return HandShake
107
     */
108
    public function getHandshake (): HandShake {
109
        return $this->handshake ?? $this->handshake = new HandShake($this);
110
    }
111
112
    /**
113
     * @return MessageHandler
114
     */
115
    public function getMessageHandler (): MessageHandler {
116
        return $this->messageHandler ?? $this->messageHandler = new MessageHandler($this);
117
    }
118
119
    /**
120
     * @return WebSocketServer
121
     */
122
    public function getServer (): WebSocketServer {
123
        return $this->server;
124
    }
125
126
    /**
127
     * @return int
128
     */
129
    public function getState (): int {
130
        return $this->state;
131
    }
132
133
    /**
134
     * @return bool
135
     */
136
    final public function isOk (): bool {
137
        return $this->state === self::STATE_OK;
138
    }
139
140
    /**
141
     * WebSockets do not use the out-of-band channel.
142
     *
143
     * The RFC says the connection must be dropped if any unsupported activity occurs.
144
     */
145
    final public function onOutOfBand (): void {
146
        $this->close(Frame::CLOSE_PROTOCOL_ERROR, "Received out-of-band data.");
147
    }
148
149
    /**
150
     * Delegates received data to handlers.
151
     *
152
     * @throws Exception
153
     */
154
    public function onReadable (): void {
155
        if (!strlen($this->recv(1, MSG_PEEK))) { // peer has shut down writing, or closed.
156
            $this->close();
157
            return;
158
        }
159
        try {
160
            switch ($this->state) {
161
                case self::STATE_HANDSHAKE:
162
                    if ($this->getHandshake()->negotiate()) {
163
                        $this->state = self::STATE_OK;
164
                        $this->onStateOk();
165
                    }
166
                    return;
167
                case self::STATE_OK:
168
                    $frameHandler = $this->getFrameHandler();
169
                    foreach ($this->getFrameReader()->getFrames() as $frame) {
170
                        $frameHandler->onFrame($frame);
171
                    }
172
                    return;
173
                case self::STATE_CLOSE:
174
                    return;
175
            }
176
        }
177
        catch (WebSocketError $e) {
178
            $this->close($e->getCode(), $e->getMessage());
179
            throw $e;
180
        }
181
        catch (Exception $e) {
182
            $this->close(Frame::CLOSE_INTERNAL_ERROR);
183
            throw $e;
184
        }
185
    }
186
187
    /**
188
     * Called when the initial connection handshake succeeds and frame I/O can occur.
189
     */
190
    protected function onStateOk (): void {
191
        // stub
192
    }
193
194
}