Feb 5, 2011

PHP:: socket_select(), socket_write() and socket_recv()

As it was already said, some clients need \0 character to end transmission, for example Flash's XMLSocket.

You should also be prepared to read less data than you have requested.

Here is an example of a socket buffer - it's an array which has socket resources for keys and an array of a timestamp and recieved data as values.

I find that the best practice for sending data is trailing it with a new line and zero character (\n\0), because you will probably have different types of clients which behave differently for reading data from sockets. Some need a \n to fire an event, some need \0.

For recieving data, sometimes you will get splitted data - this can hapen because the buffer is full (in my example 8192 bytes) or it just gets broken during transmission in lower levels.

Sometimes you can read two messages at once, but they have a zero character in between, so you can just use preg_split() to split the messages. The second message may not be complete, so you add it to your buffer.

 const message_delimiter = "\n\0";

 /*
  * Clear socket buffers older than 1 hour
  */
 function clear_buffer() {
  foreach($this->buffer as $key=>$val) {
   if(time() - $val['ts'] > 3600) {
    unset($this->buffer[$key]);
   }
  }
 }

 /*
  * Add data to a buffer
  */
 function buffer_add($sock,$data) {
  if(!isset($this->buffer[$sock])) {
   $this->buffer[$sock]['data'] = '';
  }

  $this->buffer[$sock]['data'] .= $data;
  $this->buffer[$sock]['ts'] = time();
 }

 function buffer_get($sock) {
  // split buffer by the end of string
  $lines = preg_split('/\0/',$this->buffer[$sock]['data']);

  // reset buffer to the last line of input
  // if the buffer was sent completely, the last line of input should be
  // an empty string
  $this->buffer[$sock]['data'] = trim($lines[count($lines)-1]);

  if(!empty($this->buffer[$sock]['data'])) {
   debug("buffer is not empty for $sock, len: ".strlen($this->buffer[$sock]['data']));
  }

  // remove the last line of input (incomplete data)
  // parse any complete data
  unset($lines[count($lines)-1]);

  // return only the fully sent data
  return $lines;
 }

 function read(&$sock,$len=8192,$flag=MSG_DONTWAIT) {
  $lines = array();

  $this->clear_buffer();

  $bytes_read = @socket_recv($sock,$read_data,$len,$flag);

  if ($bytes_read === false || $bytes_read == 0) {
   return false;
  } else {
   debug("recv: $read_data");
   $this->buffer_add($sock,$read_data);
   return $this->buffer_get($sock);
  }
 }

 /*
  * Write to a socket
  * add a newline and null character at the end
  * some clients don't read until new line is recieved
  *
  * try to send the rest of the data if it gets truncated
  */
 function write(&$sock,$msg) {
  $msg = $msg.self::message_delimiter;
  $length = strlen($msg);
  while(true) {
   $sent = @socket_write($sock,$msg,$length);
   if($sent <= 0) {
    return false;
   }
   if($sent < $length) {
    $msg = substr($msg, $sent);
    $length -= $sent;
    debug("Message truncated: Resending: $msg");
   } else {
    return true;
   }
  }
  return false;
 }