Server.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589
  1. <?php
  2. namespace WebIM;
  3. use Swoole;
  4. use Swoole\Filter;
  5. class Server extends Swoole\Protocol\CometServer
  6. {
  7. /**
  8. * @var Store\File;
  9. */
  10. protected $storage;
  11. /**
  12. * clientId(客户端id)
  13. * @var array
  14. */
  15. protected $users;
  16. /**
  17. * 上一次发送消息的时间
  18. * @var array
  19. */
  20. protected $lastSentTime = array();
  21. const MESSAGE_MAX_LEN = 1024; //单条消息不得超过1K
  22. const WORKER_HISTORY_ID = 0;
  23. function __construct($config = array())
  24. {
  25. //将配置写入config.js
  26. $config_js = <<<HTML
  27. var webim = {
  28. 'server' : '{$config['server']['url']}'
  29. }
  30. HTML;
  31. file_put_contents(WEBPATH . '/config.js', $config_js);
  32. //检测日志目录是否存在
  33. $log_dir = dirname($config['webim']['log_file']);
  34. if (!is_dir($log_dir))
  35. {
  36. mkdir($log_dir, 0777, true);
  37. }
  38. if (!empty($config['webim']['log_file']))
  39. {
  40. $logger = new Swoole\Log\FileLog($config['webim']['log_file']);
  41. }
  42. else
  43. {
  44. $logger = new Swoole\Log\EchoLog(true);
  45. }
  46. $this->setLogger($logger); //Logger
  47. /**
  48. * 使用文件或redis存储聊天信息
  49. */
  50. $this->storage = new Storage($config['storage']);
  51. $this->origin = $config['server']['origin'];
  52. parent::__construct($config);
  53. }
  54. /**
  55. * 登录
  56. * @param $client_id
  57. * @param $msg
  58. */
  59. function cmd_login($client_id, $msg)
  60. {
  61. $uid = Filter::escape($msg['uid']);
  62. $role = Filter::escape($msg['role']);
  63. $chatUid = Filter::escape($msg['chatUid']);
  64. $info['name'] = Filter::escape(strip_tags($msg['name']));
  65. $info['avatar'] = Filter::escape($msg['avatar']);
  66. $chatLogKey = Filter::escape($msg['chatLogKey']);
  67. $isCustomer = $role == 'customer' ? true : false;
  68. //回复给登录用
  69. $resMsg = array(
  70. 'cmd' => 'login',
  71. 'fd' => $client_id,
  72. 'uid' => $uid,
  73. 'chatUid' => $chatUid,
  74. 'name' => $info['name'],
  75. 'avatar' => $info['avatar'],
  76. 'chatLogKey' => $chatLogKey,
  77. 'isCustomer' => $isCustomer ? 1 : 0,
  78. );
  79. //把会话存起来
  80. $this->users[$client_id] = $resMsg;
  81. $this->storage->login($client_id, $resMsg, $uid, $chatUid, $isCustomer);
  82. $this->sendJson($client_id, $resMsg);
  83. //用户登录消息
  84. $loginMsg = array(
  85. 'cmd' => 'fromMsg',
  86. 'from' => 0,
  87. 'channal' => 1,
  88. 'clientId' => $client_id,
  89. 'changeClientId' => 1,
  90. 'data' => $info['name'] . "上线了",
  91. );
  92. if($isCustomer){
  93. $clientIds = $this->storage->getMerchantM2C($uid, $chatUid);//取对方客户端号(即商家)
  94. $this->log("customer login success <--> client id : #" . json_encode($clientIds) . " , customerId: $uid, sjId: $chatUid");
  95. }else{
  96. $clientIds = $this->storage->getCustomerC2M($chatUid, $uid);//取对方客户端号(即客户)
  97. $this->storage->remMerchantChater($chatUid, $uid);
  98. $merchantClientIds = $this->storage->getMerchantChatList($client_id, $uid);
  99. $this->log('商家聊天界面登录111 -- 商家聊天列表刷新消息数:clientIds' . json_encode($clientIds));
  100. if(!empty($merchantClientIds)){
  101. //将消息发送给所有商家打开的页面
  102. $this->broadcastToMerchant($merchantClientIds, $chatUid, -1);
  103. $this->log('商家聊天界面登录222 -- 商家聊天列表刷新消息数');
  104. }
  105. $this->log("merchant login success <--> client id : #" . json_encode($clientIds) . ", customerId: $chatUid, sjId: $uid");
  106. }
  107. if(!empty($clientIds)){
  108. foreach ($clientIds as $clientId){
  109. $this->send($clientId, json_encode($loginMsg));
  110. }
  111. }
  112. }
  113. /**
  114. * 下线时,通知指定的人
  115. */
  116. function onExit($client_id)
  117. {
  118. $userInfo = $this->storage->getUser($client_id);
  119. //聊天界面登录的退出
  120. if ($userInfo) {//聊天界面登录的,之前有缓存,故在此会进入
  121. $resMsg = array(
  122. 'cmd' => 'offline',
  123. 'fd' => $client_id,
  124. 'from' => 0,
  125. 'channal' => 0,
  126. 'clientId' => 0,
  127. 'data' => $userInfo['name'] . "下线了",
  128. );
  129. $this->storage->logout($client_id);
  130. unset($this->users[$client_id]);
  131. //当所有客户端都下线了时,将下线消息发送给对方
  132. $uid = $userInfo['uid'];
  133. $chatUid = $userInfo['chatUid'];
  134. if($userInfo['isCustomer'] == 1){
  135. $clientIds = $this->storage->getMerchantM2C($uid, $chatUid);//取对方(即商家)
  136. $clientIdCount = $this->storage->existCustomerClientC2M($uid, $chatUid);//自己(客户)是否还有在线的客户端
  137. $this->log("customer logout, client ids : " . json_encode($clientIds) . ", logout clientId #".$client_id);
  138. }else{
  139. $clientIds = $this->storage->getCustomerC2M($chatUid, $uid);//取对方(即客户)
  140. $clientIdCount = $this->storage->existMerchantClientC2M($chatUid, $uid);//自己(商家)是否还有在线的客户端
  141. $this->log("merchant logout, client ids : " . json_encode($clientIds). ", logout clientId #".$client_id);
  142. }
  143. if(!empty($clientIds) && $clientIdCount == 0){
  144. foreach ($clientIds as $clientId){
  145. $this->send($clientId, json_encode($resMsg));
  146. }
  147. }
  148. }
  149. //商家聊天列表页登录的退出
  150. if(empty($userInfo)){//商家聊天列表页登录的,之前无缓存,故在此会进入
  151. $this->storage->merchantChatListLogout($client_id);
  152. $this->log("merchant chatList page logout : " . $client_id);
  153. }
  154. $this->log("onOffline: " . $client_id);
  155. }
  156. function onTask($serv, $task_id, $from_id, $data)
  157. {
  158. $req = unserialize($data);
  159. if ($req)
  160. {
  161. switch($req['cmd'])
  162. {
  163. case 'getHistory':
  164. $history = array('cmd'=> 'getHistory', 'history' => $this->storage->getHistory($req['fd']));
  165. if ($this->isCometClient($req['fd'])) {
  166. return $req['fd'].json_encode($history);
  167. } else {//WebSocket客户端可以task中直接发送
  168. $this->sendJson(intval($req['fd']), $history);
  169. }
  170. break;
  171. case 'addHistory':
  172. if (empty($req['msg'])) {
  173. $req['msg'] = '';
  174. }
  175. $this->log(json_encode($req));
  176. $this->storage->addHistory($req['fd'], $req['msg']);
  177. break;
  178. default:
  179. break;
  180. }
  181. }
  182. }
  183. function onFinish($serv, $task_id, $data)
  184. {
  185. $this->send(substr($data, 0, 32), substr($data, 32));
  186. }
  187. /**
  188. * 获取在线列表
  189. */
  190. function cmd_getOnline($client_id, $msg)
  191. {
  192. $resMsg = array(
  193. 'cmd' => 'getOnline',
  194. );
  195. $users = $this->storage->getOnlineUsers();
  196. $this->log('onLineUsers:'.json_encode($users));
  197. $info = $this->storage->getUsers(array_slice($users, 0, 100));
  198. $resMsg['users'] = $users;
  199. $resMsg['list'] = $info;
  200. $this->sendJson($client_id, $resMsg);
  201. }
  202. /**
  203. * 获取历史聊天记录
  204. */
  205. function cmd_getHistory($client_id, $msg)
  206. {
  207. $task['fd'] = $client_id;
  208. $task['cmd'] = 'getHistory';
  209. $task['offset'] = '0,100';
  210. //在task worker中会直接发送给客户端
  211. $this->getSwooleServer()->task(serialize($task), self::WORKER_HISTORY_ID);
  212. }
  213. /**
  214. * 发送信息请求
  215. */
  216. function cmd_message($client_id, $msg)
  217. {
  218. $resMsg = $msg;
  219. $resMsg['cmd'] = 'fromMsg';
  220. $userInfo = $this->storage->getUser($client_id);
  221. $isCustomer = $userInfo['isCustomer'];
  222. $now = time();
  223. $who = $isCustomer ? '商家' : '客户';
  224. if($isCustomer){
  225. $customerId = $userInfo['uid'];
  226. $sjId = $userInfo['chatUid'];
  227. }else{
  228. $customerId = $userInfo['chatUid'];
  229. $sjId = $userInfo['uid'];
  230. }
  231. if (strlen($msg['data']) > self::MESSAGE_MAX_LEN) {
  232. $this->sendErrorMessage($client_id, 102, 'message max length is '.self::MESSAGE_MAX_LEN);
  233. return;
  234. }
  235. //上一次发送的时间超过了允许的值,每N秒可以发送一次
  236. if (!empty($this->lastSentTime[$client_id]) && $this->lastSentTime[$client_id] > $now - $this->config['webim']['send_interval_limit']) {
  237. $this->sendErrorMessage($client_id, 104, 'over frequency limit');
  238. return;
  239. }
  240. if ($msg['channal'] == 1) {//表示私聊
  241. $to = isset($msg['to']) ? intval($msg['to']) : 0;//这个可能不准,以下面取的缓存为准
  242. $this->log('传来的参数判断'.$who.'是否在线:'.$to);
  243. //这边也能判断商家与客户的聊天界面打开状态(有多少客户端)
  244. $customerClientIds = $this->storage->getCustomerC2M($customerId, $sjId);
  245. $this->log('##### 客户clientids:'.json_encode($customerClientIds));
  246. $merchantClientIds = $this->storage->getMerchantM2C($customerId, $sjId);
  247. $this->log('##### 商家clientids:'.json_encode($merchantClientIds));
  248. $cacheTo = $isCustomer ? (empty($merchantClientIds) ? 0 : 1) : (empty($customerClientIds) ? 0 : 1);//重新判断在线情况
  249. $this->log('取缓存判断是否在线:'.$cacheTo);
  250. if(($to > 0 && $cacheTo == 0) || ($to == 0 && $cacheTo == 1)){
  251. $to = $cacheTo;
  252. $this->log($who.'在线状态:|*** 缓存 与 传来参数判断不一至 ***|');
  253. }
  254. if($to <= 0){//不在线
  255. $this->log('cmd_message chat person not on line');
  256. if($isCustomer){//当前是客户,消息接收方是商家
  257. $merchantClientIds = $this->storage->getMerchantChatList($client_id, $sjId);
  258. $this->log('商家聊天列表接收消息111:clientIds '. json_encode($merchantClientIds));
  259. if(!empty($merchantClientIds)){
  260. //将消息发送给所有商家打开的页面
  261. $this->broadcastToMerchant($merchantClientIds, $customerId);
  262. $this->log('商家聊天列表接收消息222');
  263. }else{
  264. $this->log('有历史消息');
  265. }
  266. $this->storage->addMerchantNewChater($customerId, $sjId, $now);//记录给商家发来新消息的客户
  267. $this->storage->addMerchantChaterLog($customerId, $sjId, $now);//记录与商家聊天的客户
  268. }else{//当前是商家,消息接收方是客户
  269. $this->storage->addMerchantChaterLog($customerId, $sjId, $now);//记录商家聊过的客户
  270. }
  271. }else{//在线
  272. }
  273. if(!empty($customerClientIds)){
  274. $newResMsg = $resMsg;
  275. if($isCustomer){
  276. $newResMsg['msgType'] = 'C2M';//客户对商家
  277. $newResMsg['customerId'] = $customerId;
  278. }
  279. foreach ($customerClientIds as $clientId){
  280. if ($clientId != $client_id) {
  281. $this->sendJson($clientId, $newResMsg);
  282. }
  283. }
  284. }
  285. if(!empty($merchantClientIds)){
  286. $newResMsg = $resMsg;
  287. if(!$isCustomer){
  288. $newResMsg['msgType'] = 'M2C';//商家对客户
  289. $newResMsg['sjId'] = $sjId;
  290. }
  291. foreach ($merchantClientIds as $clientId){
  292. if ($clientId != $client_id) {
  293. $this->sendJson($clientId, $newResMsg);
  294. }
  295. }
  296. }
  297. $this->storage->addHistory($client_id, $msg['data']);
  298. }elseif ($msg['channal'] == 0) {//未使用
  299. $this->log('notice >>>>>>> cmd_message not use channel 0');
  300. }
  301. //记录本次消息发送的时间
  302. $this->lastSentTime[$client_id] = $now;
  303. }
  304. /**
  305. * 接收到消息时
  306. * @see WSProtocol::onMessage()
  307. */
  308. function onMessage($client_id, $ws)
  309. {
  310. $this->log("onMessage #$client_id: " . $ws['message']);
  311. $msg = json_decode($ws['message'], true);
  312. if (empty($msg['cmd']))
  313. {
  314. $this->sendErrorMessage($client_id, 101, "invalid command");
  315. return;
  316. }
  317. $func = 'cmd_'.$msg['cmd'];
  318. if (method_exists($this, $func))
  319. {
  320. $this->$func($client_id, $msg);
  321. }
  322. else
  323. {
  324. $this->sendErrorMessage($client_id, 102, "command $func no support.");
  325. return;
  326. }
  327. }
  328. /**
  329. * 发送错误信息
  330. * @param $client_id
  331. * @param $code
  332. * @param $msg
  333. */
  334. function sendErrorMessage($client_id, $code, $msg)
  335. {
  336. $this->sendJson($client_id, array('cmd' => 'error', 'code' => $code, 'msg' => $msg));
  337. }
  338. /**
  339. * 发送JSON数据
  340. * @param $client_id
  341. * @param $array
  342. */
  343. function sendJson($client_id, $array)
  344. {
  345. $msg = json_encode($array);
  346. if ($this->send($client_id, $msg) === false)
  347. {
  348. $this->close($client_id);
  349. }
  350. }
  351. /**
  352. * 广播JSON数据
  353. * @param $client_id
  354. * @param $array
  355. */
  356. function broadcastJson($sesion_id, $array)
  357. {
  358. $msg = json_encode($array);
  359. $this->broadcast($sesion_id, $msg);
  360. }
  361. function broadcast($current_session_id, $msg)
  362. {
  363. foreach ($this->users as $client_id => $name)
  364. {
  365. if ($current_session_id != $client_id)
  366. {
  367. $this->send($client_id, $msg);
  368. }
  369. }
  370. }
  371. /*-------------------------------------------- 商家聊天列表页 ------------------------------------------*/
  372. /**
  373. * 登录
  374. * @param $client_id
  375. * @param $msg
  376. */
  377. function cmd_chatListMerchantLogin($client_id, $msg)
  378. {
  379. $uid = Filter::escape($msg['uid']);
  380. $role = Filter::escape($msg['role']);
  381. $info['name'] = Filter::escape(strip_tags($msg['name']));
  382. $info['avatar'] = Filter::escape($msg['avatar']);
  383. $isCustomer = $role == 'customer' ? true : false;
  384. //回复给登录用
  385. $resMsg = array(
  386. 'cmd' => 'login',
  387. 'fd' => $client_id,
  388. 'uid' => $uid,
  389. 'name' => $info['name'],
  390. 'avatar' => $info['avatar'],
  391. 'isCustomer' => $isCustomer ? 1 : 0,
  392. );
  393. //把会话存起来
  394. //$this->users[$client_id] = $resMsg;
  395. $this->storage->chatListLogin($client_id, $resMsg, $uid, $isCustomer);
  396. $this->sendJson($client_id, $resMsg);
  397. //广播给其它在线用户
  398. //$resMsg['cmd'] = 'newUser';
  399. //将上线消息发送给所有人
  400. //$this->broadcastJson($client_id, $resMsg);
  401. }
  402. /**
  403. * 获取商家聊天人的历史记录
  404. * @param $client_id
  405. * @param $msg
  406. */
  407. function cmd_getMerchantChatLog($client_id, $msg){
  408. $resMsg = array(
  409. 'cmd' => 'getMerchantChatLog',
  410. );
  411. $merchant = $this->storage->getMerchant($client_id);
  412. $sjId = $merchant['uid'];
  413. $array = $this->storage->getMerchantChaterLog($sjId);
  414. $newMsgCustomers = [];
  415. foreach ($array['newMsgCustomers'] as $uid){
  416. $key = 'xhUser_' . $uid;
  417. $customer = $this->storage->getCustomer($key);
  418. $newMsgCustomers[] = ['id'=>$customer['id'], 'userName'=>$customer['userName']];
  419. }
  420. $resMsg['newMsgCustomers'] = $newMsgCustomers;
  421. $historyCustomers = [];
  422. foreach($array['historyCustomers'] as $uid){
  423. $key = 'xhUser_' . $uid;
  424. $customer = $this->storage->getCustomer($key);
  425. $historyCustomers[] = ['id'=>$customer['id'], 'userName'=>$customer['userName']];
  426. }
  427. $resMsg['historyCustomers'] = $historyCustomers;
  428. $this->log('获取商家聊天人的历史记录: merchantid '.$sjId );
  429. $this->sendJson($client_id, $resMsg);
  430. }
  431. /**
  432. * 给商家客户端广播消息
  433. * @param $clientIds
  434. * @param $customerId
  435. * @param int $count 消息数(正数增加,负数减少)
  436. */
  437. function broadcastToMerchant($clientIds, $customerId, $count = 1){
  438. //刷新客户消息数用
  439. $key = 'xhUser_' . $customerId;
  440. $customer = $this->storage->getCustomer($key);
  441. $customer = ['id'=>$customer['id'], 'userName'=>$customer['userName']];
  442. $resMsg = array(
  443. 'cmd' => 'refreshMsgCount',
  444. 'customer' => $customer,
  445. 'count' => $count,
  446. );
  447. foreach ($clientIds as $clientId){
  448. $this->sendJson($clientId, $resMsg);
  449. }
  450. }
  451. /**
  452. * 客户在微信上给商家发消息
  453. * @param $client_id
  454. * @param $msg
  455. */
  456. function cmd_sendMsgC2M($client_id, $msg){
  457. $resMsg = $msg;
  458. $resMsg['cmd'] = 'fromMsg';
  459. $now = time();
  460. $isCustomer = 1;//当前是客户
  461. $customerId = $msg['uid'];//客户Uid
  462. $sjId = $msg['chatUid'];//商家MerchantId
  463. $who = $isCustomer ? '商家' : '客户';
  464. if (strlen($msg['data']) > self::MESSAGE_MAX_LEN)
  465. {
  466. $this->sendErrorMessage($client_id, 102, 'message max length is '.self::MESSAGE_MAX_LEN);
  467. return;
  468. }
  469. if ($msg['channal'] == 1) {//表示私聊
  470. //判断商家与客户的聊天界面打开状态(有多少客户端)
  471. $customerClientIds = $this->storage->getCustomerC2M($customerId, $sjId);
  472. $this->log('##### 客户clientids:'.json_encode($customerClientIds));
  473. $merchantClientIds = $this->storage->getMerchantM2C($customerId, $sjId);
  474. $this->log('##### 商家clientids:'.json_encode($merchantClientIds));
  475. $cacheTo = $isCustomer ? (empty($merchantClientIds) ? 0 : 1) : (empty($customerClientIds) ? 0 : 1);//重新判断在线情况
  476. $this->log('取缓存判断'.$who.'是否在线:'.$cacheTo);
  477. if($cacheTo <= 0){//不在线
  478. if($isCustomer){//当前是客户,消息接收方是商家
  479. $clientIds = $this->storage->getMerchantChatList($client_id, $sjId);
  480. $this->log('商家聊天列表接收消息 1.sendMsgC2M:clientIds '. json_encode($clientIds));
  481. if(!empty($clientIds)){
  482. //将消息发送给所有商家打开的页面
  483. $this->broadcastToMerchant($clientIds, $customerId);
  484. $this->log('商家聊天列表接收消息 2.sendMsgC2M');
  485. }else{
  486. $this->log('写入历史消息');
  487. }
  488. $this->storage->addMerchantNewChater($customerId, $sjId, $now);//记录给商家发来新消息的客户
  489. $this->storage->addMerchantChaterLog($customerId, $sjId, $now);//记录与商家聊天的客户
  490. }else{//当前是商家,消息接收方是客户
  491. $this->log('cmd:sendMsgC2M not use in merchant (当前是商家,消息接收方是客户)');
  492. }
  493. }else{//在线
  494. }
  495. if(!empty($customerClientIds)){
  496. $newResMsg = $resMsg;
  497. if($isCustomer){
  498. $newResMsg['msgType'] = 'C2M';//客户对商家
  499. $newResMsg['customerId'] = $customerId;
  500. }
  501. foreach ($customerClientIds as $clientId){
  502. if ($clientId != $client_id) {
  503. $this->sendJson($clientId, $newResMsg);
  504. }
  505. }
  506. }
  507. if(!empty($merchantClientIds)){
  508. $newResMsg = $resMsg;
  509. if(!$isCustomer){
  510. $newResMsg['msgType'] = 'M2C';//商家对客户
  511. $newResMsg['sjId'] = $sjId;
  512. }
  513. foreach ($merchantClientIds as $clientId){
  514. if ($clientId != $client_id) {
  515. $this->sendJson($clientId, $newResMsg);
  516. }
  517. }
  518. }
  519. $cacheData = ['chatLogKey'=>$msg['chatLogKey'], 'isCustomer'=>$isCustomer];
  520. $this->storage->addHistoryByData($cacheData, $msg['data']);
  521. }elseif ($msg['channal'] == 0) {//未使用
  522. $this->log('cmd:sendMsgC2M channal not use 0');
  523. }
  524. }
  525. }