Socket.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. <?php
  2. namespace Socket\Raw;
  3. /**
  4. * Simple and lightweight OOP wrapper for the low-level sockets extension (ext-sockets)
  5. *
  6. * @author clue
  7. * @link https://github.com/clue/php-socket-raw
  8. */
  9. class Socket
  10. {
  11. /**
  12. * reference to actual socket resource
  13. *
  14. * @var \Socket|resource
  15. */
  16. private $resource;
  17. /**
  18. * instanciate socket wrapper for given socket resource
  19. *
  20. * should usually not be called manually, see Factory
  21. *
  22. * @param \Socket|resource $resource
  23. * @see Factory as the preferred (and simplest) way to construct socket instances
  24. */
  25. public function __construct($resource)
  26. {
  27. $this->resource = $resource;
  28. }
  29. /**
  30. * get actual socket resource
  31. *
  32. * @return \Socket|resource returns the socket resource (a `Socket` object as of PHP 8)
  33. */
  34. public function getResource()
  35. {
  36. return $this->resource;
  37. }
  38. /**
  39. * accept an incomming connection on this listening socket
  40. *
  41. * @return \Socket\Raw\Socket new connected socket used for communication
  42. * @throws Exception on error, if this is not a listening socket or there's no connection pending
  43. * @throws \Error PHP 8 only: throws \Error when socket is invalid
  44. * @see self::selectRead() to check if this listening socket can accept()
  45. * @see Factory::createServer() to create a listening socket
  46. * @see self::listen() has to be called first
  47. * @uses socket_accept()
  48. */
  49. public function accept()
  50. {
  51. $resource = @socket_accept($this->resource);
  52. if ($resource === false) {
  53. throw Exception::createFromGlobalSocketOperation();
  54. }
  55. return new Socket($resource);
  56. }
  57. /**
  58. * binds a name/address/path to this socket
  59. *
  60. * has to be called before issuing connect() or listen()
  61. *
  62. * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
  63. * @return self $this (chainable)
  64. * @throws Exception on error
  65. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  66. * @uses socket_bind()
  67. */
  68. public function bind($address)
  69. {
  70. $ret = @socket_bind($this->resource, $this->unformatAddress($address, $port), $port);
  71. if ($ret === false) {
  72. throw Exception::createFromSocketResource($this->resource);
  73. }
  74. return $this;
  75. }
  76. /**
  77. * close this socket
  78. *
  79. * ATTENTION: make sure to NOT re-use this socket instance after closing it!
  80. * its socket resource remains closed and most further operations will fail!
  81. *
  82. * @return self $this (chainable)
  83. * @throws \Error PHP 8 only: throws \Error when socket is invalid
  84. * @see self::shutdown() should be called before closing socket
  85. * @uses socket_close()
  86. */
  87. public function close()
  88. {
  89. socket_close($this->resource);
  90. return $this;
  91. }
  92. /**
  93. * initiate a connection to given address
  94. *
  95. * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
  96. * @return self $this (chainable)
  97. * @throws Exception on error
  98. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  99. * @uses socket_connect()
  100. */
  101. public function connect($address)
  102. {
  103. $ret = @socket_connect($this->resource, $this->unformatAddress($address, $port), $port);
  104. if ($ret === false) {
  105. throw Exception::createFromSocketResource($this->resource);
  106. }
  107. return $this;
  108. }
  109. /**
  110. * Initiates a new connection to given address, wait for up to $timeout seconds
  111. *
  112. * The given $timeout parameter is an upper bound, a maximum time to wait
  113. * for the connection to be either accepted or rejected.
  114. *
  115. * The resulting socket resource will be set to non-blocking mode,
  116. * regardless of its previous state and whether this method succedes or
  117. * if it fails. Make sure to reset with `setBlocking(true)` if you want to
  118. * continue using blocking calls.
  119. *
  120. * @param string $address either of IPv4:port, hostname:port, [IPv6]:port, unix-path
  121. * @param float $timeout maximum time to wait (in seconds)
  122. * @return self $this (chainable)
  123. * @throws Exception on error
  124. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  125. * @uses self::setBlocking() to enable non-blocking mode
  126. * @uses self::connect() to initiate the connection
  127. * @uses self::selectWrite() to wait for the connection to complete
  128. * @uses self::assertAlive() to check connection state
  129. */
  130. public function connectTimeout($address, $timeout)
  131. {
  132. $this->setBlocking(false);
  133. try {
  134. // socket is non-blocking, so connect should emit EINPROGRESS
  135. $this->connect($address);
  136. // socket is already connected immediately?
  137. return $this;
  138. } catch (Exception $e) {
  139. // non-blocking connect() should be EINPROGRESS (or EWOULDBLOCK on Windows) => otherwise re-throw
  140. if ($e->getCode() !== SOCKET_EINPROGRESS && $e->getCode() !== SOCKET_EWOULDBLOCK) {
  141. throw $e;
  142. }
  143. // connection should be completed (or rejected) within timeout: socket becomes writable on success or error
  144. // Windows requires special care because it uses exceptfds for socket errors: https://github.com/reactphp/event-loop/issues/206
  145. $r = null;
  146. $w = array($this->resource);
  147. $e = DIRECTORY_SEPARATOR === '\\' ? $w : null;
  148. $ret = @socket_select($r, $w, $e, $timeout === null ? null : (int) $timeout, (int) (($timeout - floor($timeout)) * 1000000));
  149. if ($ret === false) {
  150. throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
  151. } elseif ($ret === 0) {
  152. throw new Exception('Timed out while waiting for connection', SOCKET_ETIMEDOUT);
  153. }
  154. // confirm connection success (or fail if connected has been rejected)
  155. $this->assertAlive();
  156. return $this;
  157. }
  158. }
  159. /**
  160. * get socket option
  161. *
  162. * @param int $level
  163. * @param int $optname
  164. * @return mixed
  165. * @throws Exception on error
  166. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  167. * @uses socket_get_option()
  168. */
  169. public function getOption($level, $optname)
  170. {
  171. $value = @socket_get_option($this->resource, $level, $optname);
  172. if ($value === false) {
  173. throw Exception::createFromSocketResource($this->resource);
  174. }
  175. return $value;
  176. }
  177. /**
  178. * get remote side's address/path
  179. *
  180. * @return string
  181. * @throws Exception on error
  182. * @throws \Error PHP 8 only: throws \Error when socket is invalid
  183. * @uses socket_getpeername()
  184. */
  185. public function getPeerName()
  186. {
  187. $ret = @socket_getpeername($this->resource, $address, $port);
  188. if ($ret === false) {
  189. throw Exception::createFromSocketResource($this->resource);
  190. }
  191. return $this->formatAddress($address, $port);
  192. }
  193. /**
  194. * get local side's address/path
  195. *
  196. * @return string
  197. * @throws Exception on error
  198. * @throws \Error PHP 8 only: throws \Error when socket is invalid
  199. * @uses socket_getsockname()
  200. */
  201. public function getSockName()
  202. {
  203. $ret = @socket_getsockname($this->resource, $address, $port);
  204. if ($ret === false) {
  205. throw Exception::createFromSocketResource($this->resource);
  206. }
  207. return $this->formatAddress($address, $port);
  208. }
  209. /**
  210. * start listen for incoming connections
  211. *
  212. * @param int $backlog maximum number of incoming connections to be queued
  213. * @return self $this (chainable)
  214. * @throws Exception on error
  215. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  216. * @see self::bind() has to be called first to bind name to socket
  217. * @uses socket_listen()
  218. */
  219. public function listen($backlog = 0)
  220. {
  221. $ret = @socket_listen($this->resource, $backlog);
  222. if ($ret === false) {
  223. throw Exception::createFromSocketResource($this->resource);
  224. }
  225. return $this;
  226. }
  227. /**
  228. * read up to $length bytes from connect()ed / accept()ed socket
  229. *
  230. * The $type parameter specifies if this should use either binary safe reading
  231. * (PHP_BINARY_READ, the default) or stop at CR or LF characters (PHP_NORMAL_READ)
  232. *
  233. * @param int $length maximum length to read
  234. * @param int $type either of PHP_BINARY_READ (the default) or PHP_NORMAL_READ
  235. * @return string
  236. * @throws Exception on error
  237. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  238. * @see self::recv() if you need to pass flags
  239. * @uses socket_read()
  240. */
  241. public function read($length, $type = PHP_BINARY_READ)
  242. {
  243. $data = @socket_read($this->resource, $length, $type);
  244. if ($data === false) {
  245. throw Exception::createFromSocketResource($this->resource);
  246. }
  247. return $data;
  248. }
  249. /**
  250. * receive up to $length bytes from connect()ed / accept()ed socket
  251. *
  252. * @param int $length maximum length to read
  253. * @param int $flags
  254. * @return string
  255. * @throws Exception on error
  256. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  257. * @see self::read() if you do not need to pass $flags
  258. * @see self::recvFrom() if your socket is not connect()ed
  259. * @uses socket_recv()
  260. */
  261. public function recv($length, $flags)
  262. {
  263. $ret = @socket_recv($this->resource, $buffer, $length, $flags);
  264. if ($ret === false) {
  265. throw Exception::createFromSocketResource($this->resource);
  266. }
  267. return $buffer;
  268. }
  269. /**
  270. * receive up to $length bytes from socket
  271. *
  272. * @param int $length maximum length to read
  273. * @param int $flags
  274. * @param string $remote reference will be filled with remote/peer address/path
  275. * @return string
  276. * @throws Exception on error
  277. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  278. * @see self::recv() if your socket is connect()ed
  279. * @uses socket_recvfrom()
  280. */
  281. public function recvFrom($length, $flags, &$remote)
  282. {
  283. $ret = @socket_recvfrom($this->resource, $buffer, $length, $flags, $address, $port);
  284. if ($ret === false) {
  285. throw Exception::createFromSocketResource($this->resource);
  286. }
  287. $remote = $this->formatAddress($address, $port);
  288. return $buffer;
  289. }
  290. /**
  291. * check socket to see if a read/recv/revFrom will not block
  292. *
  293. * @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
  294. * @return boolean true = socket ready (read will not block), false = timeout expired, socket is not ready
  295. * @throws Exception on error
  296. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  297. * @uses socket_select()
  298. */
  299. public function selectRead($sec = 0)
  300. {
  301. $usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
  302. $r = array($this->resource);
  303. $n = null;
  304. $ret = @socket_select($r, $n, $n, $sec === null ? null : (int) $sec, $usec);
  305. if ($ret === false) {
  306. throw Exception::createFromGlobalSocketOperation('Failed to select socket for reading');
  307. }
  308. return !!$ret;
  309. }
  310. /**
  311. * check socket to see if a write/send/sendTo will not block
  312. *
  313. * @param float|null $sec maximum time to wait (in seconds), 0 = immediate polling, null = no limit
  314. * @return boolean true = socket ready (write will not block), false = timeout expired, socket is not ready
  315. * @throws Exception on error
  316. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  317. * @uses socket_select()
  318. */
  319. public function selectWrite($sec = 0)
  320. {
  321. $usec = $sec === null ? 0 : (int) (($sec - floor($sec)) * 1000000);
  322. $w = array($this->resource);
  323. $n = null;
  324. $ret = @socket_select($n, $w, $n, $sec === null ? null : (int) $sec, $usec);
  325. if ($ret === false) {
  326. throw Exception::createFromGlobalSocketOperation('Failed to select socket for writing');
  327. }
  328. return !!$ret;
  329. }
  330. /**
  331. * send given $buffer to connect()ed / accept()ed socket
  332. *
  333. * @param string $buffer
  334. * @param int $flags
  335. * @return int number of bytes actually written (make sure to check against given buffer length!)
  336. * @throws Exception on error
  337. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  338. * @see self::write() if you do not need to pass $flags
  339. * @see self::sendTo() if your socket is not connect()ed
  340. * @uses socket_send()
  341. */
  342. public function send($buffer, $flags)
  343. {
  344. $ret = @socket_send($this->resource, $buffer, strlen($buffer), $flags);
  345. if ($ret === false) {
  346. throw Exception::createFromSocketResource($this->resource);
  347. }
  348. return $ret;
  349. }
  350. /**
  351. * send given $buffer to socket
  352. *
  353. * @param string $buffer
  354. * @param int $flags
  355. * @param string $remote remote/peer address/path
  356. * @return int number of bytes actually written
  357. * @throws Exception on error
  358. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  359. * @see self::send() if your socket is connect()ed
  360. * @uses socket_sendto()
  361. */
  362. public function sendTo($buffer, $flags, $remote)
  363. {
  364. $ret = @socket_sendto($this->resource, $buffer, strlen($buffer), $flags, $this->unformatAddress($remote, $port), $port);
  365. if ($ret === false) {
  366. throw Exception::createFromSocketResource($this->resource);
  367. }
  368. return $ret;
  369. }
  370. /**
  371. * enable/disable blocking/nonblocking mode (O_NONBLOCK flag)
  372. *
  373. * @param boolean $toggle
  374. * @return self $this (chainable)
  375. * @throws Exception on error
  376. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  377. * @uses socket_set_block()
  378. * @uses socket_set_nonblock()
  379. */
  380. public function setBlocking($toggle = true)
  381. {
  382. $ret = $toggle ? @socket_set_block($this->resource) : @socket_set_nonblock($this->resource);
  383. if ($ret === false) {
  384. throw Exception::createFromSocketResource($this->resource);
  385. }
  386. return $this;
  387. }
  388. /**
  389. * set socket option
  390. *
  391. * @param int $level
  392. * @param int $optname
  393. * @param mixed $optval
  394. * @return self $this (chainable)
  395. * @throws Exception on error
  396. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  397. * @see self::getOption()
  398. * @uses socket_set_option()
  399. */
  400. public function setOption($level, $optname, $optval)
  401. {
  402. $ret = @socket_set_option($this->resource, $level, $optname, $optval);
  403. if ($ret === false) {
  404. throw Exception::createFromSocketResource($this->resource);
  405. }
  406. return $this;
  407. }
  408. /**
  409. * shuts down socket for receiving, sending or both
  410. *
  411. * @param int $how 0 = shutdown reading, 1 = shutdown writing, 2 = shutdown reading and writing
  412. * @return self $this (chainable)
  413. * @throws Exception on error
  414. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  415. * @see self::close()
  416. * @uses socket_shutdown()
  417. */
  418. public function shutdown($how = 2)
  419. {
  420. $ret = @socket_shutdown($this->resource, $how);
  421. if ($ret === false) {
  422. throw Exception::createFromSocketResource($this->resource);
  423. }
  424. return $this;
  425. }
  426. /**
  427. * write $buffer to connect()ed / accept()ed socket
  428. *
  429. * @param string $buffer
  430. * @return int number of bytes actually written
  431. * @throws Exception on error
  432. * @throws \Error PHP 8 only: throws \Error when socket or arguments are invalid
  433. * @see self::send() if you need to pass flags
  434. * @uses socket_write()
  435. */
  436. public function write($buffer)
  437. {
  438. $ret = @socket_write($this->resource, $buffer);
  439. if ($ret === false) {
  440. throw Exception::createFromSocketResource($this->resource);
  441. }
  442. return $ret;
  443. }
  444. /**
  445. * get socket type as passed to socket_create()
  446. *
  447. * @return int usually either SOCK_STREAM or SOCK_DGRAM
  448. * @throws Exception on error
  449. * @throws \Error PHP 8 only: throws \Error when socket is invalid
  450. * @uses self::getOption()
  451. */
  452. public function getType()
  453. {
  454. return $this->getOption(SOL_SOCKET, SO_TYPE);
  455. }
  456. /**
  457. * assert that this socket is alive and its error code is 0
  458. *
  459. * This will fetch and reset the current socket error code from the
  460. * socket and options and will throw an Exception along with error
  461. * message and code if the code is not 0, i.e. if it does indicate
  462. * an error situation.
  463. *
  464. * Calling this method should not be needed in most cases and is
  465. * likely to not throw an Exception. Each socket operation like
  466. * connect(), send(), etc. will throw a dedicated Exception in case
  467. * of an error anyway.
  468. *
  469. * @return self $this (chainable)
  470. * @throws Exception if error code is not 0
  471. * @throws \Error PHP 8 only: throws \Error when socket is invalid
  472. * @uses self::getOption() to retrieve and clear current error code
  473. * @uses self::getErrorMessage() to translate error code to
  474. */
  475. public function assertAlive()
  476. {
  477. $code = $this->getOption(SOL_SOCKET, SO_ERROR);
  478. if ($code !== 0) {
  479. throw Exception::createFromCode($code, 'Socket error');
  480. }
  481. return $this;
  482. }
  483. /**
  484. * format given address/host/path and port
  485. *
  486. * @param string $address
  487. * @param int $port
  488. * @return string
  489. */
  490. protected function formatAddress($address, $port)
  491. {
  492. if ($port !== 0) {
  493. if (strpos($address, ':') !== false) {
  494. $address = '[' . $address . ']';
  495. }
  496. $address .= ':' . $port;
  497. }
  498. return $address;
  499. }
  500. /**
  501. * format given address by splitting it into returned address and port set by reference
  502. *
  503. * @param string $address
  504. * @param int $port
  505. * @return string address with port removed
  506. */
  507. protected function unformatAddress($address, &$port)
  508. {
  509. // [::1]:2 => ::1 2
  510. // test:2 => test 2
  511. // ::1 => ::1
  512. // test => test
  513. $colon = strrpos($address, ':');
  514. // there is a colon and this is the only colon or there's a closing IPv6 bracket right before it
  515. if ($colon !== false && (strpos($address, ':') === $colon || strpos($address, ']') === ($colon - 1))) {
  516. $port = (int)substr($address, $colon + 1);
  517. $address = substr($address, 0, $colon);
  518. // remove IPv6 square brackets
  519. if (substr($address, 0, 1) === '[') {
  520. $address = substr($address, 1, -1);
  521. }
  522. }
  523. return $address;
  524. }
  525. }