当前位置: 首页 > 工具软件 > PHP WebSocket > 使用案例 >

php WebSocket简单实现

贺宝
2023-12-01

1,核心概念:

             websocket协议是http的升级版。

2,一次握手

           websocket在创建连接时会先向服务端发送一个普通的http请求,大致消息为:

GET / HTTP/1.1 
Host: localhost:1234 
User-Agent: Mozilla/5.0 (WindowsNT 10.0; WOW64; rv:59.0) Gecko/20100101 Firefox/59.0 
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8 
Accept-Language:zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 
Accept-Encoding: gzip, deflate 
Sec-WebSocket-Version: 13 
Origin:http://xxx.xxx.xxx 
Sec-WebSocket-Extensions: permessage-deflate 
Sec-WebSocket-Key: 3hfEc+Te7n7FSrLBsN59ig== 
Connection: keep-alive,Upgrade 
Pragma: no-cache 
Cache-Control: no-cache 
Upgrade: websocket

相比较普通的http请求多了

Sec-WebSocket-Version: 13 
Sec-WebSocket-Extensions: permessage-deflate 
Sec-WebSocket-Key: 3hfEc+Te7n7FSrLBsN59ig== 
Connection: keep-alive,Upgrade 
Upgrade: websocket

服务端通过

Upgrade: websocket

判断是否为websocket请求通过生成新的key,并拼接成新的响应消息

$new_key = base64_encode(sha1($head['Sec-WebSocket-Key']."258EAFA5-E914-47DA-95CA-C5AB0DC85B11",true));
HTTP/1.1 101 Switching Protocols 
Upgrade: websocket 
Sec-WebSocket-Version: 13 
Connection: Upgrade 
Sec-WebSocket-Accept:new-key

完成一次握手双方建立通信通道

完整的服务案例<转载的>

class WebSocketServer{
    private $sockets;//所有socket连接池包括服务端socket
    private $users;//所有连接用户
    private $server;//服务端 socket

    public function __construct($ip,$port){
        $this->server=socket_create(AF_INET,SOCK_STREAM,0);
        $this->sockets=array($this->server);
        $this->users=array();
        socket_bind($this->server,$ip,$port);
        socket_listen($this->server,3);
        echo "[*]Listening:".$ip.":".$port."\n";
    }

    public function run(){
        $write=NULL;
        $except=NULL;
        while (true){
            $active_sockets=$this->sockets;
            socket_select($active_sockets,$write,$except,NULL);
            //这个函数很重要
            foreach ($active_sockets as $socket){
                if ($socket==$this->server){
                    //服务端 socket可读说明有新用户连接
                    $user=socket_accept($this->server);
                    $key=uniqid();
                    $this->sockets[]=$user;
                    $this->users[$key]=array(
                        'socket'=>$user,
                        'handshake'=>false //是否完成websocket握手
                    );
                }else{
                    //用户socket可读
                    $buffer='';
                    $bytes=socket_recv($socket,$buffer,1024,0);
                    $key=$this->find_user_by_socket($socket); //通过socket在users数组中找出user
                    if ($bytes==0){
                        //没有数据 关闭连接
                        $this->disconnect($socket);
                    }else{
                        //没有握手就先握手
                        if (!$this->users[$key]['handshake']){
                            $this->handshake($key,$buffer);
                        }else{
                            //握手后 
                            //解析消息 websocket协议有自己的消息格式
                            //解码 编码过程固定的
                            $msg=$this->msg_decode($buffer);
                            echo $msg;
                            //编码后发送回去
                            $res_msg=$this->msg_encode($msg);
                            socket_write($socket,$res_msg,strlen($res_msg));

                        }
                    }
                }
            }
        }
    }

    //解除连接
    private function disconnect($socket){
        $key=$this->find_user_by_socket($socket);
        unset($this->users[$key]);
        foreach ($this->sockets as $k=>$v){
            if ($v==$socket)
                unset($this->sockets[$k]);
        }
        socket_shutdown($socket);
        socket_close($socket);
    }

    //通过socket在users数组中找出user
    private function find_user_by_socket($socket){
        foreach ($this->users as $key=>$user){
            if ($user['socket']==$socket){
                return $key;
            }
        }
        return -1;
    }

    private function handshake($k,$buffer){
        //截取Sec-WebSocket-Key的值并加密
        $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]['handshake']=true;
        return true;
    }


    //编码 把消息打包成websocket协议支持的格式
    private function msg_encode( $buffer ){
        $len = strlen($buffer);
        if ($len <= 125) {
            return "\x81" . chr($len) . $buffer;
        } else if ($len <= 65535) {
            return "\x81" . chr(126) . pack("n", $len) . $buffer;
        } else {
            return "\x81" . char(127) . pack("xxxxN", $len) . $buffer;
        }
    }

    //解码 解析websocket数据帧
    private function msg_decode( $buffer )
    {
        $len = $masks = $data = $decoded = null;
        $len = ord($buffer[1]) & 127;
        if ($len === 126) {
            $masks = substr($buffer, 4, 4);
            $data = substr($buffer, 8);
        }
        else if ($len === 127) {
            $masks = substr($buffer, 10, 4);
            $data = substr($buffer, 14);
        }
        else {
            $masks = substr($buffer, 2, 4);
            $data = substr($buffer, 6);
        }
        for ($index = 0; $index < strlen($data); $index++) {
            $decoded .= $data[$index] ^ $masks[$index % 4];
        }
        return $decoded;
    }
}

$ws=new WebSocketServer('127.0.0.1',1234);
$ws->run();

客户端

