天天看點

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

學習 Neutron 系列文章:

      OVS bridge 有兩種模式:“normal” 和 “flow”。“normal” 模式的 bridge 同普通的 Linux 橋,而 “flow” 模式的 bridge 是根據其流表(flow tables) 來進行轉發的。Neutron 使用兩種 OVS bridge:br-int 和 br-tun。其中,br-int 是一個 “normal” 模式的虛拟網橋,而 br-tun 是 “flow” 模式的,它比 br-int 複雜得多。

下面左圖是 Open vSwitch 中流表的結構。右圖這個流程圖較長的描述了資料包流通過一個 OpenFlow 交換機的過程。

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]
Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

Proxy ARP 就是通過一個主機(通常是Router)來作為指定的裝置對另一個裝置作出 ARP 的請求進行應答。

舉個例子:主機A,IP位址是192.168.0.11/24;主機B,IP位址是192.168.1.22/24。主機A和主機B通過路由器R相連接配接,并且路由器R啟用了Proxy ARP,并配置有路由。網絡拓撲如下:

     eth0                eth0       eth1                        eth0

    A------------------------Router R----------------------B

192.168.0.11/24   192.168.0.0/24 eth0      192.168.1.22/24

                             192.168.1.0/24 eth1  

  在主機A上執行:ping 192.168.1.22,主機 A 不知道主機 B 的 MAC 位址是多少,首先要發送 ARP 查詢封包,路由器 R 接收到主機 A 發出的 ARP 查詢封包,并代替主機 B 作出應答,應答 ARP 封包中填入的就是路由器 R 的MAC位址。這樣,主機A就會認為路由器R的位址是192.168.1.22。以後所有發往192.168.1.22的封包都發到路由器R,路由器R再根據已配置好的路由表将封包轉發給主機B。

  這樣做的好處就是,主機A上不需要設定任何預設網關或路由政策,不管路由器R的IP位址怎麼變化,主機A都能通過路由器B到達主機B,也就是實作了所謂的透明代理。相反,若主機A上設定有預設網關或路由政策時,當主機A向192.168.1.22發送封包,首先要查找路由表,而主機A所在的網段是192.168.0.0/24,主機B所在網段是192.168.1.0/24,主機A隻能通過預設網關将封包發送出去,這樣代理ARP也就失去了作用。

優點: 

最主要的一個優點就是能夠在不影響其他router的路由表的情況下在網絡上添加一個新的router,這樣使得子網的變化對主機是透明的。

proxy ARP應該使用在主機沒有配置預設網關或沒有任何路由政策的網絡上 

缺點:

1.增加了某一網段上 ARP 流量 

2.主機需要更大的 ARP table 來處理IP位址到MAC位址的映射 

3.安全問題,比如 ARP 欺騙(spoofing) 

4.不會為不使用 ARP 來解析位址的網絡工作 

5.不能夠概括和推廣網絡拓撲

OpenStack 中,Neutron 作為 OVS 的 Controller,向 OVS 發出管理 tunnel port 的指令,以及提供流表。

  Neutron 定義了多種流表。以下面的配置(配置了 GRE 和 VXLAN 兩種 tunnel types)為例:

其中,10.0.1.31 是計算節點1, 10.0.1.21 是網絡節點, 10.0.1.39 是計算節點2。

計算節點1 上 ML2 Agent 啟動後的 br-tun 的 flows:

表号

用途

例子

table=0, priority=1,in_port=3 actions=resubmit(,4) //從網絡節點來的,轉 4,結果被丢棄

table=0, priority=1,in_port=4 actions=resubmit(,3) //從網絡節點來的,轉 3

table=0, priority=1,in_port=5 actions=resubmit(,3) //從計算節點來的,轉 3

table=0, priority=1,in_port=2 actions=resubmit(,4) //從計算節點來的,轉 4,結果被丢棄

table=0, priority=1,in_port=1 actions=resubmit(,2) //從虛機來的,轉 2

table=0, priority=0 actions=drop //其餘的丢棄

DVR_PROCESS = 1

handle packets coming from patch_int unicasts go to table UCAST_TO_TUN where remote addresses are learnt

 用于 DVR

PATCH_LV_TO_TUN = 2

table=2, priority=0,dl_dst=00:00:00:00:00:00/01:00:00:00:00:00 actions=resubmit(,20) //單點傳播包,轉 20

GRE_TUN_TO_LV = 3

table=3, priority=1,tun_id=0x4 actions=mod_vlan_vid:1,resubmit(,10) //将 tun_id 為 4 的,修改 vlan id 為1,轉 10 處理

