php使用websocket编写的简易客服系统源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/w15249243295/article/details/52485660

<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);">一、websocket协议简介</span>

WebSocket是为解决客户端与服务端实时通信而产生的技术。其本质是先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。


二、php使用的一些websocket函数

resource socket_create ( int $domain , int $type , int $protocol )

bool socket_set_option ( resource $socket , int $level , int $optname , mixed $optval )

bool socket_bind ( resource $socket , string $address [, int $port = 0 ] )

bool socket_listen ( resource $socket [, int $backlog = 0 ] )

以上四个方法都是非阻塞的,而下面的几个方法都是阻塞的或者是有timeout的

int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )

resource socket_accept ( resource $socket )

int socket_recv ( resource $socket , string &$buf , int $len , int $flags )

string socket_read ( resource $socket , int $length [, int $type = PHP_BINARY_READ ] )

后面会对socket_recv()和socket_read()进行介绍


三、php服务端代码分析

<?php
/**
 * Created by PhpStorm.
 * User: changshuiwang
 * Date: 2016/9/5
 * Time: 14:35
 */
error_reporting(~E_WARNING & ~E_NOTICE);
set_time_limit(0);
class Wserver{
    public $address;
    public $port;
    public $master;  <span style="color: rgb(255, 0, 0);">// 连接 server 的 client</span>
    public $sockets = array(); <span style="color:#ff0000;">// 不同状态的 socket 管理</span>
    //public $handshake = false; <span style="color:#ff0000;">// 判断是否握手</span>
    public $request = array(); <span style="color:#ff0000;">//过来的请求</span>
    public $response = array();<span style="color:#ff0000;">//服务人员</span>
    public $client = array();   <span style="color:#ff0000;">//正在进行的会话</span>
    public $wenhou = "你好,有什么可以帮助你的么?"; <span style="color:#ff0000;">//建立请求的问候语</span>
    public $refuse = "对不起,请稍等,暂时无空闲客服人员。"; <span style="color:#ff0000;">//没有空客服人员的回话</span>
    //public $tag=false; //默认为false,标志位,是否有空客服人员

    function __construct($address, $port)
    {
        $this->address=$address;
        $this->port=$port;
        // 建立一个 socket 套接字
        $this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)
        or die("socket_create() failed");
        socket_set_option($this->master, SOL_SOCKET, SO_REUSEADDR, 1)
        or die("socket_option() failed");
        socket_bind($this->master, $this->address, $this->port)
        or die("socket_bind() failed");
        socket_listen($this->master)
        or die("socket_listen() failed");
        $this->sockets[] = $this->master;
        // debug
        echo("Master socket  : " . $this->master . "\n");

