本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性,严禁用于任何商业用途。
msn: yfydz_no1@hotmail.com
来源:http://yfydz.cublog.cn

8. 事件处理

8.1 概述

pluto使用"事件"的方式来定义各种超时,将超时操作作为一个未来将要发生的事件。当进入IKE连接中的各种状态时,需要定义对方无响应的超时处理情况,就将这种超时操作作为一个事件加入到系统的事件链表中,然后将最近将发生的一个事件的事件作为select系统调用的超时(server.c),如果没有数据触发select,select将超时,从而从事件队列中取出该事件进行相关的操作;而如果select被触发,不需要超时操作了,需要将相关事件从事件链表中删除。这和Windows的事件定义不同的,Windows的事件是真正发生的事件,如键盘鼠标的输入等,而pluto的事件是实际没有真正事件(如数据包的到来)发生超时的时候才处理的虚拟事件。

通过事件的实现,pluto实现了自己的定时器处理,相关函数在programs/pluto/timer.c中定义。

8.2 事件结构
/* programs/pluto/timer.h */
struct event
{
// 事件发生时间,秒为单位
    time_t          ev_time;
// 事件类型
    enum event_type ev_type;        /* Event type */
// 状态指针,定义了该事件的状态
    struct state   *ev_state;       /* Pointer to relevant state (if any) */
// 事件链表中的下一项
    struct event   *ev_next;        /* Pointer to next event */
};

8.3 基本函数

8.3.1 获取当前时间

// 基本就是time(3)函数, 但考虑了系统时间发生停顿等异常的情况,
// 使得该函数的返回值肯定是递增的
time_t
now(void)
{
// 这两个数都是静态量, 用于处理时间异常的情况
// last_time保存上次调用该函数的时间
    static time_t delta = 0
 , last_time = 0;
// 获取系统时间
    time_t n = time((time_t)NULL);
passert(n != (time_t)-1);
// 如果上次调用时间还超过当前时间,系统时间发生后向调整了
    if (last_time > n)
    {
 openswan_log("time moved backwards %ld seconds", (long)(last_time - n));
// 记录这种偏移值
 delta += last_time - n;
    }
    last_time = n;
// 对返回值进行时间调整
    return n + delta;
}

8.3.2 事件调度

// 该函数就是定义状态的超时处理事件,然后添加到系统事件链表
// tm是超时时间
void
event_schedule(enum event_type type, time_t tm, struct state *st)
{
// 分配事件结构
    struct event *ev = alloc_thing(struct event, "struct event in event_schedule()");
passert(tm >= 0);
// 事件类型
    ev->ev_type = type;
// 事件发生时间: 当前时间加超时时间
    ev->ev_time = tm + now();
// 定义该事件的状态, 可能为空
    ev->ev_state = st;
/* If the event is associated with a state, put a backpointer to the
     * event in the state object, so we can find and delete the event
     * if we need to (for example, if we receive a reply).
     */
    if (st != NULL)
    {
// 事件非空时,将事件指针和状态关联起来
            if(type == EVENT_DPD || type == EVENT_DPD_TIMEOUT)
            {
                    passert(st->st_dpd_event == NULL);
// 如果是DPD事件, 将其赋值到状态的DPD事件指针
                    st->st_dpd_event = ev;
            } else {
                    passert(st->st_event == NULL);
// 否则赋值到状态的事件指针
                    st->st_event = ev;
            }
    }
DBG(DBG_CONTROL,
 if (st == NULL)
     DBG_log("inserting event %s, timeout in %lu seconds"
  , enum_show(&timer_event_names, type), (unsigned long)tm);
 else
     DBG_log("inserting event %s, timeout in %lu seconds for #%lu"
  , enum_show(&timer_event_names, type), (unsigned long)tm
  , ev->ev_state->st_serialno));
// 添加到系统事件链表
    if (evlist == (struct event *) NULL
    || evlist->ev_time >= ev->ev_time)
    {
// 事件表为空或事件时间比链表中所有时间都更近, 作为链表头
 ev->ev_next = evlist;
 evlist = ev;
    }
    else
    {
 struct event *evt;
// 根据事件发生时间将事件插入到链表中合适位置, 链表头是最先发生的,链表尾是最后发生的
// 先查找链表中合适位置
 for (evt = evlist; evt->ev_next != NULL; evt = evt->ev_next)
     if (evt->ev_next->ev_time >= ev->ev_time)
  break;
#ifdef NEVER /* this seems to be overkill */
 DBG(DBG_CONTROL,
     if (evt->ev_state == NULL)
  DBG_log("event added after event %s"
      , enum_show(&timer_event_names, evt->ev_type));
     else
  DBG_log("event added after event %s for #%lu"
      , enum_show(&timer_event_names, evt->ev_type)
      , evt->ev_state->st_serialno));
#endif /* NEVER */
// 插入节点
 ev->ev_next = evt->ev_next;
 evt->ev_next = ev;
    }
}
8.3.3 获取下一个事件发生时间

