网络程序需要处理的第三类事件是定时事件,比如定期检测一个客户连接的活动状态。服务器程序通常管理着众多定时事件,因此有效地组织这些定时事件,使之能在预期的时间点被触发且不影响服务器的主要逻辑,对于服务器的性能有着至关重要的影响。为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构,比如链表、排序链表和时间轮,时间堆将所有定时器串联起来。

不过,在讨论如何组织定时器之前,我们先要介绍定时的方法。定时是指在一段时间之后触发某段代码的机制,我们可以在这段代码中依次处理所有到期的定时器。换言之,定时机制是定时器得以被处理的原动力。Linux 提供了三种定时方法,它们是:

  • socket选项SO_RCVTIMEO和SO_SNDTIMEO。
  • SIGALRM信号。
  • I/O复用系统调用的超时参数。

socket选项SO_RCVTIMEO和SO_SNDTIMEO

socket选项SO_RCVTIMEO 和SO_SNDTIMEO,它们分别用来设置socket接收数据超时时间和发送数据超时时间。因此,这两个选项仅对与数据接收和发送相关的socket专用系统调用有效,这些系统调用包括send、sendmsg、 recv、 recvmsg、accept 和connect.我们将选项SO_RCVTIMEO和SO_SNDTIMEO对这些系统调用的影响总结于表中。

由表可见,在程序中,我们可以根据系统调用(send、 sendmsg、 recv、recvmsg、accept和connect)的返回值以及errno来判断超时时间是否已到,进而决定是否开始处理定时任务。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>int timeout_connect( const char* ip, int port, int time )
{int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;inet_pton( AF_INET, ip, &address.sin_addr );address.sin_port = htons( port );int sockfd = socket( PF_INET, SOCK_STREAM, 0 );assert( sockfd >= 0 );struct timeval timeout;timeout.tv_sec = time;timeout.tv_usec = 0;socklen_t len = sizeof( timeout );ret = setsockopt( sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len );assert( ret != -1 );ret = connect( sockfd, ( struct sockaddr* )&address, sizeof( address ) );if ( ret == -1 ){if( errno == EINPROGRESS )    //超时对应的错误,成立就可以处理定时任务了{printf( "connecting timeout\n" );return -1;}printf( "error occur when connecting to server\n" );return -1;}return sockfd;
}int main( int argc, char* argv[] )
{if( argc <= 2 ){printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );return 1;}const char* ip = argv[1];int port = atoi( argv[2] );int sockfd = timeout_connect( ip, port, 10 );if ( sockfd < 0 ){return 1;}return 0;
}

SIGALRM信号

由alarm和setitimer函数设置的实时闹钟一旦超时,将触发SIGALRM信号。因此,我们可以利用该信号的信号处理函数来处理定时任务。但是,如果要处理多个定时任务,我们就需要不断地触发SIGALRM信号,并在其信号处理函数中执行到期的任务。一般而言,SIGALRM信号按照固定的频率生成,即由alarm或setitimer函数设置的定时周期T保持不变。如果某个定时任务的超时时间不是T的整数倍,那么它实际被执行的时间和预期的时间将略有偏差。因此定时周期T反映了定时的精度。

