swoole 第3章 初识之异步多线程服务器 swoole 第3章 初识之异步多线程服务器

2021-06-09

一、同步和异步

我们在 IO 模型中解释过同步和异步的概念,并非是 web 开发模式下 ajax 这种异步的请求。在常见的 web 开发模式下,我们所碰到的几乎都是同步模式。

为什么这么说?无论是 fpm 还是 httpd,同一时间内一个进程只能处理一个请求,如果当前进程处于繁忙,后面的请求也只能继续等待有新的空闲进程。如果负载稍微上去了些,我们还可以调整 fpm 和 httpd 的进程数,即增加worker 进程的数量。但是,在服务器资源有限的情况下,随着 worker 进程数量的递增,系统消耗的资源也会逐步增加,直至 over。

swoole 是既支持全异步,也支持同步,同步模式我们后面结合 fpm 再说。

从 IO 模型 中,我们也可以感受到异步很强大。为什么喃?

我们举一个一名老师指导多名学生解题的场景。

同步模式下,当该老师在给某学生 A 指导题目的时候,嘴里可能一边嘟囔着“这个要这么写...”,话没说完,另一个学生 B 喊道“老师快来,我这碰到难题了,快过来指导指导”。

“等会,没看见在忙吗?”

然后学生 B 只能乖乖的等老师给A解答完之后才可以。

异步模式就不同啦,老师在给 A 指导的同时,B 又屁颠屁颠的喊着“老师老师...”,这个时候老师态度上就 360 大转弯,“来了来了”,顺便跟 A 说了“你先理解下我刚才说的,等会好了叫我”,然后呢,后面的剧情可能就是这样的

  • A 解答完毕跟老师说“谢谢”,B 喊老师

  • B 先喊老师,A 进入 B 一开始的状态,B 解答完毕跟老师说“谢谢”

  • 剧情很多,自己没事想吧

又重温了下什么是同步和异步的概念,禁止混淆。

二、socket编程

socket是什么?

在大部分的书本或者网络文章中,你都能找到一个解释:套接字,是属于应用层和传输层之间的抽象层。真想把发明这词的人拉出来暴打一顿,这也太抽象了。

socket即套接字,是用来与另一个进程进行跨网络通信的文件,说是“文件”,也很好理解哈,因为在 linux 中一切都可以理解为“文件”。比如客户端可以借助 socket 与服务器之间建立连接。你也可以把 socket 理解为一组函数库,它确实也就是一堆函数。

我们知道,常见的网络应用都是基于 Client-Server 模型的。即一个服务器进程和多个客户端进程组合而成,如果你还理解为是一台电脑对另一台电脑,可以回去把进程/线程再看看了。在 Client-Server 模型中,服务器管理某种资源,并且通过对它管理的资源进行操作来为客户端提供服务。

那 Client 和 Server 又如何实现通信呢?这就要利用 socket 一系列的函数实现了。

基于套接字接口的网络应用的描述,用下面这张图来理解就好。

https://file.lulublog.cn/images/3/2022/07/d8U9KNu2Zr6WW16Nk2syM826SRuZm2.png

大致可以描述为:服务器创建一个 socket,绑定 ip 和端口,在该端口处进行监听,然后通过 accept 函数阻塞。当有新的客户端连接进来时,server 接收客户端数据并处理数据,然后返回给客户端,客户端关闭连接,server 关闭该客户端,一次连接交互完成。

三、初识 server

3.1、TCP 服务

server,顾名思义,就是服务器。我们平时接触比较多的无非就是 nginx 和 apache。作为 webServer,二者都是通过监听某端口对外提供服务。

下面我们来创建一个简单的 server。TCP 服务

A、创建一个 server 对象

server 的创建,只需要绑定要监听的 ip 和端口,如果 ip 指定为 127.0.0.1,则表示客户端只能位于本机才能连接,其他计算机无法连接。0.0.0.0 则表示监听所有 IP

$mode = SWOOLE_PROCESS; //默认:多进程的方式
$sock_type = SWOOLE_SOCK_TCP; //默认:TCP
$serv = new swoole_server("127.0.0.1", 9501, $mode, $sock_type);