也就是计算select()需要的超时是多少
/*
 * Return the time until the next event in the queue
 * expires (never negative), or -1 if no jobs in queue.
 */
long
next_event(void)
{
    time_t tm;
// 事件链表空, 返回-1表示不需要有超时处理
    if (evlist == (struct event *) NULL)
 return -1;
// 获取当前时间
    tm = now();
DBG(DBG_CONTROL,
 if (evlist->ev_state == NULL)
     DBG_log("next event %s in %ld seconds"
  , enum_show(&timer_event_names, evlist->ev_type)
  , (long)evlist->ev_time - (long)tm);
 else
     DBG_log("next event %s in %ld seconds for #%lu"
  , enum_show(&timer_event_names, evlist->ev_type)
  , (long)evlist->ev_time - (long)tm
  , evlist->ev_state->st_serialno));
// 如果链表头元素的时间小于当前时间,说明当前还有数据需要处理,返回0表示
// 此时不执行select函数,而是直接进行事件处理,由于事件定义是按秒为单位,
// 所以同一秒内发生多个事件是很可能的;
// 如果链表头元素的时间大于当前时间,返回差值作为select()函数的超时时间
    if (evlist->ev_time - tm <= 0)
 return 0;
    else
 return evlist->ev_time - tm;
}

8.3.4 删除事件

// 当在超时发生前状态收到相应的包不需要该超时处理时, 需要删除该状态原来定义事件
void
delete_event(struct state *st)
{
// 状态结构不为空才有删除意义
    if (st->st_event != (struct event *) NULL)
    {
 struct event **ev;
// 遍历事件表
 for (ev = &evlist; ; ev = &(*ev)->ev_next)
 {
     if (*ev == NULL)
     {
  DBG(DBG_CONTROL, DBG_log("event %s to be deleted not found",
      enum_show(&timer_event_names, st->st_event->ev_type)));
  break;
     }
// 直接比较事件结构本身的地址是否相同
     if ((*ev) == st->st_event)
     {
// 找到,从链表断开
  *ev = (*ev)->ev_next;
// 如果是重传事件,标志清零
  if (st->st_event->ev_type == EVENT_RETRANSMIT)
      st->st_retransmit = 0;
// 释放空间, 状态结构中的事件指针清空
  pfree(st->st_event);
  st->st_event = (struct event *) NULL;
break;
     }
 }
    }
}

8.3.5 删除DPD事件