#ifndef LST_TIMER
#define LST_TIMER#include <time.h>
#include <netinet/in.h>#define BUFFER_SIZE 64
class util_timer;
struct client_data
{sockaddr_in address;int sockfd;char buf[BUFFER_SIZE];util_timer *timer;
};class util_timer
{public:util_timer() : prev(NULL), next(NULL) {}public:time_t expire;void (*cb_func)(client_data *);client_data *user_data;util_timer *prev;util_timer *next;
};class sort_timer_lst
{public:sort_timer_lst() : head(NULL), tail(NULL) {}~sort_timer_lst(){util_timer *tmp = head;while (tmp){head = tmp->next;delete tmp;tmp = head;}}void add_timer(util_timer *timer){if (!timer){return;}if (!head){head = tail = timer;return;}if (timer->expire < head->expire){timer->next = head;head->prev = timer;head = timer;return;}add_timer(timer, head);}void adjust_timer(util_timer *timer){if (!timer){return;}util_timer *tmp = timer->next;if (!tmp || (timer->expire < tmp->expire)){return;}if (timer == head){head = head->next;head->prev = NULL;timer->next = NULL;add_timer(timer, head);}else{timer->prev->next = timer->next;timer->next->prev = timer->prev;add_timer(timer, timer->next);}}void del_timer(util_timer *timer){if (!timer){return;}if ((timer == head) && (timer == tail)){delete timer;head = NULL;tail = NULL;return;}if (timer == head){head = head->next;head->prev = NULL;delete timer;return;}if (timer == tail){tail = tail->prev;tail->next = NULL;delete timer;return;}timer->prev->next = timer->next;timer->next->prev = timer->prev;delete timer;}void tick(){if (!head){return;}printf("timer tick\n");time_t cur = time(NULL);util_timer *tmp = head;while (tmp){if (cur < tmp->expire){break;}tmp->cb_func(tmp->user_data);head = tmp->next;if (head){head->prev = NULL;}delete tmp;tmp = head;}}
private:void add_timer(util_timer *timer, util_timer *lst_head){util_timer *prev = lst_head;util_timer *tmp = prev->next;while (tmp){if (timer->expire < tmp->expire){prev->next = timer;timer->next = tmp;tmp->prev = timer;timer->prev = prev;break;}prev = tmp;tmp = tmp->next;}if (!tmp){prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}}
private:util_timer *head;util_timer *tail;
};
#endif

现在我们考虑上述升序定时器链表的实际应用一处理非活动连接。服务器程序通常要定期处理非活动连接:给客户端发一个重连请求,或者关闭该连接,或者其他。Linux在内核中提供了对连接是否处于活动状态的定期检查机制,我们可以通过socket选项KEEPALIVE来激活它。不过使用这种方式将使得应用程序对连接的管理变得复杂。因此,我们可以考虑在应用层实现类似于KEEPALIVE的机制,以管理所有长时间处于非活动状态的连接。比如利用alarm函数周期性地触发SIGALRM信号,该信号的信号处理函数利用管道通知主循环执行定时器链表上的定时任务一关闭非活动的连接。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "lst_timer.h"#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define TIMESLOT 5static int pipefd[2];
static sort_timer_lst timer_lst;
static int epollfd = 0;int setnonblocking(int fd)
{int old_option = fcntl(fd, F_GETFL);int new_option = old_option | O_NONBLOCK;fcntl(fd, F_SETFL, new_option);return old_option;
}void addfd(int epollfd, int fd)
{epoll_event event;event.data.fd = fd;event.events = EPOLLIN | EPOLLET;epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);setnonblocking(fd);
}void sig_handler(int sig)
{int save_errno = errno;int msg = sig;send(pipefd[1], (char *)&msg, 1, 0);errno = save_errno;
}void addsig(int sig)
{struct sigaction sa;memset(&sa, '\0', sizeof(sa));sa.sa_handler = sig_handler;sa.sa_flags |= SA_RESTART;sigfillset(&sa.sa_mask);assert(sigaction(sig, &sa, NULL) != -1);
}void timer_handler()
{timer_lst.tick();alarm(TIMESLOT);
}void cb_func(client_data *user_data)
{epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);assert(user_data);close(user_data->sockfd);printf("close fd %d\n", user_data->sockfd);
}int main(int argc, char *argv[])
{if (argc <= 2){printf("usage: %s ip_address port_number\n", basename(argv[0]));return 1;}const char *ip = argv[1];int port = atoi(argv[2]);int ret = 0;struct sockaddr_in address;bzero(&address, sizeof(address));address.sin_family = AF_INET;inet_pton(AF_INET, ip, &address.sin_addr);address.sin_port = htons(port);int listenfd = socket(PF_INET, SOCK_STREAM, 0);assert(listenfd >= 0);ret = bind(listenfd, (struct sockaddr *)&address, sizeof(address));assert(ret != -1);ret = listen(listenfd, 5);assert(ret != -1);epoll_event events[MAX_EVENT_NUMBER];int epollfd = epoll_create(5);assert(epollfd != -1);addfd(epollfd, listenfd);ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);assert(ret != -1);setnonblocking(pipefd[1]);addfd(epollfd, pipefd[0]);// add all the interesting signals hereaddsig(SIGALRM);addsig(SIGTERM);bool stop_server = false;client_data *users = new client_data[FD_LIMIT];bool timeout = false;alarm(TIMESLOT);while (!stop_server){int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);if ((number < 0) && (errno != EINTR)){printf("epoll failure\n");break;}for (int i = 0; i < number; i++){int sockfd = events[i].data.fd;if (sockfd == listenfd){struct sockaddr_in client_address;socklen_t client_addrlength = sizeof(client_address);int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);addfd(epollfd, connfd);users[connfd].address = client_address;users[connfd].sockfd = connfd;util_timer *timer = new util_timer;timer->user_data = &users[connfd];timer->cb_func = cb_func;time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;users[connfd].timer = timer;timer_lst.add_timer(timer);}else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)){int sig;char signals[1024];ret = recv(pipefd[0], signals, sizeof(signals), 0);if (ret == -1){// handle the errorcontinue;}else if (ret == 0){continue;}else{for (int i = 0; i < ret; ++i){switch (signals[i]){case SIGALRM:{timeout = true;break;}case SIGTERM:{stop_server = true;}}}}}else if (events[i].events & EPOLLIN){memset(users[sockfd].buf, '\0', BUFFER_SIZE);ret = recv(sockfd, users[sockfd].buf, BUFFER_SIZE - 1, 0);printf("get %d bytes of client data %s from %d\n", ret, users[sockfd].buf, sockfd);util_timer *timer = users[sockfd].timer;if (ret < 0){if (errno != EAGAIN){cb_func(&users[sockfd]);if (timer){timer_lst.del_timer(timer);}}}else if (ret == 0){cb_func(&users[sockfd]);if (timer){timer_lst.del_timer(timer);}}else{//send( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );if (timer){time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;printf("adjust timer once\n");timer_lst.adjust_timer(timer);}}}else{// others}}if (timeout){timer_handler();timeout = false;}}close(listenfd);close(pipefd[1]);close(pipefd[0]);delete[] users;return 0;
}

