swoole快速上手
文章目录
- 第一 安装
- 第二 配置
- 第三 快速使用
- 1. 创建tcp服务器
- 2. 创建udp服务器
- 3. 创建web服务器
- 4. 创建WebSocket服务器
- Swoole 异步操作
- 5. 定时器
- 6. 异步tcp服务器
- 7. 创建同步TCP客户端
- 8. 创建异步TCP客户端
- 9 网络通信协议设计
- 10. 使用异步客户端
- 11. 多进程共享数据
- 12. 使用协程客户端(version>=4.2.5, [协程编程须知](https://wiki.swoole.com/wiki/page/851.html))
- 13. 进程创建
- 14. 进程事件
- 15. 进程队列通信
- 16. 信号触发
- 17. 锁机制
- 18. DNS查询
- 19. 异步文件的读取
- 20. 异步文件的写入
- 21. 异步事件
- 22. 异步mysql
- 22. http/server 静态资源处理
第一 安装
第二 配置
第三 快速使用
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通信过程
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-async
、mysql-async
都是基于异步swoole_client实现的。
异步客户端需要设置回调函数,有4个事件回调必须设置onConnect
、onError
、onReceive
、onClose
。分别在客户端连接成功、连接失败、收到数据、连接关闭时触发。
$client->connect()
发起连接的操作会立即返回,不存在任何等待。当对应的IO事件完成后,swoole底层会自动调用设置好的回调函数。
9 网络通信协议设计
-
TCP协议在底层机制上解决了UDP协议的顺序和丢包重传问题。但相比UDP又带来了新的问题,TCP协议是流式的,数据包没有边界。应用程序使用TCP通信就会面临这些难题。因为TCP通信是流式的,在接收1个大数据包时,可能会被拆分成多个数据包发送。多次Send底层也可能会合并成一次进行发送。这里就需要2个操作来解决:分包(Server收到了多个数据包,需要拆分数据包);合包(Server收到的数据只是包的一部分,需要缓存数据,合并成完整的包)。所以TCP网络通信时需要设定通信协议。常见的TCP网络通信协有HTTP/HTTPS/FTP/SMTP/POP3/IMAP/SSH/Redis/Memcache/MySQL 。如果要设计一个通用协议的Server,那么就要按照通用协议的标准去处理网络数据。除了通用协议外还可以自定义协议
-
EOF结束符协议:EOF协议处理的原理是每个数据包结尾加一串特殊字符表示包已结束。如
memcache
、ftp
、stmp
都使用\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", ));
-
固定包头+包体协议:这种协议的特点是一个数据包总是由包头+包体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
提供的MySQL
、CURL
、Redis
等客户端是同步的,会导致服务器程序发生阻塞。Swoole
提供了常用的异步客户端组件,来解决此问题。编写纯异步服务器程序时,可以使用这些异步客户端。
异步客户端可以配合使用SplQueue
实现连接池,以达到长连接复用的目的。在实际项目中可以使用PHP
提供的Yield/Generator
语法实现半协程的异步框架。也可以基于Promises
简化异步程序的编写。
-
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(); }); });
与
mysqli
和PDO
等客户端不同,Swoole\MySQL
是异步非阻塞的,连接服务器、执行SQL时,需要传入一个回调数。connect
的结果不在返回值中,而是在回调函数中。query
的结果也需要在回调函数中进行处理。 -
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客户端 -
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文档 -
其他客户端
Swoole
底层目前只提供了最常用的MySQL
、Redis
、Http
异步客户端,如果你的应用程序中需要实现其他协议客户端,如Kafka
、AMQP
等协议,可以基于Swoole\Client
异步TCP
客户端,开发相关协议解析代码,来自行实现。
11. 多进程共享数据
由于PHP
语言不支持多线程,因此Swoole
使用多进程模式。在多进程模式下存在进程内存隔离,在工作进程内修改global
全局变量和超全局变量时,在其他进程是无效的。设置worker_num=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
。对应的解决方案就是使用外部存储服务:数据库,如:
MySQL
、MongoDB
;缓存服务器,如:Redis
、Memcache
;磁盘文件,多进程并发读写时需要加锁普通的数据库和磁盘文件操作,存在较多
IO
等待时间。因此推荐使用:Redis
内存数据库,读写速度非常快;/dev/shm内存文件系统,读写操作全部在内存中完成,无
IO`消耗,性能极高 -
共享内存
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
在设置好行数并创建之后,使用时不能超过限制),使用时需要根据实际情况来选择。
- shm扩展(不推荐),提供了
12. 使用协程客户端(version>=4.2.5, 协程编程须知)
在最新的4.x
版本中,协程取代了异步回调,作为我们推荐使用的编程方式。协程解决了异步回调编程困难的问题。使用协程可以以传统同步编程的方法编写代码,底层自动切换为异步IO
,既保证了编程的简单性,又可借助异步IO
,提升系统的并发能力。
-
协程实例
$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
。因此:服务器可以应对大量并发,每个请求都会创建一个新的协程,执行对应的代码;某些请求处理较慢时,只会引起这一个请求被挂起,不影响其他请求的处理 -
其他协程组件
Swoole4扩展提供了丰富的协程组件,如
Redis、
TCP/UDP/Unix客户端、
Http/WebSocket/Http2`客户端,使用这些组件可以很方便地实现高性能的并发编程。使用协程时请认真阅读 协程编程须知,避免发生错误。 -
适用场景
- 高并发服务,如秒杀系统、高性能
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/',
]);