table=3, priority=0 actions=drop //其餘的丢棄

VXLAN_TUN_TO_LV = 4

table=4, priority=0 actions=drop //丢棄

DVR_NOT_LEARN = 9

LEARN_FROM_TUN = 10

 學習table

table=10,priority=1 actions=learn(table=20,hard_timeout=300,priority=1,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:0->NXM_OF_VLAN_TCI[],load:NXM_NX_TUN_ID[]->NXM_NX_TUN_ID[],output:NXM_OF_IN_PORT[]),output:1

UCAST_TO_TUN = 20

//學習到的規則

table=20, priority=2,dl_vlan=1,dl_dst=fa:16:3e:7e:ab:cc actions=strip_vlan,set_tunnel:0x3e9,output:5 //如果vlan 為1,而且目的MAC位址等于 fa:16:3e:7e:ab:cc,設定 tunnel id,從端口 5 發出

table=20,priority=0 actions=resubmit(,22) //直接轉 22

ARP_RESPONDER = 21

 ARP table

 當使用 arp_responder 和 l2population 時候用到

FLOOD_TO_TUN  = 22

 Flood table

table=22,dl_vlan=1 actions=strip_vlan,set_tunnel:0x4,output:5,output:4 //對于 dl_vlan 為1的,設定 tunnel id 為 4,從端口4 和 5 轉出

table=22,priority=0 actions=drop

來個圖簡單些:

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

其中比較有意思的是:

(1)為什麼從 VXLAN 過來的流量都被丢棄了,最後發出去也用的是 GRE 端口。看來同時有 GRE 和 VXLAN 隧道的話,OVS 隻會選擇 GRE。具體原因待查。

(2)MAC 位址學習:Table 10 會将學習到的規則(Local VLAN id + Src MAC Addr => IN_Port)放到 table 20。當表格20 發現一個單點傳播位址是已知的時候,直接從一個特定的 GRE 端口發出;未知的話,視同多點傳播位址從所有 GRE 端口發出。

學習規則:

table=20,hard_timeout=300,priority=1,NXM_OF_VLAN_TCI[0..11],NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],load:0->NXM_OF_VLAN_TCI[],load:NXM_NX_TUN_ID[]->NXM_NX_TUN_ID[],output:NXM_OF_IN_PORT[]

table=20:修改 table 20。這是個 MAC 學習流表。

hard_timeout:該 flow 的過期時間。

NXM_OF_VLAN_TCI[0..11] :記錄 vlan tag,是以學習結果中有 dl_vlan=1

NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[] :将 mac source address 記錄,是以結果中有 dl_dst=fa:16:3e:7e:ab:cc

load:0->NXM_OF_VLAN_TCI[]:在發送出去的時候,vlan tag設為0,是以結果中有 actions=strip_vlan

load:NXM_NX_TUN_ID[]->NXM_NX_TUN_ID[] :發出去的時候,設定 tunnul id,是以結果中有set_tunnel:0x3e9

output:NXM_OF_IN_PORT[]:指定發送給哪個port,由于是從 port2 進來的,因而結果中有output:2。

學到的規則:

table=20, n_packets=1239, n_bytes=83620, idle_age=735, hard_age=65534, priority=2,dl_vlan=1,dl_dst=fa:16:3e:7e:ab:cc actions=strip_vlan,set_tunnel:0x3e9,output:2 

這裡可以看到,通過 MAC 位址學習機制,Neutron 可以一定程度地優化網絡流向,但是這種機制需要等待從别的節點的流量進來,隻能算是一種被動的機制,效率不高。而且,這種機制隻對單點傳播幀有效,而對于多點傳播群組播依然無效。其結果是網絡成本依然很高。下圖中,A 的廣播包其實隻對 3 和 4 有用,但是 2 和 5 也收到了。

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

  使用 ARP Responder 需要滿足兩個條件:

(1)設定 arp_responder = true 來使用 OVS 的ARP 處理能力 。這需要 OVS 2.1 (運作 ovs-vswitchd --version 來檢視 OVS 版本) 和 ML2 l2population 驅動的支援。當使用隧道方式的時候,OVS 可以處理一個 ARP 請求而不是使用廣播機制。如果 OVS 版本不夠的話,Neutorn 是無法設定 arp responder entry 的,你會在 openvswitch agent 日志中看到 “Stderr: '2015-07-11T04:57:32Z|00001|meta_flow|WARN|destination field arp_op is not writable\novs-ofctl: -:2: actions are invalid with specified match (OFPBAC_BAD_SET_ARGUMENT)\n'”這樣的錯誤,你也就不會在 ”ovs-ofctl dump-flows br-tun“ 指令的輸出中看到相應的 ARP Responder 條目了。

