chinawilon/learn-swoole

dev-main 2021-06-18 07:03 UTC

This package is auto-updated.

Last update: 2022-07-18 09:24:50 UTC


README

本系列课程献给在玩动网络的技术小伙伴

Day 1 swoole基础知识

  1. corotuine协程自身是串行执行的,只是遇到了异步API发生协程切换调度而已。
  2. 阻塞函数sleepfreadfwrite如果需要异步,需要开启一健协程化或者在协程容器内执行。(注意swoole支持的异步化函数)
  3. 协程里面不允许阻塞,不然不会发生切换调度,这会导致和php-fpm一样的效果甚至更差,所有逻辑同步执行。
  4. 协程在操作资源流的时候需要注意是否会引起并发读写问题,使用协程锁或者相关的
  5. 通道只能在单进程(线程)中通信(协程自身设计如此),不能跨进程,需要跨进程通信需要使用IPC技术。
  6. 通道最少有一个size=1的容量,可以实现类似,singleflight, once等机制。
  7. 多路复用的几种形式,轮训、poll/select/pselect、epoll/kqueue三者的一些重要区别。
  8. 5种IO模型,其中阻塞/非阻塞/多路复用/信号驱动式IO都属于同步IO。(想想打炒粉的场景,一直看着炒/不停的问好了没/叫你一声去拿/送你桌子上就吃,多路复用就是你同时点了炒粉另一家肠粉,坐着等人家叫你去拿)
  9. 自定义协议的通用做法,定义长度或分隔符。(想想你有一块布料,不同的人需要不同长度,你一直拉,拉多了满足裁剪,拉少继续拉直到满足裁剪,如果继续拉没有了说明断了)
  10. 网络字节序(大端),只有数字才会有的大小端问题,单字节或字符串没有这个问题。因为一个单字节就8(16)位,没有高8(16)位/低8(16)位之分。

Day 2 进程间通信技术

  1. PHP的匿名管道功能非常有限,只有一个popen以及pclose,同时只能用读或者写打开,不需要主动关闭不用的一端。返回的是一个字节流,可以用f系列函数操作。只能用于父子进程间通信,属于单向传输,退化为单工。实际上匿名管道属于半双工
  2. 命名管道可以用于没有关系的2个进程中,可以通过fifo文件进行通信。多个进程可以同时对一个fifo写入数据(需要自己保重原子性写入,一般情况下数据量小于PIPE_BUF可以原子性写入),但是不可以多个进程对同一个fifo读取,会出现竞态问题,无法保证数据会被哪个进程读取到。
  3. Socket对有2种形式,一种是stream流,一种是dgram消息。在流套接字下同样不能并发读取,但是可以并发写入(PIPE_BUF)。在信息套接字下是可以并发读写的(PIPE_BUF),内核保证读取的时候是一条条的消息。套接字是属于全双工传输模式。
  4. Unix域套接字有多种形式,根据提供的local_socketAF_INET)提供相关的实现,可以创建(tcp/udp/unix/udg)套接字。unix域套接字可以在多个不相关的进程间通信,比如常见的unix:///mysql.sockmysql服务器和客户端通信,unix:///supervisor.socksupervisor服务器和客户端通信。其他tcp://udp://可以实现网络通信套接字,可以在2台主机间通信。
  5. stream_socket_server函数可以用创建unix域套接字,这个函数相当于实现了socket,bind, listen3个系统调用。所以接下来只需要使用stream_socket_accept接受请求,获取到connection文件描述符进行f系列函数操作。
  6. 在网络编程中遇到的主要问题是多路复用问题以及异步问题,多个请求如何在一个进程中公平得到处理,否则的话容易被某一个耗时的请求阻塞住,这个时候往往需要使用多路复用机制来公平处理每个请求。轮训可以实现多路复用,但是会出现空转,如果一直都有数据进来,这种场景下,效率非常高。php提供了一种较为低级的select_stream多路复用机制,该机制可以通知有监听的流发生了监听的事件,但是自身并不知道具体是什么流和事件,是一种无差别的轮训。
  7. 关于异步问题,多路复用自身还是属于同步。php自身没有实现异步,即使非阻塞io模型也是属于同步,所以异步需要借助swoole的异步化处理。异步使得多个请求能被公平的得到处理,而不是阻塞在某个请求之上。这也是swoole为什么要做异步化的最根本的原因。

