【epoll】epoll多路复用和Reactor设计思想

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"目录","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"1、Reactor设计思想","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"文章相关视频讲解:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"C/C++ Linux服务器开发高级架构学习视频点击:","attrs":{}},{"type":"link","attrs":{"href":"https://ke.qq.com/course/417774?flowToken=1013189","title":null,"type":null},"content":[{"type":"text","text":"C/C++Linux服务器开发/Linux后台架构师-学习视频","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.bilibili.com/video/BV1oy4y1n74u/","title":null,"type":null},"content":[{"type":"text","text":"epoll原理剖析以及reactor模型应用","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://www.bilibili.com/video/BV1rN411Q7HY/","title":null,"type":null},"content":[{"type":"text","text":"linux epoll网络编程细节处理","attrs":{}}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"小前言:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reactor必要","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" 传统OIO模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.2 Reactor模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.3 单线程Reactor模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"单Reactor多线程模式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.4 多线程Reactor模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"封装Epoll实现并发","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reactor模式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"封装Epoll实现reactor模式的高性能并发服务器","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"epoll的api","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reactor模式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"EPOLL实现的要点","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"1、Reactor设计思想","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"小前言:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"reactor是对epoll的一层封装 ,epoll是对io进行管理,reactor将对io的管理转化为对事件的管理","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Reactor必要","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"传统OIO模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如图2.1所示为传统IO模式处理示意图:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/52/52c512c45dc869e111563d59fa4c0cf3.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"图中所示一般是一个请求一个单独的处理线程。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺点:server的accpet操作是阻塞的,业务处理中的handler中的读写请求也是阻塞的。那么这样的一种IO模式将会导致一个线程的请求没有处理完成无法处理下一个请求,这样就大大降低了吞吐量,这将是一个严重的问题。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"为了解决这种问题就出现了一个经典的模式——Connection Per Thread即一个线程处理一个请求。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/29/29252349f67185150ea1a66f7485c0c6.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"对于每一个新的请求都会分配一个新的线程来处理,这样的好处就是每个socket的请求相互之间不受影响,每个请求的业务逻辑相互之间也不影响。任何socket的读写操作都不会影响到后面的请求。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"缺点:不是每个链接都有请求发生,这样就浪费了很多的线程资源。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这个时候可以采用多路复用IO模型的方式来处理IO事件,使用Reactor将响应IO事件和业务处理分开,一个或多个线程来处理IO事件,然后将就绪得到事件分发到业务处理handlers线程去异步非阻塞处理。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.2 Reactor模式","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.3 单线程Reactor模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"什么是单线程Reactor模式,单线程模式采用一个Reactor线程来【处理套接字、新连接的创建】,并且【将接收到的请求分发到处理器Handler中】。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如图2.2为简单的单线程Reactor模式示意图,Reactor和数据处理(handler)都在一个线程里,图2.2参考doug lea论文《Scalable IO in Java》论文。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/ec/ec7cbbb70f816ec0560c52bedb81ad51.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"图2.2单线程Reactor模式示意图","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"单Reactor多线程模式:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a0/a01bc4aeca6368fea48f50486608602e.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"2.4 多线程Reactor模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"多线程reactor模式的设计思想就是将handler线程放入到线程次中,在多核的情况下也可以考虑多个Selector选择器来处理事件,如图2.3为简单的多线程Reactor示意图;","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/d2/d2f9d6387e7401498c111b0fd8ae6671.webp","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"图2.3多线程Reactor模式示意图","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"关于C/C++ Linux后端开发网络底层原理知识 点击 ","attrs":{}},{"type":"link","attrs":{"href":"https://jq.qq.com/?_wv=1027&k=9w0IMi5B","title":null,"type":null},"content":[{"type":"text","text":"学习资料","attrs":{}}]},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":" 获取,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b9/b963efffe2f0a8d97a4bcbc35fd2a599.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"封装Epoll实现并发","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一次学epoll时,容易错误的认为epoll可以实现并发,其实正确的说法是借助epoll可以实现高性能并发服务器,epoll只是提供了IO复用,在IO复用,真正的并发只能通过线程进程实现。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Reactor模式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Reactor模式实现非常简单,使用同步IO模型,即业务线程处理数据需要","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"主动等待或询问","attrs":{}},{"type":"text","text":",主要特点是利用epoll监听listen描述符是否有响应,及时将","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"客户连接","attrs":{}},{"type":"text","text":"信息","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"放于一个队列","attrs":{}},{"type":"text","text":",epoll和队列都是在主进程/线程中,由子进程/线程来接管各个描述符,对描述符进行下一步操作,包括connect和数据读写。主程读写就绪事件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大致流程图如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/b1/b16a62eb5fe94675dd346a71d3c5b3da.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Preactor模式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Preactor模式完全将IO处理和业务分离,使用异步IO模型,即","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"内核完成数据处理后主动通知给应用处理","attrs":{}},{"type":"text","text":",主进程/线程不仅要完成listen任务,还需要完成内核数据缓冲区的映射,直接将数据buff传递给业务线程,业务线程只需要处理业务逻辑即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大致流程如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3f/3f534762e1d42c3b7ee97a382c1bcf85.jpeg","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"封装Epoll实现reactor模式的高性能并发服务器","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"epoll的api","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先介绍epoll的api","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"int epoll_create(int size); // 创建epfd\nint epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //向epfd注册(fd,event)\n\n// epoll_event结构体定义\nstruct epoll_event {\n __uint32_t events; /* epoll 事件 */\n epoll_data_t data; /* 传递的数据,用于处理ready的fd,获得上下文关系 */\n}\n// data联合体,一般用其指针域ptr,因为要从这个data读取到上下文信息\ntypedef union epoll_data {\n void *ptr;\n int fd;\n uint32_t u32;\n uint64_t u64;\n} epoll_data_t;\n\n// op宏\n /* EPOLL_CTL_ADD(注册新的fd到epfd)\n * EPOLL_CTL_MOD(修改已经注册的fd的监听事件)\n * EPOLL_CTL_DEL(epfd删除一个fd)\n */\n*\n * events : {EPOLLIN, EPOLLOUT, EPOLLPRI, \n EPOLLHUP, EPOLLET, EPOLLONESHOT}\n */\n\nint epoll_wait(int epfd, struct epoll_event *event, \n int maxevents, int timeout); //用于轮询注册的fd,若满足相应的注册事件,\n // 则结束epoll_wait阻塞\n/* @param timeout 超时时间\n * -1: 永久阻塞\n * 0: 立即返回,非阻塞\n * >0: 指定微秒\n*/","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Reactor模式:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"io模式的历程:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"单线程,一般阻塞->多线程,一般阻塞(一条连接一线程)->线程池(减少线程创建销毁开销)->reactor(更小粒度的线程)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所谓更小的粒度的线程是指,传统的多线程是一个连接一个线程,粒度太大,比如可以把一个连接继续细分成三个步骤:read,process,send三个步骤,每个步骤占一个线程,处理完后交给主线程调度,进入下一个处理模块","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"EPOLL实现的要点","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"0. 创建epoll_fd = epoll_create(MAX_EVENT+1)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. 维护event数组","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.1 一个交给epoll_wait维护(空的放进去,ready的出来)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.1.1 特定的结构体 struct epoll_event","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.2 一个自己维护(维持对所有注册事件的监控)(全局)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.2.1 由于自己维护,想怎么写怎么写,一般的会让epoll_event.data.ptr = myevent,以作为回调函数的参数,保持一个上下文关系","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2. 创建并初始化事件(epoll_event)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.1 结构体有两个字段 event和data , event是EPOLLIN这样的宏,data的作用是记录一些消息,这样ready的时候可以访问这个消息,比如fd, 回调函数指针,status等等","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3. 向epfd注册事件","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.1 epoll_ctl(epfd , op_macro , target_fd , &epoll_event )","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.2 op_macro是代表epoll_ctl的类型,诸如EPOLL_CTL_ADD","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3.3 target_fd是要监听的fd,比如tcp监听套接字ls_socket,epoll_event就是当事件发生时,epoll_wait()里面 event[]将会出现的结构体","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4. 写回调函数","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.1 这是reactor模式的重点.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.2 典型的,检测到ls_socket可读后(epoll_wait不再阻塞),进入回调函数(通过event[i].data.ptr->callback),假如命名为accept_fn(),在这个函数简单来看要做的事就是","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1.conn_socket = accept(ls_socket)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2.创建,初始化epoll_event并注册到epfd(当然还要加进自己维护的fd列表),","attrs":{}},{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"由于epoll是轮询的模式,需要将conn_socket用fcntl设为O_NONBLOCK,非阻塞.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.3 这个不像select,需要每次重新加入fd到列表里面,注册一次即可","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.4 conn_socket被通过事件EPOLLIN注册到epfd,下一步马上的,epoll_wait检测出conn_socket可读(假设确实可读),然后回调进入recvdata函数(需要在创建并初始化epoll_","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"event指定, 实际上是自定义一个结构体,让data.ptr指向这个结构体就行),一般的,就以这个结构体组成自己维护的fd list,","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"4.5 recvdata函数首先","attrs":{}},{"type":"text","text":"调用recv(fd,my_event->buf,...),把待发送的数据存在my_event->buf , 然后修改(fd,my","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"event)到epfd为发送事件EPOLLOUT,以及改变回调函数为callback_send","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4.6 来到epoll_wait,检测到该fd可写,则回调进入senddata函数,该函数调用send(fd,my","attrs":{}},{"type":"text","marks":[{"type":"italic","attrs":{}}],"text":"event->buf)发送数据,然后修改fd的注册事件为EPOLLIN(即EPOLL_CTLMOD),清空my_","attrs":{}},{"type":"text","text":"event->buf....如此反复","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意点:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1. callback函数如何获得my_event? 我们在epoll_event中能获得event和data,在data.ptr中找到my_event,典型的my_event可能包括回调函数指针,event_type,fd,buf[BUFLEN],last_active_time,...","attrs":{}}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"nfd = epoll_wait(epfd , epoll_events , MAX_EVENT+1 , 1000)\nassert(nfd>0)\nfor i in range(nfd):\n my_event* ev = (my_event*) epoll_events[i].data.ptr\n // if (epoll_events[i].events& EPOLLIN){} 用按位与的方式检测相等,因为这种flag宏一般都是位不相同的\n // 用回调函数的方法时,不需要比较event_type,直接调用函数即可\n // ev->callback_fn(ev) 不可以 callback_fn的原型应该是void(*callvback_fn)(void*),\n // 因为定义callback原型的时候,ev还是不完整的类型\n ev->callback_fn((void*)ev)","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章