Wrap Socket

This commit is contained in:
2021-01-30 20:07:28 +03:00
parent e3485416a5
commit 72dca5c73b
10 changed files with 288 additions and 214 deletions

View File

@@ -9,139 +9,98 @@ use Nsq\Config\ConnectionConfig;
use Nsq\Exception\AuthenticationRequired;
use Nsq\Exception\BadResponse;
use Nsq\Exception\ConnectionFail;
use Nsq\Exception\NotConnected;
use Nsq\Exception\NsqError;
use Nsq\Exception\NsqException;
use Nsq\Exception\NullReceived;
use Nsq\Protocol\Error;
use Nsq\Protocol\Frame;
use Nsq\Protocol\Message;
use Nsq\Protocol\Response;
use Nsq\Reconnect\ExponentialStrategy;
use Nsq\Reconnect\ReconnectStrategy;
use Nsq\Socket\RawSocket;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Socket\Raw\Exception;
use Socket\Raw\Factory;
use Socket\Raw\Socket;
use Throwable;
use function addcslashes;
use function http_build_query;
use function implode;
use function json_encode;
use const JSON_FORCE_OBJECT;
use const JSON_THROW_ON_ERROR;
use const PHP_EOL;
/**
* @internal
*
* @property ConnectionConfig $connectionConfig
*/
abstract class Connection
{
use LoggerAwareTrait;
private string $address;
protected ClientConfig $clientConfig;
private Buffer $input;
private NsqSocket $socket;
private Buffer $output;
private ConnectionConfig $connectionConfig;
private ?Socket $socket = null;
private ReconnectStrategy $reconnect;
private ClientConfig $clientConfig;
private ?ConnectionConfig $connectionConfig = null;
private bool $closed = false;
public function __construct(
string $address,
private string $address,
ClientConfig $clientConfig = null,
ReconnectStrategy $reconnectStrategy = null,
LoggerInterface $logger = null,
) {
$this->address = $address;
$this->input = new Buffer();
$this->output = new Buffer();
$this->logger = $logger ?? new NullLogger();
$this->reconnect = $reconnectStrategy ?? new ExponentialStrategy(logger: $this->logger);
$this->clientConfig = $clientConfig ?? new ClientConfig();
}
public function connect(): void
{
$this->reconnect->connect(function (): void {
try {
$this->socket = (new Factory())->createClient($this->address);
}
// @codeCoverageIgnoreStart
catch (Exception $e) {
$this->logger->error('Connecting to {address} failed.', ['address' => $this->address]);
$socket = new RawSocket($this->address, $this->logger);
$socket->write(' V2');
throw ConnectionFail::fromThrowable($e);
}
// @codeCoverageIgnoreEnd
$this->socket = new NsqSocket($socket);
$this->socket->write(' V2');
$this->connectionConfig = ConnectionConfig::fromArray(
$this
->command('IDENTIFY', data: $this->clientConfig->toString())
->readResponse()
->toArray()
);
$body = json_encode($this->clientConfig, JSON_THROW_ON_ERROR | JSON_FORCE_OBJECT);
if ($this->connectionConfig->snappy || $this->connectionConfig->deflate) {
$this->checkIsOK();
}
$this->connectionConfig = ConnectionConfig::fromArray(
$this
->command('IDENTIFY', data: $body)
->readResponse()
->toArray()
);
if ($this->connectionConfig->snappy || $this->connectionConfig->deflate) {
$this->checkIsOK();
if ($this->connectionConfig->authRequired) {
if (null === $this->clientConfig->authSecret) {
throw new AuthenticationRequired();
}
if ($this->connectionConfig->authRequired) {
if (null === $this->clientConfig->authSecret) {
throw new AuthenticationRequired();
}
$authResponse = $this
->command('AUTH', data: $this->clientConfig->authSecret)
->readResponse()
->toArray()
;
$authResponse = $this
->command('AUTH', data: $this->clientConfig->authSecret)
->readResponse()
->toArray()
;
$this->logger->info('Authorization response: '.http_build_query($authResponse));
}
});
$this->logger->info('Authorization response: '.http_build_query($authResponse));
}
}
/**
* Cleanly close your connection (no more messages are sent).
*/
public function disconnect(): void
public function close(): void
{
if (null === $this->socket) {
if ($this->closed) {
return;
}
try {
$this->socket->write('CLS'.PHP_EOL);
$this->command('CLS');
$this->socket->close();
} catch (Throwable) {
}
// @codeCoverageIgnoreStart
catch (Exception $e) {
$this->logger->debug($e->getMessage(), ['exception' => $e]);
}
// @codeCoverageIgnoreEnd
$this->socket = null;
$this->connectionConfig = null;
$this->closed = true;
}
public function isReady(): bool
public function isClosed(): bool
{
return null !== $this->socket;
return $this->closed;
}
/**
@@ -149,49 +108,45 @@ abstract class Connection
*/
protected function command(string $command, array | string $params = [], string $data = null): self
{
$this->output->appendCommand(
[] === $params
? $command
: implode(' ', [$command, ...((array) $params)]),
);
if (null !== $data) {
$this->output->appendData($data);
if ($this->closed) {
throw new NotConnected('Connection closed.');
}
$this->flush();
$command = [] === $params
? $command
: implode(' ', [$command, ...((array) $params)]);
$this->socket->write($command, $data);
return $this;
}
public function hasMessage(float $timeout = 0): bool
public function hasMessage(float $timeout): bool
{
try {
return false !== $this->socket()->selectRead($timeout);
if ($this->closed) {
throw new NotConnected('Connection closed.');
}
// @codeCoverageIgnoreStart
catch (Exception $e) {
$this->disconnect();
throw ConnectionFail::fromThrowable($e);
try {
return false !== $this->socket->wait($timeout);
} catch (ConnectionFail $e) {
$this->close();
throw $e;
}
// @codeCoverageIgnoreEnd
}
protected function readFrame(float $timeout = null): ?Frame
protected function readFrame(): Frame
{
$timeout ??= $this->clientConfig->readTimeout;
$deadline = microtime(true) + $timeout;
if (!$this->hasMessage($timeout)) {
return null;
if ($this->closed) {
throw new NotConnected('Connection closed.');
}
$buffer = $this->read();
$buffer = $this->socket->read();
$this->logger->debug('Received buffer: '.addcslashes($buffer->bytes(), PHP_EOL));
$frame = match ($type = $buffer->consumeType()) {
return match ($type = $buffer->consumeType()) {
0 => new Response($buffer->flush()),
1 => new Error($buffer->flush()),
2 => new Message(
@@ -203,16 +158,6 @@ abstract class Connection
),
default => throw new NsqException('Unexpected frame type: '.$type)
};
if ($frame instanceof Response && $frame->isHeartBeat()) {
$this->command('NOP');
return $this->readFrame(
($currentTime = microtime(true)) > $deadline ? 0 : $deadline - $currentTime
);
}
return $frame;
}
protected function checkIsOK(): void
@@ -226,7 +171,7 @@ abstract class Connection
private function readResponse(): Response
{
$frame = $this->readFrame() ?? throw new NullReceived();
$frame = $this->readFrame();
if ($frame instanceof Response) {
return $frame;
@@ -234,7 +179,7 @@ abstract class Connection
if ($frame instanceof Error) {
if ($frame->type->terminateConnection) {
$this->disconnect();
$this->close();
}
throw new NsqError($frame);
@@ -242,69 +187,4 @@ abstract class Connection
throw new NsqException('Unreachable statement.');
}
private function read(): Buffer
{
try {
$socket = $this->socket();
$buffer = $this->input->append(
$socket->read(Bytes::BYTES_SIZE),
);
if ('' === $buffer->bytes()) {
$this->disconnect();
throw new ConnectionFail('Probably connection lost');
}
$size = $buffer->consumeSize();
do {
$buffer->append(
$socket->read($size),
);
} while ($buffer->size() < $size);
return $buffer;
}
// @codeCoverageIgnoreStart
catch (Exception $e) {
$this->disconnect();
throw ConnectionFail::fromThrowable($e);
}
// @codeCoverageIgnoreEnd
}
private function flush(): void
{
$buffer = $this->output->flush();
$this->logger->debug('Send buffer: '.addcslashes($buffer, PHP_EOL));
try {
$this->socket()->write(
$buffer,
);
}
// @codeCoverageIgnoreStart
catch (Exception $e) {
$this->disconnect();
$this->logger->error($e->getMessage(), ['exception' => $e]);
throw ConnectionFail::fromThrowable($e);
}
// @codeCoverageIgnoreEnd
}
private function socket(): Socket
{
if (null === $this->socket) {
$this->connect();
}
return $this->socket ?? throw new ConnectionFail('This connection is closed, create new one.');
}
}