DVR分布式路由
1. 背景
没有使用DVR的场景:
从图中可以明显看到东西向和南北向的流量会集中到网络节点,这会使网络节点成为瓶颈。
如果启用DVR,如下图:
对于东西向的流量, 流量会直接在计算节点之间传递。
2.部署以及流量走向
2.1东西向流量
VM1 (10.0.1.5 Net1) ping VM2 (10.0.2.5 Net2)
1) VM1 (10.0.1.5) -> qr (10.0.1.1)
VM1 根据默认路由发送arp(广播)请求qr网关的地址,请求到网关地址后,icmp报文走向qr口。
(关于报文格式的一点解释,当VM1 ping VM2时,报文的源/目的IP始终不变,报文的源/目的MAC则会根据不同的路段而变化。)
同时,br-tun网桥会丢弃目的地址是interface_distributed接口的arp广播,不至于让不必要的流量流向外面:
# ovs-ofctl dump-flows br-tun
NXST_FLOW reply (xid=0x4):
...
cookie=0x0, duration=.432s, table=, n_packets=, n_bytes=, idle_age=, priority=,arp,dl_vlan=,arp_tpa=10.0.1.1 actions=drop
...
2)qr (10.0.1.1) -> qr (10.0.2.1)
进入qrouter namespace后,利用linux内核的高级路由功能,查看路由规则。
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip rule
: from all lookup local
: from all lookup main
: from all lookup default
: from 10.0.1.5 lookup
: from 10.0.2.3 lookup
: from 10.0.1.1/ lookup
: from 10.0.1.1/ lookup
: from 10.0.2.1/ lookup
先查看main表:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip route list table main
10.0.1.0/ dev qr-ddbdc784-d7 proto kernel scope link src 10.0.1.1
10.0.2.0/ dev qr-001d0ed9- proto kernel scope link src 10.0.2.1
169.254.31.28/ dev rfp-0fbb351e-a proto kernel scope link src 169.254.31.28
在main表中满足以上路由,因此会从另一个qr口出去。(Q1:不同计算节点的同一子网下qr口ip是相同的吗?)
3)qr -> br-int
之后需要去查询10.0.2.5的MAC地址, MAC是由neutron使用静态ARP的方式设定的,由于Neutron知道所有VM的信息,因此他可以事先设定好静态ARP:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip nei
10.0.1.5 dev qr-ddbdc784-d7 lladdr fa::3e:da::6d PERMANENT
10.0.2.3 dev qr-001d0ed9- lladdr fa::3e:a4:fc: PERMANENT
10.0.1.6 dev qr-ddbdc784-d7 lladdr fa::3e:9f:: PERMANENT
10.0.2.2 dev qr-001d0ed9- lladdr fa::3e::: PERMANENT
10.0.2.5 dev qr-001d0ed9- lladdr fa::3e:::b8 PERMANENT
10.0.1.4 dev qr-ddbdc784-d7 lladdr fa::3e:da:e3:6e PERMANENT
10.0.1.7 dev qr-ddbdc784-d7 lladdr fa::3e::b8:ec PERMANENT
169.254.31.29 dev rfp-0fbb351e-a lladdr :0d:9f:::c6 STALE
此时,报文进入br-int,根据table 0 进行normal转发:
cookie=0x0, duration=.644s, table=, n_packets=, n_bytes=, idle_age=, priority= actions=NORMAL
normal动作则表示根据OVS fdb表项匹配目的MAC地址,从而决定该报文要往哪个端口发送。如果没有该MAC的fdb表项记录,则进行泛洪,对除了报文进来的端口以外的所有同属于一个vlan的端口发送该报文。例如:
# ovs-appctl fdb/show br-int
port VLAN MAC Age
LOCAL da:::cd:fb:
:::a9:b8:b0
:::a9:b8:b1
因此如果此时VM2也在该compute node上,则VM2也会直接收到该报文,不需要走br-tun(有了VM2的MAC fdb表项记录后)。否则,继续往br-tun走。
4)br-int -> br-tun -> 出compute node 1
然后报文从br-int进入br-tun匹配流表:
cookie=0x0, duration=.51s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,in_port= actions=resubmit(,)
cookie=0x0, duration=.526s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,in_port= actions=resubmit(,)
cookie=0x0, duration=.052s, table=, n_packets=, n_bytes=, idle_age=, priority=,in_port= actions=resubmit(,)
cookie=0x0, duration=.704s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority= actions=drop
cookie=0x0, duration=.811s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_vlan=,dl_src=fa::3e:::af actions=mod_dl_src:fa::3f:fe::e9,resubmit(,)
cookie=0x0, duration=.141s, table=, n_packets=, n_bytes=, idle_age=, priority=,dl_vlan=,dl_src=fa::3e::b4: actions=mod_dl_src:fa::3f:fe::e9,resubmit(,)
cookie=0x0, duration=.962s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_vlan=,dl_dst=fa::3e:::af actions=drop
cookie=0x0, duration=.297s, table=, n_packets=, n_bytes=, idle_age=, priority=,dl_vlan=,dl_dst=fa::3e::b4: actions=drop
cookie=0x0, duration=.115s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,arp,dl_vlan=,arp_tpa=10.0.1.1 actions=drop
cookie=0x0, duration=.449s, table=, n_packets=, n_bytes=, idle_age=, priority=,arp,dl_vlan=,arp_tpa=10.0.2.1 actions=drop
cookie=0x0, duration=.22s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority= actions=resubmit(,)
先匹配table 0,然后匹配table 1,它会把源MAC地址(另一个qr口)改为全局唯一与计算节点绑定的MAC。
这个全局唯一和计算节点绑定的MAC地址,是由neutron全局分配的,数据库中可以看到这个MAC是每个host一个:
它的base MAC是可以在neutron.conf中配置的:
同时,后面的两条table1会丢弃目标ip是interface_distributed接口的ARP和目的MAC是interface_distributed的包,以防止虚机发送给本地IP的包不会被转发到网络中。
然后继续查询table 2,table 2是vxlan表,如果是广播包就会查询表22,如果是单播包就查询table 20
cookie=0x0, duration=.554s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_dst=:::::/::::: actions=resubmit(,)
cookie=0x0, duration=.406s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_dst=:::::/::::: actions=resubmit(,)
广播MAC地址是FF:FF:FF:FF:FF:FF,组播MAC地址以01-00-5E开头(具体可查看http://book.51cto.com/art/200904/120471.htm),匹配规则满足CIDR。
ICMP包是单播包,因此会查询表20,由于开启了L2 pop功能,在表20中会事先学习到应该转发到哪个VTEP:
cookie=0x0, duration=.308s, table=, n_packets=, n_bytes=, idle_age=, priority=,dl_vlan=,dl_dst=fa::3e:::b8 actions=strip_vlan,set_tunnel:0x3eb,output:
(Q2:社区br-tun下面的隧道口是如何与物理口建立联系的?)
5)进compute node 2 -> br-tun
在br-tun中,从外面进入的报文将首先匹配以下table0表:
cookie=0x0, duration=.658s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,in_port= actions=resubmit(,)
cookie=0x0, duration=.368s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,in_port= actions=resubmit(,)
cookie=0x0, duration=.808s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,in_port= actions=resubmit(,)
cookie=0x0, duration=.675s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority= actions=drop
在table 4中,会将对应的vni改为本地vlan id,之后查询表9:
cookie=0x0, duration=.871s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,tun_id=0x3eb actions=mod_vlan_vid:,resubmit(,)
cookie=0x0, duration=.732s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,tun_id=0x3e9 actions=mod_vlan_vid:,resubmit(,)
cookie=0x0, duration=.115s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority= actions=drop
在表9中,如果发现包的源地址是全局唯一并与计算节点绑定的MAC地址,就将其转发到br-int:
cookie=0x0, duration=.507s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_src=fa::3f:fe::e9 actions=output:
cookie=0x0, duration=.782s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_src=fa::3f::3f:a7 actions=output:
cookie=0x0, duration=.23s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority= actions=resubmit(,)
6)br-tun -> br-int
进入br-int后,在table 0中,如果是全局唯一并与计算节点绑定的MAC地址就查询table 1,否则就正常转发;
在table 1中,事先设定好了flow,如果目的MAC是发送给VM2,就将源MAC改为Net2的网关MAC地址(qr口)(Q3:修改源MAC的原因?为了报文能返回)。
cookie=0x0, duration=.903s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,in_port=,dl_src=fa::3f::3f:a7 actions=resubmit(,)
cookie=0x0, duration=.627s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,in_port=,dl_src=fa::3f:fe::e9 actions=resubmit(,)
cookie=0x0, duration=.053s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority= actions=NORMAL
cookie=0x0, duration=.695s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_vlan=,dl_dst=fa::3e:::b8 actions=strip_vlan,mod_dl_src:fa::3e::b4:,output:
cookie=0x0, duration=.515s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,dl_vlan=,dl_dst=fa::3e::b8:ec actions=strip_vlan,mod_dl_src:fa::3e:::af,output:
cookie=0x0, duration=.369s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,ip,dl_vlan=,nw_dst=10.0.1.0/ actions=strip_vlan,mod_dl_src:fa::3e:::af,output:
cookie=0x0, duration=.559s, table=, n_packets=, n_bytes=, idle_age=, hard_age=, priority=,ip,dl_vlan=,nw_dst=10.0.2.0/ actions=strip_vlan,mod_dl_src:fa::3e::b4:,output:
7)br-int -> VM2
至此,VM2就会收到VM1的包了。从通信的过程可以看到,跨网段的东西向流量没有经过网络节点。
2.2 南北向流量(VM有floating ip)
VM1 (local ip:10.0.1.5 , floating ip: 172.24.4.5)ping 8.8.8.8
1)VM1 (10.0.1.5) -> qr (10.0.1.1)
与上面一致
2) qr (10.0.1.1) -> rfp (169.254.31.28) -> fpr (169.254.31.29)
进入qrouter namespace后:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip rule
: from all lookup local
: from all lookup main
: from all lookup default
: from 10.0.1.5 lookup
: from 10.0.2.3 lookup
: from 10.0.1.1/ lookup
: from 10.0.1.1/ lookup
: from 10.0.2.1/ lookup
在main表中没有合适的路由:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip route list table main
10.0.1.0/ dev qr-ddbdc784-d7 proto kernel scope link src 10.0.1.1
10.0.2.0/ dev qr-001d0ed9- proto kernel scope link src 10.0.2.1
169.254.31.28/ dev rfp-0fbb351e-a proto kernel scope link src 169.254.31.28
由于包是从10.0.1.5发来的之后会查看table 16,包会命中这条路由。
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip route list table
default via 169.254.31.29 dev rfp-0fbb351e-a
路由之后会通过netfilter的POSTROUTING链中进行SNAT:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa iptables -nvL -t nat
...
Chain neutron-l3-agent-float-snat ( references)
pkts bytes target prot opt in out source destination
SNAT all -- * * 10.0.2.3 0.0.0.0/ to:172.24.4.7
SNAT all -- * * 10.0.1.5 0.0.0.0/ to:172.24.4.5
...
之后就可以看到包会通过rfp-0fbb351e-a发送给169.254.31.29。
端口rfp-0fbb351e-a和fpr-0fbb351e-a是一对veth pair。在fip namespace中你可以看到这个接口:
3) fpr (169.254.31.29) -> fg (172.24.4.6)
到了fip的namespace之后,会查询路由, 在main表里有通往公网的默认路由:
# ip netns exec fip-fbd46644-c70f--a414-862a00cbd1d2 ip route
default via 172.24.4.1 dev fg-081d537b-
169.254.31.28/ dev fpr-0fbb351e-a proto kernel scope link src 169.254.31.29
172.24.4.0/ dev fg-081d537b- proto kernel scope link src 172.24.4.6
172.24.4.5 via 169.254.31.28 dev fpr-0fbb351e-a
172.24.4.7 via 169.254.31.28 dev fpr-0fbb351e-a
通过fg-081d537b-06发送到br-ex。这是从虚机发送到公网的过程。(Q4:br-ex上的流表是什么样的?如果没有br-ex,直接走br-int,流表会有什么变化?)
外网 ping VM1 ( floating ip: 172.24.4.5)
1)fip namespace
此时fip的namespace会做arp代理:
(Q5:arp代理的作用?外部arp广播报文进入fip ns,查询172.24.4.5的mac地址,由于arp报文无法跨路由器传播,而且该ip在qrouter ns里。)
# ip netns exec fip-fbd46644-c70f--a414-862a00cbd1d2 sysctl net.ipv4.conf.fg-081d537b-.proxy_arp
net.ipv4.conf.fg-081d537b-.proxy_arp =
可以看到接口的arp代理是打开的,对于floating ip 有以下路由:
# ip netns exec fip-fbd46644-c70f--a414-862a00cbd1d2 ip route
...
172.24.4.5 via 169.254.31.28 dev fpr-0fbb351e-a
172.24.4.7 via 169.254.31.28 dev fpr-0fbb351e-a
...
ARP会去通过VETH Pair到IR(Inter Router)的namespace中去查询,在IR中可以看到,接口rfp-0fbb351e-a配置了floating ip:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip addr
: lo: <LOOPBACK,UP,LOWER_UP> mtu qdisc noqueue state UNKNOWN group default
link/loopback ::::: brd :::::
inet 127.0.0.1/ scope host lo
valid_lft forever preferred_lft forever
inet6 ::/ scope host
valid_lft forever preferred_lft forever
: rfp-0fbb351e-a: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu qdisc pfifo_fast state UP group default qlen
link/ether ea:5c::9a::9c brd ff:ff:ff:ff:ff:ff
inet 169.254.31.28/ scope global rfp-0fbb351e-a
valid_lft forever preferred_lft forever
inet 172.24.4.5/ brd 172.24.4.5 scope global rfp-0fbb351e-a
valid_lft forever preferred_lft forever
inet 172.24.4.7/ brd 172.24.4.7 scope global rfp-0fbb351e-a
valid_lft forever preferred_lft forever
inet6 fe80::e85c:56ff:fe9a:369c/ scope link
valid_lft forever preferred_lft forever
: qr-ddbdc784-d7: <BROADCAST,UP,LOWER_UP> mtu qdisc noqueue state UNKNOWN group default
link/ether fa::3e:::af brd ff:ff:ff:ff:ff:ff
inet 10.0.1.1/ brd 10.0.1.255 scope global qr-ddbdc784-d7
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe66:13af/ scope link
valid_lft forever preferred_lft forever
: qr-001d0ed9-: <BROADCAST,UP,LOWER_UP> mtu qdisc noqueue state UNKNOWN group default
link/ether fa::3e::b4: brd ff:ff:ff:ff:ff:ff
inet 10.0.2.1/ brd 10.0.2.255 scope global qr-001d0ed9-
valid_lft forever preferred_lft forever
inet6 fe80::f816:3eff:fe69:b405/ scope link
valid_lft forever preferred_lft forever
因此fip的namespace会对这个floating ip进行ARP回应。
外部发起目标地址为floating ip的请求后,fip会将其转发到IR中,IR的RPOROUTING链中规则如下:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa iptables -nvL -t nat
...
Chain neutron-l3-agent-PREROUTING ( references)
pkts bytes target prot opt in out source destination
REDIRECT tcp -- * * 0.0.0.0/ 169.254.169.254 tcp dpt: redir ports
DNAT all -- * * 0.0.0.0/ 172.24.4.7 to:10.0.2.3
DNAT all -- * * 0.0.0.0/ 172.24.4.5 to:10.0.1.5
...
这条DNAT规则会将floating ip地址转换为内部地址,之后进行路由查询:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip route
10.0.1.0/ dev qr-ddbdc784-d7 proto kernel scope link src 10.0.1.1
10.0.2.0/ dev qr-001d0ed9- proto kernel scope link src 10.0.2.1
169.254.31.28/ dev rfp-0fbb351e-a proto kernel scope link src 169.254.31.28
目的地址是10.0.1.0/24网段的,因此会从qr-ddbdc784-d7转发出去。之后就会转发到br-int再到虚机。
2.3 南北向流量(VM没有floating ip)
在虚机没有floating ip的情况下,从虚机发出的包会首先到IR,IR中查询路由:
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip rule
: from all lookup local
: from all lookup main
: from all lookup default
: from 10.0.1.5 lookup
: from 10.0.2.3 lookup
: from 10.0.1.1/ lookup
: from 10.0.2.1/ lookup
会先查询main表,之后查询167772417表。(Q7:不会匹配table 16?)
# ip netns exec qrouter-0fbb351e-a65b--a409-8fb219ce16aa ip route list table
default via 10.0.1.6 dev qr-ddbdc784-d7
这个表会将其转发给10.0.1.6,而这个IP就是在network node上的router_centralized_snat接口。
在network node的snat namespace中,我们可以看到这个接口。
$ sudo ip netns exec snat-0fbb351e-a65b--a409-8fb219ce16aa iptables -nvL -t nat
...
Chain neutron-l3-agent-snat ( references)
pkts bytes target prot opt in out source destination
SNAT all -- * * 10.0.1.0/ 0.0.0.0/ to:172.24.4.4
SNAT all -- * * 10.0.2.0/ 0.0.0.0/ to:172.24.4.4
...
这里就和以前的L3类似,会将没有floating ip的包SNAT成一个172.24.4.4(DVR的网关臂)。这个过程是和以前L3类似的,不再累述。
参考:http://www.sxt.cn/u/756/blog/3168
3. QA
(未完)
DVR分布式路由的更多相关文章
- 简单了解Django应用app及分布式路由
前言 应用在Django的项目中是一个独立的业务模块,可以包含自己的路由,视图,模板,模型. 一 创建应用程序 创建步骤 用manage.py中的子命令startapp创建应用文件夹 在setting ...
- 8.-Django应用及分布式路由
一.应用 应用在Django项目中是一个独立的业务模块,可以包含自己的路由.视图.模版.模型,可以看成一个小的mtv 创建步骤 1.项目下用manage.py中的子命令创建应用文件夹 python3 ...
- Neutron三层网络服务实现原理
Neutron 对虚拟三层网络的实现是通过其 L3 Agent (neutron-l3-agent).该 Agent 利用 Linux IP 栈.route 和 iptables 来实现内网内不同网络 ...
- OpenStack Neutron DVR L2 Agent的初步解析 (一)
声明: 本博客欢迎转载,但请保留原作者信息! 作者:林凯 团队:华为杭州OpenStack团队 OpenStack Juno版本号已正式公布,这是这个开源云平台的10个版本号,在Juno版的Neutr ...
- openstack网络DVR
一.DVR描述 分布式路由 二.相关的专业术语 术语名称 术语解释 SNAT 在路由器后(POSTROUTING)将内网的ip地址修改为外网网卡的ip地址,也就是绑定浮动IP和外部通信 DNAT 在路 ...
- neutron 多租户隔离的实现以及子网间路由的实现
1.一个network相当于一个二层网络,使用vxlan 隧道连通所有的CNA节点. 2.一个VPC下有多个network,也就是会分配多个vxlan隧道,这些子网间的路由是通过DVR实现的.DVR就 ...
- React路由安装使用和多种方式传参
安装路由 npm i react-router-dom -S 引入路由 import { BowserRouter as Router, Route, Switch, ... } from " ...
- 静态路由、RIP、OSPF、BGP
主要内容包含以下四点:(1)静态路由 (2)动态路由 (3)生成树 (4)VLAN 1. 什么是静态路由? 答:静态路由是管理人员手动配置和管理的路由 2. 静态路由由那些优点? 答:配置简单 ...
- [译] 企业级 OpenStack 的六大需求(第 3 部分):弹性架构、全球交付
全文包括三部分: 第一部分:API 高可用和管理以及安全模型 第二部分:开放架构和混合云兼容 第三部分:弹性架构和全球交付 需求 5 - 扩展.弹性和性能 企业级的内容很丰富.过去,企业级往往和高可靠 ...
随机推荐
- ZBrush中的纹理-水手该怎样进行绘制
如下是一张使用ZBrush3D图形绘制软件绘制的栩栩如生的水手图片,那么有人要问了,如何创建水手渲染的皮肤纹理呢?接下来,小编将教大家学习如何创建皮肤颜色,顺便说一下,这里所选取的颜色仅仅是在ZBru ...
- C++中不能声明为虚函数的有哪些函数
常见的不不能声明为虚函数的有:普通函数(非成员函数):静态成员函数:内联成员函数:构造函数:友元函数. 1.为什么C++不支持普通函数为虚函数? 普通函数(非成员函数)只能被overload,不能被o ...
- js文本框提示和自动完成
1.模仿大型网站自动提示,就是输入“苹果”,在水果类中搜索,html代码如下: <div id="searchTips" style="display:none;w ...
- C# 应用程序配置文件操作
应用程序配置文件,对于asp.net是 web.config对于WINFORM程序是 App.Config(ExeName.exe.config). 配置文件,对于程序本身来说,就是基础和依据,其本质 ...
- 第二章 下山遇虎(@helper)
@helper方法定义 使用@helper关键字可以定义一个方法,这样就可以在页面中调 用这个方法了,和C#中的方法一样.在页面中定义的方法可以访问ViewBag,HttpContext等等页面的属性 ...
- 微软职位内部推荐-Sr. SW Engineer for Azure Networking
微软近期Open的职位: Senior SW Engineer The world is moving to cloud computing. Microsoft is betting Windows ...
- [shell]. 点的含义
. 的含义 当前目录 隐藏文件 任意一个字符 生效配置文件
- C++ Set & MultiSet
转自http://www.cppblog.com/wanghaiguang/archive/2012/06/05/177627.html STL Set介绍集合(Set)是一种包含已排序对象的关联容器 ...
- The specified LINQ expression contains references to queries that are associated with different contexts
今天在改写架构的时候,遇到这么个错误.当时单从字面意思,看上去错误是由join的两个不同的表来源不一致引起的. 其中的videoResult和deerpenList均来自与同一个edmx文件,所以两个 ...
- 2016北京PHP39期 ThinkPHP Discuz Dedecms 微信开发视频教程
2016北京PHP39期 ThinkPHP Discuz Dedecms 微信开发视频教程 所有项目均带有软件,笔记,视频,源码 日期 课程(空内容代表放假) 2015/7/10 星期五 开学典礼 ...