-
-
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.