// 和前一节删除函数结构上完全相同, 只是比较时使用的是状态的DPD事件指针来查找事件
void
_delete_dpd_event(struct state *st, const char *file, int lineno)
{
    DBG(DBG_DPD|DBG_CONTROL
 , DBG_log("state: %ld requesting event %s to be deleted by %s:%d"
    , st->st_serialno
    , (st->st_dpd_event!=NULL
     ? enum_show(&timer_event_names, st->st_dpd_event->ev_type)
     : "none")
    , file, lineno));
 
    if (st->st_dpd_event != (struct event *) NULL)
    {
        struct event **ev;
for (ev = &evlist; ; ev = &(*ev)->ev_next)
        {
            if (*ev == NULL)
            {
                DBG(DBG_DPD|DBG_CONTROL
      , DBG_log("event %s to be deleted not found",
         enum_show(&timer_event_names
     , st->st_dpd_event->ev_type)));
                break;
            }
// 用的是DPD事件指针来查找
            if ((*ev) == st->st_dpd_event)
            {
                *ev = (*ev)->ev_next;
                pfree(st->st_dpd_event);
                st->st_dpd_event = (struct event *) NULL;
                break;
            }
        }
    }
}

8.3.6 向whack输出当前所有事件

void
timer_list(void)
{
    time_t tm;
// 事件链表头
    struct event *ev = evlist;
    int type;
    struct state *st;
// 链表为空, 返回
    if (ev == (struct event *) NULL)    /* Just paranoid */
    {
 whack_log(RC_LOG, "no events are queued");
 return;
    }
// 获取当前时间
    tm = now();
whack_log(RC_LOG, "It is now: %ld seconds since epoch", (unsigned long)tm);
// 遍历事件表
    while(ev) {
 type = ev->ev_type;
 st = ev->ev_state;
// 输出whack日志
 whack_log(RC_LOG, "event %s is schd: %ld (in %lds) state:%ld"
    , enum_show(&timer_event_names, type)
    , (unsigned long)ev->ev_time
    , (unsigned long)(ev->ev_time - tm)
    , st != NULL ? (long signed)st->st_serialno : -1);
if(st && st->st_connection) {
     whack_log(RC_LOG, "    connection: \"%s\"", st->st_connection->name);
 }
ev = ev->ev_next;
    }
}

8.4 事件处理

