1 <?php 2 3 /* 4 Based on PHP WebSocket Server 0.2 5 - http://code.google.com/p/php-websocket-server/ 6 - http://code.google.com/p/php-websocket-server/wiki/Scripting 7 8 WebSocket Protocol 07 9 - http://tools.ietf.org/html/draft-ietf-hybi-thewebsocketprotocol-07 10 - Supported by Firefox 6 (30/08/2011) 11 12 Whilst a big effort is made to follow the protocol documentation, the current script version may unknowingly differ. 13 Please report any bugs you may find, all feedback and questions are welcome! 14 */ 15 16 17 class PHPWebSocket 18 { 19 // maximum amount of clients that can be connected at one time 20 const WS_MAX_CLIENTS = 100; 21 22 // maximum amount of clients that can be connected at one time on the same IP v4 address 23 const WS_MAX_CLIENTS_PER_IP = 15; 24 25 // amount of seconds a client has to send data to the server, before a ping request is sent to the client, 26 // if the client has not completed the opening handshake, the ping request is skipped and the client connection is closed 27 const WS_TIMEOUT_RECV = 10; 28 29 // amount of seconds a client has to reply to a ping request, before the client connection is closed 30 const WS_TIMEOUT_PONG = 5; 31 32 // the maximum length, in bytes, of a frame's payload data (a message consists of 1 or more frames), this is also internally limited to 2,147,479,538 33 const WS_MAX_FRAME_PAYLOAD_RECV = 100000; 34 35 // the maximum length, in bytes, of a message's payload data, this is also internally limited to 2,147,483,647 36 const WS_MAX_MESSAGE_PAYLOAD_RECV = 500000; 37 38 39 40 41 // internal 42 const WS_FIN = 128; 43 const WS_MASK = 128; 44 45 const WS_OPCODE_CONTINUATION = 0; 46 const WS_OPCODE_TEXT = 1; 47 const WS_OPCODE_BINARY = 2; 48 const WS_OPCODE_CLOSE = 8; 49 const WS_OPCODE_PING = 9; 50 const WS_OPCODE_PONG = 10; 51 52 const WS_PAYLOAD_LENGTH_16 = 126; 53 const WS_PAYLOAD_LENGTH_63 = 127; 54 55 const WS_READY_STATE_CONNECTING = 0; 56 const WS_READY_STATE_OPEN = 1; 57 const WS_READY_STATE_CLOSING = 2; 58 const WS_READY_STATE_CLOSED = 3; 59 60 const WS_STATUS_NORMAL_CLOSE = 1000; 61 const WS_STATUS_GONE_AWAY = 1001; 62 const WS_STATUS_PROTOCOL_ERROR = 1002; 63 const WS_STATUS_UNSUPPORTED_MESSAGE_TYPE = 1003; 64 const WS_STATUS_MESSAGE_TOO_BIG = 1004; 65 66 const WS_STATUS_TIMEOUT = 3000; 67 68 // global vars 69 public $wsClients = array(); 70 public $wsRead = array(); 71 public $wsClientCount = 0; 72 public $wsClientIPCount = array(); 73 public $wsOnEvents = array(); 74 75 /* 76 $this->wsClients[ integer ClientID ] = array( 77 0 => resource Socket, // client socket 78 1 => string MessageBuffer, // a blank string when there's no incoming frames 79 2 => integer ReadyState, // between 0 and 3 80 3 => integer LastRecvTime, // set to time() when the client is added 81 4 => int/false PingSentTime, // false when the server is not waiting for a pong 82 5 => int/false CloseStatus, // close status that wsOnClose() will be called with 83 6 => integer IPv4, // client's IP stored as a signed long, retrieved from ip2long() 84 7 => int/false FramePayloadDataLength, // length of a frame's payload data, reset to false when all frame data has been read (cannot reset to 0, to allow reading of mask key) 85 8 => integer FrameBytesRead, // amount of bytes read for a frame, reset to 0 when all frame data has been read 86 9 => string FrameBuffer, // joined onto end as a frame's data comes in, reset to blank string when all frame data has been read 87 10 => integer MessageOpcode, // stored by the first frame for fragmented messages, default value is 0 88 11 => integer MessageBufferLength // the payload data length of MessageBuffer 89 ) 90 91 $wsRead[ integer ClientID ] = resource Socket // this one-dimensional array is used for socket_select() 92 // $wsRead[ 0 ] is the socket listening for incoming client connections 93 94 $wsClientCount = integer ClientCount // amount of clients currently connected 95 96 $wsClientIPCount[ integer IP ] = integer ClientCount // amount of clients connected per IP v4 address 97 */ 98 99 // server state functions 100 function wsStartServer($host, $port) { 101 if (isset($this->wsRead[0])) return false; 102 103 if (!$this->wsRead[0] = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)) { 104 return false; 105 } 106 if (!socket_set_option($this->wsRead[0], SOL_SOCKET, SO_REUSEADDR, 1)) { 107 socket_close($this->wsRead[0]); 108 return false; 109 } 110 if (!socket_bind($this->wsRead[0], $host, $port)) { 111 socket_close($this->wsRead[0]); 112 return false; 113 } 114 if (!socket_listen($this->wsRead[0], 10)) { 115 socket_close($this->wsRead[0]); 116 return false; 117 } 118 119 $write = array(); 120 $except = array(); 121 122 $nextPingCheck = time() + 1; 123 while (isset($this->wsRead[0])) { 124 $changed = $this->wsRead; 125 $result = socket_select($changed, $write, $except, 1); 126 127 if ($result === false) { 128 socket_close($this->wsRead[0]); 129 return false; 130 } 131 elseif ($result > 0) { 132 foreach ($changed as $clientID => $socket) { 133 if ($clientID != 0) { 134 // client socket changed 135 $buffer = ''; 136 $bytes = @socket_recv($socket, $buffer, 4096, 0); 137 138 if ($bytes === false) { 139 // error on recv, remove client socket (will check to send close frame) 140 $this->wsSendClientClose($clientID, self::WS_STATUS_PROTOCOL_ERROR); 141 } 142 elseif ($bytes > 0) { 143 // process handshake or frame(s) 144 if (!$this->wsProcessClient($clientID, $buffer, $bytes)) { 145 $this->wsSendClientClose($clientID, self::WS_STATUS_PROTOCOL_ERROR); 146 } 147 } 148 else { 149 // 0 bytes received from client, meaning the client closed the TCP connection 150 $this->wsRemoveClient($clientID); 151 } 152 } 153 else { 154 // listen socket changed 155 $client = socket_accept($this->wsRead[0]); 156 if ($client !== false) { 157 // fetch client IP as integer 158 $clientIP = ''; 159 $result = socket_getpeername($client, $clientIP); 160 $clientIP = ip2long($clientIP); 161 162 if ($result !== false && $this->wsClientCount < self::WS_MAX_CLIENTS && (!isset($this->wsClientIPCount[$clientIP]) || $this->wsClientIPCount[$clientIP] < self::WS_MAX_CLIENTS_PER_IP)) { 163 $this->wsAddClient($client, $clientIP); 164 } 165 else { 166 socket_close($client); 167 } 168 } 169 } 170 } 171 } 172 173 if (time() >= $nextPingCheck) { 174 $this->wsCheckIdleClients(); 175 $nextPingCheck = time() + 1; 176 } 177 } 178 179 return true; // returned when wsStopServer() is called 180 } 181 function wsStopServer() { 182 // check if server is not running 183 if (!isset($this->wsRead[0])) return false; 184 185 // close all client connections 186 foreach ($this->wsClients as $clientID => $client) { 187 // if the client's opening handshake is complete, tell the client the server is 'going away' 188 if ($client[2] != self::WS_READY_STATE_CONNECTING) { 189 $this->wsSendClientClose($clientID, self::WS_STATUS_GONE_AWAY); 190 } 191 socket_close($client[0]); 192 } 193 194 // close the socket which listens for incoming clients 195 socket_close($this->wsRead[0]); 196 197 // reset variables 198 $this->wsRead = array(); 199 $this->wsClients = array(); 200 $this->wsClientCount = 0; 201 $this->wsClientIPCount = array(); 202 203 return true; 204 } 205 206 // client timeout functions 207 function wsCheckIdleClients() { 208 $time = time(); 209 foreach ($this->wsClients as $clientID => $client) { 210 if ($client[2] != self::WS_READY_STATE_CLOSED) { 211 // client ready state is not closed 212 if ($client[4] !== false) { 213 // ping request has already been sent to client, pending a pong reply 214 if ($time >= $client[4] + self::WS_TIMEOUT_PONG) { 215 // client didn't respond to the server's ping request in self::WS_TIMEOUT_PONG seconds 216 $this->wsSendClientClose($clientID, self::WS_STATUS_TIMEOUT); 217 $this->wsRemoveClient($clientID); 218 } 219 } 220 elseif ($time >= $client[3] + self::WS_TIMEOUT_RECV) { 221 // last data was received >= self::WS_TIMEOUT_RECV seconds ago 222 if ($client[2] != self::WS_READY_STATE_CONNECTING) { 223 // client ready state is open or closing 224 $this->wsClients[$clientID][4] = time(); 225 $this->wsSendClientMessage($clientID, self::WS_OPCODE_PING, ''); 226 } 227 else { 228 // client ready state is connecting 229 $this->wsRemoveClient($clientID); 230 } 231 } 232 } 233 } 234 } 235 236 // client existence functions 237 function wsAddClient($socket, $clientIP) { 238 // increase amount of clients connected 239 $this->wsClientCount++; 240 241 // increase amount of clients connected on this client's IP 242 if (isset($this->wsClientIPCount[$clientIP])) { 243 $this->wsClientIPCount[$clientIP]++; 244 } 245 else { 246 $this->wsClientIPCount[$clientIP] = 1; 247 } 248 249 // fetch next client ID 250 $clientID = $this->wsGetNextClientID(); 251 252 // store initial client data 253 $this->wsClients[$clientID] = array($socket, '', self::WS_READY_STATE_CONNECTING, time(), false, 0, $clientIP, false, 0, '', 0, 0); 254 255 // store socket - used for socket_select() 256 $this->wsRead[$clientID] = $socket; 257 } 258 function wsRemoveClient($clientID) { 259 // fetch close status (which could be false), and call wsOnClose 260 $closeStatus = $this->wsClients[$clientID][5]; 261 if ( array_key_exists('close', $this->wsOnEvents) ) 262 foreach ( $this->wsOnEvents['close'] as $func ) 263 $func($clientID, $closeStatus); 264 265 // close socket 266 $socket = $this->wsClients[$clientID][0]; 267 socket_close($socket); 268 269 // decrease amount of clients connected on this client's IP 270 $clientIP = $this->wsClients[$clientID][6]; 271 if ($this->wsClientIPCount[$clientIP] > 1) { 272 $this->wsClientIPCount[$clientIP]--; 273 } 274 else { 275 unset($this->wsClientIPCount[$clientIP]); 276 } 277 278 // decrease amount of clients connected 279 $this->wsClientCount--; 280 281 // remove socket and client data from arrays 282 unset($this->wsRead[$clientID], $this->wsClients[$clientID]); 283 } 284 285 // client data functions 286 function wsGetNextClientID() { 287 $i = 1; // starts at 1 because 0 is the listen socket 288 while (isset($this->wsRead[$i])) $i++; 289 return $i; 290 } 291 function wsGetClientSocket($clientID) { 292 return $this->wsClients[$clientID][0]; 293 } 294 295 // client read functions 296 function wsProcessClient($clientID, &$buffer, $bufferLength) { 297 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_OPEN) { 298 // handshake completed 299 $result = $this->wsBuildClientFrame($clientID, $buffer, $bufferLength); 300 } 301 elseif ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CONNECTING) { 302 // handshake not completed 303 $result = $this->wsProcessClientHandshake($clientID, $buffer); 304 if ($result) { 305 $this->wsClients[$clientID][2] = self::WS_READY_STATE_OPEN; 306 307 if ( array_key_exists('open', $this->wsOnEvents) ) 308 foreach ( $this->wsOnEvents['open'] as $func ) 309 $func($clientID); 310 } 311 } 312 else { 313 // ready state is set to closed 314 $result = false; 315 } 316 317 return $result; 318 } 319 function wsBuildClientFrame($clientID, &$buffer, $bufferLength) { 320 // increase number of bytes read for the frame, and join buffer onto end of the frame buffer 321 $this->wsClients[$clientID][8] += $bufferLength; 322 $this->wsClients[$clientID][9] .= $buffer; 323 324 // check if the length of the frame's payload data has been fetched, if not then attempt to fetch it from the frame buffer 325 if ($this->wsClients[$clientID][7] !== false || $this->wsCheckSizeClientFrame($clientID) == true) { 326 // work out the header length of the frame 327 $headerLength = ($this->wsClients[$clientID][7] <= 125 ? 0 : ($this->wsClients[$clientID][7] <= 65535 ? 2 : 8)) + 6; 328 329 // check if all bytes have been received for the frame 330 $frameLength = $this->wsClients[$clientID][7] + $headerLength; 331 if ($this->wsClients[$clientID][8] >= $frameLength) { 332 // check if too many bytes have been read for the frame (they are part of the next frame) 333 $nextFrameBytesLength = $this->wsClients[$clientID][8] - $frameLength; 334 if ($nextFrameBytesLength > 0) { 335 $this->wsClients[$clientID][8] -= $nextFrameBytesLength; 336 $nextFrameBytes = substr($this->wsClients[$clientID][9], $frameLength); 337 $this->wsClients[$clientID][9] = substr($this->wsClients[$clientID][9], 0, $frameLength); 338 } 339 340 // process the frame 341 $result = $this->wsProcessClientFrame($clientID); 342 343 // check if the client wasn't removed, then reset frame data 344 if (isset($this->wsClients[$clientID])) { 345 $this->wsClients[$clientID][7] = false; 346 $this->wsClients[$clientID][8] = 0; 347 $this->wsClients[$clientID][9] = ''; 348 } 349 350 // if there's no extra bytes for the next frame, or processing the frame failed, return the result of processing the frame 351 if ($nextFrameBytesLength <= 0 || !$result) return $result; 352 353 // build the next frame with the extra bytes 354 return $this->wsBuildClientFrame($clientID, $nextFrameBytes, $nextFrameBytesLength); 355 } 356 } 357 358 return true; 359 } 360 function wsCheckSizeClientFrame($clientID) { 361 // check if at least 2 bytes have been stored in the frame buffer 362 if ($this->wsClients[$clientID][8] > 1) { 363 // fetch payload length in byte 2, max will be 127 364 $payloadLength = ord(substr($this->wsClients[$clientID][9], 1, 1)) & 127; 365 366 if ($payloadLength <= 125) { 367 // actual payload length is <= 125 368 $this->wsClients[$clientID][7] = $payloadLength; 369 } 370 elseif ($payloadLength == 126) { 371 // actual payload length is <= 65,535 372 if (substr($this->wsClients[$clientID][9], 3, 1) !== false) { 373 // at least another 2 bytes are set 374 $payloadLengthExtended = substr($this->wsClients[$clientID][9], 2, 2); 375 $array = unpack('na', $payloadLengthExtended); 376 $this->wsClients[$clientID][7] = $array['a']; 377 } 378 } 379 else { 380 // actual payload length is > 65,535 381 if (substr($this->wsClients[$clientID][9], 9, 1) !== false) { 382 // at least another 8 bytes are set 383 $payloadLengthExtended = substr($this->wsClients[$clientID][9], 2, 8); 384 385 // check if the frame's payload data length exceeds 2,147,483,647 (31 bits) 386 // the maximum integer in PHP is "usually" this number. More info: http://php.net/manual/en/language.types.integer.php 387 $payloadLengthExtended32_1 = substr($payloadLengthExtended, 0, 4); 388 $array = unpack('Na', $payloadLengthExtended32_1); 389 if ($array['a'] != 0 || ord(substr($payloadLengthExtended, 4, 1)) & 128) { 390 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG); 391 return false; 392 } 393 394 // fetch length as 32 bit unsigned integer, not as 64 bit 395 $payloadLengthExtended32_2 = substr($payloadLengthExtended, 4, 4); 396 $array = unpack('Na', $payloadLengthExtended32_2); 397 398 // check if the payload data length exceeds 2,147,479,538 (2,147,483,647 - 14 - 4095) 399 // 14 for header size, 4095 for last recv() next frame bytes 400 if ($array['a'] > 2147479538) { 401 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG); 402 return false; 403 } 404 405 // store frame payload data length 406 $this->wsClients[$clientID][7] = $array['a']; 407 } 408 } 409 410 // check if the frame's payload data length has now been stored 411 if ($this->wsClients[$clientID][7] !== false) { 412 413 // check if the frame's payload data length exceeds self::WS_MAX_FRAME_PAYLOAD_RECV 414 if ($this->wsClients[$clientID][7] > self::WS_MAX_FRAME_PAYLOAD_RECV) { 415 $this->wsClients[$clientID][7] = false; 416 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG); 417 return false; 418 } 419 420 // check if the message's payload data length exceeds 2,147,483,647 or self::WS_MAX_MESSAGE_PAYLOAD_RECV 421 // doesn't apply for control frames, where the payload data is not internally stored 422 $controlFrame = (ord(substr($this->wsClients[$clientID][9], 0, 1)) & 8) == 8; 423 if (!$controlFrame) { 424 $newMessagePayloadLength = $this->wsClients[$clientID][11] + $this->wsClients[$clientID][7]; 425 if ($newMessagePayloadLength > self::WS_MAX_MESSAGE_PAYLOAD_RECV || $newMessagePayloadLength > 2147483647) { 426 $this->wsSendClientClose($clientID, self::WS_STATUS_MESSAGE_TOO_BIG); 427 return false; 428 } 429 } 430 431 return true; 432 } 433 } 434 435 return false; 436 } 437 function wsProcessClientFrame($clientID) { 438 // store the time that data was last received from the client 439 $this->wsClients[$clientID][3] = time(); 440 441 // fetch frame buffer 442 $buffer = &$this->wsClients[$clientID][9]; 443 444 // check at least 6 bytes are set (first 2 bytes and 4 bytes for the mask key) 445 if (substr($buffer, 5, 1) === false) return false; 446 447 // fetch first 2 bytes of header 448 $octet0 = ord(substr($buffer, 0, 1)); 449 $octet1 = ord(substr($buffer, 1, 1)); 450 451 $fin = $octet0 & self::WS_FIN; 452 $opcode = $octet0 & 15; 453 454 $mask = $octet1 & self::WS_MASK; 455 if (!$mask) return false; // close socket, as no mask bit was sent from the client 456 457 // fetch byte position where the mask key starts 458 $seek = $this->wsClients[$clientID][7] <= 125 ? 2 : ($this->wsClients[$clientID][7] <= 65535 ? 4 : 10); 459 460 // read mask key 461 $maskKey = substr($buffer, $seek, 4); 462 463 $array = unpack('Na', $maskKey); 464 $maskKey = $array['a']; 465 $maskKey = array( 466 $maskKey >> 24, 467 ($maskKey >> 16) & 255, 468 ($maskKey >> 8) & 255, 469 $maskKey & 255 470 ); 471 $seek += 4; 472 473 // decode payload data 474 if (substr($buffer, $seek, 1) !== false) { 475 $data = str_split(substr($buffer, $seek)); 476 foreach ($data as $key => $byte) { 477 $data[$key] = chr(ord($byte) ^ ($maskKey[$key % 4])); 478 } 479 $data = implode('', $data); 480 } 481 else { 482 $data = ''; 483 } 484 485 // check if this is not a continuation frame and if there is already data in the message buffer 486 if ($opcode != self::WS_OPCODE_CONTINUATION && $this->wsClients[$clientID][11] > 0) { 487 // clear the message buffer 488 $this->wsClients[$clientID][11] = 0; 489 $this->wsClients[$clientID][1] = ''; 490 } 491 492 // check if the frame is marked as the final frame in the message 493 if ($fin == self::WS_FIN) { 494 // check if this is the first frame in the message 495 if ($opcode != self::WS_OPCODE_CONTINUATION) { 496 // process the message 497 return $this->wsProcessClientMessage($clientID, $opcode, $data, $this->wsClients[$clientID][7]); 498 } 499 else { 500 // increase message payload data length 501 $this->wsClients[$clientID][11] += $this->wsClients[$clientID][7]; 502 503 // push frame payload data onto message buffer 504 $this->wsClients[$clientID][1] .= $data; 505 506 // process the message 507 $result = $this->wsProcessClientMessage($clientID, $this->wsClients[$clientID][10], $this->wsClients[$clientID][1], $this->wsClients[$clientID][11]); 508 509 // check if the client wasn't removed, then reset message buffer and message opcode 510 if (isset($this->wsClients[$clientID])) { 511 $this->wsClients[$clientID][1] = ''; 512 $this->wsClients[$clientID][10] = 0; 513 $this->wsClients[$clientID][11] = 0; 514 } 515 516 return $result; 517 } 518 } 519 else { 520 // check if the frame is a control frame, control frames cannot be fragmented 521 if ($opcode & 8) return false; 522 523 // increase message payload data length 524 $this->wsClients[$clientID][11] += $this->wsClients[$clientID][7]; 525 526 // push frame payload data onto message buffer 527 $this->wsClients[$clientID][1] .= $data; 528 529 // if this is the first frame in the message, store the opcode 530 if ($opcode != self::WS_OPCODE_CONTINUATION) { 531 $this->wsClients[$clientID][10] = $opcode; 532 } 533 } 534 535 return true; 536 } 537 function wsProcessClientMessage($clientID, $opcode, &$data, $dataLength) { 538 // check opcodes 539 if ($opcode == self::WS_OPCODE_PING) { 540 // received ping message 541 return $this->wsSendClientMessage($clientID, self::WS_OPCODE_PONG, $data); 542 } 543 elseif ($opcode == self::WS_OPCODE_PONG) { 544 // received pong message (it's valid if the server did not send a ping request for this pong message) 545 if ($this->wsClients[$clientID][4] !== false) { 546 $this->wsClients[$clientID][4] = false; 547 } 548 } 549 elseif ($opcode == self::WS_OPCODE_CLOSE) { 550 // received close message 551 if (substr($data, 1, 1) !== false) { 552 $array = unpack('na', substr($data, 0, 2)); 553 $status = $array['a']; 554 } 555 else { 556 $status = false; 557 } 558 559 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSING) { 560 // the server already sent a close frame to the client, this is the client's close frame reply 561 // (no need to send another close frame to the client) 562 $this->wsClients[$clientID][2] = self::WS_READY_STATE_CLOSED; 563 } 564 else { 565 // the server has not already sent a close frame to the client, send one now 566 $this->wsSendClientClose($clientID, self::WS_STATUS_NORMAL_CLOSE); 567 } 568 569 $this->wsRemoveClient($clientID); 570 } 571 elseif ($opcode == self::WS_OPCODE_TEXT || $opcode == self::WS_OPCODE_BINARY) { 572 if ( array_key_exists('message', $this->wsOnEvents) ) 573 foreach ( $this->wsOnEvents['message'] as $func ) 574 $func($clientID, $data, $dataLength, $opcode == self::WS_OPCODE_BINARY); 575 } 576 else { 577 // unknown opcode 578 return false; 579 } 580 581 return true; 582 } 583 function wsProcessClientHandshake($clientID, &$buffer) { 584 // fetch headers and request line 585 $sep = strpos($buffer, "\r\n\r\n"); 586 if (!$sep) return false; 587 588 $headers = explode("\r\n", substr($buffer, 0, $sep)); 589 $headersCount = sizeof($headers); // includes request line 590 if ($headersCount < 1) return false; 591 592 // fetch request and check it has at least 3 parts (space tokens) 593 $request = &$headers[0]; 594 $requestParts = explode(' ', $request); 595 $requestPartsSize = sizeof($requestParts); 596 if ($requestPartsSize < 3) return false; 597 598 // check request method is GET 599 if (strtoupper($requestParts[0]) != 'GET') return false; 600 601 // check request HTTP version is at least 1.1 602 $httpPart = &$requestParts[$requestPartsSize - 1]; 603 $httpParts = explode('/', $httpPart); 604 if (!isset($httpParts[1]) || (float) $httpParts[1] < 1.1) return false; 605 606 // store headers into a keyed array: array[headerKey] = headerValue 607 $headersKeyed = array(); 608 for ($i=1; $i<$headersCount; $i++) { 609 $parts = explode(':', $headers[$i]); 610 if (!isset($parts[1])) return false; 611 612 $headersKeyed[trim($parts[0])] = trim($parts[1]); 613 } 614 615 // check Host header was received 616 if (!isset($headersKeyed['Host'])) return false; 617 618 // check Sec-WebSocket-Key header was received and decoded value length is 16 619 if (!isset($headersKeyed['Sec-WebSocket-Key'])) return false; 620 $key = $headersKeyed['Sec-WebSocket-Key']; 621 if (strlen(base64_decode($key)) != 16) return false; 622 623 // check Sec-WebSocket-Version header was received and value is 7 624 if (!isset($headersKeyed['Sec-WebSocket-Version']) || (int) $headersKeyed['Sec-WebSocket-Version'] < 7) return false; // should really be != 7, but Firefox 7 beta users send 8 625 626 // work out hash to use in Sec-WebSocket-Accept reply header 627 $hash = base64_encode(sha1($key.'258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); 628 629 // build headers 630 $headers = array( 631 'HTTP/1.1 101 Switching Protocols', 632 'Upgrade: websocket', 633 'Connection: Upgrade', 634 'Sec-WebSocket-Accept: '.$hash 635 ); 636 $headers = implode("\r\n", $headers)."\r\n\r\n"; 637 638 // send headers back to client 639 $socket = $this->wsClients[$clientID][0]; 640 641 $left = strlen($headers); 642 do { 643 $sent = @socket_send($socket, $headers, $left, 0); 644 if ($sent === false) return false; 645 646 $left -= $sent; 647 if ($sent > 0) $headers = substr($headers, $sent); 648 } 649 while ($left > 0); 650 651 return true; 652 } 653 654 // client write functions 655 function wsSendClientMessage($clientID, $opcode, $message) { 656 // check if client ready state is already closing or closed 657 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSING || $this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSED) return true; 658 659 // fetch message length 660 $messageLength = strlen($message); 661 662 // set max payload length per frame 663 $bufferSize = 4096; 664 665 // work out amount of frames to send, based on $bufferSize 666 $frameCount = ceil($messageLength / $bufferSize); 667 if ($frameCount == 0) $frameCount = 1; 668 669 // set last frame variables 670 $maxFrame = $frameCount - 1; 671 $lastFrameBufferLength = ($messageLength % $bufferSize) != 0 ? ($messageLength % $bufferSize) : ($messageLength != 0 ? $bufferSize : 0); 672 673 // loop around all frames to send 674 for ($i=0; $i<$frameCount; $i++) { 675 // fetch fin, opcode and buffer length for frame 676 $fin = $i != $maxFrame ? 0 : self::WS_FIN; 677 $opcode = $i != 0 ? self::WS_OPCODE_CONTINUATION : $opcode; 678 679 $bufferLength = $i != $maxFrame ? $bufferSize : $lastFrameBufferLength; 680 681 // set payload length variables for frame 682 if ($bufferLength <= 125) { 683 $payloadLength = $bufferLength; 684 $payloadLengthExtended = ''; 685 $payloadLengthExtendedLength = 0; 686 } 687 elseif ($bufferLength <= 65535) { 688 $payloadLength = self::WS_PAYLOAD_LENGTH_16; 689 $payloadLengthExtended = pack('n', $bufferLength); 690 $payloadLengthExtendedLength = 2; 691 } 692 else { 693 $payloadLength = self::WS_PAYLOAD_LENGTH_63; 694 $payloadLengthExtended = pack('xxxxN', $bufferLength); // pack 32 bit int, should really be 64 bit int 695 $payloadLengthExtendedLength = 8; 696 } 697 698 // set frame bytes 699 $buffer = pack('n', (($fin | $opcode) << 8) | $payloadLength) . $payloadLengthExtended . substr($message, $i*$bufferSize, $bufferLength); 700 701 // send frame 702 $socket = $this->wsClients[$clientID][0]; 703 704 $left = 2 + $payloadLengthExtendedLength + $bufferLength; 705 do { 706 $sent = @socket_send($socket, $buffer, $left, 0); 707 if ($sent === false) return false; 708 709 $left -= $sent; 710 if ($sent > 0) $buffer = substr($buffer, $sent); 711 } 712 while ($left > 0); 713 } 714 715 return true; 716 } 717 function wsSendClientClose($clientID, $status=false) { 718 // check if client ready state is already closing or closed 719 if ($this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSING || $this->wsClients[$clientID][2] == self::WS_READY_STATE_CLOSED) return true; 720 721 // store close status 722 $this->wsClients[$clientID][5] = $status; 723 724 // send close frame to client 725 $status = $status !== false ? pack('n', $status) : ''; 726 $this->wsSendClientMessage($clientID, self::WS_OPCODE_CLOSE, $status); 727 728 // set client ready state to closing 729 $this->wsClients[$clientID][2] = self::WS_READY_STATE_CLOSING; 730 } 731 732 // client non-internal functions 733 function wsClose($clientID) { 734 return $this->wsSendClientClose($clientID, self::WS_STATUS_NORMAL_CLOSE); 735 } 736 function wsSend($clientID, $message, $binary=false) { 737 return $this->wsSendClientMessage($clientID, $binary ? self::WS_OPCODE_BINARY : self::WS_OPCODE_TEXT, $message); 738 } 739 740 function log( $message ) 741 { 742 echo date('Y-m-d H:i:s: ') . $message . "\n"; 743 } 744 745 function bind( $type, $func ) 746 { 747 if ( !isset($this->wsOnEvents[$type]) ) 748 $this->wsOnEvents[$type] = array(); 749 $this->wsOnEvents[$type][] = $func; 750 } 751 752 function unbind( $type='' ) 753 { 754 if ( $type ) unset($this->wsOnEvents[$type]); 755 else $this->wsOnEvents = array(); 756 } 757 } 758 ?>
1 <?php 2 3 set_time_limit(0); 4 5 require 'WebSocket.php'; 6 7 $server = new PHPWebSocket; 8 9 $server->bind('open', 'onOpen'); 10 $server->bind('message', 'onMessage'); 11 $server->bind('close', 'onClose'); 12 13 $server->wsStartServer('127.0.0.1', 9311); 14 15 function onOpen($clientId) 16 { 17 global $server; 18 19 $ip = long2ip($server->wsClients[$clientId][6]); 20 21 $server->log("{$ip} ({$clientId}) has connected."); 22 23 $clients = $server->wsClients; 24 foreach ($clients as $id => $client) { 25 if ($id != $clientId) { 26 $server->wsSend($id, "Visitor {$clientId} ({$ip}) has joined the room."); 27 } 28 } 29 } 30 31 function onMessage($clientId, $message, $messageLength, $binary) 32 { 33 global $server; 34 35 $ip = long2ip($server->wsClients[$clientId][6]); 36 37 if ($messageLength == 0) { 38 var_dump('on close'); 39 // $server->wsClose($clientId); 40 41 return; 42 } 43 44 if (sizeof($server->wsClients) == 1) { 45 $server->wsSend($clientId, "There isn't anyone else in the room, but I'll still listen to you. --Your Trusty Server"); 46 } else { 47 $clients = $server->wsClients; 48 foreach ($clients as $id => $client) { 49 if ($id != $clientId) { 50 $server->wsSend($id, "Visitor {$clientId} ({$ip}) said {$message}"); 51 } 52 } 53 } 54 } 55 56 function onClose($clientId, $status) 57 { 58 global $server; 59 60 $ip = long2ip($server->wsClients[$clientId][6]); 61 62 $server->log("{$ip} ({$clientId}) has disconncted."); 63 64 $clients = $server->wsClients; 65 foreach ($clients as $id => $client) { 66 $server->wsSend($id, "Visitor {$clientId} ($ip) has left the room."); 67 } 68 }
标签:false,self,clientID,client,WS,WebSocket,wsClients From: https://www.cnblogs.com/saonian/p/16898397.html