I/O复用系统调用的超时参数

Linux下的3组I/O复用系统调用都带有超时参数,因此它们不仅能统一处理信号和I/O事件,也能统一处理定时事件。 但是由于I/O复用系统调用可能在超时时间到期之前就返回(有I/O事件发生),所以如果我们要利用它们来定时,就需要不断更新定时参数以反映剩余的时间。

#define TIMEOUT 5000int timeout = TIMEOUT;
time_t start = time(NULL);
time_t end = time(NULL);
while (1)
{printf("the timeout is now %d mill-seconds\n", timeout);start = time(NULL);int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, timeout);if ((number < 0) && (errno != EINTR)){printf("epoll failure\n");break;}//如果epo11_ wait成功返回0,则说明超时时间到,此时便可处理定时任务,并重置定时时间if (number == 0){// timeouttimeout = TIMEOUT;continue;}end = time(NULL);/*如果epol1_ wait的返回值大于0,则本次epoll_ wait调用持续的时间是( end - start )*
1000 ms,我们衢要将定时时间timeout减去这段时间,以获得下次epol1_ wait 调用的超时参数*/timeout -= (end - start) * 1000;/*重新计算之后的timeout值有可能等于0,说明本次epoll_ wait调用返回时,不仅有文件描述符就
绪,而且其超时时间也刚好到达,此时我们也要处理定时任务,并重置定时时间*/if (timeout <= 0){// timeouttimeout = TIMEOUT;}// handle connections
}

时间轮


图所示的时间轮内,(实线) 指针指向轮子上的-一个槽(slot)。 它以恒定的速度顺时针转动,每转动一步就指向下一个槽( 虚线指针指向的槽),每次转动称为一个滴答(tick)。一个滴答的时间称为时间轮的槽间隔si (slot interval), 它实际上就是心搏时间。该时间轮共有N个槽,因此它运转一周的时间是N*si每个槽指向一条定时器链表,每条链表上的定时器具有相同的特征:它们的定时时间相差N*si的整数倍。时间轮正是利用这个关系将定时器散列到不同的链表中。假如现在指针指向槽cs,我们要添加一个定时时间为ti的定时器,则该定时器将被插人槽ts (timer slot)对应的链表中:

ts=(cs+(ti/si))%N

