内核版本:2.6.34
这篇是关于IP层协议接收报文时的处理,重点说明了路由表的查找,以及IP分片重组。

ip_rcv进入IP层报文接收函数
      丢弃掉不是发往本机的报文,skb->pkt_type在网卡接收报文处理以太网头时会根据dst mac设置,协议栈的书会讲不是发往本机的广播报文会在二层被丢弃,实际上丢弃是发生在进入上层之初。

if(skb->pkt_type == PACKET_OTHERHOST)  goto drop;

在取IP报头时要注意可能带有选项,因此报文长度应当以iph->ihl * 4为准。这里就需要尝试两次,第一次尝试sizeof(struct iphdr),只是为了确保skb还可以容纳标准的报头(即20字节),然后可以ip_hdr(skb)得到报头;第二次尝试ihl * 4,这才是报文的真正长度,然后重新调用ip_hdr(skb)来得到报头。两次尝试pull后要重新调用ip_hdr()的原因是pskb_may_pull()可能会调用__pskb_pull_tail()来改现现有的skb结构。

if (!pskb_may_pull(skb, sizeof(struct iphdr)))  goto inhdr_error;
iph = ip_hdr(skb);
……
if (!pskb_may_pull(skb, iph->ihl*4))  goto inhdr_error;
iph = ip_hdr(skb);

获取到IP报头后经过一些检查,获取到报文的总长度len = iph->tot_len,此时调用pskb_trim_rcsum()去除多余的字节,即大于len的。

if (pskb_trim_rcsum(skb, len)) {  IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS);  goto drop;
} 

然后调用ip_rcv_finish()继续IP层的处理,ip_rcv()可以看成是查找路由前的IP层处理,接下来的ip_rcv_finish()会查找路由表,两者间调用插入的netfilter(关于NetFilter,参考前篇http://blog.csdn.net/qy532846454/article/details/6605592)。

 return NF_HOOK(PF_INET, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish); 

进入ip_rcv_finish函数
      ip_rcv_finish()主要工作是完成路由表的查询,决定报文经过IP层处理后,是继续向上传递,还是进行转发,还是丢弃。

刚开始没有进行路由表查询,所以还没有相应的路由表项:skb_dst(skb) == NULL。则在路由表中查找ip_route_input(),关于内核的路由表,可以参见前篇http://blog.csdn.net/qy532846454/article/details/6726171:

if (skb_dst(skb) == NULL) {int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,skb->dev);if (unlikely(err)) {if (err == -EHOSTUNREACH)IP_INC_STATS_BH(dev_net(skb->dev),IPSTATS_MIB_INADDRERRORS);else if (err == -ENETUNREACH)IP_INC_STATS_BH(dev_net(skb->dev),IPSTATS_MIB_INNOROUTES);goto drop;}
}

通过路由表查找,我们知道:

- 如果是丢弃的报文,则直接drop;
         - 如果是不能接收或转发的报文,则input = ip_error
         - 如果是发往本机报文,则input = ip_local_deliver;
         - 如果是广播报文,则input = ip_local_deliver;
         - 如果是组播报文,则input = ip_local_deliver;
         - 如果是转发的报文,则input = ip_forward;
      在ip_rcv_finish()最后,会调用查找到的路由项_skb_dst->input()继续向上传递:

return dst_input(skb);

具体看下各种情况下的报文传递,如果是丢弃的报文,则报文被释放,并从IP协议层返回,完成此次报文传递流程。

drop:  kfree_skb(skb);  return NET_RX_DROP;

如果是不能处理的报文,则执行ip_error,根据error类型发送相应的ICMP错误报文。

static int ip_error(struct sk_buff *skb)
{  struct rtable *rt = skb_rtable(skb);  unsigned long now;  int code;  switch (rt->u.dst.error) {  case EINVAL:  default:  goto out;  case EHOSTUNREACH:  code = ICMP_HOST_UNREACH;  break;  case ENETUNREACH:  code = ICMP_NET_UNREACH;  IP_INC_STATS_BH(dev_net(rt->u.dst.dev),IPSTATS_MIB_INNOROUTES);  break;  case EACCES:  code = ICMP_PKT_FILTERED;  break;  }  now = jiffies;  rt->u.dst.rate_tokens += now - rt->u.dst.rate_last;  if (rt->u.dst.rate_tokens > ip_rt_error_burst)  rt->u.dst.rate_tokens = ip_rt_error_burst;  rt->u.dst.rate_last = now;  if (rt->u.dst.rate_tokens >= ip_rt_error_cost) {  rt->u.dst.rate_tokens -= ip_rt_error_cost;  icmp_send(skb, ICMP_DEST_UNREACH, code, 0);  }out: kfree_skb(skb);  return 0;
}

