socket.class.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. namespace Juju\Net;
  3. use Juju\Events;
  4. require_once(dirname(__DIR__).'/events.trait.php');
  5. class Socket {
  6. use Events;
  7. private $socket;
  8. private $clients;
  9. private $dsn;
  10. private $socket_type;
  11. private $open;
  12. const SERVER = 0;
  13. const CLIENT = 1;
  14. public static function from(string $dsnstring, bool $listen = false){
  15. return new Socket($dsnstring, $listen);
  16. }
  17. private function __construct(string $dsnstring, bool $listen = false){
  18. $parts = explode('://', $dsnstring);
  19. $dsnstring = $parts[1];
  20. $dsn = explode(';', $dsnstring);
  21. $dsn = array_reduce($dsn, function($dsn, $item){
  22. $item = explode('=', $item);
  23. $dsn[$item[0]] = $item[1];
  24. return $dsn;
  25. });
  26. $this->dsn = [
  27. 'protocol'=> $parts[0] ?? null,
  28. 'domain'=> strtoupper($dsn['domain']) ?? 'UNIX',
  29. 'type'=> strtoupper($dsn['type']) ?? 'STREAM',
  30. 'address'=> $dsn['address'] ?? '127.0.0.1',
  31. 'port'=> $dsn['port'] ?? null,
  32. 'max_connections'=> $dsn['max_connections'] ?? 5
  33. ];
  34. $this->socket_type = $listen ? static::SERVER : static::CLIENT;
  35. $this->open = false;
  36. if($listen){
  37. $this->clients = [];
  38. }
  39. }
  40. public function __get($name){
  41. switch($name){
  42. case 'socket':case 'dsn':case 'socket_type':case 'open':
  43. return $this->$name;
  44. break;
  45. case 'protocol':case 'domain':case 'type':case 'address':case 'port':case 'max_connections':
  46. return $this->dsn[$name];
  47. break;
  48. case 'error':
  49. return socket_strerror(socket_last_error());
  50. break;
  51. default:
  52. throw new \Exception("Property {$name} does not exist");
  53. }
  54. }
  55. public function __set($name, $value){
  56. throw new \Exception("Property {$name} does not exist or is read only");
  57. }
  58. private function throw_if($bool){
  59. if($bool === false){
  60. $this->throw();
  61. }
  62. }
  63. private function throw(){
  64. $error = new \Exception($this->error);
  65. if($this->fire('error', $this, $error) !== false){
  66. throw $error;
  67. }
  68. }
  69. public function open(){
  70. $this->socket = socket_create(constant("AF_{$this->dsn['protocol']}"), constant("SOCK_{$this->dsn['type']}"), is_null($this->dsn['protocol']) ? 0 : getprotobyname($this->dsn['protocol']));
  71. $this->throw_if($this->socket);
  72. if($this->socket_type == static::SERVER){
  73. if(is_null($this->port)){
  74. if(file_exists($this->address)){
  75. chmod($this->address, 0702);
  76. }elseif(function_exists('posix_mkfifo')){
  77. posix_mkfifo($this->address, 0702);
  78. }else{
  79. throw new \Exception('Unable to create socket file');
  80. }
  81. $res = socket_bind($this->socket, $this->address);
  82. }else{
  83. $res = socket_bind($this->socket, $this->address, $this->port);
  84. }
  85. $this->throw_if($res);
  86. $this->fire('bind', $this);
  87. $this->throw_if(socket_listen($this->socket, $this->max_connections));
  88. $this->fire('listen', $this);
  89. }elseif($this->socket_type == static::CLIENT){
  90. if(is_null($this->port)){
  91. $res = socket_connect($this->socket, $this->address);
  92. }else{
  93. $res = socket_connect($this->socket, $this->address, $this->port);
  94. }
  95. $this->throw_if($res);
  96. }else{
  97. throw new \Exception("Invalid socket type {$this->socket_type}");
  98. }
  99. $this->open = true;
  100. $this->fire('open', $this);
  101. return $this;
  102. }
  103. public function handle(int $length = 2048, bool $binary = false){
  104. if(!$this->open){
  105. $this->open();
  106. }
  107. socket_set_nonblock($this->socket);
  108. if($this->socket_type == static::SERVER){
  109. $regex = $binary ? '/\0/' : '/[\n\r]/';
  110. $type = $binary ? PHP_BINARY_READ : PHP_NORMAL_READ;
  111. do{
  112. $socket = socket_accept($this->socket);
  113. if($socket === false){
  114. usleep(100);
  115. }elseif($socket > 0){
  116. socket_set_nonblock($socket);
  117. $client = [$socket, ''];
  118. $this->clients[] = $client;
  119. $this->fire('connect', $this, $client);
  120. }else{
  121. $error = trim(socket_strerror($socket));
  122. if($error !== 'A non-blocking socket operation could not be completed immediately.'){
  123. $this->fire('error', $this, new \Exception($error));
  124. $this->open = false;
  125. }
  126. }
  127. $this->fire('tick', $this);
  128. if($this->open){
  129. foreach($this->clients as $client){
  130. $data = socket_read($client[0], $length, $type);
  131. if($data === false){
  132. $this->drop($client);
  133. }else{
  134. if(strlen($data) > 0 && $this->fire('data', $this, $data, $client) !== false){
  135. $client[1] .= $data;
  136. }
  137. $pos = preg_match($regex, $data, $matches, PREG_OFFSET_CAPTURE) == 1 ? $matches[0][1] : false;
  138. if($pos === strlen($data) - 1){
  139. $this->fire('read', $this, substr($client[1], 0, -1), $client);
  140. $client[1] = '';
  141. }
  142. }
  143. $this->fire('tick', $this);
  144. if($this->open === false){
  145. break;
  146. }
  147. }
  148. }
  149. }while($this->open);
  150. }elseif($this->socket_type == static::CLIENT){
  151. while($this->open && $this->read($length, $binary) !== false){};
  152. }
  153. socket_set_block($this->socket);
  154. $this->close(true);
  155. return $this;
  156. }
  157. public function write(string $data, bool $binary = false){
  158. if(!$this->open){
  159. throw new \Exception("You can't write to a socket that isn't open");
  160. }
  161. $stop = $binary ? "\0" : "\n";
  162. if($this->fire('write', $this, $data) !== false){
  163. $this->throw_if(socket_write($this->socket, $data.$stop));
  164. }
  165. return $this;
  166. }
  167. public function read(int $length = 2048, bool $binary = false){
  168. if(!$this->open){
  169. return false;
  170. }
  171. $regex = $binary ? '/\0/' : '/[\n\r]/';
  172. $type = $binary ? PHP_BINARY_READ : PHP_NORMAL_READ;
  173. $line = '';
  174. do{
  175. $data = socket_read($this->socket, $length, $type);
  176. if($data === false){
  177. $this->open = false;
  178. }elseif(strlen($data) > 0 && $this->fire('data', $this, $data) !== false){
  179. $line .= $data;
  180. }
  181. $pos = preg_match($regex, $data, $matches, PREG_OFFSET_CAPTURE) == 1 ? $matches[0][1] : false;
  182. }while($pos !== strlen($data) - 1 && $this->open !== false);
  183. $line = substr($line, 0, -1);
  184. $this->fire('read', $this, $line);
  185. return $line;
  186. }
  187. public function client_write(array $client, $data, bool $binary = false){
  188. if(!$this->open){
  189. throw new \Exception("You can't write to a socket that isn't open");
  190. }
  191. $stop = $binary ? "\0" : "\n";
  192. if($this->fire('write', $this, $data) !== false){
  193. $this->throw_if(socket_write($client[0], $data.$stop));
  194. }
  195. return $this;
  196. }
  197. public function close(bool $event = false){
  198. foreach($this->clients as $client){
  199. $this->drop($client);
  200. }
  201. socket_close($this->socket);
  202. $this->open = false;
  203. $this->fire('close', $this);
  204. return $this;
  205. }
  206. public function drop($client){
  207. socket_close($client[0]);
  208. if(in_array($client, $this->clients)){
  209. array_splice($this->clients, array_search($client, $this->clients), 1);
  210. $this->fire('disconnect', $this, $client);
  211. }
  212. }
  213. }
  214. ?>