深入浅出网络编程

在阿里云PHP技术沙龙专场中,阿里云邀请到php-nsq作者,pecl、Swoole开发组成员吴振宇分享了Swoole进程模型的原理与Swoole协程实现的原理。并结合具体开发案例讲解了Swoole在网络编程中的应用。
展开查看详情

1.深入浅出网络编程与Swoole内核

2.吴振宇 美篇架构组负责人 Swoole commiter php-nsq作者

3.目录 一. 基础概念 二. IO复用与Reactor 三. Swoole 进程模型与Reactor 四. Swoole 协程实现原理

4. socket编程 Server 端主要端步骤: 1. socket函数 创建socket 2. bind函数 绑定地址 3. listen函数 监听端口 4. accept函数 接受请求 client端主要步骤 1. socket函数 创建socket 2. connect函数 连接服务端

5.

6.

7. Socket 可读可写条件 可读 1. 该套接字接收缓冲区中的数据字节数大于等于套接字接收缓存区低水位。对于TCP和UDP套接字而言,缓冲区 低水位的值默认为1。那就意味着,默认情况下,只要缓冲区中有数据,那就是可读的。 2. 该连接的读半部关闭(也就是接收了FIN的TCP连接)。对这样的套接字的读操作,将不会阻塞,而是返回0 (也就是EOF) 3. 有新链接到达可读,该套接字是一个listen的监听套接字,并且目前已经完成的连接数不为0。是因为socket收 到了对方的connect请求,执行了三次握手的第一步:对方发送SYN请求过来,使监听socket处于可读状态;对这样 的套接字进行accept操作通常不会阻塞 4. 有一个socket有异常错误条件待处理.对于这样的socket的读操作将不会阻塞,并且返回一个错误(-1),errno则设 置成明确的错误条件.

8. Socket 可读可写条件 可写 1. 该套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓存区低水位标记时,并且该套接字已经成功连 接(UDP套接字不需要连接)。对于TCP和UDP而言,这个低水位的值默认为2048,而套接字默认的发送缓冲 区大小是8k,这就意味着一般一个套接字连接成功后,就是处于可写状态的。 2. 该连接的写半部关闭(主动发送FIN包的TCP连接)。对这样的套接字的写操作将会产生SIGPIPE信号。所以我们 的网络程序基本都要自定义处理SIGPIPE信号。因为SIGPIPE信号的默认处理方式是程序退出。 3. 使用非阻塞的connect套接字已建立连接,或者connect已经以失败告终。即connect有结果了。 4. 有一个错误的套接字待处理。对这样的套接字的写操作将不阻塞并返回-1(也就是返回了一个错误),同时把 errno设置成确切的错误条件。

9. 修手机的故事 1 在手机店 什么也不做 干瞪着眼 看这老板修手机 等他修好 2 在店里做会儿其他事情,时不时的过来问下手机有没有修好 3 回家什么也不干 等老板打电话 过去拿手机 4 回家做些其他事情 等老板打电话 去拿手机

10.1. 同步阻塞 1 在手机店 什么也不做 干瞪着眼 看这老板修手机 等他修好 2. 同步非阻塞 2 在店里做会儿其他事情,时不时的过来问下手机有没有修好 3. 异步阻塞 3 回家什么也不干 等老板打电话 过去拿手机 4. 多路IO 复用、异步非阻塞 4 回家做些其他事情 等老板打电话 去拿手机 同步、异步、阻塞、非阻塞 参考 https://cloud.tencent.com/developer/article/1005527

11. 多进程同步阻塞 每个请求一个进程 伪代码:

12. 多进程同步阻塞 先fork子进程,在子进程监听 伪代码:

13.多路IO复用/ Reactor/异步非阻塞 1. Select select的几大缺点: 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 select支持的文件描述符数量太小了,默认是1024,为什么1024 因为代码写死的 。 2. Poll Poll没有最大连接数的限制,原因是它是基于链表来存储的,但是同样又有同select一样缺点 3.Epoll 、Kqueue 创建的事件存在红黑树这样的一个数据结构 事件都会与相应的设备(网卡)驱动程序建立回调关系,当相应的事件发生后,会添加到双向链表中。 只需要检查rdlist双向链表中是否有存在注册的事件,效率非常可观

14. Epoll 为何高效-先理解下操作系统调度 1. 进程执行调度方法:操作系统总体上按照时间片来调度进程执行,进程执行调度状态分为三种:Running、Ready 和 Block(具体状态命名可 能不是教科书准确)。等待资源就绪的进程会置为 Block 状态(比如调用 accept 并阻塞的进程),资源就绪可以随时运行的进程会放在每个 CPU 的调度队列里,获得当前 CPU 时间片运行中的进程是 Running 状态,等待 CPU 时间片分配的进程是 Ready 状态。 2. 内核执行中断上下文:内核在处理硬件中断时,会直接打断正在执行的 Running 状态进程(包括系统调用),进行必要的内存拷贝和状态更新 (比如处理 TCP 握手),结束中断处理后恢复运行被打断的进程。 3. Wait Queue:Linux 内核实现进程唤醒的关键数据结构。通常一个事件体有一个 wait queue,对这个事件体感兴趣的进程或者系统会提供回调 函数,并将自己注册到这个事件体的 wait queue 上。当事件发生时,会调用注册在 wait queue 上的回调函数。常见的回调函数是,将对这个事件 感兴趣的进程的调度状态置为 Ready,于是在调度系统重新分配 CPU 时间片时,将该进程重新执行,从而实现进程等待资源就绪而唤醒的过程。