如果是主机可以接收报文,则执行ip_local_deliver。ip_local_deliver在向上传递前,会对分片的IP报文进行组包,因为IP层协议会对过大的数据包分片,在接收时,就要进行重组,而重组的操作就是在这里进行的。IP报头的16位偏移字段frag_off是由3位的标志(CE,DF,MF)和13的偏移量组成。如果收到了分片的IP报文,如果是最后一片,则MF=0且offset!=0;如果不是最后一片,则MF=1。

在这种情况下会执行ip_defrag来处理分片的IP报文,如果不是最后一片,则将该报文添加到ip4_frags中保留下来,并return 0,此次数据包接收完成;如果是最后一片,则取出之前收到的分片重组成新的skb,此时ip_defrag返回值为0,skb被重置为完整的数据包,然后继续处理,之后调用ip_local_deliver_finish处理重组后的数据包。

if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {  if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))  return 0;
}

下面来看下ip_defrag()函数,主体就是下面的代码段。它首先用ip_find()查找IP分片,并返回(如果没有则创建),然后用ip_frag_queue()将新分片加入,关于IP分片的处理,在后面的IP分片中有详细描述。

if ((qp = ip_find(net, ip_hdr(skb), user)) != NULL) {  int ret;  spin_lock(&qp->q.lock);  ret = ip_frag_queue(qp, skb);  spin_unlock(&qp->q.lock);  ipq_put(qp);  return ret;
}

然后会调用ip_local_deliver_finish()完成IP协议层的传递,两者调用间依然有netfilter,这是查找完路由表继续向上传递的中间点。

 NF_HOOK(PF_INET, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);

在ip_local_deliver_finish()中会完成IP协议层处理,再交由上层协议模块处理:ICMP、IGMP、UDP、TCP。在ip_local_deliver_finish函数中,由于IP报头已经处理完,剔除IP报头,并设置skb->transport_header指向传输层协议报头位置。

 __skb_pull(skb, ip_hdrlen(skb));  skb_reset_transport_header(skb);

protocol是IP报头中的的上层协议号,以它在inet_protos哈希表中查找处理protocol的协议模块,取出得到ipprot。

 hash = protocol & (MAX_INET_PROTOS - 1);  ipprot = rcu_dereference(inet_protos[hash]);

而关于inet_protos,它的数据结构是哈希表,用来存储IP层上的协议,包括传输层协议和3.5层协议,它在IP协议模块加载时被添加。

if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)  printk(KERN_CRIT "inet_init: Cannot add ICMP protocol\n");
if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)  printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");
if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)  printk(KERN_CRIT "inet_init: Cannot add TCP protocol\n");
#ifdef CONFIG_IP_MULTICAST
if (inet_add_protocol(&igmp_protocol, IPPROTO_IGMP) < 0)  printk(KERN_CRIT "inet_init: Cannot add IGMP protocol\n");
#endif

然后通过调用handler交由上层协议处理,至此,IP层协议处理完成。

ret = ipprot->handler(skb);

IP分片
      在收到IP分片时,会暂时存储到一个哈希表ip4_frags中,它在IP协议模块加载时初始化,inet_init() -> ipfrag_init()。要留意的是ip4_frag_match用于匹配IP分片是否属于同一个报文;ip_expire用于在IP分片超时时进行处理。

void __init ipfrag_init(void)
{  ip4_frags_ctl_register();  register_pernet_subsys(&ip4_frags_ops);  ip4_frags.hashfn = ip4_hashfn;  ip4_frags.constructor = ip4_frag_init;  ip4_frags.destructor = ip4_frag_free;  ip4_frags.skb_free = NULL;  ip4_frags.qsize = sizeof(struct ipq);  ip4_frags.match = ip4_frag_match;  ip4_frags.frag_expire = ip_expire;  ip4_frags.secret_interval = 10 * 60 * HZ;  inet_frags_init(&ip4_frags);
} 

