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 首先放出原始图像: ...
随机推荐
- 题解报告:hdu 1564 Play a game(找规律博弈)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1564 Problem Description New Year is Coming! ailyanlu ...
- cocos2dx实现单机版三国杀(二)
接上续,东西还没有做完 所以代码免不了改动 之前的头文件现在又改了不少,因为架构也改变了现在主要类就是GameScene.GameUI.PlayInfo.Poker这四个类 前面想的GameLoop ...
- EasyUI系列学习(三)-Draggable(拖动)
一.创建拖动组件 0.Draggable组件不依赖于其他组件 1.使用标签创建 <div class="easyui-draggable" id="box" ...
- jquery ajax在IE9以下进行跨域请求时无效的问题
第一步:设置浏览器安全属性,启用[通过域访问数据源]选项: 1.选择Internet选项 2.选择安全---自定义级别 3.找到其他---通过域访问数据源,选择启用,然后确定就可以了. 第二步:调用a ...
- Echarts 出现不明竖线解决方案
Echarts出现了不明竖线,百思不得其解.去查相应的解决方案也没有找到. 后来自己点来点去,突然感觉像是上一个Echarts遗留的. 然后去Echarts官网看到了 clear()方法,这个方法可以 ...
- 新浪云虚拟机ftp链接显示失败问题
新浪云虚拟机ftp链接显示失败问题 测试是在局域网遇到的 域名解析可以ping有字节回复 账号密码也没有错误,但是链接一直出现 连接失败 拒接连接等问题 解决办法: 其实是局域网内的问题,这 ...
- C#——工厂模式
之前我们接介绍了简单工厂,这次我们介绍一种更为常用的模式——工厂模式. 工厂方法模式Factory Method,又称多态性工厂模式.在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体 ...
- [Windows Server 2008] Ecshop安全设置
★ 欢迎来到[护卫神·V课堂],网站地址:http://v.huweishen.com ★ 护卫神·V课堂 是护卫神旗下专业提供服务器教学视频的网站,每周更新视频. ★ 本节我们将带领大家:ECSHO ...
- hibernate工作流程、session
hibernate是对jdbc的封装,不建议直接使用jdbc的connection操作数据库,而是通过session操作数据库.session可以理解为操作数据库的对象. session与connec ...
- centos7 安装zabbix3.4
1 打开yum安装rpm包,自动存放下载的rpm包 下次安装时,如果没有网可以自己制作yum源 打开文件 [root@localhost etc]# vim /etc/yum.conf keepcac ...