├── .gitignore ├── C_C++_use_swoole.md ├── LICENSE ├── README.md ├── big_json_tranfer ├── client.php ├── data-json │ ├── data_10m.json │ ├── data_1m.json │ ├── data_2m.json │ └── data_3m.json ├── data.json ├── runtime.md └── server.php ├── client.php ├── heap_in_swoole.md ├── http_server_client └── server.php ├── queue_in_swoole.md ├── server.php ├── splFixedArray_in_swoole.md ├── stop_server.sh ├── swoole-tutorials.md ├── tcpdump抓包工具简单使用.md ├── 使用asan检测内存.md ├── 使用systemd管理swoole服务器.md ├── 协程CPU密集场景调度实现.md ├── 将Swoole静态编译内嵌到php中.md ├── 异步回调程序内存管理.md ├── 日志等级控制.md └── 详解Swoole协程实现原理.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /C_C++_use_swoole.md: -------------------------------------------------------------------------------- 1 | #### C 环境下使用swoole 2 | 3 | swoole使用cmake来做编译配置,示例程序在swoole源码 example/server.c 中,您可以在此基础上进行代码开发。如果需要修改编译细节的选项,请直接修改CMakeLists.txt 4 | 5 | ###### 生成 config.h 6 | 7 | swoole依赖 `phpize` 和 `configure` 检测系统环境,生成 `config.h` 8 | 9 | ```shell 10 | cd swoole-src/ 11 | phpize 12 | ./configure 13 | ``` 14 | 15 | 执行成功后 `swoole-src` 目录下会有 `config.h` 16 | 17 | ###### Build & Install 18 | 19 | ```shell 20 | cmake . 21 | make 22 | make install 23 | ``` 24 | 25 | - `cmake` 命令可以增加 `cmake . -DCMAKE_INSTALL_PREFIX=/opt/swoole` 参数指定安装的路径 26 | - `make` 命令可以使用 `make DESTDIR=/opt/swoole install` 参数指定安装的路径 27 | 28 | 安装路径非系统默认的 lib 目录时,需要配置 `ld.so.conf` 将 `swoole` 动态连接库所在的目录添加到 link 路径中。 29 | 30 | ```shell 31 | sudo echo "/opt/swoole/lib" >> /etc/ld.so.conf 32 | sudo echo "/opt/swoole/lib" > /etc/ld.so.conf.d/swoole.conf 33 | ldconfig 34 | ``` 35 | 36 | ###### sample 37 | 38 | ```c 39 | // examples/server.c 40 | #include 41 | #include 42 | 43 | int main() 44 | { 45 | swServer serv; 46 | swServer_create(&serv); 47 | serv.onStart = my_onStart; 48 | swServer_start(&serv); 49 | } 50 | ``` 51 | 52 | ###### 编译运行 53 | 54 | ``` 55 | gcc -o server server.c -lswoole 56 | ./server 57 | ``` 58 | 59 | ___________________ 60 | 61 | #### C++ 中使用 swoole 62 | 63 | php编写的 Server 程序在某些情况下会表现的比较差 64 | 65 | - 内存占用高的场景,php底层使用内存结构 `zval` 来管理所有变量,会额外占用内存。比如一个 int32 的整数可能需要占用 16(php7)或者 24(php5)字节的内存,而 C/C++ 只需要 4个字节。如果系统要存储大量整数,那么占用内存会非常大。 66 | - php是动态编译方式的脚本语言,计算性能较差。纯运算型的代码可能会比 C/C++ 程序差上几十倍甚至上百倍以上,此类场景下使用纯php语言不是个好选择 67 | 68 | `cpp-swoole` 是对 `c-swoole` 的面向对象封装,支持了绝大部分 `swoole_server` 的特性包括 `task` 功能,另外还支持高精度定时器特性。 69 | 70 | `cpp-swoole` 依赖于 `libswoole.so` ,需要先编译 `c-swoole` 生成 `libswoole.so` 71 | 72 | ###### 编译 libswoole.so 73 | 74 | ```shell 75 | git clone https://github.com/swoole/cpp-swoole.git 76 | cmake . 77 | make 78 | sudo make install 79 | ``` 80 | 81 | ###### 编写程序 82 | 83 | 头文件: 84 | 85 | ```cpp 86 | #include 87 | #include 88 | ``` 89 | 90 | 服务器程序只需要继承 `swoole::Server`, 并实现响应的回调函数即可。 91 | 92 | ```c++ 93 | #include 94 | #include 95 | #include 96 | 97 | using namespace std; 98 | using namespace swoole; 99 | 100 | class MyServer : public Server 101 | { 102 | public: 103 | MyServer(string _host, int _port, int _mode = SW_MODE_PROCESS, int _type = SW_SOCK_TCP) : 104 | Server(_host, _port, _mode, _type) 105 | { 106 | serv.worker_num = 4; 107 | SwooleG.task_worker_num = 2; 108 | } 109 | 110 | virtual void onStart(); 111 | virtual void onShutdown() {}; 112 | virtual void onWorkerStart(int worker_id) {} 113 | virtual void onWorkerStop(int worker_id) {} 114 | virtual void onPipeMessage(int src_worker_id, const DataBuffer &) {} 115 | virtual void onReceive(int fd, const DataBuffer &data); 116 | virtual void onConnect(int fd); 117 | virtual void onClose(int fd); 118 | virtual void onPacket(const DataBuffer &data, ClientInfo &clientInfo) {}; 119 | 120 | virtual void onTask(int task_id, int src_worker_id, const DataBuffer &data); 121 | virtual void onFinish(int task_id, const DataBuffer &data); 122 | }; 123 | 124 | void MyServer::onReceive(int fd, const DataBuffer &data) 125 | { 126 | swConnection *conn = swWorker_get_connection(&this->serv, fd); 127 | printf("onReceive: fd=%d, ip=%s|port=%d Data=%s|Len=%ld\n", fd, swConnection_get_ip(conn), 128 | swConnection_get_port(conn), (char *) data.buffer, data.length); 129 | 130 | int ret; 131 | char resp_data[SW_BUFFER_SIZE]; 132 | int n = snprintf(resp_data, SW_BUFFER_SIZE, (char *) "Server: %*s\n", (int) data.length, (char *) data.buffer); 133 | ret = this->send(fd, resp_data, (uint32_t) n); 134 | if (ret < 0) 135 | { 136 | printf("send to client fail. errno=%d\n", errno); 137 | } 138 | else 139 | { 140 | printf("send %d bytes to client success. data=%s\n", n, resp_data); 141 | } 142 | DataBuffer task_data("hello world\n"); 143 | this->task(task_data); 144 | // this->close(fd); 145 | } 146 | 147 | void MyServer::onConnect(int fd) 148 | { 149 | printf("PID=%d\tConnect fd=%d\n", getpid(), fd); 150 | } 151 | 152 | void MyServer::onClose(int fd) 153 | { 154 | printf("PID=%d\tClose fd=%d\n", getpid(), fd); 155 | } 156 | 157 | void MyServer::onTask(int task_id, int src_worker_id, const DataBuffer &data) 158 | { 159 | printf("PID=%d\tTaskID=%d\n", getpid(), task_id); 160 | } 161 | 162 | void MyServer::onFinish(int task_id, const DataBuffer &data) 163 | { 164 | printf("PID=%d\tClose fd=%d\n", getpid(), task_id); 165 | } 166 | 167 | void MyServer::onStart() 168 | { 169 | printf("server start\n"); 170 | } 171 | 172 | class MyTimer : Timer 173 | { 174 | public: 175 | MyTimer(long ms, bool interval) : 176 | Timer(ms, interval) 177 | { 178 | 179 | } 180 | 181 | MyTimer(long ms) : 182 | Timer(ms) 183 | { 184 | 185 | } 186 | 187 | protected: 188 | virtual void callback(void); 189 | int count = 0; 190 | }; 191 | 192 | void MyTimer::callback() 193 | { 194 | printf("#%d\thello world\n", count); 195 | if (count > 9) 196 | { 197 | this->clear(); 198 | } 199 | count++; 200 | } 201 | 202 | int main(int argc, char **argv) 203 | { 204 | MyServer server("127.0.0.1", 9501, SW_MODE_SINGLE); 205 | server.listen("127.0.0.1", 9502, SW_SOCK_UDP); 206 | server.listen("::1", 9503, SW_SOCK_TCP6); 207 | server.listen("::1", 9504, SW_SOCK_UDP6); 208 | server.setEvents(EVENT_onStart | EVENT_onReceive | EVENT_onClose | EVENT_onTask | EVENT_onFinish); 209 | server.start(); 210 | } 211 | ``` 212 | 213 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Jax Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swoole-demo -------------------------------------------------------------------------------- /big_json_tranfer/client.php: -------------------------------------------------------------------------------- 1 | set(array( 12 | 'package_eof' => "\r\n\r\n", // 文件分割符 13 | 'socket_buffer_size' => 1 * 1024 * 1024, // buffer 大小 14 | 'package_max_length' => 4 * 1024 * 1024, //协议最大长度 15 | )); 16 | 17 | // 获取当前时间戳微秒数 18 | // microtime() 这个函数返回形式为 unix_timestamp + 微妙数,中间用空格隔开 19 | function microtime_float() 20 | { 21 | list($usec, $sec) = explode(" ", microtime()); 22 | return ((float)$usec + (float)$sec); 23 | } 24 | 25 | $start_time = 0; 26 | 27 | $client->on('connect', function ($cli) { 28 | echo "connected! \n"; 29 | global $start_time; 30 | $start_time = microtime_float(); 31 | global $content; 32 | $cli->send($content."\r\n\r\n"); 33 | }); 34 | $client->on('receive', function ($cli, $data) { 35 | echo "received: {$data}\n"; 36 | }); 37 | $client->on('error', function ($cli) { 38 | echo "connection failed! \n"; 39 | }); 40 | $client->on('close', function ($cli) { 41 | global $start_time; 42 | $end_time = microtime_float(); 43 | echo "connection closed! \n"; 44 | echo "Run time : " . ($end_time - $start_time) . "s. \n"; 45 | }); 46 | $client->connect('127.0.0.1', 9601, 0.5); -------------------------------------------------------------------------------- /big_json_tranfer/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"aa":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, 3 | {"aa":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, 4 | {"aa":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, 5 | {"aa":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, 6 | {"aa":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, 7 | {"aa":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}, 8 | {"aa":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} 9 | ] -------------------------------------------------------------------------------- /big_json_tranfer/runtime.md: -------------------------------------------------------------------------------- 1 | ###### 参数设置: 2 | 3 | ###### buffer:1M, 4 | 5 | ###### package_max_length: 300M 6 | 7 | | 文件大小(bytes) | 简写 | 传输时间(s) | 8 | | ----------------- | ---- | ----------------------------------- | 9 | | `1010541` | 1M | `0.21094703674316` | 10 | | `2021061` | 2M | `0.4104700088501` | 11 | | `3518229` | 3.5M | `0.8567111492157` | 12 | | `10554646` | 10M | `2.9484338760376` | 13 | | `31663894` | 30M | `9.3961210250854` | 14 | | `103755002` | 100M | `30.612946987152` | 15 | | `316638743` | 300M | 未运行,直接close connection。虚拟机内存不够 | 16 | | 1053671218 | 1G | 卡在读取文件阶段。。。 | 17 | 18 | ​ -------------------------------------------------------------------------------- /big_json_tranfer/server.php: -------------------------------------------------------------------------------- 1 | set(array( 5 | 'open_length_check' => true, 6 | 'open_eof_check' => true, 7 | 'package_eof' => "\r\n\r\n", 8 | 'open_eof_split' => true, 9 | 'package_max_length' => 4 * 1024 * 1024, //协议最大长度 10 | )); 11 | $server->on('connect', function ($server, $fd){ 12 | echo "connected! : {$fd}\n"; 13 | }); 14 | $server->on('receive', function ($server, $fd, $reactor_id, $data) { 15 | echo 'received : ' . $data . "\n"; 16 | $server->close($fd); 17 | file_put_contents('./received.json', $data); 18 | }); 19 | $server->on('close', function ($server, $fd) { 20 | echo "connection closed! : {$fd}\n"; 21 | }); 22 | $server->start(); -------------------------------------------------------------------------------- /client.php: -------------------------------------------------------------------------------- 1 | connect("127.0.0.1", 9601, 0.5)) { 7 | die("connecting failed.\n"); 8 | } 9 | 10 | 11 | 12 | // send data to server-side 13 | if (!$client->send('12312323')) { 14 | die("sending data failed.\n"); 15 | } 16 | 17 | // receive data from server-side 18 | $recv_data = $client->recv(); 19 | if (!$recv_data) { 20 | die("receiving failed.\n"); 21 | } 22 | echo $recv_data . "\n"; 23 | 24 | // close connect 25 | $client->close(); -------------------------------------------------------------------------------- /heap_in_swoole.md: -------------------------------------------------------------------------------- 1 | #### 堆(Heap) 2 | 3 | 在服务器程序开发中经常要用到排序功能,如会员积分榜。普通的 `array` 使用 `sort` 排序,即使用了最快的快排算法,实际上也会存在较大的时间开销。因此在内存中维护一个有序的内存结构,可以有效地避免 `sort` 排序的计算开销。 4 | 5 | 在php 中 `SplHeap` 就是一种有序的数据结构。数据总是按照最小或最大排在靠前的位置。新插入的数据会自动进行排序。 6 | 7 | ###### 定义 8 | 9 | `SplHeap` 数据结构需要指定一个 `compare` 方法来进行元素的对比,从而实现自动排序。`SplHeap` 本身是 `abstract` 的,不能直接 `new`。 10 | 11 | 需要编写一个子类,并实现 `compare` 方法 12 | 13 | ```php 14 | // 最大堆 15 | class MaxHeap extends SplHeap 16 | { 17 | protected function compare($a, $b) { 18 | return $a - $b; 19 | } 20 | } 21 | // 最小堆 22 | class MinHeap extends SplHeap 23 | { 24 | protected function compare($a, $b) { 25 | return $b - $a; 26 | } 27 | } 28 | ``` 29 | 30 | ###### 使用 31 | 32 | 定义好子类后,可使用 `insert` 方法插入元素。插入的元素会使用 `compare` 方法与已有元素进行对比,自动排序。 33 | 34 | ```php 35 | $list = new MaxHeap; 36 | $list->insert(56); 37 | $list->insert(22); 38 | $list->insert(35); 39 | $list->insert(11); 40 | $list->insert(88); 41 | $list->insert(36); 42 | $list->insert(89); 43 | $list->insert(123); 44 | ``` 45 | 46 | `SplHeap` 底层使用跳表数据结构, `insert` 操作的时间复杂度为 O(Log(n)) 47 | 48 | 注意这里只能插入数字,因为我们定义的 `compose` 不支持非数字对比。如果要支持插入数组或对象,可重新实现 `compare` 方法。 49 | 50 | ```php 51 | class MyHeap extends SplHeap 52 | { 53 | protected function compare($a, $b) { 54 | return $a->value - $b->value; 55 | } 56 | } 57 | class MyObject 58 | { 59 | public $value; 60 | function __construct($value) { 61 | $this->value = $value; 62 | } 63 | } 64 | $list = new MyHeap; 65 | $list->insert(new MyObject(56)); 66 | $list->insert(new MyObject(12)); 67 | ``` 68 | 69 | 使用 `foreach` 遍历堆,可以发现是有序输出。 70 | 71 | ```php 72 | foreach($list as $li) 73 | { 74 | echo $li . "\n"; 75 | } 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /http_server_client/server.php: -------------------------------------------------------------------------------- 1 | on('request', function ($request, $response) { 5 | $params = $request->get; 6 | if (!empty($params)) { 7 | $fin = fopen(__DIR__ . '/record.txt', "a"); 8 | fwrite($fin, json_encode($params)); 9 | fwrite($fin, "\r\n"); 10 | fclose($fin); 11 | } 12 | $messageLists = '

---------------------------------

'; 13 | $filePath = __DIR__ . '/record.txt'; 14 | if (file_exists($filePath)) { 15 | $fout = fopen($filePath, "r"); 16 | while (!feof($fout)) { 17 | $line = trim(fgets($fout), "\r\n"); 18 | if (!empty($line)) { 19 | $messageLists .= "

>>>{username}: " . $line . "

"; 20 | } 21 | } 22 | } 23 | 24 | $response->end("

Message list:

" . $messageLists); 25 | }); 26 | $http->start(); 27 | -------------------------------------------------------------------------------- /queue_in_swoole.md: -------------------------------------------------------------------------------- 1 | #### 队列(Queue) 2 | 3 | 异步并发的服务器里经常使用队列实现 `生产者-消费者` 模型,解决并发排队问题。php 的 spl标准库中提供了 `SplQueue` 扩展内置的队列数据结构。另外 php 的数组也提供了 `array_pop` 和 `array_shift` 可以使用数组来模拟队列数据结构。 4 | 5 | ###### SplQueue 6 | 7 | ```php 8 | $queue = new SplQueue; 9 | // 入队 10 | $queue->push($data); 11 | // 出对 12 | $data = $queue->shift(); 13 | // 查询队列中的排队数量 14 | $n = count($queue); 15 | ``` 16 | 17 | ###### Array模拟队列 18 | 19 | ```php 20 | $queue = array(); 21 | // 入队 22 | $queue[] = $data; 23 | // 出队 24 | $data = array_shift($queue); 25 | // 查询队列中的排队数量 26 | $n = count($queue); 27 | ``` 28 | 29 | ###### 性能对比 30 | 31 | 虽然使用 Array 实现队列,但实际上性能会非常差。在一个大并发的服务器程序上,建议使用 `SplQueue` 作为队列数据结构。 32 | 33 | 100万条数据随机入队,出对,使用 `SplQueue` 仅用 `2312.345ms` 即可完成,而使用 Array 模拟的队列的程序根本无法完成测试,CPU使用率持续高达 100% 34 | 35 | 降低数据量到 1 万条以后,也需要 `260ms` 才能完成测试。 36 | 37 | ```php 38 | // SplQueue 39 | 40 | $splq = new SplQueue; 41 | for($i = 0; $i < 1000000; $i++) 42 | { 43 | $data = "hello $i\n"; 44 | $splq->push($data); 45 | if($i % 100 == 99 and count($splq) > 100) { 46 | $popN = rand(10, 99); 47 | for($j = 0; $j < $popN; $j++) { 48 | $splq->shift(); 49 | } 50 | } 51 | } 52 | $popN = count($splq); 53 | for($j = 0; $j < $popN; $j++) { 54 | $splq->pop(); 55 | } 56 | ``` 57 | 58 | ```php 59 | // Array 60 | 61 | $arrq = array(); 62 | for($i = 0; $i < 1000000; $i++) { 63 | $data = "hello $i\n"; 64 | $arrq[] = $data; 65 | if($i % 100 == 99 and count($arrq) > 100) { 66 | $popN = rand(10, 99); 67 | for($j = 0; $j < $popN; $j++) { 68 | array_shift($arrq); 69 | } 70 | } 71 | } 72 | $popN = count($arrq); 73 | for($j = 0; $j < $popN; $j++) { 74 | array_shift($arrq); 75 | } 76 | ``` 77 | 78 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | on('connect', function ($serv, $fd) { 7 | echo "Client: Connect success! \n"; 8 | }); 9 | 10 | // watching receive 11 | $serv->on('receive', function ($serv, $fd, $form_id, $data) { 12 | $serv->send($fd, "Server: " . $data); 13 | }); 14 | 15 | // watching close 16 | $serv->on('close', function ($serv, $fd) { 17 | echo "Client: Closed! \n"; 18 | }); 19 | 20 | // start server 21 | $serv->start(); -------------------------------------------------------------------------------- /splFixedArray_in_swoole.md: -------------------------------------------------------------------------------- 1 | #### 定长数组(SplFixedArray) 2 | 3 | php官方的SPL库提供了一个定长数组的数据结构,类似于C语言中的数组。和普通的php数组不同,定长数组的读写性能更好。 4 | 5 | ###### 官方测试数据 6 | 7 | 测试使用php5.4,64位的linux系统 8 | 9 | ```ini 10 | small data(1, 000): 11 | write: SplFixedArray is 15 % faster 12 | read: SplFixedArray is 5 % faster 13 | larger data(512, 000): 14 | write: SplFixedArray is 33 % faster 15 | read: SplFixedArray is 10 % faster 16 | ``` 17 | 18 | ###### 使用方法 19 | 20 | SplFixedArray 使用方法与 Array 相同,但只支持数字索引的访问方式。 21 | 22 | ```php 23 | $array = new SplFixedArray(5); 24 | $array[1] = 2; 25 | $array[4] = "foo"; 26 | 27 | var_dump($array[0]); // NULL 28 | var_dump($array[1]); // int(2) 29 | ``` 30 | 31 | 可以使用 `setSize()` 方法动态改变定长数组的尺寸。 32 | 33 | -------------------------------------------------------------------------------- /stop_server.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 如果我们修改了 server.php,则必须重启一次服务才能生效。 3 | # 此脚本用来断开服务 4 | ps -eaf | grep "server.php" | grep -v "grep" | awk '{print $2}' | xargs kill -9 -------------------------------------------------------------------------------- /swoole-tutorials.md: -------------------------------------------------------------------------------- 1 | # Swoole-Tutorials 2 | 3 | _____________________________ 4 | 5 | ## swoole 6 | 7 | php的异步,并行,高性能网络通信引擎,使用纯C语言编写,提供了php语言的异步多线程服务器,异步TCP/UDP网络客户端,异步MySQL,异步Redis,数据库连接池,AsyncTask,消息队列,毫秒定时器,异步文件对俄,异步DNS查询。Swoole内置了 Http/WebSocket服务器端/客户端,Http2.0服务器端/客户端。 8 | 9 | swoole底层内置了异步非阻塞,多线程的网络IO服务器。php服务器仅需处理事件回调即可而无需关心底层。与 `nginx/Tornado/node.js` 等全异步的框架不同,swoole既支持全异步,也支持同步。 10 | 11 | 除了异步IO的支持外,swoole为php多进程的模式设计了多个并发数据结构和 `IPC` 通信机制。可以大大简化多进程并发编程的工作。其中包括了 并发原子计数器,并发 HashTable,Channel,Lock,进程间通信 IPC等丰富的功能特性。 12 | 13 | swoole从 2.0版本开始支持内置协程,可以使用完全同步的代码来实现异步的程序。php代码无需额外增加任何关键词,底层自动进行协程调度,实现异步。 14 | 15 | ## 1. 入门指引 16 | 17 | `swoole` 虽然是标准PHP扩展,实际上与普通的扩展不同。普通的扩展一般只是提供一个库函数。而swoole扩展在运行后会接管PHP的控制权,进入PHP执行的事件循环。 18 | 19 | 当 IO 事件发生后,`swoole` 会自动回调指定的 `PHP` 函数。 20 | 21 | ##### Server 22 | 23 | 强大的 `TCP/UDP Server` 框架,支持多线程,`EventLoop` , 事件驱动,异步,`Worker` 进程组,`Task` 异步任务,毫秒定时器,`SSL/TLS` 隧道加密。 24 | 25 | - `swoole_http_server` 是 `swoole_server` 的子类,内置了`http` 的支持 26 | - `swoole_websocket_server` 是 `swoole_http_server` 的子类,内置了`webSocket` 的支持 27 | - `swoole_redis_server` 是 `swoole_server` 的子类,内置了 `Redis` 服务器端协议的支持 28 | 29 | > 子类可以调用父类的所有方法和属性 30 | 31 | ##### Client 32 | 33 | 一个`TCP/UDP/UnixSocket` 客户端,支持 `IPv4/IPv6` ,支持 `SSL/TLS` 隧道加密,支持 `SSL` 双向证书,支持同步并发调用,支持异步事件驱动编程。 34 | 35 | ##### Event 36 | 37 | `EventLoop API` , 让用户可以直接操作底层的事件循环,将 `socket`, `stream`, 管道等 `Linux` 文件加入到事件循环种。 38 | 39 | > `eventloop` 接口仅可用于 `socket` 类型的文件描述符,不能用于磁盘文件读写。 40 | 41 | ##### Coroutine 42 | 43 | Swoole 在 2.0 开始内置协程(Coroutine)的能力,提供了具备协程编程能力 IO 接口(统一在命名空间 Swoole\Coroutine)。 44 | 45 | 协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程和线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。Swoole可以为每个请求创建对应的协程,根据 IO 的状态来合理地调度协程,这会带来以下几点优势: 46 | 47 | 开发者可以无感知地用同步的代码编写方式达到异步 IO 的效果和性能,避免了传统异步回调所带来的离散的代码逻辑和陷入多层回调中导致代码无法维护。 48 | 49 | 同时由于Swoole是在底层封装的协程,所以对比传统的 php 层协议框架,开发者不需要使用 yield 关键词来标识一个协程 IO 操作,所以不再需要对 yield 的语义进行深入理解以及对每一级的调用都修改为 yield,这极大地提高了开发效率。 50 | 51 | ##### Process 52 | 53 | 进程管理模块。可以方便地创建子进程,进行进程间通信,进程的管理。 54 | 55 | ##### Buffer 56 | 57 | 强大的内存区管理工具,像 `C` 语言一样进行指针计算,又无需关心内存的申请和释放,而且不用担心内存越界,底层已经都做好了。 58 | 59 | ##### Table 60 | 61 | 基于共享内存和自旋锁实现的超高性能内存表。彻底解决线程,进程间数据共享,加锁同步等问题。 62 | 63 | > `swoole_table` 的性能可以达到单线程每秒读写 200 万次。 64 | 65 | ### 1.1 环境依赖 66 | 67 | ### 1.2 编译安装 68 | 69 | #### 1.2.1 编译参数 70 | 71 | #### 1.2.2 常见错误 72 | 73 | #### 1.2.3 Cygwin 74 | 75 | ### 1.3 快速起步 76 | 77 | #### 1.3.1 创建TCP服务器 78 | 79 | ###### sample code 80 | 81 | ```php 82 | // 创建Server对象,监听9501端口 83 | $serv = new swoole_server("127.0.0.1", 9501); 84 | // 监听连接进入的事件 85 | $serv->on('connect', function($serv, $fd) { 86 | echo "Client: Connect.\n"; 87 | }); 88 | // 监听数据接收事件 89 | $serv->on('receive', function($serv, $fd, $from_id, $data) { 90 | $serv->send($fd, "Server: " . $data); 91 | }); 92 | // 监听连接关闭事件 93 | $serv->on('close', function($serv, $fd) { 94 | echo "Client: Close.\n"; 95 | }); 96 | // 启动服务器 97 | $serv->start(); 98 | ``` 99 | 100 | swoole_server 是一个异步服务器,通过监听事件的方式来实现功能。当对应的事件发生时,底层会主动回调指定的PHP函数。如果有新的 TCP 连接进入时会执行 onConnect 事件回调,当某个连接向服务器发送数据时,会回调 onReceive 函数。 101 | 102 | - 服务器可以同时被成千上万个客户端连接,$fd 就是客户端连接的唯一标识符 103 | - 调用`$server->send()` 方法向客户端连接发送数据,参数就是 $fd 客户端标识符 104 | - 调用 `$server->close()` 方法可以强制关闭某个客户端连接 105 | - 客户端可能会主动断开连接,此时触发 onClose 事件回调 106 | 107 | ###### 执行sample 108 | 109 | ```shell 110 | php server.php 111 | ``` 112 | 113 | 在命令行下运行 server.php 文件,启动成功后可以使用 `netstat` 工具可以看到,已经在监听 9501 端口。这时就可以使用 telnet/netcat 工具连接服务器。 114 | 115 | ```shell 116 | telent 127.0.0.1 9501 117 | hello 118 | Server: hello 119 | ``` 120 | 121 | 122 | 123 | #### 1.3.2 创建UDP服务器 124 | 125 | ###### sample code 126 | 127 | ```php 128 | // udp_server.php 129 | //创建Server 对象,监听 127.0.0.1:9502 端口,类型为SWOOLE_SOCK_UDP 130 | $serv = new swoole_server("127.0.0.1", 9502, SWOOLE_PROCESS, SWOOLE_SOCK_UDP); 131 | //监听数据接收事件 132 | $serv->on('Packet', function($serv, $data, $clientInfo) { 133 | $serv->sendto($clientInfo['address'], $clientInfo['port'], 'Server ' . $data); 134 | var_dump($clientInfo); 135 | }); 136 | // 启动服务器 137 | $serv->start(); 138 | ``` 139 | 140 | UDP 是单向协议。启动Server后,客户端无需Connect, 直接可以向 Server监听的端口发送数据包,对应事件为 onPacket。 141 | 142 | - clientInfo 是客户端相关信息数组,又客户端的IP和端口等 143 | - 调用$server->sendto 方法向客户端发送数据 144 | 145 | ###### 启动服务 146 | 147 | ```shell 148 | php udp_server.php 149 | ``` 150 | 151 | UDP服务器可以使用 `netcat -u` 来连接测试 152 | 153 | ```shell 154 | netcat -u 127.0.0.1 9502 155 | hello 156 | Server: hello 157 | 158 | ``` 159 | 160 | #### 1.3.3 创建Web服务器 161 | 162 | ###### sample code 163 | 164 | ```php 165 | // http_server.php 166 | $http = new swoole_http_server('0.0.0.0', 9501); 167 | $http->on('request', function($request, $response) { 168 | var_dump($request->get, $request->post); 169 | $response->header('Content-Type', 'text/html;charset=utf-8'); 170 | $response->end("

hello world. #" . rand(1000, 9999) . "