当收到一个IP分片,首先用ip_find()查找IP分片,实际上就是从ip4_frag表中取出相应项。这里的哈希值是由IP报头的(标识,源IP,目的IP,协议号)得到的。

hash = ipqhashfn(iph->id, iph->saddr, iph->daddr, iph->protocol);
q = inet_frag_find(&net->ipv4.frags, &ip4_frags, &arg, hash);

inet_frag_find实现直正的查找
      根据hash值取得ip4_frag->hash[hash]项 – inet_frag_queue,它是一个队列,然后遍历该队列,当net, id, saddr, daddr, protocol, user相匹配时,就是要找的IP分片。如果没有匹配的,则调用inet_frag_create创建它。

struct inet_frag_queue *inet_frag_find(struct netns_frags *nf,struct inet_frags *f, void *key, unsigned int hash) __releases(&f->lock)
{  struct inet_frag_queue *q;  struct hlist_node *n;  hlist_for_each_entry(q, n, &f->hash[hash], list) {  if (q->net == nf && f->match(q, key)) {  atomic_inc(&q->refcnt);  read_unlock(&f->lock);  return q;  }  }read_unlock(&f->lock);return inet_frag_create(nf, f, key);
}

inet_frag_create创建一个IP分片队列ipq,并插入相应队列中。
      首先分配空间,真正分配空间的是inet_frag_alloc中的q = kzalloc(f->qsize, GFP_ATOMIC);其中f->qsize = sizeof(struct ipq),也就是说分配了ipq大小空间,但返回的却是struct inet_frag_queue q结构,原因在于inet_frag_queue是ipq的首个属性,它们两者的联系如下图。

static struct inet_frag_queue *inet_frag_create(struct netns_frags *nf,struct inet_frags *f, void *arg)
{  struct inet_frag_queue *q;  q = inet_frag_alloc(nf, f, arg);  if (q == NULL)  return NULL;  return inet_frag_intern(nf, q, f, arg);
}

在分配并初始化空间后,由inet_frag_intern完成插入动作,首先还是根据(标识,源IP,目的IP,协议号)先成hash值,这里的qp_in即之前的q。

hash = f->hashfn(qp_in); 

然后新创建的队列qp(即上面的qp_in)插入到hash表(即ip4_frags->hash)和net->ipv4.frags中,并增加队列qp的引用计数,net中的队列nqueues统计数。至此,IP分片的创建过程完成。

atomic_inc(&qp->refcnt);
hlist_add_head(&qp->list, &f->hash[hash]);
list_add_tail(&qp->lru_list, &nf->lru_list);
nf->nqueues++; 

ip_frag_queue实现将IP分片加入队列中
      首先获取该IP分片偏移位置offset,和IP分片偏移结束位置end,其中skb->len – ihl表示IP分片的报文长度,三者间关系即为end = offset + skb->len – ihl。

offset = ntohs(ip_hdr(skb)->frag_off);
flags = offset & ~IP_OFFSET;
offset &= IP_OFFSET;
offset <<= 3;  /* offset is in 8-byte chunks */
ihl = ip_hdrlen(skb);
/* Determine the position of this fragment. */
end = offset + skb->len - ihl; 

如果该IP分片是最后一片(MF=0,offset!=0),即设置q.last_iin |= INET_FRAG_LAST_IN,表示收到了最后一个分片,qp->q.len = end,此时q.len是整个IP报文的总长度。

if ((flags & IP_MF) == 0) {  if (end < qp->q.len ||  ((qp->q.last_in & INET_FRAG_LAST_IN) && end != qp->q.len))  goto err;  qp->q.last_in |= INET_FRAG_LAST_IN;  qp->q.len = end;
} 

如果该IP分片不是最后一片(MF=1),当end不是8字节倍数时,通过end &= ~7处理为8字节整数倍(但此时会忽略掉多出的字节,如end=14 => end=8);然后如果该分片更靠后,则q.len = end。

