Non-blocking I/O with Gtk::io_add_watch()

This is a sample code to demonstrate the use of Gtk::io_add_watch(). The main goal of this function is to handle socket IO without blocking. Socket polling is done in Gtk::main() loop. You don't need to care about socket polling. This task is done by Gtk and Glib.

In this example, the function is used for building an simple IRC client that connect to an IRC server, identifying itself, connecting to #php-gtk, and doing some boring things.

Since this is just a use case for the API, there is no user interface.

There is a package on Gnope server based on this documentation and class.

Function interface description

Function call parameters

io_id Gtk::io_add_watch($stream, $conditions, $callback)

$conditions parameter description : this is a mask of the following values :

  • Gtk::IO_IN : stream is ready for reading,
  • Gtk::IO_OUT : stream is ready for writing,
  • Gtk::IO_PRI : this is the high priority channel for stream
  • Gtk::IO_ERR : stream is in error state,
  • Gtk::IO_HUP : stream has been disconnected (sighup)
  • Gtk::IO_NVAL : (invalid command to socket ?)

You can build mask like this : $conditions = Gtk::IO_IN|Gtk::IO_OUT.

Callback parameters

function io_callback($gio_channel, $conditions)

  • $gio_channel is not usable with this current API implementation ; should be used with g_io_ functions not implemented. But IO can be done with standard php functions.
  • $conditions : this is effective conditions that triggered actual event call.

Source code example

<?php
error_reporting
(E_ALL);

/*
$conditions : can be a mask of :
  Gtk::IO_IN,  Gtk::IO_OUT, 
  Gtk::IO_PRI, Gtk::IO_ERR,
  Gtk::IO_HUP, Gtk::IO_NVAL
*/

/**
* Available syntaxes for io_add_watch:
*
$id = io_add_watch($stream, $condition, $callback);
$id = io_add_watch_priority($stream, $condition, $callback,
   $priority);
*
*/

class GtkIO {
  protected
$gio_ids;
  protected
$fd;

  protected
$hostname;
  protected
$port;

  protected
$errno;    # fsocketopen error status
 
protected $errstr;

  function
__construct() {
   
$this->gio_ids = array();
  }

  function
connect($hostname, $port) {
   
$this->hostname = $hostname;
   
$this->port = $port;
   
$this->fd = fsockopen($hostname, $port,
     
$this->errno, $this->errstr);
    if(!
$this->fd)
     
trigger_error("GtkIO::connect : connexion error : "
       
. "{$this->errstr} ({$this->errno})");
    else {
     
$this->io_setup();
    }
    return
$this->fd;
  }

  function
io_setup() {
   
$this->gio_ids[Gtk::IO_IN]   =
     
Gtk::io_add_watch($this->fd, Gtk::IO_IN,
        array(
$this, 'io_in'));
   
$this->gio_ids[Gtk::IO_OUT]  =
     
Gtk::io_add_watch($this->fd, Gtk::IO_OUT,  
        array(
$this, 'io_out'));
   
$this->gio_ids[Gtk::IO_HUP]  =
     
Gtk::io_add_watch($this->fd, Gtk::IO_HUP,  
        array(
$this, 'io_hup'));
   
$this->gio_ids[Gtk::IO_PRI]  =
     
Gtk::io_add_watch($this->fd, Gtk::IO_PRI,  
        array(
$this, 'io_pri'));
   
$this->gio_ids[Gtk::IO_ERR]  =
     
Gtk::io_add_watch($this->fd, Gtk::IO_ERR,  
        array(
$this, 'io_err'));
   
$this->gio_ids[Gtk::IO_NVAL] =
     
Gtk::io_add_watch($this->fd, Gtk::IO_NVAL
        array(
$this, 'ion_val'));
  }
 
  function
write($data) {
    return
fwrite($this->fd, $data, strlen($data));
  }
 
 
# very incomplete : need to handle errors ...
 
function read($size = 2048) {
    return
fread($this->fd, $size);
  }
 
  function
eof() {
    return
feof($this->fd);
  }
 
  function
io_in($gio_channel, $conditions) {
    echo
"GtkIO::io_in()\n";
  }

  function
io_out($gio_channel, $conditions) {
    echo
"GtkIO::io_out()\n";
  }

  function
io_hup($gio_channel, $conditions) {
    echo
"GtkIO::io_hup()\n";
  }

  function
io_pri($gio_channel, $conditions) {
    echo
"GtkIO::io_pri()\n";
  }

