WebSocket 服务器简单来说就是一个遵循特殊协议监听服务器任意端口的tcp应用,它用来监听客户端发送过来的请求,需要常驻内存执行。

    首先,我们需要建立一个socket的连接 , 用来监听客户端发送的请求,需要设置监听的ip地址和端口

    1. <?php
    2. $address="127.0.0.1"; // ip地址
    3. $port=8000; // 自定义端口
    4. $socket=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
    5. socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
    6. socket_bind($socket,$address,$port);
    7. socket_listen($socket);

    当客户端第一次向服务端发出请求时,请求头信息如下

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    Origin: http://127.0.0.1:8000
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    

    其中upgrade websocket用于告诉服务器此连接需要升级到websocket。
    而下面的Sec-WebSocket-Key是客户端也就是浏览器或者其他终端随机生成一组16位的随机base64编码
    最后Sec-WebSocket-Version就是当前使用协议的版本号了。
    服务器在接受到上面的请求之后,需要返回一个response头包完成握手,由Sec-Websocket-Accept的key完成校验,具体算法如下。

    <?php
    /**
     * 握手函数
     * @param $client resource socket连接
     * @param $content string 客户端发来的头信息
     */
    function handshaking($client,$content)
    {
        // 定义头部信息
        $headers=[];
        if(preg_match('/Sec-WebSocket-Key:.*\r\n/',$content,$matchs))
        {
            $headers['Sec-WebSocket-Key'] =trim(chop(str_replace('Sec-WebSocket-Key:',"",$matchs[0])));
        }
        // 设置返回头
        $secKey = $headers['Sec-WebSocket-Key'];
        $websocket_accept=base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
        $upgrade  = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
            "Upgrade: websocket\r\n" .
            "Sec-WebSocket-Version: 13\r\n".
            "Connection: Upgrade\r\n" .
            "Sec-WebSocket-Accept:$websocket_accept\r\n\r\n";
        // 返回客户端
        socket_write($client,$upgrade,strlen($upgrade));
    }
    

    连接成功显示状态码为 101
    image.png
    此时前端在向服务端发送请求时,服务端需要监听前端发送的消息,如下

    <?php
     public function run()
        {
            // 定义写入监听连接池
            $write=null;
            // 定义权限接受连接池
            $except=null;
            // 定义超时时间
            $time_out=null;
            // 启动循环阻塞任务
            while(true)
            {
                // 复制连接池
                $changes=array_column($this->sockets, 'cli');
                // 设置阻塞监听函数 将连接阻塞在此函数 直到有消息发送才疏通
                socket_select($changes,$write,$except,$time_out);
                // 监听端口可读后操作
                foreach ($changes as $sock) {
                    // 如果监听到的是原端口
                    if($sock==$this->socket)
                    {
                        // 服务端取请求客户端
                        $client = socket_accept($this->socket);
                        // 客户端存入客户端数组
                        $this->sockets[(int)$client]=[
                            'cli'=>$client,
                            'hand'=>false // 初始握手为否
                        ];
                        continue;
                    }else{
                        // 接收客户端的请求
                        $bytes = @socket_recv($sock, $buffer, 2048, 0);
                        // 一旦客户端请求接到为false 代表用户下线
                        if(!$bytes){
                            unset($this->sockets[(int)$sock]); // 删除用户池中用户
                            continue;
                        }
                        if(!$this->sockets[(int)$sock]['hand']){
                            // 握手
                            $this->handshaking($sock,$buffer);
                            // 服务端提示
                            echo "客户端已加入连接池\n";
                        }else{
                            echo $this->message($buffer)."\n";
                            $this->send($this->sockets[(int)$sock]['cli'],"给孩子的");
                        }
                    }
                }
            }
        }
    

    客户端每次在发送消息时,固定回复,给孩子的
    image.png
    总结代码如下 :

    <?php
    class webSocket
    {
        private $socket; // 服务端
        private $sockets=[]; // 全部客户端+服务端
        public function __construct($address='127.0.0.1',$port=8000)
        {
            $socket=socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
            socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
            socket_bind($socket,$address,$port);
            socket_listen($socket);
            $this->socket=$socket;
            $this->sockets[0]=['cli'=>$socket]; // 0 为服务端
            echo "已启动websockt \n服务器地址:".$address."\n端口:".$port."\n";
        }
        public function run()
        {
            // 定义写入监听连接池
            $write=null;
            // 定义权限接受连接池
            $except=null;
            // 定义超时时间
            $time_out=null;
            // 启动循环阻塞任务
            while(true)
            {
                // 复制连接池
                $changes=array_column($this->sockets, 'cli');
                // 设置阻塞监听函数 将连接阻塞在此函数 直到有消息发送才疏通
                socket_select($changes,$write,$except,$time_out);
                // 监听端口可读后操作
                foreach ($changes as $sock) {
                    // 如果监听到的是原端口
                    if($sock==$this->socket)
                    {
                        // 服务端取请求客户端
                        $client = socket_accept($this->socket);
                        // 客户端存入客户端数组
                        $this->sockets[(int)$client]=[
                            'cli'=>$client,
                            'hand'=>false // 初始握手为否
                        ];
                        continue;
                    }else{
                        // 接收客户端的请求
                        $bytes = @socket_recv($sock, $buffer, 2048, 0);
                        // 一旦客户端请求接到为false 代表用户下线
                        if(!$bytes){
                            unset($this->sockets[(int)$sock]); // 删除用户池中用户
                            continue;
                        }
                        if(!$this->sockets[(int)$sock]['hand']){
                            // 握手
                            $this->handshaking($sock,$buffer);
                            // 服务端提示
                            echo "客户端已加入连接池\n";
                        }else{
                            echo $this->message($buffer)."\n";
                            $this->send($this->sockets[(int)$sock]['cli'],"给孩子的");
                        }
                    }
                }
            }
        }
        /**
         * 握手函数
         * @param $client resource socket连接
         * @param $content string 客户端发来的头信息
         */
        function handshaking($client,$content)
        {
            // 定义头部信息
            $headers=[];
            if(preg_match('/Sec-WebSocket-Key:.*\r\n/',$content,$matchs))
            {
                $headers['Sec-WebSocket-Key'] =trim(chop(str_replace('Sec-WebSocket-Key:',"",$matchs[0])));
            }
            // 设置返回头
            $secKey = $headers['Sec-WebSocket-Key'];
            $websocket_accept=base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
            $upgrade  = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
                "Upgrade: websocket\r\n" .
                "Sec-WebSocket-Version: 13\r\n".
                "Connection: Upgrade\r\n" .
                "Sec-WebSocket-Accept:$websocket_accept\r\n\r\n";
            // 写入缓冲
            socket_write($client,$upgrade,strlen($upgrade));
            $this->sockets[(int)$client]['hand']=true;
        }
    
        /**
         * 解析函数
         * @param $buffer string 客户端发送的原数据
         * @return null|string 解析后的数据
         */
        function message($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;
        }
        /**
         * 处理帧数据
         * @param $s string 要发送的数据
         * @return string 客户端需要的数据
         */
        function frame($s)
        {
            $a = str_split($s, 125);
            if (count($a) == 1)
            {
                return "\x81" . chr(strlen($a[0])) . $a[0];
            }
            $ns = "";
            foreach ($a as $o)
            {
                $ns .= "\x81" . chr(strlen($o)) . $o;
            }
            return $ns;
        }
        /**
         * 发送数据
         * @param $clinet resource socket资源
         * @param $msg string 发送的消息
         */
        function send($clinet, $msg){
            $msg = $this->frame($msg);
            socket_write($clinet, $msg, strlen($msg));
        }
    }
    $socketobj=new webSocket();
    $socketobj->run(); // 开始监听
    

    先将PHP设置为环境变量,再使用cmd执行刚写好的PHP脚本即可实现监听
    image.png