Thinkphp6实现websocket

项目需要连接一台自动售货机,售货机要求两边用websocket连接,监听9997端口。本文实现了一个基于PHP的WebSocket服务器,用于连接自动售货机,支持start/stop/restart命令操作

1.新建文件

新建文件 /command/socket.php

bash 复制代码
<?php
namespace app\command;

use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;

class TestSocket extends Command
{
    public $server;
    protected static $pidFile = '/tmp/test_socket.pid';
    protected static $running = true;

    protected function configure()
    {
        $this->setName('test:socket')
            ->setDescription('WebSocket server for JieLe')
            ->addArgument('action', Argument::OPTIONAL, 'start|stop|restart', 'start');
    }

    protected function execute(Input $input, Output $output)
    {
        $action = $input->getArgument('action');

        switch ($action) {
            case 'start':
                $this->startServer($output);
                break;
            case 'stop':
                $this->stopServer($output);
                break;
            case 'restart':
                $this->stopServer($output);
                sleep(1); // 等待1秒确保服务停止
                $this->startServer($output);
                break;
            default:
                $output->writeln("Invalid action. Use start|stop|restart");
                break;
        }
    }

    protected function startServer(Output $output)
    {
        // 检查是否已运行
        if (file_exists(self::$pidFile)) {
            $pid = file_get_contents(self::$pidFile);
            if (posix_getpgid($pid)) {
                $output->writeln("Server is already running (PID: {$pid})");
                return;
            }
        }

        // 创建TCP Socket服务器
        $this->server = stream_socket_server("tcp://0.0.0.0:9997", $errno, $errstr);
        if (!$this->server) {
            $output->error("Failed to start server: $errstr ($errno)");
            return;
        }

        // 保存PID
        file_put_contents(self::$pidFile, getmypid());

        // 注册信号处理器
        pcntl_signal(SIGTERM, function() {
            self::$running = false;
        });
        pcntl_signal(SIGINT, function() {
            self::$running = false;
        });

        $output->info("WebSocket server running on ws://0.0.0.0:9997");

        $clients = [];
        while (self::$running) {
            // 处理信号
            pcntl_signal_dispatch();

            $read = array_merge([$this->server], $clients);
            $write = $except = null;

            // 使用stream_select监听活动连接
            if (stream_select($read, $write, $except, 5) > 0) {
                // 处理新连接
                if (in_array($this->server, $read)) {
                    $client = stream_socket_accept($this->server);
                    $clients[] = $client;
                }

                // 处理客户端消息
                foreach ($read as $socket) {
                    if ($socket === $this->server) continue;

                    $data = fread($socket, 1024);
                    if ($data === false || $data === '') {
                        // 客户端断开
                        $key = array_search($socket, $clients);
                        unset($clients[$key]);
                        fclose($socket);
                        continue;
                    }

                    // WebSocket握手处理
                    if (strpos($data, 'Upgrade: websocket') !== false) {
                        $this->handshake($socket, $data);
                        continue;
                    }

                    // 处理WebSocket帧
                    $decoded = $this->decodeFrame($data);
                    //$decoded = $this->main($decoded); 实际处理业务的函数

                    // 发送回复
                    $response = $decoded;
                    $frame = $this->encodeFrame($response);
                    fwrite($socket, $frame);
                }
            }
        }

        // 清理工作
        foreach ($clients as $client) {
            fclose($client);
        }
        fclose($this->server);
        unlink(self::$pidFile);
        $output->writeln("Server stopped");
    }

    protected function stopServer(Output $output)
    {
        if (!file_exists(self::$pidFile)) {
            $output->writeln("Server is not running");
            return;
        }

        $pid = file_get_contents(self::$pidFile);
        if (posix_getpgid($pid)) {
            posix_kill($pid, SIGTERM);
            $output->writeln("Stopping server (PID: {$pid})...");
            // 等待进程结束
            $timeout = 10; // 10秒超时
            while ($timeout-- > 0 && posix_getpgid($pid)) {
                sleep(1);
            }
            if (posix_getpgid($pid)) {
                posix_kill($pid, SIGKILL); // 强制杀死
            }
        }

        if (file_exists(self::$pidFile)) {
            unlink(self::$pidFile);
        }
        $output->writeln("Server stopped");
    }

