swoole快速上手

第一 安装

第二 配置

第三 快速使用

1. 创建tcp服务器

swoole_server是异步服务器,所以是通过监听事件的方式来编写程序的。当对应的事件发生时底层会主动回调指定的PHP函数。如当有新的TCP连接进入时会执行onConnect事件回调,当某个连接向服务器发送数据时会回调onReceive函数。

  • 服务器可以同时被成千上万个客户端连接,$fd就是客户端连接的唯一标识符
  • 调用 $server->send() 方法向客户端连接发送数据,参数就是$fd客户端标识符
  • 调用 $server->close() 方法可以强制关闭某个客户端连接
  • 客户端可能会主动断开连接,此时会触发onClose事件回调
/*
 * $host domain
 * $port listener port
 * $mode : swoole_process 多进程方式
 * $socket_type: swoole_socket_tcp
 */
$serv = new swoole_server('0.0.0.0', 9501);

$serv->set([
	'worker_num' => 4, //worker 进程数 cpu的1-4倍,可以采用 ps -aft|grep php 来查看
	'max_request' => 10000,
]);

$serv->manager_pid;  //管理进程的PID,通过向管理进程发送SIGUSR1信号可实现柔性重启
$serv->master_pid;  //主进程的PID,通过向主进程发送SIGTERM信号可安全关闭服务器
$serv->connections; //当前服务器的客户端连接,可使用foreach遍历所有连接
/*
 * $event;
 * connect: 当建立连接时候 $serv $fd
 * receive:当连接收到数据时候
 * close: 关闭连接
 */
$serv->on("connect", function($serv, $fd){
    echo "建立连接\n";
//    var_dump($serv);
//    var_dump($fd);
});
 
$serv->on("receive", function($serv, $fd, $from_id, $data){
    echo "接收到数据\n";
    var_dump($data);
});

$serv->on("close", function($serv, $fd){
    echo "连接关闭";
});

$serv->start();

在这里插入图片描述

2. 创建udp服务器

UDP服务器与TCP服务器不同,UDP没有连接的概念。启动Server后,客户端无需Connect,直接可以向Server监听的9502端口发送数据包。对应的事件为onPacket

$swoole_type = SWOOLE_PROCESS;//多进程模式
$swoole_udp = SWOOLE_SOCK_UDP;
$serv = new swoole_server("0.0.0.0", 9502, $swoole_type, $swoole_udp);
/*
 * $server
 * $data 接收到的数据
 * $fd:客户端信息
 */
