1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Helix\Socket; |
4
|
|
|
|
5
|
|
|
use InvalidArgumentException; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Abstract parent to all sockets. |
9
|
|
|
*/ |
10
|
|
|
abstract class AbstractSocket implements SocketInterface { |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* The `SOCK_*` type constant for the class. |
14
|
|
|
* |
15
|
|
|
* @return int |
16
|
|
|
*/ |
17
|
|
|
abstract public static function getType (): int; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* The underlying PHP resource. |
21
|
|
|
* |
22
|
|
|
* @var resource |
23
|
|
|
*/ |
24
|
|
|
protected $resource; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* Creates an instance of the called class. |
28
|
|
|
* |
29
|
|
|
* @see https://php.net/socket_create |
30
|
|
|
* |
31
|
|
|
* @param int $domain `AF_*` constant. |
32
|
|
|
* @param array $extra Variadic constructor arguments. |
33
|
|
|
* @return static |
34
|
|
|
* @throws SocketError |
35
|
|
|
*/ |
36
|
|
|
public static function create (int $domain = AF_INET, ...$extra) { |
37
|
|
|
if (!$resource = @socket_create($domain, static::getType(), 0)) { // auto-protocol |
38
|
|
|
throw new SocketError; // reliable errno |
39
|
|
|
} |
40
|
|
|
return new static($resource, ...$extra); |
|
|
|
|
41
|
|
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Validates and sets the underlying socket resource. |
45
|
|
|
* |
46
|
|
|
* The resource must be open, of the correct type, and have no pending errors. |
47
|
|
|
* |
48
|
|
|
* @param resource $resource PHP socket resource. |
49
|
|
|
* @throws InvalidArgumentException Not a socket resource, or the socket is of the wrong type. |
50
|
|
|
* @throws SocketError Slippage of an existing error on the resource. |
51
|
|
|
*/ |
52
|
|
|
public function __construct ($resource) { |
53
|
|
|
if (!is_resource($resource) or get_resource_type($resource) !== 'Socket') { |
54
|
|
|
throw new InvalidArgumentException('Expected an open socket resource.', SOCKET_EBADF); |
55
|
|
|
} |
56
|
|
|
elseif (socket_get_option($resource, SOL_SOCKET, SO_TYPE) !== static::getType()) { |
57
|
|
|
throw new InvalidArgumentException('Invalid socket type for ' . static::class, SOCKET_ESOCKTNOSUPPORT); |
58
|
|
|
} |
59
|
|
|
elseif ($errno = SocketError::getLast($resource)) { |
60
|
|
|
// "File descriptor in bad state" |
61
|
|
|
throw new SocketError(SOCKET_EBADFD, 0, new SocketError($errno)); |
62
|
|
|
} |
63
|
|
|
$this->resource = $resource; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Closes the socket if it's open. |
68
|
|
|
* |
69
|
|
|
* @see close() |
70
|
|
|
*/ |
71
|
|
|
public function __destruct () { |
72
|
|
|
if ($this->isOpen()) { |
73
|
|
|
$this->close(); |
74
|
|
|
} |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Blocks until the socket becomes available on a given channel. |
79
|
|
|
* |
80
|
|
|
* Throws if the underlying resource has a pending error (non-blocking sockets only). |
81
|
|
|
* |
82
|
|
|
* @see isReady() |
83
|
|
|
* |
84
|
|
|
* @param int $channel {@link SocketInterface} channel constant. |
85
|
|
|
* @return $this |
86
|
|
|
* @throws SocketError |
87
|
|
|
*/ |
88
|
|
|
public function await (int $channel) { |
89
|
|
|
$rwe = [$channel => [$this->resource]]; |
90
|
|
|
if (!@socket_select($rwe[0], $rwe[1], $rwe[2], null)) { |
91
|
|
|
throw new SocketError($this->resource); |
92
|
|
|
} |
93
|
|
|
return $this; |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* @see await() |
98
|
|
|
* |
99
|
|
|
* @return $this |
100
|
|
|
*/ |
101
|
|
|
final public function awaitOutOfBand () { |
102
|
|
|
return $this->await(self::CH_EXCEPT); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* @see await() |
107
|
|
|
* |
108
|
|
|
* @return $this |
109
|
|
|
*/ |
110
|
|
|
final public function awaitReadable () { |
111
|
|
|
return $this->await(self::CH_READ); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @see await() |
116
|
|
|
* |
117
|
|
|
* @return $this |
118
|
|
|
*/ |
119
|
|
|
final public function awaitWritable () { |
120
|
|
|
return $this->await(self::CH_WRITE); |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Closes the underlying resource if it's open. |
125
|
|
|
* |
126
|
|
|
* @see https://php.net/socket_close |
127
|
|
|
* |
128
|
|
|
* @return $this |
129
|
|
|
*/ |
130
|
|
|
public function close () { |
131
|
|
|
if ($this->isOpen()) { |
132
|
|
|
socket_close($this->resource); |
133
|
|
|
} |
134
|
|
|
return $this; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* The `AF_*` address family constant. |
139
|
|
|
* |
140
|
|
|
* @return int |
141
|
|
|
*/ |
142
|
|
|
final public function getDomain (): int { |
143
|
|
|
return $this->getOption(39); // SO_DOMAIN is not exposed by PHP |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @return int |
148
|
|
|
*/ |
149
|
|
|
final public function getId (): int { |
150
|
|
|
return (int)$this->resource; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Retrieves an option value. |
155
|
|
|
* |
156
|
|
|
* @see https://php.net/socket_get_option |
157
|
|
|
* |
158
|
|
|
* @param int $option `SO_*` option constant. |
159
|
|
|
* @return mixed The option's value. This is never `false`. |
160
|
|
|
* @throws SocketError |
161
|
|
|
*/ |
162
|
|
|
public function getOption (int $option) { |
163
|
|
|
$value = @socket_get_option($this->resource, SOL_SOCKET, $option); |
164
|
|
|
if ($value === false) { |
165
|
|
|
throw new SocketError($this->resource, SOCKET_EINVAL); |
166
|
|
|
} |
167
|
|
|
return $value; |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* @return resource |
172
|
|
|
*/ |
173
|
|
|
final public function getResource () { |
174
|
|
|
return $this->resource; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* The local address and port, or Unix file path and port `0`. |
179
|
|
|
* |
180
|
|
|
* @see https://php.net/socket_getsockname |
181
|
|
|
* |
182
|
|
|
* @return array `[ 0 => address, 1 => port ]` |
183
|
|
|
* @throws SocketError |
184
|
|
|
*/ |
185
|
|
|
public function getSockName (): array { |
186
|
|
|
if (!@socket_getsockname($this->resource, $addr, $port)) { |
187
|
|
|
throw new SocketError($this->resource, SOCKET_EOPNOTSUPP); |
188
|
|
|
} |
189
|
|
|
return [$addr, $port]; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
/** |
193
|
|
|
* @return bool |
194
|
|
|
*/ |
195
|
|
|
public function isOpen (): bool { |
196
|
|
|
return is_resource($this->resource); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Polls for whether the socket can perform a non-blocking out-of-band read. |
201
|
|
|
* |
202
|
|
|
* @see isReady() |
203
|
|
|
* |
204
|
|
|
* @return bool |
205
|
|
|
*/ |
206
|
|
|
final public function isOutOfBand (): bool { |
207
|
|
|
return $this->isReady(self::CH_EXCEPT); |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Polls for whether the socket can perform a non-blocking read. |
212
|
|
|
* |
213
|
|
|
* @see isReady() |
214
|
|
|
* |
215
|
|
|
* @return bool |
216
|
|
|
*/ |
217
|
|
|
final public function isReadable (): bool { |
218
|
|
|
return $this->isReady(self::CH_READ); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Selects for channel availability. |
223
|
|
|
* |
224
|
|
|
* @see await() |
225
|
|
|
* |
226
|
|
|
* @param int $channel `SocketInterface` channel constant. |
227
|
|
|
* @param float|null $timeout Maximum seconds to block. `NULL` blocks forever. Defaults to a poll. |
228
|
|
|
* @return bool |
229
|
|
|
* @throws SocketError |
230
|
|
|
*/ |
231
|
|
|
public function isReady (int $channel, ?float $timeout = 0): bool { |
232
|
|
|
$rwe = [$channel => [$this->resource]]; |
233
|
|
|
// core casts non-null timeout to int. |
234
|
|
|
// usec is ignored if timeout is null. |
235
|
|
|
$usec = (int)(fmod($timeout, 1) * 1000000); |
236
|
|
|
if (false === $count = @socket_select($rwe[0], $rwe[1], $rwe[2], $timeout, $usec)) { |
237
|
|
|
throw new SocketError($this->resource); |
238
|
|
|
} |
239
|
|
|
return (bool)$count; |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Polls for whether the socket can perform a non-blocking write. |
244
|
|
|
* |
245
|
|
|
* @see isReady() |
246
|
|
|
* |
247
|
|
|
* @return bool |
248
|
|
|
*/ |
249
|
|
|
final public function isWritable (): bool { |
250
|
|
|
return $this->isReady(self::CH_WRITE); |
251
|
|
|
} |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* Enables or disables blocking. |
255
|
|
|
* |
256
|
|
|
* **Note: PHP provides no way to retrieve a socket's blocking mode.** |
257
|
|
|
* |
258
|
|
|
* Sockets are always created in blocking mode. |
259
|
|
|
* |
260
|
|
|
* If you're using a reactor, keep the sockets in blocking mode. |
261
|
|
|
* |
262
|
|
|
* Non-blocking errors are thrown when performing ordinary operations, even if unrelated to those operations. |
263
|
|
|
* This is known as error slippage. |
264
|
|
|
* To test for and clear an existing non-blocking error, use `->getOption(SO_ERROR)`. |
265
|
|
|
* |
266
|
|
|
* @see https://php.net/socket_set_block |
267
|
|
|
* @see https://php.net/socket_set_nonblock |
268
|
|
|
* |
269
|
|
|
* @param bool $blocking Whether the socket should block. |
270
|
|
|
* @return $this |
271
|
|
|
* @throws SocketError |
272
|
|
|
*/ |
273
|
|
|
public function setBlocking (bool $blocking) { |
274
|
|
|
if ($blocking ? @socket_set_block($this->resource) : @socket_set_nonblock($this->resource)) { |
275
|
|
|
return $this; |
276
|
|
|
} |
277
|
|
|
throw new SocketError($this->resource); // reliable errno |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* Sets an option on the underlying resource. |
282
|
|
|
* |
283
|
|
|
* @see https://php.net/socket_set_option |
284
|
|
|
* |
285
|
|
|
* @param int $option `SO_*` constant. |
286
|
|
|
* @param mixed $value |
287
|
|
|
* @return $this |
288
|
|
|
* @throws SocketError |
289
|
|
|
*/ |
290
|
|
|
public function setOption (int $option, $value) { |
291
|
|
|
if (!@socket_set_option($this->resource, SOL_SOCKET, $option, $value)) { |
292
|
|
|
throw new SocketError($this->resource, SOCKET_EINVAL); |
293
|
|
|
} |
294
|
|
|
return $this; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* Sets the I/O timeout length in seconds. |
299
|
|
|
* |
300
|
|
|
* @param float $timeout Zero means "no timeout" (block forever). |
301
|
|
|
* @return $this |
302
|
|
|
*/ |
303
|
|
|
public function setTimeout (float $timeout) { |
304
|
|
|
$tv = [ |
305
|
|
|
'sec' => (int)$timeout, |
306
|
|
|
'usec' => (int)(fmod($timeout, 1) * 1000000) |
307
|
|
|
]; |
308
|
|
|
$this->setOption(SO_RCVTIMEO, $tv); |
309
|
|
|
$this->setOption(SO_SNDTIMEO, $tv); |
310
|
|
|
return $this; |
311
|
|
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
|
|
* Shuts down I/O for a single channel. |
315
|
|
|
* |
316
|
|
|
* Shutting down is a formal handshake two peers can do before actually closing. |
317
|
|
|
* It's not required, but it can help assert I/O access logic. |
318
|
|
|
* |
319
|
|
|
* If writing is shut down, trying to send will throw `SOCKET_EPIPE`, |
320
|
|
|
* and the remote peer will read an empty string after receiving all pending data. |
321
|
|
|
* |
322
|
|
|
* If reading is shut down, trying to receive will return an empty string, |
323
|
|
|
* and the remote peer will get an `EPIPE` error if they try to send. |
324
|
|
|
* |
325
|
|
|
* Writing should be shut down first between two connected sockets. |
326
|
|
|
* |
327
|
|
|
* Selection always returns positive for a shut down channel, |
328
|
|
|
* and the remote peer will similarly have positive selection for the opposite channel. |
329
|
|
|
* |
330
|
|
|
* @see https://php.net/socket_shutdown |
331
|
|
|
* |
332
|
|
|
* @param int $channel `CH_READ` or `CH_WRITE` |
333
|
|
|
* @return $this |
334
|
|
|
* @throws SocketError |
335
|
|
|
*/ |
336
|
|
|
public function shutdown (int $channel) { |
337
|
|
|
if (!@socket_shutdown($this->resource, $channel)) { |
338
|
|
|
throw new SocketError($this->resource); // reliable errno |
339
|
|
|
} |
340
|
|
|
return $this; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
} |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.