Socket reader in PHP

There are lots of art­icles which show you how to cre­ate a socket reader in PHP. You can find them on google. My start­ing points were art­icles on devshed and zend’s devzone. Unfor­tu­nately bring too few light on me. Although articale of icarus, © Mel­on­fire was my start point in under­stnd­ing how to write a socket reader. So now I can dig a little bit deeper…

The best place to read about sock­ets in PHP was PHP Manual. It has great examples of using sock­ets in doc­u­ment­a­tion, which brings much more light then art­icles I men­tioned above. Example of server do not fin­ishes loop after cli­ent dis­con­nects, as it was in devshed’s art­icle. But it has a dis­ad­vant­age any­way. It do not handle mul­tiple con­nec­tions sim­u­laten­ously. So after once cli­ent con­nects, secocond cli­ent have to wait for responses until first cli­ent will disconnect.

I wanted to cre­ate a server which will handle mul­tiple con­nec­tions sim­u­laten­ously. In my sorry there’s now way to do in simple way. But I’m not the one who is look­ing for easy ways :)) So if you hae read example from doc­u­ment­a­tion I must noticed that the main hack is in nes­ted loop. So all we need is to run this nes­ted loop in another thread. But as we know PHP still do not know any­thing about threading.

We can sim­u­late mul­ti­th­read­ing in two ways. First is to imple­ment some kind of fair divi­sion of time between cli­ents, sim­ilar to how it handled in art­icle from devzone. Another way is to imple­ment two kind of dae­mons. First is the main, let’s call it dis­patcher. Second type is worker dae­mon which will handle real actions. So dis­patcher will return port of first avail­able and non-busy worker. After cli­ent recieves a resonse with host:port it will con­nect to worker to do all the work.

In this post I’m going to cre­ate a queue based server from scratch basing on PHP manual’s example in step by step man­ner. So next time I hope I’ll be able to imple­ment my second vari­ant described above. But for now I’ll cre­ate calss which I’ll refactor then to imple­ment in my Chroot­G­ate­way as one of com­mu­nic­a­tion adapters.

Now let’s step by step cre­ate our socket listener. First we’ll do some basic con­fig­ur­a­tion. You may not need it, but I left it from PHP Manual’s examples, as they seems pretty reasonable:

error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();

Then we define some vari­ables, $addr is an addres of inter­face which our list­ner should listen for and $port is a port num­ber. Third vari­able is some kind of place­holder for future socket resource:

$addr = '0.0.0.0';
$port = 12345;
$sock = null;

I have defined a socket_error($msg) func­tion which dis­plays an error if occured in the STDERR, closes socket and ter­min­ates execution:

function socket_error($msg)
{
    fwrite(STDERR, $msg . socket_strerror(socket_last_error()));

    // Close sockets
    global $sock;
    @socket_close($sock);

    exit(1);
}

Then we cre­at­ing a socket, bind­ing it and strat­ing to listen:

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (false === $sock) {
    socket_error('socket_create() failed');
}

if (false === socket_bind($sock, $addr, $port)) {
    socket_error('socket_bind() failed');
}

if (false === socket_listen($sock, 5)) {
    socket_error('socket_listen() failed');
}

All that last is to run an infin­ite loop of socket read­ing and listen­ing for new con­nec­tions. I have defined a $pool array which will store all con­nec­tions we have basic one $sock and all spawn ones retreived from socket_accept():

$pool = array($sock);

Inside infin­ite loop (while (true) { /* ... */ }) I clean the pool first on every cycle remov­ing all non-resources (if any) from it:

foreach ($pool as $conn_id => $conn) {
    if ( ! is_resource($conn)) {
        unset($pool[$conn_id]);
    }
}

As socket_listen() changes passed by ref­er­ence array, I’m cre­at­ing a copy pool:

$active = $pool;

If socket_select() fails I’m ter­min­at­ing a listener. You can add count of fails with con­juc­tion of last error occured time to build less capri­cious version :))

if (false === socket_select($active, $w = null, $e = null, null)) {
    socket_error('socket_select() failed');
}

make atten­tion on fourth argu­ment I passed in socket_select(). It’s NULL what will make this func­tion wait for any activ­ity. If you’ll pass 0 (zero) you’ll meet very aggress­ive CPU usage. If you don’t under­stand why — ask me :)) So after we have got an array of act­ive con­nec­tions, we need to first if there any new con­nec­tions, and if any register it in the pool. And don’t for­get to remove $sock from act­ive pool as all last is related to spawn socket (cli­ent con­netions) only:

if (in_array($sock, $active)) {
    $conn = socket_accept($sock);
    if (is_resource($conn)) {
        // Send welcome message
        $msg = PHP_EOL . 'WELCOME TO THE PHP SIMPLE SERVER' . PHP_EOL;
        socket_write($conn, $msg, strlen($msg));
        $pool[] = $conn;
    }
    unset($active[array_search($sock, $active)]);
}

