PHP TCP客户端 支持ws,wss、ssl、tcp

PHP TCP客户端 支持ws,wss、ssl、tcp

PHP TCP连接客户端,支持TCP,WebSocket等连接,支持WebSocket ws和wss


<?php
/**
 * NetTCP 支持ws,wss、ssl、tcp
 * @author 尹雪峰
 * @date 2022年6月6日
 * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
 */
class NetTCP {

    private $socket_uri = "wss://www.xforms.cn";
    private $isRev      = false; //是否开启错误提醒
    private $socket     = null;
    //默认设置参数
    private $options    = [
        'context'       => null,
        'filter'        => ['text', 'binary'],
        'fragment_size' => 4096,
        'headers'       => null,
        'logger'        => null,
        'origin'        => null,
        'persistent'    => false,
        'return_obj'    => false,
        'timeout'       => 5,
    ];
    //默认信息编码设置
    private $opcodes    = [
        'continuation'  => 0,
        'text'          => 1,
        'binary'        => 2,
        'close'         => 8,
        'ping'          => 9,
        'pong'          => 10,
    ];

    /**
     * 构造函数
     * @author 尹雪峰
     * @date 2022年6月6日
     * @param unknown $config
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    public function __construct($socket_uri = null){
        if(!empty($socket_uri)){
            $this->socket_uri = $socket_uri;
        }
        if(!$this->isConnect()){
            $this->connect();
        }
    }

    /**
     * 析构函数
     * @author 尹雪峰
     * @date 2022年6月7日
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    public function __destruct(){
        if ($this->isConnect() && get_resource_type($this->socket) !== 'persistent stream') {
            fclose($this->socket);
        }
        $this->socket   = null;
    }

    /**
     * 发送信息
     * @author 尹雪峰
     * @date 2022年6月6日
     * @param unknown $payload
     * @param string $opcode
     * @param bool $masked
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    public function send($payload, string $opcode = 'text', bool $masked = true){
        if (!$this->isConnect()){
            $this->connect();
        }
        if (!in_array($opcode, array_keys($this->opcodes))){
            $this->show(-1, "Bad opcode '{$opcode}'.  Try 'text' or 'binary'.");
        }
        $payload_chunks = str_split($payload, $this->options['fragment_size']);
        $frame_opcode   = $opcode;
        for ($index     = 0; $index < count($payload_chunks); ++$index) {
            $chunk      = $payload_chunks[$index];
            $final      = $index == count($payload_chunks) - 1;
            $this->sendFragment($final, $chunk, $frame_opcode, $masked);
            $frame_opcode = 'continuation';
        }
    }

    /**
     * 发送信息
     * @author 尹雪峰
     * @date 2022年6月6日
     * @param unknown $message
     * @param string $res
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    private function connect(){
        $url_parts          = parse_url($this->socket_uri);
        $scheme             = $url_parts['scheme'];
        $host               = $url_parts['host'];
        $user               = isset($url_parts['user']) ? $url_parts['user'] : '';
        $pass               = isset($url_parts['pass']) ? $url_parts['pass'] : '';
        $port               = isset($url_parts['port']) ? $url_parts['port'] : ($scheme === 'wss' ? 443 : 80);
        $path               = isset($url_parts['path']) ? $url_parts['path'] : '/';
        $query              = isset($url_parts['query'])    ? $url_parts['query'] : '';
        $fragment           = isset($url_parts['fragment']) ? $url_parts['fragment'] : '';

        $path_with_query    = $path;
        if (!empty($query)) {
            $path_with_query .= '?' . $query;
        }
        if (!empty($fragment)) {
            $path_with_query .= '#' . $fragment;
        }

        if (!in_array($scheme, ['ws', 'wss'])) {
            $this->show(-1, "Url should have scheme ws or wss, not '{$scheme}' from URI '{$this->socket_uri}'.");
        }
        $host_uri           = ($scheme === 'wss' ? 'ssl' : 'tcp') . '://' . $host;

        if (isset($this->options['context']) && !empty($this->options['context'])) {
            if (@get_resource_type($this->options['context']) === 'stream-context') {
                $context = $this->options['context'];
            } else {
                $this->show(-1, "Stream context in \$options['context'] isn't a valid context.");
            }
        } else {
            $context    = stream_context_create(array("ssl" => array("capture_peer_cert" => TRUE)));
        }

        $persistent     = true;
        $errno          = null;
        $errstr         = null;
        $persistent     = $this->options['persistent'] === true;
        $flags          = STREAM_CLIENT_CONNECT;
        $flags          = $persistent ? $flags | STREAM_CLIENT_PERSISTENT : $flags;

        //取消证书认证,避免https无法正常使用
        stream_context_set_option($context, 'ssl', 'verify_peer_name', false);
        stream_context_set_option($context, 'ssl', 'verify_peer', false);
        stream_context_set_option($context, 'ssl', 'verify_host', false);

        $this->socket = stream_socket_client(
            "{$host_uri}:{$port}",
            $errno,
            $errstr,
            $this->options["timeout"],
            $flags,
            $context
        );

        restore_error_handler();
        if($this->isConnect()){
            if(!$persistent || ftell($this->socket) == 0){
                stream_set_timeout($this->socket, $this->options["timeout"]);
                $key        = $this->generateKey();
                $headers = [
                    'Host'                  => $host . ":" . $port,
                    'User-Agent'            => 'websocket-client-php',
                    'Connection'            => 'Upgrade',
                    'Upgrade'               => 'websocket',
                    'Sec-WebSocket-Key'     => $key,
                    'Sec-WebSocket-Version' => '13',
                ];
                if ($user || $pass) {
                    $headers['authorization'] = 'Basic ' . base64_encode($user . ':' . $pass);
                }
                if (isset($this->options['origin'])) {
                    $headers['origin'] = $this->options['origin'];
                }
                if (isset($this->options['headers'])) {
                    $headers = array_merge($headers, $this->options['headers']);
                }
                $header = "GET " . $path_with_query . " HTTP/1.1\r\n" . implode("\r\n",
                    array_map(function ($key, $value) {
                        return "$key: $value";
                    },
                    array_keys($headers),
                    $headers
                    )
                    )."\r\n\r\n";

                    $matches    = array();
                    $this->write($header);
                    $response   = stream_get_line($this->socket, 1024, "\r\n\r\n");
                    $address    = "{$scheme}://{$host}{$path_with_query}";
                    if (!preg_match('#Sec-WebSocket-Accept:\s(.*)$#mUi', $response, $matches)) {
                        $this->show(-1, "Connection to '{$address}' failed: Server sent invalid upgrade response: {$response}");
                    }
                    $keyAccept  = trim($matches[1]);
                    $expectedResonse = base64_encode(pack('H*', sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
                    if ($keyAccept !== $expectedResonse) {
                        $this->show(-1, "Server sent bad upgrade response");
                    }
            }else{
                $this->show(-1, "网络连接异常,请稍后重试");
            }
        }else{
            $this->show(-1, "网络连接异常,请稍后重试");
        }
    }

    /**
     * 判断是否连接
     * @author 尹雪峰
     * @date 2022年6月6日
     * @return boolean
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    private function isConnect(){
        return $this->socket &&
        (get_resource_type($this->socket) == 'stream' || get_resource_type($this->socket) == 'persistent stream');
    }

    /**
     * Receive one message.
     * Will continue reading until read message match filter settings.
     * Return Message instance or string according to settings.
     */
    private function sendFragment(bool $final, string $payload, string $opcode, bool $masked){
        $data       = '';
        $byte_1     = $final ? 0b10000000 : 0b00000000; // Final fragment marker.
        $byte_1     |= $this->opcodes[$opcode]; // Set opcode.
        $data       .= pack('C', $byte_1);
        $byte_2     = $masked ? 0b10000000 : 0b00000000; // Masking bit marker.
        $payload_length = strlen($payload);
        if ($payload_length > 65535) {
            $data   .= pack('C', $byte_2 | 0b01111111);
            $data   .= pack('J', $payload_length);
        } elseif ($payload_length > 125) {
            $data   .= pack('C', $byte_2 | 0b01111110);
            $data   .= pack('n', $payload_length);
        } else {
            $data   .= pack('C', $byte_2 | $payload_length);
        }
        if ($masked) {
            $mask   = '';
            for ($i = 0; $i < 4; $i++) {
                $mask .= chr(rand(0, 255));
            }
            $data   .= $mask;
            for ($i = 0; $i < $payload_length; $i++) {
                $data .= $payload[$i] ^ $mask[$i % 4];
            }
        } else {
            $data   .= $payload;
        }
        $this->write($data);
    }

    /**
     * 写入数据信息
     * @author 尹雪峰
     * @date 2022年6月6日
     * @param string $data
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    private function write(string $data){
        @fwrite($this->socket, $data);
    }

    /**
     * 输入信息
     * @author 尹雪峰
     * @date 2022年6月6日
     * @param unknown $code
     * @param unknown $msg
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    private function show($code, $msg, $data = null){
        $value = array(
            "code"  =>$code,
            "msg"   =>$msg,
        );
        if(!empty($data)){
            $value["data"] = $data;
        }
        if($this->isRev){
            echo json_encode($value, JSON_UNESCAPED_UNICODE);
            die();
        }else{
            return $value;
        }
    }

    /**
     * 获取websocket key值
     * @author 尹雪峰
     * @date 2022年6月6日
     * @return string
     * @Copyright (C) 2020-2030 YinXueFeng. All Rights Reserved.
     */
    private function generateKey(){
        $key = '';
        for ($i = 0; $i < 16; $i++) {
            $key .= chr(rand(33, 126));
        }
        return base64_encode($key);
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尹雪峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值