        //socket_set_nonblock($this->master);<span style="color:#ff0000;">//让进程非阻塞,主要是socket_accept(),因为这里没有让master参与到通信中,所以可以不加这一句</span>
    }
    function start(){
        while(true) {
            $changed=$this->sockets;<span style="color:#ff0000;">//让当前的socket列表加入到监听中</span>
            $write = NULL;
            $except = NULL;
            $num=socket_select($changed, $write, $except, 0);
            if($num==0){
                continue;
            }
            <span style="color:#ff0000;">//$changed是引用变量,调用之后$changed是状态改变的数组,如果来新连接$changed里面会有$this->master</span>

            if(in_array($this->master,$changed)){
                <span style="color:#ff0000;">//来新连接了</span>
                $socket_new = socket_accept($this->master);<span style="color:#ff0000;">//返回值是一个resource</span>

                //socket_set_nonblock($socket_new);<span style="color:#ff0000;">//不能让master以外的socket为非阻塞的,因为会涉及到读socket数据</span>

                <span style="color:#ff0000;">//通过socket获取数据执行handshake</span>
                @$header = socket_read($socket_new, 1024);
                $this->perform_handshaking($header, $socket_new, $this->address, $this->port);


                @socket_getpeername($socket_new, $ip);
                $msg=$this->diff($ip);<span style="color:#ff0000;">//这里是根据ip来区分是客户</span> 
                switch ($msg){
                    case 'client':
                        $tag=$this->addClient($socket_new);
                        $message=$tag=="client"?$this->wenhou:$this->refuse;//发送的消息
                        $response = $this->mask(json_encode(array('type'=>'system', 'message'=> $message)));
                        $this->send_message([$socket_new],$response);
                        break;
                    default:
                        //$message="有新请求";
                        $this->addResponse($socket_new);
                        $index=array_search($socket_new,$this->response);
                        $this->changeStatus($index);
                        $message="客服人员已来!";
                        $response = $this->mask(json_encode(array('type'=>'system', 'message'=> $message)));
                        $this->send_message([$this->client["$index"],$response]);
                        break;
                }

                <span style="color:#ff0000;">//从改变的socket数组里面删除</span>
                $found_socket = array_search($this->master, $changed);
                unset($changed[$found_socket]);
                echo $msg." connect\n";
            }

            <span style="color:#ff0000;">//未加入会话的请求的处理,比如未加入会话但发来消息,或者是在等待过程中断开</span>
            foreach ($this->request as $request){
                if(in_array($request,$changed)){

                }
            }

<span style="color:#ff0000;">            //如果是断开连接,socket会先发送一个消息,socket_read()返回false但是socket_recv()会读取到数据,
            //所以foreach会执行两次,指的是两次while然后两次foreach
            //第一次socket_recv会读取到数据,第二次socket_recv()读取不到数据,就会直接走下面的判断语句
            //不能直接使用socket_read()因为会在读到换行时候结束</span>
            foreach ($changed as $changed_socket){

                <span style="color:#ff0000;">//如果有client数据发送过来</span>
                $tag=false;

                while(socket_recv($changed_socket, $buf, 1024, 0) >= 1)<span style="color:#ff0000;">//这里就要限制每次发送数据最多为1024,</span>
                {
                    //var_dump("wh");
                    $tag=true;
                    //解码发送过来的数据
                    $received_text = $this->unmask($buf);
                    //编码需要发出的数据
                    $response_text = $this->mask(json_encode(array('type'=>'usermsg', 'message'=>$received_text)));
                    if($response_text=="\000"){<span style="color:#ff0000;">//对应的是断开连接的,如果是断开连接,socket_recv()会收到一串字符</span>
                        break;
                    }
                    if(in_array($changed_socket,$this->client)){
                        $index=array_search($changed_socket,$this->client);
                        $this->send_message([$this->response["$index"]],$response_text);
                    }else{
                        $index=array_search($changed_socket,$this->response);
                        $this->send_message([$this->client["$index"]],$response_text);
                    }
                    break;
                    //跳出循坏,因为socket_recv()会阻塞,但是如果socket断开了之后,socket_recv()就不会阻塞了
                }

                <span style="color:#ff0000;">//已经读了数据的跳过下面的断开连接判断,不然会阻塞</span>
                if($tag==true){
                    continue;
                }

                <span style="color:#ff0000;">//检查offline的client,断开连接</span>

                @$buf = socket_read($changed_socket, 1024, PHP_NORMAL_READ);
                if ($buf === false) {<span style="color:#ff0000;">//没有接受到数据的时候,但是这个socket又有变化,也就是断开连接</span>
                    $found_socket = array_search($changed_socket, $this->sockets);<span style="color:#ff0000;">//返回下标</span>
                    socket_getpeername($changed_socket, $ip);
                    unset($this->sockets[$found_socket]);<span style="color:#ff0000;">//必须要手动删除</span>

                    $msg=$this->diff($ip);
                    $response = $this->mask(json_encode(array('type'=>'system', 'message'=>'断开连接')));
                    switch ($msg){
                        case "client":
                            $index=array_search($changed_socket,$this->client);
                            $this->client["$index"]="";
                            if($this->changeStatus($index)){
                                $this->send_message([$this->response["$index"]],$response);<span style="color:#ff0000;">//向客服发送断开</span>
                                $response = $this->mask(json_encode(array('type'=>'system', 'message'=>$this->wenhou)));
                                $this->send_message([$this->client["$index"]],$response);<span style="color:#ff0000;">//新客户的问候语</span>
                            }
                            break;
                        default:
                            $index=array_search($changed_socket,$this->response);
                            unset($this->response["$index"]);<span style="color:#ff0000;">//释放客服的数组元素</span>
                            $this->send_message([$this->client["$index"]],$response);
                            $index=array_search($this->client["$index"],$this->sockets);
                            unset($this->sockets["$index"]);<span style="color:#ff0000;">//删掉监听的socket</span>
                            break;
                    }

                }
            }
        }
    }

    function send_message($clients,$msg)
    {
        foreach($clients as $changed_socket)
        {
            socket_write($changed_socket,$msg,strlen($msg));
        }
        return true;
    }


    //解码数据
    function unmask($text) {
        $length = ord($text[1]) & 127;
        if($length == 126) {
            $masks = substr($text, 4, 4);
            $data = substr($text, 8);
        }
        elseif($length == 127) {
            $masks = substr($text, 10, 4);
            $data = substr($text, 14);
        }
        else {
            $masks = substr($text, 2, 4);
            $data = substr($text, 6);
        }
        $text = "";
        for ($i = 0; $i < strlen($data); ++$i) {
            $text .= $data[$i] ^ $masks[$i%4];
        }
        return $text;
    }

    //编码数据
    function mask($text)
    {
        $b1 = 0x80 | (0x1 & 0x0f);
        $length = strlen($text);

        if($length <= 125)
            $header = pack('CC', $b1, $length);
        elseif($length > 125 && $length < 65536)
            $header = pack('CCn', $b1, 126, $length);
        elseif($length >= 65536)
            $header = pack('CCNN', $b1, 127, $length);
        return $header.$text;
    }

    //握手的逻辑
    function perform_handshaking($receved_header,$client_conn, $host, $port)
    {
        $headers = array();
        $lines = preg_split("/\r\n/", $receved_header);
        foreach($lines as $line)
        {
            $line = chop($line);
            if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))
            {
                $headers[$matches[1]] = $matches[2];
            }
        }

        $secKey = $headers['Sec-WebSocket-Key'];
        $secAccept = 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" .
            "Connection: Upgrade\r\n" .
            "WebSocket-Origin: $host\r\n" .
            "WebSocket-Location: ws://$host:$port/demo/shout.php\r\n".
            "Sec-WebSocket-Accept:$secAccept\r\n\r\n";
        socket_write($client_conn,$upgrade,strlen($upgrade));
    }

    /**
     * 查询空客服,如果没有就返回false
     * @return bool|int|string
     */
    function searchEmptyResponse(){
        foreach ($this->response as $k=>$v){
            if(empty($this->response[$k])){
                return $k;
            }else{
                return false;
            }
        }
    }

    /**
     * 加入到正在会话中
     * @param $socket
     */
    function addClient($socket){
        if(empty($this->response)||count($this->response)==count($this->client)){
            //满了
            $this->request[]=$socket;
            return "request";
        }else{
            $index=$this->searchEmptyResponse();
            $this->client[$index]=$socket;//加入正在会话中
            $this->sockets[]=$socket;//加入到监控数组
            return "client";
        }
    }

    /**
     * 加入到客服中
     * @param $socket
     */
    function addResponse($socket){
        $this->response[]=$socket;
        $this->sockets[]=$socket;
    }

    /**
     * 区分是客服还是请求
     * @param $socket
     * @param $ip
     */
    public function diff($ip){
        if($ip=="xxxxxx"){
            //客服
            return "response";
        }else{
            return "client";
        }

    }

    /**
     * 将原先等候的加入到正在会话中
     * @param $loc
     * @return bool
     */
    private function changeStatus($loc){
        if(empty($this->request)){
            //等待队列为空
            return false;
        }else{
<span style="white-space:pre">	</span>    foreach ($this->request as $k=>$v){
          $index=$k;
          break;
        }
        $this->client["$loc"]=$this->request["$index"];
        $this->sockets[]=$this->request["$index"];
        unset($this->request["$index"]);
        return true;            
        }
    }

    //private function
}