$serv->on("packet", function($serv, $data,$fd){
    //发送数据到相应的客户端,反馈信息
    $data = "server: $data";
    $server_socket = -1;
    $serv->sendto($fd["address"], $fd["port"], $data, $server_socket);
    var_dump($fd);
});
$serv->start();
```![在这里插入图片描述](https://img-blog.csdnimg.cn/20190505014828844.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzI5Njc3ODY3,size_16,color_FFFFFF,t_70)

3. 创建web服务器

Http服务器只需要关注请求响应即可,所以只需要监听一个onRequest事件

$host = "http://127.0.0.1";
$serv = new swoole_http_server($host, 9501);
/*
 * $request: 请求信息
 * $response: 返回信息
 */
$serv -> on("request", function($request, $response){
    var_dump($request);
    $response->header("Conteny-Type", "text/html; charset=utf-8");//设置响应头
    $response->end("hello world".rand(100,999)); //发送信息
});
$serv->start();

在这里插入图片描述

4. 创建WebSocket服务器

4.1 什么是WebSocket?

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信一允许服务器主动发送信息给客户端。缺陷:HTTP的通信只能由客户端发起

4.2 WebSocket特点:

  • 建立在TCP协议之上
  • 性能开销小通信高效
  • 客户端可以与任意服务器通信
  • 协议标识符ws wss
  • 持久化网络通信协议

4.3 WebSocket 工作流程

WebSocket服务器是建立在Http服务之上的长连接服务,客户端首先会发送一个Http的请求与服务器进行握手。握手成功后会触发onOpen事件,表示连接已就绪,onOpen函数中可以得到$request对象,包含了Http握手的相关信息,如GET参数、Cookie、Http头信息等。建立连接后客户端与服务器端就可以双向通信了。

  • 客户端向服务器端发送信息时,服务器端触发onMessage事件回调
  • 服务器端可以调用$server->push()向某个客户端(使用$fd标识符)发送消息
  • 服务器端可以设置onHandShake事件回调来手工处理WebSocket握手
  • swoole_http_server是swoole_server的子类,内置了Http的支持
  • swoole_websocket_server是swoole_http_server的子类, 内置了WebSocket的支持
//创建 websocket服务器
$ws = new swoole_websocket_server("0.0.0.0", 9501);
//open 建立连接 $ws 服务器,$request:客户信息
$ws->on("open", function($ws, $request){
    var_dump($request);
    $ws->push($request->fd,"welcome \n");
});
//message 接收信息
$ws->on("message", function($ws, $request){
   echo "receive from {$request->fd} : {$request->data}, opcode:{$request->opcode},fin:{$request->finish}\n";
    $ws->push($request->fd, "get it message");
});
$ws->on("close", function($ws, $fd){
    echo "client {$fd} closed\n";
});
$ws->start();
<html lang="en">
<head>
    <title>Title</title>
</head>
<script>
    var webServer = "ws://127.0.0.1:9501";
    var webSocket = new WebSocket(webServer);
    webSocket.onopen = function(evt){
        console.log("连接成功!")
    }
    webSocket.onclose = function (evt) {
        console.log("关闭!")
    }
    webSocket.onmessage = function (evt) {
        console.log(evt.data);
    }
    webSocket.onerror = function (evt, e) {
        console.log("error")
    }
</script>
</html>
  • 不能直接使用swoole_client与websocket服务器通信,swoole_client是TCP客户端
  • 必须实现WebSocket协议才能和WebSocket服务器通信,可以使用swoole/framework提供的PHP WebSocket客户端
  • WebSocket服务器除了提供WebSocket功能之外,实际上也可以处理Http长连接。只需要增加onRequest事件监听即可实现Comet方案Http长轮询。

4.4 长连接的关闭

Swoole内置了心跳检测功能,能自动close掉长时间没有数据来往的连接。而开启心跳检测功能,只需要设置heartbeat_check_interval和heartbeat_idle_time即可。如下:

$this->serv->set(
    array(
        'heartbeat_check_interval' => 60,
        'heartbeat_idle_time' => 600,
    )
);

Swoole 异步操作

5. 定时器

swoole提供了类似JavaScript的setInterval/setTimeout异步高精度定时器,粒度为毫秒级

//循环触发
$tickId = swoole_timer_tick(2000, function($timer_id){
    echo "执行 $timer_id \n";
});
//n秒后触发
$afterId = swoole_timer_after(3000, function(){
    echo "3000 后执行\n";
});
//清除定时器
swoole_timer_clear($afterId);

在这里插入图片描述

6. 异步tcp服务器

在Server程序中如果需要执行很耗时的操作,比如一个聊天服务器发送广播,Web服务器中发送邮件。如果直接去执行这些函数就会阻塞当前进程,导致服务器响应变慢。Swoole提供了异步任务处理的功能,可以投递一个异步任务到TaskWorker进程池中执行,不影响当前请求的处理速度。

//创建tcp服务器
$serv = new swoole_server("0.0.0.0", 9501);
//设置异步 进程工作数量
$serv->set(array("task_worker_num"=>4));
//投递异步任务
$serv->on("receive", function($serv, $fd, $from_id, $data){
   $task_id = $serv->task($data);
   echo "异步ID:$task_id\n";
});
//处理异步任务
$serv->on("task", function($serv, $task_id, $from_id, $data){
   echo "执行 异步ID: $task_id";
   $serv->finish("$data ->OK");
});
//处理结果
$serv->on("finish", function ($serv, $task_id, $data){
   echo "执行完成";
});
$serv->start();

在这里插入图片描述

7. 创建同步TCP客户端

这个客户端是同步阻塞的,connect/send/recv 会等待IO完成后再返回。同步阻塞操作并不消耗CPU资源,IO操作未完成当前进程会自动转入sleep模式,当IO完成后操作系统会唤醒当前进程,继续向下执行代码。

  • TCP需要进行3次握手,所以connect至少需要3次网络传输过程
  • 在发送少量数据时$client->send都是可以立即返回的。发送大量数据时,socket缓存区可能会塞满,send操作会阻塞。
  • recv操作会阻塞等待服务器返回数据,recv耗时等于服务器处理时间+网络传输耗时之和。
$client = new swoole_client(SWOOLE_SOCK_TCP);
//连接到服务器
if (!$client->connect('127.0.0.1', 9501, 0.5)){
    die("connect failed.");
}
//php常量
fwrite(STDOUT, '请输入消息:')
$msg = trim(fgets(STDIN));

//向服务器发送数据
if (!$client->send($msg)){
    die("send failed.");
}
//从服务器接收数据
$data = $client->recv();
if (!$data){
    die("recv failed.");
}
echo $data;
//关闭连接
$client->close();

TCP通信过程
Tcp通信过程

8. 创建异步TCP客户端

//创建异步tcp客户端
$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);
//注册连接成功的回调
$client->on("connect", function($cli){
    $cli->send("hello world \n");
});
//注册数据接收回调$cli,服务端信息$data 数据
$client->on("receive", function($cli, $data){
    echo "data: $data";
});
//注册连接失败回调
$client->on("error", function ($cli){
    echo "faild \n";
});
//注册关闭函数回调
$client->on("close", function ($cli){
   echo "close \n";
});
//发起连接
$client->connect("127.0.0.1", 8080, 10);

异步客户端与上一个同步TCP客户端不同,异步客户端是非阻塞的。可以用于编写高并发的程序。swoole官方提供的redis-asyncmysql-async都是基于异步swoole_client实现的。

异步客户端需要设置回调函数,有4个事件回调必须设置onConnectonErroronReceiveonClose。分别在客户端连接成功、连接失败、收到数据、连接关闭时触发。

$client->connect() 发起连接的操作会立即返回,不存在任何等待。当对应的IO事件完成后,swoole底层会自动调用设置好的回调函数。

9 网络通信协议设计

  1. TCP协议在底层机制上解决了UDP协议的顺序和丢包重传问题。但相比UDP又带来了新的问题,TCP协议是流式的,数据包没有边界。应用程序使用TCP通信就会面临这些难题。因为TCP通信是流式的,在接收1个大数据包时,可能会被拆分成多个数据包发送。多次Send底层也可能会合并成一次进行发送。这里就需要2个操作来解决:分包(Server收到了多个数据包,需要拆分数据包);合包(Server收到的数据只是包的一部分,需要缓存数据,合并成完整的包)。所以TCP网络通信时需要设定通信协议。常见的TCP网络通信协有HTTP/HTTPS/FTP/SMTP/POP3/IMAP/SSH/Redis/Memcache/MySQL 。如果要设计一个通用协议的Server,那么就要按照通用协议的标准去处理网络数据。除了通用协议外还可以自定义协议

  2. EOF结束符协议:EOF协议处理的原理是每个数据包结尾加一串特殊字符表示包已结束。如memcacheftpstmp都使用\r\n作为结束符。发送数据时只需要在包末尾增加\r\n即可。使用EOF协议处理,一定要确保数据包中间不会出现EOF,否则会造成分包错误。

    //在`swoole_server`和`swoole_client`的代码中只需要设置2个参数就可以使用EOF协议处理。
    $server->set(array(
        'open_eof_split' => true,
        'package_eof' => "\r\n",
    ));
    $client->set(array(
        'open_eof_split' => true,
        'package_eof' => "\r\n",
    ));
    
    
  3. 固定包头+包体协议:这种协议的特点是一个数据包总是由包头+包体2部分组成。包头由一个字段指定了包体或整个包的长度,长度一般是使用2字节/4字节整数来表示。服务器收到包头后,可以根据长度值来精确控制需要再接收多少数据就是完整的数据包

    //Swoole的Server和异步Client都是在onReceive回调函数中处理数据包,当设置了协议处理后,只有收到一个完整数据包时才会触发onReceive事件。同步客户端在设置了协议处理后,调用 $client->recv() 不再需要传入长度,recv函数在收到完整数据包或发生错误后返回。
    $server->set(array(
        'open_length_check' => true,
        'package_max_length' => 81920,
        'package_length_type' => 'n', //see php pack()
        'package_length_offset' => 0,
        'package_body_offset' => 2,
    ));
    

10. 使用异步客户端

PHP提供的MySQLCURLRedis 等客户端是同步的,会导致服务器程序发生阻塞。Swoole提供了常用的异步客户端组件,来解决此问题。编写纯异步服务器程序时,可以使用这些异步客户端。

异步客户端可以配合使用SplQueue实现连接池,以达到长连接复用的目的。在实际项目中可以使用PHP提供的Yield/Generator语法实现半协程的异步框架。也可以基于Promises简化异步程序的编写。

  1. mysql

    $db = new Swoole\MySQL;
    $server = array(
        'host' => '127.0.0.1',
        'user' => 'test',
        'password' => 'test',
        'database' => 'test',
    );
    $db->connect($server, function ($db, $result) {
        $db->query("show tables", function (Swoole\MySQL $db, $result) {
            var_dump($result);
            $db->close();
        });
    });
    

    mysqliPDO等客户端不同,Swoole\MySQL是异步非阻塞的,连接服务器、执行SQL时,需要传入一个回调数。connect的结果不在返回值中,而是在回调函数中。query的结果也需要在回调函数中进行处理。

  2. Redis

    $redis = new Swoole\Redis;
    $redis->connect('127.0.0.1', 6379, function ($redis, $result) {
        $redis->set('test_key', 'value', function ($redis, $result) {
            $redis->get('test_key', function ($redis, $result) {
                var_dump($result);
            });
        });
    });
    
    

    Swoole\Redis需要Swoole编译安装hiredis,详细文档参见异步Redis客户端

  3. Http

    $cli = new Swoole\Http\Client('127.0.0.1', 80);
    $cli->setHeaders(array('User-Agent' => 'swoole-http-client'));
    $cli->setCookies(array('test' => 'value'));
    
    $cli->post('/dump.php', array("test" => 'abc'), function ($cli) {
        var_dump($cli->body);
        $cli->get('/index.php', function ($cli) {
            var_dump($cli->cookies);
            var_dump($cli->headers);
        });
    });
    

    Swoole\Http\Client的作用与CURL完全一致,它完整实现了Http客户端的相关功能。具体请参考 HttpClient文档

  4. 其他客户端
    Swoole底层目前只提供了最常用的MySQLRedisHttp异步客户端,如果你的应用程序中需要实现其他协议客户端,如KafkaAMQP等协议,可以基于Swoole\Client异步TCP客户端,开发相关协议解析代码,来自行实现。

11. 多进程共享数据

由于PHP语言不支持多线程,因此Swoole使用多进程模式。在多进程模式下存在进程内存隔离,在工作进程内修改global全局变量和超全局变量时,在其他进程是无效的。设置worker_num=1时,不存在进程隔离,可以使用全局变量保存数据

  1. 进程隔离进程隔离

    $fds = array();
    $server->on('connect', function ($server, $fd){
        echo "connection open: {$fd}\n";
        global $fds;
        $fds[] = $fd;
        var_dump($fds);
    });
    

    $fds 虽然是全局变量,但只在当前的进程内有效。Swoole服务器底层会创建多个Worker进程,在var_dump($fds)打印出来的值,只有部分连接的fd

    对应的解决方案就是使用外部存储服务:数据库,如:MySQLMongoDB;缓存服务器,如:RedisMemcache;磁盘文件,多进程并发读写时需要加锁

    普通的数据库和磁盘文件操作,存在较多IO等待时间。因此推荐使用:Redis 内存数据库,读写速度非常快;/dev/shm内存文件系统,读写操作全部在内存中完成,无IO`消耗,性能极高

  2. 共享内存
    PHP提供了多套共享内存的扩展,但实际上真正在实际项目中可用的并不多。

    • shm扩展(不推荐),提供了shm_put_var/shm_get_var共享内存读写方法。但其底层实现使用链表结构,在保存大量数值时时间复杂度为O(N),性能非常差。并且读写数据没有加锁,存在数据同步问题,需要使用者自行加锁。
    • shmop扩展(不推荐),提供了shmop_read/shmop_write共享内存读写方法。仅提供了基础的共享内存操作指令,并未提供数据结构和封装。不适合普通开发者使用。
    • apcu扩展,提供了apc_fetch/apc_store可以使用Key-Value方式访问。APC扩展总体上是可以用于实际项目的,缺点是锁的粒度较粗,在大量并发读写操作时锁的碰撞较为密集。(yac扩展,不适合用于保存数据,其设计原理导致存在一定的数据miss率,仅作为缓存,不可作为存储)
    • swoole/table扩展,Swoole官方提供的共享内存读写工具,提供了Key-Value操作方式,使用非常简单。底层使用自旋锁实现,在大量并发读写操作时性能依然非常强劲。推荐使用。Table仍然存在两个缺点(提前申请内存,Table在使用前就需要分配好内存,可能会占用较多内存;无法动态扩容,Table内存管理是静态的,不支持动态申请新内存,因此一个Table在设置好行数并创建之后,使用时不能超过限制),使用时需要根据实际情况来选择。