(2)設定 l2_population = true。同時添加 mechanism_drivers = openvswitch,l2population。OVS 需要 Neutron 作為 SDN Controller 向其輸入 ARP Table flows。

殺掉 neutron openvswitch, ovs-* 各種程序

#編譯安裝

去 http://openvswitch.org/download/ 下載下傳最新版本的代碼,解壓,進入解壓後的目錄

安裝依賴包,比如 gcc,make

uname -r

./configure --with-linux=/lib/modules/3.13.0-51-generic/build

make && make install

#檢視安裝的版本

root@compute2:/home/s1# ovs-vsctl --version

ovs-vsctl (Open vSwitch) 2.3.2

Compiled Jul 12 2015 09:09:42

DB Schema 7.6.2

#處理 db

rm /etc/openvswitch/conf.db (老的db要删除掉,否則會報錯)

ovsdb-tool create /etc/openvswitch/conf.db vswitchd/vswitch.ovsschema

#啟動 ovs

cp /usr/local/bin/ovs-* /usr/bin

ovsdb-server /etc/openvswitch/conf.db -vconsole:emer -vsyslog:err -vfile:info --remote=punix:/usr/local/var/run/openvswitch/db.sock --private-key=db:Open_vSwitch,SSL,private_key --certificate=db:Open_vSwitch,SSL,certificate --bootstrap-ca-cert=db:Open_vSwitch,SSL,ca_cert --no-chdir --log-file=/var/log/openvswitch/ovsdb-server.log --pidfile=/var/run/openvswitch/ovsdb-server.pid --detach --monitor

ovs-vswitchd unix:/usr/local/var/run/openvswitch/db.sock -vconsole:emer -vsyslog:err -vfile:info --mlockall --no-chdir --log-file=/var/log/openvswitch/ovs-vswitchd.log --pidfile=/var/run/openvswitch/ovs-vswitchd.pid --detach --monitor

#啟動 neutron openvswitch agent,確定log 檔案中 ovs-vsctl 和 ovs-ofctl 調用沒有錯誤

#修改 /usr/share/openvswitch/scripts/ovs-lib 檔案,保證機器重新開機後 OVS 正常運作

将 rundir=${OVS_RUNDIR-'/var/run/openvswitch'} 改為 rundir=${OVS_RUNDIR-'/usr/local/var/run/openvswitch'}

有了 arp_responder 以後,br-tun 的流表增加了幾項和處理:

(1)table 2 中增加一條 flow,是的從本地虛機來的 ARP 廣播幀轉到table 21

(2)在 table 21 中增加一條 flow 将其發發往 table 22

(3)由 L2 population 發來的 entry 來更新 table 21。

  table 21 是在新的 l2pop 位址進來的時候更新的。比如說,compute C 上增加了新的虛機 VM3,然後計算節點 A 和 B 收到一條 l2pop 消息說 VM3 (IP 是***,MAC 是 ***) 在 Host C 上,在 network "Z“ 中。然後,Compute A 和 B 會在 table 21 中增加相應的 flows。   

     table 21 中的每一條 flow,會和進來的幀的資料做比對(ARP 協定,network,虛機的 IP)。如果比對成功,則構造一個 ARP 響應包,其中包括了 IP 和 MAC,從原來的 port 發回到虛機。如果沒有吻合的,那麼轉發到 table 22 做泛洪。

增加的 flow tables 在紅色部分:

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

    l2pop 的原理也不複雜。Neutron 中儲存每一個端口的狀态,而端口儲存了網絡相關的資料。虛機啟動過程中,其端口狀态會從 down 到build 到 active。是以,在每次端口發生狀态變化時,函數 update_port_postcommit  都将會被調用:

    在某些狀态變化下: 

update_port_postcommit (down to active) -> _update_port_up -> add_fdb_entries -> fdb_add -> fdb_add_tun -> setup_tunnel_port  (如果 tunnel port 不存在,則建立 tunnel port), add_fdb_flow -> add FLOOD_TO_TUN flow (如果是 Flood port,則将端口添加到 Flood output ports); setup_entry_for_arp_reply('add'。如果不是 Flood port,那麼 添加 ARP Responder entry (MAC -> IP)) 以及 add UCAST_TO_TUN flow Unicast Flow entry (MAC -> Tunnel port number)。

update_port_postcommit (active to down) -> _update_port_down -> remove_fdb_entries

