标题:【Swoole系列2.2】Http、TCP、UDP服务

文章目录
    分类:PHP 标签:Swoole

    Http、TCP、UDP服务

    其实在上篇文章中,我们就已经运行起来了一个 Http 服务,也简单地说明了一下使用 Swoole 运行起来的服务与普通的 PHP 开发有什么区别。想必你现在会说这没什么大不了的呀,这些我们的传统开发又不是做不到,而且还更方便一些。在基础篇章中,我们还不会看到 Swoole 在性能上的优势,毕竟最基础的一些服务搭建还是要先了解清楚的。因此,今天我们将继续再深入的讲一下 Http 相关的内容以及了解一下 TCP、UDP 服务在 Swoole 中如何运行。

    Http

    我们还是看看上次的 Http 服务的代码。

    $http = new Swoole\Http\Server('0.0.0.0', 9501);
    
    $http->on('Request', function ($request, $response) {
        echo "接收到了请求", PHP_EOL;
        $response->header('Content-Type', 'text/html; charset=utf-8');
        $response->end('<h1>Hello Swoole. #' . rand(1000, 9999) . '</h1>');
    });
    
    echo "服务启动", PHP_EOL;
    $http->start();

    首先,我们实例化了一个 Server 对象,在这里我们传递了两个构造函数,一个是监听的 IP 地址,一个是端口号。一般情况下,如果是生产环境内网,我们建议使用内网的本机 IP ,或者直接就是 127.0.0.1 只允许本机访问。但是在我们的测试过程中,需要在虚拟机外访问的话,就需要 0.0.0.0 这样的监听全部 IP 地址。这一块相信不用我过多解释了,Linux 服务的基本知识,数据库、Redis、PHP-FPM 什么的都有这样的配置。


    接下来,使用 on() 函数,它是一个监听函数,用于监听指定的事件。在这里,我们监听的就是 Request 事件,监听到的内容将通过回调函数的参数返回,也就是第一个参数 $request ,然后它还会带一个 $response 参数用于返回响应事件。当使用 $response 参数的 end() 方法时,将响应输出指定的内容并结束当前的请求。


    上述步骤就完成了一次普通的 Http 请求响应。

    request参数

    接下来,我们尝试打印一下 $request 参数,看看里面都有什么。

    $http->on('Request', function ($request, $response) {
        // .....
    
        var_dump($request);
    
        // ....
    });

    在命令行的输出中,你会看到打印的结果,内容非常多。

    object(Swoole\Http\Request)#6 (8) {
      ["fd"]=>
      int(1)
      ["header"]=>
      array(7) {
        ["host"]=>
        string(19) "192.168.56.133:9501"
        ["connection"]=>
        string(10) "keep-alive"
        ["upgrade-insecure-requests"]=>
        string(1) "1"
        ["user-agent"]=>
        string(120) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36"
        ["accept"]=>
        string(135) "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
        ["accept-encoding"]=>
        string(13) "gzip, deflate"
        ["accept-language"]=>
        string(23) "zh-CN,zh;q=0.9,en;q=0.8"
      }
      ["server"]=>
      array(10) {
        ["request_method"]=>
        string(3) "GET"
        ["request_uri"]=>
        string(1) "/"
        ["path_info"]=>
        string(1) "/"
        ["request_time"]=>
        int(1639098961)
        ["request_time_float"]=>
        float(1639098961.89757)
        ["server_protocol"]=>
        string(8) "HTTP/1.1"
        ["server_port"]=>
        int(9501)
        ["remote_port"]=>
        int(54527)
        ["remote_addr"]=>
        string(12) "192.168.56.1"
        ["master_time"]=>
        int(1639098961)
      }
      ["cookie"]=>
      NULL
      ["get"]=>
      NULL
      ["files"]=>
      NULL
      ["post"]=>
      NULL
      ["tmpfiles"]=>
      NULL
    }

    发现什么了吗?有 header、server、cookie、get、post 等内容。这些是做什么用的呢?别急,再来测试一下,你可以尝试打印一下 $_SERVER、$_REQUEST 等相关的内容。同时为了方便查看,可以给请求链接增加一个 GET 参数,比如说这样请求:http://192.168.56.133:9501/?a=1 。

    $http->on('Request', function ($request, $response) {
        // .....
    
        var_dump($request);
        var_dump($_REQUEST);
        var_dump($_SERVER);
        // ....
    });

    在你的命令行中,输出的结果应该是这样的。

    // $request 输出
    object(Swoole\Http\Request)#6 (8) {
      ["fd"]=>
      int(1)
      ["header"]=>
      array(8) {
        ["host"]=>
        string(19) "192.168.56.133:9501"
        ["connection"]=>
        string(10) "keep-alive"
        ["cache-control"]=>
        string(9) "max-age=0"
        ["upgrade-insecure-requests"]=>
        string(1) "1"
        ["user-agent"]=>
        string(120) "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.55 Safari/537.36"
        ["accept"]=>
        string(135) "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
        ["accept-encoding"]=>
        string(13) "gzip, deflate"
        ["accept-language"]=>
        string(23) "zh-CN,zh;q=0.9,en;q=0.8"
      }
      ["server"]=>
      array(11) {
        ["query_string"]=>
        string(3) "a=1"
        ["request_method"]=>
        string(3) "GET"
        ["request_uri"]=>
        string(1) "/"
        ["path_info"]=>
        string(1) "/"
        ["request_time"]=>
        int(1639099269)
        ["request_time_float"]=>
        float(1639099269.109327)
        ["server_protocol"]=>
        string(8) "HTTP/1.1"
        ["server_port"]=>
        int(9501)
        ["remote_port"]=>
        int(54864)
        ["remote_addr"]=>
        string(12) "192.168.56.1"
        ["master_time"]=>
        int(1639099269)
      }
      ["cookie"]=>
      NULL
      ["get"]=>
      array(1) {
        ["a"]=>
        string(1) "1"
      }
      ["files"]=>
      NULL
      ["post"]=>
      NULL
      ["tmpfiles"]=>
      NULL
    }
    
    // $_REQUEST 输出
    array(0) {
    }
    
    // $_SERVER 输出
    array(38) {
      ["LC_ALL"]=>
      string(11) "en_US.UTF-8"
      ["LS_COLORS"]=>
      string(1779) "rs=0:di=38;5;33:ln=38;5;51:mh=00:pi=40;38;5;11:so=38;5;13:do=38;5;5:bd=48;5;232;38;5;11:cd=48;5;232;38;5;3:or=48;5;232;38;5;9:mi=01;05;37;41:su=48;5;196;38;5;15:sg=48;5;11;38;5;16:ca=48;5;196;38;5;226:tw=48;5;10;38;5;16:ow=48;5;10;38;5;21:st=48;5;21;38;5;15:ex=38;5;40:*.tar=38;5;9:*.tgz=38;5;9:*.arc=38;5;9:*.arj=38;5;9:*.taz=38;5;9:*.lha=38;5;9:*.lz4=38;5;9:*.lzh=38;5;9:*.lzma=38;5;9:*.tlz=38;5;9:*.txz=38;5;9:*.tzo=38;5;9:*.t7z=38;5;9:*.zip=38;5;9:*.z=38;5;9:*.dz=38;5;9:*.gz=38;5;9:*.lrz=38;5;9:*.lz=38;5;9:*.lzo=38;5;9:*.xz=38;5;9:*.zst=38;5;9:*.tzst=38;5;9:*.bz2=38;5;9:*.bz=38;5;9:*.tbz=38;5;9:*.tbz2=38;5;9:*.tz=38;5;9:*.deb=38;5;9:*.rpm=38;5;9:*.jar=38;5;9:*.war=38;5;9:*.ear=38;5;9:*.sar=38;5;9:*.rar=38;5;9:*.alz=38;5;9:*.ace=38;5;9:*.zoo=38;5;9:*.cpio=38;5;9:*.7z=38;5;9:*.rz=38;5;9:*.cab=38;5;9:*.wim=38;5;9:*.swm=38;5;9:*.dwm=38;5;9:*.esd=38;5;9:*.jpg=38;5;13:*.jpeg=38;5;13:*.mjpg=38;5;13:*.mjpeg=38;5;13:*.gif=38;5;13:*.bmp=38;5;13:*.pbm=38;5;13:*.pgm=38;5;13:*.ppm=38;5;13:*.tga=38;5;13:*.xbm=38;5;13:*.xpm=38;5;13:*.tif=38;5;13:*.tiff=38;5;13:*.png=38;5;13:*.svg=38;5;13:*.svgz=38;5;13:*.mng=38;5;13:*.pcx=38;5;13:*.mov=38;5;13:*.mpg=38;5;13:*.mpeg=38;5;13:*.m2v=38;5;13:*.mkv=38;5;13:*.webm=38;5;13:*.ogm=38;5;13:*.mp4=38;5;13:*.m4v=38;5;13:*.mp4v=38;5;13:*.vob=38;5;13:*.qt=38;5;13:*.nuv=38;5;13:*.wmv=38;5;13:*.asf=38;5;13:*.rm=38;5;13:*.rmvb=38;5;13:*.flc=38;5;13:*.avi=38;5;13:*.fli=38;5;13:*.flv=38;5;13:*.gl=38;5;13:*.dl=38;5;13:*.xcf=38;5;13:*.xwd=38;5;13:*.yuv=38;5;13:*.cgm=38;5;13:*.emf=38;5;13:*.ogv=38;5;13:*.ogx=38;5;13:*.aac=38;5;45:*.au=38;5;45:*.flac=38;5;45:*.m4a=38;5;45:*.mid=38;5;45:*.midi=38;5;45:*.mka=38;5;45:*.mp3=38;5;45:*.mpc=38;5;45:*.ogg=38;5;45:*.ra=38;5;45:*.wav=38;5;45:*.oga=38;5;45:*.opus=38;5;45:*.spx=38;5;45:*.xspf=38;5;45:"
      ["SSH_CONNECTION"]=>
      string(36) "192.168.56.1 54331 192.168.56.133 22"
      ["LANG"]=>
      string(11) "zh_CN.UTF-8"
      ["HISTCONTROL"]=>
      string(10) "ignoredups"
      ["HOSTNAME"]=>
      string(21) "localhost.localdomain"
      ["XDG_SESSION_ID"]=>
      string(1) "1"
      ["USER"]=>
      string(4) "root"
      ["SELINUX_ROLE_REQUESTED"]=>
      string(0) ""
      ["PWD"]=>
      string(25) "/home/www/2.基础/source"
      ["HOME"]=>
      string(5) "/root"
      ["SSH_CLIENT"]=>
      string(21) "192.168.56.1 54331 22"
      ["SELINUX_LEVEL_REQUESTED"]=>
      string(0) ""
      ["PHP_HOME"]=>
      string(14) "/usr/local/php"
      ["SSH_TTY"]=>
      string(10) "/dev/pts/0"
      ["MAIL"]=>
      string(20) "/var/spool/mail/root"
      ["TERM"]=>
      string(14) "xterm-256color"
      ["SHELL"]=>
      string(9) "/bin/bash"
      ["SELINUX_USE_CURRENT_RANGE"]=>
      string(0) ""
      ["SHLVL"]=>
      string(1) "1"
      ["LANGUAGE"]=>
      string(11) "en_US.UTF-8"
      ["LOGNAME"]=>
      string(4) "root"
      ["DBUS_SESSION_BUS_ADDRESS"]=>
      string(25) "unix:path=/run/user/0/bus"
      ["XDG_RUNTIME_DIR"]=>
      string(11) "/run/user/0"
      ["PATH"]=>
      string(78) "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/usr/local/php/bin:/root/bin"
      ["HISTSIZE"]=>
      string(4) "1000"
      ["LESSOPEN"]=>
      string(25) "||/usr/bin/lesspipe.sh %s"
      ["OLDPWD"]=>
      string(9) "/home/www"
      ["_"]=>
      string(22) "/usr/local/php/bin/php"
      ["PHP_SELF"]=>
      string(29) "2.2Http、TCP、UDP服务.php"
      ["SCRIPT_NAME"]=>
      string(29) "2.2Http、TCP、UDP服务.php"
      ["SCRIPT_FILENAME"]=>
      string(29) "2.2Http、TCP、UDP服务.php"
      ["PATH_TRANSLATED"]=>
      string(29) "2.2Http、TCP、UDP服务.php"
      ["DOCUMENT_ROOT"]=>
      string(0) ""
      ["REQUEST_TIME_FLOAT"]=>
      float(1639099267.431116)
      ["REQUEST_TIME"]=>
      int(1639099267)
      ["argv"]=>
      array(1) {
        [0]=>
        string(29) "2.2Http、TCP、UDP服务.php"
      }
      ["argc"]=>
      int(1)
    }

    这一下看出问题所在了吗?你会发现 $_REQUEST、$_SERVER 这些之前传统 PHP 中的全局常量都无效了。虽然 \$_SERVER 也输出了内容,但是请仔细看,这里 \$_SERVER 输出的是我们的命令行信息,不是我们请求过来的信息。除了这两个之外,$_COOKIE、$_GET、$_POST、$_FILES、$_SESSION 等等都是这种情况。那么这些内容要获取的话从哪里获取呢?相信大家也都看到了,直接在 $request 参数中就有我们需要的内容。


    这一块又是一个需要我们转变思维的地方。为什么这些全局变量不能使用了呢?最主要的原因一是进程隔离问题,二是常驻进程可能会导致的内存泄漏问题。


    关于进程隔离问题,我们可以这样来测。

    $http = new Swoole\Http\Server('0.0.0.0', 9501);
    
    $i = 1;
    
    $http->set([
        'worker_num'=>2,
    ]);
    
    $http->on('Request', function ($request, $response) {
        global $i;
        $response->end($i++);
    });
    
    $http->start();

    注意我们的 $i 变量是放在监听函数外部的,它是一个针对当前 PHP 文件的全局变量。之后我们设置当前服务的 worker_num ,它的意思是启用两个 Worker 进程,其实也就是我们的工作进程。


    启动服务后可以查看当前的进程信息,可以看到有四条 php 进程,其中第一个是主进程,剩下三个是子进程,在子进程中,还有一个管理进程,最后两个就是我们创建的两个 Worker 进程。

    [root@localhost ~]# ps -ef | grep php
    root      1675  1400  0 22:19 pts/0    00:00:00 php 2.2Http、TCP、UDP服务.php
    root      1676  1675  0 22:19 pts/0    00:00:00 php 2.2Http、TCP、UDP服务.php
    root      1678  1676  0 22:19 pts/0    00:00:00 php 2.2Http、TCP、UDP服务.php
    root      1679  1676  0 22:19 pts/0    00:00:00 php 2.2Http、TCP、UDP服务.php

    接下来,开两个不同的浏览器访问吧,看看 $i 的输出会怎么样。是不是两个浏览器刷新的时候 $i 没有同步地增加呀,体会一下多进程的效果吧。


    另一方面运行起来的程序是完全一次性加载到内存当中的,所以这些全局变量不会自动销毁,我们的程序毕竟是在一直运行的。因此,如果稍加不注意,就会出现内存泄露的问题。


    综上所述,global 声明的变量、static 声明的静态变量、静态函数、PHP 原生的超全局变量都有非常大的风险,Swoole 直接干掉了默认的超全局变量,而我们如果要使用全局变量的话也有其它的处理方式。这个我们以后再说。

    TCP

    对于 Http 服务我们又进行了一次复习,并且通过这个 Http 服务我们还看到了多进程程序的特点以及在开发时需要转变的一个重大的思维。当然,这些东西我们在后面会经常接触到。接下来,大家一起继续学习了解一下使用 Swoole 来搭建一个 TCP 服务端。


    只要是学习过一点网络相关知识的同学肯定都知道,我们的 Http 服务本身就是建立在 TCP 的基础之上的。因此,其实要建立 TCP 服务的基本步骤和 Http 服务是没啥差别的。最主要的就是监听的内容不同。

    //创建Server对象,监听 9501 端口
    $server = new Swoole\Server('0.0.0.0', 9501);
    
    //监听连接进入事件
    $server->on('Connect', function ($server, $fd) {
        echo "Client: Connect.\n";
    });
    
    //监听数据接收事件
    $server->on('Receive', function ($server, $fd, $reactor_id, $data) {
        $server->send($fd, "Server: {$data}");
    });
    
    //监听连接关闭事件
    $server->on('Close', function ($server, $fd) {
        echo "Client: Close.\n";
    });
    
    //启动服务器
    $server->start();

    相比原生的 PHP 的 socket 函数来说,Swoole 是不是清晰方便很多。我们启动服务之后,使用 telnet 命令行就可以对这个 TCP 服务器进行测试了。

    ➜  ~ telnet 192.168.56.133 9501
    Trying 192.168.56.133...
    Connected to 192.168.56.133.
    Escape character is '^]'.
    Hello
    Server: Hello
    App
    Server: App
    ^]
    telnet> quit
    Connection closed.

    Swoole 中还有对应的 TCP 和 UDP 客户端,这个我们后面再说。

    UDP

    UDP 和 TCP 的区别相信不用我多说了吧,这玩意不建立可靠连接的,但是速度快,所以现在的各种视频直播之类的应用全是建立在 UDP 之上的。可以说是支撑当前短视频和直播时代的基石了。

    $server = new Swoole\Server('0.0.0.0', 9501, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);
    
    //监听数据接收事件
    $server->on('Packet', function ($server, $data, $clientInfo) {
        var_dump($clientInfo);
        $server->sendto($clientInfo['address'], $clientInfo['port'], "Server:{$data}");
    });
    
    //启动服务器
    $server->start();

    写法也基本都是类似的,不同的还是监听的内容不同。由于它不建立连接,所以我们只需要监听接收到的数据包信息就可以了。

    ➜  ~ nc -vuz 192.168.56.133 9501
    Connection to 192.168.56.133 port 9501 [udp/\*] succeeded!
    
    ➜  ~ nc -vu 192.168.56.133 9501
    Connection to 192.168.56.133 port 9501 [udp/*] succeeded!
    Server:XServer:XServer:XServer:Xall
    Server:all
    ^C

    对于命令行的测试来说,我们也不能使用 telnet 了,在这里,我使用的也是 Linux 环境中比较常见的 nc 命令来进行测试的。

    总结

    今天我们就是简单地先看一下在整个 Swoole 中,Http、TCP、UDP 服务是如何跑起来的,另外也尝试了一下多进程对于全局变量的影响。其实要学习 Swoole ,就不可避免地要学习到很多计算机相关的基础知识,如果你还没有这方面的准备的话,可以先看看操作系统、计算机组成原理相关的内容。毕竟我也不会讲得太详细,也达不到来讲这些基础理论知识的水平。所以,有相关的内容我也只能尽已所能地去稍带地提出,毕竟我自己也还是在不断学习这些基础的过程之中的。


    测试代码:


    https://github.com/zhangyue0503/swoole/blob/main/2.基础/source/2.2Http、TCP、UDP服务.php


    参考文档:


    https://wiki.swoole.com/#/start/start_tcp_server


    https://wiki.swoole.com/#/start/start_udp_server

    视频链接

    微信文章地址:https://mp.weixin.qq.com/s/dOZUhcdeR-_lFCAwom_ptA

    B站视频地址:https://www.bilibili.com/video/BV13d4y1V76n

    微信视频地址:https://mp.weixin.qq.com/s/TIZ8WuVs40YKvue6Kn4yXw

    搜索
    关注