// 从事件链表中取出第一个事件进行处理, 这是在select()超时发生时进行的处理
// 这个函数不需要返回值
void
handle_timer_event(void)
{
    time_t tm;
// 事件头
   struct event *ev = evlist;
    int type;
    struct state *st;
    ip_address peer;
// 事件链表空, 返回
    if (ev == (struct event *) NULL)    /* Just paranoid */
    {
 DBG(DBG_CONTROL, DBG_log("empty event list, yet we're called"));
 return;
    }
// 事件类型和对应的状态
    type = ev->ev_type;
    st = ev->ev_state;
tm = now();
// 如果事件时间还没到, 返回, 也算一种异常了
    if (tm < ev->ev_time)
    {
 DBG(DBG_CONTROL, DBG_log("called while no event expired (%lu/%lu, %s)"
     , (unsigned long)tm, (unsigned long)ev->ev_time
     , enum_show(&timer_event_names, type)));
/* This will happen if the most close-to-expire event was
  * a retransmission or cleanup, and we received a packet
  * at the same time as the event expired. Due to the processing
  * order in call_server(), the packet processing will happen first,
  * and the event will be removed.
  */
 return;
    }
// 将链表头节点从链表中断开
    evlist = evlist->ev_next;  /* Ok, we'll handle this event */
DBG(DBG_CONTROL, DBG_log("handling event %s"
        , enum_show(&timer_event_names, type)));
if(DBGP(DBG_CONTROL)) {
 if (evlist != (struct event *) NULL) {
     DBG_log("event after this is %s in %ld seconds"
      , enum_show(&timer_event_names, evlist->ev_type)
      , (long) (evlist->ev_time - tm));
 }
 else {
     DBG_log("no more events are scheduled");
 }
    
    }
/* for state-associated events, pick up the state pointer
     * and remove the backpointer from the state object.
     * We'll eventually either schedule a new event, or delete the state.
     */
    passert(GLOBALS_ARE_RESET());
// 如果状态非空, 将其结构中的事件指针清空
    if (st != NULL)
    {
 struct connection *c;
// 状态对应的连接
 c = st->st_connection;
        if( type  == EVENT_DPD || type == EVENT_DPD_TIMEOUT)
        {
// 如果是DPD事件,清空的是状态中的DPD事件指针
                passert(st->st_dpd_event == ev);
                st->st_dpd_event = NULL;
        } else {
// 否则清空的是状态中的事件指针
     passert(st->st_event == ev);
     st->st_event = NULL;
        }
// 连接对方的IP地址, 是否会有连接指针为空的异常?
 peer = c->spd.that.host_addr;
 set_cur_state(st);
    }
// 根据事件类型进行相关处理
    switch (type)
    {
// 重协商密钥事件
 case EVENT_REINIT_SECRET:
     passert(st == NULL);
     DBG(DBG_CONTROL, DBG_log("event EVENT_REINIT_SECRET handled"));
// 重新初始化密钥
     init_secret();
     break;
#ifdef KLIPS
// 扫描当前的/proc/net/ipsec_eroute,检查是否有异常的eroute
// 这时定时周期操作
 case EVENT_SHUNT_SCAN:
     passert(st == NULL);
     scan_proc_shunts();
     break;
#endif
// 扫描连接是否活动, 执行DPD
// 这时定时周期操作
        case EVENT_PENDING_PHASE2:
     passert(st == NULL);
     connection_check_phase2();
     break;
 
// 周期性记录日志
 case EVENT_LOG_DAILY:
     daily_log_event();
     break;
// 重新发送数据,刚才发送的数据没有回应
 case EVENT_RETRANSMIT:
     /* Time to retransmit, or give up.
      *
      * Generally, we'll only try to send the message
      * MAXIMUM_RETRANSMISSIONS times.  Each time we double
      * our patience.
      *
      * As a special case, if this is the first initiating message
      * of a Main Mode exchange, and we have been directed to try
      * forever, we'll extend the number of retransmissions to
      * MAXIMUM_RETRANSMISSIONS_INITIAL times, with all these
      * extended attempts having the same patience.  The intention
      * is to reduce the bother when nobody is home.
      *
      * Since IKEv1 is not reliable for the Quick Mode responder,
      * we'll extend the number of retransmissions as well to
      * improve the reliability.
      */
     {
  time_t delay = 0;
  struct connection *c;
passert(st != NULL);
// 状态对应连接
  c = st->st_connection;
DBG(DBG_CONTROL, DBG_log(
      "handling event EVENT_RETRANSMIT for %s \"%s\" #%lu"
      , ip_str(&peer), c->name, st->st_serialno));
// 如果重新发送次数没超过最大数, 计算超时时间, 超时时间每次翻倍
// 超过的话超时时间就还是初始值0了
  if (st->st_retransmit < MAXIMUM_RETRANSMISSIONS)
      delay = EVENT_RETRANSMIT_DELAY_0 << (st->st_retransmit + 1);
  else if ((st->st_state == STATE_MAIN_I1 || st->st_state == STATE_AGGR_I1)
  && c->sa_keying_tries == 0
  && st->st_retransmit < MAXIMUM_RETRANSMISSIONS_INITIAL)
      delay = EVENT_RETRANSMIT_DELAY_0 << MAXIMUM_RETRANSMISSIONS;
  else if (st->st_state == STATE_QUICK_R1
  && st->st_retransmit < MAXIMUM_RETRANSMISSIONS_QUICK_R1)
      delay = EVENT_RETRANSMIT_DELAY_0 << MAXIMUM_RETRANSMISSIONS;
if (delay != 0)
  {
// 超时时间非0
// 重发次数增加
      st->st_retransmit++;
      whack_log(RC_RETRANSMISSION
   , "%s: retransmission; will wait %lus for response"
   , enum_name(&state_names, st->st_state)
   , (unsigned long)delay);
// 发送和状态相关的数据包
      send_packet(st, "EVENT_RETRANSMIT", TRUE);
// 重新设置状态超时
      event_schedule(EVENT_RETRANSMIT, delay, st);
  }
  else
  {
// 超时为0, 也就是重发次数超过了最大数
//
      /* check if we've tried rekeying enough times.
       * st->st_try == 0 means that this should be the only try.
       * c->sa_keying_tries == 0 means that there is no limit.
       */
// 是否需要重新协商参数
      unsigned long try = st->st_try;
      unsigned long try_limit = c->sa_keying_tries;
      const char *details = "";
// 设置不同状态下的消息信息可记录到日志
      switch (st->st_state)
      {
      case STATE_MAIN_I3:
   details = ".  Possible authentication failure:"
       " no acceptable response to our"
       " first encrypted message";
   break;
      case STATE_MAIN_I1:
   details = ".  No response (or no acceptable response) to our"
       " first IKE message";
   break;
      case STATE_QUICK_I1:
   if (c->newest_ipsec_sa == SOS_NOBODY)
       details = ".  No acceptable response to our"
    " first Quick Mode message:"
    " perhaps peer likes no proposal";
   break;
      default:
   break;
      }
// 记录重传次数过多的日志
      loglog(RC_NORETRANSMISSION
   , "max number of retransmissions (%d) reached %s%s"
   , st->st_retransmit
   , enum_show(&state_names, st->st_state), details);
      if (try != 0 && try != try_limit)
      {
// 如果要重新协商
   /* A lot like EVENT_SA_REPLACE, but over again.
    * Since we know that st cannot be in use,
    * we can delete it right away.
    */
   char story[80]; /* arbitrary limit */
// 增加计数
   try++;
   snprintf(story, sizeof(story), try_limit == 0
       ? "starting keying attempt %ld of an unlimited number"
       : "starting keying attempt %ld of at most %ld"
       , try, try_limit);
// 释放whack接口
   if (st->st_whack_sock != NULL_FD)
   {
       /* Release whack because the observer will get bored. */
       loglog(RC_COMMENT, "%s, but releasing whack"
    , story);
       release_pending_whacks(st, story);
   }
   else
   {
       /* no whack: just log to syslog */
       openswan_log("%s", story);
   }
// 重协商操作
   ipsecdoi_replace(st, try);
      }
// 删除状态
      delete_state(st);
  }
     }
     break;
// SA替换事件
 case EVENT_SA_REPLACE:
 case EVENT_SA_REPLACE_IF_USED:
     {
  struct connection *c;
  so_serial_t newest;
passert(st != NULL);
// 状态对应连接
  c = st->st_connection;
// 连接中最新状态序号
  newest = IS_PHASE1(st->st_state)
      ? c->newest_isakmp_sa : c->newest_ipsec_sa;
if (newest != st->st_serialno
  && newest != SOS_NOBODY)
  {
// 状态非最新的而且非0, 基本正常情况
      /* not very interesting: no need to replace */
      DBG(DBG_LIFECYCLE
   , openswan_log("not replacing stale %s SA: #%lu will do"
       , IS_PHASE1(st->st_state)? "ISAKMP" : "IPsec"
       , newest));
  }
  else if (type == EVENT_SA_REPLACE_IF_USED
  && st->st_outbound_time <= tm - c->sa_rekey_margin)
  {
// 最新序号的状态, 但从时间还不需要替换
      /* we observed no recent use: no need to replace
       *
       * The sampling effects mean that st_outbound_time
       * could be up to SHUNT_SCAN_INTERVAL more recent
       * than actual traffic because the sampler looks at change
       * over that interval.
       * st_outbound_time could also not yet reflect traffic
       * in the last SHUNT_SCAN_INTERVAL.
       * We expect that SHUNT_SCAN_INTERVAL is smaller than
       * c->sa_rekey_margin so that the effects of this will
       * be unimportant.
       * This is just an optimization: correctness is not
       * at stake.
       *
       * Note: we are abusing the DBG mechanism to control
       * normal log output.
       */
      DBG(DBG_LIFECYCLE
   , openswan_log("not replacing stale %s SA: inactive for %lus"
       , IS_PHASE1(st->st_state)? "ISAKMP" : "IPsec"
       , (unsigned long)(tm - st->st_outbound_time)));
  }
  else
  {
// 进行状态替换操作
      DBG(DBG_LIFECYCLE
   , openswan_log("replacing stale %s SA"
       , IS_PHASE1(st->st_state)? "ISAKMP" : "IPsec"));
      ipsecdoi_replace(st, 1);
  }
// 删除状态相关的DPD事件
  delete_dpd_event(st);
// 重新设置SA超时
  event_schedule(EVENT_SA_EXPIRE, st->st_margin, st);
     }
     break;
// SA超时事件
 case EVENT_SA_EXPIRE:
     {
  const char *satype;
// 要删除状态对应的连接的最新状态序号
  so_serial_t latest;
  struct connection *c;
passert(st != NULL);
// 状态对应连接
  c = st->st_connection;
// 检查状态阶段
  if (IS_PHASE1(st->st_state))
  {
// 第一阶段状态, 是ISAKMP的状态
      satype = "ISAKMP";
      latest = c->newest_isakmp_sa;
  }
  else
  {
// 第2阶段状态, 是IPSEC的状态
      satype = "IPsec";
      latest = c->newest_ipsec_sa;
  }
if (st->st_serialno != latest)
  {
// 如果当前状态序号不是最新的, 基本是正常情况
      /* not very interesting: already superseded */
      DBG(DBG_LIFECYCLE
   , openswan_log("%s SA expired (superseded by #%lu)"
       , satype, latest));
  }
  else
  {
// 这时删除最新序号的状态, 有点异常了
      openswan_log("%s SA expired (%s)", satype
   , (c->policy & POLICY_DONT_REKEY)
       ? "--dontrekey"
       : "LATEST!"
   );
  }
     }
// 继续下面的case操作
     /* FALLTHROUGH */
// 状态删除
 case EVENT_SO_DISCARD:
     /* Delete this state object.  It must be in the hash table. */
// 释放消息摘要结构
     if(st->st_suspended_md) {
  release_md(st->st_suspended_md);
  st->st_suspended_md=NULL;
     }
// 删除状态
     delete_state(st);
     break;
case EVENT_DPD:
// DPD事件处理
            dpd_event(st);
            break;
    
        case EVENT_DPD_TIMEOUT:
// DPD超时操作
            dpd_timeout(st);
            break;

#ifdef NAT_TRAVERSAL
 case EVENT_NAT_T_KEEPALIVE:
// NAT穿越保活操作,发送保活消息
     nat_traversal_ka_event();
     break;
#endif
    
        case EVENT_CRYPTO_FAILED:
// 加密失败, 删除状态
     DBG(DBG_CONTROL
  , DBG_log("event crypto_failed on state #%lu, aborting"
     , st->st_serialno));
     delete_state(st);
     break;
    
// 非法事件类型了
 default:
     loglog(RC_LOG_SERIOUS, "INTERNAL ERROR: ignoring unknown expiring event %s"
  , enum_show(&timer_event_names, type));
    }
// 删除事件结构
    pfree(ev);
    reset_cur_state();
}