delete_port_postcommit (active to down) -> _update_port_down -> remove_fdb_entries -> fdb_remove -> fdb_remove_tun -> cleanup_tunnel_port, del_fdb_flow -> mod/del FLOOD_TO_TUN flow; setup_entry_for_arp_reply ('remove'), delete UCAST_TO_TUN flow

update_port_postcommit (fixed ip changed) -> _fixed_ips_changed -> update_fdb_entries

    通過這種機制,每個節點上的如下資料得到了實時更新,進而避免了不必要的隧道連接配接和廣播。

Tunnel port

FLOOD_TO_TUN (table 22)flow

ARP responder flow

UCAST_TO_TUN (table 20) flow

有和沒有 l2pop 的效果:

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

1. def tunnel_sync(self) 函數除了上報自己的 local_ip 外不再自己見 tunnels,一切等 l2pop 的通知。

2. 在 compute1 上添加第一個虛機 81.1.180.8

neutron-server:

通知 compute1: {'segment_id': 6L, 'ports': {u'10.0.1.21': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:87:40:f3', u'81.1.180.1']]}, 'network_type': u'gre'}}

通知所有 agent: {'segment_id': 6L, 'ports': {u'10.0.1.31': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:b3:e7:7a', u'81.1.180.8']]}, 'network_type': u'gre'}} 

compute1:

添加和網絡節點的tunnel options: {df_default="true", in_key=flow, local_ip="10.0.1.31", out_key=flow, remote_ip="10.0.1.21"}

添加到網段網關的 Unicast flow:table=20, n_packets=0, n_bytes=0, idle_age=130, priority=2,dl_vlan=2,dl_dst=fa:16:3e:87:40:f3 actions=strip_vlan,set_tunnel:0x6,output:4

增加網段 81.1.180.1 網關的 ARP flows:table=21, n_packets=0, n_bytes=0, idle_age=130, priority=1,arp,dl_vlan=2,arp_tpa=81.1.180.1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:fa:16:3e:87:40:f3,load:0x2->NXM_OF_ARP_OP[],move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[],move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],load:0xfa163e8740f3->NXM_NX_ARP_SHA[],load:0x5101b401->NXM_OF_ARP_SPA[],IN_PORT

修改 Flood flow

compute 2 節點:因為它上面還沒有運作虛機,是以不做操作。

3. 在 compute 2 上添加一個虛機 81.1.180.9

neutron server:

通知 compute 2 : {'segment_id': 6L, 'ports': {u'10.0.1.31': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:b3:e7:7a', u'81.1.180.8']], u'10.0.1.21': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:87:40:f3', u'81.1.180.1']]}, 'network_type': u'gre'}}

通知所有 agent: {'segment_id': 6L, 'ports': {u'10.0.1.39': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:73:49:41', u'81.1.180.9']]}, 'network_type': u'gre'}

建立 tunnel(ID 5):  {df_default="true", in_key=flow, local_ip="10.0.1.31", out_key=flow, remote_ip="10.0.1.39"}

增加 arp responder flow(compute2 上新的虛機 IP -> MAC):table=21, n_packets=0, n_bytes=0, idle_age=79, priority=1,arp,dl_vlan=2,arp_tpa=81.1.180.9 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:fa:16:3e:73:49:41,load:0x2->NXM_OF_ARP_OP[],move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[],move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],load:0xfa163e734941->NXM_NX_ARP_SHA[],load:0x5101b409->NXM_OF_ARP_SPA[],IN_PORT

增加 unicast flow (新虛機的 MAC -> Tunnel port):table=20, n_packets=0, n_bytes=0, idle_age=79, priority=2,dl_vlan=2,dl_dst=fa:16:3e:73:49:41 actions=strip_vlan,set_tunnel:0x6,output:5

添加新的 Tunnel port 到 Flood flow:table=22, n_packets=13, n_bytes=1717, idle_age=255, hard_age=78, dl_vlan=2 actions=strip_vlan,set_tunnel:0x6,output:5,output:4

compute2:

建立和計算節點以及compute1的tunnel:options: {df_default="true", in_key=flow, local_ip="10.0.1.39", out_key=flow, remote_ip="10.0.1.21"},options: {df_default="true", in_key=flow, local_ip="10.0.1.39", out_key=flow, remote_ip="10.0.1.31"}

增加 ARP flow(compute 1 上的虛機的 MAC -> IP):table=21, n_packets=0, n_bytes=0, idle_age=268, priority=1,arp,dl_vlan=2,arp_tpa=81.1.180.8 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:fa:16:3e:b3:e7:7a,load:0x2->NXM_OF_ARP_OP[],move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[],move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],load:0xfa163eb3e77a->NXM_NX_ARP_SHA[],load:0x5101b408->NXM_OF_ARP_SPA[],IN_PORT

