- index.php 前端显示 页面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>聊天室</title> <link rel="stylesheet" href="css/style.css"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> </head> <body> <div class="head"></div> <div id="wrapper"> <div id="message"> </div> <div id="action"> <textarea id="data"></textarea> <button id="send">发送</button> </div> </div> <script> (function() { var socket = new WebSocket('ws://<?=?>:8879'); var send = document.getElementById('send'); var data = document.getElementById('data'); var message = document.getElementById('message'); var wrapper = document.getElementById('wrapper'); var height = (wrapper.offsetHeight) -270; message.style.height = height+'px'; socket.onopen = function(event) { message.innerHTML = '<p><span>连接成功!</span></p>'; } socket.onmessage = function(event) { var dl = document.createElement('dl'); var jsonData = JSON.parse(event.data); if((jsonData.soID=='<?=$soID?>'&&jsonData.toID=='<?=$toID?>')||(jsonData.soID=='<?=$toID?>'&&jsonData.toID=='<?=$soID?>') ){ var img =''; if(jsonData.soID=='<?=$soID?>' ) { img ='<?=$simg?>'; name ='<?=$sname?>'; } else { img ='<?=$timg?>'; name ='<?=$tname?>'; } dl.innerHTML = "<dt><img style='width: 50px' src="+img+"><dt><dd><span></span>" + "<dd>"+name+" 说<name/dd>" + ""+jsonData.content+"</dd>"; message.appendChild(dl); message.scrollTop = message.scrollHeight; } } socket.onerror = function(e) { console.log(e); message.innerHTML = '<p><span>连接失败!</span></p>'; } send.addEventListener('click', function() { var content = data.value; if(content.length <= 0) { alert('消息不能为空!'); return false; } var avatar = Math.random(); var message = { "soID" : "<?=$soID?>", "toID" : "<?=$toID?>", "content" : content } var json = JSON.stringify(message); socket.send(json); data.value = ''; data.focus(); //将websocket 的消息通过ajax 推送到微信 $.ajax({ type: "GET", url: "Ajax.php", data: {'a': 'websocket_send', 'openId': "<?=$soID?>", "收方oid" : "<?=$toID?>", "交互内容" : content, "消息类型":"聊天", '时间':'<?= time()?>', '消息格式':"text", 'media_id':'' }, dataType: 'text', success: function (data) { if(data !=开始聊天){ alert(data) } } }); }); })();</script></body></html>config()::application_server_ip
- websocket 的service
<?php require_once dirname(dirname(__DIR__)).'/system/bootstrap.php'; class WebSocket { private $socket; private $accept; private $isHand = array(); public function __construct($host, $port, $max) { $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, TRUE); socket_bind($this->socket, $host, $port); socket_listen($this->socket, $max); } public function start() { while(true) { $cycle = $this->accept; $cycle[] = $this->socket; socket_select($cycle, $write, $except, null); foreach($cycle as $sock) { if($sock === $this->socket) { $client = socket_accept($this->socket); $this->accept[] = $client; $key = array_keys($this->accept); $key = end($key); $this->isHand[$key] = false; } else { $length = socket_recv($sock, $buffer, 204800, 0); $key = array_search($sock, $this->accept); if($length < 7) { $this->close($sock); continue; } if(!$this->isHand[$key]) { $this->dohandshake($sock, $buffer, $key); } else { // 先解码,再编码 $data = $this->decode($buffer); $data = $this->encode($data); // 判断断开连接(断开连接时数据长度小于10) if(strlen($data) > 10) { foreach($this->accept as $client) { socket_write($client, $data, strlen($data)); } } } } } } } /** * 首次与客户端握手 */ public function dohandshake($sock, $data, $key) { if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) { $response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" . "Upgrade: websocket\r\n" . "Connection: Upgrade\r\n" . "Sec-WebSocket-Accept: " . $response . "\r\n\r\n"; socket_write($sock, $upgrade, strlen($upgrade)); $this->isHand[$key] = true; } } /** * 关闭一个客户端连接 */ public function close($sock) { $key = array_search($sock, $this->accept); socket_close($sock); unset($this->accept[$key]); unset($this->handshake[$key]); } /** * 解码过程 */ public function decode($buffer) { $len = $masks = $data = $decoded = null; $len = ord($buffer[1]) & 127; if ($len === 126) { $masks = substr($buffer, 4, 4); $data = substr($buffer, 8); } else if ($len === 127) { $masks = substr($buffer, 10, 4); $data = substr($buffer, 14); } else { $masks = substr($buffer, 2, 4); $data = substr($buffer, 6); } for ($index = 0; $index < strlen($data); $index++) { $decoded .= $data[$index] ^ $masks[$index % 4]; } return $decoded; } /** * 编码过程 */ public function encode($buffer) { $length = strlen($buffer); if($length <= 125) { return "\x81".chr($length).$buffer; } else if($length <= 65535) { return "\x81".chr(126).pack("n", $length).$buffer; } else { return "\x81".chr(127).pack("xxxxN", $length).$buffer; } } } $webSocket = new WebSocket(config()::application_server_ip, 8879, 100); $webSocket->start();
- BChat 将微信通过url 推送过来的消息 推送到 websocket
$ip =config()::application_server_ip; error_reporting(E_ALL); $testClients = 1; $client = new WebsocketClient; $client->connect($ip, 8879, '', 'foo.lh'); usleep(5000); $payload = json_encode(array( 'soID' => $fromopenId, 'toID' => $toID, 'content' => 内容, )); //和websocket的index.php 页面的对象保持一致 $clientId = rand(0, $testClients-1); return $client->sendData($payload);
- ajax.php 将index.php的消息通过ajax 推送到微信
$chat = new BChat(); $提交数据['发方oid']=$提交数据['openId']; return $reply = $chat->fwebsocket_推送微信消息( $提交数据['发方oid'], $提交数据['收方oid'], $提交数据);
-
BChat类的发送 消息 方法
public function fwebsocket_推送微信消息($from用户Openid, $to用户Openid, $content){ $appid = Base::current_公众号appid(); if ($content == '再见' ||$content == 'bye') { return static::f结束聊天_jslt($appid, $from用户Openid); } //准备数据 /** @var M交互状态表 $m交互状态to */ $m交互状态to = M交互状态表::newInstance()->rowByOpenid($to用户Openid); $m交互状态from = M交互状态表::newInstance()->rowByOpenid($from用户Openid); $m交互历史 = M交互历史表::newInstance()->setDatabywebsocket($from用户Openid,$to用户Openid,$content); //构造回复聊天的链接 $fromName = $m交互历史->get_发方姓名(); //构造回复聊天的链接 $reply = static::_构造聊天reply($m交互状态to,$m交互历史,$from用户Openid); $url = static::_构造聊天url($m交互状态from, $m交互状态to); //发送消息 if($m交互历史->get_是否发送成功() == 1) static::_发送聊天消息($fromName, $url, $m交互状态to, $m交互历史); //Tools::dump($m交互历史->data); //更新并返回 $m交互状态from->update聊天($m交互状态from,$m交互历史); if($reply){ return $reply; }else{ return '开始聊天'; } }
private static function _构造聊天reply(M交互状态表 $m交互状态to, M交互历史表 $m交互历史,$fromopenId) { $to用户Openid = $m交互状态to->get_用户oid(); $reply=''; //聊天对象不同状态,回复不同 switch ($m交互状态to->get_状态()) { case M交互状态表::enum_状态_PC端在线(): static::f发送websocket($m交互状态to,$m交互历史,$fromopenId); $reply = '[' . static::buildHref($to用户Openid, $m交互历史->get_收方姓名()) . ']已经在PC端上线,本消息已经成功发送.'; $m交互历史->set_是否发送成功(1); break; case M交互状态表::enum_状态_离线(): case M交互状态表::enum_状态_不活跃(): $reply = '[' . static::buildHref($to用户Openid, $m交互历史->get_收方姓名()) . ']已经离线,本消息将在对方上线后自动发送'; $m交互历史->set_是否发送成功(0); break; case M交互状态表::enum_状态_聊天(): if($m交互状态to->get_聊天对象oid() ==$fromopenId || $m交互状态to->get_聊天对象oid() == ''){ // $reply = '消息已发送给:[' . static::buildHref($to用户Openid, $m交互历史->get_收方姓名()) . ']'; }else{ $reply = '[' . static::buildHref($to用户Openid, $m交互历史->get_收方姓名()) . ']正在忙,请稍候'; } $m交互历史->set_是否发送成功(1); break; default: $m交互历史->set_是否发送成功(1); //no reply } return $reply; }
private static function _发送聊天消息(string $fromName, string $url, M交互状态表 $m交互状态to, M交互历史表 $m交互历史) { $appidTo = $m交互状态to->get_用户公众号appid(); $to用户Openid = $m交互状态to->get_用户oid(); $media_id = $m交互历史->get_media_id()?:''; switch ($m交互历史->get_消息格式()) { case M交互历史表::enum_消息格式_voice(): Weixin::sendText($appidTo, $to用户Openid, ' [<a href="' . $url . '">' . $fromName . '</a>]: '); Weixin::sendVoice($appidTo, $to用户Openid, $media_id); break; case M交互历史表::enum_消息格式_image(): Weixin::sendText($appidTo, $to用户Openid, ' [<a href="' . $url . '">' . $fromName . '</a>]: '); Weixin::sendImage($appidTo, $to用户Openid, $media_id); break; case M交互历史表::enum_消息格式_video(): Weixin::sendText($appidTo, $to用户Openid, ' [<a href="' . $url . '">' . $fromName . '</a>]: '); Weixin::sendVideo($appidTo, $to用户Openid, $media_id, $m交互历史['ThumbMediaId'], $m交互历史['发方姓名']); break; default: Weixin::sendText($appidTo, $to用户Openid, ' [<a href="' . $url . '">' . $fromName . '</a>]: ' . $m交互历史->get_交互内容()); } }
-
Weixin 下的发送 文本的消息
static public function sendText($appid, $ToUserName, $content) { /* @var $class Weixin */ $class = new self($appid); $msg = new MsgSendText($ToUserName, $content); return $class->send($msg); }
-
MsgSendText class
public function send(MsgSend $msg) { $url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" . $this->token(); $ret = $this->call($url, $msg->toJson()); $jsonRet = $ret; if (!empty($jsonRet->errcode) &&$jsonRet->errcode>0) { M日志调试信息::newInstance()->add日志消息发送失败($msg, $this->appid, $jsonRet->errcode); } else { M日志调试信息::newInstance()->add消息发送记录成功($msg, $this->appid); } return $ret; }private function call(string $url,$postData = null, array $header = array()) { $net = (new SNet($url))->setHeader($header); $r = json_decode($net->getOrPost($postData)); //如果token超时需要再次调用 if (Assert::weixin_invalide_access_token($r)) { $token = static::updateToken(); $url = preg_replace('/([\?&]access_token=)([^&]+)(&.*)?/ism', '$1' . $token . '$3', $url); //再次调用 $r = json_decode($net->curl($url,$postData)); } return $r; }
SNet 类
<?php /** * 封装网络请求,主要是CURL */ class SNet { /**@var string 本次请求的地址 */ private $url; /** * @param string $url */ public function setUrl($url) { $this->url = $url; } /** * 构造方法,记录请求地址 * @param $url string */ public function __construct(string $url) { $this->url = $url; } /** * 超时时间设置 * @var int */ private $timeout = 60; /** * 设置超时时间 * @param $seconds int * @return $this */ public function setTimeout($seconds) { $this->timeout = $seconds; return $this; } /** * 请求头参数数组 * @var array */ private $header; /** * 设置请求头参数 数组 * @param array $header * @return $this */ public function setHeader(array $header) { $this->header = $header; return $this; } /** * SSL 证书 * @var string */ private $ssl_cert, $ssl_key; /** * 设置 SSL 证书 * @param $cert string * @param $key string * @return $this */ public function setSSL($cert, $key) { $this->ssl_cert = $cert; $this->ssl_key = $key; return $this; } /** * 常用的请求 * @param $url string * @param array $params * @return mixed */ public static function curl($url, $params = []) { $net = new self($url); return $net->getOrPost($params); } /** * 发起GET请求 * @return mixed */ public function get() { return $this->getOrPost(); } /** * 发起POST请求 * @param $params string|array 参数 数组或字符串 * @return mixed */ public function post($params) { return $this->getOrPost($params); } /** * 发起CURL请求 * @param array|string $params * @return mixed */ public function getOrPost($params = null) { //记录开始时间 //$begin=microtime(true); //Tools::print_stack_trace(); if (isset($params['media'])) { $params = array('file' => new \CURLFile($params['media'])); } $options = [ CURLOPT_CONNECTTIMEOUT => $this->timeout, CURLOPT_RETURNTRANSFER => 1, CURLOPT_FOLLOWLOCATION => 1, CURLOPT_SSL_VERIFYPEER => FALSE, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_NOBODY => 0, CURLOPT_TIMEOUT => 10, CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 5.1; rv:21.0) Gecko/20100101 Firefox/21.0', CURLOPT_URL => $this->url ]; if ($this->header) { $options[CURLOPT_HTTPHEADER] = $this->header; } if ($params) { $options[CURLOPT_POST] = true; $options[CURLOPT_POSTFIELDS] = $params; } else { $options[CURLOPT_HTTPGET] = true; } if ($this->ssl_cert) { $options[CURLOPT_SSLCERT] = $this->ssl_cert; $options[CURLOPT_SSLKEY] = $this->ssl_key; } // M日志调试信息::newInstance()->debug('options',$options); //创建CURL请求 $ch = curl_init(); assert($ch !== false); curl_setopt_array($ch, $options); $result = curl_exec($ch); M日志调试信息::newInstance()->debug('错误码', curl_errno($ch) . curl_error($ch)); //curl no error assert(Assert::curl正确返回($ch, $result), "错误码." . curl_errno($ch) . curl_error($ch)); curl_close($ch); //记录请求时间日志 //SDebug::setNet($this->url,$params,$result,microtime(true)-$begin); return $result; } }
-
WebsocketClient 类 用于php 将信息推送到websocket
<?php ini_set('display_errors', 1); error_reporting(E_ALL); /** * Very basic websocket client. * Supporting draft hybi-10. * * @author Simon Samtleben <web@lemmingzshadow.net> * @version 2011-10-18 */ class WebsocketClient { private $_host; private $_port; private $_path; private $_origin; private $_Socket = null; private $_connected = false; public function __construct() { } public function __destruct() { $this->disconnect(); } public function sendData($data, $type = 'text', $masked = true) { if($this->_connected === false) { trigger_error("Not connected", E_USER_WARNING); return false; } if( !is_string($data)) { trigger_error("Not a string data was given.", E_USER_WARNING); return false; } if (strlen($data) == 0) { return false; } $res = @fwrite($this->_Socket, $this->_hybi10Encode($data, $type, $masked)); if($res === 0 || $res === false) { return false; } $buffer = ' '; while($buffer !== '') { $buffer = fread($this->_Socket, 512);// drop? } return true; } public function connect($host, $port, $path, $origin = false) { $this->_host = $host; $this->_port = $port; $this->_path = $path; $this->_origin = $origin; $key = base64_encode($this->_generateRandomString(16, false, true)); $header = "GET " . $path . " HTTP/1.1\r\n"; $header.= "Host: ".$host.":".$port."\r\n"; $header.= "Upgrade: websocket\r\n"; $header.= "Connection: Upgrade\r\n"; $header.= "Sec-WebSocket-Key: " . $key . "\r\n"; if($origin !== false) { $header.= "Sec-WebSocket-Origin: " . $origin . "\r\n"; } $header.= "Sec-WebSocket-Version: 13\r\n\r\n"; $this->_Socket = fsockopen($host, $port, $errno, $errstr, 2); socket_set_timeout($this->_Socket, 0, 10000); @fwrite($this->_Socket, $header); $response = @fread($this->_Socket, 1500); preg_match('#Sec-WebSocket-Accept:\s(.*)$#mU', $response, $matches); if ($matches) { $keyAccept = trim($matches[1]); $expectedResponse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'))); $this->_connected = ($keyAccept === $expectedResponse) ? true : false; } return $this->_connected; } public function checkConnection() { $this->_connected = false; // send ping: $data = 'ping?'; @fwrite($this->_Socket, $this->_hybi10Encode($data, 'ping', true)); $response = @fread($this->_Socket, 300); if(empty($response)) { return false; } $response = $this->_hybi10Decode($response); if(!is_array($response)) { return false; } if(!isset($response['type']) || $response['type'] !== 'pong') { return false; } $this->_connected = true; return true; } public function disconnect() { $this->_connected = false; is_resource($this->_Socket) and fclose($this->_Socket); } public function reconnect() { sleep(10); $this->_connected = false; fclose($this->_Socket); $this->connect($this->_host, $this->_port, $this->_path, $this->_origin); } private function _generateRandomString($length = 10, $addSpaces = true, $addNumbers = true) { $characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"§$%&/()=[]{}'; $useChars = array(); // select some random chars: for($i = 0; $i < $length; $i++) { $useChars[] = $characters[mt_rand(0, strlen($characters)-1)]; } // add spaces and numbers: if($addSpaces === true) { array_push($useChars, ' ', ' ', ' ', ' ', ' ', ' '); } if($addNumbers === true) { array_push($useChars, rand(0,9), rand(0,9), rand(0,9)); } shuffle($useChars); $randomString = trim(implode('', $useChars)); $randomString = substr($randomString, 0, $length); return $randomString; } private function _hybi10Encode($payload, $type = 'text', $masked = true) { $frameHead = array(); $frame = ''; $payloadLength = strlen($payload); switch($type) { case 'text': // first byte indicates FIN, Text-Frame (10000001): $frameHead[0] = 129; break; case 'close': // first byte indicates FIN, Close Frame(10001000): $frameHead[0] = 136; break; case 'ping': // first byte indicates FIN, Ping frame (10001001): $frameHead[0] = 137; break; case 'pong': // first byte indicates FIN, Pong frame (10001010): $frameHead[0] = 138; break; } // set mask and payload length (using 1, 3 or 9 bytes) if($payloadLength > 65535) { $payloadLengthBin = str_split(sprintf('%064b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 255 : 127; for($i = 0; $i < 8; $i++) { $frameHead[$i+2] = bindec($payloadLengthBin[$i]); } // most significant bit MUST be 0 (close connection if frame too big) if($frameHead[2] > 127) { $this->close(1004); return false; } } elseif($payloadLength > 125) { $payloadLengthBin = str_split(sprintf('%016b', $payloadLength), 8); $frameHead[1] = ($masked === true) ? 254 : 126; $frameHead[2] = bindec($payloadLengthBin[0]); $frameHead[3] = bindec($payloadLengthBin[1]); } else { $frameHead[1] = ($masked === true) ? $payloadLength + 128 : $payloadLength; } // convert frame-head to string: foreach(array_keys($frameHead) as $i) { $frameHead[$i] = chr($frameHead[$i]); } if($masked === true) { // generate a random mask: $mask = array(); for($i = 0; $i < 4; $i++) { $mask[$i] = chr(rand(0, 255)); } $frameHead = array_merge($frameHead, $mask); } $frame = implode('', $frameHead); // append payload to frame: $framePayload = array(); for($i = 0; $i < $payloadLength; $i++) { $frame .= ($masked === true) ? $payload[$i] ^ $mask[$i % 4] : $payload[$i]; } return $frame; } private function _hybi10Decode($data) { $payloadLength = ''; $mask = ''; $unmaskedPayload = ''; $decodedData = array(); // estimate frame type: $firstByteBinary = sprintf('%08b', ord($data[0])); $secondByteBinary = sprintf('%08b', ord($data[1])); $opcode = bindec(substr($firstByteBinary, 4, 4)); $isMasked = ($secondByteBinary[0] == '1') ? true : false; $payloadLength = ord($data[1]) & 127; switch($opcode) { // text frame: case 1: $decodedData['type'] = 'text'; break; case 2: $decodedData['type'] = 'binary'; break; // connection close frame: case 8: $decodedData['type'] = 'close'; break; // ping frame: case 9: $decodedData['type'] = 'ping'; break; // pong frame: case 10: $decodedData['type'] = 'pong'; break; default: return false; break; } if($payloadLength === 126) { $mask = substr($data, 4, 4); $payloadOffset = 8; $dataLength = bindec(sprintf('%08b', ord($data[2])) . sprintf('%08b', ord($data[3]))) + $payloadOffset; } elseif($payloadLength === 127) { $mask = substr($data, 10, 4); $payloadOffset = 14; $tmp = ''; for($i = 0; $i < 8; $i++) { $tmp .= sprintf('%08b', ord($data[$i+2])); } $dataLength = bindec($tmp) + $payloadOffset; unset($tmp); } else { $mask = substr($data, 2, 4); $payloadOffset = 6; $dataLength = $payloadLength + $payloadOffset; } if($isMasked === true) { for($i = $payloadOffset; $i < $dataLength; $i++) { $j = $i - $payloadOffset; if(isset($data[$i])) { $unmaskedPayload .= $data[$i] ^ $mask[$j % 4]; } } $decodedData['payload'] = $unmaskedPayload; } else { $payloadOffset = $payloadOffset - 4; $decodedData['payload'] = substr($data, $payloadOffset); } return $decodedData; } }