Note that there are some explanatory texts on larger screens.

plurals
  1. POStrange timeouts while connecting PHP sockets
    primarykey
    data
    text
    <h2>The system</h2> <p>I have written a simple message distribution system based on PHP and Apache for a (business) client. A simplified version of the data flow looks like this: A JSON message is sent over HTTP to a a PHP script running on a Apache server which does some processing on it and in turn distributes it, again using HTTP, to a number of clients, approx. 10-20. These messages occur at different intervals at up to 5 messages per second, but sometimes as infrequent as one per minute.</p> <p>The server used is a beefy Ubuntu machine with 8gB memory and a fast network card located in a well-connected data warehouse.</p> <p>Since the client has very strict requirements regarding the response time (for messages expecting a reply, the response time has to be below 200ms), I have implemented my own HttpRequest class to allow for very fine grained control over various timeouts and execution duration. The server uses this class to forward messages to its clients. The interesting part looks something like this:</p> <pre><code>class HttpRequest { private $host; private $port; private $path; private $connection = null; private $headers = array(); [...] const CONTENT_TYPE = 'application/json'; const ENCODING = 'utf-8'; const TIMEOUT = 0.2; const MAX_RESPONSE_SIZE = 0xFFFF; // 64 kB public function __construct($url, $callback = null) { if (empty($url)) { throw new HttpException("url cannot be empty"); } $url_parts = parse_url($url); $this-&gt;host = $url_parts['host']; $this-&gt;port = (empty($url_parts['port']) ? 80 : $url_parts['port']); $this-&gt;path = $url_parts['path']; $this-&gt;headers['Host'] = $this-&gt;host; $this-&gt;headers['Connection'] = 'close'; $this-&gt;headers['Cache-Control'] = 'no-cache'; } public function __destruct() { try { $this-&gt;disconnect(); } catch (Exception $e) { } } private function connect() { if ($this-&gt;connection != null) { // already connected, simply return return; } $errno = ''; $errstr = ''; $_timeout = self::TIMEOUT; $this-&gt;connection = @fsockopen($this-&gt;host, $this-&gt;port, $errno, $errstr, $_timeout); if ($this-&gt;connection === false) { throw new HttpException("error during connect: $errstr", ($errno == SOCKET_ETIMEDOUT)); } stream_set_timeout($this-&gt;connection, (int)(floor($_timeout)), (int)(($_timeout - floor($_timeout)) * 1000000)); [variable assignments] } private function disconnect() { if ($this-&gt;connection == null) { // already disconnected, simply return return; } @fclose($this-&gt;connection); $this-&gt;connection = null; } public function post($data, $fetch_response = true, $path = null) { if (empty($data)) { throw new HttpException("no data given", false); } $contenttype = 'application/x-www-form-urlencoded'; if (is_string($data)) { $data = urlencode($data); } else if (is_array($data)) { $data = http_build_query($data); } else if ($data instanceof Message) { $data = json_encode($data); $contenttype = 'application/json'; } if (!is_string($data)) { throw new HttpException("wrong datatype", false); } $encoding = mb_detect_encoding($data); if ($encoding != self::ENCODING) { $data = mb_convert_encoding($data, self::ENCODING, $encoding); } if (empty($path)) { $path = $this-&gt;path; } // set header values $this-&gt;headers['Content-Type'] = $contenttype . '; charset=' . self::ENCODING; $this-&gt;headers['Content-Length'] = mb_strlen($data); // build request $request = "POST $path HTTP/1.1\r\n"; foreach ($this-&gt;headers as $header =&gt; $value) { $request .= "$header: $value\r\n"; } $request .= "\r\n$data"; // and send it $this-&gt;sendRequest($request, $fetch_response); if ($fetch_response) { // fetch and parse response $resp = $this-&gt;receiveResponse(); return $resp; } } public function get($path = null) { [build and execute http query] } private function sendRequest($request, $keep_connected = true) { // connect the socket $this-&gt;connect(); [timer1] // write out data $result = @fwrite($this-&gt;connection, $request); [timer2] if ($result === false || $result != mb_strlen($request)) { $this-&gt;disconnect(); throw new HttpException("write to socket failed", false); } if (!$keep_connected) { $this-&gt;disconnect(); } } private function receiveResponse() { [fetch response using stream_select() and fgets() while strictly observing the timeout] } private function parseLine($msg) { [process http response, used in receiveResponse()] } } </code></pre> <p>This class is usually used like this:</p> <pre><code>$request = new HttpRequest($url); $request-&gt;post($data); </code></pre> <h2>The problem</h2> <p>Every once in a while some messages expire with timeouts of over five seconds being recorded. Just from the code this should be impossible, since every call to IO-related functions should time out long before this amount of time.</p> <p>Profiling statements (indicated in the code as [timer1] and [timer2]) have revealed that the call to HttpRequest->connect() is where this delay is happening. My best guess would be that fsockopen() for some reason ignores the timeout handed to it.</p> <p>Interestingly, whenever there are timeouts that exceed the given limit, they are usually just over 5 seconds, leading me to believe that there is a 5 second delay somewhere in the lower layers of network code (eg. exhaustion of socket resources). DNS-related issues can probably be ruled out, since this behavior also occurs when the host is specified using IP addresses.</p> <p>The problem usually occurs when a message is to be distributed to many clients, meaning many requests will be sent in rapid succession, but I have also noticed it when sending a single message to only one client. It appears to generally, but not neccessarily, occur when there are multiple simultaneous requests to Apache.</p> <p>Has anyone experienced a similar problem? The interwebs have not been very exhaustive, neither has working through the PHP source code. Any pointers on how to approach this issue?</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload