本文包含TCP,UDP,HTTPWebSocket服务的搭建,以及Task异步任务.在面试中也有些面试官喜欢TCPUDP的区别.本文也列举了几个区别.多学点理论知识也是不亏的.

TCP/UDP的区别

  首先他们都是传输层的协议.

TCPUDP
速度慢(效率低)快(效率高)
链接面向连接面向非链接
资源消耗较多较少
传输方式字节流报文
传输可靠性可靠不可靠
拥塞控制慢开始,避免拥塞,快重出,快恢复
应用场景少量数据大量数据

TCP 服务

服务端

//创建Server对象,监听 127.0.0.1:9501端口
$serv = new swoole_server("127.0.0.1", 9501);

$serv->set([
    'worker_num' => 4,  #worker进程数 建议cpu的1-4倍
    'max_request' => 1000, #表示worker进程在处理完n次请求后结束运行。manager会重新创建一个worker进程。此选项用来防止worker进程内存溢出。
]);



//监听连接进入事件
/*
 * $fd 客户端链接的唯一标示
 * $reactor_id  线程id 官方文档是没有的
 * */

$serv->on('connect', function ($serv, $fd, $reactor_id) {
    echo "form_id:{$reactor_id}-Client:{$fd} Connect.\n";
});

//监听数据接收事件
/*
 *$from_id是官方文档给的线程id。当然也可以和上面统一成一致的
 * */
$serv->on('receive', function ($serv, $fd, $from_id, $data) {
    $serv->send($fd, "from——id: $from_id-Client:{$fd}-Server: " . $data);
});

//监听连接关闭事件
$serv->on('close', function ($serv, $fd) {
    echo "Client: Close.\n";
});

//启动服务器
$serv->start();

客户端

//SWOOLE_SOCK_TCP是swoole内置常量。指通过tcp链接
//*
//swoole_client::__construct
//swoole_client->__construct(int $sock_type, int $is_sync = SWOOLE_SOCK_SYNC, string $key);
//$sock_type表示socket的类型,如TCP/UDP
//使用$sock_type | SWOOLE_SSL可以启用SSL加密
//$is_sync表示同步阻塞还是异步非阻塞,默认为同步阻塞
//$key用于长连接的Key,默认使用IP:PORT作为key。相同key的连接会被复用
//*/
$client = new swoole_client(SWOOLE_SOCK_TCP);

/*
 * swoole_client->connect接到远程服务器,函数原型
 *$swoole_client->connect(string $host, int $port, float $timeout = 0.5, int $flag = 0)
 * onnect方法接受4个参数:
 * $host是远程服务器的地址,1.10.0或更高版本已支持自动异步解析域名,$host可直接传入域名
 * $port是远程服务器端口
 * $timeout是网络IO的超时,包括connect/send/recv,单位是s,支持浮点数。默认为0.5s,即500ms
 * $flag参数在UDP类型时表示是否启用udp_connect 设定此选项后将绑定$host与$port,此UDP将会丢弃非指定host/port的数据包。
 * $flag参数在TCP类型,$flag=1表示设置为非阻塞socket,connect会立即返回。如果将$flag设置为1,那么在send/recv前必须使用swoole_client_select来检测是否完成了连接
 * */
$connect = $client->connect('127.0.0.1',9501);

if(!$connect){
    echo "connect failed error {$client->errCode}";
}

//让用户输入数据
fwrite(STDOUT,'请输入手机号');
//获取用户输入的数据
$msg = fgets(STDIN);

//将用户输入的数据发送给tcp服务端
//触发server端on函数的receive方法
$client->send($msg);

//接受来组server的数据
//来时server端on函数的receive方法的返回值
$ser_msg = $client->recv();
echo $ser_msg;

启动服务

# 先下载telnet和lsof包
yum install -y telnet lsof
# 启动服务
php tcp/server.php
# 如果报下面的错,说明端口被占用
WARNING    swSocket_bind(:426): bind(127.0.0.1:9501) failed, Error: Address already in use[98]
# 查看端口占用情况也可以用netstat -anp | grep 9501
lsof -i:9501
COMMAND   PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
php     62153 root    3u  IPv4 136767      0t0  TCP localhost:9501 (LISTEN)
# 杀死进程62153是查询的pid
kill -9 62153
# 再次启动服务
php tcp/server.php
# 没有报错说明成功~

检测服务

-- client.php 检测

# 执行client.php文件
php tcp/client.php
# 提示输入手机号,瞎写一个
请输入手机号15012345678
# 客户端返回
from——id: 0-Client:2-Server: 15012345678
# 服务端返回
form_id:0-Client:3 Connect.
Client: Close.
# ok,没问题!

-- telnet 命令检测