端口这里指定为 9501,可以通过 netstat 查看下该端口是否被占用。如果该端口被占用,可更改为其他端口,如 9502,9503 等。1024 以下的端口需要 root 权限。

B、配置

swoole 的运行模式,默认是多进程模式,这跟 fpm有点像。怎么体现多进程呢?要开启几个进程才合适呢?

这个就需要我们做一些配置了,但是并非像 fpm 直接在文件内配置,我们可以在 server 创建后,通过 $serv->set(array()) 指定配置项。当然,这个配置项也有很多,比如说我们可以指定日志文件记录具体的错误信息等等,你都可以在官网的手册上寻找有哪些配置项,我们也会在贯穿 swoole 的同时讲解一部分常用的配置项。

这里我们首要说明一下 worker 进程数的配置。

我们可以指定配置项 worker_num 等于某个正整数。这个正整数设置多少合适,即我要开多少个 worker 进程处理我们的业务逻辑才好呢?官方建议我们设置为 CPU 核数的 1-4 倍。因为我们开的进程越多,内存的占用也就更多,进程间切换也就需要耗费更多的资源。我们这里设置开启两个 worker 进程。默认该参数的值等于你机器的 CPU 核数。

$serv->set([
    "worker_num" => 2,
]);

C、事件驱动

swoole 另外一个比较吸引人的地方,就是 swoole_server 是事件驱动的。我们在使用的过程中不需要关注底层怎么样怎么样,只需要对底层相应的动作注册相应的回调,在回调函数中处理我们的业务逻辑即可。

什么意思呢?我举个例子:

你启动了一个 server,当客户端连接的时候,你不需要关心它是怎么连接的,你就单纯的注册一个 connect 函数,做一些连接后的处理即可。再比如 server 收到了 client 传递的数据,你用关心复杂的网络是怎么接受到的吗?不用,你只需要注册一个 receive 回调,处理数据就这么多。

让我们来看看几种常见的事件回调。

// 有新的客户端连接时,worker进程内会触发该回调
$serv->on("Connect", function ($serv, $fd) {
    echo "new client connected." . PHP_EOL;
});
  • 参数 $serv 是我们一开始创建的 swoole_server 对象,

  • 参数 $fd 是唯一标识,用于区分不同的客户端,同时该参数是 1-1600 万之间可以复用的整数。

我来解释下复用:假设现在客户端 1、2、3 处于连接中,客户端 4 要连接的话 $fd 就是 4,但是不巧的是客户端 3 连接不稳定,断掉了,客户端 4 连接到 server 的话,$fd 就是 3,这样看的话,实际可能远不止 1600W。那 1600W个连接够用吗?我的妈呀,你丫单个业务先做到 160W 再考虑这个问题吧...

// server接收到客户端的数据后,worker进程内触发该回调
$serv->on("Receive", function ($serv, $fd, $fromId, $data) {
    // 收到数据后发送给客户端
    $serv->send($fd, "Server". $data);
});

Receive 回调的前两个参数就不说了,刚说完。

上面说到的两个回调,都强调了是在 worker 进程内触发的。第三个参数 $fromId 指的是哪一个 reactor 线程,具体我们会在多进程模型一文中详细分析,先忽略吧。

我们看第四个参数,这个参数就是服务端接受到的数据,注意是字符串或者二进制内容哦,后面我们只谈字符串,不用怕。

注意我们在 Receive 回调内,调用了 $serv 的 send 方法,我们可以使用 send 方法,向 client 发起通知。

// 客户端断开连接或者server主动关闭连接时 worker进程内调用
$serv->on("Close", function ($serv, $fd) {
    echo "Client close." . PHP_EOL;
});

注意哦,当客户端与服务端的连接关闭的时候就会调用 close 回调,有些新手可能习惯性的会在 close 回调中继续调用 $serv->close($fd),人都关闭了才去调用这个方法,你再调用是不是想找事?

到此呢,我们基本上已经搭建到了一个高性能的 server。“我什么都没做,这就完啦?好没劲啊”

是的,非常简单,下面我们只需要调用 start 方法启动 server 即可。