<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script type="text/javascript">
    var ws,wsmonitor,uid;
    var wsurl = 'ws://127.0.0.1:1234';
    $(function(){
        ws = new WebSocket(wsurl);
        ws.onopen = function() {
            console.log('websocketed');
            var type = 0; //位置这里是一个方法,我给干掉了,写成0了,请根据项目自己来搞吧
            var userId = 1;
            var data ={articleId:0, type:type};
            ws.send(JSON.stringify(data)); //这里是向服务器发送一个JSON的数据 处理请看ArticleMonitor.php的process函数

            console.log('send data ok');
        };
        ws.onmessage = function(res){
            console.log(res.data)
        };
        ws.onclose = function(e){
            clearInterval(wsmonitor);
            console.log('websocket closed');
        }
        ws.onerror = function(e){
            clearInterval(wsmonitor);
            console.log('websocket error');
        }
    })


</script>

3,常用的函数

关键函数1
    socket_create($net参数1,$stream参数2,$protocol参数3)
    作用:创建一个socket套接字,说白了,就是一个网络数据流。
    返回值:一个套接字,或者是false,参数错误发出E_WARNING警告
    参数1是:网络协议,
        网络协议有哪些?它的选择项就下面这三个:
        AF_INET:     IPv4 网络协议。TCP 和 UDP 都可使用此协议。一般都用这个,你懂的。
        AF_INET6:    IPv6 网络协议。TCP 和 UDP 都可使用此协议。
        AF_UNIX:      本地通讯协议。具有高性能和低成本的 IPC(进程间通讯)。
    参数2:套接字流,选项有:
        SOCK_STREAM           TCP 协议套接字
          SOCK_DGRAM            UDP协议套接字
          SOCK_SEQPACKET
          SOCK_RAW
          SOCK_RDM。
    参数3:protocol协议,选项有
            SOL_TCP:  TCP 协议。
        SOL_UDP:  UDP协议。

关键函数2:
    socket_connect($socket参数1,$ip参数2,$port参数3)
    作用:连接一个套接字,返回值为true或者false
    参数1:socket_create 的函数返回值
    参数2:ip地址
    参数3:端口号


关键函数3:
    socket_bind($socket参数1,$ip参数2,$port参数3)
    作用:绑定一个套接字,返回值为true或者false
    参数1:socket_create的函数返回值
    参数2:ip地址
    参数3:端口号


关键函数4:
    socket_listen($socket参数1,$backlog 参数2)
    作用:监听一个套接字,返回值为true或者false
    参数1:socket_create的函数返回值
    参数2:最大监听套接字个数


关键函数5:
  socket_accept($socket)
  作用:接收套接字的资源信息,成功返回套接字的信息资源,失败为false
  参数:socket_create的函数返回值

关键函数6:
  socket_read($socket参数1,$length参数2)
  作用:读取套接字的资源信息,
  返回值:成功把套接字的资源转化为字符串信息,失败为false
  参数1:socket_create或者socket_accept的函数返回值
  参数2:读取的字符串的长度

关键函数7:
  socket_write($socket参数1,$msg参数2,$strlen参数3)
  作用:把数据写入套接字中
  返回值:成功返回字符串的字节长度,失败为false
  参数1:socket_create或者socket_accept的函数返回值
  参数2:字符串
  参数3:字符串的长度

关键函数8:
  socket_close($socket)
  作用:关闭套接字
  返回值:成功返回true,失败为false
  参数:socket_create或者socket_accept的函数返回值

关键函数9:
    socket_set_option($socket参数1 ,$level 参数2,$optname 参数3,$optval 参数4)
    作用:是给套接字设置数据流选项
    参数1:socket_create或者socket_accept的函数返回值
    参数2:SOL_SOCKET,好像只有这个选项
    参数3可为:
        SO_REUSEADDR        是让套接字端口释放后立即就可以被再次使用  参数3假如是这个,则参数4可以为true或者false
        SO_RCVTIMEO         是套接字的接收资源的最大超时时间
        S0_SNDTIMEO         是套接字的发送资源的最大超时时间
    参数4与参数3是相关联的,参数3假如是这两个,则参数4是一个这样的数组array('sec'=>1,'usec'=>500000)
  数组里面都是设置超时的最大时间,不过,一个是秒为单位,一个是微秒单位,作用都一样


关键函数10:
socket_select($active_sockets,$write,$except,NULL);
socket_select(array &$read, array &$write, array &$except, $tv_sec, $tv_usec = 0)
    作用:多路选择
    参数1-3:从传入的数组中选择出可读的,可写的,异常的socket,并返回
    参数4:
        若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合(socket数组)中某个文件描述符发生变化为止
        若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值
        timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述
        一般传入null

关键函数11:
    socket_last_error($socket)
    作用:是获取套接字的最后一条错误码号
    参数:socket_create的返回值
    返回值:套接字code

关键函数13:
    socket_strerror($code)
    作用:获取code的字符串信息
    参数:socket_last_error函数的返回值
    返回值:套接字的具体错误信息 <通常当做调试用,输出错误信息>


其他重要的函数:
socket_clear_error()    清除socket的错误或者最后的错误代码
socket_close()           关闭一个socket资源
socket_create_pair()    产生一对没有区别的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_readv()           读取从分散/聚合数组过来的数据
socket_recv()            从socket里结束数据到缓存
socket_recvfrom()        接受数据从指定的socket,如果没有指定则默认当前socket
socket_recvmsg()         从iovec里接受消息
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_writev()          写数据到分散/聚合数组

必须先执行socket_bind,再执行socket_listen,最后才执行socket_accept

非常全面的laravel 案例

laravel之PHP WebSocket研发+WS or WSS (websocket SSL) js websocket

 

 类似资料: