GameQ.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. <?php
  2. /**
  3. * This file is part of GameQ.
  4. *
  5. * GameQ is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU Lesser General Public License as published by
  7. * the Free Software Foundation; either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * GameQ is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Lesser General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Lesser General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. /*
  19. * Init some stuff
  20. */
  21. // Figure out where we are so we can set the proper references
  22. define('GAMEQ_BASE', realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR);
  23. // Define the autoload so we can require files easy
  24. spl_autoload_register(array('GameQ', 'auto_load'));
  25. /**
  26. * Base GameQ Class
  27. *
  28. * This class should be the only one that is included when you use GameQ to query
  29. * any games servers. All necessary sub-classes are loaded as needed.
  30. *
  31. * Requirements: See wiki or README for more information on the requirements
  32. * - PHP 5.2+ (Recommended 5.3+)
  33. * * Bzip2 - http://www.php.net/manual/en/book.bzip2.php
  34. * * Zlib - http://www.php.net/manual/en/book.zlib.php
  35. *
  36. * @author Austin Bischoff <[email protected]>
  37. */
  38. class GameQ
  39. {
  40. /*
  41. * Constants
  42. */
  43. const VERSION = '2.0.4';
  44. /*
  45. * Server array keys
  46. */
  47. const SERVER_TYPE = 'type';
  48. const SERVER_HOST = 'host';
  49. const SERVER_ID = 'id';
  50. const SERVER_OPTIONS = 'options';
  51. /* Static Section */
  52. protected static $instance = NULL;
  53. /**
  54. * Create a new instance of this class
  55. */
  56. public static function factory()
  57. {
  58. // Create a new instance
  59. self::$instance = new self();
  60. // Return this new instance
  61. return self::$instance;
  62. }
  63. /**
  64. * Attempt to auto-load a class based on the name
  65. *
  66. * @param string $class
  67. * @throws GameQException
  68. */
  69. public static function auto_load($class)
  70. {
  71. try
  72. {
  73. // Transform the class name into a path
  74. $file = str_replace('_', '/', strtolower($class));
  75. // Find the file and return the full path, if it exists
  76. if ($path = self::find_file($file))
  77. {
  78. // Load the class file
  79. require $path;
  80. // Class has been found
  81. return TRUE;
  82. }
  83. // Class is not in the filesystem
  84. return FALSE;
  85. }
  86. catch (Exception $e)
  87. {
  88. throw new GameQException($e->getMessage(), $e->getCode(), $e);
  89. die;
  90. }
  91. }
  92. /**
  93. * Try to find the file based on the class passed.
  94. *
  95. * @param string $file
  96. */
  97. public static function find_file($file)
  98. {
  99. $found = FALSE; // By default we did not find anything
  100. // Create a partial path of the filename
  101. $path = GAMEQ_BASE.$file.'.php';
  102. // Is a file so we can include it
  103. if(is_file($path))
  104. {
  105. $found = $path;
  106. }
  107. return $found;
  108. }
  109. /* Dynamic Section */
  110. /**
  111. * Defined options by default
  112. *
  113. * @var array()
  114. */
  115. protected $options = array(
  116. 'debug' => FALSE,
  117. 'timeout' => 3, // Seconds
  118. 'filters' => array(),
  119. // Advanced settings
  120. 'stream_timeout' => 200000, // See http://www.php.net/manual/en/function.stream-select.php for more info
  121. 'write_wait' => 500, // How long (in micro-seconds) to pause between writting to server sockets, helps cpu usage
  122. );
  123. /**
  124. * Array of servers being queried
  125. *
  126. * @var array
  127. */
  128. protected $servers = array();
  129. /**
  130. * Holds the list of active sockets. This array is automaically cleaned as needed
  131. *
  132. * @var array
  133. */
  134. protected $sockets = array();
  135. /**
  136. * Make new class and check for requirements
  137. *
  138. * @throws GameQException
  139. * @return boolean
  140. */
  141. public function __construct()
  142. {
  143. // @todo: Add PHP version check?
  144. }
  145. /**
  146. * Get an option's value
  147. *
  148. * @param string $option
  149. */
  150. public function __get($option)
  151. {
  152. return isset($this->options[$option]) ? $this->options[$option] : NULL;
  153. }
  154. /**
  155. * Set an option's value
  156. *
  157. * @param string $option
  158. * @param mixed $value
  159. * @return boolean
  160. */
  161. public function __set($option, $value)
  162. {
  163. $this->options[$option] = $value;
  164. return TRUE;
  165. }
  166. /**
  167. * Chainable call to __set, uses set as the actual setter
  168. *
  169. * @param string $var
  170. * @param mixed $value
  171. * @return GameQ
  172. */
  173. public function setOption($var, $value)
  174. {
  175. // Use magic
  176. $this->{$var} = $value;
  177. return $this; // Make chainable
  178. }
  179. /**
  180. * Set an output filter.
  181. *
  182. * @param string $name
  183. * @param array $params
  184. * @return GameQ
  185. */
  186. public function setFilter($name, $params = array())
  187. {
  188. // Create the proper filter class name
  189. $filter_class = 'GameQ_Filters_'.$name;
  190. try
  191. {
  192. // Pass any parameters and make the class
  193. $this->options['filters'][$name] = new $filter_class($params);
  194. }
  195. catch (GameQ_FiltersException $e)
  196. {
  197. // We catch the exception here, thus the filter is not applied
  198. // but we issue a warning
  199. error_log($e->getMessage(), E_USER_WARNING);
  200. }
  201. return $this; // Make chainable
  202. }
  203. /**
  204. * Remove a global output filter.
  205. *
  206. * @param string $name
  207. * @return GameQ
  208. */
  209. public function removeFilter($name)
  210. {
  211. unset($this->options['filters'][$name]);
  212. return $this; // Make chainable
  213. }
  214. /**
  215. * Add a server to be queried
  216. *
  217. * Example:
  218. * $this->addServer(array(
  219. * // Required keys
  220. * 'type' => 'cs',
  221. * 'host' => '127.0.0.1:27015', '127.0.0.1' or 'somehost.com:27015'
  222. * Port not required, but will use the default port in the class which may not be correct for the
  223. * specific server being queried.
  224. *
  225. * // Optional keys
  226. * 'id' => 'someServerId', // By default will use pased host info (i.e. 127.0.0.1:27015)
  227. * 'options' => array('timeout' => 5), // By default will use global options
  228. * ));
  229. *
  230. * @param array $server_info
  231. * @throws GameQException
  232. * @return boolean|GameQ
  233. */
  234. public function addServer(Array $server_info=NULL)
  235. {
  236. // Check for server type
  237. if(!key_exists(self::SERVER_TYPE, $server_info) || empty($server_info[self::SERVER_TYPE]))
  238. {
  239. throw new GameQException("Missing server info key '".self::SERVER_TYPE."'");
  240. return FALSE;
  241. }
  242. // Check for server host
  243. if(!key_exists(self::SERVER_HOST, $server_info) || empty($server_info[self::SERVER_HOST]))
  244. {
  245. throw new GameQException("Missing server info key '".self::SERVER_HOST."'");
  246. return FALSE;
  247. }
  248. // Check for server id
  249. if(!key_exists(self::SERVER_ID, $server_info) || empty($server_info[self::SERVER_ID]))
  250. {
  251. // Make an id so each server has an id when returned
  252. $server_info[self::SERVER_ID] = $server_info[self::SERVER_HOST];
  253. }
  254. // Check for options
  255. if(!key_exists(self::SERVER_OPTIONS, $server_info)
  256. || !is_array($server_info[self::SERVER_OPTIONS])
  257. || empty($server_info[self::SERVER_OPTIONS]))
  258. {
  259. // Default the options to an empty array
  260. $server_info[self::SERVER_OPTIONS] = array();
  261. }
  262. // Define these
  263. $server_id = $server_info[self::SERVER_ID];
  264. $server_ip = '127.0.0.1';
  265. $server_port = FALSE;
  266. // We have an IPv6 address (and maybe a port)
  267. if(substr_count($server_info[self::SERVER_HOST], ':') > 1)
  268. {
  269. // See if we have a port, input should be in the format [::1]:27015 or similar
  270. if(strstr($server_info[self::SERVER_HOST], ']:'))
  271. {
  272. // Explode to get port
  273. $server_addr = explode(':', $server_info[self::SERVER_HOST]);
  274. // Port is the last item in the array, remove it and save
  275. $server_port = array_pop($server_addr);
  276. // The rest is the address, recombine
  277. $server_ip = implode(':', $server_addr);
  278. unset($server_addr);
  279. }
  280. // Just the IPv6 address, no port defined
  281. else
  282. {
  283. $server_ip = $server_info[self::SERVER_HOST];
  284. }
  285. // Now let's validate the IPv6 value sent, remove the square brackets ([]) first
  286. if(!filter_var(trim($server_ip, '[]'), FILTER_VALIDATE_IP, array(
  287. 'flags' => FILTER_FLAG_IPV6,
  288. )))
  289. {
  290. throw new GameQException("The IPv6 address '{$server_ip}' is invalid.");
  291. return FALSE;
  292. }
  293. }
  294. // IPv4
  295. else
  296. {
  297. // We have a port defined
  298. if(strstr($server_info[self::SERVER_HOST], ':'))
  299. {
  300. list($server_ip, $server_port) = explode(':', $server_info[self::SERVER_HOST]);
  301. }
  302. // No port, just IPv4
  303. else
  304. {
  305. $server_ip = $server_info[self::SERVER_HOST];
  306. }
  307. // Validate the IPv4 value, if FALSE is not a valid IP, maybe a hostname. Try to resolve
  308. if(!filter_var($server_ip, FILTER_VALIDATE_IP, array(
  309. 'flags' => FILTER_FLAG_IPV4,
  310. )))
  311. {
  312. // When gethostbyname() fails it returns the original string
  313. // so if ip and the result from gethostbyname() are equal this failed.
  314. if($server_ip === gethostbyname($server_ip))
  315. {
  316. throw new GameQException("The host '{$server_ip}' is unresolvable to an IP address.");
  317. return FALSE;
  318. }
  319. }
  320. }
  321. // Create the class so we can reference it properly later
  322. $protocol_class = 'GameQ_Protocols_'.ucfirst($server_info[self::SERVER_TYPE]);
  323. // Create the new instance and add it to the servers list
  324. $this->servers[$server_id] = new $protocol_class(
  325. $server_ip,
  326. $server_port,
  327. array_merge($this->options, $server_info[self::SERVER_OPTIONS])
  328. );
  329. return $this; // Make calls chainable
  330. }
  331. /**
  332. * Add multiple servers at once
  333. *
  334. * @param array $servers
  335. * @return GameQ
  336. */
  337. public function addServers(Array $servers=NULL)
  338. {
  339. // Loop thru all the servers and add them
  340. foreach($servers AS $server_info)
  341. {
  342. $this->addServer($server_info);
  343. }
  344. return $this; // Make calls chainable
  345. }
  346. /**
  347. * Clear all the added servers. Creates clean instance.
  348. *
  349. * @return GameQ
  350. */
  351. public function clearServers()
  352. {
  353. // Reset all the servers
  354. $this->servers = array();
  355. $this->sockets = array();
  356. return $this; // Make Chainable
  357. }
  358. /**
  359. * Make all the data requests (i.e. challenges, queries, etc...)
  360. *
  361. * @return multitype:Ambigous <multitype:, multitype:boolean string mixed >
  362. */
  363. public function requestData()
  364. {
  365. // Data returned array
  366. $data = array();
  367. // Init the query array
  368. $queries = array(
  369. 'multi' => array(
  370. 'challenges' => array(),
  371. 'info' => array(),
  372. ),
  373. 'linear' => array(),
  374. );
  375. // Loop thru all of the servers added and categorize them
  376. foreach($this->servers AS $server_id => $instance)
  377. {
  378. // Check to see what kind of server this is and how we can send packets
  379. if($instance->packet_mode() == GameQ_Protocols::PACKET_MODE_LINEAR)
  380. {
  381. $queries['linear'][$server_id] = $instance;
  382. }
  383. else // We can send this out in a multi request
  384. {
  385. // Check to see if we should issue a challenge first
  386. if($instance->hasChallenge())
  387. {
  388. $queries['multi']['challenges'][$server_id] = $instance;
  389. }
  390. // Add this instance to do info query
  391. $queries['multi']['info'][$server_id] = $instance;
  392. }
  393. }
  394. // First lets do the faster, multi queries
  395. if(count($queries['multi']['info']) > 0)
  396. {
  397. $this->requestMulti($queries['multi']);
  398. }
  399. // Now lets do the slower linear queries.
  400. if(count($queries['linear']) > 0)
  401. {
  402. $this->requestLinear($queries['linear']);
  403. }
  404. // Now let's loop the servers and process the response data
  405. foreach($this->servers AS $server_id => $instance)
  406. {
  407. // Lets process this and filter
  408. $data[$server_id] = $this->filterResponse($instance);
  409. }
  410. // Send back the data array, could be empty if nothing went to plan
  411. return $data;
  412. }
  413. /* Working Methods */
  414. /**
  415. * Apply all set filters to the data returned by gameservers.
  416. *
  417. * @param GameQ_Protocols $protocol_instance
  418. * @return array
  419. */
  420. protected function filterResponse(GameQ_Protocols $protocol_instance)
  421. {
  422. // Let's pull out the "raw" data we are going to filter
  423. $data = $protocol_instance->processResponse();
  424. // Loop each of the filters we have attached
  425. foreach($this->options['filters'] AS $filter_name => $filter_instance)
  426. {
  427. // Overwrite the data with the "filtered" data
  428. $data = $filter_instance->filter($data, $protocol_instance);
  429. }
  430. return $data;
  431. }
  432. /**
  433. * Process "linear" servers. Servers that do not support multiple packet calls at once. So Slow!
  434. * This method also blocks the socket, you have been warned!!
  435. *
  436. * @param array $servers
  437. * @return boolean
  438. */
  439. protected function requestLinear($servers=array())
  440. {
  441. // Loop thru all the linear servers
  442. foreach($servers AS $server_id => $instance)
  443. {
  444. // First we need to get a socket and we need to block because this is linear
  445. if(($socket = $this->socket_open($instance, TRUE)) === FALSE)
  446. {
  447. // Skip it
  448. continue;
  449. }
  450. // Socket id
  451. $socket_id = (int) $socket;
  452. // See if we have challenges to send off
  453. if($instance->hasChallenge())
  454. {
  455. // Now send off the challenge packet
  456. fwrite($socket, $instance->getPacket('challenge'));
  457. // Read in the challenge response
  458. $instance->challengeResponse(array(fread($socket, 4096)));
  459. // Now we need to parse and apply the challenge response to all the packets that require it
  460. $instance->challengeVerifyAndParse();
  461. }
  462. // Invoke the beforeSend method
  463. $instance->beforeSend();
  464. // Grab the packets we need to send, minus the challenge packet
  465. $packets = $instance->getPacket('!challenge');
  466. // Now loop the packets, begin the slowness
  467. foreach($packets AS $packet_type => $packet)
  468. {
  469. // Add the socket information so we can retreive it easily
  470. $this->sockets = array(
  471. $socket_id => array(
  472. 'server_id' => $server_id,
  473. 'packet_type' => $packet_type,
  474. 'socket' => $socket,
  475. )
  476. );
  477. // Write the packet
  478. fwrite($socket, $packet);
  479. // Get the responses from the query
  480. $responses = $this->sockets_listen();
  481. // Lets look at our responses
  482. foreach($responses AS $socket_id => $response)
  483. {
  484. // Save the response from this packet
  485. $instance->packetResponse($packet_type, $response);
  486. }
  487. }
  488. }
  489. // Now close all the socket(s) and clean up any data
  490. $this->sockets_close();
  491. return TRUE;
  492. }
  493. /**
  494. * Process the servers that support multi requests. That means multiple packets can be sent out at once.
  495. *
  496. * @param array $servers
  497. * @return boolean
  498. */
  499. protected function requestMulti($servers=array())
  500. {
  501. // See if we have any challenges to send off
  502. if(count($servers['challenges']) > 0)
  503. {
  504. // Now lets send off all the challenges
  505. $this->sendChallenge($servers['challenges']);
  506. // Now let's process the challenges
  507. // Loop thru all the instances
  508. foreach($servers['challenges'] AS $server_id => $instance)
  509. {
  510. $instance->challengeVerifyAndParse();
  511. }
  512. }
  513. // Send out all the query packets to get data for
  514. $this->queryServerInfo($servers['info']);
  515. return TRUE;
  516. }
  517. /**
  518. * Send off needed challenges and get the response
  519. *
  520. * @param array $instances
  521. * @return boolean
  522. */
  523. protected function sendChallenge(Array $instances=NULL)
  524. {
  525. // Loop thru all the instances we need to send out challenges for
  526. foreach($instances AS $server_id => $instance)
  527. {
  528. // Make a new socket
  529. if(($socket = $this->socket_open($instance)) === FALSE)
  530. {
  531. // Skip it
  532. continue;
  533. }
  534. // Now write the challenge packet to the socket.
  535. fwrite($socket, $instance->getPacket(GameQ_Protocols::PACKET_CHALLENGE));
  536. // Add the socket information so we can retreive it easily
  537. $this->sockets[(int) $socket] = array(
  538. 'server_id' => $server_id,
  539. 'packet_type' => GameQ_Protocols::PACKET_CHALLENGE,
  540. 'socket' => $socket,
  541. );
  542. // Let's sleep shortly so we are not hammering out calls rapid fire style hogging cpu
  543. usleep($this->write_wait);
  544. }
  545. // Now we need to listen for challenge response(s)
  546. $responses = $this->sockets_listen();
  547. // Lets look at our responses
  548. foreach($responses AS $socket_id => $response)
  549. {
  550. // Back out the server_id we need to update the challenge response for
  551. $server_id = $this->sockets[$socket_id]['server_id'];
  552. // Now set the proper response for the challenge because we will need it later
  553. $this->servers[$server_id]->challengeResponse($response);
  554. }
  555. // Now close all the socket(s) and clean up any data
  556. $this->sockets_close();
  557. return TRUE;
  558. }
  559. /**
  560. * Query the server for actual server information (i.e. info, players, rules, etc...)
  561. *
  562. * @param array $instances
  563. * @return boolean
  564. */
  565. protected function queryServerInfo(Array $instances=NULL)
  566. {
  567. // Loop all the server instances
  568. foreach($instances AS $server_id => $instance)
  569. {
  570. // Invoke the beforeSend method
  571. $instance->beforeSend();
  572. // Get all the non-challenge packets we need to send
  573. $packets = $instance->getPacket('!challenge');
  574. if(count($packets) == 0)
  575. {
  576. // Skip nothing else to do for some reason.
  577. continue;
  578. }
  579. // Now lets send off the packets
  580. foreach($packets AS $packet_type => $packet)
  581. {
  582. // Make a new socket
  583. if(($socket = $this->socket_open($instance)) === FALSE)
  584. {
  585. // Skip it
  586. continue;
  587. }
  588. // Now write the packet to the socket.
  589. fwrite($socket, $packet);
  590. // Add the socket information so we can retreive it easily
  591. $this->sockets[(int) $socket] = array(
  592. 'server_id' => $server_id,
  593. 'packet_type' => $packet_type,
  594. 'socket' => $socket,
  595. );
  596. // Let's sleep shortly so we are not hammering out calls raipd fire style
  597. usleep($this->write_wait);
  598. }
  599. }
  600. // Now we need to listen for packet response(s)
  601. $responses = $this->sockets_listen();
  602. // Lets look at our responses
  603. foreach($responses AS $socket_id => $response)
  604. {
  605. // Back out the server_id
  606. $server_id = $this->sockets[$socket_id]['server_id'];
  607. // Back out the packet type
  608. $packet_type = $this->sockets[$socket_id]['packet_type'];
  609. // Save the response from this packet
  610. $this->servers[$server_id]->packetResponse($packet_type, $response);
  611. }
  612. // Now close all the socket(s) and clean up any data
  613. $this->sockets_close();
  614. return TRUE;
  615. }
  616. /* Sockets/streams stuff */
  617. /**
  618. * Open a new socket based on the instance information
  619. *
  620. * @param GameQ_Protocols $instance
  621. * @param bool $blocking
  622. * @throws GameQException
  623. * @return boolean|resource
  624. */
  625. protected function socket_open(GameQ_Protocols $instance, $blocking=FALSE)
  626. {
  627. // Create the remote address
  628. $remote_addr = sprintf("%s://%s:%d", $instance->transport(), $instance->ip(), $instance->port());
  629. // Create context
  630. $context = stream_context_create(array(
  631. 'socket' => array(
  632. 'bindto' => '0:0', // Bind to any available IP and OS decided port
  633. ),
  634. ));
  635. // Create the socket
  636. if(($socket = @stream_socket_client($remote_addr, $errno = NULL, $errstr = NULL, $this->timeout, STREAM_CLIENT_CONNECT, $context)) !== FALSE)
  637. {
  638. // Set the read timeout on the streams
  639. stream_set_timeout($socket, $this->timeout);
  640. // Set blocking mode
  641. stream_set_blocking($socket, $blocking);
  642. }
  643. else // Throw an error
  644. {
  645. // Check to see if we are in debug mode, if so throw the exception
  646. if($this->debug)
  647. {
  648. throw new GameQException(__METHOD__." Error creating socket to server {$remote_addr}. Error: ".$errstr, $errno);
  649. }
  650. // We didnt create so we need to return false.
  651. return FALSE;
  652. }
  653. unset($context, $remote_addr);
  654. // return the socket
  655. return $socket;
  656. }
  657. /**
  658. * Listen to all the created sockets and return the responses
  659. *
  660. * @return array
  661. */
  662. protected function sockets_listen()
  663. {
  664. // Set the loop to active
  665. $loop_active = TRUE;
  666. // To store the responses
  667. $responses = array();
  668. // To store the sockets
  669. $sockets = array();
  670. // Loop and pull out all the actual sockets we need to listen on
  671. foreach($this->sockets AS $socket_id => $socket_data)
  672. {
  673. // Append the actual socket we are listening to
  674. $sockets[$socket_id] = $socket_data['socket'];
  675. }
  676. // Init some variables
  677. $read = $sockets;
  678. $write = NULL;
  679. $except = NULL;
  680. // Check to see if $read is empty, if so stream_select() will throw a warning
  681. if(empty($read))
  682. {
  683. return $responses;
  684. }
  685. // This is when it should stop
  686. $time_stop = microtime(TRUE) + $this->timeout;
  687. // Let's loop until we break something.
  688. while ($loop_active && microtime(TRUE) < $time_stop)
  689. {
  690. // Now lets listen for some streams, but do not cross the streams!
  691. $streams = stream_select($read, $write, $except, 0, $this->stream_timeout);
  692. // We had error or no streams left, kill the loop
  693. if($streams === FALSE || ($streams <= 0))
  694. {
  695. $loop_active = FALSE;
  696. break;
  697. }
  698. // Loop the sockets that received data back
  699. foreach($read AS $socket)
  700. {
  701. // See if we have a response
  702. if(($response = stream_socket_recvfrom($socket, 8192)) === FALSE)
  703. {
  704. continue; // No response yet so lets continue.
  705. }
  706. // Check to see if the response is empty, if so we are done
  707. // @todo: Verify that this does not affect other protocols, added for Minequery
  708. // Initial testing showed this change did not affect any of the other protocols
  709. if(strlen($response) == 0)
  710. {
  711. // End the while loop
  712. $loop_active = FALSE;
  713. break;
  714. }
  715. // Add the response we got back
  716. $responses[(int) $socket][] = $response;
  717. }
  718. // Because stream_select modifies read we need to reset it each
  719. // time to the original array of sockets
  720. $read = $sockets;
  721. }
  722. // Free up some memory
  723. unset($streams, $read, $write, $except, $sockets, $time_stop, $response);
  724. return $responses;
  725. }
  726. /**
  727. * Close all the open sockets
  728. */
  729. protected function sockets_close()
  730. {
  731. // Loop all the existing sockets, valid or not
  732. foreach($this->sockets AS $socket_id => $data)
  733. {
  734. fclose($data['socket']);
  735. unset($this->sockets[$socket_id]);
  736. }
  737. return TRUE;
  738. }
  739. }
  740. /**
  741. * GameQ Exception Class
  742. *
  743. * Thrown when there is any kind of internal configuration error or
  744. * some unhandled or unexpected error or response.
  745. *
  746. * @author Austin Bischoff <[email protected]>
  747. */
  748. class GameQException extends Exception {}