邻居子系统之ARP协议数据处理过程
文章目录
- 无端ARP
- L2地址发生了变化
- L3地址重复检测
- 虚拟IP
- ARP选项
- arp_announce
- arp_ignore
- arp_filter
- arp_accept
- ARP报文格式
- 发送ARP请求: arp_solicit()
- 构造&&发送ARP报文: arp_send()
- 构造ARP报文: arp_create()
- 发送ARP报文: arp_xmit()
- 接收ARP报文: arp_rcv()
- 处理ARP报文: arp_process()
- ARP请求忽略识别: arp_ignore()
- ARP请求过滤: arp_filter()
- 被动学习: neigh_event_ns()
无端ARP
大多数情况下,主机发送ARP请求都是为了解析L3地址对应的L2地址,但是特殊情况下,也可以用ARP请求来通知接收方自己发生了一些事情,这种ARP请求也称为无端ARP。常见有下面三种情况。
L2地址发生了变化
默认情况下,如果一个主机L2地址发生了变化,同一个网络中的其它主句并不能立刻感知到,它们只能通过邻居子系统的过期机制探测,这会导致有一小段时间数据包是不可达的。如果愿意,发生这种情况是,本地主机主动发送一个ARP请求,触发接收方刷新缓存。
要注意的是内核并不会在L2地址变化时执行这个操作,如果需要,应该由用户态触发。
L3地址重复检测
当主机通过静态配置一个L3地址后,可以通过发送一个目的地址为配置的L3地址的ARP请求来识别同网段中是否有其它主机使用该地址,如果收到了应答,说明有人再使用,否则没有。
这只是提供了一种L3地址重复检测的方式,但是不太介意使用这种方式,DHCP方式获取IP地址才是正途。
虚拟IP
在服务器备份设计中,如果服务器A出现了问题,启动备份服务器B后,让B继承A的IP地址,然后B向网络中发送一个ARP请求,触发网络中其它主机更新对该IP地址的缓存,可以实现快速的切换。
这种场景的思想和L2地址发生变化的场景是一致的,都是为了让其它主机能够及时刷新自己的邻居项缓存。
ARP选项
在IPv4配置块中,有几个选项会影响ARP协议的行为,和IPv4配置块中的其它配置一样,这些选项可以是网络设备级别的配置,也可以是全局配置。
arp_announce
网络设备可以配置多个IP地址,当发送ARP请求时,到底应该选择使用哪个IP地址作为源地址封装到ARP请求报文中,该选项用于控制这种选择行为,可取三个值:
- 0:任何本地IP地址都可以;
- 1:如果可能,选择和目的地址位于同一个子网内的地址,否则按照配置2选择;
- 2:优先使用主地址;
arp_ignore
由于IP地址属于主机,但是是配置到网络设备上的,所以主机有可能在网络设备A上收到一个对网络设备B上的一个地址的查询(如这两个设备都有属于同一个子网的IP地址配置)。主机是否应该对这种ARP请求做出应答是可配置的,这种行为就是通过该选项控制。该选项可取的值以及它们的含义见下面的arp_ingore()函数。
arp_filter
当主机有两个网络设备,它们连接到同一个局域网,并且配置了同一个子网的IP地址时,对于子网内其它主机的的ARP请求,这两个网络设备都可以收到,该选项用于确定由哪个网络设备来对该请求做出响应。
arp_accept
如果本机没有发送ARP请求,但是收到了一个ARP响应,ARP协议使用该选项来决定是否使用该ARP响应中的源IP和源L2地址映射关系。
和其它选项不同,ARP使用的是IP配置块中all中的配置值,并非网络设备上的配置值。
ARP报文格式
struct arphdr
{__be16 ar_hrd; // L2层帧类型,取值ARPHRD_ETHER等__be16 ar_pro; // L3层地址类型,取值如ETH_P_IPunsigned char ar_hln; // L2层地址长度unsigned char ar_pln; // L3层地址长度__be16 ar_op; // ARP操作码,如ARPOP_REQUEST等// 下面的内容根据L2和L3地址长度不同有所不同,但是实现上,L3地址实际上就是IP地址(很多地方代码写死了)
#if 0/** Ethernet looks like this : This bit is variable sized however...*/unsigned char ar_sha[ETH_ALEN]; /* sender hardware address */unsigned char ar_sip[4]; /* sender IP address */unsigned char ar_tha[ETH_ALEN]; /* target hardware address */unsigned char ar_tip[4]; /* target IP address */
#endif
};
ARP协议报文的前8字节是固定的,后面部分根据协议地址长度以及L2层地址长度不同有所变化,可见在设计的时候,ARP不仅仅是为IPv4服务的,只不过实际中只有IPv4使用它而已。
发送ARP请求: arp_solicit()
在邻居子系统数据发送流程中有看到,ARP协议的邻居项操作集实现中。其中非常重要的一个接口就是Solicitation请求的发送,邻居子系统框架会在需要的时候回调该函数(在NUD_INCOMPLETE和NUD_PROBE状态)。
static void arp_solicit(struct neighbour *neigh, struct sk_buff *skb)
{__be32 saddr = 0;u8 *dst_ha = NULL;struct net_device *dev = neigh->dev;__be32 target = *(__be32*)neigh->primary_key; // 数据包的下一跳地址int probes = atomic_read(&neigh->probes);struct in_device *in_dev = in_dev_get(dev); // 找到该网络设备的IPv4配置块if (!in_dev)return;// 根据该网络设备的IPv4配置项arp_announce选择一个出口IP地址switch (IN_DEV_ARP_ANNOUNCE(in_dev)) {default:case 0:// 0配置为默认值,此时优先选择数据包的源地址if (skb && inet_addr_type(dev_net(dev), ip_hdr(skb)->saddr) == RTN_LOCAL)saddr = ip_hdr(skb)->saddr;break;case 1:// 1配置表示必须选择一个和出口数据包在同一个子网的源IP地址if (!skb)break;saddr = ip_hdr(skb)->saddr;if (inet_addr_type(dev_net(dev), saddr) == RTN_LOCAL) {// target、saddr和本地网络设备上的某个IP地址均属于同一个子网if (inet_addr_onlink(in_dev, target, saddr))break;}saddr = 0;break;case 2:/* Avoid secondary IPs, get a primary/preferred one */break;}// 可见,arp_announce配置仅仅是一种建议,如果都无法选出,则用inet_select_addr()选择一个if (in_dev)in_dev_put(in_dev);if (!saddr)// 优先选择一个和target属于同一个子网的主IP地址saddr = inet_select_addr(dev, target, RT_SCOPE_LINK);if ((probes -= neigh->parms->ucast_probes) < 0) {// 单播场景if (!(neigh->nud_state&NUD_VALID))printk(KERN_DEBUG "trying to ucast probe in NUD_INVALID\n");dst_ha = neigh->ha;read_lock_bh(&neigh->lock);} else if ((probes -= neigh->parms->app_probes) < 0) {#ifdef CONFIG_ARPD// 用户态的arp实现neigh_app_ns(neigh);
#endifreturn;}// 构造并发送ARP请求报文,如果dst_ha为NULL则发送ARP广播arp_send(ARPOP_REQUEST, ETH_P_ARP, target, dev, saddr,dst_ha, dev->dev_addr, NULL);if (dst_ha)read_unlock_bh(&neigh->lock);
}
arp_solicit()最重要的工作就是为ARP请求选择源IP地址、目的L2地址(单播或者广播)。
源IP地址的选择过程受ARP选项arp_nonounce的影响。选择策略如下:
- 如果arp_nonunce为0(默认值),若IP包头中的源IP地址就是本地地址,则选中它,否则按照步骤3选择;
- 如果arp_nonunce为1,若IP包头中的源IP地址就是本地地址,并且它和本地网络设备上的某个IP地址处于同一个子网,则选中它,否则按照步骤3选择;
- 使用inet_select_addr()选择一个IP地址,该函数会优先选择一个和输入地址(下一跳地址)位于同一个子网的地址,否则任意选择一个主地址;
构造&&发送ARP报文: arp_send()
/** Create and send an arp packet.*/
void arp_send(int type, int ptype, __be32 dest_ip, struct net_device *dev, __be32 src_ip,const unsigned char *dest_hw, const unsigned char *src_hw, const unsigned char *target_hw)
{struct sk_buff *skb;/** No arp on this interface.*/if (dev->flags&IFF_NOARP)return;// 构造ARP请求报文skb = arp_create(type, ptype, dest_ip, dev, src_ip,dest_hw, src_hw, target_hw);if (skb == NULL) {return;}// 发送ARP报文arp_xmit(skb);
}
构造ARP报文: arp_create()
/** Create an arp packet. If (dest_hw == NULL), we create a broadcast* message.*/
@type: ARP报文类型,如ARP_REQUEST;
@ptype: L2帧首部的协议字段;
@dest_ip: 下一跳IP地址;
@src_ip: 本机IP地址;
@dest_hw: 下一跳L2层地址,该地址会作为L2帧的目的地址,所以如果全0,会设置为广播
@src_hw: 本机L2层地址;
@target_hw: ARP报文中填充的目标主机L2层地址
struct sk_buff *arp_create(int type, int ptype, __be32 dest_ip, struct net_device *dev, __be32 src_ip,const unsigned char *dest_hw, const unsigned char *src_hw, const unsigned char *target_hw)
{struct sk_buff *skb;struct arphdr *arp;unsigned char *arp_ptr;/** Allocate a buffer*/// arp_hdr_len()为标准ARP首部+2个IP地址+2个L2层地址长度skb = alloc_skb(arp_hdr_len(dev) + LL_ALLOCATED_SPACE(dev), GFP_ATOMIC);if (skb == NULL)return NULL;skb_reserve(skb, LL_RESERVED_SPACE(dev));skb_reset_network_header(skb);arp = (struct arphdr *) skb_put(skb, arp_hdr_len(dev));skb->dev = dev;skb->protocol = htons(ETH_P_ARP);if (src_hw == NULL) // 源地址src_hw = dev->dev_addr;if (dest_hw == NULL) // 如果不指定目的地址构造的就是广播报文dest_hw = dev->broadcast;// 调用网络设备提供的接口填充ARP报文的L2帧首部if (dev_hard_header(skb, dev, ptype, dest_hw, src_hw, skb->len) < 0)goto out;/** Fill out the arp protocol part.** The arp hardware type should match the device type, except for FDDI,* which (according to RFC 1390) should always equal 1 (Ethernet).*//** Exceptions everywhere. AX.25 uses the AX.25 PID value not the* DIX code for the protocol. Make these device structure fields.*/// 根据不同的设备类型,填充标准ARP报文首部字段switch (dev->type) {default:arp->ar_hrd = htons(dev->type);arp->ar_pro = htons(ETH_P_IP);break;
...}arp->ar_hln = dev->addr_len;arp->ar_pln = 4;arp->ar_op = htons(type);arp_ptr=(unsigned char *)(arp+1);memcpy(arp_ptr, src_hw, dev->addr_len);arp_ptr += dev->addr_len;memcpy(arp_ptr, &src_ip, 4);arp_ptr += 4;if (target_hw != NULL)memcpy(arp_ptr, target_hw, dev->addr_len);elsememset(arp_ptr, 0, dev->addr_len);arp_ptr += dev->addr_len;memcpy(arp_ptr, &dest_ip, 4);return skb;
out:kfree_skb(skb);return NULL;
}
发送ARP报文: arp_xmit()
注意,经过过滤器后直接调用的是设备接口层的发送函数dev_queue_xmit(),不会再进入邻居子系统了。
/** Send an arp packet.*/
void arp_xmit(struct sk_buff *skb)
{/* Send it off, maybe filter it using firewalling first. */NF_HOOK(NFPROTO_ARP, NF_ARP_OUT, skb, NULL, skb->dev, dev_queue_xmit);
}
接收ARP报文: arp_rcv()
/** Receive an arp request from the device layer.*/
static int arp_rcv(struct sk_buff *skb, struct net_device *dev,struct packet_type *pt, struct net_device *orig_dev)
{struct arphdr *arp;/* ARP header, plus 2 device addresses, plus 2 IP addresses. */if (!pskb_may_pull(skb, arp_hdr_len(dev))) // 确保报文有足够的ARP报文首部goto freeskb;// 校验ARP报文首部,确保数据是发给本设备的arp = arp_hdr(skb);if (arp->ar_hln != dev->addr_len || dev->flags & IFF_NOARP ||skb->pkt_type == PACKET_OTHERHOST || skb->pkt_type == PACKET_LOOPBACK ||arp->ar_pln != 4)goto freeskb;if ((skb = skb_share_check(skb, GFP_ATOMIC)) == NULL)goto out_of_mem;// 设置skb控制块为0memset(NEIGH_CB(skb), 0, sizeof(struct neighbour_cb));// 经过过滤器后调用arp_process()return NF_HOOK(NFPROTO_ARP, NF_ARP_IN, skb, dev, NULL, arp_process);freeskb:kfree_skb(skb);
out_of_mem:return 0;
}
处理ARP报文: arp_process()
static int arp_process(struct sk_buff *skb)
{struct net_device *dev = skb->dev;struct in_device *in_dev = in_dev_get(dev);struct arphdr *arp;unsigned char *arp_ptr;struct rtable *rt;unsigned char *sha;__be32 sip, tip;u16 dev_type = dev->type;int addr_type;struct neighbour *n;struct net *net = dev_net(dev);/* arp_rcv below verifies the ARP header and verifies the device* is ARP'able.*/if (in_dev == NULL)goto out;arp = arp_hdr(skb);// 根据设备类型,再次校验ARP报文首部字段,我们只关注default分支,即以太网switch (dev_type) {default:if (arp->ar_pro != htons(ETH_P_IP) || htons(dev_type) != arp->ar_hrd)goto out;break;
...}// 只处理ARP请求和ARP应答报文if (arp->ar_op != htons(ARPOP_REPLY) && arp->ar_op != htons(ARPOP_REQUEST))goto out;/** Extract fields*/arp_ptr= (unsigned char *)(arp+1); // 实际跳过的是sizeof(arphdr)字节sha = arp_ptr; // 发送方L2层地址保存在sha中arp_ptr += dev->addr_len;memcpy(&sip, arp_ptr, 4); // 发送方IP地址保存在sip中arp_ptr += 4;arp_ptr += dev->addr_len;memcpy(&tip, arp_ptr, 4); // 目的方IP地址保存在tip中
/** Check for bad requests for 127.x.x.x and requests for multicast* addresses. If this is one such, delete it.*/// 异常的目的地址,多播地址是不需要ARP查询的if (ipv4_is_loopback(tip) || ipv4_is_multicast(tip))goto out;// 仔细阅读注释
/** Process entry. The idea here is we want to send a reply if it is a* request for us or if it is a request for someone else that we hold* a proxy for. We want to add an entry to our cache if it is a reply* to us or if it is a request for our address.* (The assumption for this last is that if someone is requesting our* address, they are probably intending to talk to us, so it saves time* if we cache their address. Their address is also probably not in* our cache, since ours is not in their cache.)** Putting this another way, we only care about replies if they are to* us, in which case we add them to the cache. For requests, we care* about those for us and those for our proxies. We reply to both,* and in the case of requests for us we add the requester to the arp* cache.*//* Special case: IPv4 duplicate address detection packet (RFC2131) */// 无端ARP中的IP地址重复检测场景,如果允许,回复ARP响应报文if (sip == 0) {if (arp->ar_op == htons(ARPOP_REQUEST) &&inet_addr_type(net, tip) == RTN_LOCAL &&!arp_ignore(in_dev, sip, tip))arp_send(ARPOP_REPLY, ETH_P_ARP, sip, dev, tip, sha, dev->dev_addr, sha);goto out;}// 对于ARP请求报文,路由查询可以过滤掉本地路由不应该接收的报文if (arp->ar_op == htons(ARPOP_REQUEST) &&ip_route_input(skb, tip, sip, 0, dev) == 0) {rt = skb->rtable;addr_type = rt->rt_type;if (addr_type == RTN_LOCAL) { // ARP请求来自本地子网int dont_send = 0;if (!dont_send)dont_send |= arp_ignore(in_dev,sip,tip); // 检查是否需要忽略该报文if (!dont_send && IN_DEV_ARPFILTER(in_dev)) // 如果开启ARP报文过滤,则进行过滤dont_send |= arp_filter(sip,tip,dev);if (!dont_send) {// ARP请求报文可以接收,作为接收方,也可以从中获取到发送方的地址映射关系,// 使用这个信息更新或者创建邻居项,相比于主动发送,这种方式也叫被动学习n = neigh_event_ns(&arp_tbl, sha, &sip, dev);// 实际上即使本地缓存失败,也应该向发送方回应ARP响应if (n) {arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);neigh_release(n);}}goto out;} else if (IN_DEV_FORWARD(in_dev)) { // ARP报文不是给本机的,并且设备开启了转发能力,根据是否启用代理功能对数据包进行转发,// 这部分逻辑分析见“邻居子系统之代理功能”if (addr_type == RTN_UNICAST && rt->u.dst.dev != dev &&(arp_fwd_proxy(in_dev, rt) || pneigh_lookup(&arp_tbl, net, &tip, dev, 0))) {n = neigh_event_ns(&arp_tbl, sha, &sip, dev);if (n)neigh_release(n);if (NEIGH_CB(skb)->flags & LOCALLY_ENQUEUED ||skb->pkt_type == PACKET_HOST ||in_dev->arp_parms->proxy_delay == 0) {arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);} else {pneigh_enqueue(&arp_tbl, in_dev->arp_parms, skb);in_dev_put(in_dev);return 0;}goto out;}}}// ARP响应报文,或者是不该接收的ARP请求报文(路由查询失败),到这里更新本地邻居表/* Update our ARP tables */// 根据ARP报文的源IP地址查询邻居项,最后一个参数为0,查询失败不会创建邻居项n = __neigh_lookup(&arp_tbl, &sip, dev, 0);if (IPV4_DEVCONF_ALL(dev_net(dev), ARP_ACCEPT)) { // 判断的是all配置中的设置/* Unsolicited ARP is not accepted by default.It is possible, that this option should be enabled for somedevices (strip is candidate)*/// 下面情况成立,说明本机没有对应的ARP请求,但是收到了一个ARP响应,// 如果arp_accept选项打开,那么也用该信息更新邻居表if (n == NULL &&arp->ar_op == htons(ARPOP_REPLY) &&inet_addr_type(net, sip) == RTN_UNICAST)n = __neigh_lookup(&arp_tbl, &sip, dev, 1);}if (n) { // 更新邻居项状态int state = NUD_REACHABLE;int override;/* If several different ARP replies follows back-to-back,use the FIRST one. It is possible, if several proxyagents are active. Taking the first reply preventsarp trashing and chooses the fastest router.*/// 锁定期内只按照第一个更新override = time_after(jiffies, n->updated + n->parms->locktime);/* Broadcast replies and request packetsdo not assert neighbour reachability.*/// 默认是更新到NUD_REACHABLE,但是对于ARP请求和广播的ARP响应,仅仅更新到NUD_STALEif (arp->ar_op != htons(ARPOP_REPLY) || skb->pkt_type != PACKET_HOST)state = NUD_STALE;neigh_update(n, sha, state, override ? NEIGH_UPDATE_F_OVERRIDE : 0);neigh_release(n);}out:if (in_dev)in_dev_put(in_dev);kfree_skb(skb);return 0;
}
ARP请求忽略识别: arp_ignore()
该函数用于识别是否应该忽略收到的ARP请求报文。
// 调用者已经确保tip是本地地址
static int arp_ignore(struct in_device *in_dev, __be32 sip, __be32 tip)
{int scope;// 根据网络设备上的arp_ignore选项做不同处理switch (IN_DEV_ARP_IGNORE(in_dev)) {case 0: // 任何本地地址的ARP请求都应该应答(默认值)return 0;case 1: // 目的地址必须是配置在输入接口上的地址才应答sip = 0;scope = RT_SCOPE_HOST;break;case 2:// 目的地址必须是配置在数据接口上,并且和sip必须是同一个子网才应答scope = RT_SCOPE_HOST;break;case 3: /* Do not reply for scope host addresses */sip = 0;scope = RT_SCOPE_LINK;break;case 4: /* Reserved */case 5:case 6:case 7:return 0;case 8:// 不应答return 1;default:return 0;}// 进行目的地址确认return !inet_confirm_addr(in_dev, sip, tip, scope);
}
ARP请求过滤: arp_filter()
该函数是对前面提到的arp_filter的实现。该函数会使得只有本机可以到达发送方,并且是出口设备收到ARP请求时才会处理ARP请求报文。
static int arp_filter(__be32 sip, __be32 tip, struct net_device *dev)
{struct flowi fl = { .nl_u = { .ip4_u = { .daddr = sip,.saddr = tip } } };struct rtable *rt;int flag = 0;/*unsigned long now; */struct net *net = dev_net(dev);if (ip_route_output_key(net, &rt, &fl) < 0)return 1;if (rt->u.dst.dev != dev) {NET_INC_STATS_BH(net, LINUX_MIB_ARPFILTER);flag = 1;}ip_rt_put(rt);return flag;
}
被动学习: neigh_event_ns()
struct neighbour *neigh_event_ns(struct neigh_table *tbl, u8 *lladdr, void *saddr,struct net_device *dev)
{// 先查询邻居表中是否有该邻居向,如果没有,该函数会新建一个(根据lladdr)struct neighbour *neigh = __neigh_lookup(tbl, saddr, dev, lladdr || !dev->addr_len);if (neigh)// 将邻居项状态更新未NUD_STALE,特别注意,如果原来是NUD_REACHABLE是不会将其变为NUD_STALE的neigh_update(neigh, lladdr, NUD_STALE, NEIGH_UPDATE_F_OVERRIDE);return neigh;
}
邻居子系统之ARP协议数据处理过程相关推荐
- Linux内核邻接子系统(arp协议)的工作原理
主要参考了<深入linux内核架构>和<精通Linux内核网络>相关章节 文章目录 Linux内核邻接子系统(二层到三层) 邻接子系统的核心 struct neighbour ...
- 邻居子系统:地址解析协议
2019独角兽企业重金招聘Python工程师标准>>> 无端ARP 一般来说,发出一个ARPOP_REQUEST是由于发送方想和一个IP地址通信,需要找到其对应的L2地址.但有时 ...
- Linux_网络_数据链路层协议 MAC帧/ARP协议 (以太网通信原理,MAC地址与IP地址的区分,MTU对IP/TCP/IP的影响,ARP协议及其通信过程)
文章目录 1. 以太网(基于碰撞区与碰撞检测的局域网通信标准) 2. 以太网的帧格式(MAC帧) MAC地址,IP地址的区分 MTU MTU对IP协议的影响 MTU对TCP/UDP协议的影响 3.AR ...
- linux网络协议栈源码分析 - 邻居子系统邻居状态转移
1.邻居项状态转移图 邻居项主要的状态转移如下(省略邻居项垃圾回收及转移原因,更权威详细的状态转移图参看<深入理解LINUX网络技术内幕>P648 "图26-13: NUD状态间 ...
- 【计算机网络】网络层 : ARP 协议 ( 使用 ARP 协议查找 目的主机 / 路由器 物理地址 )★
文章目录 一.ARP 协议 二.ARP 协议 使用过程 三.ARP 协议 四种情况 四.ARP 协议规律 五.ARP 协议 计算示例 一.ARP 协议 物理地址需求 : 在 数据链路层 传输数据帧时 ...
- 了解TCP协议,IP协议、ICMP协议和ARP协议(TCP报文,TCP的分成管理,TCP与UDP,TCP的三次握手四次挥手原理)
文章目录 了解TCP/IP协议 TCP报文格式 TCP/IP 的分层管理 TCP与UDP TCP的三次握手与四次挥手 为什么要三次握手? 为什么要四次挥手? IP数据包格式 ICMP协议 ICMP协议 ...
- 计算机网络之网络层:4、ARP协议
网络层:4.ARP协议 ARP协议产生的原因: 同一网络的ARP协议响应过程: 不同网络的ARP协议响应过程: 总结: ARP协议产生的原因: 当网络层交付数据分组给数据链路层时,数据链路层需要对IP ...
- 【计算机网络】ARP协议工作原理
地址解析协议ARP 一 发送数据的过程 在学习ARP协议的工作原理之前,我们需要先知道为什么需要ARP协议,它在数据传输过程中有怎样的作用. 以下是计算机网络中发送数据的一个大致过程. 首先要知道,源 ...
- ARP协议详解:了解数据包转发与映射机制背后的原理
数据来源 一.广播与广播域概述 1.广播与广播域 广播:将广播地址做为目标地址的数据帧 广播域:网络中能接收到同一个广播所有节点的集合(广播域越小越好,收到的垃圾广播越 ...
最新文章
- java 拆分类_拆分或不拆分类(用Java)
- 【深度学习入门到精通系列】开始恢复更新通知~!
- 利用Python编辑一个发送邮件的脚本
- 简单易懂棒棒哒的视频传输工具!
- Backpropagation 总结
- html 编辑xml,编辑XML\HTML时取消浏览“amp”
- Oracle 多行合并一行 方法
- Docker 实战教程之从入门到提高 (五)
- 56、servlet3.0-与SpringMVC整合分析
- Modularity QuickStart学习
- linux中vim编辑器_为什么Vim爱好者喜欢Herbstluftwm Linux窗口管理器
- python中head_Python(Head First)学习笔记:六
- string与wstring互转
- 5月博客恢复更新的通知
- VBXtraLib 1.0 下载
- Java学习笔记分享之Dubbo篇
- sql server 读写txt文件
- Python根据身份证得知性别
- JS数据结构中的集合结构详解
- 【51单片机】霹雳灯实验代码