1. 程式人生 > >server.php

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();