├── .gitignore ├── .editorconfig ├── examples ├── console ├── publish.php └── auth.go ├── .travis.yml ├── composer.json ├── src ├── config │ └── phpnsq.php └── phpnsq │ ├── Cmd │ ├── Base.php │ └── Subscribe.php │ ├── Stream │ ├── Socket.php │ ├── IntPacker.php │ ├── Writer.php │ ├── Message.php │ └── Reader.php │ ├── Utils │ ├── Logging.php │ └── LogFormatter.php │ ├── Conn │ ├── Lookupd.php │ ├── Config.php │ ├── Pool.php │ └── Nsqd.php │ └── PhpNsq.php ├── LICENSE ├── phpunit.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /tmp 3 | /composer.lock 4 | .idea 5 | .sonarlint 6 | phpnsq.iml 7 | .ac-php-conf.json 8 | GPATH 9 | GRTAGS 10 | GTAGS 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | #editorconfig.org 2 | root = true 3 | 4 | [*.php] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_trailing_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /examples/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new Subscribe($config, null)); 14 | 15 | $application->run(); 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - nightly 6 | 7 | install: 8 | - go get -u -v github.com/campoy/embedmd 9 | - embedmd -d *.md 10 | - docker pull nsqio/nsq 11 | - docker run -d --name lookupd -p 4160:4160 -p 4161:4161 nsqio/nsq /nsqlookupd 12 | - docker run -d --name nsqd -p 4150:4150 -p 4151:4151 nsqio/nsq /nsqd 13 | - composer install 14 | - composer dump-autoload --optimize 15 | 16 | script: 17 | - phpunit -v --coverage-clover=coverage.xml 18 | 19 | after_success: 20 | - bash <(curl -s https://codecov.io/bash) 21 | -------------------------------------------------------------------------------- /examples/publish.php: -------------------------------------------------------------------------------- 1 | setTopic("sample_topic")->publish("Hello nsq."); 11 | var_dump($msg); 12 | 13 | //defered publish 14 | $msg = $phpnsq->setTopic("sample_topic")->publishDefer("this is a defered message.", 10); 15 | var_dump($msg); 16 | 17 | //multiple publish 18 | $messages = [ 19 | "Hello, I am nsq client", 20 | "There are so many libraries developed by PHP", 21 | "Oh, no, PHP is not so good and slowly", 22 | ]; 23 | $msg = $phpnsq->setTopic("sample_topic")->publishMulti(...$messages); 24 | var_dump($msg); 25 | -------------------------------------------------------------------------------- /examples/auth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | func main() { 10 | // Hello world, the web server 11 | helloHandler := func(w http.ResponseWriter, req *http.Request) { 12 | if req.ParseForm() == nil && req.Form.Get("secret") == "secret" { 13 | io.WriteString(w, `{ 14 | "ttl": 3600, 15 | "identity": "username", 16 | "identity_url": "https://....", 17 | "authorizations": [ 18 | { 19 | "permissions": [ 20 | "subscribe", 21 | "publish" 22 | ], 23 | "topic": ".*", 24 | "channels": [ 25 | ".*" 26 | ] 27 | } 28 | ] 29 | }`) 30 | } 31 | } 32 | 33 | http.HandleFunc("/auth", helloHandler) 34 | log.Fatal(http.ListenAndServe(":8000", nil)) 35 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "okstuff/phpnsq", 3 | "description": "PHP Client for nsq", 4 | "type": "library", 5 | "keywords": ["nsq", "queue"], 6 | "homepage": "http://github.com/wk30/phpnsq", 7 | "require": { 8 | "monolog/monolog": "^1.25.0|^2.0", 9 | "symfony/console": "^v5.0", 10 | "react/event-loop": "^v0.4|^v0.5|^v1.1", 11 | "bramus/monolog-colored-line-formatter": "^3.0" 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "OkStuff\\PhpNsq\\": "src/phpnsq/" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "OkStuff\\PhpNsq\\Tests\\": "tests/" 21 | } 22 | }, 23 | "authors": [ 24 | { 25 | "name": "wk30", 26 | "email": "foo_stacker@yeah.net" 27 | } 28 | ], 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /src/config/phpnsq.php: -------------------------------------------------------------------------------- 1 | [ 5 | "nsqd_addrs" => [//this is needed 6 | "127.0.0.1:4150", 7 | ], 8 | "lookupd_addrs" => [ 9 | "127.0.0.1:4161",//only support http protocol 10 | ], 11 | "lookupd_switch" => true,//recommend to use lookupd 12 | "logdir" => "/tmp", 13 | "auth_secret" => "secret", 14 | "auth_switch" => false, 15 | //FIXME: 16 | // "tls_config" => [ 17 | // "local_cert" => "/home/vagrant/docker/nsqio/certs/client.pem", 18 | // "local_pk" => "/home/vagrant/docker/nsqio/certs/client.key", 19 | // "cafile" => "/home/vagrant/docker/nsqio/certs/ca.pem", 20 | // "passphrase" => "test", //if your cert has a passphrase 21 | // "cn_match" => "test", 22 | // "peer_fingerprint" => "sha256", 23 | // ], 24 | ], 25 | ]; 26 | -------------------------------------------------------------------------------- /src/phpnsq/Cmd/Base.php: -------------------------------------------------------------------------------- 1 | run(); 26 | } 27 | 28 | public function addReadStream($socket, Closure $closure) 29 | { 30 | self::$loop->addReadStream($socket, $closure); 31 | 32 | return $this; 33 | } 34 | 35 | public function addPeriodicTimer($interval, Closure $closure) 36 | { 37 | self::$loop->addPeriodicTimer($interval, $closure); 38 | 39 | return $this; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 okstuff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/phpnsq/Cmd/Subscribe.php: -------------------------------------------------------------------------------- 1 | setName(self::COMMAND_NAME) 17 | ->addArgument("topic", InputArgument::REQUIRED, "The topic you want to subscribe") 18 | ->addArgument("channel", InputArgument::REQUIRED, "The channel you want to subscribe") 19 | ->setDescription('subscribe new notification.') 20 | ->setHelp("This command allows you to subscribe notifications..."); 21 | } 22 | 23 | public function execute(InputInterface $input, OutputInterface $output) 24 | { 25 | $phpnsq = self::$phpnsq; 26 | $phpnsq->setTopic($input->getArgument("topic")) 27 | ->setChannel($input->getArgument("channel")) 28 | ->subscribe($this, function (Message $message) use ($phpnsq, $output) { 29 | // $output->writeln($message->toJson()); 30 | $phpnsq->getLogger()->info("READ", $message->toArray()); 31 | }); 32 | //excuted every five seconds. 33 | $this->addPeriodicTimer(5, function () use ($output) { 34 | $memory = memory_get_usage() / 1024; 35 | $formatted = number_format($memory, 3) . 'K'; 36 | $output->writeln(date("Y-m-d H:i:s") . " ############ Current memory usage: {$formatted} ############"); 37 | }); 38 | $this->runLoop(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/phpnsq/Stream/Socket.php: -------------------------------------------------------------------------------- 1 | = $written) { 23 | throw new Exception("Could not write " . strlen($buffer) . " bytes to {$socket}"); 24 | } 25 | 26 | return $written; 27 | } 28 | 29 | public static function recvFrom($socket, $length) 30 | { 31 | $buffer = @stream_socket_recvfrom($socket, $length); 32 | if (empty($buffer)) { 33 | throw new Exception("Read 0 bytes from {$socket}"); 34 | } 35 | 36 | return $buffer; 37 | } 38 | 39 | public static function select(array &$read, array &$write, $timeout) 40 | { 41 | $streamPool = [ 42 | "read" => $read, 43 | "write" => $write, 44 | ]; 45 | if ($read || $write) { 46 | $except = null; 47 | 48 | $available = @stream_select($read, $write, $except, $timeout); 49 | if ($available > 0) { 50 | return $available; 51 | } else if ($available === 0) { 52 | var_dump(date("Y-m-d H:i:s")); 53 | throw new Exception("stream_select() timeout after {$timeout} seconds"); 54 | } else { 55 | throw new Exception("stream_select() failed"); 56 | } 57 | } 58 | 59 | $timeout && usleep($timeout); 60 | 61 | return 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/phpnsq/Utils/Logging.php: -------------------------------------------------------------------------------- 1 | handler = new Logger($name); 18 | $this->dirname = $dirname; 19 | 20 | $this->handler->pushHandler((new StreamHandler($this->getLogFile()))->setFormatter(new LineFormatter())); 21 | $this->handler->pushHandler((new StreamHandler("php://stdout"))->setFormatter(new LogFormatter(true))); 22 | } 23 | 24 | public function getHandler() 25 | { 26 | return $this->handler; 27 | } 28 | 29 | public function debug($msg, ...$context) 30 | { 31 | $this->handler->debug($msg, $context); 32 | } 33 | 34 | public function info($msg, ...$context) 35 | { 36 | $this->handler->info($msg, $context); 37 | } 38 | 39 | public function warn($msg, ...$context) 40 | { 41 | $this->handler->warning($msg, $context); 42 | } 43 | 44 | public function error($msg, ...$context) 45 | { 46 | $this->handler->error($msg, $context); 47 | } 48 | 49 | public function notice($msg, ...$context) 50 | { 51 | $this->handler->notice($msg, $context); 52 | } 53 | 54 | private function getLogFile() 55 | { 56 | $filename = $this->dirname . DIRECTORY_SEPARATOR . "phpnsq-" . date("Ymd") . ".log"; 57 | try { 58 | if (!file_exists($this->dirname)) { 59 | mkdir($this->dirname, 0755); 60 | } 61 | 62 | if (!file_exists($filename)) { 63 | touch($filename); 64 | } 65 | } catch (Exception $e) { 66 | throw new Exception("Create `$filename` failed."); 67 | } 68 | 69 | return $filename; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ./src/phpnsq/ 30 | 31 | ./src/config 32 | ./src/config/phpnsq.yml 33 | 34 | 35 | 36 | 37 | 38 | 39 | ./tests 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/phpnsq/Conn/Lookupd.php: -------------------------------------------------------------------------------- 1 | config = $config; 18 | } 19 | 20 | public function getConfig() 21 | { 22 | return $this->config; 23 | } 24 | 25 | public function getProducers(string $topic) 26 | { 27 | $nsqdConns = []; 28 | 29 | if ($this->nsqdConnected) { 30 | return $nsqdConns; 31 | } 32 | 33 | $defaults = array( 34 | CURLOPT_URL => sprintf(self::lookupTopicUri, $this->config->host, $this->config->port, $topic), 35 | CURLOPT_HEADER => 0, 36 | CURLOPT_RETURNTRANSFER => TRUE, 37 | CURLOPT_TIMEOUT => 4 38 | ); 39 | 40 | $ch = curl_init(); 41 | curl_setopt_array($ch, $defaults); 42 | if( ! $result = curl_exec($ch)) { 43 | trigger_error(curl_error($ch)); 44 | } 45 | curl_close($ch); 46 | 47 | $d = json_decode($result, true); 48 | if (isset($d["message"]) && $d["message"] == "TOPIC_NOT_FOUND") { 49 | return $nsqdConns; 50 | } 51 | 52 | foreach ($d["producers"] as $producer) { 53 | array_push($nsqdConns, $this->connectProducer($producer)); 54 | } 55 | 56 | $this->nsqdConnected = true; 57 | 58 | return $nsqdConns; 59 | } 60 | 61 | private function connectProducer($producer) 62 | { 63 | $config = new Config($producer["broadcast_address"], $producer["tcp_port"]); 64 | $config->set("authSwitch", $this->config->get("authSwitch")) 65 | ->set("authSecret", $this->config->get("authSecret")) 66 | ->set("logdir", $this->config->get("logdir")); 67 | if (!empty($this->config->get("tlsConfig"))) { 68 | $config->set("tlsConfig", $this->config->get("tlsConfig")); 69 | } 70 | return new Nsqd($config); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/phpnsq/Stream/IntPacker.php: -------------------------------------------------------------------------------- 1 | 60, 16 | 'min' => 0.1, 17 | 'max' => 5 * 60, 18 | ]; 19 | private $writeTimeout = [ 20 | 'default' => 1, 21 | 'min' => 0.1, 22 | 'max' => 5 * 60, 23 | ]; 24 | 25 | //TODO: need to be fixed 26 | private $backoffStrategy; 27 | private $maxBackoffDuration = [ 28 | 'default' => 2 * 60, 29 | 'min' => 0, 30 | 'max' => 60 * 60, 31 | ]; 32 | private $backoffMultiplier = [ 33 | 'default' => 1, 34 | 'min' => 0, 35 | 'max' => 60 * 60, 36 | ]; 37 | 38 | private $maxAttempts = [ 39 | 'default' => 5, 40 | 'min' => 0, 41 | 'max' => 65535, 42 | ]; 43 | 44 | private $heartbeatInterval = 30; 45 | 46 | private $tlsConfig; 47 | 48 | private $blocking = true; 49 | 50 | private $authSwitch = false; 51 | 52 | private $authSecret = ""; 53 | 54 | private $logdir = ""; 55 | 56 | public function __construct($host = "", $port = 0) 57 | { 58 | $this->host = $host; 59 | $this->port = $port; 60 | } 61 | 62 | public function set($key, $val) 63 | { 64 | if (is_array($this->$key)) { 65 | $this->$key['default'] = $val; 66 | } else { 67 | $this->$key = $val; 68 | } 69 | 70 | return $this; 71 | } 72 | 73 | public function get($key) 74 | { 75 | return $this->$key; 76 | } 77 | 78 | //check if all the value is between min and max value. 79 | public function validate() 80 | { 81 | foreach ($this as $key => $val) { 82 | if (is_array($val) && count($val) == 3) { 83 | if (!isset($val['default']) || !isset($val['min']) || !isset($val['max'])) { 84 | throw new Exception(sprintf("invalid %s value", $key)); 85 | } 86 | 87 | if ($val['default'] < $val['min']) { 88 | throw new Exception(sprintf("invalid %s ! %v(default) < %v(min)", $key, $val['default'], $val['min'])); 89 | } 90 | 91 | if ($val['default'] > $val['max']) { 92 | throw new Exception(sprintf("invalid %s ! %v(default) > %v(max)", $key, $val['default'], $val['max'])); 93 | } 94 | } 95 | } 96 | 97 | return true; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/phpnsq/Conn/Pool.php: -------------------------------------------------------------------------------- 1 | nsqCfg = $nsq["nsq"]; 14 | 15 | if ($lookupd) { 16 | foreach ($nsq["nsq"]["lookupd_addrs"] as $value) { 17 | $addr = explode(":", $value); 18 | $config = new Config($addr[0], $addr[1]); 19 | $config->set("authSwitch", $nsq["nsq"]["auth_switch"]) 20 | ->set("authSecret", $nsq["nsq"]["auth_secret"]) 21 | ->set("logdir", $nsq["nsq"]["logdir"]); 22 | if (!empty($nsq["nsq"]["tls_config"])) { 23 | $config->set("tlsConfig", $nsq["nsq"]["tls_config"]); 24 | } 25 | $this->addLookupd(new Lookupd($config)); 26 | } 27 | } else { 28 | $this->addNsqd(); 29 | } 30 | } 31 | 32 | private function addNsqd() 33 | { 34 | foreach ($this->nsqCfg["nsqd_addrs"] as $value) { 35 | $addr = explode(":", $value); 36 | $config = new Config($addr[0], $addr[1]); 37 | $config->set("authSwitch", $this->nsqCfg["auth_switch"]) 38 | ->set("authSecret", $this->nsqCfg["auth_secret"]) 39 | ->set("logdir", $this->nsqCfg["logdir"]); 40 | if (!empty($this->nsqCfg["tls_config"])) { 41 | $config->set("tlsConfig", $this->nsqCfg["tls_config"]); 42 | } 43 | $this->addConn(new Nsqd($config)); 44 | } 45 | } 46 | 47 | public function addConn(Nsqd ...$conns) 48 | { 49 | foreach ($conns as $conn) { 50 | array_push($this->pool, $conn); 51 | } 52 | 53 | return $this; 54 | } 55 | 56 | public function getConn() 57 | { 58 | return $this->pool[array_rand($this->pool)]; 59 | } 60 | 61 | public function addNsqdByLookupd(Lookupd $conn, string $topic) 62 | { 63 | $nsqdConns = $conn->getProducers($topic); 64 | if (count($nsqdConns) <= 0) { 65 | $this->addNsqd(); 66 | } else { 67 | $this->addConn(...$nsqdConns); 68 | } 69 | 70 | return $this; 71 | } 72 | 73 | public function addLookupd(Lookupd $conn) 74 | { 75 | array_push($this->lookupdPool, $conn); 76 | 77 | return $this; 78 | } 79 | 80 | public function getLookupd() 81 | { 82 | return $this->lookupdPool[array_rand($this->lookupdPool)]; 83 | } 84 | 85 | public function getLookupdCount() 86 | { 87 | return count($this->lookupdPool); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/phpnsq/Stream/Writer.php: -------------------------------------------------------------------------------- 1 | = 1) { 99 | $str .= sprintf(" %s", implode(" ", $params)); 100 | } 101 | $str .= "\n"; 102 | 103 | return $str; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/phpnsq/Utils/LogFormatter.php: -------------------------------------------------------------------------------- 1 | ignoreEmptyContextAndExtra = $ignoreEmptyContextAndExtra; 14 | parent::__construct(null, null, null, false, $ignoreEmptyContextAndExtra); 15 | } 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function format(array $record) : string 21 | { 22 | $vars = parent::normalize($record); 23 | 24 | $output = $this->format; 25 | 26 | foreach ($vars['extra'] as $var => $val) { 27 | if (false !== strpos($output, '%extra.' . $var . '%')) { 28 | $output = str_replace('%extra.' . $var . '%', $this->stringify($val), $output); 29 | unset($vars['extra'][$var]); 30 | } 31 | } 32 | 33 | 34 | foreach ($vars['context'] as $var => $val) { 35 | if (false !== strpos($output, '%context.' . $var . '%')) { 36 | $output = str_replace('%context.' . $var . '%', $this->stringify($val), $output); 37 | unset($vars['context'][$var]); 38 | } 39 | } 40 | 41 | $output = $this->coloredOutput($vars); 42 | 43 | // remove leftover %extra.xxx% and %context.xxx% if any 44 | if (false !== strpos($output, '%')) { 45 | $output = preg_replace('/%(?:extra|context)\..+?%/', '', $output); 46 | } 47 | 48 | // Let the parent class to the formatting, yet wrap it in the color linked to the level 49 | return $output; 50 | } 51 | 52 | protected function coloredOutput(&$vars) 53 | { 54 | $coloredOutput = "[%datetime%] %channel%.%level_name%: "; 55 | foreach ($vars as $var => $val) { 56 | if (false !== strpos($coloredOutput, '%' . $var . '%')) { 57 | $coloredOutput = str_replace('%' . $var . '%', $this->stringify($val), $coloredOutput); 58 | } 59 | } 60 | //Get the Color Scheme 61 | $colorScheme = $this->getColorScheme(); 62 | $coloredOutput = $colorScheme->getColorizeString($vars['level']) . $coloredOutput . $colorScheme->getResetString(); 63 | 64 | $normalOutput = "%message% %context% %extra%\n"; 65 | if ($this->ignoreEmptyContextAndExtra) { 66 | if (empty($vars['context'])) { 67 | unset($vars['context']); 68 | $normalOutput = str_replace('%context%', '', $normalOutput); 69 | } 70 | 71 | if (empty($vars['extra'])) { 72 | unset($vars['extra']); 73 | $normalOutput = str_replace('%extra%', '', $normalOutput); 74 | } 75 | } 76 | 77 | foreach ($vars as $var => $val) { 78 | if (false !== strpos($normalOutput, '%' . $var . '%')) { 79 | $normalOutput = str_replace('%' . $var . '%', $this->stringify($val), $normalOutput); 80 | } 81 | } 82 | 83 | return $coloredOutput . $normalOutput; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/phpnsq/Stream/Message.php: -------------------------------------------------------------------------------- 1 | timestamp = microtime(true); 18 | } 19 | 20 | /** 21 | * @return bool 22 | */ 23 | public function isDecoded() 24 | { 25 | return $this->decoded; 26 | } 27 | 28 | public function setDecoded() 29 | { 30 | $this->decoded = true; 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @return mixed 37 | */ 38 | public function getId() 39 | { 40 | return $this->id; 41 | } 42 | 43 | /** 44 | * @param mixed $id 45 | */ 46 | public function setId($id) 47 | { 48 | $this->id = $id; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @return mixed 55 | */ 56 | public function getBody() 57 | { 58 | return $this->body; 59 | } 60 | 61 | /** 62 | * @param mixed $body 63 | */ 64 | public function setBody($body) 65 | { 66 | $this->body = $body; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * @return mixed 73 | */ 74 | public function getTimestamp() 75 | { 76 | return $this->timestamp; 77 | } 78 | 79 | /** 80 | * @param mixed $timestamp 81 | */ 82 | public function setTimestamp($timestamp = null) 83 | { 84 | if (null === $timestamp) { 85 | $this->timestamp = microtime(true); 86 | } 87 | $this->timestamp = $timestamp; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * @return mixed 94 | */ 95 | public function getAttempts() 96 | { 97 | return $this->attempts; 98 | } 99 | 100 | /** 101 | * @param mixed $attempts 102 | */ 103 | public function setAttempts($attempts) 104 | { 105 | $this->attempts = $attempts; 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * @return mixed 112 | */ 113 | public function getNsqdAddr() 114 | { 115 | return $this->nsqdAddr; 116 | } 117 | 118 | /** 119 | * @param mixed $nsqdAddr 120 | */ 121 | public function setNsqdAddr($nsqdAddr) 122 | { 123 | $this->nsqdAddr = $nsqdAddr; 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * @return mixed 130 | */ 131 | public function getDelegate() 132 | { 133 | return $this->delegate; 134 | } 135 | 136 | /** 137 | * @param mixed $delegate 138 | */ 139 | public function setDelegate($delegate) 140 | { 141 | $this->delegate = $delegate; 142 | 143 | return $this; 144 | } 145 | 146 | public function toArray() 147 | { 148 | return [ 149 | "id" => $this->getId(), 150 | "body" => $this->getBody(), 151 | "timestamp" => $this->getTimestamp(), 152 | "decoded" => $this->isDecoded(), 153 | "attempts" => $this->getAttempts(), 154 | "nsqdAddr" => $this->getNsqdAddr(), 155 | "delegate" => $this->getDelegate(), 156 | ]; 157 | } 158 | 159 | public function toJson() 160 | { 161 | return json_encode($this->toArray(), JSON_FORCE_OBJECT); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpnsq 2 | 3 | [![Build Status](https://travis-ci.org/okstuff/phpnsq.svg?branch=master)](https://travis-ci.org/okstuff/phpnsq) 4 | 5 | ## Install 6 | ```shell 7 | composer require okstuff/phpnsq 8 | ``` 9 | 10 | ## Subscribe one topic and channel 11 | ```shell 12 | php examples/console phpnsq:sub 13 | ``` 14 | 15 | ## Notice 16 | Before try this, you should install [nsq](http://nsq.io) by yourself. 17 | 18 | ## Examples 19 | 20 | 1. Publish 21 | 22 | [embedmd]:# (examples/publish.php php) 23 | ```php 24 | setTopic("sample_topic")->publish("Hello nsq."); 34 | var_dump($msg); 35 | 36 | //defered publish 37 | $msg = $phpnsq->setTopic("sample_topic")->publishDefer("this is a defered message.", 10); 38 | var_dump($msg); 39 | 40 | //multiple publish 41 | $messages = [ 42 | "Hello, I am nsq client", 43 | "There are so many libraries developed by PHP", 44 | "Oh, no, PHP is not so good and slowly", 45 | ]; 46 | $msg = $phpnsq->setTopic("sample_topic")->publishMulti(...$messages); 47 | var_dump($msg); 48 | ``` 49 | 50 | 2. Subscribe 51 | 52 | [embedmd]:# (src/phpnsq/Cmd/Subscribe.php php) 53 | ```php 54 | setName(self::COMMAND_NAME) 70 | ->addArgument("topic", InputArgument::REQUIRED, "The topic you want to subscribe") 71 | ->addArgument("channel", InputArgument::REQUIRED, "The channel you want to subscribe") 72 | ->setDescription('subscribe new notification.') 73 | ->setHelp("This command allows you to subscribe notifications..."); 74 | } 75 | 76 | public function execute(InputInterface $input, OutputInterface $output) 77 | { 78 | $phpnsq = self::$phpnsq; 79 | $phpnsq->setTopic($input->getArgument("topic")) 80 | ->setChannel($input->getArgument("channel")) 81 | ->subscribe($this, function (Message $message) use ($phpnsq, $output) { 82 | // $output->writeln($message->toJson()); 83 | $phpnsq->getLogger()->info("READ", $message->toArray()); 84 | }); 85 | //excuted every five seconds. 86 | $this->addPeriodicTimer(5, function () use ($output) { 87 | $memory = memory_get_usage() / 1024; 88 | $formatted = number_format($memory, 3) . 'K'; 89 | $output->writeln(date("Y-m-d H:i:s") . " ############ Current memory usage: {$formatted} ############"); 90 | }); 91 | $this->runLoop(); 92 | } 93 | } 94 | ``` 95 | 96 | 3. Console 97 | 98 | [embedmd]:# (examples/console php) 99 | ```php 100 | #!/usr/bin/env php 101 | add(new Subscribe($config, null)); 113 | 114 | $application->run(); 115 | ``` 116 | -------------------------------------------------------------------------------- /src/phpnsq/Stream/Reader.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 23 | } 24 | 25 | public function bindConn(Nsqd $conn) 26 | { 27 | $this->conn = $conn; 28 | 29 | return $this; 30 | } 31 | 32 | public function bindFrame() 33 | { 34 | $size = 0; 35 | $type = 0; 36 | try { 37 | $size = $this->readInt(4); 38 | $type = $this->readInt(4); 39 | } catch (Exception $e) { 40 | throw new Exception("Error reading message frame [$size, $type] ({$e->getMessage()})"); 41 | } 42 | 43 | $frame = [ 44 | "size" => $size, 45 | "type" => $type, 46 | ]; 47 | 48 | try { 49 | if (self::TYPE_RESPONSE == $type) { 50 | $frame["response"] = $this->readString($size - 4); 51 | } elseif (self::TYPE_ERROR == $type) { 52 | $frame["error"] = $this->readString($size - 4); 53 | } 54 | } catch (Exception $e) { 55 | throw new Exception("Error reading frame details [$size, $type] ({$e->getMessage()})"); 56 | } 57 | 58 | $this->frame = $frame; 59 | 60 | return $this; 61 | } 62 | 63 | // DecodeMessage deserializes data (as []byte) and creates a new Message 64 | // message format: 65 | // [x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x][x]... 66 | // | (int64) || || (hex string encoded in ASCII) || (binary) 67 | // | 8-byte || || 16-byte || N-byte 68 | // ------------------------------------------------------------------------------------------... 69 | // nanosecond timestamp ^^ message ID message body 70 | // (uint16) 71 | // 2-byte 72 | // attempts 73 | public function getMessage() 74 | { 75 | $msg = null; 76 | if (null !== $this->frame) { 77 | switch ($this->frame["type"]) { 78 | case self::TYPE_MESSAGE: 79 | $msg = (new Message())->setTimestamp($this->readInt64(8)) 80 | ->setAttempts($this->readUInt16(2)) 81 | ->setId($this->readString(16)) 82 | ->setBody($this->readString($this->frame["size"] - 30)) 83 | ->setDecoded(); 84 | break; 85 | case self::TYPE_RESPONSE: 86 | $msg = $this->frame["response"]; 87 | break; 88 | case self::TYPE_ERROR: 89 | $msg = $this->frame["error"]; 90 | break; 91 | } 92 | 93 | } 94 | 95 | return $msg; 96 | } 97 | 98 | public function isMessage() 99 | { 100 | return self::TYPE_MESSAGE == $this->frame["type"]; 101 | } 102 | 103 | public function isHeartbeat() 104 | { 105 | return $this->isResponse(self::HEARTBEAT); 106 | } 107 | 108 | public function isOk() 109 | { 110 | return $this->isResponse(self::OK); 111 | } 112 | 113 | public function isResponse($response = null) 114 | { 115 | return isset($this->frame["response"]) 116 | && self::TYPE_RESPONSE == $this->frame["type"] 117 | && (null === $response || $response === $this->frame["response"]); 118 | } 119 | 120 | private function readInt($size) 121 | { 122 | list(, $tmp) = unpack("N", $this->conn->read($size)); 123 | 124 | return sprintf("%u", $tmp); 125 | } 126 | 127 | private function readInt64($size) 128 | { 129 | return IntPacker::int64($this->conn->read($size)); 130 | } 131 | 132 | private function readUInt16($size) 133 | { 134 | return IntPacker::uInt16($this->conn->read($size)); 135 | } 136 | 137 | private function readString($size) 138 | { 139 | $bytes = unpack("c{$size}chars", $this->conn->read($size)); 140 | 141 | return implode(array_map("chr", $bytes)); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/phpnsq/PhpNsq.php: -------------------------------------------------------------------------------- 1 | reader = new reader(); 25 | $this->logger = new Logging("PHPNSQ", $nsq["nsq"]["logdir"]); 26 | $this->pool = new Pool($nsq, $nsq["nsq"]["lookupd_switch"]); 27 | } 28 | 29 | public function getLogger() 30 | { 31 | return $this->logger; 32 | } 33 | 34 | public function setChannel(string $channel) 35 | { 36 | $this->channel = $channel; 37 | 38 | return $this; 39 | } 40 | 41 | public function setTopic(string $topic) 42 | { 43 | $this->topic = $topic; 44 | 45 | if ($this->pool->getLookupdCount() > 0) { 46 | $lookupd = $this->pool->getLookupd(); 47 | $this->pool->addNsqdByLookupd($lookupd, $topic); 48 | } 49 | 50 | return $this; 51 | } 52 | 53 | public function publish(string $message) 54 | { 55 | $msg = null; 56 | try { 57 | $conn = $this->pool->getConn(); 58 | $conn->write(Writer::pub($this->topic, $message)); 59 | 60 | $msg = $this->reader->bindConn($conn)->bindFrame()->getMessage(); 61 | } catch (Exception $e) { 62 | $this->logger->error("publish error", $e); 63 | $msg = $e->getMessage(); 64 | } 65 | 66 | return $msg; 67 | } 68 | 69 | public function publishMulti(string ...$messages) 70 | { 71 | $msg = null; 72 | try { 73 | $conn = $this->pool->getConn(); 74 | $conn->write(Writer::mpub($this->topic, $messages)); 75 | 76 | $msg = $this->reader->bindConn($conn)->bindFrame()->getMessage(); 77 | } catch (Exception $e) { 78 | $this->logger->error("publish error", $e); 79 | $msg = $e->getMessage(); 80 | } 81 | 82 | return $msg; 83 | } 84 | 85 | public function publishDefer(string $message, int $deferTime) 86 | { 87 | $msg = null; 88 | try { 89 | $conn = $this->pool->getConn(); 90 | $conn->write(Writer::dpub($this->topic, $deferTime, $message)); 91 | 92 | $msg = $this->reader->bindConn($conn)->bindFrame()->getMessage(); 93 | } catch (Exception $e) { 94 | $this->logger->error("publish error", $e); 95 | $msg = $e->getMessage(); 96 | } 97 | 98 | return $msg; 99 | } 100 | 101 | public function subscribe(SubscribeCommand $cmd, Closure $callback) 102 | { 103 | try { 104 | $conn = $this->pool->getConn(); 105 | $sock = $conn->getSock(); 106 | 107 | $cmd->addReadStream($sock, function ($sock) use ($conn, $callback) { 108 | $this->handleMessage($conn, $callback); 109 | }); 110 | 111 | $conn->write(Writer::sub($this->topic, $this->channel)) 112 | ->write(Writer::rdy(1)); 113 | } catch (Exception $e) { 114 | $this->logger->error("subscribe error", $e); 115 | } 116 | } 117 | 118 | protected function handleMessage(Nsqd $conn, Closure $callback) 119 | { 120 | $reader = $this->reader->bindConn($conn)->bindFrame(); 121 | 122 | if ($reader->isHeartbeat()) { 123 | $conn->write(Writer::nop()); 124 | } elseif ($reader->isMessage()) { 125 | 126 | $msg = $reader->getMessage(); 127 | try { 128 | call_user_func($callback, $msg); 129 | } catch (Exception $e) { 130 | $this->logger->error("Will be requeued: ", $e->getMessage()); 131 | 132 | $conn->write(Writer::touch($msg->getId())) 133 | ->write(Writer::req( 134 | $msg->getId(), 135 | $conn->getConfig()->get("defaultRequeueDelay")["default"] 136 | )); 137 | } 138 | 139 | $conn->write(Writer::fin($msg->getId())) 140 | ->write(Writer::rdy(1)); 141 | } elseif ($reader->isOk()) { 142 | $this->logger->info('Ignoring "OK" frame in SUB loop'); 143 | } else { 144 | $this->logger->error("Error/unexpected frame received: ", $reader); 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/phpnsq/Conn/Nsqd.php: -------------------------------------------------------------------------------- 1 | config = $config; 22 | } 23 | 24 | public function getConfig() 25 | { 26 | return $this->config; 27 | } 28 | 29 | public function read($len = 0) 30 | { 31 | $data = ''; 32 | $timeout = $this->config->get("readTimeout")["default"]; 33 | $this->reader = [$sock = $this->getSock()]; 34 | while (strlen($data) < $len) { 35 | $readable = Socket::select($this->reader, $this->writer, $timeout); 36 | if ($readable > 0) { 37 | $buffer = Socket::recvFrom($sock, $len); 38 | $data .= $buffer; 39 | $len -= strlen($buffer); 40 | } 41 | } 42 | 43 | return $data; 44 | } 45 | 46 | public function write($buffer) 47 | { 48 | $timeout = $this->config->get("writeTimeout")["default"]; 49 | $this->writer = [$sock = $this->getSock()]; 50 | while (strlen($buffer) > 0) { 51 | $writable = Socket::select($this->reader, $this->writer, $timeout); 52 | if ($writable > 0) { 53 | $buffer = substr($buffer, Socket::sendTo($sock, $buffer)); 54 | } 55 | } 56 | 57 | return $this; 58 | } 59 | 60 | public function __destruct() 61 | { 62 | fclose($this->getSock()); 63 | } 64 | 65 | public function getSock() 66 | { 67 | if (null === $this->sock) { 68 | $this->sock = Socket::client($this->config->host, $this->config->port); 69 | 70 | if (false === $this->config->get("blocking")) { 71 | stream_set_blocking($this->sock, 0); 72 | } 73 | 74 | $this->write(Writer::MAGIC_V2); 75 | $this->auth(); 76 | 77 | //FIXME: Really shit php code. 78 | $tlsConfig=$this->config->get("tlsConfig"); 79 | $context = $this->sock; 80 | if (null !== $tlsConfig) { 81 | $this->write(Writer::identify(["tls_v1" => true])); 82 | 83 | if ($tlsConfig["local_cert"]) { 84 | if (!file_exists($tlsConfig["local_cert"])) { 85 | throw new Exception("Local cert file not exists"); 86 | } 87 | if (!stream_context_set_option($context, 'tcp', 'local_cert', $tlsConfig["local_cert"])) { 88 | throw new Exception("Could not set cert"); 89 | } 90 | } 91 | if ($tlsConfig["local_pk"]) { 92 | if (!file_exists($tlsConfig["local_pk"])) { 93 | throw new Exception("Local pk file not exists"); 94 | } 95 | if (!stream_context_set_option($context, 'tcp', 'local_pk', $tlsConfig["local_pk"])) { 96 | throw new Exception("Could not set local_pk"); 97 | } 98 | } 99 | if ($tlsConfig["passphrase"] && !stream_context_set_option($context, 'tcp', 'passphrase', $tlsConfig["passphrase"])) { 100 | throw New Exception("Could not set passphrase for your ssl cert"); 101 | } 102 | if ($tlsConfig["cn_match"] && !stream_context_set_option($context, 'tcp', 'CN_match', $tlsConfig["cn_match"])) { 103 | throw new Exception("Could not set CN_match"); 104 | } 105 | if ($tlsConfig["peer_fingerprint"] && !stream_context_set_option($context, 'tcp', 'peer_fingerprint', $tlsConfig["peer_fingerprint"])) { 106 | throw new Exception("Could not set peer_fingerprint"); 107 | } 108 | stream_context_set_option($context, 'tcp', 'allow_self_signed', true); 109 | stream_context_set_option($context, 'tcp', 'verify_peer', true); 110 | stream_context_set_option($context, 'tcp', 'cafile', $tlsConfig["cafile"]); 111 | } 112 | 113 | $this->sock = $context; 114 | } 115 | 116 | return $this->sock; 117 | } 118 | 119 | private function auth() 120 | { 121 | if ($this->config->get("authSwitch")) { 122 | $this->write(Writer::auth($this->config->get("authSecret"))); 123 | $msg = (new Reader())->bindConn($this)->bindFrame()->getMessage(); 124 | (new Logging("PHPNSQ", $this->config->get("logdir")))->info($msg); 125 | } 126 | } 127 | } 128 | --------------------------------------------------------------------------------