You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

621 lines
14 KiB

  1. <?php
  2. /**
  3. * Copyright (c) 2012, ideawu
  4. * All rights reserved.
  5. * @author: ideawu
  6. * @link: http://www.ideawu.com/
  7. *
  8. * SSDB PHP client SDK.
  9. */
  10. namespace App\Libs;
  11. use Exception;
  12. class SSDBException extends Exception
  13. {
  14. }
  15. class SSDBTimeoutException extends SSDBException
  16. {
  17. }
  18. /**
  19. * All methods(except *exists) returns false on error,
  20. * so one should use Identical(if($ret === false)) to test the return value.
  21. */
  22. class SimpleSSDB extends SSDB
  23. {
  24. function __construct($host, $port, $timeout_ms=2000){
  25. parent::__construct($host, $port, $timeout_ms);
  26. $this->easy();
  27. }
  28. }
  29. class SSDB_Response
  30. {
  31. public $cmd;
  32. public $code;
  33. public $data = null;
  34. public $message;
  35. function __construct($code='ok', $data_or_message=null){
  36. $this->code = $code;
  37. if($code == 'ok'){
  38. $this->data = $data_or_message;
  39. }else{
  40. $this->message = $data_or_message;
  41. }
  42. }
  43. function __toString(){
  44. if($this->code == 'ok'){
  45. $s = $this->data === null? '' : json_encode($this->data);
  46. }else{
  47. $s = $this->message;
  48. }
  49. return sprintf('%-13s %12s %s', $this->cmd, $this->code, $s);
  50. }
  51. function ok(){
  52. return $this->code == 'ok';
  53. }
  54. function not_found(){
  55. return $this->code == 'not_found';
  56. }
  57. }
  58. // Depricated, use SimpleSSDB instead!
  59. class SSDB
  60. {
  61. private $debug = false;
  62. public $sock = null;
  63. private $_closed = false;
  64. private $recv_buf = '';
  65. private $_easy = false;
  66. public $last_resp = null;
  67. function __construct($host, $port, $timeout_ms=2000){
  68. $timeout_f = (float)$timeout_ms/1000;
  69. $this->sock = @stream_socket_client("$host:$port", $errno, $errstr, $timeout_f);
  70. if(!$this->sock){
  71. throw new SSDBException("$errno: $errstr");
  72. }
  73. $timeout_sec = intval($timeout_ms/1000);
  74. $timeout_usec = ($timeout_ms - $timeout_sec * 1000) * 1000;
  75. @stream_set_timeout($this->sock, $timeout_sec, $timeout_usec);
  76. if(function_exists('stream_set_chunk_size')){
  77. @stream_set_chunk_size($this->sock, 1024 * 1024);
  78. }
  79. }
  80. function set_timeout($timeout_ms){
  81. $timeout_sec = intval($timeout_ms/1000);
  82. $timeout_usec = ($timeout_ms - $timeout_sec * 1000) * 1000;
  83. @stream_set_timeout($this->sock, $timeout_sec, $timeout_usec);
  84. }
  85. /**
  86. * After this method invoked with yesno=true, all requesting methods
  87. * will not return a SSDB_Response object.
  88. * And some certain methods like get/zget will return false
  89. * when response is not ok(not_found, etc)
  90. */
  91. function easy(){
  92. $this->_easy = true;
  93. }
  94. function close(){
  95. if(!$this->_closed){
  96. @fclose($this->sock);
  97. $this->_closed = true;
  98. $this->sock = null;
  99. }
  100. }
  101. function closed(){
  102. return $this->_closed;
  103. }
  104. private $batch_mode = false;
  105. private $batch_cmds = array();
  106. function batch(){
  107. $this->batch_mode = true;
  108. $this->batch_cmds = array();
  109. return $this;
  110. }
  111. function multi(){
  112. return $this->batch();
  113. }
  114. function exec(){
  115. $ret = array();
  116. foreach($this->batch_cmds as $op){
  117. list($cmd, $params) = $op;
  118. $this->send_req($cmd, $params);
  119. }
  120. foreach($this->batch_cmds as $op){
  121. list($cmd, $params) = $op;
  122. $resp = $this->recv_resp($cmd, $params);
  123. $resp = $this->check_easy_resp($cmd, $resp);
  124. $ret[] = $resp;
  125. }
  126. $this->batch_mode = false;
  127. $this->batch_cmds = array();
  128. return $ret;
  129. }
  130. function request(){
  131. $args = func_get_args();
  132. $cmd = array_shift($args);
  133. return $this->__call($cmd, $args);
  134. }
  135. private $async_auth_password = null;
  136. function auth($password){
  137. $this->async_auth_password = $password;
  138. return null;
  139. }
  140. function __call($cmd, $params=array()){
  141. $cmd = strtolower($cmd);
  142. if($this->async_auth_password !== null){
  143. $pass = $this->async_auth_password;
  144. $this->async_auth_password = null;
  145. $auth = $this->__call('auth', array($pass));
  146. if($auth !== true){
  147. throw new Exception("Authentication failed");
  148. }
  149. }
  150. if($this->batch_mode){
  151. $this->batch_cmds[] = array($cmd, $params);
  152. return $this;
  153. }
  154. try{
  155. if($this->send_req($cmd, $params) === false){
  156. $resp = new SSDB_Response('error', 'send error');
  157. }else{
  158. $resp = $this->recv_resp($cmd, $params);
  159. }
  160. }catch(SSDBException $e){
  161. if($this->_easy){
  162. throw $e;
  163. }else{
  164. $resp = new SSDB_Response('error', $e->getMessage());
  165. }
  166. }
  167. if($resp->code == 'noauth'){
  168. $msg = $resp->message;
  169. throw new Exception($msg);
  170. }
  171. $resp = $this->check_easy_resp($cmd, $resp);
  172. return $resp;
  173. }
  174. private function check_easy_resp($cmd, $resp){
  175. $this->last_resp = $resp;
  176. if($this->_easy){
  177. if($resp->not_found()){
  178. return NULL;
  179. }else if(!$resp->ok() && !is_array($resp->data)){
  180. return false;
  181. }else{
  182. return $resp->data;
  183. }
  184. }else{
  185. $resp->cmd = $cmd;
  186. return $resp;
  187. }
  188. }
  189. function multi_set($kvs=array()){
  190. $args = array();
  191. foreach($kvs as $k=>$v){
  192. $args[] = $k;
  193. $args[] = $v;
  194. }
  195. return $this->__call(__FUNCTION__, $args);
  196. }
  197. function multi_hset($name, $kvs=array()){
  198. $args = array($name);
  199. foreach($kvs as $k=>$v){
  200. $args[] = $k;
  201. $args[] = $v;
  202. }
  203. return $this->__call(__FUNCTION__, $args);
  204. }
  205. function multi_zset($name, $kvs=array()){
  206. $args = array($name);
  207. foreach($kvs as $k=>$v){
  208. $args[] = $k;
  209. $args[] = $v;
  210. }
  211. return $this->__call(__FUNCTION__, $args);
  212. }
  213. function incr($key, $val=1){
  214. $args = func_get_args();
  215. return $this->__call(__FUNCTION__, $args);
  216. }
  217. function decr($key, $val=1){
  218. $args = func_get_args();
  219. return $this->__call(__FUNCTION__, $args);
  220. }
  221. function zincr($name, $key, $score=1){
  222. $args = func_get_args();
  223. return $this->__call(__FUNCTION__, $args);
  224. }
  225. function zdecr($name, $key, $score=1){
  226. $args = func_get_args();
  227. return $this->__call(__FUNCTION__, $args);
  228. }
  229. function zadd($key, $score, $value){
  230. $args = array($key, $value, $score);
  231. return $this->__call('zset', $args);
  232. }
  233. function zRevRank($name, $key){
  234. $args = func_get_args();
  235. return $this->__call("zrrank", $args);
  236. }
  237. function zRevRange($name, $offset, $limit){
  238. $args = func_get_args();
  239. return $this->__call("zrrange", $args);
  240. }
  241. function hincr($name, $key, $val=1){
  242. $args = func_get_args();
  243. return $this->__call(__FUNCTION__, $args);
  244. }
  245. function hdecr($name, $key, $val=1){
  246. $args = func_get_args();
  247. return $this->__call(__FUNCTION__, $args);
  248. }
  249. private function send_req($cmd, $params){
  250. $req = array($cmd);
  251. foreach($params as $p){
  252. if(is_array($p)){
  253. $req = array_merge($req, $p);
  254. }else{
  255. $req[] = $p;
  256. }
  257. }
  258. return $this->send($req);
  259. }
  260. private function recv_resp($cmd, $params){
  261. $resp = $this->recv();
  262. if($resp === false){
  263. return new SSDB_Response('error', 'Unknown error');
  264. }else if(!$resp){
  265. return new SSDB_Response('disconnected', 'Connection closed');
  266. }
  267. if($resp[0] == 'noauth'){
  268. $errmsg = isset($resp[1])? $resp[1] : '';
  269. return new SSDB_Response($resp[0], $errmsg);
  270. }
  271. switch($cmd){
  272. case 'dbsize':
  273. case 'ping':
  274. case 'qset':
  275. case 'getbit':
  276. case 'setbit':
  277. case 'countbit':
  278. case 'strlen':
  279. case 'set':
  280. case 'setx':
  281. case 'setnx':
  282. case 'zset':
  283. case 'hset':
  284. case 'qpush':
  285. case 'qpush_front':
  286. case 'qpush_back':
  287. case 'qtrim_front':
  288. case 'qtrim_back':
  289. case 'del':
  290. case 'zdel':
  291. case 'hdel':
  292. case 'hsize':
  293. case 'zsize':
  294. case 'qsize':
  295. case 'hclear':
  296. case 'zclear':
  297. case 'qclear':
  298. case 'multi_set':
  299. case 'multi_del':
  300. case 'multi_hset':
  301. case 'multi_hdel':
  302. case 'multi_zset':
  303. case 'multi_zdel':
  304. case 'incr':
  305. case 'decr':
  306. case 'zincr':
  307. case 'zdecr':
  308. case 'hincr':
  309. case 'hdecr':
  310. case 'zget':
  311. case 'zrank':
  312. case 'zrrank':
  313. case 'zcount':
  314. case 'zsum':
  315. case 'zremrangebyrank':
  316. case 'zremrangebyscore':
  317. case 'ttl':
  318. case 'expire':
  319. if($resp[0] == 'ok'){
  320. $val = isset($resp[1])? intval($resp[1]) : 0;
  321. return new SSDB_Response($resp[0], $val);
  322. }else{
  323. $errmsg = isset($resp[1])? $resp[1] : '';
  324. return new SSDB_Response($resp[0], $errmsg);
  325. }
  326. case 'zavg':
  327. if($resp[0] == 'ok'){
  328. $val = isset($resp[1])? floatval($resp[1]) : (float)0;
  329. return new SSDB_Response($resp[0], $val);
  330. }else{
  331. $errmsg = isset($resp[1])? $resp[1] : '';
  332. return new SSDB_Response($resp[0], $errmsg);
  333. }
  334. case 'get':
  335. case 'substr':
  336. case 'getset':
  337. case 'hget':
  338. case 'qget':
  339. case 'qfront':
  340. case 'qback':
  341. if($resp[0] == 'ok'){
  342. if(count($resp) == 2){
  343. return new SSDB_Response('ok', $resp[1]);
  344. }else{
  345. return new SSDB_Response('server_error', 'Invalid response');
  346. }
  347. }else{
  348. $errmsg = isset($resp[1])? $resp[1] : '';
  349. return new SSDB_Response($resp[0], $errmsg);
  350. }
  351. break;
  352. case 'qpop':
  353. case 'qpop_front':
  354. case 'qpop_back':
  355. if($resp[0] == 'ok'){
  356. $size = 1;
  357. if(isset($params[1])){
  358. $size = intval($params[1]);
  359. }
  360. if($size <= 1){
  361. if(count($resp) == 2){
  362. return new SSDB_Response('ok', $resp[1]);
  363. }else{
  364. return new SSDB_Response('server_error', 'Invalid response');
  365. }
  366. }else{
  367. $data = array_slice($resp, 1);
  368. return new SSDB_Response('ok', $data);
  369. }
  370. }else{
  371. $errmsg = isset($resp[1])? $resp[1] : '';
  372. return new SSDB_Response($resp[0], $errmsg);
  373. }
  374. break;
  375. case 'keys':
  376. case 'zkeys':
  377. case 'hkeys':
  378. case 'hlist':
  379. case 'zlist':
  380. case 'qslice':
  381. if($resp[0] == 'ok'){
  382. $data = array();
  383. if($resp[0] == 'ok'){
  384. $data = array_slice($resp, 1);
  385. }
  386. return new SSDB_Response($resp[0], $data);
  387. }else{
  388. $errmsg = isset($resp[1])? $resp[1] : '';
  389. return new SSDB_Response($resp[0], $errmsg);
  390. }
  391. case 'auth':
  392. case 'exists':
  393. case 'hexists':
  394. case 'zexists':
  395. if($resp[0] == 'ok'){
  396. if(count($resp) == 2){
  397. return new SSDB_Response('ok', (bool)$resp[1]);
  398. }else{
  399. return new SSDB_Response('server_error', 'Invalid response');
  400. }
  401. }else{
  402. $errmsg = isset($resp[1])? $resp[1] : '';
  403. return new SSDB_Response($resp[0], $errmsg);
  404. }
  405. break;
  406. case 'multi_exists':
  407. case 'multi_hexists':
  408. case 'multi_zexists':
  409. if($resp[0] == 'ok'){
  410. if(count($resp) % 2 == 1){
  411. $data = array();
  412. for($i=1; $i<count($resp); $i+=2){
  413. $data[$resp[$i]] = (bool)$resp[$i + 1];
  414. }
  415. return new SSDB_Response('ok', $data);
  416. }else{
  417. return new SSDB_Response('server_error', 'Invalid response');
  418. }
  419. }else{
  420. $errmsg = isset($resp[1])? $resp[1] : '';
  421. return new SSDB_Response($resp[0], $errmsg);
  422. }
  423. break;
  424. case 'scan':
  425. case 'rscan':
  426. case 'zscan':
  427. case 'zrscan':
  428. case 'zrange':
  429. case 'zrrange':
  430. case 'hscan':
  431. case 'hrscan':
  432. case 'hgetall':
  433. case 'multi_hsize':
  434. case 'multi_zsize':
  435. case 'multi_get':
  436. case 'multi_hget':
  437. case 'multi_zget':
  438. case 'zpop_front':
  439. case 'zpop_back':
  440. if($resp[0] == 'ok'){
  441. if(count($resp) % 2 == 1){
  442. $data = array();
  443. for($i=1; $i<count($resp); $i+=2){
  444. if($cmd[0] == 'z'){
  445. $data[$resp[$i]] = intval($resp[$i + 1]);
  446. }else{
  447. $data[$resp[$i]] = $resp[$i + 1];
  448. }
  449. }
  450. return new SSDB_Response('ok', $data);
  451. }else{
  452. return new SSDB_Response('server_error', 'Invalid response');
  453. }
  454. }else{
  455. $errmsg = isset($resp[1])? $resp[1] : '';
  456. return new SSDB_Response($resp[0], $errmsg);
  457. }
  458. break;
  459. default:
  460. return new SSDB_Response($resp[0], array_slice($resp, 1));
  461. }
  462. return new SSDB_Response('error', 'Unknown command: $cmd');
  463. }
  464. function send($data){
  465. $ps = array();
  466. foreach($data as $p){
  467. $ps[] = strlen($p);
  468. $ps[] = $p;
  469. }
  470. $s = join("\n", $ps) . "\n\n";
  471. if($this->debug){
  472. echo '> ' . str_replace(array("\r", "\n"), array('\r', '\n'), $s) . "\n";
  473. }
  474. try{
  475. while(true){
  476. $ret = @fwrite($this->sock, $s);
  477. if($ret === false || $ret === 0){
  478. $this->close();
  479. throw new SSDBException('Connection lost');
  480. }
  481. $s = substr($s, $ret);
  482. if(strlen($s) == 0){
  483. break;
  484. }
  485. @fflush($this->sock);
  486. }
  487. }catch(Exception $e){
  488. $this->close();
  489. throw new SSDBException($e->getMessage());
  490. }
  491. return $ret;
  492. }
  493. function recv(){
  494. $this->step = self::STEP_SIZE;
  495. while(true){
  496. $ret = $this->parse();
  497. if($ret === null){
  498. try{
  499. $data = @fread($this->sock, 1024 * 1024);
  500. if($this->debug){
  501. echo '< ' . str_replace(array("\r", "\n"), array('\r', '\n'), $data) . "\n";
  502. }
  503. }catch(Exception $e){
  504. $data = '';
  505. }
  506. if($data === false || $data === ''){
  507. if(feof($this->sock)){
  508. $this->close();
  509. throw new SSDBException('Connection lost');
  510. }else{
  511. throw new SSDBTimeoutException('Connection timeout');
  512. }
  513. }
  514. $this->recv_buf .= $data;
  515. # echo "read " . strlen($data) . " total: " . strlen($this->recv_buf) . "\n";
  516. }else{
  517. return $ret;
  518. }
  519. }
  520. }
  521. const STEP_SIZE = 0;
  522. const STEP_DATA = 1;
  523. public $resp = array();
  524. public $step;
  525. public $block_size;
  526. private function parse(){
  527. $spos = 0;
  528. $epos = 0;
  529. $buf_size = strlen($this->recv_buf);
  530. // performance issue for large reponse
  531. //$this->recv_buf = ltrim($this->recv_buf);
  532. while(true){
  533. $spos = $epos;
  534. if($this->step === self::STEP_SIZE){
  535. $epos = strpos($this->recv_buf, "\n", $spos);
  536. if($epos === false){
  537. break;
  538. }
  539. $epos += 1;
  540. $line = substr($this->recv_buf, $spos, $epos - $spos);
  541. $spos = $epos;
  542. $line = trim($line);
  543. if(strlen($line) == 0){ // head end
  544. $this->recv_buf = substr($this->recv_buf, $spos);
  545. $ret = $this->resp;
  546. $this->resp = array();
  547. return $ret;
  548. }
  549. $this->block_size = intval($line);
  550. $this->step = self::STEP_DATA;
  551. }
  552. if($this->step === self::STEP_DATA){
  553. $epos = $spos + $this->block_size;
  554. if($epos <= $buf_size){
  555. $n = strpos($this->recv_buf, "\n", $epos);
  556. if($n !== false){
  557. $data = substr($this->recv_buf, $spos, $epos - $spos);
  558. $this->resp[] = $data;
  559. $epos = $n + 1;
  560. $this->step = self::STEP_SIZE;
  561. continue;
  562. }
  563. }
  564. break;
  565. }
  566. }
  567. // packet not ready
  568. if($spos > 0){
  569. $this->recv_buf = substr($this->recv_buf, $spos);
  570. }
  571. return null;
  572. }
  573. }