WebSocket

  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 }

 

posted @ 2022-11-17 09:44  菜的掉渣  阅读(47)  评论(0编辑  收藏  举报