else {  if (end&7) {  end &= ~7;  if (skb->ip_summed != CHECKSUM_UNNECESSARY)  skb->ip_summed = CHECKSUM_NONE;  }  if (end > qp->q.len) {  /* Some bits beyond end -> corruption. */  if (qp->q.last_in & INET_FRAG_LAST_IN)  goto err;  qp->q.len = end;  }
}

查找q.fragments链表,找到该IP分片要插入的位置,这里的q.fragments就是struct sk_buff类型,即各个IP分片skb都会插入到该链表中,插入的位置按偏移顺序由小到大排列,prev表示插入的前一个IP分片,next表示插入的后一个IP分片。

prev = NULL;
for (next = qp->q.fragments; next != NULL; next = next->next) {  if (FRAG_CB(next)->offset >= offset)  break; /* bingo! */  prev = next;
}

然后将skb插入到链表中,要注意fragments为空和不为空的情形,在下图中给出。

skb->next = next;
if (prev)  prev->next = skb;
else  qp->q.fragments = skb;

增加q.meat计数,表示已收到的IP分片的总长度;如果offset为0,则表明是第一个IP分片,设置q.last_in |= INET_FRAG_FIRST_IN。

qp->q.meat += skb->len;
if (offset == 0)  qp->q.last_in |= INET_FRAG_FIRST_IN;  

最后当满足一定条件时,进行IP重组。当收到了第一个和最后一个IP分片,且收到的IP分片的最大长度等于收到的IP分片的总长度时,表明所有的IP分片已收集齐,调用ip_frag_reasm重组包。具体的,当收到第一个分片(offset=0且MF=1)时设置q.last_in |= INET_FRAG_FIRST_IN;当收到最后一个分片(offset != 0且MF=0)时设置q.last_in |= INET_FRAG_LAST_IN。meat和len的区别在于,IP是不可靠传输,到达的IP分片不能保证顺序,而meat表示到达IP分片的总长度,len表示到达的IP分片中偏移最大的长度。所以当满足上述条件时,IP分片一定是收集齐了的。

if (qp->q.last_in == (INET_FRAG_FIRST_IN | INET_FRAG_LAST_IN) && qp->q.meat == qp->q.len)  return ip_frag_reasm(qp, prev, dev);  

以下图为例,原始IP报文分成了4片发送,假设收到了1, 3, 4分片,则此时q.last_in = INET_FRGA_FIRST_IN | INET_FRAG_LAST_IN,q.meat = 30,q.len = 50。表明还未收齐IP分片,等待IP分片2的到来。

这里还有一些特殊情况需要处理,它们可能是重新分片或传输时错误造成的,那就是IP分片互相间有重叠。为了避免这种情况发生,在插入IP分片前会处理掉这些重叠。
      第一种重叠是与前个分片重叠,即该分片的的偏移是从前个分片的范围内开始的,这种情况下i表示重叠部分的大小,offset+=i则将该分片偏移后移i个长度,从而与前个分片隔开,而且减少len,pskb_pull(skb, i),见下图图示。

if (prev) {  int i = (FRAG_CB(prev)->offset + prev->len) - offset;  if (i > 0) {  offset += i;  err = -EINVAL;  if (end <= offset)  goto err;  err = -ENOMEM;  if (!pskb_pull(skb, i))  goto err;  if (skb->ip_summed != CHECKSUM_UNNECESSARY)  skb->ip_summed = CHECKSUM_NONE;  }
}  

第二种重叠是与后个分片重叠,即该分片的的结束位置在后个分片的范围内,这种情况下i表示重叠部分的大小。后片重叠稍微复杂点,被i重叠的部分都要删除掉,如果i比较大,超过了分片长度,则整个分片都被覆盖,从q.fragments链表中删除。使用while处理i覆盖多个分片的情况。

while (next && FRAG_CB(next)->offset < end)

当整个分片被覆盖掉,从q.fragments中删除,并且由于减少了分片总长度,所以q.meat要减去删除分片的长度。

else {  struct sk_buff *free_it = next;  next = next->next;  if (prev)  prev->next = next;  else  qp->q.fragments = next;  qp->q.meat -= free_it->len;  frag_kfree_skb(qp->q.net, free_it, NULL);
}  

