因为本人的工作需要,偶尔被要求实现一些市面上已经有的成熟接口功能。这里要转折一下,不是说我实现的功能的稳定性和成熟度已经达到了可以商用的标准,只是被用作一个给客户展示的demo而已,有点小尴尬。
进入正题吧,用web实现im功能,目前主流的解决方案总结为如下两点:
1.使用服务器轮询技术实现。
2.使用websocket技术实现。
恰巧以上两种方式,我都没研究过,轮询技术大致懂,但是这样做容易徒废服务器资源,并且公司提供的demo服务器配置也很low,我怕给客户演示的时候,它宕掉了,我特么的工作也就被宕掉了。再说到websocket,这个基本是市面上我了解的webim接扣的技术基础,看了看websocket的php(对,我特么也是个光荣的php开发者~php是世界上最好的语言~轻喷)服务端转发代码,有点耽搁时间,遂抛弃了它。
我百度了一圈,可悲的发现,没有一个是对我有用的,可能说是没有一个在我想耗费的时间内完成。这个时候大多牛逼程序员会灵光乍现,然后手速飞快的自己敲出了一套市面上没有的新技术,看到这里,你们就懂了,我特么不是这样的程序员,好吧,在我放弃了这些技术后,我想起来以前给一个电力能效的项目用mqtt(不懂mqtt的同学可以自行百度下)挂过长连接,实时反馈电力设备数据到web管理端,反正大家都是长连接,我用来干干,也是不碍事的吧。
一、准备工作:
1.客户端:mqttws31.min.js mqtt官方给的js客户端。
2.服务端:phpMQtt.php mqttphp端脚本。
recive.php 接收信息,实例化mqtt进行转发脚本。
mqttws31.min.js可以再mqtt官网进行下载,php端代码在这里粘贴出来,需要用的可以自取(因为我也是借鉴别人的)
broker($address, $port, $clientid);
}
/* sets the broker details */
function broker($address, $port, $clientid){
$this->address = $address;
$this->port = $port;
$this->clientid = $clientid;
}
function connect_auto($clean = true, $will = NULL, $username = NULL, $password = NULL){
while($this->connect($clean, $will, $username, $password)==false){
sleep(10);
}
return true;
}
/* connects to the broker
inputs: $clean: should the client send a clean session flag */
function connect($clean = true, $will = NULL, $username = NULL, $password = NULL){
if($will) $this->will = $will;
if($username) $this->username = $username;
if($password) $this->password = $password;
$address = gethostbyname($this->address);
$this->socket = fsockopen($address, $this->port, $errno, $errstr, 60);
if (!$this->socket ) {
if($this->debug) error_log("fsockopen() $errno, $errstr \n");
return false;
}
stream_set_timeout($this->socket, 5);
stream_set_blocking($this->socket, 0);
$i = 0;
$buffer = "";
$buffer .= chr(0x00); $i++;
$buffer .= chr(0x06); $i++;
$buffer .= chr(0x4d); $i++;
$buffer .= chr(0x51); $i++;
$buffer .= chr(0x49); $i++;
$buffer .= chr(0x73); $i++;
$buffer .= chr(0x64); $i++;
$buffer .= chr(0x70); $i++;
$buffer .= chr(0x03); $i++;
//No Will
$var = 0;
if($clean) $var+=2;
//Add will info to header
if($this->will != NULL){
$var += 4; // Set will flag
$var += ($this->will['qos'] << 3); //Set will qos
if($this->will['retain']) $var += 32; //Set will retain
}
if($this->username != NULL) $var += 128; //Add username to header
if($this->password != NULL) $var += 64; //Add password to header
$buffer .= chr($var); $i++;
//Keep alive
$buffer .= chr($this->keepalive >> 8); $i++;
$buffer .= chr($this->keepalive & 0xff); $i++;
$buffer .= $this->strwritestring($this->clientid,$i);
//Adding will to payload
if($this->will != NULL){
$buffer .= $this->strwritestring($this->will['topic'],$i);
$buffer .= $this->strwritestring($this->will['content'],$i);
}
if($this->username) $buffer .= $this->strwritestring($this->username,$i);
if($this->password) $buffer .= $this->strwritestring($this->password,$i);
$head = " ";
$head{0} = chr(0x10);
$head{1} = chr($i);
fwrite($this->socket, $head, 2);
fwrite($this->socket, $buffer);
$string = $this->read(4);
if(ord($string{0})>>4 == 2 && $string{3} == chr(0)){
if($this->debug) echo "Connected to Broker\n";
}else{
error_log(sprintf("Connection failed! (Error: 0x%02x 0x%02x)\n",
ord($string{0}),ord($string{3})));
return false;
}
$this->timesinceping = time();
return true;
}
/* read: reads in so many bytes */
function read($int = 8192, $nb = false){
// print_r(socket_get_status($this->socket));
$string="";
$togo = $int;
if($nb){
return fread($this->socket, $togo);
}
while (!feof($this->socket) && $togo>0) {
$fread = fread($this->socket, $togo);
$string .= $fread;
$togo = $int - strlen($string);
}
return $string;
}
/* subscribe: subscribes to topics */
function subscribe($topics, $qos = 0){
$i = 0;
$buffer = "";
$id = $this->msgid;
$buffer .= chr($id >> 8); $i++;
$buffer .= chr($id % 256); $i++;
foreach($topics as $key => $topic){
$buffer .= $this->strwritestring($key,$i);
$buffer .= chr($topic["qos"]); $i++;
$this->topics[$key] = $topic;
}
$cmd = 0x80;
//$qos
$cmd += ($qos << 1);
$head = chr($cmd);
$head .= chr($i);
fwrite($this->socket, $head, 2);
fwrite($this->socket, $buffer, $i);
$string = $this->read(2);
$bytes = ord(substr($string,1,1));
$string = $this->read($bytes);
}
/* ping: sends a keep alive ping */
function ping(){
$head = " ";
$head = chr(0xc0);
$head .= chr(0x00);
fwrite($this->socket, $head, 2);
if($this->debug) echo "ping sent\n";
}
/* disconnect: sends a proper disconect cmd */
function disconnect(){
$head = " ";
$head{0} = chr(0xe0);
$head{1} = chr(0x00);
fwrite($this->socket, $head, 2);
}
/* close: sends a proper disconect, then closes the socket */
function close(){
$this->disconnect();
fclose($this->socket);
}
/* publish: publishes $content on a $topic */
function publish($topic, $content, $qos = 0, $retain = 0){
$i = 0;
$buffer = "";
$buffer .= $this->strwritestring($topic,$i);
//$buffer .= $this->strwritestring($content,$i);
if($qos){
$id = $this->msgid++;
$buffer .= chr($id >> 8); $i++;
$buffer .= chr($id % 256); $i++;
}
$buffer .= $content;
$i+=strlen($content);
$head = " ";
$cmd = 0x30;
if($qos) $cmd += $qos << 1;
if($retain) $cmd += 1;
$head{0} = chr($cmd);
$head .= $this->setmsglength($i);
fwrite($this->socket, $head, strlen($head));
fwrite($this->socket, $buffer, $i);
}
/* message: processes a recieved topic */
function message($msg){
$tlen = (ord($msg{0})<<8) + ord($msg{1});
$topic = substr($msg,2,$tlen);
$msg = substr($msg,($tlen+2));
$found = 0;
foreach($this->topics as $key=>$top){
if( preg_match("/^".str_replace("#",".*",
str_replace("+","[^\/]*",
str_replace("/","\/",
str_replace("$",'\$',
$key))))."$/",$topic) ){
if(is_callable($top['function'])){
call_user_func($top['function'],$topic,$msg);
$found = 1;
}
}
}
if($this->debug && !$found) echo "msg recieved but no match in subscriptions\n";
}
/* proc: the processing loop for an "allways on" client
set true when you are doing other stuff in the loop good for watching something else at the same time */
function proc( $loop = true){
if(1){
$sockets = array($this->socket);
$w = $e = NULL;
$cmd = 0;
//$byte = fgetc($this->socket);
if(feof($this->socket)){
if($this->debug) echo "eof receive going to reconnect for good measure\n";
fclose($this->socket);
$this->connect_auto(false);
if(count($this->topics))
$this->subscribe($this->topics);
}
$byte = $this->read(1, true);
if(!strlen($byte)){
if($loop){
usleep(100000);
}
}else{
$cmd = (int)(ord($byte)/16);
if($this->debug) echo "Recevid: $cmd\n";
$multiplier = 1;
$value = 0;
do{
$digit = ord($this->read(1));
$value += ($digit & 127) * $multiplier;
$multiplier *= 128;
}while (($digit & 128) != 0);
if($this->debug) echo "Fetching: $value\n";
if($value)
$string = $this->read($value,"fetch");
if($cmd){
switch($cmd){
case 3:
$this->message($string);
break;
}
$this->timesinceping = time();
}
}
if($this->timesinceping < (time() - $this->keepalive )){
if($this->debug) echo "not found something so ping\n";
$this->ping();
}
if($this->timesinceping<(time()-($this->keepalive*2))){
if($this->debug) echo "not seen a package in a while, disconnecting\n";
fclose($this->socket);
$this->connect_auto(false);
if(count($this->topics))
$this->subscribe($this->topics);
}
}
return 1;
}
/* getmsglength: */
function getmsglength(&$msg, &$i){
$multiplier = 1;
$value = 0 ;
do{
$digit = ord($msg{$i});
$value += ($digit & 127) * $multiplier;
$multiplier *= 128;
$i++;
}while (($digit & 128) != 0);
return $value;
}
/* setmsglength: */
function setmsglength($len){
$string = "";
do{
$digit = $len % 128;
$len = $len >> 7;
// if there are more digits to encode, set the top bit of this digit
if ( $len > 0 )
$digit = ($digit | 0x80);
$string .= chr($digit);
}while ( $len > 0 );
return $string;
}
/* strwritestring: writes a string to a buffer */
function strwritestring($str, &$i){
$ret = " ";
$len = strlen($str);
$msb = $len >> 8;
$lsb = $len % 256;
$ret = chr($msb);
$ret .= chr($lsb);
$ret .= $str;
$i += ($len+2);
return $ret;
}
function printstr($string){
$strlen = strlen($string);
for($j=0;$j<$strlen;$j++){
$num = ord($string{$j});
if($num > 31)
$chr = $string{$j}; else $chr = " ";
printf("%4d: %08b : 0x%02x : %s \n",$j,$num,$num,$chr);
}
}
}
?>
以上是mqttphp服务端的代码。
二、思路详解
说了这么多,萌新同学可能有点疑惑,这里为萌新同学们讲解下思路。
百度百科的mqtt有6大特性,最主要也是我们最必须了解的特性就是:使用发布/订阅消息模式,提供一对多的消息模式
这个怎么解释呢?,微信玩过吧,里面不是有很多公众号嘛,假设你是A,你朋友是B,有个公众号C,A和B都订阅了C,此时C在微信后台发布文章,A和B都能接收到。
而我们要实现的IM的思路是:每个人都订阅自己,什么意思呢?就是A订阅A,B订阅B,C订阅C,当A要给B发送消息的时候,A从客户端使用ajax给服务器异步一条数据,例如:
msg = {
to : "B",
con : "你好啊",
from : "A",
time : 2015698856
}
msg里面记录了接受者(to),发送内容(con),发送者(from),发送时间戳(time),当服务端接收到消息后,对消息进行解析,获取到to后对B进行消息发送,例如
include_once("phpMQTT.php");
$clientid = mt_rand(0,10000);
$mqtt = new phpMQTT("127.0.0.1", 1883, $clientid."a"); //127.0.0.1是连接本机,1883端口为服务端mqtt默认代理端口,$clinetid不唯一即可
if ($mqtt->connect(true, NULL,username, password)) { //mqtt服务进行连接,username,password只要跟客户端设置一致即可
$mqtt->publish($to,json_encode($msg), 0); //$to就是解析到的to,此时就是B,$msg就是原信息
$mqtt->close();
$msg = '发送成功';
} else {
$msg = '连接失败';
}
B登陆客户端后,用mqttjs端对自己进行订阅,然后将接受到的信息进行解析:例如
function subscribe(user){ //开启mqtt客户端,并订阅自己,user为自己
var client = new Paho.MQTT.Client("服务端地址/IP或者域名", Number(9001), uuid); //web客户端开启的端口必须是9001,每个客户端的uuid必须要不一样
client.connect({
onSuccess:function(){
console.log("connect success");
client.subscribe(user);//接收订阅的主题
},
onFailure:function() {
},
timeout:100,
userName:'xxxx', //mqtt服务端连接验证,用户名密码只要与服务端设置相同即可
password:'xxxx' //mqtt服务端连接验证,用户名密码只要与服务端设置相同即可
});
client.onMessageArrived = function(message) {
console.log(message); //消息接收到的处理
}
client.onConnectionLost = function(responseObject) {
console.log(responseObject) //连接断开的处理
}
}
一条完整的路线走了下来,一个简易的Im就做出来了,此时还只是文本聊天,如果你想进行表情,图片,语音,地图,请移步我下一篇文章。
严重警告:一个客户端最好只开一个mqtt,如果开多了,就会莫名的崩溃掉,尽量使用逻辑手段去实现功能,而不要呆板的以数量来实现。
三、效果展示:
视频连接地址:
点击打开链接