PageRenderTime 26ms CodeModel.GetById 16ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/Wrench/Socket/Socket.php

https://github.com/tinkajts/php-websocket
PHP | 322 lines | 158 code | 45 blank | 119 comment | 36 complexity | a0ed0fc29f508674633d28c7e37ca06a MD5 | raw file
Possible License(s): WTFPL
  1. <?php
  2. namespace Wrench\Socket;
  3. use Wrench\Resource;
  4. use Wrench\Exception\ConnectionException;
  5. use Wrench\Exception\SocketException;
  6. use Wrench\Util\Configurable;
  7. use Wrench\Protocol\Protocol;
  8. use Wrench\Protocol\Rfc6455Protocol;
  9. use \InvalidArgumentException;
  10. /**
  11. * Socket class
  12. *
  13. * Implements low level logic for connecting, serving, reading to, and
  14. * writing from WebSocket connections using PHP's streams.
  15. *
  16. * Unlike in previous versions of this library, a Socket instance now
  17. * represents a single underlying socket resource. It's designed to be used
  18. * by aggregation, rather than inheritence.
  19. */
  20. abstract class Socket extends Configurable implements Resource
  21. {
  22. /**
  23. * Default timeout for socket operations (reads, writes)
  24. *
  25. * @var int seconds
  26. */
  27. const TIMEOUT_SOCKET = 5;
  28. /**
  29. * @var int
  30. */
  31. const DEFAULT_RECEIVE_LENGTH = '1400';
  32. /**#@+
  33. * Socket name parts
  34. *
  35. * @var int
  36. */
  37. const NAME_PART_IP = 0;
  38. const NAME_PART_PORT = 1;
  39. /**#@-*/
  40. /**
  41. * @var resource
  42. */
  43. protected $socket = null;
  44. /**
  45. * Stream context
  46. */
  47. protected $context = null;
  48. /**
  49. * Whether the socket is connected to a server
  50. *
  51. * Note, the connection may not be ready to use, but the socket is
  52. * connected at least. See $handshaked, and other properties in
  53. * subclasses.
  54. *
  55. * @var boolean
  56. */
  57. protected $connected = false;
  58. /**
  59. * Whether the current read is the first one to the socket
  60. *
  61. * @var boolean
  62. */
  63. protected $firstRead = true;
  64. /**
  65. * The socket name according to stream_socket_get_name
  66. *
  67. * @var string
  68. */
  69. protected $name;
  70. /**
  71. * Configure options
  72. *
  73. * Options include
  74. * - timeout_connect => int, seconds, default 2
  75. * - timeout_socket => int, seconds, default 5
  76. *
  77. * @param array $options
  78. * @return void
  79. */
  80. protected function configure(array $options)
  81. {
  82. $options = array_merge(array(
  83. 'timeout_socket' => self::TIMEOUT_SOCKET,
  84. ), $options);
  85. parent::configure($options);
  86. }
  87. /**
  88. * Gets the name of the socket
  89. */
  90. protected function getName()
  91. {
  92. if (!isset($this->name) || !$this->name) {
  93. $this->name = @stream_socket_get_name($this->socket, true);
  94. }
  95. return $this->name;
  96. }
  97. /**
  98. * Gets part of the name of the socket
  99. *
  100. * PHP seems to return IPV6 address/port combos like this:
  101. * ::1:1234, where ::1 is the address and 1234 the port
  102. * So, the part number here is either the last : delimited section (the port)
  103. * or all the other sections (the whole initial part, the address).
  104. *
  105. * @param string $name (from $this->getName() usually)
  106. * @param int<0, 1> $part
  107. * @return string
  108. * @throws SocketException
  109. */
  110. public static function getNamePart($name, $part)
  111. {
  112. if (!$name) {
  113. throw new InvalidArgumentException('Invalid name');
  114. }
  115. $parts = explode(':', $name);
  116. if (count($parts) < 2) {
  117. throw new SocketException('Could not parse name parts: ' . $name);
  118. }
  119. if ($part == self::NAME_PART_PORT) {
  120. return end($parts);
  121. } elseif ($part == self::NAME_PART_IP) {
  122. return implode(':', array_slice($parts, 0, -1));
  123. } else {
  124. throw new InvalidArgumentException('Invalid name part');
  125. }
  126. return null;
  127. }
  128. /**
  129. * Gets the IP address of the socket
  130. *
  131. * @return string
  132. */
  133. public function getIp()
  134. {
  135. $name = $this->getName();
  136. if ($name) {
  137. return self::getNamePart($name, self::NAME_PART_IP);
  138. } else {
  139. throw new SocketException('Cannot get socket IP address');
  140. }
  141. }
  142. /**
  143. * Gets the port of the socket
  144. *
  145. * @return int
  146. */
  147. public function getPort()
  148. {
  149. $name = $this->getName();
  150. if ($name) {
  151. return self::getNamePart($name, self::NAME_PART_PORT);
  152. } else {
  153. throw new SocketException('Cannot get socket IP address');
  154. }
  155. }
  156. /**
  157. * Get the last error that occurred on the socket
  158. *
  159. * @return int|string
  160. */
  161. public function getLastError()
  162. {
  163. if ($this->isConnected() && $this->socket) {
  164. $err = @socket_last_error($this->socket);
  165. if ($err) {
  166. $err = socket_strerror($err);
  167. }
  168. if (!$err) {
  169. $err = 'Unknown error';
  170. }
  171. return $err;
  172. } else {
  173. return 'Not connected';
  174. }
  175. }
  176. /**
  177. * Whether the socket is currently connected
  178. *
  179. * @return boolean
  180. */
  181. public function isConnected()
  182. {
  183. return $this->connected;
  184. }
  185. /**
  186. * Disconnect the socket
  187. *
  188. * @return void
  189. */
  190. public function disconnect()
  191. {
  192. if ($this->socket) {
  193. stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR);
  194. }
  195. $this->socket = null;
  196. $this->connected = false;
  197. }
  198. /**
  199. * @see Wrench.Resource::getResource()
  200. */
  201. public function getResource()
  202. {
  203. return $this->socket;
  204. }
  205. /**
  206. * @see Wrench.Resource::getResourceId()
  207. */
  208. public function getResourceId()
  209. {
  210. return (int)$this->socket;
  211. }
  212. /**
  213. * @param unknown_type $data
  214. * @throws SocketException
  215. * @return boolean|int The number of bytes sent or false on error
  216. */
  217. public function send($data)
  218. {
  219. if (!$this->isConnected()) {
  220. throw new SocketException('Socket is not connected');
  221. }
  222. $length = strlen($data);
  223. if ($length == 0) {
  224. return 0;
  225. }
  226. for ($i = $length; $i > 0; $i -= $written) {
  227. $written = @fwrite($this->socket, substr($data, -1 * $i));
  228. if ($written === false) {
  229. return false;
  230. } elseif ($written === 0) {
  231. return false;
  232. }
  233. }
  234. return $length;
  235. }
  236. /**
  237. * Recieve data from the socket
  238. *
  239. * @param int $length
  240. * @return string
  241. */
  242. public function receive($length = self::DEFAULT_RECEIVE_LENGTH)
  243. {
  244. $remaining = $length;
  245. $buffer = '';
  246. $metadata['unread_bytes'] = 0;
  247. do {
  248. if (feof($this->socket)) {
  249. return $buffer;
  250. }
  251. $result = fread($this->socket, $length);
  252. if ($result === false) {
  253. return $buffer;
  254. }
  255. $buffer .= $result;
  256. if (feof($this->socket)) {
  257. return $buffer;
  258. }
  259. $continue = false;
  260. if ($this->firstRead == true && strlen($result) == 1) {
  261. // Workaround Chrome behavior (still needed?)
  262. $continue = true;
  263. }
  264. $this->firstRead = false;
  265. if (strlen($result) == $length) {
  266. $continue = true;
  267. }
  268. // Continue if more data to be read
  269. $metadata = stream_get_meta_data($this->socket);
  270. if ($metadata && isset($metadata['unread_bytes']) && $metadata['unread_bytes']) {
  271. $continue = true;
  272. $length = $metadata['unread_bytes'];
  273. }
  274. } while ($continue);
  275. return $buffer;
  276. }
  277. }