基于排序链表的定时器使用唯一的一条链表来管理所有定时器,所以插人操作的效率随着定时器数目的增多而降低。而时间轮使用哈希表的思想,将定时器散列到不同的链表上。这样每条链表上的定时器数目都将明显少于原来的排序链表上的定时器数目,插入操作的效率基本不受定时器数目的影响。

很显然,对时间轮而言,要提高定时精度,就要使si值足够小:要提高执行效率,则要求N值足够大。

图描述的是一种简单的时间轮,因为它只有一个轮子。而复杂的时间轮可能有多个轮子,不同的轮子拥有不同的粒度。相邻的两个轮子,精度高的转一圈, 精度低的仅往前移动一槽,就像水表一样。

#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER#include <time.h>
#include <netinet/in.h>
#include <stdio.h>#define BUFFER_SIZE 64
class tw_timer;
struct client_data
{sockaddr_in address;int sockfd;char buf[BUFFER_SIZE];tw_timer *timer;
};class tw_timer
{public:tw_timer(int rot, int ts): next(NULL), prev(NULL), rotation(rot), time_slot(ts) {}public:int rotation;  //记录定时器在时间轮转多少圈生效int time_slot; //记录定时器在时间轮上哪个槽void (*cb_func)(client_data *);client_data *user_data;tw_timer *next;tw_timer *prev;
};class time_wheel
{public:time_wheel() : cur_slot(0){for (int i = 0; i < N; ++i){slots[i] = NULL;}}~time_wheel(){for (int i = 0; i < N; ++i){tw_timer *tmp = slots[i];while (tmp){slots[i] = tmp->next;delete tmp;tmp = slots[i];}}}tw_timer *add_timer(int timeout){if (timeout < 0){return NULL;}int ticks = 0;if (timeout < TI){ticks = 1;}else{ticks = timeout / TI;}int rotation = ticks / N;int ts = (cur_slot + (ticks % N)) % N;tw_timer *timer = new tw_timer(rotation, ts);if (!slots[ts]){printf("add timer, rotation is %d, ts is %d, cur_slot is %d\n", rotation, ts, cur_slot);slots[ts] = timer;}else{timer->next = slots[ts];slots[ts]->prev = timer;slots[ts] = timer;}return timer;}void del_timer(tw_timer *timer){if (!timer){return;}int ts = timer->time_slot;if (timer == slots[ts]){slots[ts] = slots[ts]->next;if (slots[ts]){slots[ts]->prev = NULL;}delete timer;}else{timer->prev->next = timer->next;if (timer->next){timer->next->prev = timer->prev;}delete timer;}}void tick(){tw_timer *tmp = slots[cur_slot];printf("current slot is %d\n", cur_slot);while (tmp){printf("tick the timer once\n");if (tmp->rotation > 0){tmp->rotation--;tmp = tmp->next;}else{tmp->cb_func(tmp->user_data);if (tmp == slots[cur_slot]){printf("delete header in cur_slot\n");slots[cur_slot] = tmp->next;delete tmp;if (slots[cur_slot]){slots[cur_slot]->prev = NULL;}tmp = slots[cur_slot];}else{tmp->prev->next = tmp->next;if (tmp->next){tmp->next->prev = tmp->prev;}tw_timer *tmp2 = tmp->next;delete tmp;tmp = tmp2;}}}cur_slot = ++cur_slot % N;}private:static const int N = 60; //槽的个数static const int SI = 1; //1s转动一次tw_timer *slots[N];      //时间轮的槽,其中每个元素指向一个定时器无序 链表int cur_slot;            //时间轮的当前槽
};
#endif

时间堆

前面讨论的定时方案都是以固定的频率调用心搏函数tick,并在其中依次检测到期的定时器,然后执行到期定时器上的回调函数。设计定时器的另外一种思路是:将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样,一旦心搏函数tick被调用,超时时间最小的定时器必然到期,我们就可以在tick函数中处理该定时器。然后,再次从剩余的定时器中找出超时时间最小的一个,并将这段最小时间设置为下一次心搏间隔。如此反复,就实现了较为精确的定时。

树的基本操作是插人节点和删除节点。对最小堆而言,它们都很简单。为了将一个元素X插人最小堆,我们可以在树的下一个空闲位置创建一个空穴。如果X可以放在空穴中而不破坏堆序,则插人完成。否则就执行上虑操作,即交换空穴和它的父节点_上的元素。不断执行上述过程,直到X可以被放人空穴,则插人操作完成。比如,我们要往图所示的最小堆中插入值为14的元素,则可以按照图 所示的步骤来操作。