Day 3 网络服务器设计

  1. sockets/unixsocket都属于全双工(full duplex)通讯特殊文件,即同时可以读写。pipe/fifo属于半双工(half duplex)特殊文件,可读可写,但是同一时刻只能读或者写。只读或者只写的设备如键盘屏幕属于单工(simplex)设备。
  2. 多路复用的几种形式: 循环/select/epoll,其中epoll在处理高并发的时候非常有优势。php没有epoll/kqueue的api接口,swoole提供了 一个初级的封装(EventLoop)。仔细阅读PHPServerEpollServer领悟它们的区别和编程规范。
  3. Swoole的异步TCP/UDP服务器有2种模式:SWOOLE_BASE以及SWOOLE_PROCESS模式,其中Base模式类似PHPServer服务器的实现,连接/协议/io读写/数据收发都在这个进程里面,这也导致了它不能热更新/重启会丢失连接。相反Process模式,使用了一个Master进程的Reactor线程去处理这些请求,把接收的数据根据配置分发给特定的worker进程,这导致worker进程热重启而不中断连接成为可能。其他特性,请仔细阅读PHPServerSwooleServer的代码,结合官方文档自行体会。
  4. 编写网络服务器的时候,我们往往会拿到很多不同的数据模式,比如流对象/数据片段/数据报,其中流和数据片需要我们自己去拼接和切割,这个地方也就是所谓的"粘包"发生的地方。数据报是完整的消息(udp),但是是不可靠的,需要根据自身对数据的敏感程度去使用,比如视频/广播/这些场景丢掉1,2个数据报没什么关系。
  5. WebSocket协议是基于tcp协议之上定义的一套协议,使用了类似http协议的握手过程,和http协议本身没有关系。websocket的帧里面特别注意opcode有几个需要注意的值,0x8/0x9/0xA即关闭/ping/pong帧,也就是协议自身设计了心跳检测的帧位,使用这些帧的时候是不会携带payload数据的。还有一个FIN位,用于TCP数据流的切割,表示一段消息的最后一帧。
  6. websocket协程服务器里面的参数配置不一定适合异步服务器,比如open_websocket_ping_frame测试发现并不会自动回复pong帧,其他参数请自行测试。websocket是全双工通讯,所以不能使用半全工的http通讯思维去编程。比如不能理所当然的认为request/response是一对一的。
  7. websocket的服务端有socket_read_timeout超时,默认是60s。如果使用nginx,也会有proxy_read_timeout超时问题。超时之后服务端会主动关闭连接,这个时候我们需要使用心跳包去维持这个连接。

Informative References:

  1. swoole服务器进程模型
  2. websocket协议RFC
  3. Linux手册之Epoll系统调用
  4. nginx手册之proxy timeout

Day 4 信号和进程控制

  1. 信号大部分是可以捕获和忽略的,但其中有个2信号是例外sigkillsigstop是不可捕获和忽略。系统小于SIGRTMIN(例如32)值的信号是不可靠的,有可能丢失或者使用默认行为处理。
  2. 信号是异步执行的,会中断进程,需要考虑在信号处理器中函数的可重入性,不会影响进程中的函数执行。所以,我们需要在信号处理器中使用安全的可重入函数避免发生不可预知的错误。
  3. php原生信号默认情况下是不能捕获的需要配合declare(ticks=1)或者主动调用pcntl_signal_dispatch或者在当前进程中执行posix_kill。使用declare的时候需要注意循环里面的语句不能阻塞或者类似continue或者空,都不会被信号处理器捕获,因为它没有任何机会去执行。
  4. php信号里面有2个非常重要的参数$signo以及$siginfo,其中siginfo里面的pid可以判断信号是来着管理员还是其他进程。
  5. swoole的信号处理器使用了signalfd4系统调用,使之生成一个描述符加入到EventLoop中去监听,如果当前进程是阻塞的,同样信号处理器无法得到执行,也就是epoll没有机会运行, 即使同样适用declare也是无效的。需要放入到run协程调度器,使之类似sleep函数异步化,从而使得epoll能够有机会运行。
  6. Ctrl+D不是发送信号,而是表示一个特殊的二进制值,表示EOF。其他有ctrl+Z发送信号sigtstpctrl+C发送信号sigintctrl+\发生信号sigquit,其中信号sigtstp表示进程被stop,并不是在后台继续运行,如果需要继续运行可以使用shell的fg或者发送sigcont信号。如果进程被sigstop也可以使用sigcont信号重新运行。
  7. 拉起一个新进程使用pcntl_fork,这个时候新的进程描述符指向的是同一进程地址空间,这个时候包括进程之前的所有打开的文件描述符/信号都是一样的。只有当新进程有新的内存需要写入的时候,这个时候才会复制父进程内容。线程,其实也是属于特殊的进程(linux),只是它的地址空间和父进程一样而已,但是它有自己独立的栈。
  8. 如果父进程有信号处理,自身只是单纯的fork,需要子进程去处理信号,比如使用SIG_DFL或者SIG_IGN或者重新其他为其他信号处理器。
  9. 子进程退出如果父进程没有去监听或者主动忽略,这个时候子进程会成为僵尸进程残留在系统里面,占用文件描述符和一定的内存资源。如果子进程的父进程比子进程先退出,子进程会变成孤儿进程(这里需要分情况,有可能被兄弟进程收养),默认会被init=1的进程收养,如果子进程退出了,init进程会回收它。父进程先退出这种技术一般用来实现daemon守护进程
  10. 子进程退出的时候会向父进程发送sigchld信号,如果大量子进程同时退出,由于信号是不可靠的,父进程可能无法监听到,所以如果想要监听子进程状态,不建议使用监听sigchld信号的方式,而是在循环里面使用pcntl_wait或者pcntl_waitpid,其中有一个$status参数,可以判断子进程的状态变化,注意子进程有可能并不是退出,所以需要使用pcntl_wif系列函数判断其状态。
  11. 使用php原生编写多进程网络服务器的时候,需要注意当前逻辑是在什么进程里面,这里往往会让人抓狂。进程间需要进程通信,选择合适的ipc技术,一般会使用unixsocket进行通信,双全工非常方便,注意流的并发读问题。
  12. 进程配合信号处理可以实现优雅的关闭服务器,一般是处理死循环的逻辑。可以实现热重启,这个时候需要设计好进程模型,比如使用master-manager-worker模型,可以热重启worker进程。如果只是manager-worker模型,可以考虑新进程继续重用worker的socket套接字,重而实现热重启。以此,避免使用单进程模型设计网络服务器。

Informative References:

  1. TCP/IP 《TCP/IP Illustrated Volume 1: The Protocols》
  2. APUE《Advanced Programming in the UNIX® Environment 3rd》
  3. UNP 《UNIX Network Programming 3rd》