<?php error_reporting (E_ALL ^ E_NOTICE); ob_implicit_flush(); //地址与接口,即创建socket时需要服务器的IP和端口 $sk = new Sock( '127.0.0.1' ,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); 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 ); } } ?> |