1<?php 2/** 3 * This file is part of the FreeDSx Socket package. 4 * 5 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace FreeDSx\Socket; 12 13use FreeDSx\Socket\Exception\ConnectionException; 14 15/** 16 * TCP socket server to accept client connections. 17 * 18 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 19 */ 20class SocketServer extends Socket 21{ 22 /** 23 * Supported transport types. 24 */ 25 public const TRANSPORTS = [ 26 'tcp', 27 'udp', 28 'unix', 29 ]; 30 31 /** 32 * @var array 33 */ 34 protected $serverOpts = [ 35 'use_ssl' => false, 36 'ssl_cert' => null, 37 'ssl_cert_key' => null, 38 'ssl_cert_passphrase' => null, 39 'ssl_crypto_type' => STREAM_CRYPTO_METHOD_TLSv1_2_SERVER | STREAM_CRYPTO_METHOD_TLSv1_1_SERVER | STREAM_CRYPTO_METHOD_TLS_SERVER, 40 'ssl_validate_cert' => false, 41 'idle_timeout' => 600, 42 ]; 43 44 /** 45 * @var Socket[] 46 */ 47 protected $clients = []; 48 49 /** 50 * @param array $options 51 */ 52 public function __construct(array $options = []) 53 { 54 parent::__construct( 55 null, 56 \array_merge( 57 $this->serverOpts, 58 $options 59 ) 60 ); 61 if (!\in_array($this->options['transport'], self::TRANSPORTS, true)) { 62 throw new \RuntimeException(sprintf( 63 'The transport "%s" is not valid. It must be one of: %s', 64 $this->options['transport'], 65 implode(',', self::TRANSPORTS) 66 )); 67 } 68 } 69 70 /** 71 * Create the socket server and bind to a specific port to listen for clients. 72 * 73 * @param string $ip 74 * @param int|null $port 75 * @return $this 76 * @throws ConnectionException 77 * @internal param string $ip 78 */ 79 public function listen(string $ip, ?int $port): self 80 { 81 $flags = STREAM_SERVER_BIND; 82 if ($this->options['transport'] !== 'udp') { 83 $flags |= STREAM_SERVER_LISTEN; 84 } 85 86 $transport = $this->options['transport']; 87 if ($transport === 'tcp' && $this->options['use_ssl'] === true) { 88 $transport = 'ssl'; 89 } 90 91 if ($transport !== 'unix' && $port === null) { 92 throw new ConnectionException('The port must be set if not using a unix based socket.'); 93 } 94 95 $uri = $transport.'://'.$ip; 96 if ($port !== null && $transport !== 'unix') { 97 $uri .= ':' . $port; 98 } 99 100 $socket = @\stream_socket_server( 101 $uri, 102 $this->errorNumber, 103 $this->errorMessage, 104 $flags, 105 $this->createSocketContext() 106 ); 107 if ($socket === false) { 108 throw new ConnectionException(sprintf( 109 'Unable to open %s socket (%s): %s', 110 \strtoupper($this->options['transport']), 111 $this->errorNumber, 112 $this->errorMessage 113 )); 114 } 115 $this->socket = $socket; 116 117 return $this; 118 } 119 120 /** 121 * @param int $timeout 122 * @return null|Socket 123 */ 124 public function accept(int $timeout = -1): ?Socket 125 { 126 $socket = @\stream_socket_accept($this->socket, $timeout); 127 if (\is_resource($socket)) { 128 $socket = new Socket($socket, \array_merge($this->options, [ 129 'timeout_read' => $this->options['idle_timeout'] 130 ])); 131 $this->clients[] = $socket; 132 } 133 134 return $socket instanceof Socket ? $socket : null; 135 } 136 137 /** 138 * Receive data from a UDP based socket. Optionally get the IP address the data was received from. 139 * 140 * @todo Buffer size should be adjustable. Max UDP packet size is 65507. Currently this avoids possible truncation. 141 * @param null $ipAddress 142 * @return null|string 143 */ 144 public function receive(&$ipAddress = null) 145 { 146 $this->block(true); 147 148 return \stream_socket_recvfrom( 149 $this->socket, 150 65507, 151 0, 152 $ipAddress 153 ); 154 } 155 156 /** 157 * @return Socket[] 158 */ 159 public function getClients(): array 160 { 161 return $this->clients; 162 } 163 164 /** 165 * @param Socket $socket 166 */ 167 public function removeClient(Socket $socket): void 168 { 169 if (($index = \array_search($socket, $this->clients, true)) !== false) { 170 unset($this->clients[$index]); 171 } 172 } 173 174 /** 175 * Create the socket server. Binds and listens on a specific port 176 * 177 * @param string $ip 178 * @param int|null $port 179 * @param array $options 180 * @return SocketServer 181 * @throws ConnectionException 182 */ 183 public static function bind( 184 string $ip, 185 ?int $port, 186 array $options = [] 187 ): SocketServer { 188 return (new self($options))->listen( 189 $ip, 190 $port 191 ); 192 } 193 194 /** 195 * Create a TCP based socket server. 196 * 197 * @param string $ip 198 * @param int $port 199 * @param array $options 200 * @return SocketServer 201 * @throws ConnectionException 202 */ 203 public static function bindTcp( 204 string $ip, 205 int $port, 206 array $options = [] 207 ): SocketServer { 208 return static::bind( 209 $ip, 210 $port, 211 \array_merge( 212 $options, 213 ['transport' => 'tcp'] 214 ) 215 ); 216 } 217 218 /** 219 * Created a UDP based socket server. 220 * 221 * @param string $ip 222 * @param int $port 223 * @param array $options 224 * @return SocketServer 225 * @throws ConnectionException 226 */ 227 public static function bindUdp( 228 string $ip, 229 int $port, 230 array $options = [] 231 ): SocketServer { 232 return static::bind( 233 $ip, 234 $port, 235 \array_merge( 236 $options, 237 ['transport' => 'udp'] 238 ) 239 ); 240 } 241 242 /** 243 * Created a UNIX based socket server. 244 * 245 * @param string $socketFile 246 * @param array $options 247 * @return SocketServer 248 * @throws ConnectionException 249 */ 250 public static function bindUnix( 251 string $socketFile, 252 array $options = [] 253 ): SocketServer { 254 return static::bind( 255 $socketFile, 256 null, 257 \array_merge( 258 $options, 259 ['transport' => 'unix'] 260 ) 261 ); 262 } 263} 264