$server=new Wserver('xxxxxx','10000');
$server->start();


四、客户端代码


<?php
/**
 * Created by PhpStorm.
 * User: changshuiwang
 * Date: 2016/9/5
 * Time: 14:46
 */
?>
<!DOCTYPE html>
<script src="jquery-1.6.2.min.js"></script>
<script>
    $(function(){
        var wsServer = 'ws://xxxx:xxxx';
        var ws = new WebSocket(wsServer);
        //alert("22");
        ws.onopen = function(e){
            //alert("连接成功");
        }
        ws.onclose = function(e){
            alert("连接关闭");
        }
        ws.onerror = function (e) {
            //alert("连接错误");
        }
        ws.onmessage = function (evt) {
            //alert(evt);
            //alert("22");
            var data=JSON.parse(evt.data);
            alert(data.message);
            //var message=JSON.stringify(evt.data);
            //alert(message);
            //发送字符串,服务器端只需要unmask就可以了,如果是json串,后端就要json_decode
        };
        $(":button").click(function(){
            var message=$("#message").val();
            console.log(ws.send(message));
        })
    })
</script>
<body>
    <div style="width:200px;height:600px;border-color:#0000FF;border: 2px solid;margin-bottom:20px;">
        11
    </div>
    <form action="#">
        <textarea id="message" name="message" style="width:200px;height:50px;resize:none;">
        </textarea>
        <input type="button" name="send" value="发送">
    </form>
</body>


五、一点总结

其实这只是一个小demo,不过写到这里就发现,完全没有写下去的必要了,php用来监听socket是用的socket_select(),对应的底层实现应该就是select机制,现在都用的epoll机制了。关于select()和epoll(),大家看看这篇我读过的最好的epoll讲解

猜你喜欢

转载自blog.csdn.net/w15249243295/article/details/52485660