XSServer.class.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. <?php
  2. /**
  3. * XSServer 类定义文件
  4. *
  5. * @author hightman
  6. * @link http://www.xunsearch.com/
  7. * @copyright Copyright &copy; 2011 HangZhou YunSheng Network Technology Co., Ltd.
  8. * @license http://www.xunsearch.com/license/
  9. * @version $Id$
  10. */
  11. /**
  12. * XSCommand 命令对象
  13. * 是与服务端交互的最基本单位, 命令对象可自动转换为通讯字符串,
  14. * 命令结构参见 C 代码中的 struct xs_cmd 定义, 头部长度为 8字节.
  15. *
  16. * @property int $arg 参数, 相当于 (arg1<<8)|arg2 的值
  17. * @author hightman <hightman@twomice.net>
  18. * @version 1.0.0
  19. * @package XS
  20. */
  21. class XSCommand extends XSComponent
  22. {
  23. /**
  24. * @var int 命令代码
  25. * 通常是预定义常量 XS_CMD_xxx, 取值范围 0~255
  26. */
  27. public $cmd = XS_CMD_NONE;
  28. /**
  29. * @var int 参数1
  30. * 取值范围 0~255, 具体含义根据不同的 CMD 而变化
  31. */
  32. public $arg1 = 0;
  33. /**
  34. * @var int 参数2
  35. * 取值范围 0~255, 常用于存储 value no, 具体参照不同 CMD 而确定
  36. */
  37. public $arg2 = 0;
  38. /**
  39. * @var string 主数据内容, 最长 2GB
  40. */
  41. public $buf = '';
  42. /**
  43. * @var string 辅数据内容, 最长 255字节
  44. */
  45. public $buf1 = '';
  46. /**
  47. * 构造函数
  48. * @param mixed $cmd 命令类型或命令数组
  49. * 当类型为 int 表示命令代码, 范围是 1~255, 参见 xs_cmd.inc.php 里的定义
  50. * 当类型为 array 时忽略其它参数, 可包含 cmd, arg1, arg2, buf, buf1 这些键值
  51. * @param int $arg1 参数1, 其值为 0~255, 具体含义视不同 CMD 而确定
  52. * @param int $arg2 参数2, 其值为 0~255, 具体含义视不同 CMD 而确定, 常用于存储 value no
  53. * @param string $buf 字符串内容, 最大长度为 2GB
  54. * @param string $buf1 字符串内容1, 最大长度为 255字节
  55. */
  56. public function __construct($cmd, $arg1 = 0, $arg2 = 0, $buf = '', $buf1 = '')
  57. {
  58. if (is_array($cmd)) {
  59. foreach ($cmd as $key => $value) {
  60. if ($key === 'arg' || property_exists($this, $key)) {
  61. $this->$key = $value;
  62. }
  63. }
  64. } else {
  65. $this->cmd = $cmd;
  66. $this->arg1 = $arg1;
  67. $this->arg2 = $arg2;
  68. $this->buf = $buf;
  69. $this->buf1 = $buf1;
  70. }
  71. }
  72. /**
  73. * 转换为封包字符串
  74. * @return string 用于服务端交互的字符串
  75. */
  76. public function __toString()
  77. {
  78. if (strlen($this->buf1) > 0xff) {
  79. $this->buf1 = substr($this->buf1, 0, 0xff);
  80. }
  81. return pack('CCCCI', $this->cmd, $this->arg1, $this->arg2, strlen($this->buf1), strlen($this->buf)) . $this->buf . $this->buf1;
  82. }
  83. /**
  84. * 获取属性 arg 的值
  85. * @return int 参数值
  86. */
  87. public function getArg()
  88. {
  89. return $this->arg2 | ($this->arg1 << 8);
  90. }
  91. /**
  92. * 设置属性 arg 的值
  93. * @param int $arg 参数值
  94. */
  95. public function setArg($arg)
  96. {
  97. $this->arg1 = ($arg >> 8) & 0xff;
  98. $this->arg2 = $arg & 0xff;
  99. }
  100. }
  101. /**
  102. * XSServer 服务器操作对象
  103. * 同时兼容于 indexd, searchd, 所有交互均采用 {@link XSCommand} 对象
  104. *
  105. * @property string $project 当前使用的项目名
  106. * @property-write int $timeout 服务端IO超时秒数, 默认为 5秒
  107. * @author hightman <hightman@twomice.net>
  108. * @version 1.0.0
  109. * @package XS
  110. */
  111. class XSServer extends XSComponent
  112. {
  113. /**
  114. * 连接标志定义(常量)
  115. */
  116. const FILE = 0x01;
  117. const BROKEN = 0x02;
  118. /**
  119. * @var XS 服务端关联的 XS 对象
  120. */
  121. public $xs;
  122. protected $_sock, $_conn;
  123. protected $_flag;
  124. protected $_project;
  125. protected $_sendBuffer;
  126. /**
  127. * 构造函数, 打开连接
  128. * @param string $conn 服务端连接参数
  129. * @param XS $xs 需要捆绑的 xs 对象
  130. */
  131. public function __construct($conn = null, $xs = null)
  132. {
  133. $this->xs = $xs;
  134. if ($conn !== null) {
  135. $this->open($conn);
  136. }
  137. }
  138. /**
  139. * 析构函数, 关闭连接
  140. */
  141. public function __destruct()
  142. {
  143. $this->xs = null;
  144. $this->close();
  145. }
  146. /**
  147. * 打开服务端连接
  148. * 如果已关联 XS 对象, 则会同时切换至相应的项目名称
  149. * @param mixed $conn 服务端连接参数, 支持: <端口号|host:port|本地套接字路径>
  150. */
  151. public function open($conn)
  152. {
  153. $this->close();
  154. $this->_conn = $conn;
  155. $this->_flag = self::BROKEN;
  156. $this->_sendBuffer = '';
  157. $this->_project = null;
  158. $this->connect();
  159. $this->_flag ^= self::BROKEN;
  160. if ($this->xs instanceof XS) {
  161. $this->setProject($this->xs->getName());
  162. }
  163. }
  164. /**
  165. * 重新打开连接
  166. * 仅应用于曾经成功打开的连并且异常关闭了
  167. * @param bool $force 是否强制重新连接, 默认为否
  168. * @return XSServer 返回自己, 以便串接操作
  169. */
  170. public function reopen($force = false)
  171. {
  172. if ($this->_flag & self::BROKEN || $force === true) {
  173. $this->open($this->_conn);
  174. }
  175. return $this;
  176. }
  177. /**
  178. * 关闭连接
  179. * 附带发送发送 quit 命令
  180. * @param bool $ioerr 关闭调用是否由于 IO 错误引起的, 以免发送 quit 指令
  181. */
  182. public function close($ioerr = false)
  183. {
  184. if ($this->_sock && !($this->_flag & self::BROKEN)) {
  185. if (!$ioerr && $this->_sendBuffer !== '') {
  186. $this->write($this->_sendBuffer);
  187. $this->_sendBuffer = '';
  188. }
  189. if (!$ioerr && !($this->_flag & self::FILE)) {
  190. $cmd = new XSCommand(XS_CMD_QUIT);
  191. fwrite($this->_sock, $cmd);
  192. }
  193. fclose($this->_sock);
  194. $this->_flag |= self::BROKEN;
  195. }
  196. }
  197. /**
  198. * @return string 连接字符串
  199. */
  200. public function getConnString()
  201. {
  202. $str = $this->_conn;
  203. if (is_int($str) || is_numeric($str)) {
  204. $str = 'localhost:' . $str;
  205. } elseif (strpos($str, ':') === false) {
  206. $str = 'unix://' . $str;
  207. }
  208. return $str;
  209. }
  210. /**
  211. * 获取连接资源描述符
  212. * @return mixed 连接标识, 仅用于内部测试等目的
  213. */
  214. public function getSocket()
  215. {
  216. return $this->_sock;
  217. }
  218. /**
  219. * 获取当前项目名称
  220. * @return string 项目名称
  221. */
  222. public function getProject()
  223. {
  224. return $this->_project;
  225. }
  226. /**
  227. * 设置当前项目
  228. * @param string $name 项目名称
  229. * @param string $home 项目在服务器上的目录路径, 可选参数(不得超过255字节).
  230. */
  231. public function setProject($name, $home = '')
  232. {
  233. if ($name !== $this->_project) {
  234. $cmd = array('cmd' => XS_CMD_USE, 'buf' => $name, 'buf1' => $home);
  235. $this->execCommand($cmd, XS_CMD_OK_PROJECT);
  236. $this->_project = $name;
  237. }
  238. }
  239. /**
  240. * 设置服务端超时秒数
  241. * @param int $sec 秒数, 设为 0则永不超时直到客户端主动关闭
  242. */
  243. public function setTimeout($sec)
  244. {
  245. $cmd = array('cmd' => XS_CMD_TIMEOUT, 'arg' => $sec);
  246. $this->execCommand($cmd, XS_CMD_OK_TIMEOUT_SET);
  247. }
  248. /**
  249. * 执行服务端指令并获取返回值
  250. * @param mixed $cmd 要提交的指令, 若不是 XSCommand 实例则作为构造函数的第一参数创建对象
  251. * @param int $res_arg 要求的响应参数, 默认为 XS_CMD_NONE 即不检测, 若检测结果不符
  252. * 则认为命令调用失败, 会返回 false 并设置相应的出错信息
  253. * @param int $res_cmd 要求的响应指令, 默认为 XS_CMD_OK 即要求结果必须正确.
  254. * @return mixed 若无需要检测结果则返回 true, 其它返回响应的 XSCommand 对象
  255. * @throw XSException 操作失败或响应命令不正确时抛出异常
  256. */
  257. public function execCommand($cmd, $res_arg = XS_CMD_NONE, $res_cmd = XS_CMD_OK)
  258. {
  259. // create command object
  260. if (!$cmd instanceof XSCommand) {
  261. $cmd = new XSCommand($cmd);
  262. }
  263. // just cache the cmd for those need not answer
  264. if ($cmd->cmd & 0x80) {
  265. $this->_sendBuffer .= $cmd;
  266. return true;
  267. }
  268. // send cmd to server
  269. $buf = $this->_sendBuffer . $cmd;
  270. $this->_sendBuffer = '';
  271. $this->write($buf);
  272. // return true directly for local file
  273. if ($this->_flag & self::FILE) {
  274. return true;
  275. }
  276. // got the respond
  277. $res = $this->getRespond();
  278. // check respond
  279. if ($res->cmd === XS_CMD_ERR && $res_cmd != XS_CMD_ERR) {
  280. throw new XSException($res->buf, $res->arg);
  281. }
  282. // got unexpected respond command
  283. if ($res->cmd != $res_cmd || ($res_arg != XS_CMD_NONE && $res->arg != $res_arg)) {
  284. throw new XSException('Unexpected respond {CMD:' . $res->cmd . ', ARG:' . $res->arg . '}');
  285. }
  286. return $res;
  287. }
  288. /**
  289. * 往服务器直接发送指令 (无缓存)
  290. * @param mixed $cmd 要提交的指令, 支持 XSCommand 实例或 cmd 构造函数的第一参数
  291. * @throw XSException 失败时抛出异常
  292. */
  293. public function sendCommand($cmd)
  294. {
  295. if (!$cmd instanceof XSCommand) {
  296. $cmd = new XSCommand($cmd);
  297. }
  298. $this->write(strval($cmd));
  299. }
  300. /**
  301. * 从服务器读取响应指令
  302. * @return XSCommand 成功返回响应指令
  303. * @throw XSException 失败时抛出异常
  304. */
  305. public function getRespond()
  306. {
  307. // read data from server
  308. $buf = $this->read(8);
  309. $hdr = unpack('Ccmd/Carg1/Carg2/Cblen1/Iblen', $buf);
  310. $res = new XSCommand($hdr);
  311. $res->buf = $this->read($hdr['blen']);
  312. $res->buf1 = $this->read($hdr['blen1']);
  313. return $res;
  314. }
  315. /**
  316. * 判断服务端是否有可读数据
  317. * 用于某些特别情况在 {@link getRespond} 前先调用和判断, 以免阻塞
  318. * @return bool 如果有返回 true, 否则返回 false
  319. */
  320. public function hasRespond()
  321. {
  322. // check socket
  323. if ($this->_sock === null || $this->_flag & (self::BROKEN | self::FILE)) {
  324. return false;
  325. }
  326. $wfds = $xfds = array();
  327. $rfds = array($this->_sock);
  328. $res = stream_select($rfds, $wfds, $xfds, 0, 0);
  329. return $res > 0;
  330. }
  331. /**
  332. * 写入数据
  333. * @param string $buf 要写入的字符串
  334. * @param string $len 要写入的长度, 默认为字符串长度
  335. * @throw XSException 失败时抛出异常
  336. */
  337. protected function write($buf, $len = 0)
  338. {
  339. // quick return for empty buf
  340. $buf = strval($buf);
  341. if ($len == 0 && ($len = $size = strlen($buf)) == 0) {
  342. return true;
  343. }
  344. // loop to send data
  345. $this->check();
  346. while (true) {
  347. $bytes = fwrite($this->_sock, $buf, $len);
  348. if ($bytes === false || $bytes === 0 || $bytes === $len) {
  349. break;
  350. }
  351. $len -= $bytes;
  352. $buf = substr($buf, $bytes);
  353. }
  354. // error occured
  355. if ($bytes === false || $bytes === 0) {
  356. $meta = stream_get_meta_data($this->_sock);
  357. $this->close(true);
  358. $reason = $meta['timed_out'] ? 'timeout' : ($meta['eof'] ? 'closed' : 'unknown');
  359. $msg = 'Failed to send the data to server completely ';
  360. $msg .= '(SIZE:' . ($size - $len) . '/' . $size . ', REASON:' . $reason . ')';
  361. throw new XSException($msg);
  362. }
  363. }
  364. /**
  365. * 读取数据
  366. * @param int $len 要读入的长度
  367. * @return string 成功时返回读到的字符串
  368. * @throw XSException 失败时抛出异常
  369. */
  370. protected function read($len)
  371. {
  372. // quick return for zero size
  373. if ($len == 0) {
  374. return '';
  375. }
  376. // loop to send data
  377. $this->check();
  378. for ($buf = '', $size = $len;;) {
  379. $bytes = fread($this->_sock, $len);
  380. if ($bytes === false || strlen($bytes) == 0) {
  381. break;
  382. }
  383. $len -= strlen($bytes);
  384. $buf .= $bytes;
  385. if ($len === 0) {
  386. return $buf;
  387. }
  388. }
  389. // error occured
  390. $meta = stream_get_meta_data($this->_sock);
  391. $this->close(true);
  392. $reason = $meta['timed_out'] ? 'timeout' : ($meta['eof'] ? 'closed' : 'unknown');
  393. $msg = 'Failed to recv the data from server completely ';
  394. $msg .= '(SIZE:' . ($size - $len) . '/' . $size . ', REASON:' . $reason . ')';
  395. throw new XSException($msg);
  396. }
  397. /**
  398. * 检测服务端的连接情况
  399. * @throw XSException 连接不可用时抛出异常
  400. */
  401. protected function check()
  402. {
  403. if ($this->_sock === null) {
  404. throw new XSException('No server connection');
  405. }
  406. if ($this->_flag & self::BROKEN) {
  407. throw new XSException('Broken server connection');
  408. }
  409. }
  410. /**
  411. * 连接服务端
  412. * @throw XSException 无法连接时抛出异常
  413. */
  414. protected function connect()
  415. {
  416. // connect to server
  417. $conn = $this->_conn;
  418. if (is_int($conn) || is_numeric($conn)) {
  419. $host = 'localhost';
  420. $port = intval($conn);
  421. } elseif (!strncmp($conn, 'file://', 7)) {
  422. // write-only for saving index exchangable data to file
  423. // NOTE: this will cause file content be turncated
  424. $conn = substr($conn, 7);
  425. if (($sock = @fopen($conn, 'wb')) === false) {
  426. throw new XSException('Failed to open local file for writing: `' . $conn . '\'');
  427. }
  428. $this->_flag |= self::FILE;
  429. $this->_sock = $sock;
  430. return;
  431. } elseif (($pos = strpos($conn, ':')) !== false) {
  432. $host = substr($conn, 0, $pos);
  433. $port = intval(substr($conn, $pos + 1));
  434. } else {
  435. $host = 'unix://' . $conn;
  436. $port = -1;
  437. }
  438. if (($sock = @fsockopen($host, $port, $errno, $error, 5)) === false) {
  439. throw new XSException($error . '(C#' . $errno . ', ' . $host . ':' . $port . ')');
  440. }
  441. // set socket options
  442. $timeout = ini_get('max_execution_time');
  443. $timeout = $timeout > 0 ? ($timeout - 1) : 30;
  444. stream_set_blocking($sock, true);
  445. stream_set_timeout($sock, $timeout);
  446. $this->_sock = $sock;
  447. }
  448. }