增加 Unicast flow (compute 1 上的虛機 MAC -> Tunnel port):table=20, n_packets=0, n_bytes=0, idle_age=268, priority=2,dl_vlan=2,dl_dst=fa:16:3e:b3:e7:7a actions=strip_vlan,set_tunnel:0x6,output:4

增加 ARP flow(新虛機的網關的 MAC -> IP) table=21, n_packets=0, n_bytes=0, idle_age=268, priority=1,arp,dl_vlan=2,arp_tpa=81.1.180.1 actions=move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[],mod_dl_src:fa:16:3e:87:40:f3,load:0x2->NXM_OF_ARP_OP[],move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[],move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[],load:0xfa163e8740f3->NXM_NX_ARP_SHA[],load:0x5101b401->NXM_OF_ARP_SPA[],IN_PORT

修改 Flood flow(添加到 Compute 1 的 port):table=22, n_packets=13, n_bytes=1717, idle_age=128, dl_vlan=2 actions=strip_vlan,set_tunnel:0x6,output:5,output:4

3. 删除 compute1 上的一個vm(也是唯一的一個)

通知所有 agent: {'segment_id': 6L, 'ports': {u'10.0.1.31': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:b3:e7:7a', u'81.1.180.8']]}, 'network_type': u'gre'}

compute 1:

因為沒有别的虛機了,删除所有 tunnel ports

修改或者删除 ARP, Unicast 和 Flood flows

compute 2:

删除了 compute1 的 tunnel

删除該虛機對應的 ARP flow 

 4. 在 compute1 上建立第一個不同網絡的虛機

通知 compute 1: {u'e2022937-ec2a-467a-8cf1-f642a3f777b6': {'segment_id': 4L, 'ports': {u'10.0.1.21': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:90:e5:50', u'91.1.180.1'], [u'fa:16:3e:17:c9:26', u'90.1.180.1'], [u'fa:16:3e:69:92:30', u'90.1.180.3'], [u'fa:16:3e:69:92:30', u'91.1.180.2']]}, 'network_type': u'gre'}}

通知所有 agent:{u'e2022937-ec2a-467a-8cf1-f642a3f777b6': {'segment_id': 4L, 'ports': {u'10.0.1.31': [['00:00:00:00:00:00', '0.0.0.0'], [u'fa:16:3e:e9:ee:0c', u'91.1.180.9']]}, 'network_type': u'gre'}}

compute 1:建立和網絡節點的 tunnel port;更新 Flood flows;添加 ARP flows

compute 2:沒什麼action,因為該節點上沒有建立虛機的網絡内的虛機

過程的大概說明:

虛機在收到 fannout FDB entries 後,檢查其中每個 port 的 network_id(即 “segment_id”)。如果本機上有該 network 内的 port,那麼就處理 entries 中的 “ports”部分;否則,不處理該 entries。

是以,當計算節點上沒有運作任何虛機時,不會建立任何 tunnel。如果兩個虛機上有相同網絡内的虛機,那麼建立會建立 tunnel。

這種機制能實時建立 tunnel port,Flood entry (建立 Tunnel port 同時添加到 Flood output ports 清單), Unicast flow (虛機和網關 MAC -> Tunnel port) 和 ARP Responder entry  (虛機和網關 MAC -> IP)。下圖中的藍色部分的流表都會被及時更新。

Neutron server 在端口建立/删除/修改時,如果是該節點上的第一個虛機,首先發送直接消息;然後發通知消息給所有的計算和網絡節點。 

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

    應該說 l2pop 的原理和實作都很直接,但是在大規模部署環境中,這種通知機制(通知所有的 ML2 Agent 節點)可能會給 MQ 造成很大的負擔。一旦 MQ 不能及時處理消息,虛機之間的網絡将受到影響。下面是 l2pop 中通知機制代碼: 

Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]
Neutron 了解 (4): Neutron OVS OpenFlow 流表 和 L2 Population [Netruon OVS OpenFlow tables + L2 Population]

    不知道這個數目有沒有上限?數目很多的情況下會不會有性能問題?OVS 有沒有處理能力上限?這些問題也許得在實際的生産環境中才能得到證明和答案。

    本文轉自SammyLiu部落格園部落格,原文連結:http://www.cnblogs.com/sammyliu/p/4633814.html,如需轉載請自行聯系原作者