# 打开第二个终端(客户端),输入下面内容
telnet 127.0.0.1 9501
# 返回
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
# 说明连接成功,查看终端1(服务端)
form_id:0-Client:1 Connect.
# 这是代码中connect的回掉输出的内容.

# 在终端2(客户端)输入你好
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
'你好,QvBiLam'
# 会立刻返回服务端中receive的回调send的内容
from——id: 0-Client:2-Server: '你好,QvBiLam'
# 退出talnet.按住control + ] 中括号.
# 再回车进入telnet输入
quit
# ok,没问题!

UDP 服务

服务端

$server = new swoole_server("127.0.0.1", 9502, SWOOLE_PROCESS, SWOOLE_SOCK_UDP);

//监听数据接受事件
/*
 * UDP服务器与TCP服务器不同,UDP没有连接的概念.
 * 启动Server后,客户端无需Connect.
 * 直接可以向Server监听的9502端口发送数据包.
 * 对应的事件为onPacket。
 * $clientInfo是客户端的相关信息,是一个数组,有客户端的IP和端口等内容
 * 调用 $server->sendto 方法向客户端发送数据
 * */
$server->on('Packet', function ($server, $data, $clientInfo) {
    $server->sendto($clientInfo['address'], $clientInfo['port'], "data:" . $data);
    var_dump($clientInfo);
});

//启动服务
$server->start();

客户端

$client = new swoole_client(SWOOLE_SOCK_UDP);
// 链接UDP服务端
$connect = $client->connect('127.0.0.1',9502);
// 判断链接是否成功
if(!$connect){
    echo 'connect failed error';
}
// 向服务端发送消息
$client->send('hello!');
// 获取服务端返回消息
$msg = $client->recv();
// 输出服务端返回消息
echo $msg;

启动服务

php udp/server.php

检测服务

-- client.php 检测

# 端口1 启动服务
php udp/server.php
# 端口2 执行客户端
php udp/client.php
# 端口1 返回内容
array(4) {
  ["server_socket"]=>
  int(3)
  ["server_port"]=>
  int(9502)
  ["address"]=>
  string(9) "127.0.0.1"
  ["port"]=>
  int(41543)
}
# 端口2 返回内容
data:hello!
# ok,没问题!

-- netcat 命令检测

# 下载netcat
yum install -y nc
# 链接UDP服务器
nc -u 127.0.0.1 9502
# 键入hello , Qvbilam.服务端返回内容
data:hel, Qvbilam
# 终端1 显示
array(4) {
  ["server_socket"]=>
  int(3)
  ["server_port"]=>
  int(9502)
  ["address"]=>
  string(9) "127.0.0.1"
  ["port"]=>
  int(46331)
}
# ok,没问题!

HTTP 服务

服务端

$server = new Swoole\Http\Server("0.0.0.0",9503);
// 设置静态资源目录
/*
 * enable_static_handel开启静态文件请求处理功能,需配合document_root使用.
 * document_root配置静态文件根目录,与enable_static_handler配合使用。
 * 注意:设置了set后将不会走on方法
 * */
$server->set([
    'enable_static_handler' => true,
    'document_root' => "/data/wwwroot/test"
]);

/*
 * end向客户端浏览器发送HTML内容,只能调用一次.
 * 如果需要分多次向客户端发送数据,请使用write方法
 * */
$server->on('request', function($request,$response) {
    $response->end("<h1>hello</h1>");
});

$server->start();

启动服务

php http/server.php

检测服务

-- 没有设置set方法的测试

# 请求http服务
curl http://127.0.0.1:9503
# 返回on方法end的内容
<h1>hello</h1>

-- 开启set方法的测试
curl 127.0.0.1:9503/
# 返回
<h1>hello</h1>
# 可以看到还是走的end方法.访问test目录下的文件
curl 127.0.0.1:9503/index.html
# 返回
<h1>QvBiLam </h1>  
# 访问不存在的文件
curl 127.0.0.1:9503/index.html123
# 返回
<h1>hello</h1>

总结

  1. 设置了set,如果访问存在的文件就会不走end方法.
  2. 设置了set,如果访问不存在的文件就会走end方法.

WebSocket

服务端

$server = new Swoole\WebSocket\Server("0.0.0.0", 9500);

//监听websocket链接事件。调用onOepen函数
$server->on('open', 'onOpen');

function onOpen($server, $request)
{
    print_r('connet ' . $request->fd);
}

//监听websocket消息事件
$server->on('message', function (Swoole\WebSocket\Server $server, $frame) {
    echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
//    想客户端发送数据
    $server->push($frame->fd, "get message success:{$frame->data}");
});

$server->on('close', function ($ser, $fd) {
    echo "client {$fd} closed\n";
});

$server->start();

客户端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>QvBiLam</title>
</head>
<body>
    <h1>hello QvBiLam</h1>