Finally we can work with thing, cli­ents sent us. As you can see, server close con­nec­tion with cli­ent if cli­ent sents exit or quit request. And closes all con­nec­tions and fin­ish­ing it’s (server) loop upon stop and halt. For all other input md5 hash will be cal­cu­lated and responsed to requester:

foreach ($active as $conn) {
    $request = socket_read($conn, 2048, PHP_NORMAL_READ);

    // If connection is closed, remove it from pool and skip
    if (false === $request) {
        unset($pool[array_search($conn, $pool)]);
        continue;
    }

    $request = trim($request);

    // Skip to next if client tells nothing
    if (0 == strlen($request)) {
        continue;
    }

    printf('GOT: %s' . PHP_EOL, $request);

    if (1 === preg_match('/quit|exit/i', $request)) {
        socket_close($conn);
        unset($pool[array_search($conn, $pool)]);
        continue;
    }

    if (1 === preg_match('/stop|halt/i', $request)) {
        break 2;
    }

    $response = md5($request) . PHP_EOL;
    socket_write($conn, $response, strlen($response));
}

Finally, I’m clos­ing a con­nec­tion out­side the loop:

socket_close($sock);
exit(0);

Ok, now put­ting it all together, so you can simply copy-paste it for free. :)) And don’t affraid to post a com­ment :)) even if you think that I wrote some piece of sh*t.

< ?php

// Some basic configuration.
error_reporting(E_ALL);
set_time_limit(0);
ob_implicit_flush();

// Address and port to listen on
$addr = '0.0.0.0';
$port = 12345;
$sock = null;

// Helper to break execution upon socket related error
function socket_error($msg)
{
    fwrite(STDERR, $msg . socket_strerror(socket_last_error()));

    // Close sockets
    global $sock;
    @socket_close($sock);

    exit(1);
}

// Creating and binding socket
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
if (false === $sock) {
    socket_error('socket_create() failed');
}

if (false === socket_bind($sock, $addr, $port)) {
    socket_error('socket_bind() failed');
}

if (false === socket_listen($sock, 5)) {
    socket_error('socket_listen() failed');
}

// Client connections' pool
$pool = array($sock);

// Main cycle
while (true) {
    // Clean-up pool
    foreach ($pool as $conn_id => $conn) {
        if ( ! is_resource($conn)) {
            unset($pool[$conn_id]);
        }
    }

    // Create a copy of pool for socket_select()
    $active = $pool;

    // Halt execution if socket_select() failed
    if (false === socket_select($active, $w = null, $e = null, null)) {
        socket_error('socket_select() failed');
    }

    // Register new client in the pool
    if (in_array($sock, $active)) {
        $conn = socket_accept($sock);
        if (is_resource($conn)) {
            // Send welcome message
            $msg = PHP_EOL . 'WELCOME TO THE PHP SIMPLE SERVER' . PHP_EOL;
            socket_write($conn, $msg, strlen($msg));
            $pool[] = $conn;
        }
        unset($active[array_search($sock, $active)]);
    }

    // Handle every active client
    foreach ($active as $conn) {
        $request = socket_read($conn, 2048, PHP_NORMAL_READ);

        // If connection is closed, remove it from pool and skip
        if (false === $request) {
            unset($pool[array_search($conn, $pool)]);
            continue;
        }

        $request = trim($request);

        // Skip to next if client tells nothing
        if (0 == strlen($request)) {
            continue;
        }

        printf('GOT: %s' . PHP_EOL, $request);

        if (1 === preg_match('/quit|exit/i', $request)) {
            socket_close($conn);
            unset($pool[array_search($conn, $pool)]);
            continue;
        }

        if (1 === preg_match('/stop|halt/i', $request)) {
            break 2;
        }

        $response = md5($request) . PHP_EOL;
        socket_write($conn, $response, strlen($response));
    }
}

// Finally close socket
socket_close($sock);
exit(0);
  • del.icio.us
  • Google Bookmarks
  • Identi.ca
  • Twitter
  • Technorati
  • Digg
  • Slashdot
  • Facebook
  • MisterWong
  • Reddit
  • StumbleUpon
  • Mixx
  • HelloTxt
  • LinkedIn
  • PDF
  • email
  • Print
This entry was posted in Development, Networking and tagged , , . Bookmark the permalink.

2 Responses to Socket reader in PHP

  1. Ale says:

    It looks like very use­ful, but I think that my prob­lem is not solved with this hint.

    I need to read packet sent by a pc to my address. I remain listen­ing to the receive port but I can’t see noth­ing on the screen (timeout or all clean).

    I’m using fsock­o­pen instead of socket_create, but I can’t exit from the infin­ite loop.

    I need to catch the data and save them in a file or on a DB, but it’s impossible for me now.

    Can you help me?

  2. ixti says:

    @Ale
    Right now I don’t have much time but I hope I will on week­end, so if you’ll explain your prob­lem in details and give me sources which fails — I’ll try to help.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>