当只覆盖分片一部分时,offset+=i则将后个分片偏移后移i个长度,从而与该分片隔开,同时这样相当于减少了IP分片的长度,所以q.meat -= i;见下图图示,

if (i < next->len) {  if (!pskb_pull(next, i))  goto err;  FRAG_CB(next)->offset += i;  qp->q.meat -= i;  if (next->ip_summed != CHECKSUM_UNNECESSARY)  next->ip_summed = CHECKSUM_NONE;  break;
}

ip_frag_reasm函数实现IP分片的重组
      ip_frag_reasm传入的参数是prev,而重组完成后ip_defrag会将skb替换成重组后的新的skb,而在之前的操作中,skb插入了qp->q.fragments中,并且prev->next即为skb,因此第一步就是让skb变成qp->q.fragments,即IP分片的头部。

if (prev) {  head = prev->next;  fp = skb_clone(head, GFP_ATOMIC);  if (!fp)  goto out_nomem;  fp->next = head->next;  prev->next = fp;  skb_morph(head, qp->q.fragments);  head->next = qp->q.fragments->next;  kfree_skb(qp->q.fragments);  qp->q.fragments = head;
}

下面图示说明了上面代码段作用,skb是IP分片3,通过skb_clone拷贝一份3_copy替代之前的分片3,再通过skb_morph拷贝q.fragments到原始IP分片3,替代分片1,并释放分片1:

获取IP报头长度ihlen,head就是ip_defrag传入参数中的skb,并且它已经成为了IP分片队列的头部;len为整个IP报头+报文的总长度,qp->q.len是未分片前IP报文的长度。

ihlen = ip_hdrlen(head);
len = ihlen + qp->q.len; 

此时head就是skb,并且它的skb->data存储了第一个IP分片的内容,其它IP分片的内容将存储在紧接skb的空间 – frag_list;skb_push将skb->data回归原位,即未处理IP报头前的位置,因为之前的IP分片处理会调用skb_pull移走IP报头,将它回归原位是因为skb即将作为重组后的报文而被处理,那里会真正的skb_pull移走IP报头,再交由上层协议处理。

skb_shinfo(head)->frag_list = head->next;
skb_push(head, head->data - skb_network_header(head)); 

上面所说的frag_list是struct skb_shared_info的一个属性,在分配skb时分配在其后空间,通过skb_shinfo(skb)进行引用。下面分配skb大小size和skb_shared_info大小的代码摘自[net/core/skbuff.c]

size = SKB_DATA_ALIGN(size);
data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),gfp_mask, node);

这里要弄清楚sk_buff中线性存储区和paged buffer的区别,线性存储区就是存储报文,如果是分片后的,则只是第一个分片的内容;而paged buffer则存储其余分片的内容。而skb->data_len则表示paged buffer中内容长度,而skb->len则是paged buffer + linear buffer。下面这段代码就是根据余下的分片增加data_len和len计数。

for (fp=head->next; fp; fp = fp->next) {  head->data_len += fp->len;  head->len += fp->len;  ……
}

IP分片已经重组完成,分片从q.fragments链表移到了frag_list上,因此head->next和qp->q.fragments置为NULL。偏移量frag_off置0,总长度tot_len置为所有分片的长度和,这样,skb就相当于没有分片的完整的大数据包,继续向上传递。

head->next = NULL;
head->dev = dev;
……
iph = ip_hdr(head);
iph->frag_off = 0;
iph->tot_len = htons(len);
IP_INC_STATS_BH(net, IPSTATS_MIB_REASMOKS);
qp->q.fragments = NULL;