...... 待续 ......

转载于:https://blog.51cto.com/enchen/157907

pluto实现分析(7)相关推荐

  1. pluto实现分析(22)

    本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性, 严禁用于任何商业用途. msn: yfydz_no1@hotmail.com 来源:http: ...

  2. Ubuntu 16.04 安装 Wireshark分析tcpdump的pcap包——sudo apt install wireshark-qt

    tcpdump 的抓包保存到文件的命令参数是-w xxx.cap 抓eth1的包  tcpdump -i eth1 -w /tmp/xxx.cap  抓 192.168.1.123的包  tcpdum ...

  3. Linux/Centos7系统管理之深入理解Linux文件系统与日志分析

    前言:inode(文件节点)与block(数据块)硬链接与软连接恢复误删除的文件 (即rm-rf 的操作,可以先进行备份的操作,然后可以进行恢复ext4和xfs文件系统皆可)日志文件的分类用户日志与程 ...

  4. Pluto - iOS 上一个高性能的排版渲染引擎

    Pluto 是 iOS 上的一个排版渲染引擎,通过 JSON/JS 文件可以很方便地描述界面元素,开发效率很高,并且在流畅度,内存等方便有保证.pluto.oa.com 上有更多详细资料. Qzone ...

  5. CentOS 7文件系统与日志分析详解

    Linux 文件系统 在处理 Linux 系统出现的各种故障时,故障的症状是最易发现的,而导致这一故障的原因才是最终排除故障的关键.熟悉 Linux 系统中常见的日志文件,了解一般故障的分析与解决办法 ...

  6. Portal产品对比分析报告

    目录 1概述 2Portal相关产品介绍 2.1商业Portal 2.1.1Bea weblogic portal 2.1.2IBM websphere portal 2.1.3Oracle port ...

  7. 我是如何将Pluto作为library分享到jCenter

    最近小明正在看一本名叫<历代经济变革得失>从历史角度分析经济变化的书.通过研究经济方式,作为一个工科毕业的小明,当然直接想到是数据统计,建立数学理论模型,这本书入手方式就成为了第一个吸引点 ...

  8. 理论:深入理解Linux文件系统与日志分析

    前言: inode(文件节点)与block(数据块) 硬链接与软连接 恢复误删除的文件 (即rm-rf 的操作,可以先进行备份的操作,然后可以进行恢复ext4和xfs文件系统皆可) 日志文件的分类 用 ...

  9. 0640-6.1.1-Hue上SQL查询结果显示不全异常分析-补充

    Fayson的github: https://github.com/fayson/cdhproject 推荐关注微信公众号:"Hadoop实操",ID:gh_c4c535955d0 ...