// 启动server
$serv->start();

如此,便开启了一个 server 服务。

由于 swoole_server 只能运行在 CLI 模式下,所以不要试图通过浏览器进行访问,这样是无效的。不信的可以试试。

我们在命令行下面执行

php server.php

回车。

随后继续回车随便输入点什么都没有效果,感觉当前终端卡住了有木有?

我们平时执行完一个指令,执行完就结束了,但是现在的情况正好相反,当前程序一直处于执行中的状态,并没有退出终端。退出状态一般为当前终端的执行权交给了终端,即可用在终端下进行其他操作。

还记得我们第一步初始化 server 所填写的 ip 和端口吗,也就是说 server 现在正在监听 9501 端口提供服务。

当前终端暂时不动,我们新开一个终端,看看是不是这样。

netstat -an| grep 9501
tcp 0 0 127.0.0.1:9501 0.0.0.0:* LISTEN

发现本地的 9501 端口正在被监听对不对?server 启动好了能干什么呢?常见的网络编程模式都是 client-server 的,也就是说我们还需要模拟一个客户端与之交互。

关于客户端,我们可以先通过 telnet 模拟

https://file.lulublog.cn/images/3/2022/07/TPaAxOohh9h51CvCHuJCcxFpzGH15x.png

上图中上侧是开启的 server 窗口,下侧是我们用 telnet 模拟 client 的结果。

从结果中可以看出,客户端输入 xxx,服务端就会直接返回 Server xxx,这正是我们在 Receive 回调方法中调用 $serv->send 方法发送给客户端的数据。而且在 server 启动的窗口下,也有我们在 connect 回调打印的信息。

在整个过程中,swoole server 提供了类似 web 服务器的功能,监听端口,做出响应。

也可以使用网络调试助手进行简单调试。

icon_rar.gif网络调试助手.zip

此外,swoole 还提供了一套对 socket 客户端的封装,而且啊而且,这个要重点说一下,同步阻塞的 swoole_client 可以用于 php-fpm 或者 apache 环境。

swoole 的大部分模块都只能运行在 CLI 模式下,像我们刚刚在 cli 下启动的 server。但是对于面向 web 的应用怎么办?所以,swoole_client 是我们与服务端交互的一个重要方法,先笔记记下。

下面我们用 swoole_client 来模拟下客户端。TCP 客户端

新建一个 Client.php 文件。

代码如下:

$client = new swoole_client(SWOOLE_SOCK_TCP);
$client->connect("127.0.0.1", 9501) || exit("connect failed. Error: {$client->errCode}\n");
// 向服务端发送数据
$client->send("hello server.");
// 从服务端接收数据
$response = $client->recv();
// 输出接受到的数据
echo $response . PHP_EOL;
// 关闭连接
$client->close();

我们看到,客户端无非就是创建一个 socket 对象,然后指定 ip 和端口,连接 server,随后向 server 发送了一段数据,而后接收 server 的数据并输出,最后关闭连接。

看下模拟结果

https://file.lulublog.cn/images/3/2022/07/hDCLI85iNu5lz53U7criDZU5RS38Ww.png

注意到无论是 server 还是 client,都是在 CLI下 执行的。

从模拟的结果中我们也可以清晰的看到 client 与 server 交互的整个过程。

但是,相信很多人都会有疑问,尤其是 phper,server 和客户端都这么玩,完全看不到实际应用啊。先慢慢练习吧,我们这才刚打响与 swoole 之间的战役。

server 的关闭,手动执行 Ctrl+C 即退出。

3.2、UDP 服务

$mode = SWOOLE_PROCESS; //默认:多进程的方式
$sock_type = SWOOLE_SOCK_UDP; //默认:TCP
$serv = new swoole_server("0.0.0.0", 9501, $mode, $sock_type);
//监听数据接收事件
$serv->on('packet', function($serv, $data, $fd){
    //发送数据到相应的客户端
    $serv->sendto($fd['address'], $fd['port'], "Server:$data");
    var_dump($fd);
});
$serv->start();

打赏

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码打赏,你说多少就多少

打开微信扫一扫,即可进行扫码打赏哦

阅读 884