Android WebView实现WebSocket即时通讯

参考博客

Android HTML5多线程&本地缓存&文件上传

Websocket协议的学习、调研和实现

Android版本要求及H5 api支持情况

在Android版本4.4之前,由于维护和开发Android版本时使用的是AppleWebkit开源内核,虽然也非常不错,但不支持许多html5 api,在Android4.4使用了Chromium才得以发展,

目前支持的html 5如下:

Web Workers 支持 javaScript多线程
WebSocket 支持 javascript套接字,TCP长链接
IDBFactory/indexDB 支持 索引数据库
ApplicationCache 支持 web离线缓存
postMessage/onMessage 支持 收发消息
ondeviceorientation,ondevicemotion,onorientationchange 支持 屏幕旋转,移动
onvolumechange 支持 声音改变
RequestAnimationFrame 支持 页面UI动画更新引擎
LocalStorage/sessionStorage 支持 本地缓存
FileReader 支持 本地文件读取
FormData 支持 模拟表单,表单模型
EventSource 支持 Server-Sent Events(SSE)功能,允许服务端推送数据到客户端。(通常叫数据推送)
CacheStorage 不支持 异步缓存
Promise 不支持 异步范式
Crypto 不支持 javascript加密API
WebAudio 不支持 流媒体播放
WebRTC 不支持 流媒体通讯
WebGL 不支持 Web GL图像框架
GeoLocation 支持 地理定位
Notification 不支持 web通知
Blob 支持 二进制数据对象

在这里我们主要了解WebSocket在Android WebView上的支持,这里给出一个基于php WebSocketServer的例子:

【以下例子来自开源中国博客】

体验位置: http://www.yxsss.com/ui/sk.html

请使用你的PC浏览器和你的Android4.4的设备上的浏览器

WebSocket Server端实现

php端

<?php
error_reporting(E_ALL ^ E_NOTICE);
ob_implicit_flush();
 
class Sock{
    public $sockets;
    public $users;
    public $master;
     
    private $rec_data_packets=array();//已接收的数据
    private $sum_length=array();//数据总长度
    private $rec_data_length=array();//接收数据的长度
    private $salt_key=array();//加密key
    private $per_data_len=array(); //每组数据长度
     
    public function __construct($address, $port,$maxLink=16){
        $this->master=$this->createSocketServer($address, $port,$maxLink);
        $this->sockets=array($this->master);
    }
     
    function run()
    {
        while(true){
            $changes=$this->sockets;
            $write=NULL;
            $except=NULL;
            
            socket_select($changes,$write,$except,NULL); 
            /**
            *  监听读写操作,socket_select (array &$read, array &$write, array &$except, $tv_sec, $tv_usec = null)
            *  $read 注意,这里使用了引用,$read表示要监听读操作的socket,socket_select可以调用多次,之前监听的socket不会被取消(注意,socket连接也是read)
            *  $write 注意,这里使用了引用,$read表示要监听写操作的socket,socket_select可以调用多次,之前监听的socket不会被取消
            *  $$except 监听异常的socket
            *  以上&read,&write,&except是被引用的,也就意味着变量的值可以输入输出,把监听到的socket输出,把想被监听的socket输入
            *  最后一个参数是超时时间,默认是0
            *  
            *  该函数会阻塞
            */
            foreach($changes as $sock){
                if($sock==$this->master){
                	/**
                	 * 接受客户端socket
                	 * @var unknown
                	 */
                    $client=socket_accept($this->master);
                    $key=uniqid();
                    $this->sockets[]=$client;
                    $this->users[$key]=array(
                        'socket'=>$client,
                        'handshake'=>false
                    );
                }else{
                    $len=0;
                    $buffer='';
                    do{
                        $l=socket_recv($sock,$buf,1000,0);
                        $len+=$l;
                        $buffer.=$buf;
                    }while($l==1000);
                    $k=$this->search($sock);
                    if($len<7){
                        $this->send2($k);
                        continue;
                    }
                    if(!$this->users[$k]['handshake']){
                        $this->handshake($k,$buffer);
                    }else{
                        $buffer = $this->uncode($buffer,$k);
                        if($buffer==false){
                            continue;
                        }
                        $this->send($k,$buffer);
                    }
                }
            }
             
        }
         
    }
     
