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);
}
}