Completed
Push — master ( 1675cb...1c5db4 )
by Arthur
02:30 queued 01:10
created

Connection::broadCastMany()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4888
c 0
b 0
f 0
cc 5
nc 7
nop 2
1
<?php
2
3
namespace WSSC\Components;
4
5
use WSSC\Contracts\CommonsContract;
6
use WSSC\Contracts\ConnectionContract;
7
use WSSC\Contracts\WebSocketServerContract;
8
9
class Connection implements ConnectionContract, CommonsContract
10
{
11
12
    private $socketConnection;
13
    private $clients;
14
15
    /**
16
     * Connection constructor.
17
     *
18
     * @param $sockConn
19
     * @param array $clients
20
     */
21
    public function __construct($sockConn, array $clients = [])
22
    {
23
        $this->socketConnection = $sockConn;
24
        $this->clients = $clients;
25
    }
26
27
    /**
28
     * Closes clients socket stream
29
     *
30
     * @throws \Exception
31
     */
32
    public function close(): void
33
    {
34
        if (is_resource($this->socketConnection)) {
35
            fwrite($this->socketConnection, $this->encode('', self::EVENT_TYPE_CLOSE));
36
            fclose($this->socketConnection);
37
        }
38
    }
39
40
    /**
41
     * This method is invoked when user implementation call $conn->send($data)
42
     * writes data to the clients stream socket
43
     *
44
     * @param string $data pure decoded data from server
45
     * @throws \Exception
46
     */
47
    public function send(string $data): void
48
    {
49
        fwrite($this->socketConnection, $this->encode($data));
50
    }
51
52
    /**
53
     * @param string $data data to send to clients
54
     * @throws \Exception
55
     */
56
    public function broadCast(string $data): void
57
    {
58
        foreach ($this->clients as $client) {
59
            if (is_resource($client)) { // check if not yet closed/broken etc
60
                fwrite($client, $this->encode($data));
61
            }
62
        }
63
    }
64
65
    /**
66
     * Broadcasting many messages with delay
67
     *
68
     * @param array $data   An array of messages (strings) sent to many clients
69
     * @param int $delay    Time in seconds to delay between messages
70
     * @throws \Exception
71
     */
72
    public function broadCastMany(array $data, int $delay = 0): void
73
    {
74
        foreach ($data as $message) {
75
            foreach ($this->clients as $client) {
76
                if (is_resource($client)) { // check if not yet closed/broken etc
77
                    fwrite($client, $this->encode($message));
78
                }
79
            }
80
81
            if ($delay > 0) {
82
                sleep($delay);
83
            }
84
        }
85
    }
86
87
    /**
88
     * Encodes data before writing to the client socket stream
89
     *
90
     * @param string $payload
91
     * @param string $type
92
     * @param boolean $masked
93
     * @return mixed
94
     * @throws \Exception
95
     */
96
    private function encode($payload, string $type = self::EVENT_TYPE_TEXT, bool $masked = false)
97
    {
98
        $frameHead = $this->getOpType($type);
99
        $payloadLength = strlen($payload);
100
101
        // set mask and payload length (using 1, 3 or 9 bytes)
102
        if ($payloadLength > self::PAYLOAD_MAX_BITS) {
103
            $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), self::PAYLOAD_CHUNK);
104
            $frameHead[1] = ($masked === true) ? self::MASK_255 : self::MASK_127;
105
106
            for ($i = 0; $i < 8; $i++) {
107
                $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
108
            }
109
110
            // most significant bit MUST be 0
111
            if ($frameHead[2] > self::MASK_127) {
112
                return [
113
                    'type'    => $type,
114
                    'payload' => $payload,
115
                    'error'   => WebSocketServerContract::ERR_FRAME_TOO_LARGE,
116
                ];
117
            }
118
        } elseif ($payloadLength > self::MASK_125) {
119
            $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), self::PAYLOAD_CHUNK);
120
            $frameHead[1] = ($masked === true) ? self::MASK_254 : self::MASK_126;
121
            $frameHead[2] = bindec($payloadLengthBin[0]);
122
            $frameHead[3] = bindec($payloadLengthBin[1]);
123
        } else {
124
            $frameHead[1] = ($masked === true) ? $payloadLength + self::MASK_128 : $payloadLength;
125
        }
126
127
        return $this->getComposedFrame($frameHead, $payload, $payloadLength, $masked);
128
    }
129
130
    /**
131
     * Gets frame-head based on type of operation
132
     *
133
     * @param string $type  Types of operation encode-frames
134
     * @return array
135
     */
136
    private function getOpType(string $type): array
137
    {
138
        $frameHead = [];
139
140
        switch ($type) {
141
            case self::EVENT_TYPE_TEXT:
142
                // first byte indicates FIN, Text-Frame (10000001):
143
                $frameHead[0] = self::ENCODE_TEXT;
144
                break;
145
146
            case self::EVENT_TYPE_CLOSE:
147
                // first byte indicates FIN, Close Frame(10001000):
148
                $frameHead[0] = self::ENCODE_CLOSE;
149
                break;
150
151
            case self::EVENT_TYPE_PING:
152
                // first byte indicates FIN, Ping frame (10001001):
153
                $frameHead[0] = self::ENCODE_PING;
154
                break;
155
156
            case self::EVENT_TYPE_PONG:
157
                // first byte indicates FIN, Pong frame (10001010):
158
                $frameHead[0] = self::ENCODE_PONG;
159
                break;
160
        }
161
162
        return $frameHead;
163
    }
164
165
    private function getComposedFrame(array $frameHead, string $payload, int $payloadLength, bool $masked)
166
    {
167
        // convert frame-head to string:
168
        foreach (array_keys($frameHead) as $i) {
169
            $frameHead[$i] = chr($frameHead[$i]);
170
        }
171
172
        // generate a random mask:
173
        $mask = [];
174
        if ($masked === true) {
175 View Code Duplication
            for ($i = 0; $i < 4; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
176
                $mask[$i] = chr(random_int(0, self::MASK_255));
177
            }
178
179
            $frameHead = array_merge($frameHead, $mask);
180
        }
181
        $frame = implode('', $frameHead);
182
183
        // append payload to frame:
184 View Code Duplication
        for ($i = 0; $i < $payloadLength; $i++) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
185
            $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
186
        }
187
188
        return $frame;
189
    }
190
191
    /**
192
     * Gets unique socket id from resource
193
     *
194
     * @return int
195
     */
196
    public function getUniqueSocketId(): int
197
    {
198
        return (int)$this->socketConnection;
199
    }
200
201
    /**
202
     *  Gets client socket address host/port or UNIX path
203
     *
204
     * @return string
205
     */
206
    public function getPeerName(): string
207
    {
208
        return stream_socket_get_name($this->socketConnection, true);
209
    }
210
}
211