PHP原生soceket做WebSocket聊天室

PHP原生soceket做WebSocket聊天室

socket相关函数:

socket_accept() 接受一个Socket连接
socket_bind() 把socket绑定在一个IP地址和端口上
socket_clear_error() 清除socket的错误或者最后的错误代码
socket_close() 关闭一个socket资源
socket_connect() 开始一个socket连接
socket_create_listen() 在指定端口打开一个socket监听
socket_create_pair() 产生一对没有区别的socket到一个数组里
socket_create() 产生一个socket,相当于产生一个socket的数据结构
socket_get_option() 获取socket选项
socket_getpeername() 获取远程类似主机的ip地址
socket_getsockname() 获取本地socket的ip地址
socket_iovec_add() 添加一个新的向量到一个分散/聚合的数组
socket_iovec_alloc() 这个函数创建一个能够发送接收读写的iovec数据结构
socket_iovec_delete() 删除一个已经分配的iovec
socket_iovec_fetch() 返回指定的iovec资源的数据
socket_iovec_free() 释放一个iovec资源
socket_iovec_set() 设置iovec的数据新值
socket_last_error() 获取当前socket的最后错误代码
socket_listen() 监听由指定socket的所有连接
socket_read() 读取指定长度的数据			# 仅仅读取数据
socket_readv() 读取从分散/聚合数组过来的数据
socket_recv() 从socket里结束数据到缓存	# 读取数据同时保存在缓存去中, 同时返回内容的字符串
socket_recvfrom() 接受数据从指定的socket,如果没有指定则默认当前socket
socket_recvmsg() 从iovec里接受消息
socket_select() 多路选择
socket_send() 这个函数发送数据到已连接的socket
socket_sendmsg() 发送消息到socket
socket_sendto() 发送消息到指定地址的socket
socket_set_block() 在socket里设置为块模式
socket_set_nonblock() socket里设置为非块模式
socket_set_option() 设置socket选项
socket_shutdown() 这个函数允许你关闭读、写、或者指定的socket
socket_strerror() 返回指定错误号的详细错误
socket_write() 写数据到socket缓存
socket_writev() 写数据到分散/聚合数组

PHPsocket具体流程:

  先从服务器端说起。服务器端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

PHP socket和websocket通信的基本步骤

websocket 详解:
var Socket = new WebSocket(url, [protocol] );
常用属性:
Socket.readyState -> 只读属性: 0 - 表示连接尚未建立。1 - 表示连接已建立,可以进行通信。2 - 表示连接正在进行关闭。3 - 表示连接已经关闭或者连接不能打开。
Socket.bufferedAmount -> 只读属性 bufferedAmount 已被 send() 放入正在队列中等待传输,但是还没有发出的 UTF-8 文本字节数。

常用事件:
Socket.onopen			连接建立时触发
Socket.onmessage		客户端接收服务端数据时触发
Socket.onerror			通信发生错误时触发
Socket.onclose			连接关闭时触发

常用方法:
Socket.send();		使用连接发送信息
Socket.close();		关闭连接

前端实例

//判断浏览器是否兼容websocket
if(!'websocket' in window) console.log('你的浏览器不兼容websocket');
   console.log('你的浏览器兼容websocket');
   //设置参数
   var ip = '192.168.80.131';
   var port = '8000';
   //初始化websocket
   var ws = new WebSocket('ws://'+ ip+ port);
   //判断websocket状态
   if(ws.readyState != 1) {
      //输出错误信息  
      console.log(ws.onerror);
   }
   //发送信息
   ws.send= function(sendMsg){
      console.log(sendMsg);
      console.log('信息已发送');
   };
   //接收信息
   ws.onmessage= function(msg){
      console.log(msg);
      console.log('信息已接受');
   };
   //连接关闭时触发
   ws.onclose = function(){
      console.log('连接已经关闭');
   }

PHP socket 详解:

基本流程:

/* +-------------------------------

  • @socket通信整个过程 +-------------------------------
  • @socket_create 创建socket连接句柄
  • @socket_bind 绑定socket 的IP和端口号
  • @socket_listen 开始监听
  • @socket_select 套接字是否处于可操作状态( 接受三个套接字数组,分别检查数组中的套接字是否处于可以操作的状态(返回时只保留可操作的套接字))
  • @socket_accept 接受一个socket连接
  • @socket_read / socket_recv 读取socket信息 (两者区别: socket_recv效率更高一些)
  • @socket_write 写入信息
  • @socket_close 关闭连接 +-------------------------------- */

后端实例:

<?php
/**
 * Created by PhpStorm.
 * User: Dawn
 * Date: 2016-11-23
 * Time: 10:25
 */
