上一篇,我们一起初步接触了协程。我相信只有一节的讨论,很多小伙伴对于“协程”与“线程”的区分可能还有点模糊。我们这里以两者的比较作为本篇开头,进行一番比较。
首先,“协程”与“线程”的任务调度机制不一样。“协程”重在“协调”,“线程”重在“抢占”。
举个例子:现在有一个任务,需要5个“子协程”同时进行处理,那么执行顺序是:子协程1执行->子协程1暂停->子协程2执行->子协程2暂停……顺序执行,直到结束。所以,协程的特点是某个协程让出时间片后,下一个协程会接上;如果这个协程还在阻塞中,会再次移交给下一个协程。这样往复执行,直到结束。
同样的,如果现在有一个任务,需要5个“线程”同时执行,那么执行顺序是,主线程同时开辟启动5个线程,这5个线程哪一个先执行,当先执行的线程让出时间片后,接下来由哪一个线程接上,这些完全听天由命。所以线程的执行才叫做“抢占式”,直到任务结束。
其次,“协程”与“线程”解决的事情不一样。“协程”解决的的是“并发”问题,“线程”解决的是“并行”问题。
举个栗子,我们的服务器需要支撑500人同时访问,以大名鼎鼎的TOMCAT为例,正常情况下会开辟300个线程进行处理。某个线程一旦接收了请求就会进行阻塞,在处理完成之前就不会再接收第二个请求,这就决定了这时TOMCAT只能“并行”支持300人同时访问,另外的200人只能收到500的报错。但是这里存在一个问题,500人的请求业务并不是全都是阻塞行为(访问数据库、读写文件等),所以我们完全可以在一个线程中开辟N个协程,一个线程同时接受N个请求,对于有阻塞的业务请求进行等待直到业务完成,对于没有阻塞的请求,我们就可以立即响应,这样就可以将“并发”从300,提高到300*N(N>=2)。
当然,从上面这个栗子,我们可以看出“协程”针对的是IO密集型的业务(多阻塞),而线程针对的是计算密集型的业务。但我们不能简单认为IO密集型业务,协程开得越多越好。因为对于IO的处理也需要IO资源,如果同时接受太多的需要IO请求,服务器对于IO的处理在超时时间范围内无法处理完,同样会出现超时错误。
有了以上的比较,我们对于“协程”与“线程”的比较应该是更加明晰了。接下来,我们就一起进入协程风格的TCP服务器话题。先看以下这段代码:
//多进程管理模块 $pool = new SwooleProcessPool(2); //让每个OnWorkerStart回调都自动创建一个协程 $pool->set(['enable_coroutine' => true]); $pool->on('workerStart', function ($pool, $id) { //每个进程都监听9501端口 $server = new SwooleCoroutineServer('127.0.0.1', '9501' , false, true); //收到15信号关闭服务 SwooleProcess::signal(SIGTERM, function () use ($server) { $server->shutdown(); }); //接收到新的连接请求 并自动创建一个协程 $server->handle(function (SwooleCoroutineServerConnection $conn) { while (true) { //接收数据 $data = $conn->recv(); if (empty($data)) { $conn->close(); break; } //发送数据 $conn->send('hello'); Co::sleep(1); } }); //开始监听端口 $server->start(); }); $pool->start();
这是一段完整的协程风格TCP服务器的代码,对于这段代码涉及到的技术点进行以下讨论。
1、关于SwooleProcessPool
对于协程代码,我们一般需要一个“协程容器”,来保证一组协程的完整性。在swoole中,创建“协程容器”的方法有两种(编者注:官网写的是三种,但这里不考虑将异步风格转为协程风格的方式):
1) 调用管理模块 Process 和 ProcessPool 的 start 方法,指定构造函数的 enable_coroutine 参数,此种启动方式会在进程启动的时候创建协程容器。
2) 手动创建协程容器,即调用Co un() 函数,或者使用CoroutineScheduler(这两者是一样的,前者是后者的简易调用)。
对于TCP服务器,我们需要并发,需要多个进程进行管理,所以选择了SwooleProcessPool(这个进程池的使用,后续我们会详细讨论)。
对于Pool初使化后,在子进程创建时(onWorkerStart事件触发),内部实例化了协程化的TCP服务器。
当$pool->start()调用后,这时会创建出2个子进程(Pool构造函数中传入的子进程数量),所以创建后共有3个进程。主进程进入wait状态,对子进程进行管理,一旦子进程退出,就会再进行启动。这时我们可以把onWorkerStart事件的处理函数看成是一个大的协程容器。
2、SwooleCoroutineServer->__construct
SwooleCoroutineServer->__construct(string $host, int $port = 0, bool $ssl = false, bool $reuse_port = false);
$host:监听的地址,有三种格式:0.0.0.0/127.0.0.1: IPv4 地址 || ::/::1: IPv6 地址 || unix:/tmp/test.sock: UnixSocket 地址
$port:监听的端口【如果为 0 将由操作系统随机分配一个端口】
$ssl:是否开启 SSL 加密
$reuse_port:是否开启端口重用
SwooleCoroutineServer 是一个完全协程化的类,用于创建协程 TCP 服务器,支持 TCP 和 unixSocket 类型。它与 Server 模块不同之处在于以下两点:
1) 动态创建销毁,在运行时可以动态监听端口,也可以动态关闭服务器;
2) 处理连接的过程是完全同步的,程序可以顺序处理 Connect、Receive、Close 事件
3、SwooleCoroutineServer->handle:设置连接处理函数,必须在 start() 之前设置处理函数
SwooleCoroutineServer->handle(callable $fn);
$fn:设置连接处理函数
注意:
-服务器在 Accept(建立连接) 成功后,会自动创建协程并执行 $fn
-$fn 是在新的子协程空间内执行,因此在函数内无需再次创建协程
-$fn 接受一个参数,类型为 SwooleCoroutineServerConnection 对象
-可以使用 exportSocket() 得到当前连接的 Socket 对象
示例:
$server->handle(function (SwooleCoroutineServerConnection $conn) { while (true) { $data = $conn->recv(); } });
4、关于粘包处理SwooleCoroutineServer->set
SwooleCoroutineServer->set(array $options);
用法与异步风格的TCP服务器完全一致,如果已经遗忘的小伙伴,请点击这里
示例:
$server->set([ 'open_eof_split' => true, 'package_eof' => "abc" ]);
如果客户端发送,1234abc5678abc,则服务端就会recv到两条数据,分别是1234abc和5678abc。
关于协程风格的TCP服务器就介绍到这里。如果想要了解协程风格的类SwooleCoroutineServer、SwooleCoroutineServerConnection、SwooleProcessPool请阅读官网,这部分非常直观没有什么太多难点。
--------------------------- 我是可爱的分割线 ----------------------------
最后博主借地宣传一下,漳州编程小组招新了,这是一个面向漳州青少年信息学/软件设计的学习小组,有意向的同学点击链接,联系我吧。