侯体宗的博客
  • 首页
  • 人生(杂谈)
  • 技术
  • 关于我
  • 更多分类
    • 文件下载
    • 文字修仙
    • 中国象棋ai
    • 群聊
    • 九宫格抽奖
    • 拼图
    • 消消乐
    • 相册

php中使用websocket详解

php  /  管理员 发布于 7年前   150

在PHP中,开发者需要考虑的东西比较多,从socket的连接、建立、绑定、监听等都需要开发者自己去操作完成,对于初学者来说,难度方面也挺大的,所以本文的思路如下:

1、socket协议的简介

2、介绍client与server之间的连接原理

3、PHP中建立socket的过程讲解

4、用一个聊天室作为实例详细讲解在PHP中如何使用socket

一、socket协议的简介

  WebSocket是什么,有什么优点

  WebSocket是一个持久化的协议,这是相对于http非持久化来说的。

  举个简单的例子,http1.0的生命周期是以request作为界定的,也就是一个request,一个response,对于http来说,本次client与server的会话到此结束;而在http1.1中,稍微有所改进,即添加了keep-alive,也就是在一个http连接中可以进行多个request请求和多个response接受操作。然而在实时通信中,并没有多大的作用,http只能由client发起请求,server才能返回信息,即server不能主动向client推送信息,无法满足实时通信的要求。而WebSocket可以进行持久化连接,即client只需进行一次握手,成功后即可持续进行数据通信,值得关注的是WebSocket实现client与server之间全双工通信,即server端有数据更新时可以主动推送给client端。

二、介绍client与server之间的socket连接原理

1、下面是一个演示client和server之间建立WebSocket连接时握手部分

2、client与server建立socket时握手的会话内容,即request与response

  a、client建立WebSocket时向服务器端请求的信息

  GET /chat HTTP/1.1   Host: server.example.com   Upgrade: websocket //告诉服务器现在发送的是WebSocket协议  Connection: Upgrade   Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== //是一个Base64 encode的值,这个是浏览器随机生成的,用于验证服务器端返回数据是否是WebSocket助理  Sec-WebSocket-Protocol: chat, superchat   Sec-WebSocket-Version: 13   Origin: http://example.com

  b、服务器获取到client请求的信息后,根据WebSocket协议对数据进行处理并返回,其中要对Sec-WebSocket-Key进行加密等操作

  HTTP/1.1 101 Switching Protocols   Upgrade: websocket //依然是固定的,告诉客户端即将升级的是Websocket协议,而不是mozillasocket,lurnarsocket或者shitsocket  Connection: Upgrade   Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= //这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key,也就是client要求建立WebSocket验证的凭证  Sec-WebSocket-Protocol: chat

3、socket建立连接原理图:

三、PHP中建立socket的过程讲解

1、在PHP中,client与server之间建立socket通信,首先在PHP中创建socket并监听端口信息,代码如下:

//传相应的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);  return $server;}

2、设计一个循环挂起WebSocket通道,进行数据的接收、处理和发送

//对创建的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);    foreach($changes as $sock){ //如果有新的client连接进来,则      if($sock==$this->master){         //接受一个socket连接        $client=socket_accept($this->master);         //给新连接进来的socket一个唯一的ID        $key=uniqid();        $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);        }      }    }       }   }

3、以上服务器端完成的WebSocket的前期工作后,就等着client连接进行,client创建WebSocket很简单,代码如下:

var ws = new WebSocket("ws://IP:端口");//握手监听函数ws.onopen=function(){   //状态为1证明握手成功,然后把client自定义的名字发送过去  if(so.readyState==1){     //握手成功后对服务器发送信息   so.send('type=add&ming='+n);  }}//错误返回信息函数ws.onerror = function(){  console.log("error");};//监听服务器端推送的消息ws.onmessage = function (msg){  console.log(msg);} //断开WebSocket连接ws.onclose = function(){  ws = false;}

四、聊天室实例代码