    /************************************************** websocket转码相关函数  *******************************************************/

    // WebSocket帧解码
    public function decodeFrame($data)
    {
        $len = ord($data[1]) & 127;
        if ($len === 126) {
            $masks = substr($data, 4, 4);
            $data = substr($data, 8);
        } elseif ($len === 127) {
            $masks = substr($data, 10, 4);
            $data = substr($data, 14);
        } else {
            $masks = substr($data, 2, 4);
            $data = substr($data, 6);
        }
        $decoded = '';
        for ($i = 0; $i < strlen($data); $i++) {
            $decoded .= $data[$i] ^ $masks[$i % 4];
        }
        return $decoded;
    }

    // WebSocket帧编码
    public function encodeFrame($data)
    {
        $frame = [];
        $frame[0] = 0x81; // FIN + text frame
        $len = strlen($data);

        if ($len <= 125) {
            $frame[1] = $len;
        } elseif ($len <= 65535) {
            $frame[1] = 126;
            $frame[2] = ($len >> 8) & 255;
            $frame[3] = $len & 255;
        } else {
            $frame[1] = 127;
            for ($i = 0; $i < 8; $i++) {
                $frame[$i + 2] = ($len >> (8 * (7 - $i))) & 255;
            }
        }
        $frame = array_map('chr', $frame);
        $frame = implode('', $frame) . $data;
        return $frame;
    }


    // WebSocket握手处理
    public function handshake($socket, $headers)
    {
        if (preg_match('/Sec-WebSocket-Key: (.*)\r\n/', $headers, $match)) {
            $key = base64_encode(sha1($match[1] . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
            $response = "HTTP/1.1 101 Switching Protocols\r\n";
            $response .= "Upgrade: websocket\r\n";
            $response .= "Connection: Upgrade\r\n";
            $response .= "Sec-WebSocket-Accept: $key\r\n\r\n";
            fwrite($socket, $response);
        }
    }
}

2.开启服务

docker exec php7.3 php /lnmp/nginx/data/thinkphp6/think test:socket start

docker exec php7.3 php /lnmp/nginx/data/thinkphp6/think test:socket stop

docker exec php7.3 php /lnmp/nginx/data/thinkphp6/think test:socket restart

3.在nginx配置目录,可通过浏览器访问socket业务

bash 复制代码
server {
	listen 80;
	root 省略;

	#172.18.0.3是提供php服务的ip
	
	location /rtsp {
		proxy_pass http://172.18.0.3:9997;
		proxy_http_version 1.1;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";
		proxy_set_header Host $host;
		proxy_read_timeout 600s;
	}

	location ~ \.php$ {
		#省略
    }
	
}

4.测试

相关推荐
Chenyu_3102 分钟前
05.MySQL表的约束
android·开发语言·网络·数据库·网络协议·mysql·php
Rocky40117 分钟前
开发的几种格式,TCP的十个重要机制
运维·服务器·网络
三体世界1 小时前
Linux --TCP协议实现简单的网络通信(中英翻译)
linux·c语言·开发语言·网络·c++·windows·tcp/ip
zqx_72 小时前
随记 配置服务器的ssl整个过程
网络·网络协议·ssl
ywyy67982 小时前
小程序定制开发:从需求到落地,打造企业专属数字化入口
大数据·网络·人工智能·小程序·短剧
GoldenaArcher3 小时前
Fullstack 面试复习笔记:操作系统 / 网络 / HTTP / 设计模式梳理
网络·笔记·面试
茶本无香3 小时前
HTTP协议接口三种测试方法之-JMeter(保姆教程)
网络协议·jmeter·http
superior tigre3 小时前
神经网络基础:从单个神经元到多层网络(superior哥AI系列第3期)
网络·人工智能·神经网络
Johny_Zhao3 小时前
Burp Suite 企业级深度实战教程
linux·网络·网络安全·信息安全·云计算·shell·burp suite·系统运维·itsm
s_little_monster4 小时前
【Linux】网络--网络层--IP协议
linux·运维·网络·经验分享·笔记·学习·tcp/ip