并发IO一直是服务端编程的技术难题,从最早的同步阻塞直接Fork进程,到Worker进程池/线程池,再到现在的异步IO和协程。因为PHP程序员拥有强大的LAMP框架,所以对这类底层知识知之甚少。本文旨在详细介绍PHP对并发IO编程的各种尝试,最后介绍Swoole的使用,深入浅出全面分析并发IO问题。多进程/多线程同步阻塞最早的服务器端程序是通过多进程多线程来解决并发IO的问题。进程模型最早出现,进程的概念从Unix系统诞生时就已经有了。最早的服务器端程序一般是接受一个客户端连接创建一个进程,然后子进程进入一个循环,以同步阻塞的方式与客户端连接进行交互,发送和接收数据。后来出现了多线程模式。与进程相比,线程更轻,线程之间共享内存栈,因此不同线程之间的交互很容易实现。例如,在聊天室这样的程序中,客户端连接可以相互交互,聊天室??中的玩家可以向任何其他人发送消息。多线程方式实现起来非常简单,线程可以直接向某个客户端连接发送数据。多进程模式需要使用管道、消息队列和共享内存,这些统称为进程间通信(IPC)和复杂技术。代码示例:多进程/线程模型的过程是创建socket,绑定服务器端口(bind),监听端口(listen)。以上三个步骤可以使用PHP中的stream_socket_server函数来完成。当然也可以使用下层的sockets扩展单独实现。进入while循环,阻塞在accept操作上,等待客户端连接进入。此时,程序会进入休眠状态,直到有新的客户端向服务器发起连接,操作系统才会唤醒该进程。accept函数返回客户端连接的socket主进程。多进程模型下使用fork(php:pcntl_fork)创建子进程,多线程模型下使用pthread_create(php:newThread)创建子线程。如果下面没有特殊说明,会同时使用process来表示process/thread。子进程创建成功后,进入while循环,阻塞在recv(php:fread)调用上,等待客户端向服务端发送数据。服务端程序收到数据后进行处理,然后使用send(php:fwrite)向客户端发送响应。长连接服务会继续与客户端交互,而短连接服务一般会在收到响应后关闭。当客户端连接关闭时,子进程退出并销毁所有资源。主进程会回收子进程。这种模型最大的问题是进程/线程的创建和销毁非常昂贵。所以上面的模式不能应用于非常繁忙的服务器程序。相应的改进版本解决了这个问题,就是经典的Leader-Follower模型。代码示例:其特点是程序启动后会创建N个进程。每个子进程进入Accept,等待新的连接进来。当客户端连接到服务器时,其中一个子进程被唤醒,开始处理客户端请求,不再接受新的TCP连接。当这个连接关闭时,子进程会被释放,重新进入Accept,参与处理新的连接。这种模型的好处是流程可以完全复用,不需要额外的消耗,性能非常好。很多常见的服务器程序都是基于这种模型的,比如Apache和PHP-FPM。多进程模型也有一些缺点。该模型严重依赖进程数来解决并发问题。一个客户端连接需要占用一个进程。工作进程的数量取决于并发处理能力。操作系统可以创建的进程数是有限的。启动大量进程会带来额外的进程调度消耗。当有数百个进程时,进程上下文切换调度的消耗可能占CPU的不到1%,可以忽略不计。如果启动几千甚至几万个进程,消耗就会暴增。调度消耗可能占CPU的百分之几十甚至百分之一百。另外还有一些场景是多进程模型解决不了的,比如即时通讯程序(IM),一个服务器要同时维护几万甚至几十万上百万的连接(经典的C10K问题),多进程模型不能如愿以偿。另一种场景也是多进程模型的弱点。通常Web服务器启动100个进程。如果一个请求消耗100ms,100个进程可以提供1000qps,这是一个不错的处理能力。但是如果请求需要调用外网Http接口,比如QQ、微博登录等,时间会比较长,一个请求需要10s。那一个进程每秒只能处理0.1个请求,100个进程也只能达到10qps,处理能力太差了。有没有一种技术可以在一个进程中处理所有并发IO?答案是肯定的,这就是IO多路复用技术。IO多路复用/事件循环/异步非阻塞其实IO多路复用的历史和多进程一样悠久。Linux很早就提供了select系统调用,一个进程可以维持1024个连接。后来增加了poll系统调用,poll做了一些改进,解决了1024个限制的问题,可以保持任意数量的连接。但是select/poll还有一个问题就是需要循环检测连接上是否有事件。这就是问题所在。如果服务器有100万个连接,某一时刻只有一个连接向服务器发送数据,则select/poll需要循环100万次,其中只有1次命中,其余99万次9999次无效,白白浪费CPU资源。直到Linux2.6内核提供了一个新的epoll系统调用,无需轮询即可保持无限个连接,这才真正解决了C10K的问题。现在各种高并发的异步IO服务器程序都是基于epoll实现的,比如Nginx、Node.js、Erlang、Golang。像Node.js这样的单进程单线程程序,可以维持100万以上的TCP连接,这都要归功于epoll技术。IO多路复用异步非阻塞程序采用经典的Reactor模型。顾名思义,Reactor是反应器的意思,它不处理任何数据的发送和接收。只能监听一个套接字句柄的事件变化。Reactor有4个核心操作:addAddsockettomonitortoreactor,可以是listensocket或者clientsocket,也可以设置修改事件监听,比如pipeline,eventfd,signal等。可以设置监听的类型,比如readable,可以写。它易于阅读且易于理解。对于listensocket,表示有新的客户端连接到达,需要接受。对于客户端连接是接收数据,recv是必需的。可写事件有点难以理解。SOCKET有一个缓冲区。如果要给客户端连接发送2M的数据,是不能一次性发送的。操作系统默认的TCP缓冲区只有256K。一次只能发送256K,缓冲区满后send会返回EAGAIN错误。这时候就需要监听可写事件了。在纯异步编程中,需要监听可写事件,保证发送操作是完全非阻塞的。del从反应器中移除,不再监听事件。回调是事件发生后相应的处理逻辑。它通常在添加/设置期间制定。C语言是用函数指针实现的,JS可以使用匿名函数,PHP可以使用匿名函数、对象方法数组、字符串函数名。Reactor只是一个事件生成器,对socket句柄的实际操作,如connect/accept、send/recv、close,都是在回调中完成的。具体编码可以参考如下伪代码:Reactor模型还可以结合多进程、多线程使用,既实现了异步非阻塞IO,又利用了多核。目前流行的异步服务器程序都是这样的:比如Nginx:多进程ReactorNginx+Lua:多进程Reactor+协程Golang:单线程Reactor+多线程协程Swoole:多线程Reactor+多进程Worker协程协程是什么,从底层技术来看,其实是一种异步IOReactor模型。应用层自己实现任务调度,利用Reactor在当前执行的用户态线程之间切换,但是Reactor的存在对用户代码是完全感知不到的。PHP并发IO编程实践PHP相关扩展Stream:PHP内核提供的socket包Sockets:底层SocketAPI的包Libevent:libevent库的包Event:基于Libevent更高级的包,提供面向对象的接口,定时器,和信号处理支持Pcntl/Posix:多进程、信号、进程管理支持Pthread:多线程、线程管理、锁支持PHP和相关扩展,涉及共享内存、信号量、消息队列PECL:PHP扩展库,包括系统的底层,数据分析、算法、驱动、科学计算、图形等一应俱全。如果在PHP标准库中没有找到,可以在PECL中寻找需要的函数。PHP语言的优缺点PHP的优点:第一个就是简单,PHP比其他任何语言都简单,如果上手的话,一个星期就可以真正上手PHP。有一本关于C++的书,叫做《21天深入学习C++》。其实21天是不可能学会的。甚至可以说,C++没有3-5年是无法深入掌握的。但是PHP绝对可以7天上手。所以PHP程序员的数量非常多,招聘也比其他语言容易。PHP非常强大,因为PHP官方的标准库和扩展库提供了99%可以用于服务器端编程的东西。PHP的PECL扩展库中您想要的任何函数。另外,PHP已经有20多年的历史,生态系统非常庞大。你可以在Github上找到很多代码。PHP的缺点:性能比较差,因为毕竟是动态脚本,不适合密集计算。同样的PHP程序如果用C/C++写,PHP版本会比它差一百倍。大家都知道函数命名标准很差。PHP更注重实用性,没有一些标准。有些函数的命名很混乱,每次都得翻PHP手册。提供的数据结构和函数的接口粒度比较粗。PHP只有一种Array数据结构,底层是基于HashTable的。PHP的Array集成了Map、Set、Vector、Queue、Stack、Heap等数据结构的功能。另外,PHP有一个SPL,提供对其他数据结构的类封装。因此,PHPPHP更适合实际应用层面的程序。PHP作为业务开发和快速实施的利器,并不适合开发底层软件。C/C++、JAVA、Golang等静态编译语言作为PHP的补充。动静结合使用IDE工具实现自动补全。,语法提示PHP的Swoole扩展基于以上扩展,可以完全使用纯PHP实现异步web服务器和客户端程序。但是,如果要实现多IO线程,仍然需要做很多繁琐的编程工作,包括如何管理连接,如何保证发送和接收数据的原子性,以及网络协议的处理等。另外,PHP代码在协议处理部分的性能比较差,所以我开始了一个新的开源项目Swoole,使用C语言和PHP的结合来完成工作。灵活多变的业务模块使用PHP开发效率高,基础底层和协议处理部分用C语言实现,保证了高性能。它以扩展的方式加载到PHP中,提供一个完整的网络通信框架,然后用PHP代码编写一些业务。其模型基于多线程Reactor+多进程Worker,同时支持全异步和半异步半同步。Swoole的一些特性:Accept线程,解决Accept性能瓶颈和shockgroup问题多IO线程,可以更好的利用多核提供全异步和半同步半异步两种模式来处理高并发IO部分复杂业务使用异步模式逻辑部分使用同步模式底层,支持所有连接遍历,数据相互传输,数据包自动合并拆分,数据传输原子性。Swoole的进程/线程模型:Swoole程序执行流程:使用PHP+Swoole扩展实现异步通信编程示例代码在https://github.com/swoole/swo...首页查看。TCPserver和client异步TCPserver:这里是新建swoole_server对象,然后传入参数传入监听的HOST和PORT,然后设置3个回调函数,分别是onConnect有新连接,onReceive有接收到数据某个客户端,onClose客户端关闭连接。最后调用start启动服务器程序。swoole底层会根据当前机器的CPU核数启动相应数量的Reactor线程和Worker进程。异步客户端:客户端的使用方法和服务端类似,只是有4个回调事件,onConnect成功连接到服务端,就可以向服务端发送数据了。onError无法连接到服务器。onReceive服务器向客户端连接发送数据。onClose连接关闭。设置事件回调后,发起连接服务器,参数为服务器的IP、PORT和超时时间。同步客户端:同步客户端不需要设置任何事件回调,没有Reactor监听,是阻塞串行的。等待IO完成,然后再继续下一步。异步任务:异步任务函数用于在纯异步Server程序中执行一个耗时或阻塞的函数。底层实现使用进程池,任务完成后触发onFinish,在程序中可以获取到任务处理的结果。比如一个IM需要广播,在异步代码中直接广播可能会影响其他事件的处理。另外,文件读写也可以使用异步任务来实现,因为文件句柄不能像sockets那样被Reactor监听。因为文件句柄总是可读的,直接读取文件可能会阻塞服务器程序,使用异步任务是一个非常好的选择。异步毫秒定时器的两个接口实现了类似JS的setInterval和setTimeout函数,可以设置为间隔n毫秒执行一个函数或者n毫秒后执行一个函数。异步MySQL客户端swoole还提供了一个内置连接池的MySQL异步客户端,可以设置最大MySQL连接数。并发的SQL请求可以重用这些连接,而不是重复创建它们,这样可以保护MySQL不会耗尽连接资源。异步Redis客户端异步Web程序的逻辑是从Redis中读取一段数据,然后显示HTML页面。使用ab测试性能如下:同样逻辑在php-fpm下的性能测试结果如下:WebSocket程序swoole内置了websocketserver,基于它可以实现主动推送网页的功能实现了,比如WebIM。有一个开源项目可以作为参考。https://github.com/matyhtf/ph...
