views:

33

answers:

1

Below is partial code to an experimental http server app I'm building from scratch from a PHP CLI script (Why? Because I have too much time on my hands). The example below more closely matches PHP's manual page on this function. The problem I'm getting is when connecting to this server app via a browser (Firefox or IE8 from two separate systems tested so far), the browser sends an empty request payload to the server and aborts roughly every 1 in 6 page loads.

The server console displays the "Connected with [client info]" each time. However, about 1 in 6 connections will result in a "Client request is empty" error. No error is given telling the header/body response write to the socket failed. The browser will generally continue to read what I give it, but this isn't usable as I can't fulfill the client's intended request without knowing what it is.

<?php

$s_socket_uri = 'tcp://localhost:80';
// establish the server on the above socket
$s_socket = stream_socket_server($s_socket_uri, $errno, $errstr, 30) OR
    trigger_error("Failed to create socket: $s_socket_uri, Err($errno) $errstr", E_USER_ERROR);
$s_name = stream_socket_get_name($s_socket, false) OR
    trigger_error("Server established, yet has no name.  Fail!", E_USER_ERROR);
if (!$s_socket || !$s_name) {return false;}

/*
   Wait for connections, handle one client request at a time
   Though to not clog up the tubes, maybe a process fork is
   needed to handle each connection?
*/
while($conn = stream_socket_accept($s_socket, 60, $peer)) {
    stream_set_blocking($conn, 0);

    // Get the client's request headers, and all POSTed values if any
    echo "Connected with $peer.  Request info...\n";
    $client_request = stream_get_contents($conn);
    if (!$client_request) {
        trigger_error("Client request is empty!");
        }
    echo $client_request."\n\n";  // just for debugging

    /*
      <Insert request handling and logging code here>
    */

    // Build headers to send to client
    $send_headers = "HTTP/1.0 200 OK\n"
        ."Server: mine\n"
        ."Content-Type: text/html\n"
        ."\n";

    // Build the page for client view
    $send_body = "<h1>hello world</h1>";

    // Make sure the communication is still active
    if ((int) fwrite($conn, $send_headers . $send_body) < 1) {
        trigger_error("Write to socket failed!");
        }

    // Response headers and body sent, time to end this connection
    stream_socket_shutdown($conn, STREAM_SHUT_WR);
    }

?>

Any solution to bring down the number of unintended aborts down to 0, or any method to get more stable communication going? Is this solvable on my server's end, or just typical browser behavior?

+1  A: 

I tested your code and it seems I got better results reading the socket with fread(). You also forgot the main loop(while(1), while(true) or for(;;).

Modifications to your code:

  • stream_socket_accept with @stream_socket_accept [sometimes you get warnings because "the connected party did not properly respond", which is, of course, the timeout of stream_socket_accept()]
  • Added the big while(1) { } loop
  • Changed the reading from the socket from $client_request = stream_get_contents($conn); to while( !preg_match('/\r?\n\r?\n/', $client_request) ) { $client_request .= fread($conn, 1024); }

Check the source code below (I used 8080 port because I already had an Apache listening on 80):

<?php

$s_socket_uri = 'tcp://localhost:8080';
$s_socket = stream_socket_server($s_socket_uri, $errno, $errstr, 30) OR
    trigger_error("Failed to create socket: $s_socket_uri, Err($errno) $errstr", E_USER_ERROR);
$s_name = stream_socket_get_name($s_socket, false) OR
    trigger_error("Server established, yet has no name.  Fail!", E_USER_ERROR);
if (!$s_socket || !$s_name) {return false;}

while(1)
{
    while($conn = @stream_socket_accept($s_socket, 60, $peer)) 
    {
        stream_set_blocking($conn, 0);
        echo "Connected with $peer.  Request info...\n";
        //    $client_request = stream_get_contents($conn);

        $client_request = "";
        // Read until double \r
        while( !preg_match('/\r?\n\r?\n/', $client_request) )
        {
            $client_request .= fread($conn, 1024);
        }

        if (!$client_request) 
        {
            trigger_error("Client request is empty!");
        }
        echo $client_request."\n\n";
        $headers = "HTTP/1.0 200 OK\n"
            ."Server: mine\n"
            ."Content-Type: text/html\n"
            ."\n";
        $body = "<h1>hello world</h1><br><br>".$client_request;
        if ((int) fwrite($conn, $headers . $body) < 1) {
            trigger_error("Write to socket failed!");
            }
    stream_socket_shutdown($conn, STREAM_SHUT_WR);
    }
}
Bogdan Constantinescu
Cool. Much better. For some reason, I was thinking `stream_get_contents()` actually waited until the full request was received. I guess the client didn't have time to send, being shoved out when the next `while($conn = ...` came and overwrote the connection. It looks like the `while( !preg_match...` loop stalls and blocks other connections if the client is slow or intentionally aborted. Should I fork off the process at that point or do you know of a better way?
bob-the-destroyer
Also, it looks like this works for GET requests since the header section seems to end with [line feed][Carriage return]. But, no reliable character pattern is returned on POST requests with post data just below that. I'm stuck figuring out how the client can tell me when their request message is complete.
bob-the-destroyer
Just found out the main `while(1)` loop is not necessary. `stream_socket_accept()` timeout can be set to -1 (never). Doing that may be better to capture any other types of errors that function may spit out (if it even has more internal error cases defined). All in all you gave insight into the fact that I specifically have to wait until the client finishes sending me their full request, rather than just processing whatever I'm given right away. Maybe I should read more deeply into http specs first to know how to handle them. I'll mark this accepted if no one else responds soon. Thanks!
bob-the-destroyer