Linux内核分析 - 网络[八]:IP协议相关推荐

  1. Linux内核分析 - 网络[八补]:IP协议补充

    内核版本:2.6.34 在前一篇"IP协议"中对报文接收时IP层的处理进行了分析,本篇分析将针对报文发送时IP层的处理.       传输层处理完后,会调用ip_push_pend ...

  2. linux内核分析 网络九,“Linux内核分析”实验报告(九)

    一 Linux内核分析博客简介及其索引 本次实验简单的分析了计算机如何进行工作,并通过简单的汇编实例进行解释分析 在本次实验中 通过听老师的视频分析,和自己的学习,初步了解了进程切换的原理.操作系统通 ...

  3. Linux内核分析——第八周学习笔记

    实验作业:进程调度时机跟踪分析进程调度与进程切换的过程 20135313吴子怡.北京电子科技学院 [第一部分]理解Linux系统中进程调度的时机 1.Linux的调度程序是一个叫schedule()的 ...

  4. Linux内核分析 - 网络[十四]:IP选项

    内核版本:2.6.34       在发送报文时,可以调用函数setsockopt()来设置相应的选项,本文主要分析IP选项的生成,发送以及接收所执行的流程,选取了LSRR为例子进行说明,主要分为选项 ...

  5. Linux内核分析 - 网络[十六]:TCP三次握手

    内核:2.6.34       TCP是应用最广泛的传输层协议,其提供了面向连接的.可靠的字节流服务,但也正是因为这些特性,使得TCP较之UDP异常复杂,还是分两部分[创建与使用]来进行分析.这篇主要 ...

  6. Linux内核分析 - 网络[五]:vlan协议-802.1q

    内核版本:2.6.34 802.1q 1. 注册vlan网络系统子空间, [cpp] view plaincopy err = register_pernet_subsys(&vlan_net ...

  7. Linux内核分析 - 网络[四]:路由表

    路由表 在内核中存在路由表fib_table_hash和路由缓存表rt_hash_table.路由缓存表主要是为了加速路由的查找,每次路由查询都会先查找路由缓存,再查找路由表.这和cache是一个道理 ...

  8. Linux内核分析 - 网络[十二]:UDP模块 - socket

    内核版本:2.6.34 这部分内容在于说明socket创建后如何被内核协议栈访问到,只关注两个问题:sock何时插入内核表的,sock如何被内核访问的.对于核心的sock的插入.查找函数都给出了流程图 ...

  9. Linux内核分析 - 网络[十一]:ICMP模块

    内核版本:2.6.34 ICMP模块比较简单,要注意的是icmp的速率限制策略,向IP层传输数据ip_append_data()和ip_push_pending_frames(). 在net/ipv4 ...

最新文章

  1. java求职_Java 求职怎么积累知识才可以找到工作
  2. Abp框架之执行Update-Database 命令系列错误
  3. Visual Studio 2017全面上市
  4. Linux的ext4文件系统学习笔记
  5. 【JVM】三色标记法与读写屏障
  6. 你根本不懂数据仓库!对于80%的大公司数仓只是地基,它才是房子
  7. Play framework(二)
  8. ABB变频器维修,ABB变频器,ABB变频器配件FS300R12KE3/AGDR-61C 驱动模块APOW-01C 电源板AINP-01C 可控硅触发板
  9. RS-485半双工延时问题
  10. 微信公众号应用开发(一)
  11. mkv格式提取文件方法
  12. c语言学习指南app,c语言学习手册app
  13. Uva 11137 Ingenuous Cubrency(整数划分方案 背包)
  14. gif动图怎么制作?分享三个好用的方法
  15. dotnet 基于 debian 创建一个 docker 的 sdk 镜像
  16. 掌上智维技术支持 App Tech Support
  17. 数据显示强生新冠疫苗对德尔塔变异病毒有效;康方生物派安普利提交第三个上市申请 | 医药健闻...
  18. DeFi总锁仓金额突破36亿美元, OKEx赋能DeFi大盘点
  19. 【学习OpenCV4】图像金字塔总结
  20. ROS自学实践(11):利用map_server功能包创建自己的地图

热门文章

  1. Python wxpy通过ModBus控制电脑鼠标和键盘
  2. php代码的健壮性,代码健壮性的几点思考 - 逍遥客 - 51Testing软件测试网 51Testing软件测试网-软件测试人的精神家园...
  3. 使用pandas 按同一列名称合并,并解决concat() got an unexpected keyword argument ‘join_axes‘报错
  4. 解决scrapy不执行Request回调函数callback
  5. 服务器并发性能报告,一般的服务器瞬时并发应该怎么样才算是合格呢?
  6. html页面手机端console,vue项目以及独立HTML项目在手机端查看控制台日志 vconsole
  7. 性能测试——接口、协议篇
  8. 重构现有代码:Refactoring
  9. C++Event机制的简单实现
  10. Android 中像素px和dp的转化