diff --git a/src/connection/Socket.php b/src/connection/Socket.php index 79cd3ad..0dc5bd4 100644 --- a/src/connection/Socket.php +++ b/src/connection/Socket.php @@ -16,19 +16,25 @@ class Socket extends AConnection { /** - * @var resource|\Socket|bool + * @var \Socket|bool */ private $socket = false; - private const POSSIBLE_TIMEOUTS_CODES = [11, 10060]; - private const POSSIBLE_RETRY_CODES = [4, 10004]; + private const POSSIBLE_RETRY_CODES = [ + SOCKET_EINTR, + SOCKET_EWOULDBLOCK + ]; - public function connect(): bool + public function __construct(string $ip = '127.0.0.1', int $port = 7687, float $timeout = 15) { if (!extension_loaded('sockets')) { throw new ConnectException('PHP Extension sockets not enabled'); } + parent::__construct($ip, $port, $timeout); + } + public function connect(): bool + { $this->socket = @socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($this->socket === false) { throw new ConnectException('Cannot create socket'); @@ -42,10 +48,9 @@ public function connect(): bool socket_set_option($this->socket, SOL_SOCKET, SO_KEEPALIVE, 1); $this->configureTimeout(); - $conn = @socket_connect($this->socket, $this->ip, $this->port); - if (!$conn) { - $code = socket_last_error($this->socket); - throw new ConnectException(socket_strerror($code), $code); + $start = microtime(true); + if (!@socket_connect($this->socket, $this->ip, $this->port)) { + $this->throwConnectException($start); } return true; @@ -57,14 +62,21 @@ public function write(string $buffer): void throw new ConnectException('Not initialized socket'); } - if (Bolt::$debug) + if (Bolt::$debug) { $this->printHex($buffer); + } + $start = microtime(true); $size = mb_strlen($buffer, '8bit'); while (0 < $size) { $sent = @socket_write($this->socket, $buffer, $size); - if ($sent === false) - $this->throwConnectException(); + if ($sent === false || $sent === 0) { + if (in_array(socket_last_error($this->socket), self::POSSIBLE_RETRY_CODES, true)) { + continue; + } + $this->throwConnectException($start); + } + $buffer = mb_strcut($buffer, $sent, null, '8bit'); $size -= $sent; } @@ -72,25 +84,32 @@ public function write(string $buffer): void public function read(int $length = 2048): string { - if ($this->socket === false) + if ($this->socket === false) { throw new ConnectException('Not initialized socket'); + } $output = ''; - $t = microtime(true); + $start = microtime(true); do { - if (mb_strlen($output, '8bit') == 0 && $this->timeout > 0 && (microtime(true) - $t) >= $this->timeout) - throw new ConnectionTimeoutException('Read from connection reached timeout after ' . $this->timeout . ' seconds.'); - $readed = @socket_read($this->socket, $length - mb_strlen($output, '8bit')); - if ($readed === false) { - if (in_array(socket_last_error($this->socket), self::POSSIBLE_RETRY_CODES, true)) + if ($this->timeout > 0 && (microtime(true) - $start) >= $this->timeout) { + $this->throwConnectException($start); + } + $readed = ''; + $result = @socket_recv($this->socket, $readed, $length - mb_strlen($output, '8bit'), 0); + if ($result === false) { + if (in_array(socket_last_error($this->socket), self::POSSIBLE_RETRY_CODES, true)) { continue; - $this->throwConnectException(); + } + $this->throwConnectException($start); + } elseif ($result === 0) { + throw new ConnectException('Connection closed by remote host'); } $output .= $readed; } while (mb_strlen($output, '8bit') < $length); - if (Bolt::$debug) + if (Bolt::$debug) { $this->printHex($output, 'S: '); + } return $output; } @@ -111,23 +130,28 @@ public function setTimeout(float $timeout): void private function configureTimeout(): void { - if ($this->socket === false) + if ($this->socket === false) { return; - $timeoutSeconds = floor($this->timeout); - $microSeconds = floor(($this->timeout - $timeoutSeconds) * 1000000); + } + $timeoutSeconds = (int)floor($this->timeout); + $microSeconds = (int)floor(($this->timeout - $timeoutSeconds) * 1000000); $timeoutOption = ['sec' => $timeoutSeconds, 'usec' => $microSeconds]; socket_set_option($this->socket, SOL_SOCKET, SO_RCVTIMEO, $timeoutOption); socket_set_option($this->socket, SOL_SOCKET, SO_SNDTIMEO, $timeoutOption); } /** + * Throws an exception based on the last socket error or timeout. + * @param float|null $start * @throws ConnectException * @throws ConnectionTimeoutException */ - private function throwConnectException(): void + private function throwConnectException(float|null $start = null): void { $code = socket_last_error($this->socket); - if (in_array($code, self::POSSIBLE_TIMEOUTS_CODES)) { + if ($code === SOCKET_ETIMEDOUT) { + throw new ConnectionTimeoutException('Connection timeout reached after ' . $this->timeout . ' seconds.'); + } elseif ($start !== null && $this->timeout > 0 && (microtime(true) - $start) >= $this->timeout) { throw new ConnectionTimeoutException('Connection timeout reached after ' . $this->timeout . ' seconds.'); } elseif ($code !== 0) { throw new ConnectException(socket_strerror($code), $code); diff --git a/tests/BoltTest.php b/tests/BoltTest.php index d23976a..53b2db9 100644 --- a/tests/BoltTest.php +++ b/tests/BoltTest.php @@ -21,7 +21,7 @@ public function testSockets(): void if (!extension_loaded('sockets')) $this->markTestSkipped('Sockets extension not available'); - $conn = new \Bolt\connection\Socket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687, 3); + $conn = new \Bolt\connection\Socket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT'], 3); $this->assertInstanceOf(\Bolt\connection\Socket::class, $conn); $bolt = new Bolt($conn); @@ -58,7 +58,7 @@ public function testAura(): void public function testHello(): AProtocol { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn); @@ -150,7 +150,7 @@ public function testRoute(AProtocol $protocol): void if (version_compare($protocol->getVersion(), 4.3, '>=')) { $response = $protocol ->route([ - 'address' => ($GLOBALS['NEO_HOST'] ?? '127.0.0.1') . ':' . ($GLOBALS['NEO_PORT'] ?? 7687) + 'address' => $GLOBALS['NEO_HOST'] . ':' . $GLOBALS['NEO_PORT'] ]) ->getResponse(); $this->assertEquals(Signature::SUCCESS, $response->signature); diff --git a/tests/TestLayer.php b/tests/TestLayer.php index d7f7362..21a5b32 100644 --- a/tests/TestLayer.php +++ b/tests/TestLayer.php @@ -23,9 +23,13 @@ public static function setUpBeforeClass(): void $host = getenv('GDB_HOST'); if (!empty($host)) $GLOBALS['NEO_HOST'] = $host; + if (!isset($GLOBALS['NEO_HOST'])) + $GLOBALS['NEO_HOST'] = '127.0.0.1'; $port = getenv('GDB_PORT'); if (!empty($port)) - $GLOBALS['NEO_PORT'] = $port; + $GLOBALS['NEO_PORT'] = (int)$port; + if (!isset($GLOBALS['NEO_PORT'])) + $GLOBALS['NEO_PORT'] = 7687; } /** diff --git a/tests/connection/ConnectionTest.php b/tests/connection/ConnectionTest.php index 195320b..26c5b80 100644 --- a/tests/connection/ConnectionTest.php +++ b/tests/connection/ConnectionTest.php @@ -120,6 +120,6 @@ public function testTimeoutRecoverAndReset(string $alias): void private function getConnection(string $class): IConnection { - return new $class($GLOBALS['NEO_HOST'] ?? '127.0.0.1', (int)($GLOBALS['NEO_PORT'] ?? 7687), 1); + return new $class($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT'], 1); } } diff --git a/tests/error/ErrorsTest.php b/tests/error/ErrorsTest.php index 73ceff0..ab4870f 100644 --- a/tests/error/ErrorsTest.php +++ b/tests/error/ErrorsTest.php @@ -33,7 +33,7 @@ public function testPackException1(): void public function testPackException2(): void { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new \Bolt\Bolt($conn); diff --git a/tests/packstream/v1/BytesTest.php b/tests/packstream/v1/BytesTest.php index 2717b1a..6abd3b0 100644 --- a/tests/packstream/v1/BytesTest.php +++ b/tests/packstream/v1/BytesTest.php @@ -15,7 +15,7 @@ class BytesTest extends TestLayer { public function testInit(): AProtocol { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn); diff --git a/tests/packstream/v1/PackerTest.php b/tests/packstream/v1/PackerTest.php index 1caa956..92b55c8 100644 --- a/tests/packstream/v1/PackerTest.php +++ b/tests/packstream/v1/PackerTest.php @@ -17,7 +17,7 @@ class PackerTest extends TestLayer { public function testInit(): AProtocol { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn); diff --git a/tests/packstream/v1/UnpackerTest.php b/tests/packstream/v1/UnpackerTest.php index a0eef2a..27c5c03 100644 --- a/tests/packstream/v1/UnpackerTest.php +++ b/tests/packstream/v1/UnpackerTest.php @@ -18,7 +18,7 @@ class UnpackerTest extends TestLayer { public function testInit(): AProtocol { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn); diff --git a/tests/structures/V6/StructuresTest.php b/tests/structures/V6/StructuresTest.php index 0da9557..c9d576b 100644 --- a/tests/structures/V6/StructuresTest.php +++ b/tests/structures/V6/StructuresTest.php @@ -17,7 +17,7 @@ class StructuresTest extends \Bolt\tests\structures\StructureLayer { public function testInit(): AProtocol { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn); diff --git a/tests/structures/v1/StructuresTest.php b/tests/structures/v1/StructuresTest.php index fba19aa..8f9b864 100644 --- a/tests/structures/v1/StructuresTest.php +++ b/tests/structures/v1/StructuresTest.php @@ -39,7 +39,7 @@ class StructuresTest extends \Bolt\tests\structures\StructureLayer { public function testInit(): AProtocol|V4_4|V4_3|V4_2|V3 { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn); diff --git a/tests/structures/v4_3/StructuresTest.php b/tests/structures/v4_3/StructuresTest.php index 573beb2..a87359a 100644 --- a/tests/structures/v4_3/StructuresTest.php +++ b/tests/structures/v4_3/StructuresTest.php @@ -30,7 +30,7 @@ class StructuresTest extends \Bolt\tests\structures\StructureLayer { public function testInit(): AProtocol|V4_4|V4_3 { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn); diff --git a/tests/structures/v5/StructuresTest.php b/tests/structures/v5/StructuresTest.php index ca2e6dc..2887d9e 100644 --- a/tests/structures/v5/StructuresTest.php +++ b/tests/structures/v5/StructuresTest.php @@ -4,11 +4,7 @@ use Bolt\Bolt; use Bolt\protocol\AProtocol; -use Bolt\tests\structures\v1\DateTimeTrait; -use Bolt\tests\structures\v1\DateTimeZoneIdTrait; use Bolt\protocol\v5\structures\{ - DateTime, - DateTimeZoneId, Node, Relationship, UnboundRelationship @@ -26,7 +22,7 @@ class StructuresTest extends \Bolt\tests\structures\StructureLayer { public function testInit(): AProtocol { - $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'] ?? '127.0.0.1', $GLOBALS['NEO_PORT'] ?? 7687); + $conn = new \Bolt\connection\StreamSocket($GLOBALS['NEO_HOST'], $GLOBALS['NEO_PORT']); $this->assertInstanceOf(\Bolt\connection\StreamSocket::class, $conn); $bolt = new Bolt($conn);