"); 171 | }); 172 | $http->start(); 173 | ``` 174 | 175 | Http服务器只需要关注请求响应即可,所以只需要监听 onRequest 事件。当有新的 Http 请求进入时就会触发此事件。事件的回调函数有两个参数,一个是 request 对象,包含了请求的相关信息,如 GET/POST 请求数据。 176 | 177 | 另外要给是 response对象,对 request 的响应可以通过操作 response 对象来完成。$response->end() 方法表示输出一段 HTML 内容,并结束此请求。 178 | 179 | - 0.0.0.0 表示监听所有 IP 地址,一台服务器可能有多个 IP,如127.0.0.1 本地回环 IP,192.168.1.100 局域网IP,210.127.20.2 外网IP,当然我们也可以指定监听某个单独IP 180 | - 9501 监听的端口,如果被占用程序会抛出致命错误,中断执行。 181 | 182 | ###### URL 路由 183 | 184 | 应用程序可以根据 $request->server['request_uri'] 实现路由,如 http://127.0.0.1:9501/test/index/?a=1, 代码中可以这样实现 URL 路由。 185 | 186 | ```php 187 | $http->on('request', function($request, $response){ 188 | list($controller, $action) = explode('/', trim($request->server['request_uri'], '/')); 189 | // 根据$controller, $action 映射到不同的控制器类和方法 190 | (new $controller)->$action($request, $response); 191 | }); 192 | ``` 193 | 194 | 195 | 196 | #### 1.3.4 创建WebSocket服务器 197 | 198 | ###### sample code 199 | 200 | ```php 201 | // ws_server.php 202 | // 创建websocket服务器对象,监听0.0.0.0:9502 203 | $ws = new swoole_websocket_server('0.0.0.0', 9502); 204 | // 监听WebSocket连接打开事件 205 | $ws->on('open', function($ws, $request) { 206 | var_dump($request->fd, $request->get, $request->server); 207 | $ws->push($request->fd, "hello, welcome\n"); 208 | }); 209 | // 监听websocket消息事件 210 | $ws->on('message', function($ws, $frame){ 211 | echo "Message:{$frame->data}\n"; 212 | $ws->push($frame->fd, "server:{$frame->data}"); 213 | }); 214 | // 监听websocket连接关闭事件 215 | $ws->on('close', function($ws, $fd){ 216 | echo "client-{$fd} is closed\n"; 217 | }); 218 | $ws->start(); 219 | ``` 220 | 221 | websocket 服务器是建立在Http服务器之上的长连接服务器,客户端首先会发送一个Http请求与服务器握手。握手成功以后触发 onOpen事件表示连接就续,onOpen函数中包含$request对象,包含握手的详细信息,如 GET/POST参数,cookie,header等。 222 | 223 | 建立连接后客户端与服务器即可双向通信 224 | 225 | - 客户端向服务器端发送信息时,服务器端触发 onMessage 事件回调 226 | - 服务器端可以调用 $server->push() 向某个客户端($fd 标识符)发送消息 227 | - 服务端可设置 onHandShake 事件回调来手动处理 websocket 握手。 228 | - swoole_http_server 是 swoole_server 的子类,内置了 Http 的支持 229 | - swoole_websocket_server 是swoole_http_server 的子类,内置了 websocket 的支持运行程序 230 | 231 | 运行 `php ws_server.php` 232 | 233 | 可以使用chrome浏览器测试,JS代码为: 234 | 235 | ```php 236 | var wsServer = 'ws://127.0.0.1:9502'; 237 | var websocket = new WebSocket(wsServer); 238 | websocket.onopen = function(e) { 239 | console.log('connected to websocket server!'); 240 | }; 241 | websocket.onclose = function(e) { 242 | console.log('disconnected.'); 243 | }; 244 | websocket.onmessage = function(e) { 245 | console.log('retrived data from server: ' + e.data); 246 | }; 247 | websocket.onerror = function(e, t) { 248 | console.log('error occured: ' + e.data); 249 | }; 250 | ``` 251 | 252 | - 无法用 swoole_client 与 websocket 的服务器通信,因为swoole_client 是TCP型 253 | 254 | - 必须实现 websocket协议才能和 websocket 服务器通信,可以使用 swoole/framework 提供的 255 | 256 | [PHP WebSocket客户端]: https://github.com/swoole/framework/blob/master/libs/Swoole/Client/WebSocket.php 257 | 258 | ###### Comet 259 | 260 | WebSocket服务器除了提供 websocket 功能以外,实际上也可以处理 Http 长连接,只需要增加 onRequest 事件监听即可实现 Comet 方案 Http 长轮询。 261 | 262 | #### 1.3.5 设置定时器 263 | 264 | swoole提供类似 JS 的 setInterval/setTimeout 异步定时器,粒度为毫秒级。 265 | 266 | ###### sample code 267 | 268 | ```php 269 | // 每隔2000ms触发一次 270 | swoole_timer_trick(2000, function($timer_id) { 271 | echo "tick-2000ms\n"; 272 | }); 273 | // 3000ms以后执行函数 274 | swoole_timer_after(3000, function() { 275 | echo "after 3000ms\n"; 276 | }); 277 | 278 | ``` 279 | 280 | - swoole_timer_tick = setInterval, 返回值 int,代表定时器ID 281 | - swoole_timer_after = setTimeout 返回值 int,代表定时器ID 282 | - swoole_timer_clear = clearInterval/clearTimeout, 参数为定时器ID 283 | 284 | 285 | 286 | #### 1.3.6 执行异步任务 287 | 288 | 在server里如果需要执行耗时很长的动作,比如要给聊天服务器此时需要发送广播,web服务器中发送邮件等等。如果直接去执行这些操作就会阻塞当前进程,导致服务器响应严重被拖慢。 289 | 290 | swoole可以执行异步任务处理,投递一个异步任务到 TaskWorker 进程池中去执行,同时不影响当前请求的执行。 291 | 292 | ###### sample code 293 | 294 | ```php 295 | // 基于第一个TCP服务器,只需要增加 onTask 和 onFinish 两个事件回调即可。另外需要设置task进程数,可以根据任务的耗时和任务量配置适当的 task进程。 296 | $serv = new swoole_server('127.0.0.1', 9501); 297 | // 设置异步任务的工作进程数量 298 | $serv->set(['task_worker_num' => 4]); 299 | $serv->on('receive', function($serv, $fd, $from_id, $data) { 300 | // 投递异步任务 301 | $task_id = $serv->task($data); 302 | echo "Dispath AsyncTask: id=$task_id\n"; 303 | }); 304 | // 处理异步任务 305 | $serv->on('task', function($serv, $task_id, $from_id, $data) { 306 | echo "New AsyncTask[id=$task_id]" . PHP_EOL; 307 | // 返回任务执行的结果 308 | $serv->finish("$data->OK"); 309 | }); 310 | // 处理异步任务的结果 311 | $serv->on('finish', function($serv, $task_id, $data) { 312 | echo "AsyncTask[$task_id] Finish: $data" . PHP_EOL; 313 | }); 314 | $serv->start(); 315 | ``` 316 | 317 | 调用 $serv->task() 后,程序立即返回,继续向下执行代码。onTask 回调函数 Task 进程池内被异步执行。执行完成之后调用 $serv->finish() 返回结果。 318 | 319 | > finish 操作非必填 320 | 321 | #### 1.3.7 创建同步TCP客户端 322 | 323 | ###### sample code 324 | 325 | ```php 326 | // client.php 327 | $client = new swoole_client(SWOOLE_SOCK_TCP); 328 | // 连接到服务器 329 | if(!$client->connect('127.0.0.1', 9501, 0.5)) 330 | die("connect failed."); 331 | // send 332 | if(!$client->send('hello world')) 333 | die('send failed.'); 334 | // receive 335 | $data = $client->recv(); 336 | if(!data) 337 | die("receive failed."); 338 | echo $data; 339 | // close connection 340 | $client->close(); 341 | ``` 342 | 343 | 创建一个TCP的同步客户端,此客户端可以用于连接到我们第一个示例TCP服务器,向服务器端发送一个 hello world 字符串,服务端返回一个 Server: hello world 字符串 344 | 345 | 这个客户端是同步阻塞的,connect/send/recv 会等待 IO 完成后再返回。同步阻塞操作并不消耗 CPU 资源。IO 操作未完成当前进程的话会自动转入 sleep 模式,当IO完成后操作系统会唤醒当前进程,继续向下执行。 346 | 347 | - tcp 建连接需要三次握手,所以connect至少要三次网络传输过程 348 | - 在发送少量数据时 $client->send 可以立即返回,发送大量数据时,socket 缓存区可能会被塞满,send 操作会阻塞。 349 | - recv 操作会阻塞等待服务器返回数据 350 | - recv耗时 = 服务器处理时间 + 网络传输耗时 351 | 352 | ###### tcp通信图解 353 | 354 | ![tcp通信图解](https://camo.githubusercontent.com/f8315b68c96c2b19bf6cf89454555404cf4c9e22/68747470733a2f2f7777772e73776f6f6c652e636f6d2f7374617469632f696d6167652f7463705f73796e2e706e67) 355 | 356 | 测试一下 357 | 358 | ```shell 359 | php client.php 360 | Server: hello world 361 | ``` 362 | 363 | 364 | 365 | #### 1.3.8 创建异步TCP客户端 366 | 367 | ###### sample code 368 | 369 | ```php 370 | // async_client.php 371 | $client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC); 372 | 373 | $client->on('connect', function($cli) { 374 | $cli->send("Hello world\n"); 375 | }); 376 | 377 | $client->on('receive', function($cli, $data) { 378 | echo "Received: " . $data . "\n"; 379 | }); 380 | 381 | $client->on('error', function($cli) { 382 | echo "connect failed.\n"; 383 | }); 384 | 385 | $client->on('close', function($cli) { 386 | echo "connection close.\n"; 387 | }); 388 | 389 | $client->connect('127.0.0.1', 9501, 0.5); 390 | ``` 391 | 392 | 异步TCP客户端与同步TCP客户端不同,异步TCP客户端是非阻塞的,可以用来完成高并发的任务。swoole 提供的 redis-async, mysql-async 都是基于异步 swoole_client 实现。 393 | 394 | 异步客户端需要设置回调函数,有4个时间回调必须设置 onConnect, onError, onReceiver, onClose。分别在客户端连接成功,连接失败,收到数据,连接关闭时触发。 395 | 396 | $client->connect() 发起连接的操作会立即返回,不存在等待时间。当对应的IO事件完成后,swoole 底层会自动调用设置好的回调 397 | 398 | > 异步客户端只能用于命令行环境 399 | 400 | 401 | 402 | #### 1.3.9 网络通信协议选择 403 | 404 | ###### 为什么需要用通信协议 405 | 406 | TCP协议在底层机制上解决了UDP协议的顺序和丢包重传的问题,当相比UDP又带来了新的问题,TCP协议是流式的,数据包没有边界,应用程序使用TCP进行通信就面临这个问题——TCP传数据没有边界。 407 | 408 | 因为TCP通信是流式的,在接收一个大数据包时,会被拆分成多个数据包发送,多次Send底层也可能会合并成一次来发送。这里就需要两个操作来解决: 409 | 410 | - 分包:Server 收到了多个数据包,需要拆分数据包 411 | 412 | - 合并包:Server 收到的数据只是包的一部分,需要缓存数据,合并成完整的包 413 | 414 | 所以TCP网络通信时需要设定通信协议,常见的TCP网络通信协议有`HTTP` , `HTTPS`, `FTP`, `SMTP`, `POP3`, `IMAP`, `SSH`, `Redis`, `Memcache`, `MySQL` 415 | 416 | 如果要设计一个通用的协议Server,那么就要按照通用协议的标准去处理网络数据,我们可以自己定义一个满足自己需要的协议。 417 | 418 | Swoole 内置了两个自定义型协议 419 | 420 | ###### EOF结束符协议 421 | 422 | EOF协议处理的原理是每个数据包结尾加一串特殊字符表示包已结束。如 `memcache`, `ftp`,`stmp` 使用 "\r\n" 当结束符。发送数据时在包末尾加上 "\r\n" 就行了。 423 | 424 | 使用EOF协议处理,一定要确保数据包中不会出现EOF,否则分包和预期会不一样 425 | 426 | 在swoole_server 和 swoole_client 的代码中只要设置2个参数就可以用EOF协议处理。 427 | 428 | ```php 429 | $server->set([ 430 | 'open_eof_split' => true, 431 | 'package_eof' => "\r\n", 432 | ]); 433 | $client->set([ 434 | 'open_eof_split' => true, 435 | 'package_eof' => "\r\n", 436 | ]); 437 | ``` 438 | 439 | ###### 【固定包头 + 包体】协议 440 | 441 | 固定包头协议很常用,在BAT 的服务器程序中经常能看到。 442 | 443 | 这种协议的特点是一个数据包总由 `包头` + `包体` 2部分组成。包头中有一个字段指定了包体或整个包的长度,长度一般使用 2或4 byte 的整数来表示。服务器收到包头后,可根据长度值来精确地控制需要在接收多少数据才算完整的数据包。Swoole 的配置可以很好地支持这种协议,可灵活地设置 4 项参数应对所有情形。 444 | 445 | swoole 的 server 和异步 client 都是在 onReceive 回调函数中处理数据包,当设置好协议部分后,只有收到一个完整的数据包时才会去触发 onReceive 事件。同步客户端在设置了协议处理后,调用 $client->recv() 不再需要传入长度,recv 函数在收到完整数据包或发生错误后返回。 446 | 447 | ```php 448 | $server->set([ 449 | 'open_length_check' => true, 450 | 'package_max_length' => 80 * 1024, 451 | 'package_length_type' => 'n', // see php pack() 452 | 'package_length_offset' => 0, 453 | 'package_body_offset' => 2, 454 | ]); 455 | ``` 456 | 457 | 458 | 459 | #### 1.3.10 使用异步客户端 460 | 461 | php提供的 MySQL,CURL,Redis 等客户端是同步的,会导致服务器发生阻塞。 462 | 463 | swoole 提供了常用的异步客户端组件来解决此问题。在编写纯异步服务器程序时,可以使用这些异步客户端。 464 | 465 | 异步客户端可以配合使用 SplQueue 实现连接池,以达到长连接复用的目的。在实际项目中可以使用 php 提供的 Yield/Generator 语法实现半协程的异步框架。也可以使用基于 Promises 来简化异步程序的编写。 466 | 467 | ###### MySQL 468 | 469 | ```php 470 | $db = new Swoole\MySQL; 471 | $server = [ 472 | 'host' => '127.0.0.1', 473 | 'user' => 'test', 474 | 'password' => 'test', 475 | 'database' => 'test', 476 | ]; 477 | $db->connect($server, function($db, $result) { 478 | $db->query('show tables', function(Swoole\MySQL $db, $result) { 479 | var_dump($result); 480 | $db->close(); 481 | }); 482 | }); 483 | ``` 484 | 485 | 与 mysqli 和 PDO 等客户端不同,Swoole\MySQL 是异步非阻塞的,连接服务器,执行SQL 时,需要传入一个回调函数。connect 的结果不在返回值中,而是在回调函数中。query 的结果也需要在回调函数中处理。 486 | 487 | ###### Redis 488 | 489 | ```php 490 | $redis = new Swoole\Redis; 491 | $redis->connect('127.0.0.1', 6379, function ($redis, $result) { 492 | $redis->set('test_key', 'value', function($redis, $result) { 493 | $redis->get('test_key', function($redis, $result) { 494 | var_dump($result); 495 | }); 496 | }); 497 | }); 498 | ``` 499 | 500 | Swoole\Redis 需要 swoole编译安装 hiredis,详细文档参见 501 | 502 | [异步Redis客户端]: https://wiki.swoole.com/wiki/page/p-redis.html 503 | 504 | ###### Http 505 | 506 | ```php 507 | $cli = new Swoole\Http\Client('127.0.0.1', 80); 508 | $cli->setHeaders(['User-Agent' => 'swoole-http-client']); 509 | $cli->setCookies(['test' => 'value']); 510 | 511 | $cli->post('/dump.php', ['test' => 'abc'], function($cli) { 512 | var_dump($cli->body); 513 | $cli->get('/index.php', function($cli) { 514 | var_dump($cli->cookies); 515 | var_dump($cli->headers); 516 | }); 517 | }); 518 | ``` 519 | 520 | Swoole\Http\Client 的作用与CURL 完全一致,它完整地实现了 Http 客户端的相关功能。详细参见 521 | 522 | [HttpClient文档]: https://wiki.swoole.com/wiki/page/p-http_client.html 523 | 524 | ###### 其他客户端 525 | 526 | swoole 底层目前只提供了最常用的 MySQL, Redis,Http 异步客户端,如果我们应用程序中需要实现其他协议客户端,比如Kafka,AMQP 等协议,可以基于Swoole\Client 异步TCP 客户端,开发相关协议解析代码自行实现。 527 | 528 | #### 1.3.11 多进程共享数据 529 | 530 | 由于php本身不支持多线程,因此swoole使用多进程模式,在多进程模式下存在进程内存隔离,在工作进程内修改 global 全局变量和超全局变量时,在其他进程是无效的。 531 | 532 | > 设置 worker_num = 1 时,不存在进程隔离,可以使用全局变量保存数据 533 | 534 | ##### 进程隔离 535 | 536 | ```php 537 | $fds = []; 538 | $server->on('connect', function($server, $fd) { 539 | echo "connection open: {$fd}\n"; 540 | global $fds; 541 | $fds[] = $fd; 542 | var_dump($fds); 543 | }); 544 | ``` 545 | 546 | `$fds` 虽然是全局变量,当只在当前进程有效,`swoole` 服务器底层会创建多个 `Worker` 进程,在`var_dump($fds)` 打印出来的值,只有部分连接的 `fd`。 547 | 548 | 对应的解决方案是使用外部存储服务: 549 | 550 | - 数据库,如:`MySQL`, `MongoDB` 551 | - 缓存服务器,如:`Redis`, `Memcache` 552 | - 磁盘文件,多进程并发读写时需要加锁 553 | 554 | 普通的数据库和磁盘文件操作,存在较多的 `IO` 等待事件,因此推荐使用: 555 | 556 | - `Redis` 内存 noSQL,读写速度极快 557 | - `/dev/shm` 内存文件系统,读写操作全部在内存中完成,无IO损耗,性能极高 558 | 559 | 除了使用存储之外,还可以使用共享内存来保存数据 560 | 561 | ##### 共享内存 562 | 563 | `PHP` 提供了多套共享内存的扩展,但实际上真正在实际项目中可用的并不多。 564 | 565 | ###### shm扩展 566 | 567 | 提供了 `shm_put_var` 和 `shm_get_var` 共享内存读写方法,但其底层实现使用链表结构,在保存大量数值时时间复杂度为 O(N),性能非常差。并且读写数据没有加锁,存在数据同步问题,需要使用者自行加锁。 568 | 569 | > 不推荐使用 570 | 571 | ###### shmop扩展 572 | 573 | 提供了 `shmop_read` 和 `shmop_write` 共享内存读写方法,仅提供了基础的共享内存操作指令,并未提供数据结构和封装,不适合普通开发者使用。 574 | 575 | > 不推荐使用 576 | 577 | ###### apcu扩展 578 | 579 | 提供了 `apc_fetch`和`apc_store`,可以使用 key-value 方式访问,APC 扩展总体上是可以用于实际项目的,缺点是锁的粒度比较粗,在大量并发读写操作时锁的碰撞较为密集。 580 | 581 | > `yac` 扩展,不适合用于保存数据,其设计原理导致存在一定的数据 miss 率,仅作为缓存,不可作为存储。 582 | 583 | ###### swoole\Table 584 | 585 | `swoole` 官方提供的共享内存读写工具,提供了 key-value 访问方式,使用非常简单。底层使用自旋锁实现,在大量并发读写操作时性能依然非常强劲。推荐使用。 `Swoole\Table` 目前仍存在两个缺点,使用时需要根据情况来选择 586 | 587 | - 需要预先申请内存,`Table` 在使用前就需要分配好内存,可能会占用较多的内存 588 | - 无法动态扩容,`Table` 内存管理方式是静态的,不支持动态申请新内存,因此一个 `Table` 在设置好函数并创建之后,使用时不能超出限制。 589 | 590 | 591 | 592 | #### 1.3.12 使用协程客户端 593 | 594 | 在swoole 的 4.x 版本中,协程取代了异步回调,作为我们推荐使用的编程方式。 595 | 596 | 协程解决了异步回调编程困难的情况。使用协程可以以传统同步编程的方式来些代码,而底层又能自动切换为异步IO。 597 | 598 | 协程往往用来提供系统设计的并发能力。 599 | 600 | > 使用swoole 版本 4.2.5+ 601 | 602 | ###### sample code 603 | 604 | ```php 605 | $http = new swoole_http_server('0.0.0.0', 9501); 606 | $http->on('request', function ($request, $response) { 607 | $db = new Swoole\Coroutine\MySQL(); 608 | $db->connect([ 609 | 'host' => '127.0.0.1', 610 | 'port' => 3306, 611 | 'user' => 'user', 612 | 'password' => 'pass', 613 | 'database' => 'test', 614 | ]); 615 | $data = $db->query('select * from test_table'); 616 | $response->end(json_encode($data)); 617 | }); 618 | $http->start(); 619 | ``` 620 | 621 | 上面的代码编写与同步阻塞模式程序完全一致,但是底层自动进行了协程切换处理,变为异步 IO, 因此服务器可以用来处理大量并发,每一个请求都会创建一个新的协程,执行对应的代码。 622 | 623 | 如果某个请求处理较慢,会引起这个请求被挂起,不影响其他请求的处理。 624 | 625 | ###### 其他协程组件 626 | 627 | `swoole4` 扩展提供了丰富的协程组件,如 `Redis`,`TCP/UDP/Unix` 客户端,`Http/WebSocket/Http2` 客户端,使用这些组件可以方便地实现高性能的并发编程。 628 | 629 | 使用协程时参见 630 | 631 | [协程编程须知]: https://wiki.swoole.com/wiki/page/851.html 632 | 633 | ###### 使用场景 634 | 635 | 适合用协程的场景有 636 | 637 | - 高并发服务,如秒杀系统,高性能API接口,RPC 服务器。 使用协程可以让服务器容错率大大提高,某些接口出现故障时也不会导致整个服务瘫痪掉 638 | - 爬虫。可以实现强大的并发能力,即使是慢速的网络环境,也可以高效利用带宽 639 | - 即时通讯。如`IM` 聊天,游戏服务器,消息服务器等。可以确保消息通信完全无阻塞,每个消息包均可即使地被处理。 640 | 641 | #### 1.3.13 协程:并发 shell_exec 642 | 643 | 在php程序中经常要用到 shell_exec 执行一些命令。而普通的 shell_exec 是阻塞的,如果命令执行的时间较长,那么很可能导致进程完全被卡住。在 swooel4 协程环境下可以用 `Co::exec` 并发地执行很多命令 644 | 645 | #### 1.3.14 协程:Go + Chan + Defer 646 | 647 | swoole4 为 php 提供了强大的 `CSP` 协程编程模式。底层提供了3个关键词,可以方便地实现各类功能。 648 | 649 | - swoole4 提供的 `php协程` 语法借鉴自 `go` 650 | - `php+swoole` 协程与 `go` 各有优势。`go` 是静态编译语言,性能好。php 动态脚本语言,开发速度快。 651 | 652 | > 下面测试速度所用的环境是 php7.2 + swoole4.2 653 | 654 | ###### php关键词 655 | 656 | - `go` :创建一个协程 657 | - `chan` :创建一个通道 658 | - `defer` :延迟任务。在协程退出时执行,先进后出 659 | 660 | 这3个关键词底层表现方式均为***内存操作****,没有任何IO资源消耗,就像php 的数组一样是廉价的,只要有需要就可以直接使用。所以与 `socket`和`file` 操作不同,后者需要向操作系统申请接口和文件描述符,读写可能会产生阻塞的IO等待。 661 | 662 | ##### 协程并发 663 | 664 | 使用`go` 函数可以让一个函数并发地去执行,在编程过程中,如果某一段逻辑可以并发执行,就可以把它放到 `go` 协程中去执行。 665 | 666 | ###### 顺序执行 667 | 668 | ```php 669 | function a() { 670 | sleep(1); 671 | echo 'b'l 672 | } 673 | function b() { 674 | sleep(2); 675 | echo 'b'; 676 | } 677 | a(); 678 | b(); 679 | ``` 680 | 681 | 结果: 682 | 683 | ```shell 684 | bchtf@LAPTOP-0K15EFQI:~$ time php co.php 685 | bc 686 | real 0m2.076s 687 | user 0m0.000s 688 | sys 0m0.078s 689 | ``` 690 | 691 | > 并发执行的任务,其总执行时间等于 max(t1, t2, t3, t4, ....) 692 | > 693 | > 顺序执行的任务,其总执行时间等于 t1 + t2 + t3 + t4 694 | 695 | ##### 协程通信 696 | 697 | 有了go 关键词之后,并发编程简单了很多。于此同时产生一个新问题。如果有2个协程并发执行,另外一个协程需要依赖这两个协程的执行结果,如何解决? 698 | 699 | 办法是使用通道(`channel`),在swoole4 协程中使用 `new chan`来创建一个通道。通道我们可以理解为自带协议调度功能的队列。通道有两个接口 `push` ,`pop`: 700 | 701 | - `push` :向通道中写入内容,如果已满,它会进入等待状态,有空间的时候才自动恢复 702 | - `pop` :从通道中读内容,如果为空则进入等待状态,有数据时自动恢复 703 | 704 | 使用通道可以方便地实现**并发管理** 705 | 706 | ```php 707 | $chan = new chan(2); 708 | // 协程1 709 | go(function () use ($chan) { 710 | $result = []; 711 | for($i = 0; $i < 2; $i++) { 712 | $result += $chan->pop(); 713 | } 714 | var_dump($result); 715 | }); 716 | // 协程2 717 | go(function () use ($chan) { 718 | $cli = new Swoole\Coroutine\Http\Client('www.qq.com', 80); 719 | $cli->set(['timeout' => 10]); 720 | $cli->setHeaders([ 721 | 'Host' => 'www.qq.com', 722 | 'User-Agent' => 'Chrome/49.0.2587.3', 723 | 'Accept' => 'text/html,application/xhtml+xml,application/xml', 724 | 'Accept-Encoding' => 'gzip', 725 | ]); 726 | $ret = $cli->get('/'); 727 | // $cli->body 响应内容过大,这里用Http状态码作为测试 728 | $chan->push(['www.qq.com' => $cli->statusCode]); 729 | }); 730 | // 协程3 731 | go(function () use ($chan) { 732 | $cli = new Swoole\Coroutine\Http\Client('www.163.com', 80); 733 | $cli->set(['timeout' => 10]); 734 | $cli->setHeaders([ 735 | 'Host' => 'www.163.com', 736 | 'User-Agent' => 'Chrome/49.0.2587.3', 737 | 'Accept' => 'text/html,application/xhtml+xml,application/xml', 738 | 'Accept-Encoding' => 'gzip', 739 | ]); 740 | $ret = $cli->get('/'); 741 | // $cli->body 响应内容过大,这里用Http状态码作为测试 742 | $chan->push(['www.163.com' => $cli->statusCode]); 743 | }); 744 | ``` 745 | 746 | 执行结果: 747 | 748 | ```shell 749 | htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$ time php co2.php 750 | array(2) { 751 | ["www.qq.com"]=> 752 | int(302) 753 | ["www.163.com"]=> 754 | int(200) 755 | } 756 | 757 | real 0m0.268s 758 | user 0m0.016s 759 | sys 0m0.109s 760 | htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$ 761 | ``` 762 | 763 | - 协程1对管道进行两次 pop,刚开始时因为队列为空,所以进入等待状态 764 | - 协程2和协程3执行完成后,会push数据,协程1拿到两个的结果,而这个等待时间仅是 二者取最大的执行时间而已。不用串行等待了。 765 | 766 | ##### 延迟任务 767 | 768 | 在协程编程中,可能需要在协程退出时自动执行一些任务做清理工作。类似于php 的`register_shutdown_function` ,在 swoole4 中可以使用 `defer` 实现 769 | 770 | ```php 771 | Swoole\Runtime::enableCoroutine(); 772 | go(function () { 773 | echo 'a'; 774 | defer(function () { 775 | echo '~a'; 776 | }); 777 | echo 'b'; 778 | defer(function () { 779 | echo '~b'; 780 | }); 781 | sleep(1); 782 | echo 'c'; 783 | }); 784 | ``` 785 | 786 | ###### 执行结果: 787 | 788 | ```shell 789 | htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$ time php defer.php 790 | abc~b~a 791 | real 0m1.068s 792 | user 0m0.016s 793 | sys 0m0.047s 794 | htf@LAPTOP-0K15EFQI:~/swoole-src/examples/5.0$ 795 | ``` 796 | 797 | 798 | 799 | #### 1.3.15 协程:实现 Go 语言风格的 defer 800 | 801 | 由于`go` 语言不提供析构方法,而php 对象是有析构函数的,我们使用 __destruct 就可以实现 `go` 风格的 `defer`. 802 | 803 | ###### sample code 804 | 805 | ```php 806 | class DeferTask 807 | { 808 | private $task; 809 | function add(callable $fn) { 810 | $this->tasks[] = $fn; 811 | } 812 | function __destruct() { 813 | // 反转 814 | $tasks = array_reverse($this->tasks); 815 | foreach($tasks as $fn) { 816 | $fn(); 817 | } 818 | } 819 | } 820 | ``` 821 | 822 | - 基于php对象的析构方法实现的`defer` 更为灵活,如果希望改变执行的实际,甚至可以将 `DeferTask` 对象赋值给其他生命周期较长的变量,`defer` 任务的执行可以延长生命周期。 823 | - 默认情况下和 `go` 的 `defer` 一致,在函数退出时自动执行。 824 | 825 | ###### 使用 defer 826 | 827 | ```php 828 | function testDefer() { 829 | $a = new DeferTask(); 830 | 831 | $a->add(function () { 832 | // details 833 | }); 834 | 835 | $a->add(function () { 836 | // details2 837 | }); 838 | // 函数结束时,对象自动 destruct,defer 任务自动执行 839 | return $retval; 840 | } 841 | ``` 842 | 843 | 844 | 845 | #### 1.3.16 协程: 实现 sync.WaitGroup 功能 846 | 847 | 在swoole4 中可以使用 `channel` 实现协程间通信,依赖管理,协程同步。基于 `channel` 可以轻松实现 `go` 的 `sync.WaitGroup`功能。 848 | 849 | ###### sample code 850 | 851 | ```php 852 | class WaitGroup 853 | { 854 | private $count = 0; 855 | private $chan; 856 | 857 | public function __construct() 858 | { 859 | $this->chan = new chan; 860 | } 861 | 862 | // 增加计数 863 | public function add() 864 | { 865 | $this->count++; 866 | } 867 | 868 | // 任务完成 869 | public function done() 870 | { 871 | $this->chan->push(true); 872 | } 873 | 874 | // 等待所有任务完成,恢复当前协程的执行 875 | public function wait() 876 | { 877 | while($this->count--) { 878 | $this->chan->pop(); 879 | } 880 | } 881 | } 882 | ``` 883 | 884 | - `WaitGroup` 对象可以复用,`add`, `done`, `wait` 之后可以再次使用 885 | 886 | ###### sample code 887 | 888 | ```php 889 | go(function () { 890 | $wg = new WaitGroup(); 891 | $result = []; 892 | $wg->add(); 893 | // 启动第一个协程 894 | go(function () use ($wg, &$result) { 895 | // 启动一个协程客户端client来请求淘宝网首页 896 | $cli = new Client('www.taobao.com', 443, true); 897 | $cli->setHeaders([ 898 | 'Host' => 'www.taobao.com', 899 | 'User-Agent' => 'Chrome/49.0.2587.3', 900 | 'Accept' => 'text/html,application/xhtml+xml,application/xml', 901 | 'Accept-Encoding' => 'gzip', 902 | ]); 903 | $cli->set(['timeout' => 1]); 904 | $cli->get('/index.php'); 905 | $result['taobao'] = $cli->body; 906 | $cli->close(); 907 | $wg->done(); 908 | }); 909 | $wg->add(); 910 | // 启动第二个协程 911 | go(function () use ($wg, &$result) { 912 | // 启动一个协程客户端client来请求百度首页 913 | $cli = new Client('www.baidu.com', 443, true); 914 | $cli->setHeaders([ 915 | 'Host' => 'www.baidu.com', 916 | 'User-Agent' => 'Chrome/49.0.2587.3', 917 | 'Accept' => 'text/html,application/xhtml+xml,application/xml', 918 | 'Accept-Encoding' => 'gzip', 919 | ]); 920 | $cli->set(['timeout' => 1]); 921 | $cli->get('/index.php'); 922 | $result['taobao'] = $cli->body; 923 | $cli->close(); 924 | $wg->done(); 925 | }); 926 | // 挂起当前协程,等待所有任务完成后恢复 927 | $wg->wait(); 928 | // 此时$result 已经包含了2个任务执行结果 929 | var_dump($result); 930 | }) 931 | ``` 932 | 933 | 934 | 935 | ### 1.4 注意点 936 | 937 | #### 1.4.1 sleep/usleep 的影响 938 | 939 | 在异步IO 的程序中,**不允许使用sleep/usleep/time_sleep_until/time_nanosleep, 等睡眠函数** 940 | 941 | 原因如下: 942 | 943 | - sleep等函数会使进程陷入睡眠阻塞 944 | - 直到指定的时间后OS才会重新唤起当前睡眠了的进程 945 | - sleep执行过程中,只有signal才能打断 946 | - 由于swoole的signal 处理是基于signalfd 实现的,所以即使发送 signal 也无法中断swoole 的sleep 947 | 948 | swoole提供的 `swoole_event_add`, `swoole_timer_tick`, `swoole_timer_after`, `swoole_process:signal`, 异步 swoole_client 在进程 sleep 后会停止工作,swoole_serer 也无法处理新的请求。 949 | 950 | ###### sample code 951 | 952 | ```php 953 | $serv = new swoole_server('127.0.0.1', 9501); 954 | $serv->set(['worker_num' => 1]); 955 | $serv->on('receive', function($serv, $fd, $from_id, $data) { 956 | sleep(100); 957 | $serv->send($fd, 'Swoole: ' . $data); 958 | }); 959 | $serv->start(); 960 | ``` 961 | 962 | onReceive 事件中执行了 sleep函数,100秒内我们的 server无法处理任何进来的请求。 963 | 964 | #### 1.4.2 exit/die 函数的影响 965 | 966 | 在swoole代码中尽量别使用 `exit` 和 `die`,如果PHP代码中有 `exit`和 `die` ,当前工作的 `worker` 进程,`task` 进程,`user`进程,以及 `swoole_process` 进程会立即退出。 967 | 968 | 使用 `exit` / `die` 之后,`worker` 进程会因为异常而退出,被 `master` 进程再次唤起,最终造成进程不断退出又不断启动和产生大量报警日志。 969 | 970 | 建议使用 `try` / `catch` 来替换掉 `exit` / `die` ,实现中断执行,以跳出当前的 php 函数调用栈。 971 | 972 | ```php 973 | function swoole_exit(msg) { 974 | // php-fpm的环境 975 | if(ENV == 'PHP') 976 | exit; 977 | // swoole的环境 978 | else 979 | throw new Swoole\ExitException($msg); 980 | } 981 | ``` 982 | 983 | > 上面的段代码还不能在项目中直接用,我们还要实现 ENV 变量和 Swoole\ExitException. 984 | 985 | 异常处理的方式比 `exit` / `die` 要更友好,因为异常是可控的,`exit` / `die` 不可控,在最外层执行 try catch 操作就能捕获异常,仅终止当前的任务。`worker` 进程可以继续处理新的请求,而 `exit` / `die` 会导致进程直接退出,当前进程保存的所有资源和变量值等都会被销毁。如果进程内还有其他任务要处理,此操作也会导致数据全部丢失 :( 986 | 987 | 988 | 989 | #### 1.4.3 while 循环的影响 990 | 991 | 异步程序如果碰到了死循环,事件将无法触发。异步IO程序使用 `Reactor` 模型,运行过程中必须在 `reactor->wait` 处轮询。如果遇到死循环,那么程序的控制权就在 while 中了,reactor 无法获取控制权,无法检测事件。所以 IO 事件回调函数也将无法触发。 992 | 993 | > 密集运算的代码没有任何IO操作,所以不能称之为阻塞。 994 | 995 | ###### sample code 996 | 997 | ```php 998 | $serv = new swoole_server('127.0.0.1', 9501); 999 | $serv->set(['worker_num' => 1]); 1000 | $serv->on('receive', function ($serv, $fd, $reactorId, $data) { 1001 | while(1) { 1002 | $i++; 1003 | } 1004 | $serv->send($fd, 'Swoole: ' . $data); 1005 | }); 1006 | $serv->start(); 1007 | ``` 1008 | 1009 | onReceive 事件中执行了死循环,结束不掉,所以 server 此时收不到任何客户端的请求。 1010 | 1011 | #### 1.4.4 stat 缓存清理 1012 | 1013 | php 底层对 `stat` 系统调用增加了 `cache` , 在使用 `stat`, `fstat`, `filemtime` 等函数时,底层可能会命中缓存,返回历史的数据。 1014 | 1015 | 我们可以主动用 `clearstatcache` 方法来清理 `stat` 文件缓存。 1016 | 1017 | #### 1.4.5 mt_rand 随机数 1018 | 1019 | 在 swoole 中如果我们于父进程中调用了 `mt_rand`,不同的子进程内再次调用 `mt_rand` , 返回的结果会一模一样。如果想要得到真正的随机,我们要在子进程中重新 "种时间种子"。 1020 | 1021 | > 注:shuffer 和 array_rand 等依赖随机数的 php 函数同样会受到影响。 1022 | 1023 | ```php 1024 | mt_rand(0, 1); 1025 | // start 1026 | $worker_num = 16; 1027 | // fork 1028 | for($i = 0; $i < $worker_num; $i++) { 1029 | $process = new swoole_process('child_async', false, 2); 1030 | $pid = $process->start(); 1031 | } 1032 | // async exec 1033 | function child_async(swoole_process $worker) { 1034 | mt_srand(); 1035 | echo mt_rand(0, 100) . PHP_EOL; 1036 | $worker->exit(); 1037 | } 1038 | ``` 1039 | 1040 | 1041 | 1042 | #### 1.4.6 进程隔离 1043 | 1044 | 进程隔离是很多新手会经常遇到的问题。修改了全局变量的值,为什么就是不生效?原因在于全局变量在不同的进程,不同的内存空间里是隔离的。所以我在一个进程里改的全局变量,在另一个进程里使用时不会生效。 1045 | 1046 | - 不同进程的php变量是不共享的,即使是全局变量,在A进程内修改了它的值,在B进程里面也是无效的。 1047 | - 如果需要在不同的 worker 进程中共享数据,可以选择 `redis`, `mysql`, 文件,`Swoole\Table`, `APCu`, `shmget` 等工具来实现。 1048 | - 不同进程的文件句柄是隔离的,所以在A进程创建的 socket 连接或打开的文件,在B进程里是无效的,即使是将它的fd发送到B进程也是不可用的。 1049 | 1050 | ###### sample code 1051 | 1052 | ```php 1053 | $server = new Swoole\Http\Server('127.0.0.1', 9501); 1054 | $i = 1; 1055 | $server->on('Request', function($request, $response) { 1056 | global $i; 1057 | $response->end($i++); 1058 | }); 1059 | $server->start(); 1060 | ``` 1061 | 1062 | 在多进程的服务器中,$i 变量虽然是全局变量(global),但由于进程隔离的原因,假设现在有 4 个进程在工作中,在进程1中进行 $i++, 实际上只有进程1中的 $i 变成 2,另外其他3个进程里的 $i 还是1 1063 | 1064 | 正确的做法是用 Swoole 提供的 Swoole\Table, 或 Swoole\Atomic 数据结构来保存数据,如上面代码可以这样实现: 1065 | 1066 | ```php 1067 | $server = new Swoole\Http\Server('127.0.0.1', 9501); 1068 | $atomic = new Swoole\Atomic(1); 1069 | $server->on('Request', function($request, $response) use ($atomic) { 1070 | $response->end($atomic->add(1)); 1071 | }); 1072 | ``` 1073 | 1074 | 注:Swoole\Atomic 是建立在共享内存之上的,使用 add 方法加1时,在其他工作进程里也有效。 1075 | 1076 | ### 1.5 相关linux内核参数调整 1077 | 1078 | ##### ulimit 设置 1079 | 1080 | ulimit -n 要调整为 100000 甚至更大。命令行执行 ulimit -n 100000 即可修改。如果不能修改,需要设置 /etc/security/limits.conf, 加入 1081 | 1082 | ```ini 1083 | * soft nofile 262140 1084 | * hard nofile 262140 1085 | root soft nofile 262149 1086 | root hard nofile 262140 1087 | * soft core unlimited 1088 | * hard core unlimited 1089 | root soft core unlimited 1090 | root hard core unlimited 1091 | ``` 1092 | 1093 | 注意:修改 `limits.conf` 文件后,需要重启系统才能生效 1094 | 1095 | ##### 内核设置 1096 | 1097 | linux 操作系统修改内核参数的方式有三种: 1098 | 1099 | 1. 方式1:修改 `/etc/sysctl.conf` 文件,加入配置选项,格式为 `key` = `value` 形式,修改保存后调用 `sysctl -p` 加载新配置 1100 | 2. 方式2:使用 `sysctl` 命令临时修改,如:`sysctl -w net.ipv4.tcp_mem = "379008 505344 758016"` 1101 | 3. 方式3:直接修改 `/proc/sys/` 目录下的文件,如:`echo "379008 505344 758016"` 1102 | 1103 | > 第一种方式要在操作系统重启后才能生效。第二和第三种方法重启后反而会失效 1104 | 1105 | **net.unix.max_dgram_qlen=100** swoole 使用 unix socket dgram 来做进程间通信,如果请求量很大,需要调整此参数。系统默认为 10,可以设置为 100或者更大。或者增加 worker 进程的数量,减少单个worker进程分配的请求量。 1106 | 1107 | ###### net.core.wmem_max 1108 | 1109 | 修改此参数增加 socket 缓存区的内存大小 1110 | 1111 | ```ini 1112 | net.ipv4.tcp_mem = 379008 505344 758016 1113 | net.ipv4.tcp_wmem = 4096 16384 4194304 1114 | net.ipv4.tcp_rmem = 4096 87380 4194304 1115 | net.core.wmem_default = 8388608 1116 | net.core.rmem_default = 8388608 1117 | net.core.rmem_max = 16777216 1118 | net.core.wmem_max = 16777216 1119 | ``` 1120 | 1121 | ###### net.ipv4.tcp_tw_reuse 1122 | 1123 | 是否socket reuse,此函数的作用是重启server 时可以快速重新使用监听的端口。如果没有设置此参数会导致 server 重启时发生端口未及时释放而启动失败。 1124 | 1125 | ###### net.ipv4.tcp_tw_recycle 1126 | 1127 | 使用 socket 快速回收,短链接 Server 需要开启此参数。此参数表示开启 tcp 连接中 TIME-WAIT sockets 的快速回收,Linux 系统中默认为 0,表示关闭。打开此参数可能会导致 NAT 用户连接不稳定,请谨慎测试后再开启。 1128 | 1129 | ###### 消息队列设置 1130 | 1131 | 当使用消息队列作为进程间通信方式时,需要调整此内核参数 1132 | 1133 | - kernel.msgmnb = 4203520, 消息队列的最大字节数 1134 | - kernel.msgmni = 64, 最多允许创建多少个消息队列 1135 | - kernel.msgmax = 8192,消息队列单条数据最大的长度 1136 | 1137 | ###### 开启CoreDump 1138 | 1139 | 设置内核参数 1140 | 1141 | ```ini 1142 | kernel.core_pattern = /data/core_files/core-%e-%p-%t 1143 | ``` 1144 | 1145 | 通过ulimit -c 命令查看当前 coredump 文件的限制 1146 | 1147 | ```ini 1148 | ulimit -c 1149 | ``` 1150 | 1151 | 如果为 0,需要修改 /etc/security/limits.conf 设置limit 1152 | 1153 | > 开启core-dump 后,一旦程序发生异常,会将进程导出到文件。对于debug有很大帮助 1154 | > 1155 | > ###### 其他重要配置 1156 | > 1157 | > - net.ipv4.tcp_syncookies = 1 1158 | > - net.ipv4.tcp_max_syn_backlog = 81920 1159 | > - net.ipv4.tcp_synack_retries = 3 1160 | > - net.ipv4.tcp_syn_retries = 3 1161 | > - net.ipv4.tcp_fin_timeout = 30 1162 | > - net.ipv4.tcp_keepalive_time = 300 1163 | > - net.ipv4.tcp_tw_reuse = 1 1164 | > - net.ipv4.tcp_tw_recycle = 1 1165 | > - net.ipv4.ip_local_port_range = 20000 65000 1166 | > - net.ipv4.tcp_max_tw_buckets = 200000 1167 | > - net.ipv4.route.max_size = 5242880 1168 | > 1169 | > ###### 查看配置是否生效 1170 | > 1171 | > 比如修改 `net.unix.max_dgram_qlen = 100` 后,通过 `cat /proc/sys/net/unix/max_dgram_qlen`. 如果修改成功,这里是新设置的值。 1172 | 1173 | ## 2. Server 1174 | 1175 | ### 2.1 函数列表 1176 | 1177 | ### 2.2 属性列表 1178 | 1179 | ### 2.3 配置选项 1180 | 1181 | ### 2.4 监听端口 1182 | 1183 | ### 2.5 预定义常量 1184 | 1185 | ### 2.6 事件回调函数 1186 | 1187 | ### 2.7 高级特性 1188 | 1189 | #### 2.7.5 TCP-Keepalive 死连接检测 1190 | 1191 | 在tcp中有一个 `keep-Alive` 机制可以检测死连接,应用层如果对死连接周期不敏感或没有实现心跳机制,可以用操作系统提供的 `keepalive` 机制来踢掉死连接。在 `Server::set` 配置中增加 `open_tcp_keepalive >= 1` 表示启用 `tcp keepalive` 。另外有三个参数可以对 `keepAlive` 进行微调。 1192 | 1193 | `keep-Alive` 机制不会强制切断连接,如果连接存在但是一直不发生数据交互,那么 `keep-Alive` 不会去切断连接。而应用层实现的心跳检测 `heartbeat_check` 即使连接存在,但在不产生数据交互的情况下仍会强制切断连接。 1194 | 1195 | > 推荐使用 `heartbeat_check` 实现心跳检测 1196 | 1197 | ###### tcp_keepidle 1198 | 1199 | 连接在 n 秒内没有任何数据请求,则将开始对此连接进行探测。 1200 | 1201 | ###### tcp_keepcount 1202 | 1203 | 探测的次数,超过次数后将 `close` 此连接 1204 | 1205 | ###### tcp_keepinterval 1206 | 1207 | 探测的时间间隔,单位为秒 1208 | 1209 | #### 2.7.6 TCP服务器心跳维持方案 1210 | 1211 | 正常情况下客户端中断 TCP 连接时,会发送一个 FIN 包,进行4次断开握手来通知服务器。但一些异常情况下,如果客户端突然断电断网或者网络异常,服务器可能无法得知客户端已断开了连接。 1212 | 1213 | 尤其是移动网络,TCP连接非常不稳定,因此需要一套机制来保证服务器和客户端之间连接的有效性。 1214 | 1215 | swoole扩展本身内置了这种机制,开发者只需要配置一个参数即可启用。swoole在每次收到客户端数据时,会记录一个时间戳,当客户端在一定时间内未向服务器端发送数据,则服务器会自动切断连接。 1216 | 1217 | ###### 使用方法: 1218 | 1219 | ```php 1220 | $serv->set(array( 1221 | 'heartbeat_check_interval' => 5, 1222 | 'heartbeat_idle_time' => 10, 1223 | )); 1224 | ``` 1225 | 1226 | 上面的设置就是每 5 秒检测一次心跳,一个TCP连接如果在 10 秒内未向服务器端发送数据,则将被切断。 1227 | 1228 | ###### 高级用法: 1229 | 1230 | 使用 swoole_server::heartbeat() 函数手工检测心跳是否到期。此函数会返回闲置时间超过 heartbeat_idle_time 的所有 TCP 连接。程序中可以将这些连接做一些操作,如发送数据或关闭连接。 1231 | 1232 | ### 2.8 压力测试 1233 | 1234 | ## 3. Coroutine 1235 | 1236 | 从swoole2.0开始,提供了协程(coroutine)特性,可使用协程 + 通道的全新编程模式来代替异步回调。应用层可以使用完全同步的编程方式,底层调度自动实现异步 IO 1237 | 1238 | ```php 1239 | go(function() { 1240 | $redis = new Swoole\Coroutine\Redis(); 1241 | $redis->connect('127.0.0.1', 6379); 1242 | $val = $redis->get('key'); 1243 | }); 1244 | ``` 1245 | 1246 | > 4.0.0+ 仅支持 PHP7的版本 1247 | > 1248 | > 4.0.1 版本开始,去除了 --enable-coroutine 编译选项,改为 1249 | > 1250 | > [动态配置]: https://wiki.swoole.com/wiki/page/949.html 1251 | > 1252 | > 。 1253 | 1254 | 协程可以理解为纯用户态的线程,其通过**协作** 而不是抢占来进行切换。相对于进程或线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。swoole 可以为每一个请求创建对应的协程。根据 IO 的状态来合理的调度协程,这会带来几点好处: 1255 | 1256 | 1. 开发者可以无感知地用同步的代码编写方式达到异步 IO 的效果和性能。避免了传统异步回调带来的离散代码逻辑和陷入多层回调中,导致代码无法维护。 1257 | 2. 由于底层封装了协程,所以对比传统的php层协程框架,我们不需要使用 `yield` 关键词来表示一个协程 IO 操作,不需要再对 `yield` 语义进行深入理解以及对每一级的调用都修改为 `yield` ,提高编码效率 1258 | 1259 | 可满足大部分的场景需要,对于需要自定义网络协议的,开发者可以用协程 的 TCP 或 UDP 接口去封装自定义的协议。 1260 | 1261 | ###### 环境要去 1262 | 1263 | - PHP version >=7.0 1264 | - 基于 `Server`, `Http\Server`, `WebSocket\Server` 进行开发,底层在 `onRequest`, `onReceive`, `onConnect` 等事件回调之前自动创建一个协程,在回调函数中使用协程 API 1265 | - 使用 coroutine::create 或 go 方法创建协程,在创建的协程中使用协程 API 1266 | 1267 | ###### 相关配置 1268 | 1269 | 在`server` 的 `set` 方法中增加了一个配置参数 `max_coroutine`, 用于配置一个 `worker` 进程最多同时处理的协程数。因为随着 `worker` 进程处理的协程数目越来越多,其占用的内存也会增加,为了避免超出 php 的 `memory_limit` 限制,需要根据业务的实际压力测试结果来调,默认为 3000 1270 | 1271 | ###### sample code 1272 | 1273 | ```php 1274 | $http = new swoole_http_server('127.0.0.1', 9501); 1275 | $http->on('request', function($request, $response) { 1276 | $client = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP); 1277 | $client->connect('127.0.0.1', 8888, 0.5); 1278 | // 调用 connect 将触发协程切换 1279 | $client->send('hello world from swoole'); 1280 | // recv() 也会触发协程切换 1281 | $ret = $client->recv(); 1282 | $response->header('Content-Type', "text/plain"); 1283 | $response->end($ret); 1284 | $client->close(); 1285 | }); 1286 | $http->start(); 1287 | ``` 1288 | 1289 | 当代码执行到 connect() 和 recv() 时,底层会触发进行协程切换,此时可以去处理其他的事件或接收新的请求。当此客户端 connect 成功或后端服务 **回包**后,底层会恢复协程上下文,代码继续从切换点开始恢复执行。这整个过程底层自动完成,开发者不需要参与。 1290 | 1291 | ###### 全局变量 1292 | 1293 | 1. 全局变量:协程使得原有的异步逻辑同步化,但是在协程的切换是隐式发生的,所以在协程切换的前后不能保证全局变量以及 static 变量的一致性。 1294 | 2. 请勿在 4.0 一下的版本的两种场景下触发协程切换: 1295 | - 析构函数 1296 | - 魔术方法 __call(), __ get(), __set() 等 1297 | 3. 与 xdebug,xhprof,blackfire 等 zend 扩展不兼容。比如不能使用 xhprof 对协程 server 进行性能采样分析。 1298 | 1299 | ###### 协程组件 1300 | 1301 | 1. TCP/UDP Client: Swoole\Coroutine\Client 1302 | 2. HTTP/WebSocket Client: Swoole\Coroutine\HTTP\Client 1303 | 3. HTTP2 Client: Swoole\Coroutine\HTTP2\Client 1304 | 4. Redis Client: Swoole\Coroutine\Redis 1305 | 5. MySQL Client: Swoole\Coroutine\MySQL 1306 | 6. Postgresql Client: Swoole\Coroutine\PostgreSQL 1307 | 1308 | - 在协程 Server 中使用协程版 Client, 可以实现全异步 Server 1309 | - 在其他程序中可以使用 go 关键词手动创建协程 1310 | - 同时 Swoole 提供了协程工具集:Swoole\Coroutine, 提供了获取当前协程 id,反射调用等能力。 1311 | 1312 | ### 3.1 Coroutine 1313 | 1314 | ###### 创建协程 1315 | 1316 | ```php 1317 | go(function() { 1318 | co::sleep(0.5); 1319 | echo "hello"; 1320 | }); 1321 | go('test'); 1322 | go([$object, 'method']); 1323 | ``` 1324 | 1325 | ###### channel 操作 1326 | 1327 | ```php 1328 | $c = new chan(1); 1329 | $c->push($data); 1330 | $c->pop(); 1331 | ``` 1332 | 1333 | ###### 协程客户端 1334 | 1335 | ```php 1336 | $redis = new Co\Redis; 1337 | $mysql = new Co\MySQL; 1338 | $http = new Co\Http\Client; 1339 | $tcp = new Co\Client; 1340 | $http2 = new Co\Http2\Client; 1341 | ``` 1342 | 1343 | ###### 其他API 1344 | 1345 | ```php 1346 | co::sleep(100); 1347 | co::fread($fp); 1348 | co::gethostbyname('www.baidu.com'); 1349 | ``` 1350 | 1351 | ###### 延迟执行 1352 | 1353 | ```php 1354 | defer(function () use ($db) { 1355 | $db->close(); 1356 | }); 1357 | ``` 1358 | 1359 | #### 3.1.1 Coroutine::getCid 1360 | 1361 | #### 3.1.2 Coroutine::create 1362 | 1363 | #### 3.1.3 Coroutine::yield 1364 | 1365 | #### 3.1.4 Coroutine::resume 1366 | 1367 | #### 3.1.5 Coroutine::defer 1368 | 1369 | #### 3.1.6 Coroutine::fread 1370 | 1371 | #### 3.1.7 Coroutine::fgets 1372 | 1373 | #### 3.1.8 Coroutine::write 1374 | 1375 | #### 3.1.9 Coroutine::sleep 1376 | 1377 | #### 3.1.10 Coroutine::gethostbyname 1378 | 1379 | #### 3.1.11 Coroutine::getaddrinfo 1380 | 1381 | #### 3.1.12 Coroutine::exec 1382 | 1383 | #### 3.1.13 Coroutine::readFile 1384 | 1385 | #### 3.1.14 Coroutine::writeFile 1386 | 1387 | #### 3.1.15 Coroutine::stats 1388 | 1389 | #### 3.1.16 Coroutine::statvfs 1390 | 1391 | #### 3.1.17 Coroutine::getBackTrace 1392 | 1393 | #### 3.1.18 Coroutine::listCoroutines 1394 | 1395 | #### 3.1.19 Coroutine::set 1396 | 1397 | ### 3.2 Coroutine\Channel 1398 | 1399 | #### 3.2.1 Coroutine\Channel->__construct 1400 | 1401 | #### 3.2.2 Coroutine\Channel->push 1402 | 1403 | #### 3.2.3 Coroutine\Channel->pop 1404 | 1405 | #### 3.2.4 Coroutine\Channel->stats 1406 | 1407 | #### 3.2.5 Coroutine\Channel->close 1408 | 1409 | #### 3.2.6 Coroutine\Channel->length 1410 | 1411 | #### 3.2.7 Coroutine\Channel->isEmpty 1412 | 1413 | #### 3.2.8 Coroutine\Channel->isFull 1414 | 1415 | #### 3.2.9 Coroutine\Channel->$capacity 1416 | 1417 | #### 3.2.10 Coroutine\Channel->$errCode 1418 | 1419 | ### 3.3 Coroutine\Client 1420 | 1421 | #### 3.3.1 Coroutine\Client->connect 1422 | 1423 | #### 3.3.2 Coroutine\Client->send 1424 | 1425 | #### 3.3.3 Coroutine\Client->recv 1426 | 1427 | #### 3.3.4 Coroutine\Client->close 1428 | 1429 | #### 3.3.5 Coroutine\Client->peek 1430 | 1431 | ### 3.4 Coroutine\Http\Client 1432 | 1433 | #### 3.4.1 属性列表 1434 | 1435 | #### 3.4.2 Coroutine\Http\Client->get 1436 | 1437 | #### 3.4.3 Coroutine\Http\Client->post 1438 | 1439 | #### 3.4.4 Coroutine\Http\Client->upgrade 1440 | 1441 | #### 3.4.5 Coroutine\Http\Client->push 1442 | 1443 | #### 3.4.6 Coroutine\Http\Client->recv 1444 | 1445 | #### 3.4.7 Coroutine\Http\Client->addFile 1446 | 1447 | #### 3.4.8 Coroutine\Http\Client->addData 1448 | 1449 | #### 3.4.9 Coroutine\Http\Client->download 1450 | 1451 | ### 3.5 Coroutine\Http2\Client 1452 | 1453 | #### 3.5.1 Coroutine\Http2\Client->__construct 1454 | 1455 | #### 3.5.2 Coroutine\Http2\Client->set 1456 | 1457 | #### 3.5.3 Coroutine\Http2\Client->connect 1458 | 1459 | #### 3.5.4 Coroutine\Http2\Client->send 1460 | 1461 | #### 3.5.5 Coroutine\Http2\Client->write 1462 | 1463 | #### 3.5.6 Coroutine\Http2\Client->recv 1464 | 1465 | #### 3.5.7 Coroutine\Http2\Client->close 1466 | 1467 | ### 3.6 Coroutine\Redis 1468 | 1469 | #### 3.6.1 Coroutine\Redis::__construct 1470 | 1471 | #### 3.6.2 Coroutine\Redis::setOptions 1472 | 1473 | #### 3.6.3 属性列表 1474 | 1475 | #### 3.6.4 事务模式 1476 | 1477 | #### 3.6.5 订阅模式 1478 | 1479 | ### 3.7 Coroutine\Socket 1480 | 1481 | #### 3.7.1 Coroutine\Socket::__construct 1482 | 1483 | #### 3.7.2 Coroutine\Socket->bind 1484 | 1485 | #### 3.7.3 Coroutine\Socket->listen 1486 | 1487 | #### 3.7.4 Coroutine\Socket->accept 1488 | 1489 | #### 3.7.5 Coroutine\Socket->connect 1490 | 1491 | #### 3.7.6 Coroutine\Socket->send 1492 | 1493 | #### 3.7.7 Coroutine\Socket->recv 1494 | 1495 | #### 3.7.8 Coroutine\Socket->sendto 1496 | 1497 | #### 3.7.9 Coroutine\Socket->recvfrom 1498 | 1499 | #### 3.7.10 Coroutine\Socket->getsockname 1500 | 1501 | #### 3.7.11 Coroutine\Socket->getpeername 1502 | 1503 | #### 3.7.12 Coroutine\Socket->close 1504 | 1505 | ### 3.8 Coroutine\MySQL 1506 | 1507 | #### 3.8.1 属性列表 1508 | 1509 | #### 3.8.2 Coroutine\MySQL->connect 1510 | 1511 | #### 3.8.3 Coroutine\MySQL->query 1512 | 1513 | #### 3.8.4 Coroutine\MySQL->prepare 1514 | 1515 | #### 3.8.5 Coroutine\MySQL->escape 1516 | 1517 | #### 3.8.6 Coroutine\MySQL\Statement->execute 1518 | 1519 | #### 3.8.7 Coroutine\MySQL\Statement->fetch 1520 | 1521 | #### 3.8.8 Coroutine\MySQL\Statement->fetchAll 1522 | 1523 | #### 3.8.9 Coroutine\MySQL\Statement->nextResult 1524 | 1525 | ### 3.9 Coroutine\PostgreSQL 1526 | 1527 | #### 3.9.1 Coroutine\PostgreSQL->connect 1528 | 1529 | #### 3.9.2 Coroutine\PostgreSQL->query 1530 | 1531 | #### 3.9.3 Coroutine\PostgreSQL->fetchAll 1532 | 1533 | #### 3.9.4 Coroutine\PostgreSQL->affectedRows 1534 | 1535 | #### 3.9.5 Coroutine\PostgreSQL->numRows 1536 | 1537 | #### 3.9.6 Coroutine\PostgreSQL->fetchObject 1538 | 1539 | #### 3.9.7 Coroutine\PostgreSQL->fetchAssoc 1540 | 1541 | #### 3.9.8 Coroutine\PostgreSQL->fetchArray 1542 | 1543 | #### 3.9.9 Coroutine\PostgreSQL->fetchRow 1544 | 1545 | #### 3.9.10 Coroutine\PostgreSQL->metaData 1546 | 1547 | ### 3.10 Server 1548 | 1549 | ### 3.11 并发调用 1550 | 1551 | 在协程版本的 client 中实现了多个客户端并发发包的功能(`setDefer` 功能) 1552 | 1553 | 通常如果一个业务请求中需要做一次 redis 请求和一次 mysql请求,那么网络 IO 会是这样: 1554 | 1555 | `redis发包 -> redis收包 -> mysql发包 -> mysql收包` 1556 | 1557 | 以上流程网络IO的时间就等于 redis网络IO时间 + mysql网络IO时间 1558 | 1559 | 但对于协程版本的 client,网络IO可以是这样的: 1560 | 1561 | `redis发包 -> mysql发包 -> redis收包 -> mysql收包` 1562 | 1563 | 以上流程网络IO的时间就接近于 max(redis网络IO时间,mysql网络IO时间) 1564 | 1565 | 目前支持并发请求的 client如下: 1566 | 1567 | - Swoole\Coroutine\Client 1568 | - Swoole\Coroutine\Redis 1569 | - Swoole\Coroutine\MySQL 1570 | - Swoole\Coroutine\Http\Client 1571 | 1572 | 除了 Swoolen\Coroutine\Client, 其他client 都实现了 defer 特性,用于声明延迟收包。因为 Swoole\Coroutine\Client 的发包和收包方法是分开的,所以就不需要实现 defer 特性了,而其他 client 的发包和收包都在一个方法中,所以需要要给 setDefer 方法来声明延迟收包,然后通过 recv 方法收包。 1573 | 1574 | ###### setDefer 使用示例 1575 | 1576 | ```php 1577 | function onRequest($request, $response) 1578 | { 1579 | // 并发请求n 1580 | $n = 5; 1581 | for($i = 0; $i < $n; $i++) { 1582 | $cli = new Swoole\Coroutine\Http\Client('127.0.0.1', 80); 1583 | $cli->setHeaders([ 1584 | 'Host' => 'local.aa.com', 1585 | 'User-Agent' => 'Chrome/49.0.2587.3', 1586 | 'Accept' => 'text/html,application/xhtml+xml,application/xml', 1587 | 'Accept-Encoding' => 'gzip', 1588 | ]); 1589 | $cli->set(['timeout' => 2]); 1590 | $cli->setDefer(); 1591 | $cli->get('/test.php'); 1592 | $clients[] = $cli; 1593 | } 1594 | for($i = 0; $i < $n; $i++) { 1595 | $r = $clients[$i]->recv(); 1596 | $result[] = $clients[$i]->body; 1597 | } 1598 | $response->end(json_encode($data)); 1599 | } 1600 | ``` 1601 | 1602 | 1603 | 1604 | #### 3.11.1 setDefer 机制 1605 | 1606 | 绝大部分协程组件,都支持了setDefer 特性,setDefer 特性可以将响应式的接口分拆为两个步骤,使用此机制可以实现并发请求。 1607 | 1608 | 以 `HttpClient` 为例,设置 setDefer(true) 以后,发起 $http->get() 请求,将不再等待服务器返回结果,而是在 send request 之后,立即返回 true。在此之后可以继续发起其他 `HttpClient` ,`MySQL`, `Redis` 等请求,最后再使用 $http->recv() 接收响应内容。 1609 | 1610 | ###### sample code 1611 | 1612 | ```php 1613 | set(['worker_num' => 1]); 1616 | $server->on('Request', function($request, $response) { 1617 | $tcpClient = new Swoole\Coroutine\Client(SWOOLE_SOCK_TCP); 1618 | $tcpClient->connect('127.0.0.1', 9501, 0.5); 1619 | $tcpClient->send("hello world\n"); 1620 | 1621 | $redis = new Swoole\Coroutine\Redis(); 1622 | $redis->connect('127.0.0.1', 6379); 1623 | $redis->setDefer(); 1624 | $redis->get('key'); 1625 | 1626 | $mysql = new Swoole\Coroutine\MySQL(); 1627 | $mysql->connect([ 1628 | 'host' => '127.0.0.1', 1629 | 'user' => 'user', 1630 | 'password' => 'password', 1631 | 'database' => 'test', 1632 | ]); 1633 | $mysql->setDefer(); 1634 | $mysql->query('select sleep(1)'); 1635 | 1636 | $httpClient = new Swoole\Coroutine\Http\Client('0.0.0.0', 9599); 1637 | $httpClient->setHeaders(['Host' => 'api.mp.qq.com']); 1638 | $httpClient->set(['timeout' => 1]); 1639 | $httpClient->setDefer(); 1640 | $httpClient->get('/'); 1641 | 1642 | $tcp_res = $tcpClient->recv(); 1643 | $redis_res = $redis->recv(); 1644 | $mysql_res = $mysql->recv(); 1645 | $http_res = $httpClient->recv(); 1646 | 1647 | $response->end('Test End'); 1648 | }); 1649 | $server->start(); 1650 | ``` 1651 | 1652 | 1653 | 1654 | #### 3.11.2 子协程,通道 1655 | 1656 | 除了只用底层内置的 `setDefer` 机制实现并发请求之外,还可以用 `子协程` + `通道` 实现并发。 1657 | 1658 | ###### sample code 1659 | 1660 | ```php 1661 | $serv = new \swoole_http_server('127.0.0.1', 9501, SWOOLE_BASE); 1662 | $serv->on('request', function($request, $response) { 1663 | $channel = new chan(2); 1664 | go(function () use ($chan) { 1665 | $cli = new Swoole\Coroutine\Http\Client('www.qq.com', 80); 1666 | $cli->set(['timeout' => 10]); 1667 | $cli->setHeaders([ 1668 | 'Host' => 'www.qq.com', 1669 | 'User-Agent' => 'Chrome/49.0.2587.3', 1670 | 'Accept' => 'text/html,application/xhtml+xml,application/xml', 1671 | 'Accept-Encoding' => 'gzip', 1672 | ]); 1673 | $ret = $cli->get('/'); 1674 | $channel->push(['www.qq.com' => $cli->body]); 1675 | }); 1676 | 1677 | go(function () use ($chan) { 1678 | $cli = new Swoole\Coroutine\Http\Client('www.163.com', 80); 1679 | $cli->set(['timeout' => 10]); 1680 | $cli->setHeaders([ 1681 | 'Host' => 'www.163.com', 1682 | 'User-Agent' => 'Chrome/49.0.2587.3', 1683 | 'Accept' => 'text/html,application/xhtml+xml,application/xml', 1684 | 'Accept-Encoding' => 'gzip', 1685 | ]); 1686 | $ret = $cli->get('/'); 1687 | $channel->push(['www.163.com' => $cli->body]); 1688 | }); 1689 | $result = []; 1690 | for($i = 0; $i < 2; $i++) 1691 | $result += $channel->pop(); 1692 | $response->end(json_encode($result)); 1693 | }); 1694 | $serv->start(); 1695 | ``` 1696 | 1697 | ###### 实现原理 1698 | 1699 | - 在`onRequest` 中需要并发两个http请求,可以使用 go 函数创建两个子协程,并发地请求多个 url 1700 | - 创建一个 channel,使用 use 闭包引用语法,传递给子协程 1701 | - 主协程循环调用 $channel->pop(),等待子协程完成任务,yield 进入挂起状态 1702 | - 并发的两个子协程其中某个完成请求时,调用 $channel->push() 将数据推送给主协程 1703 | - 子协程完成 url 请求后退出,主协程从挂起状态中恢复,继续向下执行调用 $response->end() 发送响应结果。 1704 | 1705 | ### 3.12 实现原理 1706 | 1707 | `swoole-2.0` 基于 `setjmp`, `longjmp` 实现,在进行协程切换时会自动保存 `Zend VM` 的内存状态(主要是 `EG` 全局内存和 `vm stack` ) 1708 | 1709 | - `setjmp` 和 `longjmp` 主要用于从 `ZendVm` 的 `C` 堆栈跳回 `Swoole` 的 `C` 回调函数 1710 | - 协程的创建,切换,挂起,销毁,全部都是内存操作。消耗是非常低的。 1711 | 1712 | ###### sample code 1713 | 1714 | ```php 1715 | $server = new Swoole\Http\Server('127.0.0.1', 9501, SWOOLE_BASE); 1716 | #1 1717 | $server->on('Request', function($request, $response) { 1718 | $mysql = new Swoole\Coroutine\MySQL(); 1719 | #2 1720 | $res = $mysql->connect([ 1721 | 'host' => '127.0.0.1', 1722 | 'use' => 'root', 1723 | 'password' => '123456', 1724 | 'database' => 'test', 1725 | ]); 1726 | #3 1727 | if ($res == false) { 1728 | $response->end('MySQL connect fail!'); 1729 | return; 1730 | } 1731 | $ret = $mysql->query('show tables', 2); 1732 | $response->end('Swoole response is ok, result=' . var_export($ret, true)); 1733 | }); 1734 | $server->start(); 1735 | ``` 1736 | 1737 | - 上面代码仅用了一个进程,就可以并发处理大量请求。 1738 | - 程序的性能基本上与异步回调方式相同 1739 | 1740 | ###### 运行过程 1741 | 1742 | - 调用 `onRequest` 事件回调函数时,底层会调用 C 函数 coro_create 创建一个协程(#1),同时保存这个时间点的 CPU 寄存器状态和 ZendVM 的 栈信息。 1743 | - 调用 mysql->connect 时发生IO操作,底层会调用 C 函数 coro_save 保存当前协程的状态,包括 Zend VM 上下文以及协程描述信息,并调用 coro_yield 让出程序控制权,当前的请求会挂起(#2) 1744 | - 协程让出程序控制权以后,会继续进入 EventLoop 处理其他事件,这时 swoole 会继续去处理其他客户端发来的 request请求 1745 | - IO 事件完成后,MySQL 连接成功或失败,底层会调用 C 函数 coro_resume 恢复对应的协程,恢复 Zend VM 上下文,继续向下执行(#3) 1746 | - mysql->query() 的执行过程与 mysql->connect 一致,也会进行一次协程切换调度 1747 | - 所有操作完成后,调用 end 方法返回结果,并销毁此协程 1748 | 1749 | ###### 协程开销 1750 | 1751 | 相比普通的异步回调程序,协程会多占用额外的内存。 1752 | 1753 | - swoole4 协程需要为每个并发保存 `zend stack` 栈内存并维护对应的 虚拟机状态。如果程序并发很大,可能会占用大量内存。取决于 C 函数,php 函数调用栈的深度 1754 | 1755 | - 协程调度会增加额外的一些 cpu 开销,可使用官方提供的 1756 | 1757 | [协程切换压测脚本]: https://github.com/swoole/swoole-src/blob/master/benchmark/co_switch.php 1758 | 1759 | 测试性能 1760 | 1761 | #### 3.12.1 协程与线程 1762 | 1763 | swoole 的协程在底层实现上是单线程的。同一事件只有一个协程在工作,协程的执行是串行的。这与线程不同,多个线程会被操作系统调度到多个 CPU **并行** 执行。 1764 | 1765 | 当一个协程在运行时,其他协程会停止工作。当前协程执行阻塞 IO 的操作时会挂起,底层调度器会进入 EventLoop,当有 IO完成事件时,底层调度器恢复事件对应的协程的执行。 1766 | 1767 | 对于 CPI 多核的利用,仍然依赖于 swoole 引擎的多进程机制。 1768 | 1769 | #### 3.12.2 发送数据协程调度 1770 | 1771 | ###### 现状 1772 | 1773 | 现在 Server/Client->send 在缓存区已满的情况下,会直接返回 false,需要借助 onBufferFull 和 onBufferEmpty 这样复杂的事件通知机制才能实现任务的暂停和恢复。 1774 | 1775 | 在实现需要大量发送的场景下,现有机制虽然可以实现,但非常复杂。 1776 | 1777 | ###### 思路 1778 | 1779 | 现在基于协程可以实现一种机制,直接在当前协程内 yield,等待数据发送完成,缓存区清空时,自动 resume 当前协程,继续 send 数据。 1780 | 1781 | - Server/Client->send 返回 false,并且错误码 为 `SW_ERROR_OUTPUT_BUFFER_OVERFLOW` 时,不返回 false 到 php 层,而是 yield 挂起当前协程。 1782 | - Server/Client 监听 onBufferEmpty 事件,在该事件触发后,缓存区内的数据已被发送完毕,这时 resume 对应的协程 1783 | - 协程恢复后,继续调用 Server/Client->send 向缓存区内写入数据,这时因为缓存区已空,发送必然是成功的。 1784 | 1785 | ###### sample code 1786 | 1787 | 改进前: 1788 | 1789 | ```php 1790 | for($i = 0; $i < 100; $i++) { 1791 | // 在缓存区塞满时会直接 返回 false 1792 | $server->send($fd, $data_2m); 1793 | } 1794 | ``` 1795 | 1796 | 改进后: 1797 | 1798 | ```php 1799 | for($i = 0; $i > 100; $i++) { 1800 | // 在缓存区塞满时,会 yield 当前协程,发送完成后 resume 继续向下执行 1801 | $server->send($fd, $data_2m); 1802 | } 1803 | ``` 1804 | 1805 | 可选项: 1806 | 1807 | 此项特性会改变底层的默认行为,因此需要额外的一个参数来开启。 1808 | 1809 | ```php 1810 | $serv->set([ 1811 | 'send_yield' => true, 1812 | ]); 1813 | ``` 1814 | 1815 | ###### 影响范围 1816 | 1817 | - Swoole\Server::send 1818 | - Swoole\Http\Response::write 1819 | - Swoole\WebSocket\Server::push 1820 | - Swoole\Coroutine\Client::send 1821 | - Swoole\Coroutine\\Http\Client::push 1822 | 1823 | #### 3.12.3 协程内存开销 1824 | 1825 | swoole4.0 的版本实现了 C 栈 + PHP 栈 的协程实现方案。Server 程序每次请求的事件回调函数中会创建一个新的协程,处理完成后协程退出。 1826 | 1827 | 在协程创建时需要创建要给全新的内存段作为 C 和PHP 的栈,底层默认 分配 2M(C) 的虚拟内存 + 8K(PHP)内存(PHP-7.2+)。实际并不会分配这么多内存,系统会根据在内存实际读写时发生缺页中断,再分配实际内存。 1828 | 1829 | > 由于 PHP 7.1/7.0 未提供设置栈内存尺寸的接口,这些版本的每个协程将申请 256K 的php内存。 1830 | 1831 | 相比于异步回调的程序,协程会增加一些内存管理的开销。、会产生一定的新跟那个损耗。经过压力测试 QPS 依然可以达到较高的水平。 1832 | 1833 | ```shell 1834 | ab -c 100 -n 500000 -k http://127.0.0.1:9501/ 1835 | This is ApacheBench, Version 2.3 <$Revision: 1706008 $> 1836 | Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ 1837 | Licensed to The Apache Software Foundation, http://www.apache.org/ 1838 | 1839 | Benchmarking 127.0.0.1 (be patient) 1840 | Completed 50000 requests 1841 | Completed 100000 requests 1842 | Completed 150000 requests 1843 | Completed 200000 requests 1844 | Completed 250000 requests 1845 | Completed 300000 requests 1846 | Completed 350000 requests 1847 | Completed 400000 requests 1848 | Completed 450000 requests 1849 | Completed 500000 requests 1850 | Finished 500000 requests 1851 | 1852 | 1853 | Server Software: swoole-http-server 1854 | Server Hostname: 127.0.0.1 1855 | Server Port: 9501 1856 | 1857 | Document Path: / 1858 | Document Length: 24 bytes 1859 | 1860 | Concurrency Level: 100 1861 | Time taken for tests: 3.528 seconds 1862 | Complete requests: 500000 1863 | Failed requests: 0 1864 | Keep-Alive requests: 500000 1865 | Total transferred: 132500000 bytes 1866 | HTML transferred: 12000000 bytes 1867 | Requests per second: 141738.54 [#/sec] (mean) 1868 | Time per request: 0.706 [ms] (mean) 1869 | Time per request: 0.007 [ms] (mean, across all concurrent requests) 1870 | Transfer rate: 36680.38 [Kbytes/sec] received 1871 | 1872 | Connection Times (ms) 1873 | min mean[+/-sd] median max 1874 | Connect: 0 0 0.0 0 2 1875 | Processing: 0 1 0.9 0 7 1876 | Waiting: 0 1 0.9 0 7 1877 | Total: 0 1 0.9 0 7 1878 | WARNING: The median and mean for the processing time are not within a normal deviation 1879 | These results are probably not that reliable. 1880 | WARNING: The median and mean for the waiting time are not within a normal deviation 1881 | These results are probably not that reliable. 1882 | WARNING: The median and mean for the total time are not within a normal deviation 1883 | These results are probably not that reliable. 1884 | 1885 | Percentage of the requests served within a certain time (ms) 1886 | 50% 0 1887 | 66% 0 1888 | 75% 2 1889 | 80% 2 1890 | 90% 2 1891 | 95% 3 1892 | 98% 3 1893 | 99% 3 1894 | 100% 7 (longest request) 1895 | ``` 1896 | 1897 | 1898 | 1899 | #### 3.12.4 4.0协程实现原理 1900 | 1901 | ###### 内存栈 1902 | 1903 | swoole4 的版本实现了 PHP栈+C栈 的双栈模式,创建协程时会创建一个 C 栈,默认尺寸为 2m,创建一个php栈,默认尺寸为 8k, 1904 | 1905 | C 栈主要用于保存底层函数调用的局部变量数据,用于解决 `call_user_func`, `array_map` 等 C 函数调用在协程切换时未能还原的问题。 1906 | 1907 | swoole4 无论如何切换协程,底层总能正确地切换回原先的 C 函数栈帧,继续向下执行。 1908 | 1909 | > C 栈分配的 2m 内存使用了虚拟内存,并不会分配实际内存。 1910 | 1911 | swoole4 的底层还支持了 嵌套关系,在协程内创建子协程,子协程挂起时仍然可以恢复父进程的执行。 1912 | 1913 | > 底层最大允许 128 层嵌套 1914 | 1915 | ```c 1916 | Context::Context(size_t stack_size, coroutine_func_t fn, void* private_data) : fn_(fn), stack_size(stack_size), private_data_(private_data) { 1917 | protect_page_ = 0; 1918 | end = false, 1919 | swap_ctx_ = NULL; 1920 | 1921 | stack_ = (char*) sw_malloc(stack_size_); 1922 | swDebug("alloc stack: size=%u, ptr=%p.", stack_size_, stack_); 1923 | } 1924 | ``` 1925 | 1926 | php栈主要保存php函数调用的全局变量数据,主要是 `zval` 结构体,php 中的标量类型,如整型,浮点型,布尔型等直接保存在 `zval` 结构体内,而 `object`, `string`, `array` 是使用引用计数管理且在堆上存储的。 1927 | 1928 | 8K 的php栈足以保存整个函数调用的 全局变量。 1929 | 1930 | ```php 1931 | static inline void sw_vm_stack_init () { 1932 | uint32_t size = COROG.stack_size; 1933 | zend_vm_stack page = (zend_vm_stack) emalloc(size); 1934 | 1935 | page->top = ZEND_VM_STACK_ELEMENTS(page); 1936 | page->end = (zval*) ((char*)page + size); 1937 | page->prev = NULL; 1938 | 1939 | EG(vm_stack) = page; 1940 | EG(vm_stack)->top++; 1941 | EG(vm_stack_top) = EG(vm_stack)->top; 1942 | EG(vm_stack_end) = EG(vm_stack)->end; 1943 | } 1944 | ``` 1945 | 1946 | ###### 进程切换 1947 | 1948 | C 栈切换使用了 boost.context 1.60 汇编代码,用于保存寄存器,切换指令序列。只要是 `jump_fcontext` 这个 ASM 函数提供。php栈的切换是随 C栈的切换同步进行的。底层会切换 EG(vm_stack) 使 php 恢复到正确的 php 函数栈帧。swoole4.0.2 版本增加了 ob 输出缓存区的切换,ob_start 等操作也可以用于协程。 1949 | 1950 | > boost.context 汇编切换协程栈的效率非常高,经过测试每秒可完成 2亿 次切换 1951 | > 1952 | > 某些平台下不支持 boost.context 汇编,底层将使用 ucontext 1953 | 1954 | ###### 性能对比 1955 | 1956 | - boost.context: 8ns / 23 cycles 1957 | - ucontext: 547ns / 1433 cycles 1958 | - php context: 170ns 1959 | 1960 | ###### 调用栈切换 1961 | 1962 | ```c 1963 | int sw_coro_resume(php_context *sw_current_context, zval *retval, zval *coro_retval) { 1964 | coro_task *task = SWCC(current_task); 1965 | resume_php_stack(stack); 1966 | if(EG(current_execute_data)->prev_execute_data->online->result_type != IS_UNUSED && retval) { 1967 | ZVAL_COPY(SWCC(current_coro_return_value_ptr), retval); 1968 | } 1969 | if(OG(handlers).elements) { 1970 | php_outputs_deactivate(); 1971 | if(!SWCC(current_coro_output_ptr)) { 1972 | php_output_activate(); 1973 | } 1974 | } 1975 | if(SWCC(current_coro_output_ptr)) { 1976 | memcpy(SWOG, SWCC(current_coro_output_ptr), sizeof(zend_output_globals)); 1977 | efree(SWCC(current_coro_output_ptr)); 1978 | SWCC(current_coro_output_ptr) = NULL; 1979 | } 1980 | swTraceLog(SW_TRACE_COROUTINE, "cid = %d", task->cid); 1981 | coroutine_resume_naked(task->co); 1982 | 1983 | if(unlikely(EG(exception))) { 1984 | if(retval) { 1985 | zval_ptr_dtor(retval); 1986 | } 1987 | zend_exception_error(EG(exception), E_ERROR TSRMLS_CC); 1988 | } 1989 | return CORO_END; 1990 | } 1991 | ``` 1992 | 1993 | ###### 协程调度 1994 | 1995 | swoole4 的协程实现中,主协程即为 `Reactor` 协程,负责整个 EventLoop 运行。主协程实现事件监听,在 IO 事件完成后唤醒其他工作协程。 1996 | 1997 | **协程挂起:** 1998 | 1999 | 在工作协程中执行一些 IO 操作时,底层会将 IO 事件注册到 EventLoop,并让出执行权。 2000 | 2001 | - 嵌套创建的非初代协程,会逐个让出到父协程,直到回到主协程。 2002 | - 在主协程上创建的初代协程,会立即回到主协程 2003 | - 主协程的 `Reactor` 会继续处理IO 事件,wait 监听新事件(`epoll_wait`) 2004 | 2005 | > 初代协程是在 `EventLoop` 内直接创建的协程,例如 `onReceive` 回调中的内置协程就是初代协程 2006 | > 2007 | > 2008 | 2009 | **协程恢复:** 2010 | 2011 | 当主协程的 `Reactor` 接收到新的 IO 事件,底层会挂起主协程,并恢复 IO 事件对应的工作协程。该工作协程挂起或退出时,会再次回到主协程。 2012 | 2013 | #### 3.12.5 协程客户端超时规则 2014 | 2015 | > 在swoole版本 >= 4.2.10 下生效 2016 | 2017 | 为了统一各个客户端混乱的超时规则,避免开发者需要处处谨慎设置,从 4.2.10 版本开始,所有协程客户端统一超时规则如下: 2018 | 2019 | ###### 全局Socket超时配置项 2020 | 2021 | 一下配置项可通过 Co::set 方法配置,如 2022 | 2023 | ```php 2024 | Co::set([ 2025 | 'socket_connect_timeout' => 1, 2026 | 'socket_timeout' => 5, 2027 | ]); 2028 | ``` 2029 | 2030 | - socket_connect_timeout, 建立 socket 连接超时时间默认为 1秒 2031 | - socket_timeout ,socket 读写操作超时时间默认为 `-1`,即永不超时 2032 | 2033 | 即:所有协程客户端默认连接超时时间为 1s,其它读写操作默认为永不超时 2034 | 2035 | ###### 超时时间设置规则 2036 | 2037 | - -1 : 永不超时 2038 | - 0 : 不更改超时时间 2039 | - 其他正 int 值 : 设置相应秒数的超时定时器,最大进度为 1毫秒 2040 | 2041 | ###### 生效范围 2042 | 2043 | - Co::set => 全局 2044 | - 通过 set 等方法设置 => 所处的客户端 2045 | - 读写方法的函数传参 => 所在方法的读写操作内 2046 | 2047 | ###### php官方的网络库超时规则 2048 | 2049 | 在swoole 中很多 php 官方提供的 网络库 API 也可以协程化成 异步非阻塞 IO,他们的超时时间受 `default_socket_timeout` 配置影响,开发者可以通过 `ini_set('default_socket_timeout', 60)` 来单独设置,它的默认值是 60. 2050 | 2051 | #### 3.12.6 协程执行流程 2052 | 2053 | 协程执行的流程遵循一下规则: 2054 | 2055 | - 协程没有 IO 等待,正常执行php 代码,不会产生执行流程切换 2056 | - 协程遇到 IO 等待会立即将控制权切换,待 IO 操作完成后,重新将执行流切回到原来协程所切出的点 2057 | - 协程并行协程依次执行, 2058 | - 协程嵌套执行流程是由外向内,逐层进入,直到发生 IO,然后再逐层由内向外切到外邻的协程,父协程不会等待子协程结束。 2059 | 2060 | ###### 无IO等待 2061 | 2062 | > 正常执行 php 代码,不会产生执行流程切换。 2063 | > 2064 | > 无IO 操作的协程,相当于一次 php 函数调用 2065 | 2066 | ```php 2067 | echo "main start\n"; 2068 | go(function() { 2069 | echo "coro " . co::getcid() . " start\n"; 2070 | }); 2071 | echo "end\n"; 2072 | /* 2073 | main start 2074 | coro 1 start 2075 | end 2076 | */ 2077 | ``` 2078 | 2079 | ###### IO 等待 2080 | 2081 | > 立即将控制权切出,等 IO 完成后,重新将执行流切回到原来协程切出去的点。 2082 | > 2083 | > ```php 2084 | > echo "main start\n"; 2085 | > go(function() { 2086 | > echo "coro " . co::getcid() . " start\n"; 2087 | > co::sleep(.1); // switch,切出控制权 2088 | > echo "coro " . co::getcid() . " end\n"; 2089 | > }); 2090 | > echo " end\n"; 2091 | > /* 2092 | > main start 2093 | > coro 1 start 2094 | > end 2095 | > coro 1 end 2096 | > */ 2097 | > ``` 2098 | > 2099 | > ###### 协程并行 2100 | > 2101 | > 多个协程其实是串行依次执行的。 2102 | > 2103 | > ```php 2104 | > echo "main start\n"; 2105 | > go(function() { 2106 | > echo "coro " . co::getcid() . " start\n"; 2107 | > co::sleep(.1); // switch, 切除控制权 2108 | > echo "coro " . co::getcid() . " end\n"; 2109 | > }); 2110 | > echo "main flag\n"; 2111 | > go(function() { 2112 | > echo "coro " . co::getcid() . " start\n"; 2113 | > co::sleep(.1); 2114 | > echo "coro " . co::getcid() . " end\n"; 2115 | > }); 2116 | > echo " end \n"; 2117 | > /* 2118 | > main start 2119 | > coro 1 start 2120 | > main flag 2121 | > coro 2 start 2122 | > end 2123 | > coro 1 end 2124 | > coro 2 end 2125 | > */ 2126 | > ``` 2127 | > 2128 | > ###### 协程的嵌套 2129 | > 2130 | > 协程的执行流程是由外向内逐层进入,直到发生IO操作,然后从内向外切回外邻协程,父协程不会等待子协程结束。 2131 | > 2132 | > ```php 2133 | > echo "main start\n"; 2134 | > go(function() { 2135 | > echo "coro " . co::getcid() . " start\n"; 2136 | > go(function() { 2137 | > echo "coro " . co::getcid() . " start\n"; 2138 | > co::sleep(.1); 2139 | > echo "coro " . co::getcid() . " end\n"; 2140 | > }); 2141 | > echo "coro " . co::getcid() . " dont wait child coroutine\n"; 2142 | > co::sleep(.2); 2143 | > echo "coro " . co::getcid() . " end\n"; 2144 | > }); 2145 | > echo "end\n"; 2146 | > /* 2147 | > main start 2148 | > coro 1 start 2149 | > coro 2 start 2150 | > coro 1 do not wait children coroutine 2151 | > end 2152 | > coro 2 end 2153 | > coro 1 end 2154 | > */ 2155 | > ``` 2156 | > 2157 | > ```php 2158 | > echo "main start\n"; 2159 | > go(function() { 2160 | > echo "coro " . co::getcid() . " start\n"; 2161 | > go(function() { 2162 | > echo "coro " . co::getcid() . " start\n"; 2163 | > co::sleep(.2); 2164 | > echo "coro " . co::getcid() . " end\n"; 2165 | > }); 2166 | > echo "coro " . co::getcid() . " dont wait child coroutine\n"; 2167 | > co::sleep(.1); 2168 | > echo "coro " . co::getcid() . " end\n"; 2169 | > }); 2170 | > echo "end\n"; 2171 | > /* 2172 | > main start 2173 | > coro 1 start 2174 | > coro 2 start 2175 | > coro 1 do not wait children coroutine 2176 | > end 2177 | > coro 1 end 2178 | > coro 2 end 2179 | > */ 2180 | > ``` 2181 | > 2182 | > 2183 | 2184 | ### 3.13 注意点 2185 | 2186 | ###### 范式 2187 | 2188 | - 协程内部禁止使用全局变量 2189 | - 协程使用 `use` 关键字引入外部变量到当前作用域时,禁止使用引用方式 2190 | - 协程之间进行通信必须使用通道 `channel` 2191 | 2192 | 换句话说,协程间通信不要使用全局变量或者引用外部变量到局部作用域,而要使用 channel 2193 | 2194 | - 项目中如果有扩展 `hook` 了 `zend_execute_ex` 或者 `zend_execute_internal` ,特别需要注意 C 栈,可以用 `co::set` 重新设置 C 栈大小 2195 | 2196 | `hook` 这两个入口函数之后,大部分情况下会把平坦的php指令调用变为 C 函数调用,增加 C 栈的消耗。 2197 | 2198 | ###### 与其他php扩展的冲突 2199 | 2200 | 因为某些跟踪调试的 php 扩展大量使用了 全局变量,可能会导致 swoole 协程发生崩溃。这些扩展有: xdebug, phptrace, aop, molten, xhprof, phalcon(swoole的协程无法运行在 phalcon 框架下) 2201 | 2202 | ###### 以下行为可能导致严重错误 2203 | 2204 | - 在多个协程间共用一个连接 2205 | - 使用类静态变量 / 全局变量 来保存上下文 2206 | 2207 | #### 3.13.1 在多个协程间共用同一个协程客户端 2208 | 2209 | 与同步阻塞程序不同,协程是并发处理请求的,因此同一事件可能会有多个请求在并行处理,一旦共用客户端连接,就会导致不同协程之间发生数据错乱。 2210 | 2211 | ###### 错误的用法 2212 | 2213 | ```php 2214 | $server = new Swoole\Http\Server('127.0.0.1', 9501); 2215 | $server->on('Receive', function($serv, $fd, $rid, $data) { 2216 | $redis = RedisFactory::getRedis(); 2217 | $result = $redis->hgetall('key'); 2218 | $resp->end(var_export($result, true)); 2219 | }); 2220 | 2221 | $server->start(); 2222 | 2223 | class RedisFactory 2224 | { 2225 | private static $_redis = null; 2226 | public static function getRedis() 2227 | { 2228 | if(self::$_redis === null) { 2229 | $redis = new \Swoole\Coroutine\Redis(); 2230 | $redis->connect('127.0.0.1', 6379); 2231 | self::$_redis = $redis; 2232 | } 2233 | return self::$_redis; 2234 | } 2235 | } 2236 | ``` 2237 | 2238 | ###### 避免严重错误,我们可以这样写 2239 | 2240 | ```php 2241 | // 基于 `SplQueue` 实现协程客户端的连接池,可以复用协程客户端,实现长连接。 2242 | $pool = new RedisPool(); 2243 | $server = new Swoole\Http\Server('127.0.0.1', 9501); 2244 | $server->set([ 2245 | // 如果开启异步安全重启,需要在 workerExit 释放连接池资源 2246 | 'reload_async' => true 2247 | ]); 2248 | $server->on('start', function(swoole_http_server $server) { 2249 | var_dump($server->master_pid); 2250 | }); 2251 | $server->on('workerExit', function(swoole_http_server $server) use ($pool) { 2252 | $pool->destruct(); 2253 | }); 2254 | $server->on('request', function(swoole_http_request $request, swoole_http_response $response) use ($pool) { 2255 | // 从连接池中获取一个 Redis 协程客户端 2256 | $redis = $pool->get(); 2257 | // connect fail 2258 | if($redis === false) { 2259 | $response->end('error'); 2260 | return; 2261 | } 2262 | $result = $redis->hgetall('key'); 2263 | $response->end(var_export($result, true)); 2264 | // 释放客户端,其他协程可复用此对象 2265 | $pool->put($redis); 2266 | }); 2267 | 2268 | $server->start(); 2269 | 2270 | class RedisPool 2271 | { 2272 | protected $available = true; 2273 | protected $pool; 2274 | 2275 | public function __construct() 2276 | { 2277 | $this->pool = new SplQueue; 2278 | } 2279 | 2280 | public function put($redis) 2281 | { 2282 | $this->pool->push($redis); 2283 | } 2284 | 2285 | /** 2286 | * @return bool|mixed|\Swoole\Coroutine\Redis 2287 | */ 2288 | public function get() 2289 | { 2290 | // 当有空闲连接且连接池处于可用状态 2291 | if($this->available && count($this->pool) > 0) { 2292 | return $this->pool->pop(); 2293 | } 2294 | // 没有空闲连接时,创建新连接 2295 | $redis = new Swoole\Coroutine\Redis(); 2296 | $res = $redis->connect('127.0.0.1', 6379); 2297 | if($res == false) 2298 | return false; 2299 | else 2300 | return $redis; 2301 | } 2302 | 2303 | public function destruct() 2304 | { 2305 | // 连接池销毁,将其置为不可用状态,防止新的客户端进入常驻连接池,导致服务器无法平滑地退出 2306 | $this->available = false; 2307 | while(!$this->pool->isEmpty()) { 2308 | $this->pool->pop(); 2309 | } 2310 | } 2311 | } 2312 | ``` 2313 | 2314 | 2315 | 2316 | #### 3.13.2 禁止使用协程API的场景(2.x版本) 2317 | 2318 | 在 `ZendVM` 中,魔术方法,反射函数,`call_user_func`, `call_user_func_array` 是由 C 函数实现,并未 `opcode` ,这些操作可能会与 `swoole` 底层的协程调度产生冲突。因此禁止在这些地方使用协程的 API,我们最好使用 php 提供的动态函数调用语法来实现上述之类的功能。 2319 | 2320 | > 在swoole4+ 版本已解决此问题,可以在任意函数中使用协程,下列禁用场景仅适用于 swoole 2.x 版本 2321 | 2322 | `__get()`, `__set()`, `__call()`, `__callStatic`, `__toString`, `__invoke`, `__destruct`, `call_user_func`, `call_user_func_array`, `ReflectionFunction::invoke`, `ReflectionFunction::invokeArgs`, `ReflectionMethod::invoke`, `ReflectionMethod::invokeArgs`, `array_walk / array_map` 2323 | 2324 | ###### 字符串函数 2325 | 2326 | ```php 2327 | // 错误写法 2328 | $func = 'test'; 2329 | $retval = call_user_func($func, 'hello'); 2330 | ``` 2331 | 2332 | ```php 2333 | // 正确写法 2334 | $func = 'test'; 2335 | $retval = $func('hello'); 2336 | ``` 2337 | 2338 | ###### 对象方法 2339 | 2340 | ```php 2341 | // 错误的写法 2342 | $retval = call_user_func(array($obj, 'test'), 'hello'); 2343 | $retval = call_user_func_array(array($obj, 'test'), 'hello', array(1, 2, 3)); 2344 | ``` 2345 | 2346 | ```php 2347 | // 正确写法 2348 | $method = 'test'; 2349 | $args = array(1, 2, 3); 2350 | $retval = $obj->$method('hello'); 2351 | $retval = $obj->$method('hello', ... $args); 2352 | ``` 2353 | 2354 | 2355 | 2356 | #### 3.13.3 使用类静态变量/全局变量保存上下文 2357 | 2358 | 多个协程是并发执行的,因此不能使用类静态变量 / 全局变量 来保存协程上下文内容。使用局部变量是安全的,因为局部变量的值会自动保存在协程栈中,其他协程访问不到协程的局部变量。 2359 | 2360 | ###### sample code 2361 | 2362 | ```php 2363 | // 错误写法 2364 | $_array = []; 2365 | $serv->on('Request', function($request, $response) { 2366 | global $_array; 2367 | // 请求 /a (协程1) 2368 | if($request->server['request_uri'] == '/a') { 2369 | $_array['name'] = 'a'; 2370 | co::sleep(1.0); 2371 | echo $_array['name']; 2372 | $response->end($_array['name']); 2373 | } 2374 | // 请求 /b (协程2) 2375 | else { 2376 | $_array['name'] = 'b'; 2377 | $response->end(); 2378 | } 2379 | }); 2380 | ``` 2381 | 2382 | 发起两个并发请求 2383 | 2384 | ```shell 2385 | curl http://127.0.0.1:9501/a 2386 | curl http://127.0.0.1:9501/b 2387 | ``` 2388 | 2389 | 协程1中设置了全局变量 `$_array['name']` 的值为 `a`, 协程1 调用co::sleep 挂起,然后协程2执行,将 `$_array['name']` 的值设为 b,协程2 结束。这是定时器返回,底层恢复协程1 的运行,而协程1的逻辑中有一个上下文的依赖关系。当再次打印 `$_array['name']` 的值时,程序预期是 a,但这个值已被协程2所修改,所以实际结果是 b,这样就造成逻辑错误. 2390 | 2391 | 同理, 使用类静态变量 `class::$array`, 全局对象属性 `$object->array`, 其他超全局变量 `$GLOBALS` 等,进行上下文保存在协程程序中是非常危险的,可能会出现不符合预期的行为。 2392 | 2393 | ###### 使用 Context 管理上下文 2394 | 2395 | - 可以使用一个 `Context` 类来管理协程的上下文,在 `Context` 类中,使用 `Coroutine::getUid` 获取协程 `ID`, 然后隔离不同协程之间的全局变量 2396 | - 协程退出时,清理上下文数据 2397 | 2398 | Context: 2399 | 2400 | ```php 2401 | use Swoole\Coroutine; 2402 | class Context 2403 | { 2404 | protected static $pool = []; 2405 | static function get($key) 2406 | { 2407 | $cid = Coroutine::getUid(); 2408 | if($cid < 0) 2409 | return null; 2410 | if(isset(self::$pool[$cid][$key])) { 2411 | return self::$pool[$cid][$key]; 2412 | } 2413 | return null; 2414 | } 2415 | 2416 | static function put($key, $item) 2417 | { 2418 | $cid = Coroutine::getuid(); 2419 | if($cid > 0) 2420 | self::$pool[$cid][$key] = $item; 2421 | } 2422 | 2423 | static function delete($key = null) 2424 | { 2425 | $cid = Coroutine::getuid(); 2426 | if($cid > 0) { 2427 | if($key) 2428 | unset(self::$pool[$cid][$key]); 2429 | else 2430 | unset(self::$pool[$cid]); 2431 | } 2432 | } 2433 | } 2434 | ``` 2435 | 2436 | ```php 2437 | $serv->on('Request', function($request, $response) { 2438 | if($request->server['request_uri'] == '/a') { 2439 | Context::put('name', 'a'); 2440 | co::sleep(1.0); 2441 | echo Context::get('name'); 2442 | $response->end(Context::get('name')); 2443 | // 退出协程时清理 2444 | Context::delete('name'); 2445 | } else { 2446 | Context::put('name', 'b'); 2447 | $response->end(); 2448 | // 退出协程时清理 2449 | Context::delete(); 2450 | } 2451 | }); 2452 | ``` 2453 | 2454 | 2455 | 2456 | #### 3.13.4 退出协程 2457 | 2458 | #### 3.13.5 异常处理 2459 | 2460 | 在协程编程中,可以直接使用 `try/catch` 处理异常。但必须在协程内捕获,不能跨协程捕获异常 2461 | 2462 | ###### sample code 2463 | 2464 | ```php 2465 | // 在协程内捕获异常 2466 | function test() 2467 | { 2468 | throw new \RuntimeException(__FILE__, __LINE__); 2469 | } 2470 | Swoole\Coroutine::create(function () { 2471 | try{ 2472 | test(); 2473 | } catch (\Throwable $e) { 2474 | echo $e; 2475 | } 2476 | }); 2477 | ``` 2478 | 2479 | ### 3.14 扩展组件 2480 | 2481 | #### 3.14.1 MongoDB 2482 | 2483 | ### 3.15 编程调试 2484 | 2485 | ## 4. Runtime 2486 | 2487 | swoole4.0 底层增加了一个新的特性,可以在运行时动态地将基于 `php_stream` 实现的扩展和 `php` 网络客户端代码一键协程化。底层替换了 `ZendVM` `Stream` 的函数指针,所有使用 `php_stream` 运行的 `socket` 的操作均变成协程调度的异步 IO 2488 | 2489 | 目前有php原生的 `Redis`, `PDO`, `MySQLi` 协程化的支持。 2490 | 2491 | ###### 函数原型 2492 | 2493 | ```php 2494 | function Runtime::enableCoroutine(bool $enable = true, int $flags = SWOOLE_HOOK_ALL); 2495 | ``` 2496 | 2497 | - `$enable` : 打开或关闭协程 2498 | - `$flags` : 选择要 `Hook` 的类型,可以多选,默认为全选。仅在 `$enable = true` 时有效 2499 | 2500 | ###### 可用列表 2501 | 2502 | - `redis` 扩展 2503 | - 使用 `mysqlnd` 模式的 `pdo`, `mysqli` 的扩展,如果未启用 `mysqlnd`, 将不支持协程化 2504 | - `soap` 扩展 2505 | - `file_get_contents`, `fopen` 2506 | - `stream_socket_client` (predis) 2507 | - `stream_socket_server` 2508 | - `fsockopen` 2509 | 2510 | ###### 不可用列表 2511 | 2512 | - `mysql`: 底层使用 `libmysqlclient` 2513 | - `curl`: 底层使用 `libcurl` (即不能使用 `CURL` 驱动的 `Guzzle`) 2514 | - `mongo` : 底层使用 `mongo-c-client` 2515 | - `pdo-pgsql` 2516 | - `pdo-ori` 2517 | - `pdo-odbc` 2518 | - `pdo-firebird` 2519 | 2520 | ###### sample code 2521 | 2522 | ```php 2523 | Swoole\Runtime::enableCoroutine(); 2524 | go(function() { 2525 | $redis = new redis; 2526 | $retval = $redis->connect('127.0.0.1', 6379); 2527 | var_dump($retval, $redis->getLastError()); 2528 | var_dump($redis->get("key")); 2529 | var_dump($redis->set("key", "value2")); 2530 | var_dump($redis->get("key")); 2531 | $redis->close(); 2532 | }); 2533 | ``` 2534 | 2535 | ###### 方法放置的位置 2536 | 2537 | 调用方法后,当前进程内全局生效,一般放在整个项目最开头,以期获得100% 覆盖的效果,协程内外会自动切换模式,不会影响php原生环境的使用。 2538 | 2539 | 注意:不建议在 onRequest 等回调中开启,会多次调用造成不必要的调用开销。 2540 | 2541 | ### 4.1 文件操作 2542 | 2543 | swoole4 增加了对文件操作的 `Hook`, 在运行时开启协程后,可以将文件读写的 `IO` 操作转为协程模式。 2544 | 2545 | 底层使用了 `AIO` 线程池模拟实现,在 `IO` 完成时唤醒对应协程。 2546 | 2547 | ###### 可用列表 2548 | 2549 | `foepn`, `fread`/`fgets`, `fwrite`/`fputs`, `file_get_contents`/`file_put_contents`, `unlink`, `mkdir`, `rmdir` 2550 | 2551 | ###### sample code 2552 | 2553 | ```php 2554 | Swoole\Routine::enableCoroutine(true); 2555 | go(function() { 2556 | $fp = fopen('test.log', 'a+'); 2557 | fwrite($fp, str_repeat('A', 2048)); 2558 | fwrite($fp, str_repeat('B', 2048)); 2559 | fclose($fp); 2560 | }); 2561 | ``` 2562 | 2563 | 2564 | 2565 | ### 4.2 睡眠函数 2566 | 2567 | swoole4 增加了对 `sleep` 函数的 `Hook`, 底层替换了 `sleep`, `usleep`, `time_nanosleep`, `time_sleep_until` 四个函数。 2568 | 2569 | 当调用这些睡眠函数时,会自动切换为协程定时器调度,不会阻塞进程。 2570 | 2571 | ###### sample code 2572 | 2573 | ```php 2574 | Swoole\Runtime::enableCoroutine(true); 2575 | go(function() { 2576 | sleep(1); 2577 | echo "sleep 1s\n"; 2578 | usleep(1000); 2579 | echo "sleep 1ms\n"; 2580 | }); 2581 | ``` 2582 | 2583 | ###### 例外情况 2584 | 2585 | 由于底层的定时器最小粒度是 `1ms`, 因此使用 `usleep` 等高精度睡眠函数时,如果设置为低于 `1ms` 时,将直接使用 `sleep` 系统调用。可能会引起非常短暂的睡眠阻塞。 2586 | 2587 | ### 4.3 开关选项 2588 | 2589 | swoole 4 版本中,`Runtime::enableCoroutine` 增加了第二个参数,可以设置开关选项,选择要 `Hook` 哪些php函数 2590 | 2591 | ###### 支持的选项 2592 | 2593 | - `SWOOLE_HOOK_SLEEP`: 睡眠函数 2594 | - `SWOOLE_HOOK_FILE`: 文件操作 `stream` 2595 | - `SWOOLE_HOOK_TCP`: `TCP Socket` 类型的 `stream` 2596 | - `SWOOLE_HOOK_UDP` : `UDP Socket` 类型的 `stream` 2597 | - `SWOOLE_HOOK_UNIX` : `Unix Stream Socket` 类型的 `stream` 2598 | - `SWOOLE_HOOK_UDG` : `Unix Dgram Socket` 类型的 `stream` 2599 | - `SWOOLE_HOOK_SSL` : `SSL Socket` 类型的 `stream` 2600 | - `SWOOLE_HOOK_TLS` : `TLS Socket` 类型的 `stream` 2601 | - `SWOOLE_HOOK_ALL` : 打开所有类型 2602 | 2603 | ###### sample code 2604 | 2605 | ```php 2606 | Swoole\Runtime::enableCoroutine(true, SWOOLE_HOOK_SLEEP); 2607 | go(function() { 2608 | sleep(1); 2609 | // 注意仅 hook 了睡眠函数,下面的文件操作函数会导致阻塞 2610 | $fp = fopen('test.log', 'a+'); 2611 | fwrite($fp, str_repeat('A', 2048)); 2612 | fwrite($fp, str_repeat('B', 2048)); 2613 | fclose(); 2614 | }); 2615 | ``` 2616 | 2617 | ###### 关闭协程 2618 | 2619 | 调用 `Runtime::enableCoroutine(false)` 关闭上一次设置的所有选项协程 `Hook` 设置。 2620 | 2621 | 注意关闭操作不接受第二个参数,底层会判断上一次打开时设置的选项列表,关闭对应的协程 `Hook` 设置 2622 | 2623 | ### 4.4 严格模式 2624 | 2625 | > 注意:严格模式和 `enableCoroutine` 存在冲突,不要同时启用 2626 | 2627 | 在 swoole4 版本以后,开启严格模式以后,调用常用的阻塞 IO 的函数和方法会出现警告。 2628 | 2629 | ###### function prototype 2630 | 2631 | ```php 2632 | function Runtime::enableStrictMode(); 2633 | ``` 2634 | 2635 | ###### sample code and warning 2636 | 2637 | ```php 2638 | Swoole\Runtime::enableStrictMode(); 2639 | sleep(1); // Warning: sleep() has been disabled for security reasons in strictmode.php on line 8 2640 | ``` 2641 | 2642 | 2643 | 2644 | ## 5. Timer 2645 | 2646 | 毫秒精度的定时器。底层基于 `epoll_wait` 和 `setitimer` 实现,数据结构使用 `最小堆`,可支持添加大量定时器。 2647 | 2648 | - 在同步进程中使用 `setitimer` 和信号实现,如 `Manager` 和 `TaskWorker` 进程 2649 | - 在异步进程中使用 `epoll_wait`/`kevent`/`poll`/`select` 超时时间实现 2650 | 2651 | ###### 性能 2652 | 2653 | 底层使用最小堆数据结构实现定时器,定时器的添加和删除,全部为内存操作,因此性能是非常高的。官方的 2654 | 2655 | [基准测试脚本]: https://github.com/swoole/swoole-src/blob/master/benchmark/timer.php 2656 | 2657 | 中添加或删除 `10万` 个随机时间的定时器耗时为 `0.08s` 左右。 2658 | 2659 | ```php 2660 | ~/workspace/swoole/benchmark$ php timer.php 2661 | add 100000 timer :0.091133117675781s 2662 | del 100000 timer :0.084658145904541s 2663 | ``` 2664 | 2665 | > 定时器是内存损耗,而没有IO损耗 2666 | 2667 | ###### 差异 2668 | 2669 | `Timer` 和 `PHP` 本身的 `pcntl_alarm` 是不同的。`pcntl_alarm` 是基于时钟信号 + `tick` 函数实现,存在一些如下几点缺点: 2670 | 2671 | - 最大进度是秒级,而 `Timer` 最大进度是毫秒 2672 | - 不支持同时设定多个定时器程序 2673 | - `pcntl_alarm` 依赖于 `declare(ticks = 1)`, 性能很差 2674 | 2675 | ###### 零毫秒定时器 2676 | 2677 | 底层不支持时间参数为 `0` 的定时器。这与 `Node.js` 等编程语言不同,在`Swoole` 里可以使用 `Swoole\Event::defer` 实现类似功能。 2678 | 2679 | ```php 2680 | Swoole\Event::defer(function () { 2681 | echo "hello\n"; 2682 | }); 2683 | ``` 2684 | 2685 | 上述代码与 `JS` 中的 `setTimeout(0, func)` 效果是完全一致的。 2686 | 2687 | ### 5.1 swoole_timer_tick 2688 | 2689 | 设置一个间隔时钟定时器,与 `after` 定时器不同的是 `tick` 定时器会持续触发,直到调用 `swoole_timer_clear` 清楚这个 timer 2690 | 2691 | ```php 2692 | int swoole_timer_tick(int $mesc, callable $callback, [$mixed $param]); 2693 | ``` 2694 | 2695 | - `$mesc` 指定时间,单位为毫秒。如 `1000` 表示 `1` 秒,最大不得超过 `86400000` 2696 | - `$callback_function` 时间到期后所执行的函数,必须是可以调用的 2697 | - 可以使用匿名函数的 `use` 语法传递参数到回调函数中 2698 | - 定时器仅在当前进程空间内有效 2699 | - 定时器是纯异步实现。不能与阻塞 IO 的函数一起使用,否则定时器的执行时间会发生错乱 2700 | 2701 | 定时器在执行的过程中可能存在一定误差 2702 | 2703 | ###### 回调函数 2704 | 2705 | ```php 2706 | function callbackFunction(int $timer_id, [$mixed $param]); 2707 | ``` 2708 | 2709 | - `$timer_id` 定时器的 ID,可用于 `swoole_timer_clear` 清除此定时器 2710 | - `$params` 由 `swoole_timer_tick` 传入的第三个参数 $param, 此参数也为可选参数 2711 | 2712 | ###### 定时器校正 2713 | 2714 | 定时器回调函数的执行时间不影响下一次定时器执行的时间。 2715 | 2716 | 比如,在 0.002s 时设置了 10ms 的 tick 定时器,第一次会在 0.012s 执行回调函数,如果回调函数执行了 5ms,下一次定时器仍然会在 0.022s 时触发,而不是 0.027s 。 2717 | 2718 | 然而如果定时器回调函数的执行时间过于长,延申到了下一次定时器执行的时间。底层会进行时间校正,丢弃已过期的**欲定时执行**的行为,在下一时间间隔点进行回调。比如上面的例子中,0.012s 时的回调函数执行了 15ms, 本该在 0.022s 产生的回调,实际上直到 0.027s 时才返回。那么 0.022s 时本欲执行的行为,就被“丢弃”了。再到下一个间隔即 0.032s 继续执行回调。 2719 | 2720 | ###### 协程模式 2721 | 2722 | 在协程环境下,`swoole_timer_tick` 回调中会自动创建一个协程,可以直接使用协程相关 API,无需调用 `go` 创建协程。 2723 | 2724 | > 可设置 `enable_coroutine` 关闭自动创建协程 2725 | 2726 | ###### sample code 2727 | 2728 | ```php 2729 | // 1 2730 | swoole_timer_tick(1000, function() { 2731 | echo "test timer.\n"; 2732 | }); 2733 | // 2 2734 | swoole_timer_tick(3000, function() { 2735 | echo "after 3000ms.\n"; 2736 | swoole_timer_after(14000, function() { 2737 | echo "after 14000ms.\n"; 2738 | }); 2739 | }); 2740 | ``` 2741 | 2742 | 2743 | 2744 | ### 5.2 swoole_timer_after 2745 | 2746 | 在指定的时间后执行函数 2747 | 2748 | ```php 2749 | int swoole_timer_after(int $after_timer_ms, mixed $callback_function); 2750 | ``` 2751 | 2752 | `swoole_timer_after` 函数是一个一次性定时器,执行完成后就会销毁。此函数与 php 标准库提供的 `sleep` 函数不同,`after` 是非阻塞的。而 `sleep` 调用后会导致当前进程进入阻塞,将无法处理新的请求。 2753 | 2754 | 执行成功返回定时器 ID,若取消定时器,可调用 `swoole_timer_clear` 2755 | 2756 | - `$after_time_ms` 最大步地超过 86400000,即一天 2757 | - `$callback_function` 时间到期后所执行的函数,必须是可以调用的。 2758 | - 可以使用 匿名函数的 `use` 方法传参到回调函数中 2759 | 2760 | ###### 协程模式 2761 | 2762 | 在协程模式下,`swoole_timer_after` 回调中会自动创建一个协程,可直接使用协程相关 API,无需调用 `go` 来创建协程。 2763 | 2764 | > 可设置 `enable_coroutine` 关闭自动创建协程 2765 | 2766 | ###### sample code 2767 | 2768 | ```php 2769 | swoole_timer_after(1000, function () use ($str) { 2770 | echo "timeout, " . $str . "\n"; 2771 | }); 2772 | ``` 2773 | 2774 | 2775 | 2776 | ### 5.3 swoole_timer_clear 2777 | 2778 | 使用定时器ID来删除定时器 2779 | 2780 | ```php 2781 | bool swoole_timer_clear(int $timer_id); 2782 | ``` 2783 | 2784 | - `$timer_id` : 定时器ID,调用 `swoole_timer_tick`, `swoole_timer_after` 后会返回一个int 的 timer_id 2785 | - `swoole_timer_clear` 不能用来清除其他进程的定时器,只能作用于当前进程 2786 | 2787 | ###### sample code 2788 | 2789 | ```php 2790 | $timer = swoole_timer_after(1000, function () { 2791 | echo "timeout\n"; 2792 | }); 2793 | var_dump(swoole_timer_clear($timer)); // bool(true) 2794 | var_dump($timer); // int(1) 2795 | ``` 2796 | 2797 | 2798 | 2799 | ## 6. Memory 2800 | 2801 | ### 6.1 Lock 2802 | 2803 | ### 6.2 Buffer 2804 | 2805 | ### 6.3 Table 2806 | 2807 | Swoole\Table 是一个基于共享内存和锁实现的超高性能,并发数据结构。用于解决多进程/多线程数据共享和同步加锁问题。 2808 | 2809 | 最新版本已移除 `lock` 和 `unlock` 方法,改用 `Swoole\Lock` 来实现数据同步 2810 | 2811 | 请谨慎使用数组方式去读写 `swoole_table`, 建议使用文档中提供的 API 来进行操作,数组方式取出的 `swoole_table_row` 对象是一次性对象,请勿依赖其进行过多操作。 2812 | 2813 | ###### swoole_table 的优势 2814 | 2815 | - 性能强,单线程每秒读写可达 `200万` 次 2816 | - 引用代码无需加锁,`swoole_table` 内置行锁自旋锁,所有操作均是多线程 / 多进程安全。用户层完全不需要考虑数据同步问题。 2817 | - 支持多进程,`swoole_table` 可用于多进程之间共享数据 2818 | - 使用行锁,而不是全局锁,仅当2个进程在同一个 CPU 时间下并发读取同一条数据时才会发生 “抢锁” 现象。 2819 | 2820 | > `swoole_table` 不受 php 的memory_limit 控制 2821 | > 2822 | > `swoole_table` 在 1.7.5 以上可用 2823 | 2824 | ###### 遍历Table 2825 | 2826 | swoole_table 类实现了迭代器和 Countable 接口,可以使用 foreach 遍历,使用count 计算当前行数。 2827 | 2828 | > 遍历Table 依赖 pcre,如果发现无法遍历 swoole_table,先查下有没安装 pcre-devel 扩展 2829 | 2830 | ```php 2831 | foreach($table as $row) 2832 | { 2833 | var_dump($row); 2834 | } 2835 | echo count($table); 2836 | ``` 2837 | 2838 | 2839 | 2840 | ### 6.4 Atomic 2841 | 2842 | Swoole\Atomic 是 swoole 扩展提供的原子技术操作类,可以方便整数的无锁原子增减。 2843 | 2844 | - Swoole\Atomic 使用共享内存,可以在不同的进程之间操作计数 2845 | - Swoole\Atomic 基于 gcc 提供的CPU 原子指令,无需加锁 2846 | - Swoole\Atomic 在服务器程序中必须在 `swoole_server->start` 前创建才能在 Worker 进程中使用 2847 | - Swoole\Atomic 默认使用32位无符号整型,如要使用64位无符号整型,可以改用 Swoolen\Atomic\Long 2848 | 2849 | 注意:请勿在 `onReceive` 等回调函数中创建原子数,否则底层的 `GlobalMemory` 内存会持续增长,造成内存泄漏 2850 | 2851 | ###### 64位长整型 2852 | 2853 | swoole 1.9.20增加了对64位有符号长整型原子计数的支持。使用 `new Swoole\Atomic\Long` 来创建。 2854 | 2855 | - `Swoole\Atomic\Long` 不支持 `wait` 和 `wakeup` 方法 2856 | 2857 | ###### sample code 2858 | 2859 | ```php 2860 | $atomic = new swoole_atomic(123); 2861 | echo $atomic->add(12) . "\n"; 2862 | echo $atomic->sub(11) . "\n"; 2863 | echo $atomic->cmpset(122, 999) . "\n"; 2864 | echo $atomic->cmpset(124, 999) . "\n"; 2865 | echo $atomic->get() . "\n"; 2866 | ``` 2867 | 2868 | 2869 | 2870 | ### 6.5 mmap 2871 | 2872 | ### 6.6 Channel 2873 | 2874 | ### 6.7 Serialize 2875 | 2876 | ## 7. Http\Server 2877 | 2878 | > `Http\Server` 对 `Http` 协议的支持不完整,建议仅作为应用服务器,并且在前端增加 `Nginx` 作为代理 2879 | 2880 | swoole包含了内置 http 服务器,通过同步的代码即可造出异步非阻塞多进程的 http 服务器 2881 | 2882 | ```php 2883 | $http = new Swoole\Http\Server('127.0.0.1', 9501); 2884 | $http->on('request', function($request, $response) { 2885 | $response->end("

