server.php

<?php

//设置超时的时间为无限
set_time_limit(0);

class WebSocket {

    //用户存储服务器socket
    private $socket;
    //用于存储客户端socket
    private $accept = [];
    //用于存储不同聊天室的客户端
    private $clientRoom = [];
    //用于存储客户端的链接状态。是否握手
    private $ishand = [];
    //当前在线用户
    private $onlineUser = [];
    //用于存储不同聊天室的用户列表
    private $userRoom = [];
    //链接最大值
    private $maxConnect = 10;

    public function __construct() {
        //创建socket
        $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//        socket_set_option($this->socket, SOL_SOCKET, SO_REUSEADDR, TRUE);
        //绑定端口
        socket_bind($this->socket, '127.0.0.1', 9795);
        //开始监听端口
        socket_listen($this->socket, $this->maxConnect);
    }

    /**
     * 处理socket
     */
    public function start() {
        while (true) {
            //将所有socket存入一个数组:包括服务器与所有的客户端socket
            $sockets = $this->accept;
            $sockets[] = $this->socket;
            //使用select查看当前活跃的socket
            socket_select($sockets, $write, $except, NULL);

            //当程序走到这一步的时候,证明有活跃的socket,需要处理
            foreach ($sockets as $socket) {
                //判断当前活跃的socket是否为服务器的socket,如果是,证明有新的链接
                if ($socket == $this->socket) {
                    $accept = socket_accept($this->socket);
//                    echo 'get new socket:' . $accept . "\r\n";
                    //获取新的客户端socket并存入accept数组中
                    $this->accept[] = $accept;
                    //设置当前客户端握手状态为false
                    $this->ishand[(int) $accept] = false;
                    continue;
                }
                //从当前活跃的客户端socket获取内容
                $content = @socket_read($socket, 4096);
                //当socket中的内容长度小于7的时候,我们就需要断开与客户端的链接
                if (strlen($content) < 7) {
//                    echo 'close socket:' . $socket . "\r\n";
                    socket_close($socket);
                    $key = array_search($socket, $this->accept);
                    unset($this->accept[$key]);
                    unset($this->ishand[(int) $socket]);
                    $userData = $this->onlineUser[(int) $socket];
                    unset($this->onlineUser[(int) $socket]);
                    unset($this->clientRoom[$userData['room']][(int) $socket]);
                    unset($this->userRoom[$userData['room']][(int) $socket]);
                    $this->sendLogoutMessage($userData['nickname'], $userData['room']);
                    continue;
                }
                if (!$this->ishand[(int) $socket]) {
//                    echo 'hand socket:' . $socket . "\r\n";
                    //如果当前客户端没有握手,执行握手流程,建立链接
                    $this->dohandshake($socket, $content);
                    $this->ishand[(int) $socket] = true;
                } else {
                    $this->message($content, $socket);
                }
            }
        }
    }

    private function sendLogoutMessage($username, $room) {
        $data = [
            'msg_type' => 'logout',
            'msg' => '【' . $username . '】离开了我们!',
            'users' => $this->userRoom[$room]
        ];
        $json = json_encode($data, JSON_UNESCAPED_UNICODE);
        $encodeData = $this->encode($json);
        foreach ($this->clientRoom[$room] as $client) {
            socket_write($client, $encodeData, strlen($encodeData));
        }
    }

    /*
     * 处理数据
     */

    private function message($content, $client) {
        $jsonData = $this->decode($content);
        $dataArr = json_decode($jsonData, true);
        if (!$dataArr) {
            return null;
        }
        switch ($dataArr['act']) {
            case '@': $this->privateMessage($dataArr, $client);
                break;
            default: $this->baseMessage($dataArr, $client);
                break;
        }
    }

    private function baseMessage($dataArr, $client) {
        $room = $dataArr['room'];
        if ($dataArr['act'] == 'login') {
            $data = [
                'msg_type' => 'login',
                'msg' => '欢迎【' . $dataArr['nickname'] . '】登录!',
            ];
            $this->onlineUser[(int) $client] = [
                'nickname' => $dataArr['nickname'],
                'login_time' => date('Y-m-d H:i:s'),
                'client' => $client,
                'room' => $room
            ];
            //处理当前客户端在哪个聊天室
            $this->clientRoom[$room][(int) $client] = $client;
            $this->userRoom[$room][(int) $client] = $dataArr['nickname'];
        } else {
            $data = [
                'msg_type' => 'text',
                'nickname' => $dataArr['nickname'],
                'msg' => $dataArr['con']
            ];
        }
        $data['users'] = $this->userRoom[$room];
        $encodeData = $this->encode(json_encode($data, JSON_UNESCAPED_UNICODE));
        foreach ($this->clientRoom[$room] as $accept) {
            socket_write($accept, $encodeData, strlen($encodeData));
        }
        return;
    }

    private function privateMessage($dataArr, $client) {
        $room = $dataArr['room'];
        //取出所有的客户端列表,并且使用客户端的昵称作为下标
        $user = array_column($this->onlineUser, 'client', 'nickname');
        if($this->onlineUser[(int) $user[$dataArr['to']]]['room'] != $room){
            return null;
        }
        //先组装个数据
        $data = [
            'msg_type' => '@me',
            'from' => $dataArr['from'],
            'to' => $dataArr['to'],
            'msg' => $dataArr['con']
        ];
        //赋值一个在线用户列表
        $data['users'] = $this->userRoom[$room];
        //将数据转换为json
        $json = json_encode($data, JSON_UNESCAPED_UNICODE);
        //将json转换为数据帧
        $encodeData = $this->encode($json);
        //给接收人的客户端发送消息
        socket_write($user[$dataArr['to']], $encodeData, strlen($encodeData));
        //给发送人的客户端发送消息
        socket_write($client, $encodeData, strlen($encodeData));
        return;
    }

    /*
     * 响应客户端握手头
     * @param $sock 客户端资源
     * @param $data 客户端请求头
     */

    private function dohandshake($sock, $data) {
        //截取客户端请求头中Sec-WebSocket-Key的值
        if (preg_match("/Sec-WebSocket-Key: (.*)\r\n/", $data, $match)) {
            //将key值后面追加一个固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11 然后sha1加密再转base64编码
            $response = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
            //拼接一个握手头 注意每一个参数后面都需要使用 \r\n 换行
            $upgrade = "HTTP/1.1 101 Switching Protocol\r\n" .
                    "Upgrade: websocket\r\n" .
                    "Connection: Upgrade\r\n" .
                    "Sec-WebSocket-Accept: " . $response . "\r\n\r\n";
            //将内容写入客户端套接字,客户端将会接收到数据并验证成功即可与服务器建立链接
            socket_write($sock, $upgrade, strlen($upgrade));
        }
    }

    /**
     * 解码过程:可以解析客户端发送的数据帧为一个json
     */
    private function 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;
    }

    /**
     * 编码过程:可以将json数据转换为客户端可以解析的数据帧
     */
    private function encode($buffer) {
        $length = strlen($buffer);
        if ($length <= 125) {
            return "\x81" . chr($length) . $buffer;
        } else if ($length <= 65535) {
            return "\x81" . chr(126) . pack("n", $length) . $buffer;
        } else {
            return "\x81" . char(127) . pack("xxxxN", $length) . $buffer;
        }
    }

}

$socket = new WebSocket();
$socket->start();

猜你喜欢

转载自blog.csdn.net/qq_41610686/article/details/80170750
今日推荐