Swoole源代码学习记录(十二)——ReactorThread模块
Swoole版本号:1.7.5-stable
Github地址:https://github.com/LinkedDestiny/swoole-src-analysis
这一章将分析Swoole的ReactorThread模块。尽管叫Thread。可是实际上使用的是swFactoryProcess也就是多进程模式。可是。在ReactorThread中。全部的事件监听是在线程中执行的(Rango仅仅是简单提到了PHP不支持多线程安全,详细原因还有待请教……),比方在UDP模式下,是针对每个监听的host开辟一个线程执行reactor。在TCP模式下,则是开启指定的reactor_num个线程用于执行reactor。
那么OK,先上swReactorThread结构体。该结构体封装的事实上是一个执行着Reactor的线程Thread的相关信息,其声明在Server.h文件的104 – 112行。例如以下:
typedef struct _swReactorThread
{
pthread_t thread_id;
swReactor reactor;
swUdpFd *udp_addrs;
swMemoryPool *buffer_input;
swArray *buffer_pipe;
int c_udp_fd;
} swReactorThread;
当中thread_id为ReactorThread的id,udp_addrs和c_udp_fd专门用于处理udp请求,buffer_input为RingBuffer。用于开启了RingBuffer选项的处理,buffer_pipe用于存放来自管道的数据
还有一个结构体用来封装须要传递给Thread的參数,其声明在swoole.h的575 - 579行,例如以下:
typedef struct _swThreadParam
{
void *object;
int pti;
} swThreadParam;
第一个void*指针指向了參数内容的地址。第二个參数标记线程的id(不是pid)
ReactorThread在Server.h中一共同拥有……嗯……12个函数……声明在Server.h的555 - 568行。 这里分下类,当中3个函数用于创建、启动、释放。归为操作类函数。7个函数用于回调操作。归为回调函数,剩下2个用于发送数据,归为发送函数。
首先看3个操作类函数,这三个函数的声明例如以下:
int swReactorThread_create(swServer *serv);
int swReactorThread_start(swServer *serv, swReactor *main_reactor_ptr);
void swReactorThread_free(swServer *serv);
首先是swReactorThread_create函数。该函数实质上并非创建一个ReactorThread,而是初始化swServer中相关变量,并创建对应的Factory。以下上核心源代码:
serv->reactor_threads = SwooleG.memory_pool->alloc(SwooleG.memory_pool, (serv->reactor_num * sizeof(swReactorThread)));
if (serv->reactor_threads == NULL)
{
swError("calloc[reactor_threads] fail.alloc_size=%d", (int )(serv->reactor_num * sizeof(swReactorThread)));
return SW_ERR;
} #ifdef SW_USE_RINGBUFFER
int i;
for (i = 0; i < serv->reactor_num; i++)
{
serv->reactor_threads[i].buffer_input = swRingBuffer_new(SwooleG.serv->buffer_input_size, 1);
if (!serv->reactor_threads[i].buffer_input)
{
return SW_ERR;
}
}
#endif serv->connection_list = sw_shm_calloc(serv->max_connection, sizeof(swConnection));
if (serv->connection_list == NULL)
{
swError("calloc[1] failed");
return SW_ERR;
}
源代码解释:初始化执行reactor的线程池,假设指定使用了RingBuffer,则将reactor_threads里的输入缓存区的类型设置为RingBuffer。随后。在共享内存中初始化connectoin_list连接列表的内存空间。
//create factry object
if (serv->factory_mode == SW_MODE_THREAD)
{
if (serv->writer_num < 1)
{
swError("Fatal Error: serv->writer_num < 1");
return SW_ERR;
}
ret = swFactoryThread_create(&(serv->factory), serv->writer_num);
}
else if (serv->factory_mode == SW_MODE_PROCESS)
{
if (serv->writer_num < 1 || serv->worker_num < 1)
{
swError("Fatal Error: serv->writer_num < 1 or serv->worker_num < 1");
return SW_ERR;
}
ret = swFactoryProcess_create(&(serv->factory), serv->writer_num, serv->worker_num);
}
else
{
ret = swFactory_create(&(serv->factory));
}
源代码解释:推断swServer的factory_mode。
假设为SW_MODE_THREAD(线程模式),则创建FactoryThread。假设为SW_MODE_PROCESS(进程模式),则创建FactoryProcess。否则,为SW_MODE_BASE(基础模式)。创建Factory。
创建完后,就须要启动了。swReactorThread_start函数倒是真的用于启动ReactorThread了……核心源代码例如以下:
if (serv->have_udp_sock == 1)
{
if (swUDPThread_start(serv) < 0)
{
swError("udp thread start failed.");
return SW_ERR;
}
} //listen TCP
if (serv->have_tcp_sock == 1)
{
//listen server socket
ret = swServer_listen(serv, main_reactor_ptr);
if (ret < 0)
{
return SW_ERR;
}
//create reactor thread
for (i = 0; i < serv->reactor_num; i++)
{
thread = &(serv->reactor_threads[i]);
param = SwooleG.memory_pool->alloc(SwooleG.memory_pool, sizeof(swThreadParam));
if (param == NULL)
{
swError("malloc failed");
return SW_ERR;
} param->object = serv;
param->pti = i; if (pthread_create(&pidt, NULL, (void * (*)(void *)) swReactorThread_loop_tcp, (void *) param) < 0)
{
swError("pthread_create[tcp_reactor] failed. Error: %s[%d]", strerror(errno), errno);
}
thread->thread_id = pidt;
}
} //timer
if (SwooleG.timer.fd > 0)
{
main_reactor_ptr->add(main_reactor_ptr, SwooleG.timer.fd, SW_FD_TIMER);
}
源代码解释:假设swServer须要监听UDP,则调用swUDPThread_start函数启动UDP监听线程;假设swServer须要监听TCP,首先调用swServer_listen函数在main_reactor中注冊accept监听,然后创建reactor_num个reactor执行线程用于监听TCP连接的其它事件(读、写)。
最后。假设使用了Timer,则将Timer的fd监听增加到main_reactor中。
swReactorThread_free函数用于释放所有的正在执行的线程以及相关资源。核心源代码例如以下:
if (serv->have_tcp_sock == 1)
{
//create reactor thread
for (i = 0; i < serv->reactor_num; i++)
{
thread = &(serv->reactor_threads[i]);
if (pthread_join(thread->thread_id, NULL))
{
swWarn("pthread_join() failed. Error: %s[%d]", strerror(errno), errno);
} for (j = 0; j < serv->worker_num; j++)
{
swWorker *worker = swServer_get_worker(serv, i);
swBuffer *buffer = *(swBuffer **) swArray_fetch(thread->buffer_pipe, worker->pipe_master);
swBuffer_free(buffer);
}
swArray_free(thread->buffer_pipe); #ifdef SW_USE_RINGBUFFER
thread->buffer_input->destroy(thread->buffer_input);
#endif
}
} if (serv->have_udp_sock == 1)
{
swListenList_node *listen_host;
LL_FOREACH(serv->listen_list, listen_host)
{
shutdown(listen_host->sock, SHUT_RDWR);
if (listen_host->type == SW_SOCK_UDP || listen_host->type == SW_SOCK_UDP6 || listen_host->type == SW_SOCK_UNIX_DGRAM)
{
if (pthread_join(listen_host->thread_id, NULL))
{
swWarn("pthread_join() failed. Error: %s[%d]", strerror(errno), errno);
}
}
}
}
源代码解释:假设使用了TCP,则遍历所有的reactor_thread,并调用pthread_join函数结束线程,并释放线程中用于管道通信的缓存区。假设使用了RingBuffer,还须要释放buffer_input输入缓存。假设使用了UDP。则首先遍历监听列表,使用shutdown终止连接,然后调用pthread_join函数结束线程。
接着先看发送函数。一共两个发送函数,一个用于发送数据到client客户端或者输出buffer,一个用于发送数据到worker进程。两个函数的声明例如以下:
int swReactorThread_send(swSendData *_send);
int swReactorThread_send2worker(void *data, int len, uint16_t target_worker_id);
swReactorThread_send函数用于发送数据到client,也就是通过swConnection发送数据,其核心源代码例如以下:
volatile swBuffer_trunk *trunk;
swConnection *conn = swServer_connection_get(serv, fd); if (conn == NULL || conn->active == 0)
{
swWarn("Connection[fd=%d] is not exists.", fd);
return SW_ERR;
} #if SW_REACTOR_SCHEDULE == 2
reactor_id = fd % serv->reactor_num;
#else
reactor_id = conn->from_id;
#endif swTraceLog(SW_TRACE_EVENT, "send-data. fd=%d|reactor_id=%d", fd, reactor_id);
swReactor *reactor = &(serv->reactor_threads[reactor_id].reactor);
源代码解释:假设不是直传。则须要现将数据放入缓存。首先创建connection的out_buffer输出缓存,假设发送数据长度为0,则指定缓存的trunk类型为SW_TRUNK_CLOSE(关闭连接),假设发送数据的类型为sendfile,则调用swConnection_sendfile函数,否则调用swBuffer_append函数将发送数据增加缓存中。最后,在reactor中设置fd为可写状态。
swReactorThread_send2worker函数在此不再贴源代码分析。基本思路就是消息队列模式就扔队列不是消息队列模式就扔缓存或者直接扔管道……应该都看得懂了。
这里直接上数量最多的回调函数的分析。这几个回调式用于处理接收数据的,从名字上大家基本能看出,Swoole提供的一些特性比方包长检測、eof检測还有UDP的报文接收都是通过这些不同的回调来实现的。
首先来看swReactorThread_onReceive_no_buffer函数,这个是最主要的接收函数,没有缓存,没有检測,收到多少数据就发给worker多少数据。以下上核心源代码:
#ifdef SW_USE_EPOLLET
n = swRead(event->fd, task.data.data, SW_BUFFER_SIZE);
#else
//非ET模式会持续通知
n = swConnection_recv(conn, task.data.data, SW_BUFFER_SIZE, 0);
#endif if (n < 0)
{
switch (swConnection_error(errno))
{
case SW_ERROR:
swWarn("recv from connection[fd=%d] failed. Error: %s[%d]", event->fd, strerror(errno), errno);
return SW_OK;
case SW_CLOSE:
goto close_fd;
default:
return SW_OK;
}
}
//须要检測errno来区分是EAGAIN还是ECONNRESET
else if (n == 0)
{
close_fd:
swTrace("Close Event.FD=%d|From=%d|errno=%d", event->fd, event->from_id, errno);
swServer_connection_close(serv, event->fd, 1);
/**
* skip EPOLLERR
*/
event->fd = 0;
return SW_OK;
}
源代码解释:假设使用epoll的ET模式。则调用swRead函数直接从fd中读取数据。否则,在非ET模式下,调用swConnection_recv函数接收数据。假设接收数据失败,则依据errno运行相应的操作,假设接收数据为0,须要关闭连接,调用swServer_connection_close函数关闭fd。
conn->last_time = SwooleGS->now; //heartbeat ping package
if (serv->heartbeat_ping_length == n)
{
if (serv->heartbeat_pong_length > 0)
{
send(event->fd, serv->heartbeat_pong, serv->heartbeat_pong_length, 0);
}
return SW_OK;
} task.data.info.fd = event->fd;
task.data.info.from_id = event->from_id;
task.data.info.len = n; #ifdef SW_USE_RINGBUFFER uint16_t target_worker_id = swServer_worker_schedule(serv, conn->fd);
swPackage package; package.length = task.data.info.len;
package.data = swReactorThread_alloc(&serv->reactor_threads[SwooleTG.id], package.length);
task.data.info.type = SW_EVENT_PACKAGE; memcpy(package.data, task.data.data, task.data.info.len);
task.data.info.len = sizeof(package);
task.target_worker_id = target_worker_id;
memcpy(task.data.data, &package, sizeof(package)); #else
task.data.info.type = SW_EVENT_TCP;
task.target_worker_id = -1;
#endif //dispatch to worker process
ret = factory->dispatch(factory, &task); #ifdef SW_USE_EPOLLET
//缓存区还有数据没读完,继续读。EPOLL的ET模式
if (sw_errno == EAGAIN)
{
swWarn("sw_errno == EAGAIN");
ret = swReactorThread_onReceive_no_buffer(reactor, event);
}
#endif
源代码解释:首先更新近期收包的时间。随后,检測是否是心跳包。假设接收长度等于心跳包的长度而且指定了发送心跳回应,则发送心跳包并返回。假设不是心跳包,则设置接收数据的fd、reactor_id以及长度。假设指定使用了RingBuffer,则须要将数据封装到swPackage中然后放进ReactorThread的input_buffer中。随后调用factory的dispatch方法将数据投递到相应的worker中。最后。假设是LT模式,而且缓存区的数据还没读完。则继续调用swReactorThread_onReceive_no_buffer函数读取数据。
接下来是swReactorThread_onReceive_buffer_check_length函数。该函数用于接收开启了包长检測的数据包。包长检測是Swoole用于支持固定包头+包体的自己定义协议的特性,当然有不少小伙伴不理解怎么使用这个特性……以下上核心源代码:
//new package
if (conn->object == NULL)
{
do_parse_package:
do
{
package_total_length = swReactorThread_get_package_length(serv, (void *)tmp_ptr, (uint32_t) tmp_n); //Invalid package, close connection
if (package_total_length < 0)
{
goto close_fd;
}
//no package_length
else if(package_total_length == 0)
{
char recv_buf_again[SW_BUFFER_SIZE];
memcpy(recv_buf_again, (void *) tmp_ptr, (uint32_t) tmp_n);
do
{
//前tmp_n个字节存放不完整包头
n = recv(event->fd, (void *)recv_buf_again + tmp_n, SW_BUFFER_SIZE, 0);
try_count ++; //连续5次尝试补齐包头,认定为恶意请求
if (try_count > 5)
{
swWarn("No package head. Close connection.");
goto close_fd;
}
}
while(n < 0 && errno == EINTR); if (n == 0)
{
goto close_fd;
}
tmp_ptr = recv_buf_again;
tmp_n = tmp_n + n; goto do_parse_package;
}
//complete package
if (package_total_length <= tmp_n)
{
tmp_package.size = package_total_length;
tmp_package.length = package_total_length;
tmp_package.str = (void *) tmp_ptr; //swoole_dump_bin(buffer.str, 's', buffer.length);
swReactorThread_send_string_buffer(swServer_get_thread(serv, SwooleTG.id), conn, &tmp_package); tmp_ptr += package_total_length;
tmp_n -= package_total_length;
continue;
}
//wait more data
else
{
if (package_total_length >= serv->package_max_length)
{
swWarn("Package length more than the maximum size[%d], Close connection.", serv->package_max_length);
goto close_fd;
}
package = swString_new(package_total_length);
if (package == NULL)
{
return SW_ERR;
}
memcpy(package->str, (void *)tmp_ptr, (uint32_t) tmp_n);
package->length += tmp_n;
conn->object = (void *) package;
break;
}
}
while(tmp_n > 0);
return SW_OK;
}
源代码解释:假设connection连接中没有缓存数据,则判定为新的数据包,进入接收循环。在接收循环中。首先从数据缓存中尝试获取包体的长度(这个长度存在包头中),假设长度小于0,说明这个数据包不合法,丢弃并关闭连接。假设长度等于0,说明包头还没接收完整,继续接收数据。假设连续5次补全包头失败,则认定为恶意请求,关闭连接;假设长度大于0而且已经接收的数据长度超过或等于包体长度,则说明已经收到一个完整的数据包。通过swReactorThread_send_string_buffer函数将数据包放入缓存;假设已接收数据长度小于包体长度。则将不完整的数据包放入connection的object域。等待下一批数据。
else
{
package = conn->object;
//swTraceLog(40, "wait_data, size=%d, length=%d", buffer->size, buffer->length); /**
* Also on the require_n byte data is complete.
*/
int require_n = package->size - package->length; /**
* Data is not complete, continue to wait
*/
if (require_n > n)
{
memcpy(package->str + package->length, recv_buf, n);
package->length += n;
return SW_OK;
}
else
{
memcpy(package->str + package->length, recv_buf, require_n);
package->length += require_n;
swReactorThread_send_string_buffer(swServer_get_thread(serv, SwooleTG.id), conn, package);
swString_free((swString *) package);
conn->object = NULL; /**
* Still have the data, to parse.
*/
if (n - require_n > 0)
{
tmp_n = n - require_n;
tmp_ptr = recv_buf + require_n;
goto do_parse_package;
}
}
}
源代码解释:假设connecton的object已经存有数据,则先推断这个数据包还剩下多少个字节未接受,并用当前接收的数据补全数据包。假设数据不够。则继续等待下一批数据;假设数据够。则补全数据包并将数据包发送到缓存中,并清空connection的object域。
假设在补全数据包后仍有剩余数据,则開始下一次数据包的解析。
接下来分析swReactorThread_onReceive_buffer_check_eof函数。这个函数用于检測用户定义的数据包的切割符,用于解决TCP长连接发送数据的粘包问题,保证onReceive回调每次拿到的是一个完整的数据包。
以下是核心源代码:
//读满buffer了,可能还有数据
if ((buffer->trunk_size - trunk->length) == n)
{
recv_again = SW_TRUE;
} trunk->length += n;
buffer->length += n; //over max length, will discard
//TODO write to tmp file.
if (buffer->length > serv->package_max_length)
{
swWarn("Package is too big. package_length=%d", buffer->length);
goto close_fd;
} // printf("buffer[len=%d][n=%d]-----------------\n", trunk->length, n);
//((char *)trunk->data)[trunk->length] = 0; //for printf
// printf("buffer-----------------: %s|fd=%d|len=%d\n", (char *) trunk->data, event->fd, trunk->length); //EOF_Check
isEOF = memcmp(trunk->store.ptr + trunk->length - serv->package_eof_len, serv->package_eof, serv->package_eof_len);
// printf("buffer ok. EOF=%s|Len=%d|RecvEOF=%s|isEOF=%d\n", serv->package_eof, serv->package_eof_len, (char *)trunk->data + trunk->length - serv->package_eof_len, isEOF); //received EOF, will send package to worker
if (isEOF == 0)
{
swReactorThread_send_in_buffer(swServer_get_thread(serv, SwooleTG.id), conn);
return SW_OK;
}
else if (recv_again)
{
trunk = swBuffer_new_trunk(buffer, SW_TRUNK_DATA, buffer->trunk_size);
if (trunk)
{
goto recv_data;
}
}
源代码解释:这里使用了connection的in_buffer输入缓存。首先判定trunk的剩余空间,假设trunk已经满了,此时可能还有数据,则须要新开一个trunk接收数据。因此设定recv_again标签为true。随后判定已经接收的数据长度是否超过了最大包长。假设超过,则丢弃数据包(这里能够看到TODO标签,Rango将在后期把超过包长的数据写入tmp文件里)。
随后判定EOF。将数据末尾的长度为eof_len的字符串和指定的eof字符串对照,假设相等,则将数据包发送到缓存区,假设不相等,而recv_again标签为true。则新开trunk用于接收数据。
这里须要说明,Rango仅仅是简单判定了数据包末尾是否为eof。而不是在已经接收到的字符串中去匹配eof,因此并不能非常准确的依据eof来拆分数据包。所以各位假设希望能准确解决粘包问题,还是使用固定包头+包体这样的协议格式较好。
剩下的几个回调函数都较为简单。有兴趣的读者能够依据之前的分析自己尝试解读一下这几个函数。
至此,ReactorThread模块已所有解析完毕。能够看出。ReactorThread模块主要在处理连接接收到的数据,并把这些数据投递到相应的缓存中交由Worker去处理。因此能够理出一个主要的结构: Reactor响应fd请求->ReactorThread接收数据并放入缓存->ReactorFactory将数据从缓存取出发送给Worker->Worker处理数据。
Swoole源代码学习记录(十二)——ReactorThread模块的更多相关文章
- python学习(十二)模块
怎么一下子就来学了模块? 其实学了判断.循环.函数等知识就可以开始下水写程序了,不用在意其他的细节,等你用到的时候再回过头去看,此所谓囫囵吞枣学习法. 为啥学模块? 有点用的.或者有点规模的程序都是要 ...
- Swoole源代码学习记录(十五)——Timer模块分析
swoole版本号:1.7.7-stable Github地址:点此查看 1.Timer 1.1.swTimer_interval_node 声明: // swoole.h 1045-1050h ty ...
- Swoole源代码学习记录(十三)——Server模块具体解释(上)
Swoole版本号:1.7.5-stable Github地址:https://github.com/LinkedDestiny/swoole-src-analysis 最终能够正式进入Server. ...
- Spring学习记录(十二)---AOP理解和基于注解配置
Spring核心之二:AOP(Aspect Oriented Programming) --- 面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术.AOP是OOP的延续,是软 ...
- 【Python】学习笔记十二:模块
模块(module) 在Python中,一个.py文件就是一个模块.通过模块,你可以调用其它文件中的程序 引入模块 先写一个first.py文件,内容如下: def letter(): print(' ...
- Lua和C++交互 学习记录之二:栈操作
主要内容转载自:子龙山人博客(强烈建议去子龙山人博客完全学习一遍) 部分内容查阅自:<Lua 5.3 参考手册>中文版 译者 云风 制作 Kavcc vs2013+lua-5.3.3 1 ...
- python3.4学习笔记(十二) python正则表达式的使用,使用pyspider匹配输出带.html结尾的URL
python3.4学习笔记(十二) python正则表达式的使用,使用pyspider匹配输出带.html结尾的URL实战例子:使用pyspider匹配输出带.html结尾的URL:@config(a ...
- Go语言学习笔记十二: 范围(Range)
Go语言学习笔记十二: 范围(Range) rang这个关键字主要用来遍历数组,切片,通道或Map.在数组和切片中返回索引值,在Map中返回key. 这个特别像python的方式.不过写法上比较怪异使 ...
- Tensorflow深度学习之十二:基础图像处理之二
Tensorflow深度学习之十二:基础图像处理之二 from:https://blog.csdn.net/davincil/article/details/76598474 首先放出原始图像: ...
随机推荐
- centos语言设置
. echo $LANG可以查看当前使用的系统语言 . 在终端输入 locale命令,如有zh cn 表示已经安装了中文 . 安装中文语言包yum groupinstall chinese-suppo ...
- ACM_整数反转
整数反转 Time Limit: 2000/1000ms (Java/Others) Problem Description: 给定一个32位int型的整数,把这个整数反着输出,如123,输出321. ...
- html表单——使用frameset写一个导航栏效果
主页面: <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4 ...
- UNIX环境高级编程--7
进程环境main函数: C程序总是从main函数开始执行.main函数原型是: int main(int argc, char *argv[]); 当内核执行C程序时(使用一个exe ...
- Spring Boot (24) 使用Spring Cache集成Redis
Spring 3.1引入了基于注解(annotation)的缓存(cache)技术,它本质不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的个助攻annotatio ...
- InputStream和Reader
java.io下面有两个抽象类:InputStream和ReaderInputStream是表示字节输入流的所有类的超类Reader是用于读取字符流的抽象类InputStream提供的是字节流的读取, ...
- suse 下的gcc安装
在付出了一天的努力之后终于在win7系统上面硬盘安装suse操作系统成功,可是随之而来的问题居然是没有安装GCC,这对我来说是一个不小的打击,因为很多工作和工具安装需要通过GCC来编译,因此我只好求助 ...
- html5——边框
精确控制 /*水平半径 垂直半径;*/ border-top-left-radius: 30px 40px; border-top-right-radius: 30px 40px; border-bo ...
- 回顾Google IO 2016 -Keynote【图解】
Google IO大会倒计时进行中~~ 两名演奏者在使用高空“古筝”. 最后5秒倒计时~~~~全场轰动~ 倒计时结束,IO大会正式开始.屏幕中,一个人把纯白的唱片放入唱片机中. 然后欢快的音乐响起,台 ...
- DECLARE_DYNAMIC
DECLARE_DYNAMIC(class_name) DECLARE_DYNCREATE 包含了DECLARE_DYNAMIC的功能,并且可以在运行过程中动态创建对象.如果需要动态创建类对象,需要使 ...