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

        因为本人的工作需要,偶尔被要求实现一些市面上已经有的成熟接口功能。这里要转折一下,不是说我实现的功能的稳定性和成熟度已经达到了可以商用的标准,只是被用作一个给客户展示的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,如果开多了,就会莫名的崩溃掉,尽量使用逻辑手段去实现功能,而不要呆板的以数量来实现。

        三、效果展示:

        视频连接地址:

        点击打开链接

        漫漫的webim(一) web实现简易im功能_第1张图片

        


 
  

你可能感兴趣的:(漫漫的webim)