关于最小堆的性质可以看这篇文章

#ifndef intIME_HEAP
#define intIME_HEAP#include <iostream>
#include <netinet/in.h>
#include <time.h>
using std::exception;#define BUFFER_SIZE 64class heap_timer;
struct client_data
{sockaddr_in address;int sockfd;char buf[BUFFER_SIZE];heap_timer *timer;
};class heap_timer
{public:heap_timer(int delay){expire = time(NULL) + delay;}public:time_t expire;//定时器生成的绝对时间void (*cb_func)(client_data *);client_data *user_data;
};class time_heap
{public:time_heap(int cap) throw(std::exception): capacity(cap), cur_size(0){array = new heap_timer *[capacity];if (!array){throw std::exception();}for (int i = 0; i < capacity; ++i){array[i] = NULL;}}time_heap(heap_timer **init_array, int size, int capacity) throw(std::exception): cur_size(size), capacity(capacity){if (capacity < size){throw std::exception();}array = new heap_timer *[capacity];if (!array){throw std::exception();}for (int i = 0; i < capacity; ++i){array[i] = NULL;}if (size != 0){for (int i = 0; i < size; ++i){array[i] = init_array[i];}for (int i = (cur_size - 1) / 2; i >= 0; --i){percolate_down(i);}}}~time_heap(){for (int i = 0; i < cur_size; ++i){delete array[i];}delete[] array;}public:void add_timer(heap_timer *timer) throw(std::exception){if (!timer){return;}if (cur_size >= capacity){resize();}int hole = cur_size++;int parent = 0;for (; hole > 0; hole = parent){parent = (hole - 1) / 2;if (array[parent]->expire <= timer->expire){break;}array[hole] = array[parent];}array[hole] = timer;}void del_timer(heap_timer *timer){if (!timer){return;}// lazy deleltetimer->cb_func = NULL;}heap_timer *top() const{if (empty()){return NULL;}return array[0];}void pop_timer(){if (empty()){return;}if (array[0]){delete array[0];array[0] = array[--cur_size];percolate_down(0);}}void tick(){heap_timer *tmp = array[0];time_t cur = time(NULL);while (!empty()){if (!tmp){break;}if (tmp->expire > cur){break;}if (array[0]->cb_func){array[0]->cb_func(array[0]->user_data);}pop_timer();tmp = array[0];}}bool empty() const { return cur_size == 0; }private:void percolate_down(int hole){heap_timer *temp = array[hole];int child = 0;for (; ((hole * 2 + 1) <= (cur_size - 1)); hole = child){child = hole * 2 + 1;if ((child < (cur_size - 1)) && (array[child + 1]->expire < array[child]->expire)){++child;}if (array[child]->expire < temp->expire){array[hole] = array[child];}else{break;}}array[hole] = temp;}void resize() throw(std::exception){heap_timer **temp = new heap_timer *[2 * capacity];for (int i = 0; i < 2 * capacity; ++i){temp[i] = NULL;}if (!temp){throw std::exception();}capacity = 2 * capacity;for (int i = 0; i < cur_size; ++i){temp[i] = array[i];}delete[] array;array = temp;}private:heap_timer **array; //堆数组int capacity;       //堆数组容量int cur_size;       //堆数组当前包含元素的个数
};
#endif

Linux服务器定时器相关推荐

  1. 如何快速实现分布式定时器丨红黑树|跳表|堆|时间轮|缓存|锁|事务|架构|高性能|消息队列丨C/C++Linux服务器开发丨C++后端开发

    如何快速实现分布式定时器 视频讲解如下,点击观看: 如何快速实现分布式定时器丨红黑树|跳表|堆|时间轮|缓存|锁|事务|架构|高性能|消息队列丨C/C++Linux服务器开发丨C++后端开发丨中间件 ...

  2. 如何提升 Linux 服务器安全的开源工具和技巧?

    本文基于我作为初学者迄今所学的知识,详细介绍了六个简单的步骤,以提高个人使用的 Linux 环境的安全性.在我的整个旅程中,我利用开源工具来加速我的学习过程,并熟悉了与提升 Linux 服务器安全有关 ...

  3. Linux服务器数据备份

    2019独角兽企业重金招聘Python工程师标准>>> 服务器上的数据是如此重要,以至于我们定期要对其进行备份!这里讲一下mysql的备份和使用rsync同步服务器上的文件,从而实现 ...

  4. (转)为Linux服务器部署高效防毒软件

    作为一个操作系统,Linux对病毒的抵抗能力是人所共知的.这主要得力于其优秀的技术设计,这不仅使它的作业系统难以宕机,而且也使其难以被滥用.首先,Linux早期的使用者一般都是专业人士,就算是今天,虽 ...

  5. 为Linux服务器部署高效防毒软件

    为一个操作系统,Linux对病毒的抵抗能力是人所共知的.这主要得力于其优秀的技术设计,这不仅使它的作业系统难以宕机,而且也使其难以被滥用.首先,Linux早期的使用者一般都是专业人士,就算是今天,虽然 ...

  6. jemter在linux上怎么安装_jmeter 在linux服务器的安装和运行教程图解

    linux环境下使用就meter进行压力测试: 1.linux环境部署:在Linux服务器先安装jdk: 2.以jdk-8u172-linux-x64.tar.gz为例: 下载地址:http://ww ...

  7. linux服务器防病毒,Linux服务器防病毒实战(3)

    随着Linux应用的日益广泛,有大量的网络服务器使用Linux操作系统.由于Linux的桌面应用和Windows相比还有一定的差距,所以在企业应 用中往往是Linux和Windows操作系统共存形成异 ...

  8. php服务器视频教程,从PHP基础到实战高手 高性能Linux服务器构建实战 千峰教育PHP全新版高级视频教程...

    从PHP基础到实战高手 高性能Linux服务器构建实战 千峰教育PHP全新版高级视频教程 非常厉害的一套PHP实战课程,系统的学习完成后可以从零基础的PHP新手, 成长为能独立完成高性能服务器部署的实 ...

  9. C/C++Linux服务器开发高级架构师/Linux后台开发架构师丨高级进阶学习

    01 课程介绍 [录播]课程介绍(66分钟) 免费试学 [录播]磁盘存储链式的B树与B+树(131分钟) 免费试学 免费学习视频链接点击:C/C++Linux服务器开发高级架构师/Linux后台架构师 ...

最新文章

  1. 如何用php弄导航栏,怎样用PHP来给网页做导航栏
  2. python程序后台运行的实现
  3. 根据HttpServletRequest request 获取当前用户ip地址和ip所属区域
  4. 测试php框架漏洞,ThinkPHP框架通杀所有版本的一个SQL注入漏洞详细分析及测试方法...
  5. 分布式文件系统HDFS,大数据存储实战(一)
  6. android tag 快捷_Android Studio快捷键使用
  7. c语言十六实验答案,《C语言》上机实验题及参考答案
  8. MapReduce异常
  9. 黑科技!颠覆者!区块链,再不关注就晚了
  10. Maven Web项目配置Mybatis出现SqlSessionFactory错误的解决方案
  11. 路由器故障排除的思路与理论
  12. jquey知识点整理
  13. [SPOJ CIRU]The area of the union of circles(自适应Simpson积分求圆并面积)
  14. 实现了一个跨平台的 YUV 文件图片查看器
  15. Ubuntu 64 测试ODB
  16. 文本挖掘带你分析苏轼的一生
  17. GBase 8a - 开启防火墙安装集群添加端口策略
  18. docker 清理磁盘
  19. 三方协议服务器不填,三方协议档案转寄地址可以不填吗
  20. nginx光速入门到进阶

热门文章

  1. 关于JAVA中事件分发和监听机制实现的代码实例-绝对原创实用
  2. python 随机森林调参_python的随机森林模型调参
  3. Android--intent-filter 过滤规则的匹配 (Activity通过Uri传递参数)
  4. 企业移动内容管理(MCM)
  5. Shell——函数参数传递
  6. PHP json_encode 有序无序问题
  7. Linux 驱动程序之字符驱动
  8. 超简单jq完成点击图片放大功能
  9. 常用的电源防反接电路总结
  10. TCN论文及代码解读总结