最新文章

  1. UVA1108 Mining Your Own Business(思维、割点)(2011 ICPC - WorldFinal)
  2. 背景属性的相关属性设置
  3. LeetCode:2. Add Two Numbers
  4. msfvenom生成木马和内网穿透
  5. 计算机视觉各领域前沿算法积累
  6. VS2010 自动化整理代码(1)--- VS正则表达替换 PK Vim
  7. [转]android 获取 imei号码
  8. 全国计算机二级c 笔记,[IT认证]全国计算机等级考试二级C语言笔记.doc
  9. 这样的家居选购界面让你忍不住剁手的冲动!
  10. 03-15 截图、日志与录屏
  11. 最新!Dubbo 远程代码执行漏洞通告,速度升级
  12. Vmware+Virtualbox+Ubuntu+debian+USB转串口+kermit
  13. 我心中最敬业的天王 刘德华|分析天王近700多首歌曲
  14. mysql修改字符集utf8_修改mysql数据库字符集为UTF8
  15. [Poi2003 ][bzoj 2601]MAL猴子捞月
  16. 思维模型:建立高品质思维的30种模型
  17. 力扣:电话号码的字母组合
  18. 聚商汇WMS:开源仓库管理系统
  19. bit和byte以及千字节关系
  20. 联想拯救者R7000搜索不到WiFi解决方法(史上最全)

热门文章

  1. 对话图森无人车CEO陈默:IPO,我们只差最后一个必要条件
  2. 「镁客·请讲」天宝陈朝晖:AI 的准确译法不是人工智能,而是机器智能...
  3. springCloud-4.RestTemplat的使用(两个client之间的调用)
  4. Hibernate 中配置属性详解(hibernate.properties)
  5. Linux基本命令四(文件系统)
  6. SQL CREATE TABLE 语句(转)
  7. 成为男人眼中魅力女人的十大要素
  8. 为ASP.NET MVC配置基于Active Directory的表单认证方式
  9. .net 2.0 点击按钮用js控制是否回发关于vs2005的webproject补丁
  10. 华为手机有没有html,华为手机,到底有没有自己的核心技术?看内行人怎么说...