  function
io_err($gio_channel, $conditions) {
    echo
"GtkIO::io_err()\n";
  }
}

define('GTK_IRC_CONNECTED',       1);
define('GTK_IRC_DISCONNECTED',    2);

class
IrcProtocol extends GtkIO {
  protected
$nick;
  protected
$user;
  protected
$channels;

  protected
$timeout_id;
  protected
$irc_status;

  function
__construct() {
   
$this->timeout = 1000;
   
$this->timeout_id = Gtk::timeout_add($this->timeout,
      array(
$this, 'timeout'));
   
$this->irc_status = GTK_IRC_DISCONNECTED;
  }

  function
connect($hostname, $port,
   
$nick, $user, $channels) {
   
$status = parent::connect($hostname, $port);
    if (
$status === false)
      die(
"can't connect to irc server");

   
$this->irc_status = GTK_IRC_CONNECTED;
   
$this->nick = $nick;
   
$this->user = $user;

   
# initial channels to join;
   
$this->channels = $channels;

   
$this->nick($nick);
   
$this->user($user);

   
$this->join($channels);
  }

  function
send($str) {
    echo
"Irc::send($str)\n";
     
$this->write($str . "\n\r");
  }

  function
timeout() {
    static
$count = 0;
    echo
"timeout ...\n";
   
$count++;

   
# debug - for gtk_io function monitor
 
if ($count > 3) {
   
$this->send_chan('#php-gtk', "I'm going out; bye ...");
   
$this->disconnect();
    return;
    }

  if  (
$this->irc_status == GTK_IRC_CONNECTED
   
&& $this->fd != null)
   
$this->timeout_id = Gtk::timeout_add($this->timeout,
      array(
$this, 'timeout'));
   
$this->send_chan('#php-gtk', "hello ; I'm php-gtk-irc bot");
  }

  function
io_in($gio_channel, $conditions) {
   
$data = $this->read();
    echo
"$data\n";
  }

  function
io_hup($gio_channel, $conditions) {
    echo
"irc : disconnected\n";
   
$this->fd = null;
   
Gtk::main_quit();
  }

  function
disconnect() {
   
$this->send('QUIT');
   
$this->irc_status = GTK_IRC_DISCONNECTED;
   
Gtk::main_quit();
  }

  function
send_chan($chan, $msg) {
   
$this->send("PRIVMSG $chan :$msg");
  }

  function
nick($nick) {
   
$this->send("NICK $nick");
  }

  function
user($user) {
   
$this->send("USER $user $user $user localhost");
    }

  function
join($channels) {
    foreach(
$channels as $chan)
     
$this->send("JOIN $chan");
    }
  }

$irc = new IrcProtocol();

/**
* You'll need to change these parameters
* maybe to connect to chat.freenode.net
*/
$irc->connect(
 
$server   = 'irc.freenode.net',
 
$port     = '6667',
 
$nick     = 'mq-boot',
 
$user     = 'Marc'# put you name here ...
 
$channels = array('#php-gtk', '#php'));

Gtk::main();
?>

Notes :

  • Irc class is incomplete,
  • GtkIO::read() is very incomplete to.

Links

You can refer to thoses links for a complete documentation. Gtk::io_add_watch() is based on Glib function io_add_watch().

Note that, although the function is available on all platforms, this small example doesn't completely work on Windows.

Comentários

io_add_watch

Note:

If the function called by Gtk::io_add_watch does not return true, then Gtk will stop watching:

  • returning true tells gtk to keep on watching
  • returning false tells gtk to stop watching

GtkIO Gnope package

A package for this GtkIO class is now available in the gnope repository. This class is now separated in two parts :

  • GtkIO class
  • GtkSocketIO class

This is a nice contribution of Leon Pegg.

refresh Gtk::main_loop() events

This is nearly in the same chapter ; when you have long tasks with blocking or semi-blocking code, you can refresh the event loop like this :

<?php
while(1)
  {
 
do_some_work();
 
# refresh Gtk Window :
 
while(Gtk::events_pending())
   
Gtk::main_iteration();
  }
?>

Also, you can take into account this function when doing some backgroup task : Gtk::idle_add()

please take care of a little bug

there is a probleme with this script. When FD is closed (hup), you need to close watches with this function : Gtk::input_remove($id);

now, I have a Pipe based

now, I have a Pipe based class fully working here :

http://php-gtk.pastebin.ca/544279

you can use it and adjust to you needs.

Opções de exibição de comentários

Escolha seu modo de exibição preferido e clique em "Salvar configurações" para ativar.