15. Epoll 为何高效 1. 网卡收到 SYN,触发内核中断,直接打断当前执行的进程,CPU 进行中断处理逻辑, 最终将该 SYN 连接信 息保存在相应 listen socket 的半连接队列里,并向对方发送 SYN-ACK,然后恢复运行被打断的进程。 2. 进程执行完当前作业,调用 accept 系统调用(阻塞)继续处理新连接。accept 发现连接队列当前没有新连 接后,于是在 listen socket 的 wait queue 的上注册唤醒自身进程的回调函数,然后内核将这个进程置为 Block 状态,并让出 CPU 执行其他 Ready 状态的进程。 3. 网卡收到 ACK,继续触发内核中断,内核完成标准的三次握手,将连接从半连接队列移入连接队列,于是 listen socket 有可读事件,内核调用 listen socket 的 wait queue 的唤醒回调函数,将之前阻塞的 accept 进 程置为 Ready 调度状态。 网卡有数据——>中断回调——>添加到rdlist

16.多路IO复用/ Reactor ——epoll的使用 1. int epoll_create(int size); 2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 参数二: EPOLL_CTL_ADD :注册新的fd到epfd中; EPOLL_CTL_MOD :修改已经注册的fd的监听事件; EPOLL_CTL_DEL :从epfd中删除一个fd; 参数四 events可以是以下几个宏的集合: EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭); EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。 EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 4. Level_triggered(水平触发) 、Edge_triggered(边缘触发)

17.改造成epoll 下页继续

18. 改造成epoll 多路IO复用/ Reactor ——epoll的使用

19.多路IO复用之 Reactor 模型

20.Swoole —— reactor

21.单线程模式(SWOOLE_BASE) BASE模式下Reactor和Worker是同一个角色 BASE模式的优点: BASE模式没有IPC开销,性能更好 BASE模式代码更简单,不容易出错 BASE模式的缺点: TCP连接是在Worker进程中维持的,所以当某个Worker进程挂掉时,此Worker内的所有连接都将被关闭 少量TCP长连接无法利用到所有Worker进程 TCP连接与Worker是绑定的,长连接应用中某些连接的数据量大,这些连接所在的Worker进程负载会非常高。但某些连接数据量小, 所以在Worker进程的负载会非常低,不同的Worker进程无法实现均衡。 BASE模式的适用场景: 如果客户端连接之间不需要交互,可以使用BASE模式。如Memcache、Http服务器等。 进程模式(SWOOLE_PROCESS) 多进程模式是最复杂的方式,用了大量的进程间通信、进程管理机制。适合业务逻辑非常复杂的场景。Swoole提供了完善的进程管理、 内存保护机制。 Swoole在Reactor线程中提供了Buffer的功能,可以应对大量慢速连接和逐字节的恶意客户端。另外也提供了CPU亲 和设置选项,使程序运行的效率更好。 进程模式的优点: 连接与数据请求发送是分离的,不会因为某些连接数据量大某些连接数据量小导致Worker进程不均衡 Worker进程发送致命错误时,连接并不会被切断 可实现单连接并发,仅保持少量TCP连接,请求可以并发地在多个Worker进程中处理 进程模式的缺点: 存在2次IPC的开销,master进程与worker进程需要使用UnixSocket进行通信 不支持某些高级功能,如sendwait、pause、resume等操作

22.Swoole 非BASE 模式

23.多路IO复用/ Reactor —— swoole中的实现

24.多路IO复用/ Reactor —— Swoole中的实现

25. Swoole 协程 • 事件驱动 • 栈切换

26.C 语言函数调用栈 • Ebp 指向栈底 • Esp 指向栈顶

27.

28.step1: 为当前作用域分配一块内存,充当运行栈,zend_execute_data结构、所有局部变量、中间变量等等都在此内存上分配 step2: 初始化全局变量符号表,然后将全局执行位置指针EG(current_execute_data)指向step1新分配的zend_execute_data,然 后将zend_execute_data.opline指向op_array的起始位置 step3: 从EX(opline)开始调用各opcode的C处理handler(即_zend_op.handler),每执行完一条opcode将EX(opline)++继续执行 step3.1: if语句将根据条件的成立与否决定EX(opline) + offset所加的偏移量,实现跳转 step3.2: 如果是函数调用,则首先从EG(function_table)中根据function_name取出此function对应的编译完成的 zend_op_array,然后像step1一样新分配一个zend_execute_data结构,将EG(current_execute_data)赋值给新结构的 prev_execute_data,再将EG(current_execute_data)指向新的zend_execute_data,最后从新的zend_execute_data.opline 开始执行,切换到函数内部,函数执行完以后将EG(current_execute_data)重新指向EX(prev_execute_data),释放分配的运行 栈,销毁局部变量,继续从原来函数调用的位置执行 step3.3: 类方法的调用与函数基本相同,后面分析对象实现的时候再详细分析

29.PHP函数调用栈——函数信息存储