</body>
</html>
<script>
    var WsUrl = "ws://127.0.0.1:9502"
    var websocket = new WebSocket(WsUrl);

    //链接websock服务
    websocket.onopen = function(evt){
        console.log("connet success")
        //向服务端发送消息hhhh
        websocket.send('hhhhhh');
    }

    //接受服务端消息
    websocket.onmessage = function(evt){
        //获取服务端传来的数据push
        console.log(evt.data)
    }

    websocket.onclose = function(evt){
        console.log("cloes")
    }

    websocket.onerror = function(evt,e){
        console.log(evt.data)
    }

</script>

测试结果

  客户端显示

  服务端显示

面向对象优化

class ws_oop
{
    CONST HOST = '0.0.0.0';
    CONST PORT = 9502;
    public $ws = null;
    public $timer_id = null;

    public function __construct()
    {
        $this->ws = new Swoole\WebSocket\Server(self::HOST, self::PORT);
        $this->ws->set([
            'work_num' => 2,
            'task_worker_num' => 2
        ]);
        $this->ws->on('open', [$this, "onOpen"]);
        $this->ws->on('message', [$this, "onMessage"]);
        $this->ws->on('close', [$this, "onClose"]);
        $this->ws->on('task', [$this, 'onTask']);
        $this->ws->on('finish', [$this, 'onFinish']);
        $this->ws->start();
    }

    /*
     *监听链接事件
     * */
    public function onOpen($ws, $request)
    {
        var_dump($request->fd);
        $ws->push($request->fd,'欢迎链接,' . $request->fd);
        Swoole_timer_tick(1000,function($timer) use($ws,$request){
            $ws->push($request->fd,'现在开始进行攻击,伤害:100');
            $this->timer_id = $timer;
        });
        $timer = $this->timer_id;
        Swoole_timer_after(5000,function() use($ws,$request,$timer){
            Swoole_timer_clear($timer);
            $ws->push($request->fd,'攻击完成!成功击毙敌人');
        });
        $ws->push($request->fd,'发现敌人,准备进行攻击');
    }

    public function onMessage($ws, $farme)
    {
        echo 'client-push-message :' . $farme->data . "\n";
        $data = [
            'task' => 1,
            'fd' => $farme->fd
        ];
//        $ws->task($data);
        $ws->push($farme->fd, "server-push:" . $farme->data . time());
    }

    public function onClose($ws, $fd)
    {
        echo "close : {$fd}\n";
    }
}

new ws_oop();

Task

启用Task

  至于是个什么我也说不明白,咱啥也不知道,咱也不敢乱说.实践起来很好用,咱就用.执行一些比较耗时的错做.例如群发邮件,聊天广播等.我们就可以用到Task异步任务.

 通过Websocket服务的OOP代码修改.

public function onMessage($ws, $farme)
    {
        echo 'client-push-message :' . $farme->data . "\n";
        $data = [
            'task' => 1,
            'fd' => $farme->fd
        ];
//        $ws->task($data);
        $ws->push($farme->fd, "server-push:" . $farme->data . time());
    }

    /*
        * 当前的Task进程在调用onTask回调函数时会将进程状态切换为忙碌,这时将不再接收新的Task,当onTask函数返回时会将进程状态切换为空闲然后继续接收新的Task。
        * $src_worker_id来自于哪个worker进程
        * $data 是任务的内容(就是task($data)的$data)
    */
    public function onTask($sever, $tastId, $workId, $data)
    {
        print_r($data);
        sleep(10);
        return "on task finish";
    }

    //    执行成功后的回调函数$data为onTask返回的值
    /*
     * 当worker进程投递的任务在task_worker中完成时,task进程会通过swoole_server->finish()方法将任务处理的结果发送给worker进程。
     * $task_id是任务的ID
     * $data是任务处理的结果内容
     * task进程的onTask事件中没有调用finish方法或者return结果,worker进程不会触发onFinish
     * 执行onFinish逻辑的worker进程与下发task任务的worker进程是同一个进程
    */
    public function onFinish($server, $taskId, $data)
    {
        echo "taskId:{$taskId}\n";
        echo "finish-data-success:{$data}\n";
    }

解释:

当客户端向服务端发送消息会触发onMessage()然后走到onTask()

onTask()中有个非常耗时的操作,sleep(10)等待10s.

onTask()执行成功会调用onFinish()

Task是异步任务,会同时进行的~

执行结果

按照正常的PHP的执行顺序应该是

  1. 停10s
  2. 输出client-push-message
  3. 输出数组$data
  4. 输出on task finish
  5. 输出taskIdfinish-data-success

  10s后的输出结果

应用Task后的执行结果

  1. 输出client-push-message
  2. 输出数组$data
  3. 停10s
  4. 输出on task finish
  5. 输出taskIdfinish-data-success

4.gif

Last modification:February 18th, 2020 at 10:22 pm