    function close($k){
        socket_close($this->users[$k]['socket']);
        unset($this->users[$k]);
        $this->sockets=array($this->master);
        foreach($this->users as $v){
            $this->sockets[]=$v['socket'];
        }
        $this->e("key:$k close");
    }
     
    function search($sock){
        foreach ($this->users as $k=>$v){
            if($sock==$v['socket'])
            return $k;
        }
        return false;
    }
     
    /**
     * 
     * @param string $address 地址
     * @param int $port 端口
     * @param number $maxLink 最大连接数
     */
    function createSocketServer($address,$port,$maxLink=16){ 
    	
        $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);
        socket_bind($server, $address, $port);
        socket_listen($server,$maxLink);
        $this->e('Server Started : '.date('Y-m-d H:i:s'));
        $this->e('Listening on   : '.$address.' port '.$port);
        return $server;
    }
     
     /**
      * 握手
      * @param string $k 要握手的socket
      * @param byte $buffer 缓冲数据
      * @return boolean
      */
    function handshake($k,$buffer){
        $buf  = substr($buffer,strpos($buffer,'Sec-WebSocket-Key:')+18);
        $key  = trim(substr($buf,0,strpos($buf,"\r\n")));
     
        $new_key = base64_encode(sha1($key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
         
        $new_message = "HTTP/1.1 101 Switching Protocols\r\n";
        $new_message .= "Upgrade: websocket\r\n";
        $new_message .= "Sec-WebSocket-Version: 13\r\n";
        $new_message .= "Connection: Upgrade\r\n";
        $new_message .= "Sec-WebSocket-Accept: " . $new_key . "\r\n\r\n";
         
        socket_write($this->users[$k]['socket'],$new_message,strlen($new_message));
        $this->users[$k]['handshake']=true;
        return true;
         
    }
     
    function uncode($str,$key){
        $mask = array();  
        $data = '';  
        $msg = unpack('H*',$str);
        $head = substr($msg[1],0,2);  
        if ($head == '81' && !isset($this->sum_length[$key])) {  
            $len=substr($msg[1],2,2);
            $len=hexdec($len);
            if(substr($msg[1],2,2)=='fe'){
                $len=substr($msg[1],4,4);
                $len=hexdec($len);
                $msg[1]=substr($msg[1],4);
            }else if(substr($msg[1],2,2)=='ff'){
                $len=substr($msg[1],4,16);
                $len=hexdec($len);
                $msg[1]=substr($msg[1],16);
            }
            $mask[] = hexdec(substr($msg[1],4,2));  
            $mask[] = hexdec(substr($msg[1],6,2));  
            $mask[] = hexdec(substr($msg[1],8,2));  
            $mask[] = hexdec(substr($msg[1],10,2));
            $s = 12;
            $n=0;
        }else if($this->sum_length[$key] > 0){
            $len=$this->sum_length[$key];
            $mask=$this->salt_key[$key];
            $n=$this->per_data_len[$key];
            $s = 0;
        }
         
        $e = strlen($msg[1])-2;
        for ($i=$s; $i<= $e; $i+= 2) {  
            $data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2)));  
            $n++;  
        }  
        $dlen=strlen($data);
         
        if($len > 255 && $len > $dlen+intval($this->rec_data_length[$key])){
            $this->salt_key[$key]=$mask;
            $this->sum_length[$key]=$len;
            $this->rec_data_length[$key]=$dlen+intval($this->rec_data_length[$key]);
            $this->rec_data_packets[$key]=$this->rec_data_packets[$key].$data;
            $this->per_data_len[$key]=$n;
            return false;
        }else{
            unset($this->salt_key[$key],$this->sum_length[$key],$this->rec_data_length[$key],$this->per_data_len[$key]);
            $data=$this->rec_data_packets[$key].$data;
            unset($this->rec_data_packets[$key]);
            return $data;
        }
         
    }
     
    function code($msg){
        $frame = array();  
        $frame[0] = '81';  
        $len = strlen($msg);
        if($len < 126){
            $frame[1] = $len<16?'0'.dechex($len):dechex($len);
        }else if($len < 65025){
            $s=dechex($len);
            $frame[1]='7e'.str_repeat('0',4-strlen($s)).$s;
        }else{
            $s=dechex($len);
            $frame[1]='7f'.str_repeat('0',16-strlen($s)).$s;
        }
        $frame[2] = $this->ord_hex($msg);  
        $data = implode('',$frame);  
        return pack("H*", $data);  
    }
     
    function ord_hex($data)  {  
        $msg = '';  
        $l = strlen($data);  
        for ($i= 0; $i<$l; $i++) {  
            $msg .= dechex(ord($data{$i}));  
        }  
        return $msg;  
    }
     
    //用户加入
    function send($k,$msg){
        parse_str($msg,$g);
        $ar=array();
        if($g['type']=='add'){
            $this->users[$k]['name']=$g['ming'];
            $ar['type']='add';
            $ar['name']=$g['ming'];
            $key='all';
        }else{
            $ar['nrong']=$g['nr'];
            $key=$g['key'];
        }
        $this->send1($k,$ar,$key);
    }
     
    function getusers(){
        $ar=array();
        foreach($this->users as $k=>$v){
            $ar[]=array('code'=>$k,'name'=>$v['name']);
        }
        return $ar;
    }
     
    /**
     * @param string $k	发信息人的code 
     * @param string $ar 数据
     * @param string $key 接受人的 code
     */
    function send1($k,$ar,$key='all'){
        $ar['code1']=$key;
        $ar['code']=$k;
        $ar['time']=date('m-d H:i:s');
        $str = $this->code(json_encode($ar));
        if($key=='all'){
            $users=$this->users;
            if($ar['type']=='add'){
                $ar['type']='madd';
                $ar['users']=$this->getusers();
                $str1 = $this->code(json_encode($ar));
                socket_write($users[$k]['socket'],$str1,strlen($str1));
                unset($users[$k]);
            }
            foreach($users as $v){
                socket_write($v['socket'],$str,strlen($str));
            }
        }else{
            socket_write($this->users[$k]['socket'],$str,strlen($str));
            socket_write($this->users[$key]['socket'],$str,strlen($str));
        }
    }
     
    //用户退出
    function send2($k){
        $this->close($k);
        $ar['type']='rmove';
        $ar['nrong']=$k;
        $this->send1(false,$ar,'all');
    }
     
    function e($str){
        //$path=dirname(__FILE__).'/log.txt';
        $str=$str."\n";
        //error_log($str,3,$path);
        echo iconv('utf-8','gbk//IGNORE',$str);
    }
}

