-
-
Save brianlmoon/442310033bf44565bddd to your computer and use it in GitHub Desktop.
<?php | |
/** | |
* I was having trouble with socket connections timing out reliably. Sometimes, | |
* my timeout would be reached. Other times, the connect would fail after three | |
* to six seconds. I finally figured out it had to do with trying to connect to | |
* a routable, non-localhost address. It seems the socket_connect call would | |
* not fail immediately for those connections. This function is what I finally | |
* ended up with that reliably connects to a working server, fails quickly for | |
* a server that has an address/port that is not reachable and will reach the | |
* timeout for routable addresses that are not up. | |
* | |
* Full Story: http://brian.moonspot.net/socket-connect-timeout | |
* | |
* Copyright (c) 2015, Brian Moon of DealNews.com, Inc. | |
* All rights reserved. | |
* | |
* Redistribution and use in source and binary forms, with or without | |
* modification, are permitted provided that the following conditions | |
* are met: | |
* | |
* * Redistributions of source code must retain the above copyright | |
* notice, this list of conditions and the following disclaimer. | |
* * Redistributions in binary form must reproduce the above | |
* copyright notice, this list of conditions and the following | |
* disclaimer in the documentation and/or other materials provided | |
* with the distribution. | |
* * Neither the name of DealNews.com Inc. nor the names of its | |
* contributors may be used to endorse or promote products derived | |
* from this software without specific prior written permission. | |
* | |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS | |
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE | |
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | |
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) | |
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, | |
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED | |
* OF THE POSSIBILITY OF SUCH DAMAGE. | |
* | |
*/ | |
/** | |
* Example: | |
* | |
* Assuming these hosts are valid for your setup, this example would produce | |
* something like this: | |
* | |
* Trying: Good host... | |
* resource(10) of type (Socket) | |
* | |
* Trying: Routable, but not up... | |
* Failed to connect to 10.1.2.30:4730. (timed out after 102.1051ms) | |
* NULL | |
* | |
* Trying: Up but not listening... | |
* Failed to connect to 127.0.0.1:7676. (111: Connection refused; after 0.051ms) | |
* NULL | |
* | |
* ===== | |
* | |
* $timeout = 100; | |
* | |
* $hosts = array( | |
* array( | |
* "desc" => "Good host", | |
* "host" => "127.0.0.1", | |
* "port" => "4730" | |
* ), | |
* array( | |
* "desc" => "Routable, but not up", | |
* "host" => "10.1.2.30", | |
* "port" => "4730" | |
* ), | |
* array( | |
* "desc" => "Up but not listening", | |
* "host" => "127.0.0.1", | |
* "port" => "7676" | |
* ), | |
* ); | |
* | |
* foreach($hosts as $host){ | |
* | |
* echo "Trying: $host[desc]...\n"; | |
* | |
* try{ | |
* $socket = socket_connect_timeout($host["host"], $host["port"], $timeout); | |
* } catch(Exception $e){ | |
* echo $e->getMessage()."\n"; | |
* $socket = null; | |
* } | |
* | |
* var_dump($socket); | |
* | |
* echo "\n"; | |
* | |
* } | |
* | |
*/ | |
function socket_connect_timeout($host, $port, $timeout=100){ | |
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); | |
/** | |
* Set the send and receive timeouts super low so that socket_connect | |
* will return to us quickly. We then loop and check the real timeout | |
* and check the socket error to decide if its conected yet or not. | |
*/ | |
$connect_timeval = array( | |
"sec"=>0, | |
"usec" => 100 | |
); | |
socket_set_option( | |
$socket, | |
SOL_SOCKET, | |
SO_SNDTIMEO, | |
$connect_timeval | |
); | |
socket_set_option( | |
$socket, | |
SOL_SOCKET, | |
SO_RCVTIMEO, | |
$connect_timeval | |
); | |
$now = microtime(true); | |
/** | |
* Loop calling socket_connect. As long as the error is 115 (in progress) | |
* or 114 (already called) and our timeout has not been reached, keep | |
* trying. | |
*/ | |
$err = null; | |
$socket_connected = false; | |
do{ | |
socket_clear_error($socket); | |
$socket_connected = @socket_connect($socket, $host, $port); | |
$err = socket_last_error($socket); | |
$elapsed = (microtime(true) - $now) * 1000; | |
} | |
while (($err === 115 || $err === 114) && $elapsed < $timeout); | |
/** | |
* For some reason, socket_connect can return true even when it is | |
* not connected. Make sure it returned true the last error is zero | |
*/ | |
$socket_connected = $socket_connected && $err === 0; | |
if($socket_connected){ | |
/** | |
* Set keep alive on so the other side does not drop us | |
*/ | |
socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1); | |
/** | |
* set the real send/receive timeouts here now that we are connected | |
*/ | |
$timeval = array( | |
"sec" => 0, | |
"usec" => 0 | |
); | |
if($timeout >= 1000){ | |
$ts_seconds = $timeout / 1000; | |
$timeval["sec"] = floor($ts_seconds); | |
$timeval["usec"] = ($ts_seconds - $timeval["sec"]) * 1000000; | |
} else { | |
$timeval["usec"] = $timeout * 1000; | |
} | |
socket_set_option( | |
$socket, | |
SOL_SOCKET, | |
SO_SNDTIMEO, | |
$timeval | |
); | |
socket_set_option( | |
$socket, | |
SOL_SOCKET, | |
SO_RCVTIMEO, | |
$timeval | |
); | |
} else { | |
$elapsed = round($elapsed, 4); | |
if(!is_null($err) && $err !== 0 && $err !== 114 && $err !== 115){ | |
$message = "Failed to connect to $host:$port. ($err: ".socket_strerror($err)."; after {$elapsed}ms)"; | |
} else { | |
$message = "Failed to connect to $host:$port. (timed out after {$elapsed}ms)"; | |
} | |
throw new Exception($message); | |
} | |
return $socket; | |
} |
@rotexdegba thanks for pointing those out. I extracted this from a lib to make it something I could share and messed up some variables. I will get them fixed.
Updated the script with typos fixed. I never have a timeout over 1000ms, so I did not hit that part of the if. Sorry about that.
Hey I tried implementing this code, set the $timeout
to 100
and this is what I get back
Failed to connect to xx.xx.xx. (60: Operation timed out; after 75069.3259ms)
Running on PHP Version 5.6.8
Also checked some of the PHP config values and the default_socket_timeout
is even set to 60
so I'm not sure how it's staying alive for 75 seconds anyways? Any advice?
@frostover default_socket_timeout
meaning 'sending or receiving data' timeout, not 'connect timeout'.
Where does the variable $tv_sec on line 169 come from? It seems like it's not being declared anywhere. Also on lines 168, 169 and 171, the array $timeval is spelt incorrectly as $timval.
Thanks for making this code public.