专业IM即时通讯软件开发,值得信赖!

漫漫的webim(一) web实现简易im功能

即时通讯软件开发 云聊IM 269℃

因为本人的工作需要,偶尔被要求实现一些市面上已经有的成熟接口功能。这里要转折一下,不是说我实现的功能的稳定性和成熟度已经达到了可以商用的标准,只是被用作一个给客户展示的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端代码在这里粘贴出来,需要用的可以自取(因为我也是借鉴别人的)

/*
 	phpMQTT
	A simple php class to connect/publish/subscribe to an MQTT broker
 
*/
 
/*
	Licence
	Copyright (c) 2010 Blue Rhinos Consulting | Andrew Milsted
	andrew@bluerhinos.co.uk | http://www.bluerhinos.co.uk
	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:
	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.
	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
	
*/
 
/* phpMQTT */
class phpMQTT {
 
	private $socket; 			/* holds the socket	*/
	private $msgid = 1;			/* counter for message id */
	public $keepalive = 10;		/* default keepalive timmer */
	public $timesinceping;		/* host unix time, used to detect disconects */
	public $topics = array(); 	/* used to store currently subscribed topics */
	public $debug = false;		/* should output debug messages */
	public $address;			/* broker address */
	public $port;				/* broker port */
	public $clientid;			/* client id sent to brocker */
	public $will;				/* stores the will of the client */
	private $username;			/* stores username */
	private $password;			/* stores password */
 
	function __construct($address, $port, $clientid){
		$this->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,如果开多了,就会莫名的崩溃掉,尽量使用逻辑手段去实现功能,而不要呆板的以数量来实现。

三、效果展示:

喜欢 (0)
聊天软件开发
点击这里给我发消息