error_reporting(E_ALL ^ E_NOTICE);
ob_implicit_flush(); #开/关闭绝对刷送
date_default_timezone_set('Asia/Shanghai');
//地址与接口,即创建socket时需要服务器的IP和端口
$sk=new Sock('192.168.80.131',8000);

//对创建的socket循环进行监听,处理数据
$sk->run();

//下面是sock类
class Sock{
    public $sockets; //socket的连接池,即client连接进来的socket标志
    public $users;  //所有client连接进来的信息,包括socket、client名字等
    public $master; //socket的resource,即前期初始化socket时返回的socket资源

    private $sda=array();  //已接收的数据
    private $slen=array(); //数据总长度
    private $sjen=array(); //接收数据的长度
    private $ar=array();  //加密key
    private $n=array();

    public function __construct($address, $port){

        //创建socket并把保存socket资源在$this->master
        $this->master=$this->WebSocket($address, $port);

        //创建socket连接池
        $this->sockets=array($this->master);
    }

    //对创建的socket循环进行监听,处理数据
    function run(){
        //死循环,直到socket断开
        while(true){
            $changes=$this->sockets;
            $write=NULL;
            $except=NULL;

            /*
            //这个函数是同时接受多个连接的关键,我的理解它是为了阻塞程序继续往下执行。
            socket_select ($sockets, $write = NULL, $except = NULL, NULL);

            $sockets可以理解为一个数组,这个数组中存放的是文件描述符。当它有变化(就是有新消息到或者有客户端连接/断开)时,socket_select函数才会返回,继续往下执行。
            $write是监听是否有客户端写数据,传入NULL是不关心是否有写变化。
            $except是$sockets里面要被排除的元素,传入NULL是”监听”全部。
            最后一个参数是超时时间
            如果为0:则立即结束
            如果为n>1: 则最多在n秒后结束,如遇某一个连接有新动态,则提前返回
            如果为null:如遇某一个连接有新动态,则返回
            */
            socket_select($changes,$write,$except,NULL);
           /**
            * 我对socket_slect的理解  20170328
            * socket_select(array $read, array $write, array $except, int $tv_sec);
            * $read    套接字读取数组中,列出将看是否可用字符阅读(更确切的说是,是否不会阻塞,特别是一个套接字资源文件尾也准备好了,在这种情况下, socet_read()将返回一个零长度的字符串)
            * $write   列出的套接字写数组,写是否会被阻塞.
            * $except  排除的部分
            * $tv_sec  超时参数, 超时是一个上限的时间流逝之前socket_select()返回.   tv_sec可能是零,导致socket_select()立即返回; 如果tv_sec为空(没有超时),socket_select无限期()可以阻止。
            * 
            * 百度到的理解:
            *   socket_select 接受三个套接字数组,分别检查数组中的套接字是否处于可以操作的状态(返回时只保留可操作的套接字)
            *   使用最多的是 $read,因此以读为例
            *   在套接字数组 $read 中最初应保有一个服务端监听套接字
            *   每当该套接字可读时,就表示有一个用户发起了连接。此时你需要对该连接创建一个套接字,并加入到 $read 数组中
            *   当然,并不只是服务端监听的套接字会变成可读的,用户套接字也会变成可读的,此时你就可以读取用户发来的数据了
            *   socket_select 只在套接字数组发生了变化时才返回。也就是说,一旦执行到 socket_select 的下一条语句,则必有一个套接字是需要你操作的
            */
            foreach($changes as $sock){

                //如果有新的client连接进来,则
                if($sock==$this->master){

                    //接受一个socket连接
                    $client=socket_accept($this->master);

                    //给新连接进来的socket一个唯一的ID
                    $key=uniqid();
                    echo $key;
                    $this->sockets[]=$client; //将新连接进来的socket存进连接池
                    $this->users[$key]=array(
                        'socket'=>$client, //记录新连接进来client的socket信息
                        'shou'=>false    //标志该socket资源没有完成握手
                    );
                    //否则1.为client断开socket连接,2.client发送信息
                }else{
                    $len=0;
                    $buffer='';
                    //读取该socket的信息,注意:第二个参数是引用传参即接收数据,第三个参数是接收数据的长度
                    do{
                        $l=socket_recv($sock,$buf,1000,0);
                        $len+=$l;
                        $buffer.=$buf;
                    }while($l==1000);

                    //根据socket在user池里面查找相应的$k,即健ID
                    $k=$this->search($sock);

                    //如果接收的信息长度小于7,则该client的socket为断开连接
                    if($len<7){
                        //给该client的socket进行断开操作,并在$this->sockets和$this->users里面进行删除
                        $this->send2($k);
                        continue;
                    }
                    //判断该socket是否已经握手
                    if(!$this->users[$k]['shou']){
                        //如果没有握手,则进行握手处理
                        $this->woshou($k,$buffer);
                    }else{
                        //走到这里就是该client发送信息了,对接受到的信息进行uncode处理
                        $buffer = $this->uncode($buffer,$k);
                        if($buffer==false){
                            continue;
                        }
                        //如果不为空,则进行消息推送操作
                        $this->send($k,$buffer);
                    }
                }
            }

        }

    }