Hello Swoole. #" . rand(1000, 9999) . "

"); 2886 | }); 2887 | $http->start(); 2888 | ``` 2889 | 2890 | ###### 使用 http2 协议 2891 | 2892 | - 需要依赖 `nghttp2` 库,下载 nghttp2 后编译安装 2893 | - 使用 SSL 下的 `Http2` 协议必须安装 `openssl` ,且需要高版本 `openssl` 必须支持 `TLS1.2`,`ALPN`, `NPN` 2894 | - 使用HTTP2不一定要开启SSL 2895 | 2896 | ```shell 2897 | ./configure --enable-openssl --enable-http2 2898 | ``` 2899 | 2900 | 设置 http 服务器的 `open_http2_protocol` 为 `true` 2901 | 2902 | ```php 2903 | $serv = new Swoole\Http\Server("127.0.0.1", SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL); 2904 | $serv->set([ 2905 | 'ssl_cert_file' => $ssl_dir . '/ssl.crt', 2906 | 'ssl_key_file' => $ssl_dir . '/ssl.key', 2907 | 'open_http2_protocol' => true, 2908 | ]); 2909 | ``` 2910 | 2911 | ###### nginx+swoole配置 2912 | 2913 | ```ini 2914 | server { 2915 | root /data/wwwroot/; 2916 | server_name local.swoole.com; 2917 | 2918 | location / { 2919 | proxy_http_version 1.1; 2920 | proxy_set_header Connection "keep-alive"; 2921 | proxt_set_header X-Real-IP $remote_addr; 2922 | if (!-e $request_filename) { 2923 | proxy_pass http://127.0.0.1:9501; 2924 | } 2925 | } 2926 | } 2927 | ``` 2928 | 2929 | > 通过读取 `$request->header['x-real-ip']` 来获取客户端的真实IP 2930 | 2931 | ## 8. WebSocket\Server 2932 | 2933 | swoole内置了 `WebSocket` 服务器支持,通过几行 php 代码就可以写出一个异步非阻塞多进程的 `WebSocket` 服务器。 2934 | 2935 | ```php 2936 | $server = new Swoole\WebSocket\Server('0.0.0.0', 9501); 2937 | 2938 | $server->on('open', function(Swoole\WebSocket\Server $server, $request) { 2939 | echo "Server: handshare success with fd{$request->fd}\n"; 2940 | }); 2941 | 2942 | $server->on('message', function(Swoole\WebSocket\Server $server, $frame) { 2943 | echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; 2944 | $server->push($frame->fd, "this is server"); 2945 | }); 2946 | 2947 | $server->on('close', function($ser, $fd) { 2948 | echo "client {$fd} closed\n"; 2949 | }); 2950 | 2951 | $server->start(); 2952 | ``` 2953 | 2954 | ###### onRequest回调 2955 | 2956 | `WebSocket\Server` 继承自 `Http\Server` 2957 | 2958 | - 设置了 `onRequest` 回调,`WebSocket\Server` 也可以同时作为 `http` 服务器 2959 | - 未设置 `onRequest` 回调, `WebSocket\Server` 收到http请求后会返回 400 错误 2960 | - 如果想通过接收 http 触发所有 `websocket` 的推送,需要注意作用域的问题,面向过程请使用 `global` 对 `WebSocket\Server` 进行引用,面向对象可以把 `WebSocket\Server` 设置成一个成员属性 2961 | 2962 | **1.面向过程的写法** 2963 | 2964 | ```php 2965 | $server = new Swoole\WebSocket\Server('0.0.0.0', 9501); 2966 | $server->on('open', function(Swoole\WebSocket\Server $server, $request) { 2967 | echo "Server: handshake success with fd{$request->fd}\n"; 2968 | }); 2969 | $server->on('message', function(Swoole\WebSocket\Server $server, $frame) { 2970 | echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; 2971 | $server->push($frame->fd, "this is server"); 2972 | }); 2973 | $server->on('close', function($ser, $fd) { 2974 | echo "client {$fd} closed\n"; 2975 | }); 2976 | $server->on('request', function(Swoole\Http\Request $request, Swoole\Http\Response $response) { 2977 | global $server; // 调用外部的 server 2978 | // $server->connections 遍历所有的 websocket 连接用户的 fd,给所有用户推送 2979 | foreach($server->connections as $fd) { 2980 | $server->push($fd, $request->get['message']); 2981 | } 2982 | }); 2983 | $server->start(); 2984 | ``` 2985 | 2986 | **2.面向对象的写法** 2987 | 2988 | ```php 2989 | class WebSocketTest { 2990 | public $server; 2991 | public function __construct() { 2992 | $this->server = new Swoole\WebSocke\Server('0.0.0.0', 9501); 2993 | $this->server->on('open', function(swoole_websocket_server $server, $request) { 2994 | echo "Server: handshake success with fd{$request->fd}\n"; 2995 | }); 2996 | $this->server->on('message', function(Swoole\WebSocket\Server $server, $frame) { 2997 | echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n"; 2998 | $server->push($frame->fd, "this is server"); 2999 | }); 3000 | $this->server->on('close', function($ser, $fd) { 3001 | echo "client {$fd} closed\n"; 3002 | }); 3003 | $this->server->on('request', function($request, $response) { 3004 | // 接收 http 请求从 get 获取message参数的值,给用户推送 3005 | // $this->server->connections 遍历所有 websocket 连接用户的 fd, 给所有用户推送 3006 | foreach($this->server->connections as $fd) { 3007 | $this->server->push($fd, $request->get['message']); 3008 | } 3009 | }); 3010 | $this->server->start(); 3011 | } 3012 | } 3013 | new WebsocketTest(); 3014 | ``` 3015 | 3016 | ###### 客户端 3017 | 3018 | - `chrome`/`Firefox` / 高版本`ie`/`Safari` 等浏览器内置了 JS 的 websocket 客户端 3019 | - 微信小程序开发框架内置了 websocket 客户端 3020 | - 异步的 php 程序中可以使用 `Swoole\Http\Client` 作为 websocket 客户端 3021 | - apache/php-fpm 或其他同步阻塞的 php程序中可以使用 `swoole/framework` 提供的同步WebSocket 客户端 3022 | - 非 websocket 客户端不能与 `SebSocket` 服务器通信 3023 | 3024 | ## 9. Redis\Server 3025 | 3026 | `swoole1.8.14`开始增加了一个兼容 `Redis` 服务端协议的 server 框架,可以基于此框架实现 `Redis` 协议的服务器程序。`Swoole\Redis\Server` 继承自 `Swoole\Server` ,可调用父类提供的所有方法。 3027 | 3028 | `Redis\Server` 不需要设置 `onReceive` 回调。 3029 | 3030 | [实例程序]: https://github.com/swoole/swoole-src/blob/master/examples/redis/server.php 3031 | 3032 | ###### 可用的客户端 3033 | 3034 | - 任意编程语言的 redis 客户端,包括php的redis扩展和phpredis库 3035 | - swoole扩展提供的异步redis客户端 3036 | - redis提供的命令行工具,包括 `redis-cli`,`redis-benchmark` 3037 | 3038 | ###### 协程 3039 | 3040 | 在 swoole2.0 协程版本中,无法使用 `return` 返回值的方式发送响应结果。可以使用 `$server->send` 方式发送数据。 3041 | 3042 | ```php 3043 | use Swoole\Redis\Server; 3044 | use Swoole\Coroutine\Redis; 3045 | 3046 | $serv = new Server('0.0.0.0', 10086, SWOOLE_PROCESS, SWOOLE_SOCK_TCP); 3047 | $serv->setHandler('set', function($fd. $data) use ($serv) { 3048 | $cli = new Redis; 3049 | $cli->connect('0.0.0.0', 6379); 3050 | $cli->set($data[0], $data[1]); 3051 | 3052 | $serv->send($fd, Server::format(Server::INT, 1)); 3053 | }); 3054 | 3055 | $serv->start(); 3056 | ``` 3057 | 3058 | ###### Redis\Server::setHandler 3059 | 3060 | `Swoole\Redis\Server` 继承自 `Swoole\Server`, 可以使用父类提供的所有方法。 3061 | 3062 | `Redis\Server` 不需要设置 `onReceive` 回调。只需使用 `setHandler` 方法设置对应命令的处理函数,收到未支持的命令后自动向客户端发送 `ERROR` 响应,消息为 `ERR unknown command '$command'` 3063 | 3064 | setHandler : 设置Redis 命令字的处理器。 3065 | 3066 | ```php 3067 | function Redis\Server->setHandler(string $command, callable $callback); 3068 | ``` 3069 | 3070 | - `$command` 命令的名称 3071 | - `$callback` 命令的处理函数,回调函数返回字符串类型时会自动发送给客户端 3072 | - `$callback` 返回的数据必须为 `Redis` 格式,可使用 `format` 静态方法进行打包 3073 | 3074 | sample code: 3075 | 3076 | ```php 3077 | use Swoole\Redis\Server; 3078 | 3079 | $server = new Server('127.0.0.1', 9501); 3080 | 3081 | // 同步模式 3082 | $server->setHandler('Set', function($fd, $data) use ($server) { 3083 | $server->array($data[0], $data[1]); 3084 | return Server::format(Server::INT, 1); 3085 | }); 3086 | 3087 | // 异步模式 3088 | $server->setHandler('Get', function ($fd, $data) use ($server) { 3089 | $db->query($sql, function ($db, $result) use ($fd) { 3090 | $server->send($fd, Server::format(Server::LIST, $result)); 3091 | }); 3092 | }); 3093 | 3094 | $server->start(); 3095 | ``` 3096 | 3097 | 客户端实例: 3098 | 3099 | ```shell 3100 | redis-cli -h 127.0.0.1 -p 9501 set name rango 3101 | ``` 3102 | 3103 | ###### Redis\Server::format 3104 | 3105 | 格式化命令响应数据。 3106 | 3107 | ```php 3108 | function Redis\Server::format(int $type, mixed $value = null); 3109 | ``` 3110 | 3111 | - `$type` 表示数据类型, `NIL` 类型不需要传入 `$value`, `ERROR` 和 `STATUS` 类型 `$value` 可选,`INT`, `STRING`, `SET`, `MAP` 必选 3112 | 3113 | ###### 用到的常量 3114 | 3115 | 格式化参数常量: 3116 | 3117 | (主要用于 `format` 函数打包 redis 响应数据) 3118 | 3119 | - Server::NIL 返回 `nil` 数据 3120 | - Server::ERROR 返回错误码 3121 | - Server::STATUS 返回状态 3122 | - Server::INT 返回整数, `format` 必须传入参数值,类型必须为整数 3123 | - Server::STRING 返回字符串, `format` 必须传入参数值,类型必须为整数 3124 | - Server::SET 返回列表,`format` 必须传入参数值,类型必须为数组 3125 | - Server::MAP 返回 Map, `format` 必须传入参数值,类型必须为关联索引数组 3126 | 3127 | ## 10. Process 3128 | 3129 | swoole 中新增了一个进程管理模块,用来替代 php 的 `pcntl` 3130 | 3131 | 需要注意 Process 进程在系统里是非常昂贵的资源,创建进程消耗很大。另外创建的进程过多会导致进程切换开销大幅上升。 3132 | 3133 | ```shell 3134 | vmstat 1 1000 3135 | procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- 3136 | r b swpd free buff cache si so bi bo in cs us sy id wa st 3137 | 0 0 0 8250028 509872 4061168 0 0 10 13 88 86 1 0 99 0 0 3138 | 0 0 0 8249532 509872 4061936 0 0 0 0 451 1108 0 0 100 0 0 3139 | 0 0 0 8249532 509872 4061884 0 0 0 0 684 1855 1 3 95 0 0 3140 | 0 0 0 8249532 509880 4061876 0 0 0 16 492 1332 0 0 99 0 0 3141 | 0 0 0 8249532 509880 4061844 0 0 0 0 379 893 0 0 100 0 0 3142 | 0 0 0 8249532 509880 4061844 0 0 0 0 440 1116 0 0 99 0 0 3143 | ``` 3144 | 3145 | php 自带的 `pcntl` 存在以下缺点: 3146 | 3147 | - `pcntl` 没有提供进程间通信 3148 | - `pcntl` 不支持重定向标准输入和输出 3149 | - `pcntl` 只提供了 `fork` 这种原始的接口,使用上容易出错 3150 | - swoole_process 提供了比 `pcntl` 跟强大的功能,更易用的API,是php在多进程编程上更轻松 3151 | 3152 | swoole\process 提供了如下特性: 3153 | 3154 | - 基于 `unix socket` 和 `sysvmsg` 消息队列的进程间通信,只需调用 `write/read` 或 `push/pop` 即可 3155 | - 支持重定向标准输出和输入,在子进程内 echo 不会打印屏幕,而是写入 channel,读键盘输入可以重定向为从 channel 读取数据 3156 | - 配合Event模块,创建的 php 子进程可以异步地驱动事件 3157 | - 提供了 `exec` 接口,创建的进程可以执行其他程序,与原php父进程之间可以方便地通信。 3158 | 3159 | ###### sample code 3160 | 3161 | - 子进程异常退出时自动重启 3162 | - 主进程异常退出时,子进程会继续执行,完成所有任务后退出 3163 | 3164 | ```php 3165 | (new class{ 3166 | public $mpid = 0; 3167 | public $works = []; 3168 | public $max_process = 1; 3169 | public $new_index = 0; 3170 | 3171 | public function __construct() 3172 | { 3173 | try{ 3174 | swoole_set_process_name(sprintf('php-ps:%s', 'master')); 3175 | $this->mpid = posix_getpid(); 3176 | $this->run(); 3177 | $this->processWait(); 3178 | }catch(\Exception $e) { 3179 | die('All ERROR:' . $e->getMessage()); 3180 | } 3181 | } 3182 | 3183 | public function run() 3184 | { 3185 | for($i = 0; $i < $this->max_process; $i++) { 3186 | $this->CreateProcess(); 3187 | } 3188 | } 3189 | 3190 | public function CreateProcess($index = null) 3191 | { 3192 | $process = new swoole_process(function (swoole_process $worker) use ($index) { 3193 | if(is_null($index)) { 3194 | $index = $this->new_index; 3195 | $this->new_index++; 3196 | } 3197 | swoole_set_process_name(sprintf('php-ps:%s', $index)); 3198 | for($j = 0; $j < 16000; $j++) { 3199 | $this->checkMpid($worker); 3200 | echo "msg: {$j}\n"; 3201 | sleep(1); 3202 | } 3203 | }, false, false); 3204 | $pid = $process->start(); 3205 | $this->works[$index] = $pid; 3206 | return $pid; 3207 | } 3208 | 3209 | public function checkMpid(&$worker) 3210 | { 3211 | if(!swoole_process::kill($this->mpid, 0)) { 3212 | $worker->exit(); 3213 | // log 3214 | echo "Master process exited, I [{$worker['pid']}] also quit\n"; 3215 | } 3216 | } 3217 | 3218 | public function rebootProcess($ret) 3219 | { 3220 | $pid = $ret['pid']; 3221 | $index = array_search($pid, $this->works); 3222 | if($index !== false) { 3223 | $index = intval($index); 3224 | $new_pid = $this->CreateProcess($index); 3225 | echo "rebootProcess: {$index}={$new_pid} Done\n"; 3226 | return; 3227 | } 3228 | throw new \Exception('reboot Process Error: no pid'); 3229 | } 3230 | 3231 | public function processWait() { 3232 | while(1) { 3233 | if(count($this->works)) { 3234 | $ret = swoole_process::wait(); 3235 | if($ret) { 3236 | $this->rebootProcess($ret); 3237 | } 3238 | } else { 3239 | break; 3240 | } 3241 | } 3242 | } 3243 | }); 3244 | ``` 3245 | 3246 | 3247 | 3248 | ## 11. Process\Pool 3249 | 3250 | 进程池,基于 `Server` 的`Manager`模块实现。可管理多个工作进程。该模块的核心功能为**进程管理**,相比 `Process` 实现多进程,`Process\Pool` 更加简单,封装层次更高,开发者无需编写过多代码即可实现进程管理功能。 3251 | 3252 | > 此特性需要2.1.2+ 3253 | 3254 | ###### 常量定义 3255 | 3256 | - `SWOOLE_IPC_MSGQUEUE`:系统消息队列信息 3257 | - `SWOOLE_IPC_SOCKET`:socket 通信 3258 | 3259 | ###### 异步支持 3260 | 3261 | - 可在 `onWorkerStart` 中使用swoole 提供的 异步或协程 api,工作进程即可实现异步 3262 | - 底层自带的消息队列和 socket 通信均为同步阻塞 IO 3263 | - 如果进程为异步模式,则不要使用任何自带的同步 IPC 进程通信功能(无法使用message回调) 3264 | 3265 | > 4.0 版本以下需在 `onWorkerStart` 末尾添加 `swoole_event_wait` 进入事件循环 3266 | 3267 | ###### sample code 3268 | 3269 | ```php 3270 | $workerNum = 10; 3271 | $pool = new Swoole\Process\Pool($workerNum); 3272 | 3273 | $pool->on('WorkerStart', function($pool, $workerId) { 3274 | echo "Worker #{$workerId} is started. \n"; 3275 | $redis = new Redis(); 3276 | $redis->connect('127.0.0.1', 6379); 3277 | $key = "key1"; 3278 | while(1) { 3279 | $msgs = $redis->brpop($key, 2); 3280 | if($msgs == null) 3281 | continue; 3282 | var_dump($msgs); 3283 | } 3284 | }); 3285 | 3286 | $pool->on("WorkerStop", function($pool, $workerId) { 3287 | echo "Worker #{$workerId} is stopped. \n"; 3288 | }); 3289 | $pool->start(); 3290 | ``` 3291 | 3292 | ## 12. Client 3293 | 3294 | `client` 提供了 `TCP/UDP` `socket` 的客户端的封装代码,使用时仅需 `new Swoole\Client` 即可。 3295 | 3296 | ###### 优势 3297 | 3298 | - `stream` 函数存在超时设置的陷阱和 `Bug`, 一旦没处理好会导致 Server 端长时间阻塞。 3299 | - `stream` 函数的 fread 默认最大长度 8192 限制,无法支持 UDP 的大包 3300 | - `Client` 支持 `waitall`,在有确定包长度时可一次取完,不必循环去读 3301 | - `Client` 支持 `UDP connect`, 解决了 UDP 串包问题 3302 | - `Client` 是纯 C 编写。专门处理 `socket` ,`stream` 的复杂函数,性能更好。 3303 | - `Client` 支持长连接 3304 | 3305 | 除了普通的异步阻塞 + select 的使用方法外,`Client` 还支持异步非阻塞回调。 3306 | 3307 | ###### 同步阻塞客户端 3308 | 3309 | ```php 3310 | $client = new swoole_client(SWOOLE_SOCK_TCP); 3311 | if(!$client->connect('127.0.0.1', 9501, -1)) { 3312 | exit("Connect failed. Error: {$client->errCode}\n"); 3313 | } 3314 | $client->send("hello world\n"); 3315 | echo $client->recv(); 3316 | $client->close(); 3317 | ``` 3318 | 3319 | > `php-fpm/apache` 环境下只能使用同步客户端 3320 | > 3321 | > apache环境下仅支持 prefork 多进程模式。不支持 prework 多线程 3322 | 3323 | ###### 异步非阻塞客户端 3324 | 3325 | ```php 3326 | $client = new Swoole\Client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC); 3327 | $client->on('connect', function(swoole_client $cli) { 3328 | $cli->send("GET /HTTP/1.1\r\n\r\n"); 3329 | }); 3330 | $client->on('receive', function(swoole_client $cli, $data) { 3331 | echo "Receive: $data"; 3332 | $cli->send(str_repeat('A', 100) . "\n"); 3333 | sleep(1); 3334 | }); 3335 | $client->on('error', function(swoole_client $cli) { 3336 | echo "error\n"; 3337 | }); 3338 | $client->on('close', function(swoole_client $cli) { 3339 | echo "connect close\n"; 3340 | }); 3341 | $client->connect('127.0.0.1', 9501); 3342 | ``` 3343 | 3344 | > 异步客户端只能在 cli 下使用 3345 | 3346 | ### 12.1 方法列表 3347 | 3348 | ###### SSL/TLS 3349 | 3350 | - 依赖 `openssl` 库,需要在编译 swoole 时增加 `enable-openssl` 或 `with-openssl-dir` 3351 | - 必须在定义 `Client` 时增加 `SWOOLE_SSL` 3352 | 3353 | > 低于 1.9.5 的版本在设置 `ssl_key_file` 后会自动启用 SSL 3354 | 3355 | ```php 3356 | $client = new Swoole\Client(SWOOLE_TCP|SWOOLE_ASYNC|SWOOLE_SSL); 3357 | ``` 3358 | 3359 | #### 12.1.1 construct 3360 | 3361 | ```php 3362 | swoole_client->__construct(int $sock_type, int $is_sync = SWOOLE_SOCK_SYNC, string $key); 3363 | ``` 3364 | 3365 | 可以使用 swoole 提供的宏来指定类型,参考本 markdown 文档附录的 `swoole常量定义` 3366 | 3367 | - `$sock_type` 表明 `socket` 的类型,如 `TCP`/`UDP` 3368 | - 使用 `$sock_type`/`SWOOLE_SSL` 可启用SSL加密 3369 | - `$is_sync` 表示同步阻塞还是异步非阻塞,默认为同步阻塞 3370 | - `$key` 用于长连接的 `key`,默认使用 IP :端口 作为 `key`,相同 `key` 的连接会被复用 3371 | 3372 | ###### 在php-fpm / apache 中创建长连接 3373 | 3374 | ```php 3375 | $cli = new swoole_client(SWOOLE_TCP|SWOOLE_KEEP); 3376 | ``` 3377 | 3378 | 加入 `SWOOLE_KEEP` 标志后,创建的 TCP 连接在PHP请求结束或调用 `$cli->close` 时不会关闭,下一次执行 connect 调用时会复用上一次创建的连接。长连接保存的方式默认是以 ServerHost : ServerPort 为 key 的,可以在第三个参数内指定 key。 3379 | 3380 | - `SWOOLE_KEEP`只适用于同步客户端 3381 | 3382 | > > swoole_client 在 unset 时会自动调用 close 方法关闭 socket 3383 | > 3384 | > 异步模式 unset 时会自动关闭 socket 并从 epoll 事件轮询中移除 3385 | 3386 | ###### 在 swoole_server 中使用 swoole_client 3387 | 3388 | - 必须在事件回调函数中使用 `swoole_client `,不能在 `swoole_server->start` 前就创建。 3389 | - `swoole_server` 可以用任何语言编写的 socket client 连接,同样 `swoole_client` 也可以去连接任何语言编写的 socket server 3390 | 3391 | #### 12.1.2 set 3392 | 3393 | #### 12.1.3 on 3394 | 3395 | #### 12.1.4 connect 3396 | 3397 | #### 12.1.5 isConnected 3398 | 3399 | #### 12.1.6 getSocket 3400 | 3401 | #### 12.1.7 getSockName 3402 | 3403 | #### 12.1.8 getPeerName 3404 | 3405 | #### 12.1.9 getPeerCert 3406 | 3407 | #### 12.1.10 send 3408 | 3409 | #### 12.1.11 sendto 3410 | 3411 | #### 12.1.12 sendfile 3412 | 3413 | #### 12.1.13 recv 3414 | 3415 | #### 12.1.14 close 3416 | 3417 | #### 12.1.15 sleep 3418 | 3419 | #### 12.1.16 wakeup 3420 | 3421 | #### 12.1.17 enableSSL 3422 | 3423 | ### 12.2 回调函数 3424 | 3425 | #### 12.2.1 onConnect 3426 | 3427 | #### 12.2.2 onError 3428 | 3429 | #### 12.2.3 onReceive 3430 | 3431 | #### 12.2.4 onClose 3432 | 3433 | #### 12.2.5 onBufferFull 3434 | 3435 | #### 12.2.6 onBufferEmpty 3436 | 3437 | ### 12.3 属性列表 3438 | 3439 | #### 12.3.1 errCode 3440 | 3441 | #### 12.3.2 sock 3442 | 3443 | #### 12.3.3 reuse 3444 | 3445 | ### 12.4 并行 3446 | 3447 | #### 12.4.1 swoole_client_select 3448 | 3449 | #### 12.4.2 TCP客户端异步连接 3450 | 3451 | #### 12.4.3 SWOOLE_KEEP参数创建 TCP 长连接 3452 | 3453 | ### 12.5 常量 3454 | 3455 | ### 12.6 配置选项 3456 | 3457 | #### 12.6.1 ssl_verify_peer 3458 | 3459 | #### 12.6.2 ssl_host_name 3460 | 3461 | #### 12.6.3 ssl_cafile 3462 | 3463 | #### 12.6.4 ssl_capath 3464 | 3465 | #### 12.6.5 package_length_func 3466 | 3467 | #### 12.6.6 http_proxy_host 3468 | 3469 | ### 12.7 常见问题 3470 | 3471 | ## 13. Event 3472 | 3473 | 除了异步 `server` 和 `client` 库之外,`swoole` 扩展还提供了直接操作底层 `epoll`/`kqueue` 事件循环的接口。可将其他扩展创建的 `socket` ,php代码中 `stream` / `socket` 扩展创建的 `socket` 等加入到 `Swoole` 的 `EventLoop` 中。 3474 | 3475 | ###### 事件优先级 3476 | 3477 | 1. 通过 `Process::signal` 设置的信号处理回调函数 3478 | 2. 通过 `Event::defer` 设置的延迟执行函数 3479 | 3. 通过 `Timer::tick` 和 `Timer::after` 设置的定时器回调 3480 | 4. 通过 `Event::cycle` 设置的周期回调函数 3481 | 3482 | 3483 | 3484 | ## 14. 高级特性 3485 | 3486 | ### 14.1 swoole的实现 3487 | 3488 | swoole 使用 `C/C++11` 编写,不依赖其他第三方库。 3489 | 3490 | - swoole 并没使用 libevent,所以不依赖于 libevent扩展 3491 | - swoole 并不依赖 php 的 stream / sockets / pcntl / posix / sysvmsg 等扩展 3492 | 3493 | ###### socket 部分 3494 | 3495 | swoole 使用底层的 socket 系统调用。参加源码的 sys/socket.h 3496 | 3497 | ###### IO 事件循环 3498 | 3499 | - 在linux 系统下使用 `epoll` , `MacOS / FreeBSD` 下使用 kqueue 3500 | - task 进程没有事件循环,进程会循环阻塞读取管道 3501 | 3502 | > 很多人使用 `strace -p` 查看 swoole 主进程只能看到 poll 系统调用。正确的查看方法是 stace -f -p 3503 | > 3504 | > ###### 多进程 / 多线程 3505 | > 3506 | > - 多进程使用 `fork()` 系统调用 3507 | > - 多线程使用 `pthread` 线程库 3508 | > 3509 | > ###### EventFd 3510 | > 3511 | > swoole 使用了 `eventfd` 作为线程 / 进程间消息通知的机制。 3512 | > 3513 | > ###### Signalfd 3514 | > 3515 | > swoole 中使用了 `signalfd` 来实现对信号的屏蔽和处理。可以有效地避免线程 / 进程被信号打断,系统调用 `restart` 的问题。在主进程中 `Reactor/AIO` 线程不会接收任何信号。 3516 | 3517 | ### 14.2 Reactor线程 3518 | 3519 | swoole 的主进程是一个多线程的程序。其中有一组很重要的线程,称之为 Reactor 线程。它是真正处理 TCP 连接,收发数据的线程。 3520 | 3521 | swoole 的主进程在 Accept 新的连接后,会将这个连接分配给一个固定的 Reactor 线程,并由这个线程负责监听此 socket。在 socket 可读时读取数据,并进行协议解析,将请求投递到 worker 进程。在 socket 可写时将数据发送给 TCP 客户端。 3522 | 3523 | > 分配计算的方式是 fd % serv-> reactor_num 3524 | > 3525 | > ###### TCP 和 UDP 的差异 3526 | > 3527 | > - TCP 客户端,worker 进程处理完请求后,调用 `$server->send` 会将数据发送给 `Reactor` 线程,由 `Reactor` 线程再发送给客户端 3528 | > - UDP 客户端,worker 进程处理完请求后,调用 `$server->sendto` 直接发送给客户端,无需经过 `Reactor` 线程 3529 | 3530 | ### 14.3 Manager进程 3531 | 3532 | swoole 中 worker/task 进程都是由 Manager 进程 Fork 并管理的。 3533 | 3534 | - 子进程结束运行时,manager 进程负责回收此子进程,避免成为僵尸进程。并创建新的子进程 3535 | - 服务器关闭时,manager 进程将发送信号给所有子进程,通知子进程关闭服务 3536 | - 服务器reload 时,manager进程会逐个关闭或重启子进程 3537 | 3538 | 为什么不是 master 进程呢,因为 master 进程是多线程的,不能安全地执行 fork 操作 3539 | 3540 | ### 14.4 Worker进程 3541 | 3542 | Swoole提供了完善的进程管理机制,当 worker 进程异常退出时,如果发生 php 的致命错误,被其他程序误杀,或达到 max_request 次数之后正常退出。主进程会重新拉起新的 worker 进程。worker 进程内可以像普通的 apache+php 或 php-fpm 中那样写逻辑,而不需要像 nodejs 那样写异步回调的程序。 3543 | 3544 | ###### 主进程内的回调函数 3545 | 3546 | `onStart`, `onShutdown`, `onTimer` 3547 | 3548 | ###### worker 进程内的回调函数 3549 | 3550 | `onWorkerStart`, `onWorkerStop`, `onConnect`, `onClose`, `onReceive`, `onFinish` 3551 | 3552 | ###### task_worker 进程内的回调函数 3553 | 3554 | `onTask`, `onWorkerStart` 3555 | 3556 | ###### 管理进程内的回调函数 3557 | 3558 | `onManagerStart`, `onManagerStop` 3559 | 3560 | ### 14.5 Reactor,Worker,TaskWorker的关系 3561 | 3562 | `Reactor`, `Worker`, `TaskWorker` 三者分别的职责是: 3563 | 3564 | ###### Reactor线程 3565 | 3566 | - 负责维护客户端 tcp 连接,处理网络IO,处理协议,收发数据 3567 | - 完全是异步非阻塞的模式 3568 | - 全部为 C 代码,除 `start`/`shutdown` 事件回调以外,不执行任何 php 代码 3569 | - 将 tcp 客户端发来的数据缓冲,拼接,拆分成完整的一个请求数据包 3570 | - `Reactor` 以多线程的方式运行 3571 | 3572 | ###### Worker 进程 3573 | 3574 | - 接收由 `Reactor` 线程投递的请求数据包,并执行 php 回调函数处理数据 3575 | - 生成响应数据并发送给 `Reactor` 线程,由 `Reactor` 线程发送给 TCP 客户端 3576 | - 可以是异步非阻塞模式,也可以是同步阻塞模式 3577 | - `Worker` 以多进程的方式运行 3578 | 3579 | ###### TaskWorker 进程 3580 | 3581 | - 接收由 `Worker` 进程通过 `swoole_server->task/taskwait` 方法投递的任务 3582 | - 处理任务,并将结果数据返回给`Worker` 进程处理(`swoole_server->finish`) 3583 | - 完全是**同步阻塞**模式 3584 | - `TaskWorker` 以多进程的方式运行 3585 | 3586 | ###### 关系 3587 | 3588 | 可以理解为 `Reactor` 就是 `nginx` , `Worker` 是 `php-fpm`, `Reactor` 线程异步并行地处理网络请求,然后再转发给 `Worker` 进程中处理。`Reactor` 和 `Worker` 间通过 `Unix Socket` 通信。 3589 | 3590 | 在 `php-fpm` 的应用中,经常会将一个任务异步投递到 `Redis` 等队列中,并在后台启动一些 `php` 进程异步地处理这些任务。`Swoole` 提供的 `TaskWorker` 是一套完整的方案,将任务的投递,队列,php任务处理进程管理合为一体。通过底层提供的api 可以简便地实现异步任务的处理。另外 `TaskWorker` 还可以在任务执行完成后再返回一个结果到 `Worker`. 3591 | 3592 | `Swoole` 的 `Reactor`, `Worker`, `TaskWorker` 之间可以紧密地结合起来,提供更高级的使用方式。 3593 | 3594 | 一个更通俗的比喻,假设 `Server` 是一个工厂,那么 `Reactor` 就是销售,接收客户订单。而 `Worker` 是工人,当销售接收到订单后,`Worker` 去生产出客户要的东西。而 `TaskWorker` 可以理解为行政人员,可以帮助 `Worker` 干些杂事,让 `Worker` 专心工作。 3595 | 3596 | 底层会为 `Worker` 进程,`TaskWorker` 进程分配一个唯一的 ID 3597 | 3598 | 不同的 `Worker` 和 `TaskWorker` 进程之间可以通过 `sendMessage` 接口来通信 3599 | 3600 | ### 14.6 Task/Finish 特性的使用 3601 | 3602 | task模块用来做一些异步的慢速任务,比如 webim 中发广播,发送邮件等。 3603 | 3604 | - task进程必须是同步阻塞的 3605 | - task进程支持定时器 3606 | 3607 | nodejs 假如由10万个连接,要发广播时会循环10万次,这时候程序不能做任何事情,不能接收新的连接,也不能收包和发包。 3608 | 3609 | 但是swoole不同,任务交给task进程之后,worker 进程可以继续处理新的数据请求。任务完成后会异步地通知 worker 进程告诉它此任务已经完成。 3610 | 3611 | 当然task模块的作用不仅限于此,实现php的数据库连接池,异步队列等,还需要进一步挖掘。 3612 | 3613 | 3614 | 3615 | ### 14.7 在php-fpm 或 apache 中使用 swoole 3616 | 3617 | swoole 中绝大部分的模块只能用于 cli 环境,只有同步阻塞的 `swoole_client` 可以用于 `php-fpm` 和 `apache` 环境 3618 | 3619 | ###### 同步swoole_client 3620 | 3621 | ```php 3622 | $client = new swoole_client(SWOOLE_SOCK_TCP); // 同步阻塞 3623 | $client->connect('127.0.0.1', 9501) or die("connect failed\n"); 3624 | $client->send(str_repeat('A', 600)); 3625 | $data = $client->recv(700, 0) or die("recv failed\n"); 3626 | echo "recv: " . $data . "\n"; 3627 | ``` 3628 | 3629 | 3630 | 3631 | ### 14.8 swoole选择异步还是同步 3632 | 3633 | swoole不仅支持异步,还支持同步。什么情况下用同步什么情况下用异步,我们可以这样判断: 3634 | 3635 | 我们不赞成用异步回调 的方式去做功能开发,传统的php同步方式实现功能和逻辑是最简单的,也是最佳的方案。像nodejs这样到处 callback,只会牺牲可维护性和开发效率。 3636 | 3637 | 但有些时候很适合用异步,如FTP工具,聊天服务器,smtp,代理服务器等。此类场景均已通信和读写磁盘为主,功能和业务逻辑其次的业务场景。 3638 | 3639 | “php的扩展函数API 全是同步的”,这个说法不对。实际上同步阻塞的地方主要是网络调用,文件读写。例如 mysql_query 需要与 mysql 数据库服务器同i性能,curl 需要联网,file_get_contents 需要读取文件,以及其他 `fopen` / `fwrite` / `fread` / `fgets` / `fputs` 这些都是同步阻塞的API,除此紫外php 的 `array` / `string` / `mbstring` 等函数是非阻塞的。 3640 | 3641 | swoole 提供了异步的socket 客户端,异步的mysql, 而且提供了异步文件读写,异步DNS 查询的功能。另外还提供了 task/finish 的API,完全可以解决阻塞 IO 问题。 3642 | 3643 | ### 14.9 TCP/UDP 压力测试工具 3644 | 3645 | `swoole` 提供了一套 `tcp`/`udp` 压测工具,`benchmark/run.php` 中,基于 `swoole_client` + `pcntl` 实现。与 `ab` ,`http_bench` 等工具不同,run.php 是基于多进程实现并发测试的。使用方法如下: 3646 | 3647 | ```shell 3648 | php run.php -c 100 -n 10000 -s tcp://127.0.0.1:9501 -f long_tcp 3649 | ``` 3650 | 3651 | - -c : 并发的数量,会启动对应数量的进程用于测试 3652 | - -n : 请求的总数量,-n 10000,-c 100, 平均到每个子进程的数量为 100 3653 | - -s : Server 的 ip+port 3654 | - -f : 测试单元的名称。目前提供了 `long_tcp` / `short_tcp` / `udp` / `websocket` 函数,可自行实现单元测试函数 3655 | 3656 | 测试打印结果如下: 3657 | 3658 | ```json 3659 | concurrency: 100 //并发数量 3660 | request num: 10000 //请求总数 3661 | lost num: 0 //失败次数 3662 | success num: 10000 //成功次数 3663 | total time: 0.157 //总耗时 3664 | req per second: 63558 //qps,每秒处理的请求数 3665 | one req use(ms): 0.015 //单个请求的平均时长,此结果目前不准确,请勿作为参考 3666 | ``` 3667 | 3668 | ###### 异步客户端 3669 | 3670 | 在`benchmark` 目录下还提供了一个 `async.php` 异步压测工具,使用方法与同步压测脚本 `run.php` 相同。但底层使用了异步IO,因此可以支持更大规模的并发压测。 3671 | 3672 | ###### 注意事项 3673 | 3674 | - 压测不要使用 `--enable-debug` 和 `--enable-swoole-debug` 参数的版本 3675 | - 压测的服务器程序中不要使用 `echo` 打印内容到屏幕,否则会严重拉低测试分值 3676 | 3677 | ### 14.10 MySQL连接池,异步,短线重连 3678 | 3679 | ###### MySQL长连接 3680 | 3681 | MySQL短连接每次请求操作数据库都要建立与MySQL服务器建立TCP连接,这是需要时间开销的。TCP连接需要三次网络通信,这样就增加了一定的延时和额外IO开销。请求结束后关闭 MySQL 连接,还会发生3-4次网络通信。 3682 | 3683 | > close操作不会增加响应的延时,原因是close后是由操作系统自动进行通信的,应用程序感知不到。 3684 | 3685 | 长连接可以避免每次请求都创建连接的开销,节省了时间和IO消耗,提升php程序的性能。 3686 | 3687 | ###### 断线重连 3688 | 3689 | 在 cli 环境下,php程序需要长时间运行,客户端和MySQL服务器之间的TCP连接是不稳定的。 3690 | 3691 | - MySQL-server 会在一定时间内自动切断连接 3692 | - php程序遇到空闲期时,长时间没有mysql查询,mysql-server 也会自动切断连接来节省资源 3693 | - 其他情况,在MySQL服务器中执行 kill process 杀掉某个连接,MySQL服务器可能会重启 3694 | 3695 | 此时php中的MySQL连接就无效了。 3696 | 3697 | ### 14.11 php中哪些函数是同步阻塞的 3698 | 3699 | ###### 同步阻塞函数 3700 | 3701 | - mysql,mysqli,pdo以及其他DB操作类函数 3702 | - sleep,usleep 3703 | - curl 3704 | - stream,socket扩展的函数 3705 | - swoole_client 同步模式 3706 | - memcache,redis扩展函数 3707 | - file_get_contents/fread 等文件读取函数 3708 | - swoole_server->taskwait 3709 | - swoole_server->sendwait 3710 | 3711 | > swoole_server的php代码中有上述函数,server就是同步阻塞的 3712 | > 3713 | > 代码中没有异步服务器 3714 | 3715 | ###### 异步非阻塞函数 3716 | 3717 | - swoole_client 异步模式 3718 | - mysql-async 库 3719 | - redis-async 库 3720 | - swoole_timer_tick / swoole_timer_after 3721 | - swoole_event 及相关event函数 3722 | - swoole_table / swoole_atomic / swoole_buffer 3723 | - swoole_server->task / finish 函数 3724 | 3725 | 3726 | 3727 | ### 14.12 守护进程程序常用数据结构 3728 | 3729 | #### 队列 3730 | 3731 | ## 附录 3732 | 3733 | ### swoole预定义常量 3734 | 3735 | | 常量名 | 作用 | 3736 | | ---------------------------- | ------------------------------------------------------------ | 3737 | | SWOOLE_VERSION | 版本号 | 3738 | | SWOOLE_BASE | 使用base模式,业务代码在Reactor进程中直接执行(swoole_server::__construct中) | 3739 | | SWOOLE_PROCESS | 使用进程模式,业务代码在 Worker 进程中执行(swoole_server::__construct中) | 3740 | | SWOOLE_SOCK_TCP | 创建tcp socket(swoole_client::__construct) | 3741 | | SWOOLE_SOCK_TCP6 | 创建tcp ipv6 socket (swoole_client::__construct) | 3742 | | SWOOLE_SOCK_UDP | 创建udp socket(swoole_client::__construct) | 3743 | | SWOOLE_SOCK_UDP6 | 创建udp ipv6 socket (swoole_client::__construct) | 3744 | | SWOOLE_SOCK_UNIX_DGRAM | 创建 unix dgram socket (swoole_client::__construct) | 3745 | | SWOOLE_SOCK_UNIX_STREAM | 创建 unix stream socket (swoole_client::__construct) | 3746 | | SWOOLE_SOCK_SYNC | 同步客户端(swoole_client::__construct) | 3747 | | SWOOLE_SOCK_ASYNC | 异步客户端(swoole_client::__construct) | 3748 | | SWOOLE_FILELOCK | 创建文件锁(swoole_lock::__construct中) | 3749 | | SWOOLE_MUTEX | 创建互斥锁(swoole_lock::__construct中) | 3750 | | SWOOLE_RWLOCK | 创建读写锁(swoole_lock::__construct中) | 3751 | | SWOOLE_SPINLOCK | 创建自旋锁(swoole_lock::__construct中) | 3752 | | SWOOLE_SEM | 创建信号量(swoole_lock::__construct中) | 3753 | | SWOOLE_SSLv3_METHOD | SSL 加密方法中用到 | 3754 | | SWOOLE_SSLv3_SERVER_METHOD | SSL 加密方法中用到 | 3755 | | SWOOLE_SSLv3_CLIENT_METHOD | SSL 加密方法中用到 | 3756 | | SWOOLE_SSLv23_METHOD | **默认采用的加密方式** | 3757 | | SWOOLE_SSLv23_SERVER_METHOD | SSL 加密方法中用到 | 3758 | | SWOOLE_SSLv23_CLIENT_METHOD | SSL 加密方法中用到 | 3759 | | SWOOLE_TLSv1_METHOD | SSL 加密方法中用到 | 3760 | | SWOOLE_TLSv1_SERVER_METHOD | SSL 加密方法中用到 | 3761 | | SWOOLE_TLSv1_CLIENT_METHOD | SSL 加密方法中用到 | 3762 | | SWOOLE_TLSv1_1_METHOD | SSL 加密方法中用到 | 3763 | | SWOOLE_TLSv1_1_SERVER_METHOD | SSL 加密方法中用到 | 3764 | | SWOOLE_TLSv1_1_CLIENT_METHOD | SSL 加密方法中用到 | 3765 | | SWOOLE_TLSv1_2_METHOD | SSL 加密方法中用到 | 3766 | | SWOOLE_TLSv1_2_SERVER_METHOD | SSL 加密方法中用到 | 3767 | | SWOOLE_TLSv1_2_CLIENT_METHOD | SSL 加密方法中用到 | 3768 | | SWOOLE_DTLSv1_METHOD | SSL 加密方法中用到 | 3769 | | SWOOLE_DTLSv1_SERVER_METHOD | SSL 加密方法中用到 | 3770 | | SWOOLE_DTLSv1_CLIENT_METHOD | SSL 加密方法中用到 | -------------------------------------------------------------------------------- /tcpdump抓包工具简单使用.md: -------------------------------------------------------------------------------- 1 | #### tcpdump 抓包工具的简单使用 2 | 3 | 在调试网络通信程序时,tcpdump 是必备的工具。tcpdump 工具很强大,可以看到网络通信的每个细节,如TCP,可以看到3次握手,PUSH/ACK 数据推送,close 4次挥手,全部细节。包括每一次网络收包的字节数,时间等。 4 | 5 | ###### sample 6 | 7 | ```shell 8 | sudo tcpdump -i any tcp port 9501 9 | ``` 10 | 11 | - -i 指定网卡,any表示所有网卡 12 | - tcp 指定仅监听 tcp 协议的包 13 | - port 指定监听的端口 14 | 15 | > tcpdump 需要root权限 16 | > 17 | > 需要看通信的数据内容,可以加 -Xnlps0 参数,其他更多参数可以搜 18 | > 19 | > 运行结果 20 | > 21 | > ```shell 22 | > 13:29:07.788802 IP localhost.42333 > localhost.9501: Flags [S], seq 828582357, win 43690, options [mss 65495,sackOK,TS val 2207513 ecr 0,nop,wscale 7], length 0 23 | > 13:29:07.788815 IP localhost.9501 > localhost.42333: Flags [S.], seq 1242884615, ack 828582358, win 43690, options [mss 65495,sackOK,TS val 2207513 ecr 2207513,nop,wscale 7], length 0 24 | > 13:29:07.788830 IP localhost.42333 > localhost.9501: Flags [.], ack 1, win 342, options [nop,nop,TS val 2207513 ecr 2207513], length 0 25 | > 13:29:10.298686 IP localhost.42333 > localhost.9501: Flags [P.], seq 1:5, ack 1, win 342, options [nop,nop,TS val 2208141 ecr 2207513], length 4 26 | > 13:29:10.298708 IP localhost.9501 > localhost.42333: Flags [.], ack 5, win 342, options [nop,nop,TS val 2208141 ecr 2208141], length 0 27 | > 13:29:10.298795 IP localhost.9501 > localhost.42333: Flags [P.], seq 1:13, ack 5, win 342, options [nop,nop,TS val 2208141 ecr 2208141], length 12 28 | > 13:29:10.298803 IP localhost.42333 > localhost.9501: Flags [.], ack 13, win 342, options [nop,nop,TS val 2208141 ecr 2208141], length 0 29 | > 13:29:11.563361 IP localhost.42333 > localhost.9501: Flags [F.], seq 5, ack 13, win 342, options [nop,nop,TS val 2208457 ecr 2208141], length 0 30 | > 13:29:11.563450 IP localhost.9501 > localhost.42333: Flags [F.], seq 13, ack 6, win 342, options [nop,nop,TS val 2208457 ecr 2208457], length 0 31 | > 13:29:11.563473 IP localhost.42333 > localhost.9501: Flags [.], ack 14, win 342, options [nop,nop,TS val 2208457 ecr 2208457], length 0 32 | > ``` 33 | > 34 | > - 13:29:11.563473 时间带有精确到微秒 35 | > - localhost.42333 > localhost.9501 表示通信的流向,42333是客户端,9501是服务器端 36 | > - [S] 表示这是一个SYN请求 37 | > - [.] 表示这是一个ACK确认包,(client)SYN->(server)SYN->(client)ACK 就是3次握手过程 38 | > - [P] 表示这个是一个数据推送,可以是从服务器端向客户端推送,也可以从客户端向服务器端推 39 | > - [F] 表示这是一个FIN包,是关闭连接操作,client/server都有可能发起 40 | > - [R] 表示这是一个RST包,与F包作用相同,但RST表示连接关闭时,仍然有数据未被处理。可以理解为是强制切断连接 41 | > - win 342是指滑动窗口大小 42 | > - length 12指数据包的大小 43 | 44 | -------------------------------------------------------------------------------- /使用asan检测内存.md: -------------------------------------------------------------------------------- 1 | #### 使用 asan 检测内存 2 | 3 | 高版本 `gcc` 和 `clang` 支持 `asan` 内存检测,与 `valgrind` 相比 `asan` 消耗非常低,甚至可以直接在生产环境中启用 `asan` 排查跟踪内存问题。 4 | 5 | 使用 `asan` 特性必须将php也编译为 `asan`,否则运行时会报错。 6 | 7 | ###### 编译 php 8 | 9 | 执行 `./configure` 后,修改 `Makefile` 修改 `CFLAGS_CLEAN` 末尾追加 `-fsanitize=address -fno-omit-frame-pointer`,然后执行 `make clean && make install` 10 | 11 | ###### 编译 Swoole 12 | 13 | ```shell 14 | phpize 15 | ./configure --enable-asan 16 | make 17 | make install 18 | ``` 19 | 20 | ###### 关闭内存泄漏检测 21 | 22 | php 中 `ZendVM` 有较多进程退出时内存释放的逻辑,可能会引起 `asan` 误报,可以设置 `export ASAN_OPTIONS=detect_leaks=0` 暂时关闭 `asan` 的内存泄漏检测。 -------------------------------------------------------------------------------- /使用systemd管理swoole服务器.md: -------------------------------------------------------------------------------- 1 | #### 使用systemd管理swoole服务 2 | 3 | Systemd是Linux系统中新一代的初始化系统(init),它主要的设计目标是客服 sysvinit 固有的缺点,提高系统的启动速度。很多新的Linux发行版已经使用 `Systemd` 取代了 `init` ,作为初始守护进程。 4 | 5 | Swoole的服务器程序可以编写一段 `Service` 脚本,交由 `systemd` 进行管理。实现故障重启,开机自启动等功能。 6 | 7 | ###### 编写Service脚本 8 | 9 | Systemd 的 Service 配置在 `/etc/systemd/system/` 目录中,可以创建一个 `echo.service` 文件,实际项目应改为对应的名称。编辑此文件,添加下列内容: 10 | 11 | ```ini 12 | [Unit] 13 | Description = Echo Http Server 14 | After = network.target 15 | After = syslog.target 16 | 17 | [Service] 18 | Type = simple 19 | LimitNOFILE = 65535 20 | ExecStart = /usr/bin/php /opt/servers/echo/server.php 21 | ExecReload = /bin/kill -USR1 $MAINPID 22 | Restart = always 23 | 24 | [Install] 25 | WantedBy = multi-user.target graphical.target 26 | ``` 27 | 28 | - `After` 指令约定了启动的顺序,必须在 `network` 和 `syslog` 启动后才启动 `echo` 服务 29 | - `Service` 中填写了应用程序的路径信息,请修改为实际项目对应的路径 30 | - `Restart=always` 表示如果进程挂掉会自动拉起 31 | - `WantedBy` 约定了在哪些环境下启动,multi-user.target graphical.target 表示在图形界面和命令行环境都会启动 32 | 33 | 编写完成后需 reload 守护进程使其生效 34 | 35 | ```shell 36 | sudo systemctl --system daemon-reload 37 | ``` 38 | 39 | ###### sample 40 | 41 | ```php 42 | $http = new swoole_http_server("0.0.0.0", 9501); 43 | $http->on('request', function($request, $response) { 44 | $response->header("Content-Type", "text/html;charset=utf-8"); 45 | $response->end("

Hello Swoole. #" . rand(1000, 9999) . "

"); 46 | }); 47 | $http->start(); 48 | ``` 49 | 50 | ###### 管理服务 51 | 52 | ```shell 53 | # 启动服务 54 | sudo systemctl start echo.service 55 | # reload 服务 56 | sudo systemctl reload echo.service 57 | # 关闭服务 58 | sudo systemctl stop echo.service 59 | ``` 60 | 61 | ###### 查看服务状态 62 | 63 | ```shell 64 | sudo systemctl status echo.service 65 | ``` 66 | 67 | -------------------------------------------------------------------------------- /协程CPU密集场景调度实现.md: -------------------------------------------------------------------------------- 1 | #### 协程CPU密集场景调度实现 2 | 3 | ###### 抢占式 / 非抢占式 4 | 5 | 如果服务场景是 IO 密集型,那么非抢占式可行。如果服务中加入了CPU密集型操作,我们就不得不考虑重新安排协程的调度模式了。 6 | 7 | 在 Swoole 协程系列文章中我们曾经介绍过 IO 密集场景下协程基于非抢占式调度的优势和卓越的性能。但是在CPU密集的场景下抢占式调度是非常重要的。试想有以下场景,程序中有 A,B 两个协程,协程A一直在执行 CPU 密集型的计算,非抢占式的调度模型中,A不会主动让出控制权,从而导致B得不到时间片,协程得不到均衡调度。导致的问题是假如当前服务 A,B同时对外提供服务,B协程处理的请求就可能因为得不到时间片而导致请求超时,在企业级应用中,这种情况是有危害的。 8 | 9 | ###### php 对此的应对方式 10 | 11 | 由于php是单线程运行的,所以针对 php 的协程调度和 `go` 完全不同,我们选择使用 `declare(tick=N)` 语法功能实现协程调度。Tick(时钟周期)是一个在 declare 代码段中解释器每执行 N 条可计时的低级语句就会发生的事情。N 的值是在 declare 中的 directive 部分用 ticks = N 来指定的。不是所有语句都可计时。通常条件表达式和参数表达式都不可计时,以下类型是不可被 `tick` 计数的。 12 | 13 | ```c 14 | static inline zend_bool zend_is_unticked_stmt(zend_ast *ast) 15 | { 16 | return ast->kind == ZEND_AST_STMT_LIST || ast->kind == ZEND_AST_LABEL || ast->kind == ZEND_AST_PROP_DECL || ast->kind == ZEND_AST_CLASS_CONST_DECL || ast->kind == ZEND_AST_USE_TRAIT || ast->kind == ZEND_AST_METHOD; 17 | } 18 | ``` 19 | 20 | 协程调度的逻辑是每次触发 `tick handler`,我们判断当前协程相对最近一次调度时间是否大于协程最大执行时间。这样就可以让协程超出执行时间后被其他协程抢占(让出),这种调度表现为抢占式调度,且不是基于IO,首先来一段php最简单加法指定执行的压测。 21 | 22 | ```php 23 | $max_msec, 63 | ]); 64 | $s = microtime(true); 65 | echo "start\n"; 66 | $flag = 1; 67 | go(function() use (&$flag, $max_msec) { 68 | echo "coroutine 1 start to loop for $max_msec msec\n"; 69 | $i = 0; 70 | while(1) $i++; 71 | echo "coroutine 1 can exit\n"; 72 | }); 73 | $t = microtime(true); 74 | $u = round(($t - $s) * 1000, 5); 75 | echo "schedule use time " . $u . "ms\n"; 76 | 77 | go(function () use (&$flag) { 78 | echo "coroutine 2 set flag = false\n"; 79 | $flag = false; 80 | }); 81 | echo "end\n"; 82 | ``` 83 | 84 | 输出结果为 85 | 86 | ```python 87 | start 88 | coroutine 1 start to loop for 10 msec 89 | schedule use time 10.1835ms 90 | coroutine 2 set flag = false 91 | end 92 | coroutine 1 can exit 93 | ``` 94 | 95 | 其中 coroutine1 的 opcodes 为 96 | 97 | ```ini 98 | {closure}: ; (lines=18, args=0, vars=3, tmps=5) 99 | ; (before optimizer) 100 | ; /path-to-tick/tick.php:12-19 101 | L0 (12): BIND_STATIC (ref) CV0($flag) string("flag") 102 | L1 (12): BIND_STATIC CV1($max_msec) string("max_msec") 103 | L2 (13): T4 = ROPE_INIT 3 string("coro 1 start to loop for ") 104 | L3 (13): T4 = ROPE_ADD 1 T4 CV1($max_msec) 105 | L4 (13): T3 = ROPE_END 2 T4 string(" msec 106 | ") 107 | L5 (13): ECHO T3 108 | L6 (13): TICKS 1000 109 | L7 (14): ASSIGN CV2($i) int(0) 110 | L8 (14): TICKS 1000 111 | L9 (15): JMP L13 112 | L10 (16): T7 = POST_INC CV2($i) 113 | L11 (16): FREE T7 114 | L12 (16): TICKS 1000 115 | L13 (15): JMPNZ CV0($flag) L10 116 | L14 (15): TICKS 1000 117 | L15 (18): ECHO string("coro 1 can exit 118 | ") 119 | L16 (18): TICKS 1000 120 | L17 (19): RETURN null 121 | LIVE RANGES: 122 | 4: L2 - L4 (rope) 123 | ``` 124 | 125 | 现在基于 tick 的调度实现已单独放在 126 | 127 | [分支]: https://github.com/swoole/swoole-src/tree/schedule 128 | 129 | ,测试用例在 130 | 131 | [这里]: https://github.com/swoole/swoole-src/tree/schedule/tests/swoole_coroutine/schedule 132 | 133 | ###### 小结 134 | 135 | 使用 tick 的方式实现有 一个较大的缺点就是需要用户在 php 层的脚本开始的地方声明 `declare(tick=N)` ,这样使得这个功能对于扩展层来说不够完备。但是它能够处理所有的php指令,同时我们在处理 tick handler 时,HOOK 了php 默认的方式,因为使用了默认的方式,php用户层可以注册 `tick` 函数造成干扰。我们发现,历史提交记录中有一种方式是基于 HOOK 循环指令的方式实现的。我们假设使得 CPU 密集的类型是大量的循环操作,我们检测循环的次数和当前协程运行的时间,即每次遇到循环指令的 handler,我们去检查当前循环的次数和协程执行的时间,进而可以发现执行时间较长的卸车嗯。 136 | 137 | 但是这种方式无法处理没有使用循环的情况,假如只有单纯的大量php指令密集运算是无法检测到的。权衡优缺点,swoole 最终使用 php `tick` 这种方式实现。 138 | 139 | -------------------------------------------------------------------------------- /将Swoole静态编译内嵌到php中.md: -------------------------------------------------------------------------------- 1 | #### 使用systemd管理swoole服务器 2 | 3 | `swoole-1.9.15`+ 支持了静态编译,可以将 `Swoole` 内嵌到 php 中。 4 | 5 | ###### 准备 6 | 7 | 1. 需要将 `swoole-src` 和 `php-src` 两份源码 8 | 2. 将`swoole` 源码放置到 `php-src/ext` 目录中 9 | 3. 清理 `swoole` 源码目录,使用 `phpize --clean` 和 `./clear.sh` 10 | 11 | ###### 配置 12 | 13 | - 目前 `swoole` 只支持 `cli` 静态内联,必须关闭其他 `SAPI` 包括 `php-fpm` ,`CGI`, `phpdbg` 等 14 | - 需要增加 `--enable-swoole-static` 和 `--with-swoole` 两项编译配置参数 15 | 16 | ###### 构建 17 | 18 | ```shell 19 | cd php-src/ 20 | ./buildconf --force 21 | /configure --disable-all --enable-swoole-static --with-zlib --with-swoole --enable0-cli --disable-cgi --disable-phpdbg 22 | make -j 23 | ``` 24 | 25 | ###### 使用 26 | 27 | 编译完成,在 `sapi/cli` 目录中可以得到 `php` 可执行文件。使用 `./php --ri swoole` 查看信息 -------------------------------------------------------------------------------- /异步回调程序内存管理.md: -------------------------------------------------------------------------------- 1 | #### 异步回调程序内存管理 2 | 3 | 异步回调程序与同步阻塞程序的内存管理方式不同,异步程序是基于回调链引用计数实现内存的管理。本文会用一个最简单的实例讲解异步程序的内存管理。 4 | 5 | ###### sample 6 | 7 | ```php 8 | $serv = new Swoole\Http\Server("127.0.0.1", 9502); 9 | $serv->on('Request', function($request, $response) { 10 | $cli = new Swoole\Http\Client('127.0.0.1', 80); 11 | $cli->post('/dump.php', ['key' => 'value'], function($cli) use ($request, $response) { 12 | $response->send("

{$cli->body}

"); 13 | $cli->close(); 14 | }); 15 | }); 16 | $serv->start(); 17 | ``` 18 | 19 | ###### onRequest 20 | 21 | - 请求到来时,会触发 `onRequest` 回调函数,可以得到 `$request` 和 `$response` 对象 22 | - 在 `onRequest` 回调中,创建了一个 `Http\Client`, 并发起一次 `POST` 请求 23 | - 然后 `onRequest` 函数结束并返回 24 | 25 | 这时按照正常的 php 函数调用流程, `$request` 和 `$response` 对象会被销毁。 26 | 27 | 但在上述程序中,`$request` 和 `$response` 对象被使用了 `use` 语法,绑定到了匿名函数上,因此这2个对象的引用计数会被加一,`onRequest` 函数返回时就不会真正销毁这两个对象了。 28 | 29 | ###### 引用链依赖 30 | 31 | ```php 32 | request/response => post(Closure回调函数) => $cli(HttpClient对象) => post($cli->connect) 33 | ``` 34 | 35 | `$cli` 对象,是在 `onRequest` 函数创建的局部变量,按照正常逻辑 `$cli` 对象在 `onRequest` 函数退出时也应该被销毁。但 `Swoole` 底层有一个特殊的逻辑,所有异步客户端对象在发起连接时会自动增加一次引用计数,在连接关闭时减少一次引用计数,因此 `$cli` 对象也不会销毁。`POST` 请求中的匿名函数对象也不会销毁。 36 | 37 | ###### Http响应 38 | 39 | 1.创建的 `$cli` 对象,接收到来自服务器端的响应,或连接超时,响应超时,这时会回调指定的匿名函数,调用 `send` 向客户端发送响应 40 | 41 | 2.回调函数中调用了 `$cli->close` 这时切断连接,`$cli` 的引用计数减一。这时匿名函数退出底层会自动销毁 `$cli`, `$request`, `$response` 3个对象 42 | 43 | ###### 引用链解除 44 | 45 | ```php 46 | cli => close => Closure销毁 => cli销毁 => request/response 销毁 47 | ``` 48 | 49 | ###### 多层嵌套 50 | 51 | 如果 `Http\Client` 的回调函数中调用了其他的异步客户端,如 `Swoole\Redis`,对象会继续传读引用,形成一个异步调用链。当调用链的最后一个对象销毁时会向着调用链头部逐个递减引用计数,最终销毁对象。 52 | 53 | ```php 54 | $serv = new Swoole\Http\Server("127.0.0.1", 9502); 55 | $serv->on('Request', function($request, $response) { 56 | $cli = new Swoole\Http\Client('127.0.0.1', 80); 57 | // 发起连接,$cli 引用计数增加 58 | $cli->post('/dump.php', ["key" => "value"], function($cli) use ($request, $response) { 59 | $redis = new Swoole\Redis; 60 | // 发起连接,$redis 引用计数增加 61 | $redis->connect('127.0.0.1', 6379, function($cli) use ($request, $response, $cli) { 62 | $redis->get('test_key', function($redis, $result) use ($request, $response) { 63 | $response->end("

{$result}

"); 64 | $cli->close(); // $cli 引用计数减少 65 | $redis->close(); // $redis 引用计数减少 66 | }); 67 | }); 68 | }); 69 | }); 70 | $serv->start(); 71 | ``` 72 | 73 | 1.这里 `$request` 和 `$request` 对象被 POST 匿名函数,`Redis->connect` 匿名函数,`Redis->get` 匿名函数引用,因此需要等到这3个函数执行后,引用计数减少为 0,才会真正地销毁。 74 | 75 | 2.`$cli` 和 `$redis` 对象在发起 TCP 连接时,会被 `Swoole` 底层增加引用计数,只有 `$cli->close()` 和 `$redis->close` 被调用,或远程服务器关闭连接,触发 `$cli->onClose` 和 `$redis->onClose`,`$cli` 和 `$redis` 这2个对象的,引用计数才会减少,函数退出时会销毁 76 | 77 | 3.POST 匿名函数,`Redis->connect` 匿名函数,`Redis->get` 匿名函数,3个对象依附于 `$cli` 和 `$redis` 对象,当 `$cli` 和 `$redis` 对象销毁时,这3个对象也会被销毁 78 | 79 | 4.POST 匿名函数,`Redis->connect` 匿名函数,`Redis->get` 匿名函数,匿名函数销毁时会把通过 `use` 语法引用的 `$request`, `$response` 对象一起销毁 -------------------------------------------------------------------------------- /日志等级控制.md: -------------------------------------------------------------------------------- 1 | #### 日志等级控制 2 | 3 | 在最新版本中底层已规范了日志等级控制相关的特性,并定义了相关常量。可使用下列方法设置 `log_level` 和 `trace_flags` 选项: 4 | 5 | - `Swoole\Server->set` 方法 6 | - `Swoole\Coroutine::set` 静态方法 7 | - `Swoole\Async::set` 静态方法 8 | 9 | ```php 10 | $serv->set([ 11 | 'log_level' => SWOOLE_LOG_TRACE, 12 | 'trace_flags' => SWOOLE_TRACE_ALL, 13 | ]); 14 | ``` 15 | 16 | ###### 日志等级 17 | 18 | 可通过设置 `log_level` 控制日志等级,底层支持 6 种错误日志等级: 19 | 20 | - `SWOOLE_LOG_DEBUG` :调试日志,仅作内核开发调试使用 21 | - `SWOOLE_LOG_TRACE` :跟踪日志,可用于跟踪系统问题,调试日志是经过精心设置的,会携带关键性信息 22 | - `SWOOLE_LOG_INFO` :普通信息,仅作为信息展示 23 | - `SWOOLE_LOG_NOTICE` :提示信息,系统可能存在某些行为,如重启,关闭 24 | - `SWOOLE_LOG_WARNING` :警告信息,系统可能存在某些问题 25 | - `SWOOLE_LOG_ERROR` :错误信息,系统发生了某些关键性的错误,需要及时解决 26 | 27 | 其中 `SWOOLE_LOG_DEBUG` 和 `SWOOLE_LOG_TRACE` 两种日志,必须在编译 `swoole` 扩展时使用 `--enable-swoole-debug` 或 `--enable-trace-log` 后才可以使用。正常版本中即使设置了 `log_level=SWOOLE_LOG_TRACE` 也是没法打印此类日志的。 28 | 29 | ###### 跟踪标签 30 | 31 | 线上运行的服务,随时都有大量请求在处理,底层抛出的日志数量非常巨大。可使用 `trace_flags` 设置跟踪日志的标签,仅打印部分跟踪日志。`trace_flags` 支持使用 `|` 或操作符设置多个跟踪项。 32 | 33 | ```php 34 | $serv->set([ 35 | 'log_level' => SWOOLE_LOG_TRACE, 36 | 'trace_flags' => SWOOLE_TRACE_SERVER | SWOOLE_TRACE_HTTP2, 37 | ]); 38 | ``` 39 | 40 | 底层支持以下跟踪项,可使用 `SWOOLE_TRACE_ALL` 表示跟踪所有项目: 41 | 42 | - `SWOOLE_TRACE_SERVER` 43 | - `SWOOLE_TRACE_CLIENT` 44 | - `SWOOLE_TRACE_BUFFER` 45 | - `SWOOLE_TRACE_CONN` 46 | - `SWOOLE_TRACE_EVENT` 47 | - `SWOOLE_TRACE_WORKER` 48 | - `SWOOLE_TRACE_REACTOR` 49 | - `SWOOLE_TRACE_PHP` 50 | - `SWOOLE_TRACE_HTTP2` 51 | - `SWOOLE_TRACE_EOF_PROTOCOL` 52 | - `SWOOLE_TRACE_LENGTH_PROTOCOL` 53 | - `SWOOLE_TRACE_CLOSE` 54 | - `SWOOLE_TRACE_HTTP_CLIENT` 55 | - `SWOOLE_TRACE_COROUTINE` 56 | - `SWOOLE_TRACE_REDIS_CLIENT` 57 | - `SWOOLE_TRACE_MYSQL_CLIENT` 58 | - `SWOOLE_TRACE_AIO` 59 | - `SWOOLE_TRACE_ALL` 60 | 61 | -------------------------------------------------------------------------------- /详解Swoole协程实现原理.md: -------------------------------------------------------------------------------- 1 | # 详解Swoole协程的实现 2 | 3 | ###### 写在最前 4 | 5 | Swoole 协程的诞生经历了几个大的阶段,我们要在前进的道路上时常总结和回顾自己的发展历程。 6 | 7 | ###### 什么是协程? 8 | 9 | 协程的概念早就出现了,摘自 `wiki`: 10 | 11 | > According to Donald Knuth, the term coroutine was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program. The first published explanation of the coroutine appeared later, in 1963. 12 | 13 | 协程要比 C 语言历史更长,究其概念,协程是一种子程序,可以通过 `yield` 的方式转移程序控制权,协程之间不是调用者与被调用者的关系,而是彼此对称,平等的。协程完全由用户态程序控制,所以也被称为用户态的线程。协程由用户以非抢占式的方式调度,而不是操作系统。正因为如此,没有系统调度和上下文切换的开销,协程实现了**轻量,高效,快速**等特点。(大部分为非抢占式,但是比如 `go` 在 1.4 版本也加入了抢占式调度,其中一个协程发生死循环,不至于其他协程被“饿死”。需要在必要的时刻让出CPU,Swoole 后续也会增加这个特性)。 14 | 15 | 协程开始流行很大一部分原因归功于 `go` 语言的流行,很多人开始使用它。目前支持协程的语言由很多,`go`,`lua`,`python`,`C#`,`Javascript`。我们可以用很短的时间用 C/C++ 描述出协程的模型,当然 php 也有自己的协程实现,也就是生成器,在此不探讨这个点。 16 | 17 | ###### Swoole 1.x 版本 18 | 19 | Swoole 最终设计目的是要做**高性能网络通讯引擎**, Swoole 1.x 的编码主要是异步回调的方式,虽然性能很高效,但很多开发者会发现,随着项目工程的复杂度上升,用异步回调方式写业务逻辑是和我们人的正常思维方式不那么符合的。尤其是回调中嵌套了多层子回调时,不仅维护成本指数级上升,而且犯错的几率也在加速上升。 20 | 21 | 更符合人类思维习惯的方式是:同步的代码,运行出异步非阻塞的效果。所以 Swoole 很早就开始研究如何达到这个目的。 22 | 23 | 最初的协程版本是基于PHP生成器Generators\Yield的方式实现的,可以参考PHP大神Nikita的早期博客的关于[协程](https://nikic.github.io/2012/12/22/Cooperative-multitasking-using-coroutines-in-PHP.html)介绍。PHP和Swoole的事件驱动的结合可以参考腾讯出团队开源的[TSF](https://github.com/Tencent/tsf)框架,我们也在很多生产项目中使用了该框架,确实让大家感受到了,以同步编程的方式写异步代码的快感,然而,现实总是很残酷,这种方式有几个致命的缺点: 24 | 25 | - 所有主动让出的逻辑都需要yield关键字。这会给程序员带来极大的概率犯错,导致大家对协程的理解转移到了对Generators语法的原理的理解。 26 | - 由于语法无法兼容老的项目,改造老的项目工程复杂度巨大,成本太高。 27 | 28 | 这样使得无论新老项目,使用都无法得心应手。 29 | 30 | ###### Swoole2.x 31 | 32 |    2.x之后的协程都是基于内核原生的协程,无需yield关键字。2.0的版本是一个非常重要的里程碑,实现了php的栈管理,深入zend内核在协程创建,切换以及结束的时候操作PHP栈。在Swoole的文档中也介绍了很多关于每个版本实现的细节,我们这篇文章只对每个版本的协程驱动技术做简单介绍。**原生协程都有对php栈的管理,后续我们会单独拿一片文章来深入分析PHP栈的管理和切换。** 33 | 34 |    2.x主要使用了setjmp/longjmp的方式实现协程,很多C项目主要采用这种方式实现try-catch-finally,大家也可以参考Zend内核的用法。setjmp的首次调用返回值是0,longjmp跳转时,setjmp的返回值是传给longjmp的value。 setjmp/longjmp由于只有控制流跳转的能力。虽然可以还原PC和栈指针,但是无法还原栈帧,因此会出现很多问题。比如longjmp的时候,setjmp的作用域已经退出,当时的栈帧已经销毁。这时就会出现未定义行为。假设有这样一个调用链: 35 | 36 | > func0() -> func1() -> ... -> funcN() 37 | 38 | 只有在func{i}()中setjmp,在func{i+k}()中longjmp的情况下,程序的行为才是可预期的。 39 | 40 | ###### Swoole3.x 41 | 42 | 3.x是生命周期很短的一个版本,主要借鉴了[fiber-ext](https://github.com/fiberphp/fiber-ext)项目,使用了PHP7的VM interrupts机制,该机制可以在vm中设置标记位,在执行一些指令的时候(例如:跳转和函数调用等)检查标记位,如果命中就可以执行相应的hook函数来切换vm的栈,进而实现协程。 43 | 44 | \####Swoole4.x 4.x协程我们放在最后。 45 | 46 | 协程之旅前篇结束,下一篇文章我们将深入Zend分析Swoole原生协程PHP部分的实现。 47 | 48 | ###### Swoole中php部分 49 | 50 | 本篇我们开始深入PHP来分析Swoole协程的PHP部分。 51 | 52 |  先从一个协程最简单的例子入手: 53 | 54 | ```php 55 | literals + 偏移量读取 116 | /* ... */ 117 | }; 118 | ``` 119 | 120 | 我们已经熟知php的函数内部有自己的单独的作用域,这归功于每个zend_op_array包含有当前作用域下所有的堆栈信息,函数之间的调用关系也是基于zend_op_array的切换来实现。 121 | 122 | - PHP栈帧 123 | 124 | PHP执行需要的所有状态都保存在一个个通过链表结构关联的VM栈里,每个栈默认会初始化为256K,Swoole可以单独定制这个栈的大小(协程默认为8k),当栈容量不足的时候,会自动扩容,仍然以链表的关系关联每个栈。在每次函数调用的时候,都会在VM Stack空间上申请一块新的栈帧来容纳当前作用域执行所需。栈帧结构的内存布局如下所示: 125 | 126 | ```shell 127 | +----------------------------------------+ 128 | | zend_execute_data | 129 | +----------------------------------------+ 130 | | VAR[0] = ARG[1] | arguments 131 | | ... | 132 | | VAR[num_args-1] = ARG[N] | 133 | | VAR[num_args] = CV[num_args] | remaining CVs 134 | | ... | 135 | | VAR[last_var-1] = CV[last_var-1] | 136 | | VAR[last_var] = TMP[0] | TMP/VARs 137 | | ... | 138 | | VAR[last_var+T-1] = TMP[T] | 139 | | ARG[N+1] (extra_args) | extra arguments 140 | | ... | 141 | +----------------------------------------+ 142 | ``` 143 | 144 | zend_execute_data 最后要介绍的一个结构,也是最重要的一个。 145 | 146 | ```c 147 | struct _zend_execute_data { 148 | const zend_op *opline;//当前执行的opcode,初始化会zend_op_array起始 149 | zend_execute_data *call;// 150 | zval *return_value;//返回值 151 | zend_function *func;//当前执行的函数(非函数调用时为空) 152 | zval This;/* this + call_info + num_args */ 153 | zend_class_entry *called_scope;//当前call的类 154 | zend_execute_data *prev_execute_data; 155 | zend_array *symbol_table;//全局变量符号表 156 | void **run_time_cache; /* cache op_array->run_time_cache */ 157 | zval *literals; /* cache op_array->literals */ 158 | }; 159 | ``` 160 | 161 | `prev_execute_data` 表示前一个栈帧结构,当前栈执行结束以后,会把当前执行指针(类比PC)指向这个栈帧。 PHP的执行流程正是将很多个zend_op_array依次装载在栈帧上执行。这个过程可以分解为以下几个步骤: 162 | 163 | - **1:** 为当前需要执行的op_array从vm stack上申请当前栈帧,结构如上。初始化全局变量符号表,将全局指针EG(current_execute_data)指向新分配的zend_execute_data栈帧,EX(opline)指向op_array起始位置。 164 | 165 | - 2: 166 | 167 | 168 | 169 | 从 170 | 171 | ``` 172 | EX(opline) 173 | ``` 174 | 175 | 开始调用各opcode的C处理handler(即_zend_op.handler),每执行完一条opcode将 176 | 177 | ``` 178 | EX(opline)++ 179 | ``` 180 | 181 | 继续执行下一条,直到执行完全部opcode,遇到函数或者类成员方法调用: 182 | 183 | - 从`EG(function_table)`中根据function_name取出此function对应的zend_op_array,然后重复步骤1,将EG(current_execute_data)赋值给新结构的`prev_execute_data`,再将EG(current_execute_data)指向新的zend_execute_data栈帧,然后开始执行新栈帧,从位置`zend_execute_data.opline`开始执行,函数执行完将EG(current_execute_data)重新指向`EX(prev_execute_data)`,释放分配的运行栈帧,执行位置回到函数执行结束的下一条opline。 184 | 185 | - **3:** 全部opcodes执行完成后将1分配的栈帧释放,执行阶段结束 186 | 187 | ------ 188 | 189 | 有了以上php执行的细节,我们回到最初的例子,可以发现协程需要做的是,**改变原本php的运行方式,不是在函数运行结束切换栈帧,而是在函数执行当前op_array中间任意时候(swoole内部控制为遇到IO等待),可以灵活切换到其他栈帧。**接下来我们将Zend VM和Swoole结合分析,如何创建协程栈,遇到IO切换,IO完成后栈恢复,以及协程退出时栈帧的销毁等细节。 先介绍协程PHP部分的主要结构 190 | 191 | - 协程 php_coro_task 192 | 193 | ```c 194 | struct php_coro_task 195 | { 196 | /* 只列出关键结构*/ 197 | /*...*/ 198 | zval *vm_stack_top;//栈顶 199 | zval *vm_stack_end;//栈底 200 | zend_vm_stack vm_stack;//当前协程栈指针 201 | /*...*/ 202 | zend_execute_data *execute_data;//当前协程栈帧 203 | /*...*/ 204 | php_coro_task *origin_task;//上一个协程栈帧,类比prev_execute_data的作用 205 | }; 206 | ``` 207 | 208 | 协程切换主要是针对当前栈执行发生中断时对上下文保存,和恢复。结合上面VM的执行流程我们可以知道上面几个字段的作用。 209 | 210 | - `execute_data` 栈帧指针需要保存和恢复是毋容置疑的 211 | - `vm_stack*` 系列是什么作用呢?原因是PHP是动态语言,我们上面分析到,每次有新函数进入执行和退出的时候,都需要在全局stack上创建和释放栈帧,所以需要正确保存和恢复对应的全局栈指针,才能保障每个协程栈帧得到释放,不会导致内存泄漏的问题。(当以debug模式编译PHP后,每次释放都会检查当全局栈是否合法) 212 | - `origin_task` 是当前协程执行结束后需要自动执行的前一个栈帧。 213 | 214 | 主要涉及到的操作有: 215 | 216 | - 协程的创建 `create`,在全局stack上为协程申请栈帧。 217 | 218 | - 协程的创建是创建一个闭包函数,将函数(可以理解为需要执行的op_array)当作一个参数传入Swoole的内建函数go(); 219 | 220 | - 协程让出,`yield`,遇到IO,保存当前栈帧的上下文信息 221 | 222 | - 协程的恢复,`resume`,IO完成,恢复需要执行的协程上下文信息到yield让出前的状态 223 | 224 | - 协程的退出,`exit`,协程op_array全部执行完毕,释放栈帧和swoole协程的相关数据。 225 | 226 | 经过上面的介绍大家应该对Swoole协程在运行过程中可以在函数内部实现跳转有一个大概了解,回到最初我们例子结合上面php执行细节,我们能够知道,该例子会生成3个op_array,分别为 主脚本,协程1,协程2。我们可以利用一些工具打印出opcodes来直观的观察一下。通常我们会使用下面两个工具 227 | 228 | ``` 229 | //Opcache, version >= PHP 7.1 230 | php -d opcache.opt_debug_level=0x10000 test.php 231 | 232 | //vld, 第三方扩展 233 | php -d vld.active=1 test.php 234 | ``` 235 | 236 | 我们用opcache来观察没有被优化前的opcodes,我们可以很清晰的看到这三组op_array的详细信息。 237 | 238 | ```ini 239 | php -dopcache.enable_cli=1 -d opcache.opt_debug_level=0x10000 test.php 240 | $_main: ; (lines=11, args=0, vars=0, tmps=4) 241 | ; (before optimizer) 242 | ; /path-to/test.php:2-6 243 | L0 (2): INIT_FCALL 1 96 string("go") 244 | L1 (2): T0 = DECLARE_LAMBDA_FUNCTION string("") 245 | L2 (6): SEND_VAL T0 1 246 | L3 (6): DO_ICALL 247 | L4 (7): ECHO string("main flag 248 | ") 249 | L5 (8): INIT_FCALL 1 96 string("go") 250 | L6 (8): T2 = DECLARE_LAMBDA_FUNCTION string("") 251 | L7 (12): SEND_VAL T2 1 252 | L8 (12): DO_ICALL 253 | L9 (13): ECHO string("main end 254 | ") 255 | L10 (14): RETURN int(1) 256 | 257 | {closure}: ; (lines=6, args=0, vars=0, tmps=1) 258 | ; (before optimizer) 259 | ; /path-to/test.php:2-6 260 | L0 (9): ECHO string("coro 2 start 261 | ") 262 | L1 (10): INIT_STATIC_METHOD_CALL 1 string("co") string("sleep") 263 | L2 (10): SEND_VAL_EX int(1) 1 264 | L3 (10): DO_FCALL//yiled from 当前op_array [coro 1] ; resume 265 | L4 (11): ECHO string("coro 2 exit 266 | ") 267 | L5 (12): RETURN null 268 | 269 | {closure}: ; (lines=6, args=0, vars=0, tmps=1) 270 | ; (before optimizer) 271 | ; /path-to/test.php:2-6 272 | L0 (3): ECHO string("coro 1 start 273 | ") 274 | L1 (4): INIT_STATIC_METHOD_CALL 1 string("co") string("sleep") 275 | L2 (4): SEND_VAL_EX int(1) 1 276 | L3 (4): DO_FCALL//yiled from 当前op_array [coro 2];resume 277 | L4 (5): ECHO string("coro 1 exit 278 | ") 279 | L5 (6): RETURN null 280 | coro 1 start 281 | main flag 282 | coro 2 start 283 | main end 284 | coro 1 exit 285 | coro 2 exit 286 | ``` 287 | 288 | Swoole在执行`co::sleep()`的时候让出当前控制权,跳转到下一个op_array,结合以上注释,也就是在`DO_FCALL`的时候分别让出和恢复协程执行栈,达到原生协程控制流跳转的目的。 289 | 290 | 我们分析下 `INIT_FCALL` `DO_FCALL`指令在内核中如何执行。以便于更好理解函数调用栈切换的关系。 291 | 292 | > VM内部指令会根据当前的操作数返回值等特殊化为一个c函数,我们这个例子中 有以下对应关系 293 | 294 | > `INIT_FCALL` => ZEND_INIT_FCALL_SPEC_CONST_HANDLER 295 | 296 | > `DO_FCALL` => ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER 297 | 298 | ```c 299 | ZEND_INIT_FCALL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 300 | { 301 | USE_OPLINE 302 | 303 | zval *fname = EX_CONSTANT(opline->op2); 304 | zval *func; 305 | zend_function *fbc; 306 | zend_execute_data *call; 307 | 308 | fbc = CACHED_PTR(Z_CACHE_SLOT_P(fname)); 309 | if (UNEXPECTED(fbc == NULL)) { 310 | func = zend_hash_find(EG(function_table), Z_STR_P(fname)); 311 | if (UNEXPECTED(func == NULL)) { 312 | SAVE_OPLINE(); 313 | zend_throw_error(NULL, "Call to undefined function %s()", Z_STRVAL_P(fname)); 314 | HANDLE_EXCEPTION(); 315 | } 316 | fbc = Z_FUNC_P(func); 317 | CACHE_PTR(Z_CACHE_SLOT_P(fname), fbc); 318 | if (EXPECTED(fbc->type == ZEND_USER_FUNCTION) && UNEXPECTED(!fbc->op_array.run_time_cache)) { 319 | init_func_run_time_cache(&fbc->op_array); 320 | } 321 | } 322 | 323 | call = zend_vm_stack_push_call_frame_ex( 324 | opline->op1.num, ZEND_CALL_NESTED_FUNCTION, 325 | fbc, opline->extended_value, NULL, NULL); //从全局stack上申请当前函数的执行栈 326 | call->prev_execute_data = EX(call); //将正在执行的栈赋值给将要执行函数栈的prev_execute_data,函数执行结束后恢复到此处 327 | EX(call) = call; //将函数栈赋值到全局执行栈,即将要执行的函数栈 328 | ZEND_VM_NEXT_OPCODE(); 329 | } 330 | ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER(ZEND_OPCODE_HANDLER_ARGS) 331 | { 332 | USE_OPLINE 333 | zend_execute_data *call = EX(call);//获取到执行栈 334 | zend_function *fbc = call->func;//当前函数 335 | zend_object *object; 336 | zval *ret; 337 | 338 | SAVE_OPLINE();//有全局寄存器的时候 ((execute_data)->opline) = opline 339 | EX(call) = call->prev_execute_data;//当前执行栈execute_data->call = EX(call)->prev_execute_data 函数执行结束后恢复到被调函数 340 | /*...*/ 341 | LOAD_OPLINE(); 342 | 343 | if (EXPECTED(fbc->type == ZEND_USER_FUNCTION)) { 344 | ret = NULL; 345 | if (0) { 346 | ret = EX_VAR(opline->result.var); 347 | ZVAL_NULL(ret); 348 | } 349 | 350 | call->prev_execute_data = execute_data; 351 | i_init_func_execute_data(call, &fbc->op_array, ret); 352 | 353 | if (EXPECTED(zend_execute_ex == execute_ex)) { 354 | ZEND_VM_ENTER(); 355 | } else { 356 | ZEND_ADD_CALL_FLAG(call, ZEND_CALL_TOP); 357 | zend_execute_ex(call); 358 | } 359 | } else if (EXPECTED(fbc->type < ZEND_USER_FUNCTION)) { 360 | zval retval; 361 | 362 | call->prev_execute_data = execute_data; 363 | EG(current_execute_data) = call; 364 | /*...*/ 365 | ret = 0 ? EX_VAR(opline->result.var) : &retval; 366 | ZVAL_NULL(ret); 367 | 368 | if (!zend_execute_internal) { 369 | /* saves one function call if zend_execute_internal is not used */ 370 | fbc->internal_function.handler(call, ret); 371 | } else { 372 | zend_execute_internal(call, ret); 373 | } 374 | 375 | EG(current_execute_data) = execute_data; 376 | zend_vm_stack_free_args(call);//释放局部变量 377 | 378 | if (!0) { 379 | zval_ptr_dtor(ret); 380 | } 381 | 382 | } else { /* ZEND_OVERLOADED_FUNCTION */ 383 | /*...*/ 384 | } 385 | 386 | fcall_end: 387 | /*...*/ 388 | } 389 | zend_vm_stack_free_call_frame(call);//释放栈 390 | if (UNEXPECTED(EG(exception) != NULL)) { 391 | zend_rethrow_exception(execute_data); 392 | HANDLE_EXCEPTION(); 393 | } 394 | ZEND_VM_SET_OPCODE(opline + 1); 395 | ZEND_VM_CONTINUE(); 396 | } 397 | ``` 398 | 399 | Swoole在PHP层可以按照以上方式来进行切换,至于执行过程中有IO等待发生,需要额外的技术来驱动,我们后续的文章将会介绍每个版本的驱动技术结合Swoole原有的事件模型,讲述Swoole协程如何进化到现在。 400 | 401 | ###### 原理详解 402 | 403 | 本篇我们开始深入PHP来分析Swoole协程的驱动部分,也就是C栈部分。 404 | 405 |  由于我们系统存在C栈和PHP栈两部分,约定名字: 406 | 407 | - C协程 C栈管理部分, 408 | - PHP协程 PHP栈管理部分。 409 | 410 |  增加C栈是4.x协程最重要也是最关键的部分,之前的版本种种无法完美支持PHP语法也是由于没有保存C栈信息。接下来我们将展开分析,C栈切换的支持最初我们是使用腾讯出品[libco](https://github.com/Tencent/libco)来支持,但通过压测会有内存读写错误而且开源社区很不活跃,有问题无法得到及时的反馈处理,所以,我们剥离的c++ boost库的汇编部分,现在的协程C栈的驱动就是在这个基础上做的。 411 | 412 |  先来一张简单的系统架构图。 413 | [![Swoole4.x架构图](https://camo.githubusercontent.com/300db6deee465a98a7f4d7a617d0cfa0f32831d5/68747470733a2f2f77696b692e73776f6f6c652e636f6d2f7374617469632f75706c6f6164732f77696b692f3230313930312f32392f3432313433303930303735302e706e67)](https://camo.githubusercontent.com/300db6deee465a98a7f4d7a617d0cfa0f32831d5/68747470733a2f2f77696b692e73776f6f6c652e636f6d2f7374617469632f75706c6f6164732f77696b692f3230313930312f32392f3432313433303930303735302e706e67)可以发现,Swoole的角色是粘合在系统API和php ZendVM,给PHPer用户深度接口编写高性能的代码;不仅如此,也支持给C++/C用户开发使用,详细请参考文档[C++开发者如何使用Swoole](https://wiki.swoole.com/wiki/page/633.html)。 C部分的代码主要分为几个部分 414 | 415 | 1. 汇编ASM驱动 416 | 2. Conext 上下文封装 417 | 3. Socket协程套接字封装 418 | 4. PHP Stream系封装,可以无缝协程化PHP相关函数 419 | 5. ZendVM结合层 420 | 421 | Swoole底层系统层次更加分明,Socket将作为整个网络驱动的基石,原来的版本中,每个客户端都要基于异步回调的方式维护上下文,所以4.x版本较之前版本比较,无论是从项目的复杂程度,还是系统的稳定性,可以说都有一个质的飞跃。 代码目录层级 422 | 423 | ``` 424 | $ tree swoole-src/src/coroutine/ 425 | swoole-src/src/coroutine/ 426 | ├── base.cc //C协程API,可回调PHP协程API 427 | ├── channel.cc //channel 428 | ├── context.cc //协程实现 基于ASM make_fcontext jump_fcontext 429 | ├── hook.cc //hook 430 | └── socket.cc //网络操作协程封装 431 | swoole-src/swoole_coroutine.cc //ZendVM相关封装,PHP协程API 432 | ``` 433 | 434 | 我们从用户层到系统至上而下有 PHP协程API, C协程API, ASM协程API。其中Socket层是兼容系统API的网络封装。我们至下而上进行分析。 ASM x86-64架构为例,共有16个64位通用寄存器,各寄存器及用途如下 435 | 436 | - %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中。在imul 指令中,两个64位的乘法最多会产生128位的结果,需要 %rax 与 %rdx 共同存储乘法结果,在div 指令中被除数是128 位的,同样需要%rax 与 %rdx 共同存储被除数。 437 | - %rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。 438 | - %rbp 是栈帧指针,用于标识当前栈帧的起始位置 439 | - %rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数 440 | - %rbx,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则 441 | - %r10,%r11 用作数据存储,遵循调用者使用规则 442 | 443 | 也就是说在进入汇编函数后,第一个参数值已经放到了 %rdi 寄存器中,第二个参数值已经放到了 %rsi 寄存器中,并且栈指针 %rsp 指向的位置即栈顶中存储的是父函数的返回地址 x86-64使用swoole-src/thirdparty/boost/asm/make_x86_64_sysv_elf_gas.S 444 | 445 | ``` 446 | //在当前栈顶创建一个上下文,用来执行执行第三个参数函数fn,返回初始化完成后的执行环境上下文 447 | fcontext_t make_fcontext(void *sp, size_t size, void (*fn)(intptr_t)); 448 | make_fcontext: 449 | /* first arg of make_fcontext() == top of context-stack */ 450 | movq %rdi, %rax 451 | 452 | /* shift address in RAX to lower 16 byte boundary */ 453 | andq $-16, %rax 454 | 455 | /* reserve space for context-data on context-stack */ 456 | /* size for fc_mxcsr .. RIP + return-address for context-function */ 457 | /* on context-function entry: (RSP -0x8) % 16 == 0 */ 458 | leaq -0x48(%rax), %rax 459 | 460 | /* third arg of make_fcontext() == address of context-function */ 461 | movq %rdx, 0x38(%rax) 462 | 463 | /* save MMX control- and status-word */ 464 | stmxcsr (%rax) 465 | /* save x87 control-word */ 466 | fnstcw 0x4(%rax) 467 | 468 | /* compute abs address of label finish */ 469 | leaq finish(%rip), %rcx 470 | /* save address of finish as return-address for context-function */ 471 | /* will be entered after context-function returns */ 472 | movq %rcx, 0x40(%rax) 473 | 474 | ret /* return pointer to context-data * 返回rax指向的栈底指针,作为context返回/ 475 | //将当前上下文(包括栈指针,PC程序计数器以及寄存器)保存至*ofc,从nfc恢复上下文并开始执行。 476 | intptr_t jump_fcontext(fcontext_t *ofc, fcontext_t nfc, intptr_t vp, bool preserve_fpu = false); 477 | 478 | jump_fcontext: 479 | //保存当前寄存器,压栈 480 | pushq %rbp /* save RBP */ 481 | pushq %rbx /* save RBX */ 482 | pushq %r15 /* save R15 */ 483 | pushq %r14 /* save R14 */ 484 | pushq %r13 /* save R13 */ 485 | pushq %r12 /* save R12 */ 486 | 487 | /* prepare stack for FPU */ 488 | leaq -0x8(%rsp), %rsp 489 | 490 | /* test for flag preserve_fpu */ 491 | cmp $0, %rcx 492 | je 1f 493 | 494 | /* save MMX control- and status-word */ 495 | stmxcsr (%rsp) 496 | /* save x87 control-word */ 497 | fnstcw 0x4(%rsp) 498 | 499 | 1: 500 | /* store RSP (pointing to context-data) in RDI 保存当前栈顶到rdi 即:将当前栈顶指针保存到第一个参数%rdi ofc中*/ 501 | movq %rsp, (%rdi) 502 | 503 | /* restore RSP (pointing to context-data) from RSI 修改栈顶地址,为新协程的地址 ,rsi为第二个参数地址 */ 504 | movq %rsi, %rsp 505 | 506 | /* test for flag preserve_fpu */ 507 | cmp $0, %rcx 508 | je 2f 509 | 510 | /* restore MMX control- and status-word */ 511 | ldmxcsr (%rsp) 512 | /* restore x87 control-word */ 513 | fldcw 0x4(%rsp) 514 | 515 | 2: 516 | /* prepare stack for FPU */ 517 | leaq 0x8(%rsp), %rsp 518 | // 寄存器恢复 519 | popq %r12 /* restrore R12 */ 520 | popq %r13 /* restrore R13 */ 521 | popq %r14 /* restrore R14 */ 522 | popq %r15 /* restrore R15 */ 523 | popq %rbx /* restrore RBX */ 524 | popq %rbp /* restrore RBP */ 525 | 526 | /* restore return-address 将返回地址放到 r8 寄存器中 */ 527 | popq %r8 528 | 529 | /* use third arg as return-value after jump*/ 530 | movq %rdx, %rax 531 | /* use third arg as first arg in context function */ 532 | movq %rdx, %rdi 533 | 534 | /* indirect jump to context */ 535 | jmp *%r8 536 | ``` 537 | 538 | context管理位于context.cc,是对ASM的封装,提供两个API 539 | 540 | ```c 541 | bool Context::SwapIn() 542 | bool Context::SwapOut() 543 | ``` 544 | 545 | 最终的协程API位于base.cc,最主要的API为 546 | 547 | ```c 548 | //创建一个c栈协程,并提供一个执行入口函数,并进入函数开始执行上下文 549 | //例如PHP栈的入口函数Coroutine::create(PHPCoroutine::create_func, (void*) &php_coro_args); 550 | long Coroutine::create(coroutine_func_t fn, void* args = nullptr); 551 | //从当前上下文中切出,并且调用钩子函数 例如php栈切换函数 void PHPCoroutine::on_yield(void *arg) 552 | void Coroutine::yield() 553 | //从当前上下文中切入,并且调用钩子函数 例如php栈切换函数 void PHPCoroutine::on_resume(void *arg) 554 | void Coroutine::resume() 555 | //C协程执行结束,并且调用钩子函数 例如php栈清理 void PHPCoroutine::on_close(void *arg) 556 | void Coroutine::close() 557 | ``` 558 | 559 | 接下来是ZendVM的粘合层 位于swoole-src/swoole_coroutine.cc 560 | 561 | ```c 562 | PHPCoroutine 供C协程或者底层接口调用 563 | //PHP协程创建入口函数,参数为php函数 564 | static long create(zend_fcall_info_cache *fci_cache, uint32_t argc, zval *argv); 565 | //C协程创建API 566 | static void create_func(void *arg); 567 | //C协程钩子函数 上一部分base.cc的C协程会关联到以下三个钩子函数 568 | static void on_yield(void *arg); 569 | static void on_resume(void *arg); 570 | static void on_close(void *arg); 571 | //PHP栈管理 572 | static inline void vm_stack_init(void); 573 | static inline void vm_stack_destroy(void); 574 | static inline void save_vm_stack(php_coro_task *task); 575 | static inline void restore_vm_stack(php_coro_task *task); 576 | //输出缓存管理相关 577 | static inline void save_og(php_coro_task *task); 578 | static inline void restore_og(php_coro_task *task); 579 | 580 | ``` 581 | 582 | 有了以上基础部分的建设,结合我们上一篇文章中PHP内核执行栈管理,就可以从C协程驱动PHP协程,实现C栈+PHP栈的双栈的原生协程。 583 | 584 | 下一篇文章,我们将挑一个客户端实现分析socket层,把协程和Swoole事件驱动结合来分析C协程以及PHP协程在底层网络库的应用和实践。 --------------------------------------------------------------------------------