$sk=new Sock('127.0.0.1',8000);
$sk->run();
?>

WebSocket Client端实现

client端

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"/>
<title>HTML5 websocket 网页聊天室 javascript php</title>
<style type="text/css">
body,p{margin:0px; padding:0px; font-size:14px; color:#333; font-family:Arial, Helvetica, sans-serif;}
#ltian,.rin{width:98%; margin:5px auto;}
#ltian{border:1px #ccc solid;overflow-y:auto; overflow-x:hidden; position:relative;}
#ct{margin-right:111px; height:100%;overflow-y:auto;overflow-x: hidden;}
#us{width:110px; overflow-y:auto; overflow-x:hidden; float:right; border-left:1px #ccc solid; height:100%; background-color:#F1F1F1;}
#us p{padding:3px 5px; color:#08C; line-height:20px; height:20px; cursor:pointer; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;}
#us p:hover,#us p:active,#us p.ck{background-color:#069; color:#FFF;}
#us p.my:hover,#us p.my:active,#us p.my{color:#333;background-color:transparent;}
button{float:right; width:80px; height:35px; font-size:18px;}
input{width:100%; height:30px; padding:2px; line-height:20px; outline:none; border:solid 1px #CCC;}
.rin p{margin-right:160px;}
.rin span{float:right; padding:6px 5px 0px 5px; position:relative;}
.rin span img{margin:0px 3px; cursor:pointer;}
.rin span form{position:absolute; width:25px; height:25px; overflow:hidden; opacity:0; top:5px; right:5px;}
.rin span input{width:180px; height:25px; margin-left:-160px; cursor:pointer}

#ct p{padding:5px; line-height:20px;}
#ct a{color:#069; cursor:pointer;}
#ct span{color:#999; margin-right:10px;}
.c2{color:#999;}
.c3{background-color:#DBE9EC; padding:5px;}
.qp{position:absolute; font-size:12px; color:#666; top:5px; right:130px; text-decoration:none; color:#069;}
#ems{position:absolute; z-index:5; display:none; top:0px; left:0px; max-width:230px; background-color:#F1F1F1; border:solid 1px #CCC; padding:5px;}
#ems img{width:44px; height:44px; border:solid 1px #FFF; cursor:pointer;}
#ems img:hover,#ems img:active{border-color:#A4B7E3;}
#ems a{color:#069; border-radius:2px; display:inline-block; margin:2px 5px; padding:1px 8px; text-decoration:none; background-color:#D5DFFD;}
#ems a:hover,#ems a:active,#ems a.ck{color:#FFF; background-color:#069;}
.tc{text-align:center; margin-top:5px;}
</style>
</head>

<body>
<div id="ltian">
	<div id="us" class="jb"></div>
	<div id="ct"></div>
    <a href="javascript:;" class="qp" onClick="this.parentNode.children[1].innerHTML=''">清屏</a>
</div>
<div class="rin">
    <button id="sd">发送</button>
    <span><img src="http://www.yxsss.com/ui/sk/t.png" title="表情" id="imgbq"><img src="http://www.yxsss.com/ui/sk/e.png" title="上传图片"><form><input type="file" title="上传图片" id="upimg"></form></span>
    <p><input id="nrong"></p>
</div>
<div id="ems"><p></p><p class="tc"></p></div>
<script>
if(typeof(WebSocket)=='undefined'){
	alert('你的浏览器不支持 WebSocket ,推荐使用Google Chrome 或者 Mozilla Firefox');	
}
</script>
<script src="http://www.yxsss.com/ui/p/a.js" type="text/javascript"></script>
<script>
(function(){
	var key='all',mkey;
	var users={};
	var url='ws://127.0.0.1:8000';
	var so=false,n=false;
	var lus=A.$('us'),lct=A.$('ct');
	function st(){
		n=prompt('请给自己取一个响亮的名字:');
		n=n.substr(0,16);
		if(!n){
			return ;	
		}
		so=new WebSocket(url);
		so.onopen=function(){
			if(so.readyState==1){
				so.send('type=add&ming='+n);
			}
		}
		
		so.onclose=function(){
			so=false;
			lct.appendChild(A.$$('<p class="c2">退出聊天室</p>'));
		}
		
		so.onmessage=function(msg){
			eval('var da='+msg.data);
			var obj=false,c=false;
			if(da.type=='add'){
				var obj=A.$$('<p>'+da.name+'</p>');
				lus.appendChild(obj);
				cuser(obj,da.code);
				obj=A.$$('<p><span>['+da.time+']</span>欢迎<a>'+da.name+'</a>加入</p>');
				c=da.code;
			}else if(da.type=='madd'){
				mkey=da.code;
				da.users.unshift({'code':'all','name':'大家'});
				for(var i=0;i<da.users.length;i++){
					var obj=A.$$('<p>'+da.users[i].name+'</p>');
					lus.appendChild(obj);
					if(mkey!=da.users[i].code){
						cuser(obj,da.users[i].code);
					}else{
						obj.className='my';
						document.title=da.users[i].name;
					}
				}
				obj=A.$$('<p><span>['+da.time+']</span>欢迎'+da.name+'加入</p>');
				users.all.className='ck';
			}
			
			if(obj==false){
				if(da.type=='rmove'){
					var obj=A.$$('<p class="c2"><span>['+da.time+']</span>'+users[da.nrong].innerHTML+'退出聊天室</p>');
					lct.appendChild(obj);
					users[da.nrong].del();
					delete users[da.nrong];
				}else{
					da.nrong=da.nrong.replace(/{\\(\d+)}/g,function(a,b){
						return '<img src="sk/'+b+'.gif">';
					}).replace(/^data\:image\/png;base64\,.{50,}$/i,function(a){
						return '<img src="'+a+'">';
					});
					//da.code 发信息人的code
					if(da.code1==mkey){
						obj=A.$$('<p class="c3"><span>['+da.time+']</span><a>'+users[da.code].innerHTML+'</a>对我说:'+da.nrong+'</p>');
						c=da.code;
					}else if(da.code==mkey){
						if(da.code1!='all')
						obj=A.$$('<p class="c3"><span>['+da.time+']</span>我对<a>'+users[da.code1].innerHTML+'</a>说:'+da.nrong+'</p>');
						else
						obj=A.$$('<p><span>['+da.time+']</span>我对<a>'+users[da.code1].innerHTML+'</a>说:'+da.nrong+'</p>');
						c=da.code1;
					}else if(da.code==false){
						obj=A.$$('<p><span>['+da.time+']</span>'+da.nrong+'</p>');
					}else if(da.code1){
						obj=A.$$('<p><span>['+da.time+']</span><a>'+users[da.code].innerHTML+'</a>对'+users[da.code1].innerHTML+'说:'+da.nrong+'</p>');
						c=da.code;
					}
				}
			}
			if(c){
					obj.children[1].onclick=function(){
						users[c].onclick();
					}
				}
			lct.appendChild(obj);
			lct.scrollTop=Math.max(0,lct.scrollHeight-lct.offsetHeight);
		}
	}
	A.$('sd').onclick=function(){
		if(!so){
			 return st();
		}
		var da=A.$('nrong').value.trim();
		if(da==''){
			alert('内容不能为空');
			return false;	
		}
		A.$('nrong').value='';
		so.send('nr='+esc(da)+'&key='+key);
	}
	A.$('nrong').onkeydown=function(e){
		var e=e||event;
		if(e.keyCode==13){
			A.$('sd').onclick();
		}
	}
	function esc(da){
		da=da.replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');
		return encodeURIComponent(da);
	}
	function cuser(t,code){
		users[code]=t;
		t.onclick=function(){
			t.parentNode.children.rcss('ck','');
			t.rcss('','ck');
			key=code;
		}
	}
	A.$('ltian').style.height=(document.documentElement.clientHeight - 70)+'px';
	st();
	

	var bq=A.$('imgbq'),ems=A.$('ems');
	var l=80,r=4,c=5,s=0,p=Math.ceil(l/(r*c));
	var pt='sk/';
	bq.onclick=function(e){
		var e=e||event;
		if(!so){
			 return st();
		}
		ems.style.display='block';
		document.onclick=function(){
			gb();	
		}
		ct();
		try{e.stopPropagation();}catch(o){}
	}
	
	for(var i=0;i<p;i++){
		var a=A.$$('<a href="javascript:;">'+(i+1)+'</a>');
		ems.children[1].appendChild(a);
		ef(a,i);
	}
	ems.children[1].children[0].className='ck';
	
	function ct(){
		var wz=bq.weiz();
		with(ems.style){
			top=wz.y-242+'px';
			left=wz.x+bq.offsetWidth-235+'px';
		}
	}
		
	function ef(t,i){
		t.onclick=function(e){
			var e=e||event;
			s=i*r*c;
			ems.children[0].innerHTML='';
			hh();
			this.parentNode.children.rcss('ck','');
			this.rcss('','ck');
			try{e.stopPropagation();}catch(o){}
		}
	}
	
	function hh(){
		var z=Math.min(l,s+r*c);
		for(var i=s;i<z;i++){
			var a=A.$$('<img src="'+pt+i+'.gif">');
			hh1(a,i);
			ems.children[0].appendChild(a);
		}
		ct();
	}
	
	function hh1(t,i){
		t.onclick=function(e){
			var e=e||event;
			A.$('nrong').value+='{\\'+i+'}';
			if(!e.ctrlKey){
				gb();
			}
			try{e.stopPropagation();}catch(o){}
		}
	}
	
	function gb(){
		ems.style.display='';
		A.$('nrong').focus();
		document.onclick='';
	}
	hh();
	A.on(window,'resize',function(){
		A.$('ltian').style.height=(document.documentElement.clientHeight - 70)+'px';
		ct();
	})	

	var fimg=A.$('upimg');
	var img=new Image();
	var dw=400,dh=300;
	A.on(fimg,'change',function(ev){
		if(!so){
			st();
			return false;
		}
		if(key=='all'){
			alert('由于资源限制 发图只能私聊');
			return false;	
		}
		var f=ev.target.files[0];
		if(f.type.match('image.*')){
			var r = new FileReader();
			r.onload = function(e){
				img.setAttribute('src',e.target.result);
		    };
			r.readAsDataURL(f);
		}
	});
	img.onload=function(){
		ih=img.height,iw=img.width;
		if(iw/ih > dw/dh && iw > dw){
			ih=ih/iw*dw;
			iw=dw;
		}else if(ih > dh){
			iw=iw/ih*dh;
			ih=dh;
		}
		var rc = A.$$('canvas');
		var ct = rc.getContext('2d');
		rc.width=iw;
		rc.height=ih;
		ct.drawImage(img,0,0,iw,ih);
		var da=rc.toDataURL();
		so.send('nr='+esc(da)+'&key='+key);
	}
	
})();
</script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值