WebSocketClient.php 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <?php
  2. class WebSocketClient
  3. {
  4. private $_host;
  5. private $_port;
  6. private $_path;
  7. private $_origin;
  8. private $_Socket = null;
  9. private $_connected = false;
  10. private $timeout = .01;
  11. // private function __construct()
  12. // {
  13. // $this->connect('127.0.0.1', 8866, '/wss');
  14. // }
  15. public static $instance;
  16. public static function getInstance()
  17. {
  18. if (!(self::$instance instanceof self)) {
  19. self::$instance = new self();
  20. }
  21. return self::$instance;
  22. }
  23. public function __destruct()
  24. {
  25. $this->disconnect();
  26. }
  27. public function sendData($data, $type = 'text', $masked = true)
  28. {
  29. if ($this->_connected === false) {
  30. trigger_error("Not connected", E_USER_WARNING);
  31. return false;
  32. }
  33. if (!is_string($data)) {
  34. trigger_error("Not a string data was given.", E_USER_WARNING);
  35. return false;
  36. }
  37. if (strlen($data) == 0) {
  38. return false;
  39. }
  40. $res = @fwrite($this->_Socket, $this->_hybi10Encode($data, $type, $masked));
  41. if ($res === 0 || $res === false) {
  42. return false;
  43. }
  44. $buffer = ' ';
  45. while ($buffer !== '') {
  46. $buffer = fread($this->_Socket, 512);
  47. }
  48. return true;
  49. }
  50. public function connect($host, $port, $path, $origin = false,$count = 0)
  51. {
  52. $this->_host = $host;
  53. $this->_port = $port;
  54. $this->_path = $path;
  55. $this->_origin = $origin;
  56. $key = base64_encode($this->_generateRandomString(16, false, true));
  57. $header = "GET " . $path . " HTTP/1.1\r\n";
  58. $header .= "Host: " . $host . ":" . $port . "\r\n";
  59. $header .= "Upgrade: websocket\r\n";
  60. $header .= "Connection: Upgrade\r\n";
  61. //$header.= "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n";
  62. $header .= "Sec-WebSocket-Key: " . $key . "\r\n";
  63. if ($origin !== false) {
  64. $header .= "Sec-WebSocket-Origin: " . $origin . "\r\n";
  65. }
  66. $header .= "Sec-WebSocket-Version: 13\r\n\r\n";
  67. $this->_Socket = @fsockopen($host, $port, $errno, $errstr, $this->timeout);
  68. if($errno!=0){
  69. $errstr = iconv('gbk','utf-8',$errstr);
  70. echo "WebSocket Error:[#{$errno}] {$errstr}";
  71. exit;
  72. }
  73. if($this->_Socket!=false){
  74. socket_set_timeout($this->_Socket, $this->timeout, 10000);
  75. }else{
  76. echo 'WebSocket Error:socket not connected';
  77. exit;
  78. }
  79. //socket_write($this->_Socket, $header);
  80. $res = @fwrite($this->_Socket, $header);
  81. if ($res === false) {
  82. echo "fwrite false \n";
  83. }
  84. $response = @fread($this->_Socket, 1500);
  85. //$response = socket_read($this->_Socket);
  86. preg_match('#Sec-WebSocket-Accept:\s(.*)$#mU', $response, $matches);
  87. if ($matches) {
  88. $keyAccept = trim($matches[1]);
  89. $expectedResonse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
  90. $this->_connected = ($keyAccept === $expectedResonse) ? true : false;
  91. }
  92. if ($this->_connected === false) {
  93. if ($count < 5) {
  94. sleep(1);
  95. $count ++;
  96. return $this->connect($host, $port, $path, $origin,$count);
  97. }
  98. }
  99. return $this->_connected;
  100. }
  101. public function checkConnection()
  102. {
  103. $this->_connected = false;
  104. // send ping:
  105. $data = 'ping?';
  106. @fwrite($this->_Socket, $this->_hybi10Encode($data, 'ping', true));
  107. $response = @fread($this->_Socket, 300);
  108. if (empty($response)) {
  109. return false;
  110. }
  111. $response = $this->_hybi10Decode($response);
  112. if (!is_array($response)) {
  113. return false;
  114. }
  115. if (!isset($response['type']) || $response['type'] !== 'pong') {
  116. return false;
  117. }
  118. $this->_connected = true;
  119. return true;
  120. }
  121. public function disconnect()
  122. {
  123. $this->_connected = false;
  124. is_resource($this->_Socket) and fclose($this->_Socket);
  125. }
  126. public function reconnect()
  127. {
  128. sleep(10);
  129. $this->_connected = false;
  130. fclose($this->_Socket);
  131. $this->connect($this->_host, $this->_port, $this->_path, $this->_origin);
  132. }
  133. private function _generateRandomString($length = 10, $addSpaces = true, $addNumbers = true)
  134. {
  135. $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"ยง$%&/()=[]{}';
  136. $useChars = array();
  137. // select some random chars:
  138. for ($i = 0; $i < $length; $i++) {
  139. $useChars[] = $characters[mt_rand(0, strlen($characters) - 1)];
  140. }
  141. // add spaces and numbers:
  142. if ($addSpaces === true) {
  143. array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' ');
  144. }
  145. if ($addNumbers === true) {
  146. array_push($useChars, rand(0, 9), rand(0, 9), rand(0, 9));
  147. }
  148. shuffle($useChars);
  149. $randomString = trim(implode('', $useChars));
  150. $randomString = substr($randomString, 0, $length);
  151. return $randomString;
  152. }
  153. private function _hybi10Encode($payload, $type = 'text', $masked = true)
  154. {
  155. $frameHead = array();
  156. $frame = '';
  157. $payloadLength = strlen($payload);
  158. switch ($type) {
  159. case 'text':
  160. // first byte indicates FIN, Text-Frame (10000001):
  161. $frameHead[0] = 129;
  162. break;
  163. case 'close':
  164. // first byte indicates FIN, Close Frame(10001000):
  165. $frameHead[0] = 136;
  166. break;
  167. case 'ping':
  168. // first byte indicates FIN, Ping frame (10001001):
  169. $frameHead[0] = 137;
  170. break;
  171. case 'pong':
  172. // first byte indicates FIN, Pong frame (10001010):
  173. $frameHead[0] = 138;
  174. break;
  175. }
  176. // set mask and payload length (using 1, 3 or 9 bytes)
  177. if ($payloadLength > 65535) {
  178. $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8);
  179. $frameHead[1] = ($masked === true) ? 255 : 127;
  180. for ($i = 0; $i < 8; $i++) {
  181. $frameHead[$i + 2] = bindec($payloadLengthBin[$i]);
  182. }
  183. // most significant bit MUST be 0 (close connection if frame too big)
  184. if ($frameHead[2] > 127) {
  185. $this->close(1004);
  186. return false;
  187. }
  188. } elseif ($payloadLength > 125) {
  189. $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8);
  190. $frameHead[1] = ($masked === true) ? 254 : 126;
  191. $frameHead[2] = bindec($payloadLengthBin[0]);
  192. $frameHead[3] = bindec($payloadLengthBin[1]);
  193. } else {
  194. $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength;
  195. }
  196. // convert frame-head to string:
  197. foreach (array_keys($frameHead) as $i) {
  198. $frameHead[$i] = chr($frameHead[$i]);
  199. }
  200. if ($masked === true) {
  201. // generate a random mask:
  202. $mask = array();
  203. for ($i = 0; $i < 4; $i++) {
  204. $mask[$i] = chr(rand(0, 255));
  205. }
  206. $frameHead = array_merge($frameHead, $mask);
  207. }
  208. $frame = implode('', $frameHead);
  209. // append payload to frame:
  210. $framePayload = array();
  211. for ($i = 0; $i < $payloadLength; $i++) {
  212. $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i];
  213. }
  214. return $frame;
  215. }
  216. private function _hybi10Decode($data)
  217. {
  218. $payloadLength = '';
  219. $mask = '';
  220. $unmaskedPayload = '';
  221. $decodedData = array();
  222. // estimate frame type:
  223. $firstByteBinary = sprintf('%08b', ord($data[0]));
  224. $secondByteBinary = sprintf('%08b', ord($data[1]));
  225. $opcode = bindec(substr($firstByteBinary, 4, 4));
  226. $isMasked = ($secondByteBinary[0] == '1') ? true : false;
  227. $payloadLength = ord($data[1]) & 127;
  228. switch ($opcode) {
  229. // text frame:
  230. case 1:
  231. $decodedData['type'] = 'text';
  232. break;
  233. case 2:
  234. $decodedData['type'] = 'binary';
  235. break;
  236. // connection close frame:
  237. case 8:
  238. $decodedData['type'] = 'close';
  239. break;
  240. // ping frame:
  241. case 9:
  242. $decodedData['type'] = 'ping';
  243. break;
  244. // pong frame:
  245. case 10:
  246. $decodedData['type'] = 'pong';
  247. break;
  248. default:
  249. return false;
  250. break;
  251. }
  252. if ($payloadLength === 126) {
  253. $mask = substr($data, 4, 4);
  254. $payloadOffset = 8;
  255. $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset;
  256. } elseif ($payloadLength === 127) {
  257. $mask = substr($data, 10, 4);
  258. $payloadOffset = 14;
  259. $tmp = '';
  260. for ($i = 0; $i < 8; $i++) {
  261. $tmp .= sprintf('%08b', ord($data[$i + 2]));
  262. }
  263. $dataLength = bindec($tmp) + $payloadOffset;
  264. unset($tmp);
  265. } else {
  266. $mask = substr($data, 2, 4);
  267. $payloadOffset = 6;
  268. $dataLength = $payloadLength + $payloadOffset;
  269. }
  270. if ($isMasked === true) {
  271. for ($i = $payloadOffset; $i < $dataLength; $i++) {
  272. $j = $i - $payloadOffset;
  273. if (isset($data[$i])) {
  274. $unmaskedPayload .= $data[$i] ^ $mask[$j % 4];
  275. }
  276. }
  277. $decodedData['payload'] = $unmaskedPayload;
  278. } else {
  279. $payloadOffset = $payloadOffset - 4;
  280. $decodedData['payload'] = substr($data, $payloadOffset);
  281. }
  282. return $decodedData;
  283. }
  284. }