12. 使用协程客户端(version>=4.2.5, 协程编程须知)

在最新的4.x版本中,协程取代了异步回调,作为我们推荐使用的编程方式。协程解决了异步回调编程困难的问题。使用协程可以以传统同步编程的方法编写代码,底层自动切换为异步IO,既保证了编程的简单性,又可借助异步IO,提升系统的并发能力。

  1. 协程实例

    $http = new swoole_http_server("0.0.0.0", 9501);
    $http->on('request', function ($request, $response) {
        $db = new Swoole\Coroutine\MySQL();
        $db->connect([
            'host' => '127.0.0.1',
            'port' => 3306,
            'user' => 'user',
            'password' => 'pass',
            'database' => 'test',
        ]);
        $data = $db->query('select * from test_table');
        $response->end(json_encode($data));
    });
    $http->start();
    
    

    上面的代码编写与同步阻塞模式的程序完全一致的。但是底层自动进行了协程切换处理,变为异步IO。因此:服务器可以应对大量并发,每个请求都会创建一个新的协程,执行对应的代码;某些请求处理较慢时,只会引起这一个请求被挂起,不影响其他请求的处理

  2. 其他协程组件
    Swoole4扩展提供了丰富的协程组件,如RedisTCP/UDP/Unix客户端、Http/WebSocket/Http2`客户端,使用这些组件可以很方便地实现高性能的并发编程。使用协程时请认真阅读 协程编程须知,避免发生错误。

  3. 适用场景

    • 高并发服务,如秒杀系统、高性能API接口、RPC服务器,使用协程模式,服务的容错率会大大增加,某些接口出现故障时,不会导致整个服务崩溃
    • 爬虫,可实现非常巨大的并发能力,即使是非常慢速的网络环境,也可以高效地利用带宽
    • 即时通信服务,如IM聊天、游戏服务器、物联网、消息服务器等等,可以确保消息通信完全无阻塞,每个消息包均可即时地被处理

13. 进程创建

//创建进程对应的执行函数
function doProcess(swoole_process $worker){
    echo "PID".$worker->pid."\n";
    sleep(10);
}
/*
 * 创建一个进程
 * $function 子进程创建成功后执行的函数
 * $redirect_stdin_stdout 重定向子进程的标准输入输出
 * $create_pipe 是否创建管道。启用
 * $redirect_stdin_stdout 将忽略用户的参数
 */
$process = new swoole_process("doProcess");
$pid = $process->start();
//创建多个进程,继续new swoole_process();
$process = new swoole_process("doProcess");
$pid = $process->start();
//等待结束,避免出现僵尸进程
swoole_process::wait();

14. 进程事件

<?php
$workers = [];//进程池
//创建 启动进程
for($i=0; $i<3; $i++){
    $process = new swoole_process("doProcess");//创建单独进程
    $pid = $process->start();//启动进程,并获取进程pid
    $workers[$pid] = $process;//存入 进程池
}
// 创建进程执行函数
function doProcess(swoole_process $process){
    $process->write("pid:$process->pid");//子进程写入信息,写入到管道里
    echo "写入信息: $process->pid $process->callback";
}
//添加进程事件,向每个子进程添加需要执行的动作
foreach($workers as $process){
    swoole_event_add($process->pipe, function($pipe) use($process){
       $data = $process->read();//读取管道中的数据
        echo "接收到: $data \n";
    });
}

在这里插入图片描述

15. 进程队列通信

$workers = [];
$worker_num = 2;
//批量创建进程
for($i=0;$i<$worker_num;$i++){
    //利用管道通信
    $process = new swoole_process("doProcess", false, false);
    $process->useQueue();//开启队列(类似开辟了新的内存)
    $pid = $process->start();
    $workers[$pid] = $process;
}
// 进程执行函数
function doProcess(swoole_process $process){
    $recv = $process->pop();//默认8192个长度
    echo "从主进程获取到的数据:$recv \n";
    sleep(5);
    $process->exit(0);
}
// 主进程 向子进程添加数据
foreach ($workers as $pid=>$process){
    $process->push("Hell 子进程 $pid \n");
}
// 等待子进程结束,回收资源
for($i=0;$i<$worker_num;$i++){
    $ret = swoole_process::wait();//等待执行完成
    $pid = $ret['pid'];
    unset($workers[$pid]);
    echo "子进程退出 $pid \n";
}

在这里插入图片描述

16. 信号触发

//触发函数
swoole_process::signal(SIGALRM, function (){
    static $i = 0;
    echo "$i \n";
    $i++;
    if($i>10){
        swoole_process::alarm(-1);//清除定时器
    }
});
//定时信号
swoole_process::alarm(100*1000);

在这里插入图片描述

17. 锁机制

//锁包含:文件锁/读写锁/信号量/互斥锁/自旋锁
//创建锁对象
$lock = new swoole_lock(SWOOLE_MUTEX);//互斥锁
echo "创建互斥锁 \n";
$lock->lock();//开始锁定 主进程
if(pcntl_fork() > 0){
    sleep(1);
    $lock->unlock();//解锁
} else{
    echo "子进程 等待锁 \n";
    $lock->lock();//子进程上锁
    echo "子进程获取锁\n";
    $lock->unlock();//释放锁
    exit("子进程退出");
}
echo "主进程 释放锁";
unset($lock);
sleep(1);
echo "子进程退出";

在这里插入图片描述

18. DNS查询

//dns 查询
//执行dns查询
swoole_async_dns_lookup("www.baidu.com",function ($host,$ip){
    echo "$host $ip";
});

19. 异步文件的读取

swoole_async_readfile(__DIR__."a.txt", function ($filename, $content){
    echo "$filename $content";
});

20. 异步文件的写入

$content = "hello world";
swoole_async_writefile("2.txt", $content, function ($filename){
    echo $filename;
});

21. 异步事件

$fp = stream_socket_client("tcp://www.qq.com:80", $errorno, $errstr, 30);
fwrite($fp, "GET / HTTP/1.1\r\nHost:www.qq.com\r\n\r\n");
//添加我们的异步事件
swoole_event_add($fp, function($fp){
    $resp = fread($fp, 8192);
    var_dump($resp);
    swoole_event_del($fp);
    fclose($fp);
});
echo "这个先执行完成\n";

在这里插入图片描述

22. 异步mysql

$db = new swoole_mySQL();
$config = [
  'host' => '127.0.0.1',
    'user' => 'root',
    'password' => 'root',
    'database' => 'mysql',
    'charset' => 'utf8',
];
$db->connect($config, function ($db, $r){
    if($r == false){
        var_dump($db->connect_errno, $db->connect_errno);
        die('失败');
    }
    $sql = ' show tables';
    $db->query($sql, function(swoole_mySQL $db, $r){
        if($r == false){
            var_dump($db->error);
            die("操作失败");
        } elseif($r == true) {
            var_dump($db->affected_rows, $db->insert_id);
        }
        var_dump($r);
        $db->close();
    });
});

22. http/server 静态资源处理

$server = new swoole_server("0.0.0.0", 9501);
$server -> set([
	'enable_static_handler' = true,
	'document_root' = '/data/',
]);
posted @ 2019-05-05 01:58  南山道士  阅读(127)  评论(0编辑  收藏  举报