1、PHP部分

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);      foreach($changes as $sock){     //如果有新的client连接进来,则        if($sock==$this->master){           //接受一个socket连接          $client=socket_accept($this->master);           //给新连接进来的socket一个唯一的ID          $key=uniqid();          $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);  }}?>  

2、client部分

HTML5 websocket 网页聊天室 javascript php 
清屏

您可能感兴趣的文章:

  • php使用websocket示例详解
  • Javascript WebSocket使用实例介绍(简明入门教程)
  • Nginx反向代理websocket配置实例
  • Python通过websocket与js客户端通信示例分析
  • 使用Java和WebSocket实现网页聊天室实例代码
  • Java后端Tomcat实现WebSocket实例教程
  • Android中使用WebSocket实现群聊和消息推送功能(不使用WebView)
  • 让ie6也支持websocket采用flash封装实现
  • Spring和Websocket相结合实现消息的推送
  • 完美解决spring websocket自动断开连接再创建引发的问题
  • JavaScript之WebSocket技术详解
  • 浅析nodejs实现Websocket的数据接收与发送
  • 微信小程序 WebSocket详解及应用
  • 使用swoole扩展php websocket示例
  • php+html5基于websocket实现聊天室的方法
  • 详解WebSocket+spring示例demo(已使用sockJs库)
  • C#实现WebSocket协议客户端和服务器websocket sharp组件实例解析
  • HTML5 WebSocket技术使用详解


  • 上一条:
    php使用glob函数遍历文件和目录详解
    下一条:
    PHP用FTP类上传文件视频等的简单实现方法
  • 昵称:

    邮箱:

    0条评论 (评论内容有缓存机制,请悉知!)
    最新最热
    • 分类目录
    • 人生(杂谈)
    • 技术
    • linux
    • Java
    • php
    • 框架(架构)
    • 前端
    • ThinkPHP
    • 数据库
    • 微信(小程序)
    • Laravel
    • Redis
    • Docker
    • Go
    • swoole
    • Windows
    • Python
    • 苹果(mac/ios)
    • 相关文章
    • Laravel从Accel获得5700万美元A轮融资(0个评论)
    • PHP 8.4 Alpha 1现已发布!(0个评论)
    • 用Time Warden监控PHP中的代码处理时间(0个评论)
    • 在PHP中使用array_pop + yield实现读取超大型目录功能示例(0个评论)
    • Property Hooks RFC在PHP 8.4中越来越接近现实(0个评论)
    • 近期文章
    • 在windows10中升级go版本至1.24后LiteIDE的Ctrl+左击无法跳转问题解决方案(0个评论)
    • 智能合约Solidity学习CryptoZombie第四课:僵尸作战系统(0个评论)
    • 智能合约Solidity学习CryptoZombie第三课:组建僵尸军队(高级Solidity理论)(0个评论)
    • 智能合约Solidity学习CryptoZombie第二课:让你的僵尸猎食(0个评论)
    • 智能合约Solidity学习CryptoZombie第一课:生成一只你的僵尸(0个评论)
    • 在go中实现一个常用的先进先出的缓存淘汰算法示例代码(0个评论)
    • 在go+gin中使用"github.com/skip2/go-qrcode"实现url转二维码功能(0个评论)
    • 在go语言中使用api.geonames.org接口实现根据国际邮政编码获取地址信息功能(1个评论)
    • 在go语言中使用github.com/signintech/gopdf实现生成pdf分页文件功能(95个评论)
    • gmail发邮件报错:534 5.7.9 Application-specific password required...解决方案(0个评论)
    • 近期评论
    • 122 在

      学历:一种延缓就业设计,生活需求下的权衡之选中评论 工作几年后,报名考研了,到现在还没认真学习备考,迷茫中。作为一名北漂互联网打工人..
    • 123 在

      Clash for Windows作者删库跑路了,github已404中评论 按理说只要你在国内,所有的流量进出都在监控范围内,不管你怎么隐藏也没用,想搞你分..
    • 原梓番博客 在

      在Laravel框架中使用模型Model分表最简单的方法中评论 好久好久都没看友情链接申请了,今天刚看,已经添加。..
    • 博主 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 @1111老铁这个不行了,可以看看近期评论的其他文章..
    • 1111 在

      佛跳墙vpn软件不会用?上不了网?佛跳墙vpn常见问题以及解决办法中评论 网站不能打开,博主百忙中能否发个APP下载链接,佛跳墙或极光..
    • 2016-10
    • 2016-11
    • 2017-06
    • 2017-07
    • 2017-08
    • 2017-09
    • 2017-11
    • 2017-12
    • 2018-01
    • 2018-02
    • 2018-03
    • 2020-03
    • 2020-04
    • 2020-05
    • 2020-06
    • 2020-07
    • 2020-09
    • 2021-02
    • 2021-03
    • 2021-04
    • 2021-05
    • 2021-06
    • 2021-07
    • 2021-08
    • 2021-09
    • 2021-10
    • 2021-11
    • 2021-12
    • 2022-01
    • 2022-02
    • 2022-05
    • 2022-06
    • 2022-07
    • 2022-08
    • 2022-09
    • 2022-10
    • 2022-11
    • 2022-12
    • 2023-01
    • 2023-02
    • 2023-03
    • 2023-04
    • 2023-05
    • 2023-06
    • 2023-07
    • 2023-08
    • 2023-09
    • 2023-10
    • 2023-11
    • 2023-12
    • 2024-01
    • 2024-02
    • 2024-03
    • 2024-04
    • 2024-05
    • 2024-06
    • 2024-07
    • 2024-09
    Top

    Copyright·© 2019 侯体宗版权所有· 粤ICP备20027696号 PHP交流群

    侯体宗的博客