本系列课程献给在玩动网络的技术小伙伴
corotuine
协程自身是串行执行的,只是遇到了异步API
发生协程切换调度而已。- 阻塞函数
sleep
,fread
,fwrite
如果需要异步
,需要开启一健协程化
或者在协程容器内执行。(注意swoole支持的异步化函数) - 协程里面不允许阻塞,不然不会发生切换调度,这会导致和
php-fpm
一样的效果甚至更差,所有逻辑同步执行。 - 协程在操作
资源流
的时候需要注意是否会引起并发读写问题,使用协程锁
或者相关的池
- 通道只能在单进程(线程)中通信(协程自身设计如此),不能跨进程,需要跨进程通信需要使用
IPC
技术。 - 通道最少有一个
size=1
的容量,可以实现类似锁
,singleflight
,once
等机制。 - 多路复用的几种形式,轮训、poll/select/pselect、epoll/kqueue三者的一些重要区别。
- 5种IO模型,其中阻塞/非阻塞/多路复用/信号驱动式IO都属于同步IO。(想想打炒粉的场景,一直看着炒/不停的问好了没/叫你一声去拿/送你桌子上就吃,多路复用就是你同时点了炒粉另一家肠粉,坐着等人家叫你去拿)
- 自定义协议的通用做法,定义长度或分隔符。(想想你有一块布料,不同的人需要不同长度,你一直拉,拉多了满足裁剪,拉少继续拉直到满足裁剪,如果继续拉没有了说明断了)
- 网络字节序(大端),只有数字才会有的大小端问题,单字节或字符串没有这个问题。因为一个单字节就8(16)位,没有高8(16)位/低8(16)位之分。
- PHP的匿名管道功能非常有限,只有一个
popen
以及pclose
,同时只能用读或者写打开,不需要主动关闭不用的一端。返回的是一个字节流,可以用f系列
函数操作。只能用于父子进程间通信,属于单向传输,退化为单工
。实际上匿名管道
属于半双工
。 - 命名管道可以用于没有关系的2个进程中,可以通过
fifo
文件进行通信。多个进程可以同时对一个fifo写入数据(需要自己保重原子性写入,一般情况下数据量小于PIPE_BUF
可以原子性写入),但是不可以多个进程对同一个fifo读取,会出现竞态问题,无法保证数据会被哪个进程读取到。 - Socket对有2种形式,一种是
stream
流,一种是dgram
消息。在流套接字下同样不能并发读取,但是可以并发写入(PIPE_BUF)。在信息套接字下是可以并发读写的(PIPE_BUF),内核保证读取的时候是一条条的消息。套接字是属于全双工
传输模式。 - Unix域套接字有多种形式,根据提供的
local_socket
(AF_INET
)提供相关的实现,可以创建(tcp/udp/unix/udg)套接字。unix域套接字可以在多个不相关的进程间通信,比如常见的unix:///mysql.sock
mysql服务器和客户端通信,unix:///supervisor.sock
supervisor服务器和客户端通信。其他tcp://
和udp://
可以实现网络通信套接字,可以在2台主机间通信。 stream_socket_server
函数可以用创建unix域套接字,这个函数相当于实现了socket
,bind
,listen
3个系统调用。所以接下来只需要使用stream_socket_accept
接受请求,获取到connection
文件描述符进行f系列
函数操作。- 在网络编程中遇到的主要问题是
多路复用问题
以及异步问题
,多个请求如何在一个进程中公平得到处理,否则的话容易被某一个耗时的请求阻塞住,这个时候往往需要使用多路复用机制来公平处理每个请求。轮训
可以实现多路复用,但是会出现空转,如果一直都有数据进来,这种场景下,效率非常高。php提供了一种较为低级的select_stream
多路复用机制,该机制可以通知有监听的流发生了监听的事件,但是自身并不知道具体是什么流和事件,是一种无差别的轮训。 - 关于
异步
问题,多路复用自身还是属于同步。php自身没有实现异步,即使非阻塞io模型也是属于同步,所以异步需要借助swoole的异步化处理。异步
使得多个请求能被公平的得到处理,而不是阻塞在某个请求之上。这也是swoole为什么要做异步化的最根本的原因。
- sockets/unixsocket都属于
全双工(full duplex)
通讯特殊文件,即同时可以读写。pipe/fifo属于半双工(half duplex)
特殊文件,可读可写,但是同一时刻只能读或者写。只读或者只写的设备如键盘
,屏幕
属于单工(simplex)
设备。 - 多路复用的几种形式: 循环/select/epoll,其中epoll在处理高并发的时候非常有优势。php没有epoll/kqueue的api接口,swoole提供了 一个初级的封装(EventLoop)。仔细阅读
PHPServer
和EpollServer
领悟它们的区别和编程规范。 - Swoole的异步TCP/UDP服务器有2种模式:
SWOOLE_BASE
以及SWOOLE_PROCESS
模式,其中Base
模式类似PHPServer
服务器的实现,连接/协议/io读写/数据收发都在这个进程里面,这也导致了它不能热更新/重启会丢失连接。相反Process
模式,使用了一个Master进程的Reactor
线程去处理这些请求,把接收的数据根据配置分发给特定的worker进程,这导致worker进程热重启而不中断连接成为可能。其他特性,请仔细阅读PHPServer
和SwooleServer
的代码,结合官方文档自行体会。 - 编写网络服务器的时候,我们往往会拿到很多不同的数据模式,比如流对象/数据片段/数据报,其中流和数据片需要我们自己去拼接和切割,这个地方也就是所谓的"粘包"发生的地方。数据报是完整的消息(udp),但是是不可靠的,需要根据自身对数据的敏感程度去使用,比如视频/广播/这些场景丢掉1,2个数据报没什么关系。
- WebSocket协议是基于tcp协议之上定义的一套协议,使用了类似http协议的握手过程,和http协议本身没有关系。websocket的帧里面特别注意
opcode
有几个需要注意的值,0x8/0x9/0xA即关闭/ping/pong帧,也就是协议自身设计了心跳检测的帧位,使用这些帧的时候是不会携带payload
数据的。还有一个FIN
位,用于TCP数据流的切割,表示一段消息的最后一帧。 - websocket协程服务器里面的参数配置不一定适合异步服务器,比如
open_websocket_ping_frame
测试发现并不会自动回复pong
帧,其他参数请自行测试。websocket是全双工
通讯,所以不能使用半全工
的http通讯思维去编程。比如不能理所当然的认为request/response
是一对一的。 - websocket的服务端有
socket_read_timeout
超时,默认是60s。如果使用nginx,也会有proxy_read_timeout
超时问题。超时之后服务端会主动关闭连接,这个时候我们需要使用心跳包去维持这个连接。
Informative References:
- 信号大部分是可以捕获和忽略的,但其中有个2信号是例外
sigkill
和sigstop
是不可捕获和忽略。系统小于SIGRTMIN(例如32)值的信号是不可靠的,有可能丢失或者使用默认行为处理。 - 信号是异步执行的,会中断进程,需要考虑在信号处理器中函数的可重入性,不会影响进程中的函数执行。所以,我们需要在信号处理器中使用安全的可重入函数避免发生不可预知的错误。
- php原生信号默认情况下是不能捕获的需要配合
declare(ticks=1)
或者主动调用pcntl_signal_dispatch
或者在当前进程中执行posix_kill
。使用declare
的时候需要注意循环里面的语句不能阻塞或者类似continue
或者空,都不会被信号处理器捕获,因为它没有任何机会去执行。 - php信号里面有2个非常重要的参数
$signo
以及$siginfo
,其中siginfo里面的pid可以判断信号是来着管理员还是其他进程。 - swoole的信号处理器使用了
signalfd4
系统调用,使之生成一个描述符加入到EventLoop
中去监听,如果当前进程是阻塞的,同样信号处理器无法得到执行,也就是epoll没有机会运行, 即使同样适用declare
也是无效的。需要放入到run
协程调度器,使之类似sleep
函数异步化,从而使得epoll能够有机会运行。 Ctrl+D
不是发送信号,而是表示一个特殊的二进制值,表示EOF。其他有ctrl+Z
发送信号sigtstp
,ctrl+C
发送信号sigint
,ctrl+\
发生信号sigquit
,其中信号sigtstp
表示进程被stop
,并不是在后台继续运行,如果需要继续运行可以使用shell的fg
或者发送sigcont
信号。如果进程被sigstop
也可以使用sigcont
信号重新运行。- 拉起一个新进程使用
pcntl_fork
,这个时候新的进程描述符指向的是同一进程地址空间,这个时候包括进程之前的所有打开的文件描述符/信号都是一样的。只有当新进程有新的内存需要写入的时候,这个时候才会复制父进程内容。线程,其实也是属于特殊的进程(linux),只是它的地址空间和父进程一样而已,但是它有自己独立的栈。 - 如果父进程有信号处理,自身只是单纯的
fork
,需要子进程去处理信号,比如使用SIG_DFL
或者SIG_IGN
或者重新其他为其他信号处理器。 - 子进程退出如果父进程没有去监听或者主动忽略,这个时候子进程会成为
僵尸进程
残留在系统里面,占用文件描述符和一定的内存资源。如果子进程的父进程比子进程先退出,子进程会变成孤儿进程(这里需要分情况,有可能被兄弟进程收养),默认会被init=1
的进程收养,如果子进程退出了,init
进程会回收它。父进程先退出这种技术一般用来实现daemon守护进程
。 - 子进程退出的时候会向父进程发送
sigchld
信号,如果大量子进程同时退出,由于信号是不可靠的,父进程可能无法监听到,所以如果想要监听子进程状态,不建议使用监听sigchld
信号的方式,而是在循环里面使用pcntl_wait
或者pcntl_waitpid
,其中有一个$status
参数,可以判断子进程的状态变化,注意子进程有可能并不是退出,所以需要使用pcntl_wif系列函数
判断其状态。 - 使用php原生编写多进程网络服务器的时候,需要注意当前逻辑是在什么进程里面,这里往往会让人
抓狂
。进程间需要进程通信,选择合适的ipc技术,一般会使用unixsocket进行通信,双全工非常方便,注意流的并发读问题。 - 进程配合信号处理可以实现优雅的关闭服务器,一般是处理死循环的逻辑。可以实现热重启,这个时候需要设计好进程模型,比如使用
master-manager-worker
模型,可以热重启worker进程。如果只是manager-worker
模型,可以考虑新进程继续重用worker的socket套接字,重而实现热重启。以此,避免使用单进程
模型设计网络服务器。
Informative References:
- TCP/IP 《TCP/IP Illustrated Volume 1: The Protocols》
- APUE《Advanced Programming in the UNIX® Environment 3rd》
- UNP 《UNIX Network Programming 3rd》