    //指定关闭$k对应的socket
    function close($k){
        //断开相应socket
        socket_close($this->users[$k]['socket']);
        //删除相应的user信息
        unset($this->users[$k]);
        //重新定义sockets连接池
        $this->sockets=array($this->master);
        foreach($this->users as $v){
            $this->sockets[]=$v['socket'];
        }
        //输出日志
        $this->e("key:$k close");
    }

    //根据sock在users里面查找相应的$k
    function search($sock){
        foreach ($this->users as $k=>$v){
            if($sock==$v['socket'])
                return $k;
        }
        return false;
    }

    //传相应的IP与端口进行创建socket操作
    function WebSocket($address,$port){
        $server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        socket_set_option($server, SOL_SOCKET, SO_REUSEADDR, 1);//1表示接受所有的数据包
        socket_bind($server, $address, $port);
        socket_listen($server);
        $this->e('Server Started : '.date('Y-m-d H:i:s'));
        $this->e('Listening on  : '.$address.' port '.$port);
        return $server;
    }


    /*
    * 方法说明:对client的请求进行回应,即握手操作
    * @$k clien的socket对应的健,即每个用户有唯一$k并对应socket
    * @$buffer 接收client请求的所有信息
    */
    function woshou($k,$buffer){

        //截取Sec-WebSocket-Key的值并加密,其中$key后面的一部分258EAFA5-E914-47DA-95CA-C5AB0DC85B11字符串应该是固定的
        $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));

        //对已经握手的client做标志
        $this->users[$k]['shou']=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->slen[$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->slen[$key] > 0){
            $len=$this->slen[$key];
            $mask=$this->ar[$key];
            $n=$this->n[$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->sjen[$key])){
            $this->ar[$key]=$mask;
            $this->slen[$key]=$len;
            $this->sjen[$key]=$dlen+intval($this->sjen[$key]);
            $this->sda[$key]=$this->sda[$key].$data;
            $this->n[$key]=$n;
            return false;
        }else{
            unset($this->ar[$key],$this->slen[$key],$this->sjen[$key],$this->n[$key]);
            $data=$this->sda[$key].$data;
            unset($this->sda[$key]);
            return $data;
        }

    }

    //与uncode相对
    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;
    }

    //用户加入或client发送信息
    function send($k,$msg){
        //将查询字符串解析到第二个参数变量中,以数组的形式保存如:parse_str("name=Bill&age=60",$arr)
        parse_str($msg,$g);
        $ar=array();

        if($g['type']=='add'){
            //第一次进入添加聊天名字,把姓名保存在相应的users里面
            $this->users[$k]['name']=$g['ming'];
            $ar['type']='add';
            $ar['name']=$g['ming'];
            $key='all';
        }else{
            //发送信息行为,其中$g['key']表示面对大家还是个人,是前段传过来的信息
            $ar['nrong']=$g['nr'];
            $key=$g['key'];
        }
        //推送信息
        $this->send1($k,$ar,$key);
    }

    //对新加入的client推送已经在线的client
    function getusers(){
        $ar=array();
        foreach($this->users as $k=>$v){
            $ar[]=array('code'=>$k,'name'=>$v['name']);
        }
        return $ar;
    }

    //$k 发信息人的socketID $key接受人的 socketID ,根据这个socketID可以查找相应的client进行消息推送,即指定client进行发送
    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;
            //如果是add表示新加的client
            if($ar['type']=='add'){
                $ar['type']='madd';
                $ar['users']=$this->getusers();    //取出所有在线者,用于显示在在线用户列表中
                $str1 = $this->code(json_encode($ar)); //单独对新client进行编码处理,数据不一样
                //对新client自己单独发送,因为有些数据是不一样的
                socket_write($users[$k]['socket'],$str1,strlen($str1));
                //上面已经对client自己单独发送的,后面就无需再次发送,故unset
                unset($users[$k]);
            }
            //除了新client外,对其他client进行发送信息。数据量大时,就要考虑延时等问题了
            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));
        }
    }

    //用户退出向所用client推送信息
    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);
    }
}

转载于:https://my.oschina.net/chinaliuhan/blog/3063751

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值