標籤: 網站設計

  • 007.OpenShift管理應用部署

    007.OpenShift管理應用部署

    一 REPLICATION CONTROLLERS

    1.1 RC概述

    RC確保pod指定數量的副本一直運行。如果pod被殺死或被管理員顯式刪除,複製控制器將自動部署相應的pod。類似地,如果運行的pod數量超過所需的數量,它會根據需要刪除pod,以匹配指定的副本計數。
    RC的定義主要包括:

    • 所需的副本數量
    • 用於創建複製pod的pod定義
    • 用於標識後續管理操作的selector

    selector是一組label,RC管理的所有pod都必須匹配這些標籤。RC實例化的pod定義中必須包含相同的標籤集。RC使用這個selector來確定已經運行了多少pod實例,以便根據需要進行調整。
    提示:不執行自動縮放,因為它不跟蹤負載或流量。
    儘管Kubernetes通常直接管理RC,但OpenShift推薦的方法是管理根據需要創建或更改RC的DC。

    1.2 從DC創建RC

    在OpenShift中創建應用程序的最常見方法是使用oc new-app命令或web控制台。以這種方式創建的應用程序使用DeploymentConfig資源在運行時創建RC來創建應用程序pod。DeploymentConfig資源定義定義了要創建的pod的副本的數量,以及要創建的pod的模板。
    注意:不要將DeploymentConfig或ReplicationController資源中的template屬性誤認為OpenShift模板資源類型,OpenShift模板資源用於基於一些常用的語言運行時和框架構建應用程序。

    1.3 pod副本數控制

    DeploymentConfig或ReplicationController資源中的副本數量可以使用oc scale命令動態更改。
    $ oc get dc
    NAME REVISION DESIRED CURRENT TRIGGERED BY
    myapp 1 3 3 config,image(scaling:latest)
    $ oc scale –replicas=5 dc myapp
    DeploymentConfig資源將更改信息傳遞至ReplicationController,該控制器通過創建新的pod(副本)或刪除現有的pod來響應更改。
    雖然可以直接操作ReplicationController資源,但推薦的做法是操作DeploymentConfig資源。在觸發部署時,直接對ReplicationController資源所做的更改可能會丟失,例如,使用容器image的新版本重新創建pod。

    1.4 自動伸縮pod

    OpenShift可以通過HorizontalPodAutoscaler資源類型根據應用程序pod上的當前負載自動調整部署配置。
    HorizontalPodAutoscaler (HPA)資源使用OpenShift metrics子系統收集的性能指標,即如果沒有度量子系統(模塊),更確切地說是Heapster組件,自動縮放是不可能的。
    創建HorizontalPodAutoscaler資源的推薦方法是使用oc autoscale命令,例如:
    $ oc autoscale dc/myapp –min 1 –max 10 –cpu-percent=80
    該命令創建一個HorizontalPodAutoscaler資源,該資源更改myapp部署配置上的副本數量,以將其pod的CPU使用量控制在請求的總CPU使用量的80%以下。
    oc autoscale命令使用DC的名稱作為參數(在前面的示例中是myapp)創建一個HorizontalPodAutoscaler資源。
    HorizontalPodAutoscaler資源的最大值和最小值用於容納突發負載,並避免重載OpenShift集群。如果應用程序上的負載變化太快,建議保留一些備用的pod來處理突然出現的用戶請求。相反,過多的pod會耗盡所有集群容量,並影響共享相同OpenShift集群的其他應用程序。
    要獲取當前項目中關於HorizontalPodAutoscaler資源的信息,可使用oc get和oc describe命令。例如
    $ oc get hpa/frontend
    $ oc describe hpa/frontend
    注意:HorizontalPodAutoscaler資源只適用於為引用性能指標定義資源請求的pod。
    oc new-app命令創建的大多數pod沒有定義任何資源請求。因此,使用OpenShift autoscaler可能需要為應用程序創建定製的YAML或JSON資源文件,或者向項目添加資源範圍資源。

    二 擴展程序實驗

    2.1 前置準備

    準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

    2.2 創建應用

      1 [student@workstation ~]$ oc login -u developer -p redhat https://master.lab.example.com
      2 [student@workstation ~]$ oc new-project scaling
      3 [student@workstation ~]$ oc new-app -o yaml -i php:7.0 \
      4 http://registry.lab.example.com/scaling > ~/scaling.yml		#將部署的yaml導出至本地
      5 [student@workstation ~]$ vi ~/scaling.yml
      6 ……
      7   spec:
      8     replicas: 3
      9     selector:
     10       app: scaling
     11       deploymentconfig: scaling				#修改副本數
     12 ……
     13 [student@workstation ~]$ oc create -f ~/scaling.yml	#以修改副本數后的yaml部署應用

     

    2.3 監視部署

      1 [student@workstation ~]$ watch -n 3 oc get builds
      2 Every 3.0s: oc get builds                                                                Mon Jul 22 11:12:02 2019
      3 
      4 NAME        TYPE      FROM          STATUS     STARTED              DURATION
      5 scaling-1   Source    Git@0bdae71   Complete   About a minute ago   1m0s
      6 [student@workstation ~]$ oc get pods
      7 NAME              READY     STATUS      RESTARTS   AGE
      8 scaling-1-build   0/1       Completed   0          2m
      9 scaling-1-ft249   1/1       Running     0          1m
     10 scaling-1-gjvkp   1/1       Running     0          1m
     11 scaling-1-mtrxr   1/1       Running     0          1m

     

    2.4 暴露服務

      1 [student@workstation ~]$ oc expose service scaling \
      2 --hostname=scaling.apps.lab.example.com

     

    2.5 web查看相關信息

    瀏覽器訪問https://master.lab.example.com,使用developer用戶和redhat密碼登陸。選擇scaling項目。
     

    2.6 測試負載均衡

      1 [student@workstation ~]$ for i in {1..5};do curl -s \http://scaling.apps.lab.example.com | grep IP;done	#多次請求
      2  <br/> Server IP: 10.128.0.17
      3  <br/> Server IP: 10.129.0.35
      4  <br/> Server IP: 10.129.0.36
      5  <br/> Server IP: 10.128.0.17
      6  <br/> Server IP: 10.129.0.35

     
    提示:瀏覽器可能無法嚴格檢查均衡性,因為OpenShift route存在會話關聯性(也稱為粘性會話)。即來自同一個web瀏覽器的所有請求都將轉到同一個pod。

    2.7 擴容應用

      1 [student@workstation ~]$ oc describe dc scaling | grep Replicas
      2 Replicas:       3
      3         Replicas:       3 current / 3 desired
      4 [student@workstation ~]$ oc scale --replicas=5 dc scaling

     

      1 [student@workstation ~]$ oc get pods -o wide

    2.8 測試負載均衡

      1 [student@workstation ~]$ for i in {1..5};do curl -s \http://scaling.apps.lab.example.com | grep IP;done	#多次請求
      2  <br/> Server IP: 10.128.0.17
      3  <br/> Server IP: 10.128.0.18
      4  <br/> Server IP: 10.129.0.35
      5  <br/> Server IP: 10.129.0.36
      6  <br/> Server IP: 10.129.0.37

     

    三 pod調度控制

    3.1 pod調度算法

    pod調度程序確定新pod在OpenShift集群中的節點上的位置。該調度算法被設計為可高度配置和適應不同集群。OCP 3.9附帶的默認配置通過使用node label、affinity rules,anti-affinity rules中的定義來支持zone和regions的調用。
    在OCP以前的版本中,安裝程序master節點標記為污點標記,表示不允許在master上部署pod。在新版的OCP 3.9中,在安裝和升級過程中,master會自動標記為可調度的。使得可以通過deploy調度pod至maste節點。而不僅僅是作為master的組件運行。
    默認節點selector是在安裝和升級期間默認設置的。它被設置為node-role.kubernetes.io/compute=true,除非使用osm_default_node_selector的Ansible變量覆蓋它。
    在安裝和升級期間,不管osm_default_node_selector配置如何,都會對庫存文件中定義的主機執行以下自動標記。
    compute節點配置non-master、non-dedicated的角色(默認情況下,具有region=infra標籤的節點),節點使用node-role.kubernetes.io/compute=true標記。
    master節點被標記為node-role.kubernetes.io/master=true,從而分配master節點角色。

    3.2 調度算法步驟

    • 過濾節點

    調度程序根據節點資源(如主機端口)的可用性篩選正在運行的節點列表,然後進一步根據節點selector和來自pod的資源請求篩選。最終的縮小是可運行pod的候選node列表。
    pod可以定義與集群節點中的標籤匹配的節點選擇器,標籤不匹配的節點視為不合格。
    pod還可以為計算資源(如CPU、內存和存儲)定義資源請求,沒有足夠的空閑計算機資源的節點視為不合格。

    • 對過濾后的節點列表進行優先級排序

    候選節點列表使用多個優先級標準進行評估,這些標準加起來就是權重,權重值較高的節點更適合運行pod。
    其中有affinity(親和規則)和anti-affinity(反親和規則),pod親和力較高的節點得分較高,而anti-affinity較高的節點權重低。
    affinity的一個常見用法是:出於性能原因,將相關的pod安排得彼此親和。例如,需要保持彼此同步的pod使用相同的網絡棧。
    anti-affinity的一個常見用法是:為了獲得高可用性,將相關的pod安排的盡量分散。例如,避免將所有pod從同一個應用程序調度到同一個節點。

    • 選擇最合適的節點。

    根據權重對候選列表進行排序,並選擇權重最高的節點來承載pod。如果多個節點得分相同,則隨機選擇一個節點。
    調度程序配置文件位於/etc/original/master/scheduler.json,其定義了一組predicates,用作過濾器或優先級函數。通過這種方式,可以將調度程序配置為支持不同的集群。

    3.3 調度拓撲

    對於大型數據中心,例如雲提供商,一個常見的拓撲結構是將主機組織成regions和zones:
    region:是一個地理區域內的一組主機,這保證了它們之間的內網高速連接;
    zone:也稱為可用區,是一組主機,它們可能一起失敗,因為它們共享公共的關鍵基礎設施組件,比如網絡、存儲或電源。
    OpenShift pod調度器可支持根據region和zone標籤在集群內調度,如:

      • 從相同的RC創建的或從相同的DC創建的pod副本調度至具有相同region標籤值的節點中運行。
      • 副本Pod調位至具有不同zone標籤的節點中運行。

    實例圖如下:

    要實現上圖中的樣例拓撲,可以使用集群管理員通過以下命令oc label:

      1 $ oc label node1 region=ZheJiang zone=Cloud1A --overwrite
      2 $ oc label node node2 region=ZheJiang zone=Cloud1A --overwrite
      3 $ oc label node node3 region=ZheJiang zone=Cloud2A --overwrite
      4 $ oc label node node4 region=ZheJiang zone=Cloud2A --overwrite
      5 $ oc label node node5 region=HuNan zone=Cloud1B --overwrite
      6 $ oc label node node6 region=HuNan zone=Cloud1B --overwrite
      7 $ oc label node node7 region=HuNan zone=Cloud2B --overwrite
      8 $ oc label node node8 region=HuNan zone=Cloud2B --overwrite

     
    提示:每個節點必須由其完全限定名(FQDN)標識,為了簡潔,如上命令使用了簡短的名稱。
    對區域標籤的更改需要–overwrite選項,因為OCP 3.9高級安裝方法默認情況下使用region=infra標籤配置節點。
    示例:要檢查分配給節點的標籤,可以使用oc get node命令和–show-labels選項。
    $ oc get node node1.lab.example.com –show-labels
    注意,一個節點可能有一些OpenShift分配的默認標籤,包含kubernetes.io後綴鍵值的標籤,此類標籤不應由集群管理員人為更改,因為它們由調度程序在內部使用。
    集群管理員還可以使用-L選項來確定單個標籤的值。
    示例:

      1 $ oc get node node1.lab.example.com -L region
      2 $ oc get node node1.lab.example.com -L region -L zone	#支持oc get跟多個-L選項

     

    3.4 UNSCHEDULABLE節點

    有時候,集群管理員需要關閉節點進行維護,如節點可能需要硬件升級或內核安全更新。要在對OpenShift集群用戶影響最小的情況下關閉節點,管理員應該遵循兩個步驟。
    將節點標記為不可調度,從而防止調度程序向節點分配新的pod。

      1 $ oc adm manage-node --schedulable=false node2.lab.example.com

    Drain節點,這將銷毀在pod中運行的所有pod,並假設這些pod將通過DC在其他可用節點中會重新創建。

      1 $ oc adm drain node2.lab.example.com

    維護操作完成后,使用oc adm management -node命令將節點標記為可調度的。

      1 $ oc adm manage-node --schedulable=true node2.lab.example.com

    3.5 控制pod位置

    有些應用程序可能需要在一組指定的node上運行。例如,某些節點為某些類型的工作負載提供硬件加速,或者集群管理員不希望將生產應用程序與開發應用程序混合使用。此類需求,都可以使用節點標籤和節點選擇器來實現。
    node selector是pod定義的一部分,但建議更改dc,而不是pod級別的定義。要添加節點選擇器,可使用oc edit命令或oc patch命令更改pod定義。
    示例:配置myapp的dc,使其pods只在擁有env=qa標籤的節點上運行。

      1 $ oc patch dc myapp --patch '{"spec":{"template":{"nodeSelector":{"env":"qa"}}}}'

    此更改將觸發一個新的部署,並根據新的節點選擇器調度新的pod。
    如果集群管理員不希望讓開發人員控制他們pod的節點選擇器,那麼應該在項目資源中配置一個默認的節點選擇器。

    3.5 管理默認項目

    生產環境一個常見實踐是指定一組節點來運行OCP的系統基礎Pod,比如route和內部倉庫。這些pod在默認項目中定義。
    通常可通過以下兩個步驟實現:

    1. 使用region=infra標籤標記專用節點;
    2. 為缺省名稱空間配置缺省節點選擇器。

    要配置項目的默認節點選擇器,可使用openshift.io/node-selector鍵值向名稱空間資源添加註釋。可以使用oc edit或oc annotate命令。

      1 $ oc annotate --overwrite namespace default \
      2 openshift.io/node-selector='region=infra'

     
    OCP 3.9 quick installer和advanced installer的Ansible playbook都支持Ansible變量,這些變量控制安裝過程中分配給節點的標籤,也控制分配給每個基礎設施pod的節點選擇器。
    安裝OCP子系統(如metrics子系統)的劇本還支持這些子系統節點選擇器的變量。

    四 控制Pod調度

    4.1 前置準備

    準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

    4.2 本練習準備

      1 [student@workstation ~]$ lab schedule-control setup
      2 [student@workstation ~]$ oc login -u admin -p redhat https://master.lab.example.com

     

    4.3 查看region

      1 [student@workstation ~]$ oc get nodes -L region
      2 NAME                     STATUS    ROLES     AGE       VERSION             REGION
      3 master.lab.example.com   Ready     master    2d        v1.9.1+a0ce1bc657
      4 node1.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   infra
      5 node2.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   infra

     

    4.4 創建project

      1 [student@workstation ~]$ oc new-project schedule-control

    4.5 創建應用

      1 [student@workstation ~]$ oc new-app --name=hello \
      2 --docker-image=registry.lab.example.com/openshift/hello-openshift

     

    4.6 擴展應用

      1 [student@workstation ~]$ oc scale dc hello --replicas=5
      2 deploymentconfig "hello" scaled
      3 [student@workstation ~]$ oc get pod -o wide
      4 NAME            READY     STATUS    RESTARTS   AGE       IP            NODE
      5 hello-1-c5z2n   1/1       Running   0          7s        10.128.0.21   node1.lab.example.com
      6 hello-1-hhvp7   1/1       Running   0          34s       10.129.0.38   node2.lab.example.com
      7 hello-1-jqrkb   1/1       Running   0          7s        10.128.0.20   node1.lab.example.com
      8 hello-1-tgmbr   1/1       Running   0          7s        10.129.0.39   node2.lab.example.com
      9 hello-1-z2bn7   1/1       Running   0          7s        10.128.0.22   node1.lab.example.com

     

    4.7 修改節點label

      1 [student@workstation ~]$ oc label node node2.lab.example.com region=apps --overwrite=true
      2 [student@workstation ~]$ oc get nodes -L region		#確認修改
      3 NAME                     STATUS    ROLES     AGE       VERSION             REGION
      4 master.lab.example.com   Ready     master    2d        v1.9.1+a0ce1bc657
      5 node1.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   infra
      6 node2.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   apps

     

    4.8 導出dc

      1 [student@workstation ~]$ oc get dc hello -o yaml > dc.yaml

    4.9 修改node2調度策略

    添加dc.yaml中的調度策略,使pod調度至apps標籤的node。

      1 [student@workstation ~]$ vi dc.yaml
      2 ……
      3   template:
      4 ……
      5     spec:
      6       nodeSelector:		#添加節點選擇器
      7         region: apps
      8 ……

     

    4.10 應用更新

      1 [student@workstation ~]$ oc apply -f dc.yaml

    4.11 確認驗證

      1 [student@workstation ~]$ oc get pod -o wide
      2 NAME            READY     STATUS    RESTARTS   AGE       IP            NODE
      3 hello-2-4c2gv   1/1       Running   0          40s       10.129.0.42   node2.lab.example.com
      4 hello-2-6966b   1/1       Running   0          38s       10.129.0.43   node2.lab.example.com
      5 hello-2-dcqbr   1/1       Running   0          36s       10.129.0.44   node2.lab.example.com
      6 hello-2-dlf8k   1/1       Running   0          36s       10.129.0.45   node2.lab.example.com
      7 hello-2-rnk4w   1/1       Running   0          40s       10.129.0.41   node2.lab.example.com

     
    #驗證是否觸發了新的部署,並等待所有新的應用pod都準備好並運行。所有5個pod都應該調度至node2。

    4.12 修改node1調度策略

      1 [student@workstation ~]$ oc label node node1.lab.example.com region=apps --overwrite=true
      2 [student@workstation ~]$ oc get node -L region
      3 NAME                     STATUS    ROLES     AGE       VERSION             REGION
      4 master.lab.example.com   Ready     master    2d        v1.9.1+a0ce1bc657
      5 node1.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   apps
      6 node2.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   apps

     

    4.13 終止node2

      1 [student@workstation ~]$ oc adm manage-node --schedulable=false node2.lab.example.com
      2 NAME                    STATUS                     ROLES     AGE       VERSION
      3 node2.lab.example.com   Ready,SchedulingDisabled   compute   2d        v1.9.1+a0ce1bc657

     

    4.14 刪除pod

    刪除node2的pod,並使用node1創建的pod替換。

      1 [student@workstation ~]$ oc adm drain node2.lab.example.com --delete-local-data

    4.15 查看pod

      1 [student@workstation ~]$ oc get pods -o wide
      2 NAME            READY     STATUS    RESTARTS   AGE       IP            NODE
      3 hello-2-bjsj4   1/1       Running   0          51s       10.128.0.25   node1.lab.example.com
      4 hello-2-kmmmn   1/1       Running   0          50s       10.128.0.23   node1.lab.example.com
      5 hello-2-n6wvj   1/1       Running   0          51s       10.128.0.24   node1.lab.example.com
      6 hello-2-plr65   1/1       Running   0          50s       10.128.0.26   node1.lab.example.com
      7 hello-2-xsz68   1/1       Running   0          51s       10.128.0.27   node1.lab.example.com

     

    五 管理IS、image、Templates

    5.1 image介紹

    在OpenShift中,image是一個可部署的runtime模板,它包含運行單個容器的所有需求,還包括imag功能的元數據。image可以通過多種方式管理,如tag、import、pull和update。
    image可以跨多個主機部署在多個容器中。開發人員可以使用Docker構建image,也可以使用OpenShift構建工具。
    OpenShift實現了靈活的image管理機制。一個image名稱實際上可以引用同一image的許多不同版本。唯一的image由它的sha256哈希引用,Docker不使用版本號。相反,它使用tag來管理image,例如v1、v2或默認的latest tag。

    5.2 IS

    IS包括由tags標識的任意數量的容器images。它是相關image的統一虛擬視圖,類似於Docker image倉庫。開發人員有許多與image和IS交互的方法。例如,當添加或修改新image時,build和deployment可以接收通知,並通過運行新build或新deployment做出相應的動作。

    5.3 標記image

    OCP提供了oc tag命令,它類似於docker tag命令,但是,它是對IS而不是image進行操作。
    可以向image添加tag,以便更容易地確定它們包含什麼。tag是指定image版本的標識符。
    示例:將Apache web服務器2.4版本的映像,可將該image執行以下標記。
    apache: 2.4
    如果倉庫包含Apache web服務器的最新版本,他們可以使用latest標籤來表示這是倉庫中可用的最新image。
    apache:latest
    oc tag命令用於標籤image:
    [user@demo ~]$ oc tag source destination
    source:現有tag或圖像流中的圖像。
    destination:標籤在一個或多個IS中的最新image。
    示例:將ruby image的現有latest標記修改為當前版本v2.0標識,
    [user@demo ~]$ oc tag ruby:latest ruby:2.0

    5.4 刪除tag

    若要從image中刪除標記,可使用-d參數。
    [user@demo ~]$ oc tag -d ruby:latest
    可以使用不同類型的標籤,默認行為使用permanent tag,即源文件發生更改,該tag也會及時指向image,與目標tag無關。
    tracking tag指示在導入image期間導入目標tag的元數據。要確保目標tag在源tag更改時得到更新,需使用–alias=true標識。
    [user@demo ~]$ oc tag –alias=true source destination
    要重新導入tag,可使用–scheduled=true標識。
    [user@demo ~]$ oc tag –scheduled=true source destination
    要配置Docker始終從內部倉庫中獲取image,可使用–reference-policy=local標誌。默認情況下,image指向本地倉庫。從而實現在之後調用image的時候可以快速pull。
    [user@demo ~]$ oc tag –reference-policy=local source destination

    5.5 建議的tag形式

    在管理tag時,開發人員應該考慮映像的生命周期,參考下錶開發人員用來管理映像的可能的標記命名約定。

    描述 示例
    Revision myimage:v2.0.1
    Architecture myimage:v2.0-x86_64
    Base Image myimage:v1.2-rhel7
    Latest Image myimage:latest
    Latest Stable Image myimage:stable

    5.6 Templates介紹

    模板描述一組對象,其中包含處理後生成對象列表的參數。可以處理模板來創建開發人員有權在項目中創建的任何內容,例如service、build、configuration和dc。
    模板還可以定義一組標籤,應用於它定義的每個對象。開發人員可以使用命令行界面或web控制台從模板創建對象列表。

    5.7 Templates管理

    開發人員可以用JSON或YAML格式編寫模板,並使用命令行界面或web控制台導入它們。模板被保存到項目中,以供對該特定項目具有適當訪問權限的任何用戶重複使用。
    示例:導入模板。
    [user@demo ~]$ oc create -f filename
    還可以在導入模板時分配標籤,這意味着模板定義的所有對象都將被標記。
    [user@demo ~]$ oc create -f filename -l name=mylabel

    5.8 使用模板

    OCP提供了許多默認的instant app和QuickStart模板,允許開發人員為不同的語言快速創建新的應用程序。為Rails (Ruby)、Django (Python)、Node.js、CakePHP (PHP)和Dancer (Perl)提供了模板。
    要列出集群中的可用模板,請運行oc get templates命令。參數-n指定要使用的項目。
    [user@demo ~]$ oc get templates -n openshift
    開發人員還可以使用web控制台瀏覽模板,當您選擇模板時,可以調整可用的參數來自定義模板定義的資源。

    六 管理IS

    6.1 前置準備

    準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

    6.2 本練習準備

      1 [student@workstation ~]$ lab schedule-is setup

    6.3 創建項目

      1 [student@workstation ~]$ oc login -u developer -p redhat \
      2 https://master.lab.example.com
      3 [student@workstation ~]$ oc new-project schedule-is

     

    6.4 創建應用

      1 [student@workstation ~]$ oc new-app --name=phpmyadmin \
      2 --docker-image=registry.lab.example.com/phpmyadmin/phpmyadmin:4.7

     

    6.5 創建服務賬戶

      1 [student@workstation ~]$ oc login -u admin -p redhat
      2 [student@workstation ~]$ oc project schedule-is
      3 [student@workstation ~]$ oc create serviceaccount phpmyadmin-account

     

    6.6 授權特權運行

      1 [student@workstation ~]$ oc adm policy add-scc-to-user anyuid \
      2 -z phpmyadmin-account

     

    6.7 更新pod

      1 [student@workstation ~]$ oc login -u developer
      2 [student@workstation ~]$ oc patch dc/phpmyadmin --patch \
      3 '{"spec":{"template":{"spec":{"serviceAccountName": "phpmyadmin-account"}}}}'

     
    更新負責管理phpmyadmin部署的dc資源,以便使用新創建的服務帳戶。可以使用oc patch或oc edit命令。此命令可以從/home/student/DO280/labs/secure-review文件夾中的patch-dc.sh腳本中複製。

      1 [student@workstation ~]$ oc get pods		#確認驗證
      2 NAME                 READY     STATUS    RESTARTS   AGE
      3 phpmyadmin-2-vh29z   1/1       Running   0          3m

     
    提示:name后的2表示這個pod是第二次部署,即進行過迭代。

    6.8 更新內部倉庫image

      1 [student@workstation ~]$ cd /home/student/DO280/labs/schedule-is/
      2 [student@workstation schedule-is]$ ls
      3 phpmyadmin-latest.tar  trust_internal_registry.sh
      4 [student@workstation schedule-is]$ docker load -i phpmyadmin-latest.tar
      5 #使用docker load命令加載新的image。
      6 [student@workstation schedule-is]$ docker images
      7 REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
      8 <none>              <none>              93d0d7db5ce2        13 months ago       166 MB

     

    6.9 tag鏡像

      1 [student@workstation schedule-is]$ docker tag 93d0d7db5ce2 \
      2 docker-registry-default.apps.lab.example.com/schedule-is/phpmyadmin:4.7
      3 #打完標記進行推送。

     

    6.10 登錄docker倉庫


    結論:docker倉庫會提示因為是自簽名證書,因此判定為不安全的方式。

    6.11 修改信任

    本環境使用/home/student/DO280/labs/secure-review文件夾中的trust_internal_registry.sh腳本,配置docker倉庫信任OpenShift內部倉庫。

      1 [student@workstation schedule-is]$ ./trust_internal_registry.sh

    6.12 推送image

      1 [student@workstation schedule-is]$ docker push \
      2 docker-registry-default.apps.lab.example.com/schedule-is/phpmyadmin:4.7

     

    6.13 確認更新

    驗證當源image更新后,是否能自動觸發OpenShift進行pod更新。

      1 [student@workstation schedule-is]$ oc get pods
      2 NAME                 READY     STATUS    RESTARTS   AGE
      3 phpmyadmin-3-hnfjk   1/1       Running   0          23s

     

    七 管理應用部署實驗

    7.1 前置準備

    準備完整的OpenShift集群,參考《003.OpenShift網絡》2.1。

    7.2 本練習準備

      1 [student@workstation ~]$ lab manage-review setup

    7.3 確認region

      1 [student@workstation ~]$ oc login -uadmin -predhat https://master.lab.example.com
      2 [student@workstation ~]$ oc get nodes -L region
      3 NAME                     STATUS    ROLES     AGE       VERSION             REGION
      4 master.lab.example.com   Ready     master    2d        v1.9.1+a0ce1bc657
      5 node1.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   infra
      6 node2.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   infra

     

    7.4 修改region

      1 [student@workstation ~]$ oc label node node1.lab.example.com region=services --overwrite=true
      2 [student@workstation ~]$ oc label node node2.lab.example.com region=applications --overwrite=true
      3 [student@workstation ~]$ oc get nodes -L region
      4 NAME                     STATUS    ROLES     AGE       VERSION             REGION
      5 master.lab.example.com   Ready     master    2d        v1.9.1+a0ce1bc657
      6 node1.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   services
      7 node2.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   applications

     

    7.5 創建項目

      1 [student@workstation ~]$ oc new-project manage-review

    7.6 創建應用

      1 [student@workstation ~]$ oc new-app -i php:7.0 \
      2 http://registry.lab.example.com/version

     

    7.7 擴展應用

      1 [student@workstation ~]$ oc scale dc version --replicas=3
      2 [student@workstation ~]$ oc get pods -o wide		#確認驗證
      3 NAME              READY     STATUS      RESTARTS   AGE       IP            NODE
      4 version-1-9626w   1/1       Running     0          40s       10.129.0.55   node2.lab.example.com
      5 version-1-build   0/1       Completed   0          1m        10.129.0.52   node2.lab.example.com
      6 version-1-f6vj2   1/1       Running     0          40s       10.129.0.56   node2.lab.example.com
      7 version-1-mrhk4   1/1       Running     0          45s       10.129.0.54   node2.lab.example.com

     
    結論:應用程序pod並沒有均分在兩個集群node節點之間,因為每個節點屬於不同的region,並且默認的OpenShift調度器配置打開了區域粘性。

    7.8 調度pod

      1 [student@workstation ~]$ oc export dc version -o yaml > version-dc.yml	#導出yaml
      2 spac
      3 ……
      4   template:
      5     metadata:
      6 ……
      7     spec:
      8       nodeSelector:		#添加節點選擇器
      9         region: applications
     10 ……

     

    7.9 迭代部署

      1 [student@workstation ~]$ oc replace -f version-dc.yml	#迭代

    7.10 確認驗證

      1 [student@workstation ~]$ oc get pod -o wide
      2 NAME              READY     STATUS      RESTARTS   AGE       IP            NODE
      3 version-1-build   0/1       Completed   0          15m       10.129.0.52   node2.lab.example.com
      4 version-2-2bmqq   1/1       Running     0          58s       10.129.0.60   node2.lab.example.com
      5 version-2-nz58r   1/1       Running     0          1m        10.129.0.59   node2.lab.example.com
      6 version-2-rlj2h   1/1       Running     0          1m        10.129.0.58   node2.lab.example.com

     
    驗證是否啟動了新的部署,並且在node2節點上運行了一組新的版本莢。等待所有三個新的應用程序莢都準備好並運行

    7.11 修改region

      1 [student@workstation ~]$ oc label node node1.lab.example.com region=applications --overwrite=true
      2 [student@workstation ~]$ oc get nodes -L region		#確認驗證
      3 NAME                     STATUS    ROLES     AGE       VERSION             REGION
      4 master.lab.example.com   Ready     master    2d        v1.9.1+a0ce1bc657
      5 node1.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   applications
      6 node2.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   applications

     

    7.12 終止node2

      1 [student@workstation ~]$ oc adm manage-node --schedulable=false node2.lab.example.com
      2 NAME                    STATUS                     ROLES     AGE       VERSION
      3 node2.lab.example.com   Ready,SchedulingDisabled   compute   2d        v1.9.1+a0ce1bc657

     

    7.13 刪除pod

    刪除node2的pod,並使用node1創建的pod替換。

      1 [student@workstation ~]$ oc adm drain node2.lab.example.com --delete-local-data

    7.14 查看pod

      1 [student@workstation ~]$ oc get pods -o wide
      2 NAME              READY     STATUS    RESTARTS   AGE       IP            NODE
      3 version-2-d9fhp   1/1       Running   0          3m        10.128.0.34   node1.lab.example.com
      4 version-2-jp5gr   1/1       Running   0          3m        10.128.0.35   node1.lab.example.com
      5 version-2-z5lv5   1/1       Running   0          3m        10.128.0.33   node1.lab.example.com

     

    7.15 暴露服務

      1 [student@workstation ~]$ oc expose service version --hostname=version.apps.lab.example.com
      2 [student@workstation ~]$ curl http://version.apps.lab.example.com	#確認測試
      3 <html>
      4  <head>
      5   <title>PHP Test</title>
      6  </head>
      7  <body>
      8  <p>Version v1</p>
      9  </body>
     10 </html>

     

    7.16 確認驗證

      1 [student@workstation ~]$ lab manage-review grade	#環境腳本判斷

    7.17 還原環境

      1 [student@workstation ~]$ oc adm manage-node --schedulable=true node2.lab.example.com
      2 [student@workstation ~]$ oc label node node1.lab.example.com region=infra --overwrite=true
      3 [student@workstation ~]$ oc label node node2.lab.example.com region=infra --overwrite=true
      4 [student@workstation ~]$ oc get node -L region
      5 NAME                     STATUS    ROLES     AGE       VERSION             REGION
      6 master.lab.example.com   Ready     master    2d        v1.9.1+a0ce1bc657
      7 node1.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   infra
      8 node2.lab.example.com    Ready     compute   2d        v1.9.1+a0ce1bc657   infra
      9 [student@workstation ~]$ oc delete project manage-review

      本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 驗證碼原理及驗證

    驗證碼的原理

    驗證碼的作用:

     驗證碼是是一種區分用戶是計算機還是人的公共全自動程序,可以防止:惡意破解密碼、刷票、論壇灌水、有效防止某個黑客對某一特定註冊用戶,用特定程序暴力破解方式進行不斷的登錄嘗試。實際上驗證碼是現在很多網站通行的方式,我們利用比較簡易的方式實現了這個功能。

    生成驗證碼

    生成驗證碼這個功能已經特別成熟了 在網上可以找到很多資源

    以下是生成驗證碼的相關代碼:

    package com._yhnit.randomcode;
    
    import java.awt.Color;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.image.BufferedImage;
    import java.io.IOException;
    import java.util.Random;
    
    import javax.imageio.ImageIO;
    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    /**
     *  生成驗證碼的Servlet
     * @author yhn
     *
     */
    @WebServlet("/createRandomcode")
    public class RandomCodeServlet extends HttpServlet{
    
    	private static final long serialVersionUID = 1L;
    	 public RandomCodeServlet() {
    	        super();
    
    	    }
    
        protected void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            doPost(request, response);
        }
    
        protected void doPost(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException {
    
            // 響應頭信息
            response.setHeader("Pragma", "No-Cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expries", 0);
    
            // 隨機數生成類
            Random random = new Random();
    
            // 定義驗證碼的位數
            int size = 5;
    
            // 定義變量保存生成的驗證碼
            String vCode = "";
            char c;
            // 產生驗證碼
            for (int i = 0; i < size; i++) {
                // 產生一個26以內的隨機整數
                int number = random.nextInt(26);
                // 如果生成的是偶數,則隨機生成一個数字
                if (number % 2 == 0) {
                    c = (char) ('0' + (char) ((int) (Math.random() * 10)));
                    // 如果生成的是奇數,則隨機生成一個字母
                } else {
                    c = (char) ((char) ((int) (Math.random() * 26)) + 'A');
                }
                vCode = vCode + c;
            }
    
            // 保存生成的5位驗證碼
            request.getSession().setAttribute("RANDOMCODE_IN_SESSION", vCode);
    
            // 驗證碼圖片的生成
            // 定義圖片的寬度和高度
            int width = (int) Math.ceil(size * 20);
            int height = 30;
            BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
            // 獲取圖片的上下文
            Graphics gr = image.getGraphics();
            // 設定圖片背景顏色
            gr.setColor(Color.WHITE);
            gr.fillRect(0, 0, width, height);
            // 設定圖片邊框
            gr.setColor(Color.GRAY);
            gr.drawRect(0, 0, width - 1, height - 1);
            // 畫十條幹擾線
            for (int i = 0; i < 5; i++) {
                int x1 = random.nextInt(width);
                int y1 = random.nextInt(height);
                int x2 = random.nextInt(width);
                int y2 = random.nextInt(height);
                gr.setColor(randomColor());
                gr.drawLine(x1, y1, x2, y2);
            }
            // 設置字體,畫驗證碼
            gr.setColor(randomColor());
            gr.setFont(randomFont());
            gr.drawString(vCode, 10, 22);
            // 圖像生效
            gr.dispose();
            // 輸出到頁面
            ImageIO.write(image, "JPEG", response.getOutputStream());
    
        }
    
        // 生成隨機的顏色
        private Color randomColor() {
            int red = r.nextInt(150);
            int green = r.nextInt(150);
            int blue = r.nextInt(150);
            return new Color(red, green, blue);
        }
    
        private String[] fontNames = { "宋體", "華文楷體", "黑體", "微軟雅黑", "楷體_GB2312" };
        private Random r = new Random();
    
        // 生成隨機的字體
        private Font randomFont() {
            int index = r.nextInt(fontNames.length);
            String fontName = fontNames[index];// 生成隨機的字體名稱
            int style = r.nextInt(4);
            int size = r.nextInt(3) + 24; // 生成隨機字號, 24 ~ 28
            return new Font(fontName, style, size);
        }
    }
    
    
    
    

    上述代碼中 定義了生成5位数字+字母的驗證碼

    生成的驗證碼 將存放到兩個地方:

    1. Session中
    2. 放到圖片上去

    最重要的是 將驗證碼存入Session,因為後台校驗驗證碼是否正確要依靠這一步

    // 保存生成的5位驗證碼
     request.getSession().setAttribute("RANDOMCODE_IN_SESSION", vCode);
    

    前端頁面實現驗證碼的切換

    在很多應用中 ,我們都會看見驗證碼的切換操作

    比如:點擊圖片切換,或者點擊後面文字(類如 看不清,換一張) 進行切換

    其實 切換很簡單 只是將圖片元素 的src 屬性 變換一下就可以完成

    這裏給驗證碼圖片 和 換一張文字添加點擊事件

    驗證碼:<input type="text" maxlength="5" required="required" name ="randomcode">
    	   <img  src="/createRandomcode" style="cursor: pointer;" onclick="change();"  id="randomcodeImg">
    		<a href="" onclick="change();">換一張</a><br>
    
    

    點擊事件 是一個名字為change函數

    function change(){
    	// 因為有緩存  所以加一個隨機數  表示不同的請求
    	document.getElementById("randomcodeImg").src="/createRandomcode?"+new Date().getTime();	
    	}
    

    注意這裏:src不能也寫 /createRandomcode,因為瀏覽器有緩存 因為之前的src就是它

    所以點擊時不會發生切換,所以我們可以加個隨機數代表每一次都是一個新的請求。

    這樣就可以實現驗證碼的切換了。

    驗證碼的後台驗證

    驗證其實也很簡單,只需要把輸入的和圖片中的驗證碼進行對比即可

    獲取輸入的驗證碼:

    String code = req.getParameter("randomcode");
    

    獲取圖片中的驗證碼:

    (生成的時候 已經存在Session中 這時只需要從Session中取出即可)

    String Imgcode = req.getSession().getAttribute("RANDOMCODE_IN_SESSION").toString();
    

    兩者進行對比驗證:

    if (!code.equalsIgnoreCase(Imgcode)) {
        // 設置一些錯誤提示  提示用戶輸入錯誤 
    	req.getSession().setAttribute("errorMes", "請輸入正確的驗證碼或已經過期");
    	req.getRequestDispatcher("randomcode/RandomCodeLogin.jsp").forward(req, resp);
    	return;
    }
    		
    // 此時驗證碼成功
    System.out.println("驗證碼成功");
    // 避免重複提交  去除Session中這一次驗證碼
    req.getSession().removeAttribute("RANDOMCODE_IN_SESSION");
    
    // 繼續驗證用戶名和密碼  ....
    

    驗證碼驗證成功之後 要銷毀Session中這次的驗證碼(驗證碼一次性使用) 避免重複提交

    // 避免重複提交  去除Session中這一次驗證碼
    req.getSession().removeAttribute("RANDOMCODE_IN_SESSION");
    

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • 走出舒適圈的信念和勇氣——“Learning by doing!” 我的軟工2020春季教學總結

          看着大家陸續提交個人學期總結,我還不敢去翻看,怕思緒紛飛思維定式,變了自己寫總結的初心和思路。一篇總結,開頭起筆尤是最難,總是想各式各樣的開頭,翻來覆去,寫了念,念了刪,寫了念,念了刪。還是回到初心,回首一下為什麼上這門課。經過一個學期,我的態度和想法有沒有變化,我的收穫和驚喜是什麼。

           為什麼我想上這個班?
           過去沒有一次課,會像這次一樣,具有一點使命感。過去也沒有一次課,全程都是線上進行。也沒有一次課,如果放大忐忑的心情,也會挺忐忑的。
           作為一門臨時穿插安排的選修課,上課前,我聽到的關於系裡同學的水平和积極主動性,負向居多。如果課程要求高和嚴,還可能一翻兩瞪眼,學生投訴或紛紛消極放棄,那麼我大多數的時間精力可能都會用來勸說、解釋和雞湯。如果上的太差,也有可能搞砸後續我想進行的課程教學改革和質量提升。雖然考慮過這些困難,但相比於還沒有為系裡學生完整上過一門課,還沒有近距離感受不同學生的精彩作品和風格,還沒有為後續其他課程的改革做探索和調研,前面那些疑慮早已經拋到九霄雲外,只有許多期待和興奮感。

           你們給我的驚喜:

           你們從個人Github開始,也結束於團隊協作的GitHub,而且不少組做的很不錯,互幫互學,其團隊Github實踐能力,遠勝過以往我的班級;
           你們其中有的組分享的Android或小程序開發的經驗和教程,寫的用心,媲美以往我的班級;
           你們不少組用上了一些自動化測試工具,而且妥帖,用的好,遠勝過以往我的班級;
           更難能可貴的是,你們的不少作品,都離用戶很近,教務課程表、查寢點名、圖書館佔座、校園失物招領……等等,都將可以被用得上,希望你們不斷將作品成型,離之更近。
           不少同學的能力和潛質,都讓我覺得相見恨晚,也相識太短。匆匆你們也就要進入畢業年級。什麼是課程?就是拋卻那些具體的什麼理論和知識,回憶自己能留下的,就是這門課要教給你的。 如果問我,我們短暫的線上相聚,這門課,要交給你們什麼呢?是“Learing by doing”嗎? 這也許是之一。做中學,其實就是做自己所不會的。我們常有的觀念是,我不會,所以我做不了。而“Learning by doing”給我們的勇氣和信念是:做我所不會的,但又是對於自己發展非常重要的,甚至是關鍵路徑上的實現和突破。換句簡單的話來說,這門課想交(交,不是教,我沒有寫錯別字)給你們的是:走出舒適圈。其中的信念和勇氣就是,我可以“Learning by doing!”

          你們給了我更多在專業推行課程實踐改革的信心和動力。信心是:你們有這麼多潛質和能力,怎麼就不能做成項目,達成自己的能力提升呢? 動力是:在你們最好的時節,遇到你們,如果我們不能抓住這樣的機會,把握這樣的機會,做的更好,錯過了,將可惜許多未來的你們。成為系裡不少項目的開發者、成為課程核心助教、成為我們改革的初創者和開拓者,是這門課和這個學期,你們給我的最大收穫。

          就算是自賦的使命感吧,我想,當初來,並不是讓自己來掙課時費的。希望能了解現狀,立足現實,理解問題,分析原因,給出方法,執意推行,做出成果。前四點,無論老師或是學生,多多少少都有感慨和認知。不少知名的企業家,也常常在不同場合,對義務教育、高等教育提出屬於自己的真知灼見,大多數時是痛心疾首,哀其不幸,怒其不爭,覺得應該這樣改那樣改。為什麼問題顯而易見,現狀人人不滿(至少是不滿意),但改變卻牛步而行,各層次教育依然故我。人人都能對教育發表評論,因為重要,教育關係千家萬戶,關係國計民生,關係百年基業;也因為平凡,人人都受過教育,當過學生,也大多教育別人(比如養兒育女)。但這些其實是一種錯覺。企業也很重要啊,但少有人能夠對企業經營管理指摘或評論,很少其他人指導企業經營者應該怎麼做。我想,原因可能在距離。是否直接與學生互動,感受和方法,可能會真的不一樣。教學不是做菜,學生不是食材,互動勝過一切。這也是慕課為什麼知易行難,選課人數多,堅持人數微乎其微的原因。即使堅持了,效果也遠遠不如近距離教學的收穫和感受。評論或建議教育的人的錯覺,就在於希望自己的想法能被教育者一以貫之,卻常常忽略受教育者的感受、過程、反饋和互動。教學相長,如同沒有一次軟件開發項目是可以完全一樣完全照搬的,也沒有一次課程教學是可以完全一樣完全照搬的。更沒有什麼理論或建議,是可以醍醐灌頂,直接有效快速解決教育難題的。孔聖人之所以較其他名家更偉大一些,稱為至聖先師,除了有真知灼見,更在於自己帶領弟子三千,是真正戰鬥在教育一線的教育者。與學生的互動,一問一答,一段經歷,一個故事,乃至於對學生的點評,都成為後人傳頌的至理名言。將距離拉近,師生作為教學相長的團隊一體,才能相互促進,提升和改變。
         《構建之法》的作者,也正是將在清華、北航、微軟亞洲研究院的教學實踐的課程講義,凝結成書,並不斷在教學實踐中推陳出新,改版完善,才讓書具有了強大和茁壯的生命力,書中凝練的教學做法不斷推廣鋪開和可持續化,成為不少其他教學研討場合里都會提到的話題。回到根源,是經過實踐檢驗的“Learning by doing”才具有了這樣的生命力。如何繼往開來,我想,也要回到這樣的初心。不斷實踐,不斷改變。比如,線上教學,是可以發揮這樣的優勢的。作者對某些博客夜以繼日或苦心費思的點評,礙於單篇博客本身的閱讀量,點評被看到率不算高,回復率更讓人着急,也不一定能夠符合這個時代視頻影音影響力的特點。所以,正是線上教學,視頻直播,考慮到傳播力、影響力、互動性,作者可以考慮不斷前進,為不同開展的學校,做一次線上互動的軟工講座,分享軟件工程思維、案例、心得,和互動問答,這些,都將長久影響不少學生,也能逐步累積與一線學生互動的思考、感受和來自廣大讀者受眾的聲音。對作者來說,這一步,其實也是走出舒適圈,“Learning by doing!”

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • .NET 5 嘗鮮 – 開源項目TerminalMACS WPF管理端支持.NET 5

    .NET 5 嘗鮮 – 開源項目TerminalMACS WPF管理端支持.NET 5

    .NET 5 嘗鮮 – 開源項目TerminalMACS WPF管理端支持.NET 5

    一個使用 Prism 作為模塊化框架、基於多個開源控件庫作為UI控件選擇、集成開源 UI 界面設計的 .NET 5 WPF 客戶端項目。

    • 項目名稱:TerminalMACS WPF管理端
    • 項目開源地址:
      • Github:https://github.com/dotnet9/TerminalMACS.ManagerForWPF
      • Gitee:https://gitee.com/dotnet9/TerminalMACS.ManagerForWPF
    • 作者:Dotnet9

    1. 特性

    • 使用 .NET 5 開發,體驗最新 .NET 平台(和 .NET Core 3.1 無縫兼容)

    .NET 5 是 .NET Framework 和 .NET Core 的未來,最終將成為一個統一平台,.NET5將包含ASP.NET核心、實體框架核心、WinForms、WPF、Xamarin 和 ML.NET。

    • 基於 Prism 8 搭建模塊化框架,方便程序擴展

    Prism為程序設計提供指導,旨在幫助用戶更加容易的設計和構建豐富、靈活、易於維護WPF桌面應用程序。Prism使用設計模式(如MVVM,複合視圖,事件聚合器),幫助你創建一個松耦合的程序。遵循這些設計模式原則,將目標程序解耦成獨立的模塊。這些類型的應用程序被稱為複合應用程序。

    • 已使用或即將使用到多個開源WPF控件庫

      • MaterialDesignInXamlToolkit
      • HandyControl
      • PanuonUI.Silver
      • AduSkin。

    參考以上多種開源 WPF UI 庫,多個選擇,開發 WPF 項目更方便。

    • ECharts

    界面設計有使用到ECharts,使用WPF WebBrowser控件加載html的方式

    ECharts:pie-doughnut

    • 本地化支持

    • 動態國際化支持

    • 支持主題色動態切換

    2. 支持環境

    • .NET 5.0。

    3. 當前版本

    0.1

    4. 鏈接

    • 官方網站:Dotnet9

    5. 項目界面截圖

    5.1. 關於

    5.2. 首頁模塊

    正在開發中…

    5.3. 服務端模塊

    正在開發中…

    5.4. 客戶端模塊

    正在開發中…

    5.5. 測試案例

    收集全球優秀的開源WPF界面設計,實時收集、實時添加更新,下面是部分實例截圖:

    登錄註冊分類 1

    1. 簡單登錄窗體設計1

    參考視頻:C# WPF Material Design UI: Login Window

    參考源碼:Login2

    1. 簡單登錄窗體設計2

    參考視頻:C# WPF Material Design UI: Login Window

    參考源碼:Login1

    1. 美食應用登錄

    參考視頻:WPF Food App Login UI Material Design [Speed Design]

    菜單類 2

    1. 抽屜式菜單

    參考視頻:C# WPF Material Design UI: Animated Colorful Navigation Drawer

    參考源碼:AnimatedColorfulMenu

    1. 菜單切換用戶控件

    參考視頻:C# WPF Material Design UI: Fast Food Sales

    參考源碼:Pizzaria1

    1. 菜單切換動畫

    參考視頻:C# WPF Material Design UI: Animated Menu

    參考源碼:AnimatedMenu1

    其他界面設計 3

    1. 移動應用儀錶盤

    參考視頻:WPF Dashboard UI – Material Design [Speed Design]

    參考源碼:WPF-Dashboard-UI-Material-Design-Concept

    1. 簡易儀錶盤2

    參考視頻:WPF Dashboard UI – Material Design [Speed Design]

    參考源碼:WPF-Dashboard-UI-Material-Design-Concept

    ECharts:pie-doughnut

    1. Instagram重新設計

    參考視頻:C# WPF Material Design UI: Redesign Instagram

    參考源碼:Instagram

    1. LoLGoal

    參考視頻:dotnet9

    參考源碼:dotnet9

    1. 簡易音樂播放器1

    參考視頻:C# WPF Material Design UI: Dashboard

    參考源碼:Dashboard

    1. 百度地圖

    通過WPF WebBrowser控件加載html5文件的形式加載百度地圖,使用JavaScript與C#互操作實現地圖交互。

    1. 聊天界面設計

    參考視頻:

    • C# WPF Design UI – 1/3 – Contact List
    • C# WPF Design UI – 2/3 – Profile
    • C# WPF Design UI – 3/3 – Chat

    參考源碼:Chat

    1. 計算器

    參考視頻:

    • Calcalator

    關注Dotnet9,分享更多好文
    如果本文對你有用,歡迎轉載,Dotnet9對應原文有markdown格式原文分享下載哦。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • Tensorflow2 自定義數據集圖片完成圖片分類任務

    Tensorflow2 自定義數據集圖片完成圖片分類任務

    對於自定義數據集的圖片任務,通用流程一般分為以下幾個步驟:

    • Load data

    • Train-Val-Test

    • Build model

    • Transfer Learning

    其中大部分精力會花在數據的準備和預處理上,本文用一種較為通用的數據處理手段,並通過手動構建,簡單模型, 層數較深的resnet網絡,和基於VGG19的遷移學習。

    你可以通過這個例子,快速搭建網絡,並訓練處一個較為滿意的結果。

    1. Load data

    數據集來自Pokemon的5分類數據, 每一種的圖片數量為200多張,是一個較小型的數據集。

    官方項目鏈接:

    Keras and Convolutional Neural Networks (CNNs)

    1.1 數據集介紹

    Pokemon文件夾中包含5個子文件,其中每個子文件夾名為對應的類別名。文件夾中包含有png, jpeg的圖片文件。

    1.2 解題思路

    • 由於文件夾中沒有劃分,訓練集和測試集,所以需要構建一個csv文件讀取所有的文件,及其類別

    • shuffle數據集以後,劃分Train_val_test

    • 對數據進行預處理, 數據標準化,數據增強, 可視化處理

    “””python
    # 創建数字編碼錶

      import os
      import glob
      import random
      import csv
      import tensorflow as tf
      from tensorflow import keras
      import matplotlib.pyplot as plt
      import time
      
      
      def load_csv(root, filename, name2label):
          """
          將分散在各文件夾中的圖片, 轉換為圖片和label對應的一個dataset文件, 格式為csv
          :param root: 文件路徑(每個子文件夾中的文件屬於一類)
          :param filename: 文件名
          :param name2label: 類名編碼錶  {'類名1':0, '類名2':1..}
          :return: images, labels
          """
          # 判斷是否csv文件已經生成
          if not os.path.exists(os.path.join(root, filename)):  # join-將路徑與文件名何為一個路徑並返回(沒有會生成新路徑)
              images = []  # 存的是文件路徑
              for name in name2label.keys():
                  # pokemon\pikachu\00000001.png
                  # glob.glob() 利用通配符檢索路徑內的文件,類似於正則表達式
                  images += glob.glob(os.path.join(root, name, '*'))  # png, jpg, jpeg
              print(name2label)
              print(len(images), images)
      
              random.shuffle(images)
      
              with open(os.path.join(root, filename), 'w', newline='') as f:
                  writer = csv.writer(f)
                  for img in images:
                      name = img.split(os.sep)[1]  # os.sep 表示分隔符 window-'\\' , linux-'/'
                      label = name2label[name]  # 0, 1, 2..
                      # 'pokemon\\bulbasaur\\00000000.png', 0
                      writer.writerow([img, label])  # 如果不設定newline='', 2個數據會分為2行寫
                  print('write into csv file:', filename)
      
          # 讀取現有文件
          images, labels = [], []
          with open(os.path.join(root, filename)) as f:
              reader = csv.reader(f)
              for row in reader:
                  # 'pokemon\\bulbasaur\\00000000.png', 0
                  img, label = row
                  label = int(label)  # str-> int
                  images.append(img)
                  labels.append(label)
      
          assert len(images) == len(labels)
      
          return images, labels
      
      
      def load_pokemon(root, mode='train'):
          """
          # 創建数字編碼錶
          :param root: root path
          :param mode: train, valid, test
          :return: images, labels, name2label
          """
      
          name2label = {}  # {'bulbasaur': 0, 'charmander': 1, 'mewtwo': 2, 'pikachu': 3, 'squirtle': 4}
          for name in sorted(os.listdir(os.path.join(root))):
              # sorted() 是為了復現結果的一致性
              # os.listdir - 返迴路徑下的所有文件(文件夾,文件)列表
              if not os.path.isdir(os.path.join(root, name)):  # 是否為文件夾且是否存在
                  continue
              # 每個類別編碼一個数字
              name2label[name] = len(name2label)
      
          # 讀取label
          images, labels = load_csv(root, 'images.csv', name2label)
      
          # 劃分數據集 [6:2:2]
          if mode == 'train':
              images = images[:int(0.6 * len(images))]
              labels = labels[:int(0.6 * len(labels))]  # len(images) == len(labels)
      
          elif mode == 'valid':
              images = images[int(0.6 * len(images)):int(0.8 * len(images))]
              labels = labels[int(0.6 * len(labels)):int(0.8 * len(labels))]
      
          else:
              images = images[int(0.8 * len(images)):]
              labels = labels[int(0.8 * len(labels)):]
      
          return images, labels, name2label
      
      
      # imagenet 數據集均值, 方差
      img_mean = tf.constant([0.485, 0.456, 0.406])  # 3 channel
      img_std = tf.constant([0.229, 0.224, 0.225])
      
      def normalization(x, mean=img_mean, std=img_std):
          # [224, 224, 3]
          x = (x - mean) / std
          return x
      
      def denormalization(x, mean=img_mean, std=img_std):
          x = x * std + mean
          return x
      
      
      def preprocess(x, y):
          # x: path, y: label
          x = tf.io.read_file(x)  # 2進制
          # x = tf.image.decode_image(x)
          x = tf.image.decode_jpeg(x, channels=3)  # RGBA
          x = tf.image.resize(x, [244, 244])
      
          # data augmentation
          # x = tf.image.random_flip_up_down(x)
          x = tf.image.random_flip_left_right(x)
          x = tf.image.random_crop(x, [224, 224, 3])  # 模型縮減比例不宜過大,否則會增大訓練難度
      
          x = tf.cast(x, dtype=tf.float32) / 255. # unit8 -> float32
          # U[0,1] -> N(0,1)  # 提高訓練準確度
          x = normalization(x)
      
          y = tf.convert_to_tensor(y)
      
          return x, y
      
      def main():
          images, labels, name2label = load_pokemon('pokemon', 'train')
          print('images:', len(images), images)
          print('labels:', len(labels), labels)
          # print(name2label)
      
          # .map()函數要位於.batch()之前, 否則 x=tf.io.read_file()會一次讀取一個batch的圖片,從而報錯
          db = tf.data.Dataset.from_tensor_slices((images, labels)).map(preprocess).shuffle(1000).batch(32)
      
          # tf.summary()
          # 提供了各類方法(支持各種多種格式)用於保存訓練過程中產生的數據(比如loss_value、accuracy、整個variable),
          # 這些數據以日誌文件的形式保存到指定的文件夾中。
      
          # 數據可視化:而tensorboard可以將tf.summary()
          # 記錄下來的日誌可視化,根據記錄的數據格式,生成折線圖、統計直方圖、圖片列表等多種圖。
          # tf.summary()
          # 通過遞增的方式更新日誌,這讓我們可以邊訓練邊使用tensorboard讀取日誌進行可視化,從而實時監控訓練過程。
          writer = tf.summary.create_file_writer('logs')
          for step, (x, y) in enumerate(db):
              with writer.as_default():
                  x = denormalization(x)
                  tf.summary.image('img', x, step=step, max_outputs=9)  # STEP:默認選項,指的是橫軸显示的是訓練迭代次數
      
                  time.sleep(5)
      
      
      
      if __name__ == '__main__':
          main()
    

    “””

    2. 構建模型進行訓練

    2.1 自定義小型網絡

    由於數據集數量較少,大型網絡的訓練中往往會出現過擬合情況,這裏就定義了一個2層卷積的小型網絡。
    引入early_stopping回調函數后,3個epoch沒有較大變化的情況下,模型訓練的準確率為0.8547

    “””
    # 1. 自定義小型網絡
    model = keras.Sequential([
    layers.Conv2D(16, 5, 3),
    layers.MaxPool2D(3, 3),
    layers.ReLU(),
    layers.Conv2D(64, 5, 3),
    layers.MaxPool2D(2, 2),
    layers.ReLU(),
    layers.Flatten(),
    layers.Dense(64),
    layers.ReLU(),
    layers.Dense(5)
    ])

      model.build(input_shape=(None, 224, 224, 3))  
      model.summary()
      
      early_stopping = EarlyStopping(
          monitor='val_loss',
          patience=3,
          min_delta=0.001
      )
      
      
      model.compile(optimizer=optimizers.Adam(lr=1e-3),
                     loss=losses.CategoricalCrossentropy(from_logits=True),
                     metrics=['accuracy'])
      model.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
                 callbacks=[early_stopping])
      model.evaluate(db_test)
    

    “””

    2.2 自定義的Resnet網絡

    resnet 網絡對於層次較深的網絡的可訓練型提升很大,主要是通過一個identity layer保證了深層次網絡的訓練效果不會弱於淺層網絡。
    其他文章中有詳細介紹resnet的搭建,這裏就不做贅述, 這裏構建了一個resnet18網絡, 準確率0.7607。

    “””
    import os

      import numpy as np
      import tensorflow as tf
      from tensorflow import keras
      from tensorflow.keras import layers
      
      tf.random.set_seed(22)
      np.random.seed(22)
      os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
      assert tf.__version__.startswith('2.')
      
      
      class ResnetBlock(keras.Model):
      
          def __init__(self, channels, strides=1):
              super(ResnetBlock, self).__init__()
      
              self.channels = channels
              self.strides = strides
      
              self.conv1 = layers.Conv2D(channels, 3, strides=strides,
                                         padding=[[0, 0], [1, 1], [1, 1], [0, 0]])
              self.bn1 = keras.layers.BatchNormalization()
              self.conv2 = layers.Conv2D(channels, 3, strides=1,
                                         padding=[[0, 0], [1, 1], [1, 1], [0, 0]])
              self.bn2 = keras.layers.BatchNormalization()
      
              if strides != 1:
                  self.down_conv = layers.Conv2D(channels, 1, strides=strides, padding='valid')
                  self.down_bn = tf.keras.layers.BatchNormalization()
      
          def call(self, inputs, training=None):
              residual = inputs
      
              x = self.conv1(inputs)
              x = tf.nn.relu(x)
              x = self.bn1(x, training=training)
              x = self.conv2(x)
              x = tf.nn.relu(x)
              x = self.bn2(x, training=training)
      
              # 殘差連接
              if self.strides != 1:
                  residual = self.down_conv(inputs)
                  residual = tf.nn.relu(residual)
                  residual = self.down_bn(residual, training=training)
      
              x = x + residual
              x = tf.nn.relu(x)
              return x
      
      
      class ResNet(keras.Model):
      
          def __init__(self, num_classes, initial_filters=16, **kwargs):
              super(ResNet, self).__init__(**kwargs)
      
              self.stem = layers.Conv2D(initial_filters, 3, strides=3, padding='valid')
      
              self.blocks = keras.models.Sequential([
                  ResnetBlock(initial_filters * 2, strides=3),
                  ResnetBlock(initial_filters * 2, strides=1),
                  # layers.Dropout(rate=0.5),
      
                  ResnetBlock(initial_filters * 4, strides=3),
                  ResnetBlock(initial_filters * 4, strides=1),
      
                  ResnetBlock(initial_filters * 8, strides=2),
                  ResnetBlock(initial_filters * 8, strides=1),
      
                  ResnetBlock(initial_filters * 16, strides=2),
                  ResnetBlock(initial_filters * 16, strides=1),
              ])
      
              self.final_bn = layers.BatchNormalization()
              self.avg_pool = layers.GlobalMaxPool2D()
              self.fc = layers.Dense(num_classes)
      
          def call(self, inputs, training=None):
              # print('x:',inputs.shape)
              out = self.stem(inputs, training = training)
              out = tf.nn.relu(out)
      
              # print('stem:',out.shape)
      
              out = self.blocks(out, training=training)
              # print('res:',out.shape)
      
              out = self.final_bn(out, training=training)
              # out = tf.nn.relu(out)
      
              out = self.avg_pool(out)
      
              # print('avg_pool:',out.shape)
              out = self.fc(out)
      
              # print('out:',out.shape)
      
              return out
      
      
      def main():
          num_classes = 5
      
          resnet18 = ResNet(5)
          resnet18.build(input_shape=(None, 224, 224, 3))
          resnet18.summary()
      
      
      if __name__ == '__main__':
          main()
    

    “””

    “””
    # 2.resnet18訓練, 圖片數量較小,訓練結果不是特別好
    # resnet = ResNet(5) # 0.7607
    # resnet.build(input_shape=(None, 224, 224, 3))
    # resnet.summary()
    “””

    2.3 VGG19遷移學習

    遷移學習利用了數據集之間的相似性,對於數據集數量較少的時候,訓練效果會遠優於其他。
    在訓練過程中,使用include_top=False, 去掉最後分類的基層Dense, 重新構建並訓練就可以了。準確率0.9316

    “””
    # 3. VGG19遷移學習,遷移學習利用數據集之間的相似性, 結果遠好於其他2種
    # 為了方便,這裏仍然使用resnet命名
    net = tf.keras.applications.VGG19(weights=’imagenet’, include_top=False, pooling=’max’ )
    net.trainable = False
    resnet = keras.Sequential([
    net,
    layers.Dense(5)
    ])
    resnet.build(input_shape=(None, 224, 224, 3)) # 0.9316
    resnet.summary()

      early_stopping = EarlyStopping(
          monitor='val_loss',
          patience=3,
          min_delta=0.001
      )
      
      
      resnet.compile(optimizer=optimizers.Adam(lr=1e-3),
                     loss=losses.CategoricalCrossentropy(from_logits=True),
                     metrics=['accuracy'])
      resnet.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
                 callbacks=[early_stopping])
      resnet.evaluate(db_test)
    

    “””

    附錄:

    train_scratch.py 代碼

    “””

    import os
    
    os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
    
    import tensorflow as tf
    import numpy as np
    from tensorflow import keras
    from tensorflow.keras import layers, optimizers, losses
    from tensorflow.keras.callbacks import EarlyStopping
    
    tf.random.set_seed(22)
    np.random.seed(22)
    assert tf.__version__.startswith('2.')
    
    # 設置GPU顯存按需分配
    # gpus = tf.config.experimental.list_physical_devices('GPU')
    # if gpus:
    #     try:
    #         # Currently, memory growth needs to be the same across GPUs
    #         for gpu in gpus:
    #             tf.config.experimental.set_memory_growth(gpu, True)
    #         logical_gpus = tf.config.experimental.list_logical_devices('GPU')
    #         print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
    #     except RuntimeError as e:
    #         # Memory growth must be set before GPUs have been initialized
    #         print(e)
    
    from pokemon import load_pokemon, normalization
    from resnet import ResNet
    
    
    def preprocess(x, y):
        # x: 圖片的路徑,y:圖片的数字編碼
        x = tf.io.read_file(x)
        x = tf.image.decode_jpeg(x, channels=3)  # RGBA
        # 圖片縮放
        # x = tf.image.resize(x, [244, 244])
        # 圖片旋轉
        # x = tf.image.rot90(x,2)
        # 隨機水平翻轉
        x = tf.image.random_flip_left_right(x)
        # 隨機豎直翻轉
        # x = tf.image.random_flip_up_down(x)
    
        # 圖片先縮放到稍大尺寸
        x = tf.image.resize(x, [244, 244])
        # 再隨機裁剪到合適尺寸
        x = tf.image.random_crop(x, [224, 224, 3])
    
        # x: [0,255]=> -1~1
        x = tf.cast(x, dtype=tf.float32) / 255.
        x = normalization(x)
        y = tf.convert_to_tensor(y)
        y = tf.one_hot(y, depth=5)
    
        return x, y
    
    
    batchsz = 32
    
    # create train db
    images1, labels1, table = load_pokemon('pokemon', 'train')
    db_train = tf.data.Dataset.from_tensor_slices((images1, labels1))
    db_train = db_train.shuffle(1000).map(preprocess).batch(batchsz)
    # create validation db
    images2, labels2, table = load_pokemon('pokemon', 'valid')
    db_val = tf.data.Dataset.from_tensor_slices((images2, labels2))
    db_val = db_val.map(preprocess).batch(batchsz)
    # create test db
    images3, labels3, table = load_pokemon('pokemon', mode='test')
    db_test = tf.data.Dataset.from_tensor_slices((images3, labels3))
    db_test = db_test.map(preprocess).batch(batchsz)
    
    
    # 1. 自定義小型網絡
    # resnet = keras.Sequential([
    #     layers.Conv2D(16, 5, 3),
    #     layers.MaxPool2D(3, 3),
    #     layers.ReLU(),
    #     layers.Conv2D(64, 5, 3),
    #     layers.MaxPool2D(2, 2),
    #     layers.ReLU(),
    #     layers.Flatten(),
    #     layers.Dense(64),
    #     layers.ReLU(),
    #     layers.Dense(5)
    # ])  # 0.8547
    
    
    # 2.resnet18訓練, 圖片數量較小,訓練結果不是特別好
    # resnet = ResNet(5)  # 0.7607
    # resnet.build(input_shape=(None, 224, 224, 3))
    # resnet.summary()
    
    
    # 3. VGG19遷移學習,遷移學習利用數據集之間的相似性, 結果遠好於其他2種
    net = tf.keras.applications.VGG19(weights='imagenet', include_top=False, pooling='max' )
    net.trainable = False
    resnet = keras.Sequential([
        net,
        layers.Dense(5)
    ])
    resnet.build(input_shape=(None, 224, 224, 3))   # 0.9316
    resnet.summary()
    
    early_stopping = EarlyStopping(
        monitor='val_loss',
        patience=3,
        min_delta=0.001
    )
    
    
    resnet.compile(optimizer=optimizers.Adam(lr=1e-3),
                   loss=losses.CategoricalCrossentropy(from_logits=True),
                   metrics=['accuracy'])
    resnet.fit(db_train, validation_data=db_val, validation_freq=1, epochs=100,
               callbacks=[early_stopping])
    resnet.evaluate(db_test)
    

    “””

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 為什麼用抓包工具看HTTPS包是明文的

    為什麼用抓包工具看HTTPS包是明文的

    測試或者開發調試的過程中,經常會進行抓包分析,並且裝上抓包工具的證書就能抓取 HTTPS 的數據包並显示。由此就產生了一個疑問,為什麼抓包工具裝上證書後就能抓到 HTTPS 的包並显示呢?不是說 HTTPS 是加密傳輸的嗎?

    今天這篇文章就來探究下上面這個問題,要解釋清楚這個問題,我會通過解答以下兩個問題來講述:

    1. HTTPS 到底是什麼?
    2. 抓包工具抓包的原理?

    HTTPS 到底是什麼

    HTTP 作為一種被廣泛使用的傳輸協議,也存在一些的缺點:

    1. 無狀態(可以通過 Cookie 或 Session 解決);
    2. 明文傳輸;
    3. 不安全;

    為了解決 “明文” 和 “不安全” 兩個問題,就產生了 HTTPSHTTPS 不是一種單獨的協議,它是由 HTTP + SSL/TLS 組成。

    HTTP與HTTPS

    所以要理解 HTTPS 就只需在 HTTP 的基礎上理解 SSL/TLS (TLS 是 SSL 的後續版本,現在一般使用 TLS),下面就來了解下 TLS 是什麼。

    TLS

    傳輸層安全性協議(英語:Transport Layer Security,縮寫:TLS)及其前身安全套接層(英語:Secure Sockets Layer,縮寫:SSL)是一種安全協議,目的是為互聯網通信提供安全及數據完整性保障。

    TLS 由記錄協議、握手協議、警報協議、變更密碼規範協議、擴展協議等幾個子協議組成,綜合使用了對稱加密、非對稱加密、身份認證等許多密碼學前沿技術。

    • 記錄協議 規定
      TLS 收發數據的基本單位為:記錄。類似
      TCP 里的
      segment,所有其它子協議都需要通過記錄協議發出。
    • 警報協議 的職責是向對方發出警報信息,類似於
      HTTP 里的狀態碼。
    • 握手協議
      TLS 里最複雜的子協議,瀏覽器和服務器在握手過程中會協商
      TLS 版本號、隨機數、密碼套件等信息,然後交換證書和密鑰參數,最終雙方協商得到會話密鑰,用於後續的混合加密系統。
    • 變更密碼規範協議 用於告知對方,後續的數據都將使用加密傳輸。

    TLS 的握手過程:

    TLS握手過程

    握手過程抓包显示:

    TLS抓包
    TLS所傳輸的數據

    交換密鑰的過程為:

    1. 客戶端發起一個請求給服務器;
    2. 服務器生成一對非對稱的公鑰(
      pubkey)和私鑰(
      privatekey),然後把公鑰附加到一個
      CA数字證書 上返回給客戶端;
    3. 客戶端校驗該證書是否合法(通過瀏覽器內置的廠商根證書等手段校驗),然後從證書中提取出公鑰(
      pubkey);
    4. 客戶端生成一個隨機數(
      key),然後使用公鑰(
      pubkey)對這個隨機數進行加密后發送給服務器;
    5. 服務器利用私鑰(
      privatekey)對收到的隨機數密文進行解密得到
      key ;
    6. 後續客戶端和服務器傳輸數據使用該
      key 進行加密后再傳輸;

    抓包工具抓包的原理

    先來看看抓 HTTP 包的原理

    HTTP抓包過程

    1. 首先抓包工具會提供出代理服務,客戶端需要連接該代理;
    2. 客戶端發出
      HTTP 請求時,會經過抓包工具的代理,抓包工具將請求的原文進行展示;
    3. 抓包工具使用該原文將請求發送給服務器;
    4. 服務器返回結果給抓包工具,抓包工具將返回結果進行展示;
    5. 抓包工具將服務器返回的結果原樣返回給客戶端;

    抓包工具就相當於個透明的中間人,數據經過的時候它一隻手接到數據,然後另一隻手把數據傳出去。

    再來看看 HTTPS 的抓包

    HTTPS抓包過程

    這個時候抓包工具對客戶端來說相當於服務器,對服務器來說相當於客戶端。在這個傳輸過程中,客戶端會以為它就是目標服務器,服務器也會以為它就是請求發起的客戶端。

    1. 客戶端連接抓包工具提供的代理服務;
    2. 客戶端需要安裝抓包工具的根證書;
    3. 客戶端發出
      HTTPS 請求,抓包工具模擬服務器與客戶端進行
      TLS 握手交換密鑰等流程;
    4. 抓包工具發送一個
      HTTPS 請求給客戶端請求的目標服務器,並與目標服務器進行
      TLS 握手交換密鑰等流程;
    5. 客戶端使用與抓包工具協定好的密鑰加密數據后發送給抓包工具;
    6. 抓包工具使用與客戶端協定好的密鑰解密數據,並將結果進行展示;
    7. 抓包工具將解密后的客戶端數據,使用與服務器協定好的密鑰進行加密后發送給目標服務器;
    8. 服務器解密數據后,做對應的邏輯處理,然後將返回結果使用與抓包工具協定好的密鑰進行加密發送給抓包工具;
    9. 抓包工具將服務器返回的結果,用與服務器協定好的密鑰解密,並將結果進行展示;
    10. 抓包工具將解密后的服務器返回數據,使用與客戶端協定好的密鑰進行加密后發送給客戶端;
    11. 客戶端解密數據;

    總結

    • HTTPS 不是單獨的一個協議,它是
      HTTP +
      SSL/TLS 的組合;
    • TLS 是傳輸層安全性協議,它會對傳輸的
      HTTP 數據進行加密,使用非對稱加密和對稱加密的混合方式;
    • 抓包工具的原理就是“偽裝“,對客戶端偽裝成服務器,對服務器偽裝成客戶端;
    • 使用抓包工具抓
      HTTPS 包必須要將抓包工具的證書安裝到客戶端本地,並設置信任;
    • HTTPS 數據只是在傳輸時進行了加密,而抓包工具是接收到數據后再重新加密轉發,所以抓包工具抓到的
      HTTPS 包可以直接看到明文;

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • 三文搞懂學會Docker容器技術(上)

    三文搞懂學會Docker容器技術(上)

    1,Docker簡介

      1.1 Docker是什麼?

    Docker官網: https://www.docker.com/

    Docker 是一個開源的應用容器引擎,基於 Go 語言 並遵從Apache2.0協議開源。
    Docker 可以讓開發者打包他們的應用以及依賴包到一個輕量級、可移植的容器中,然後發布到任何流行的 Linux 機器上,也可以實現虛擬化。
    容器是完全使用沙箱機制,相互之間不會有任何接口(類似 iPhone 的 app),更重要的是容器性能開銷極低。
    Docker 從 17.03 版本之後分為 CE(Community Edition: 社區版) 和 EE(Enterprise Edition: 企業版),我們用社區版就可以了。

      1.2 Docker架構原理?

     

    Docker三要素,鏡像,容器,倉庫

    1.鏡像

    Docker 鏡像(Image)就是一個只讀的模板,它可以是一個可運行軟件(tomcat,mysql),也可以是一個系統(centos)。鏡像可以用來創建 Docker 容器,一個鏡像可以創建很多容器。

    2.容器

    Docker 利用容器(Container)獨立運行的一個或一組應用。容器是用鏡像創建的運行實例。它可以被啟動、開始、停止、刪除。每個容器都是相互隔離的、保證安全的平台。可以把容器看做是一個簡易版的 Linux 環境(包括root用戶權限、進程空間、用戶空間和網絡空間等)和運行在其中的應用程序。容器的定義和鏡像幾乎一模一樣,也是一堆層的統一視角,唯一區別在於容器的最上面那一層是可讀可寫的。

    3.倉庫

    倉庫(Repository)是集中存放鏡像文件的場所,類似GitHub存放項目代碼一樣,只不過Docker Hub是由來存鏡像(image)的。倉庫(Repository)和倉庫註冊服務器(Registry)是有區別的。倉庫註冊服務器上往往存放着多個倉庫,每個倉庫中又包含了多個鏡像,每個鏡像有不同的標籤(tag,類似版本號)。

    倉庫分為公開倉庫(Public)和私有倉庫(Private)兩種形式。

    最大的公開倉庫是 Docker Hub(https://hub.docker.com/),存放了數量龐大的鏡像供用戶下載。國內的公開倉庫包括阿里雲 、網易雲 等。

     

    容器與鏡像的關係類似於面向對象編程中的對象與類。

    Docker 面向對象
    容器 對象
    鏡像

      1.3 Docker有什麼用?

        1,簡化環境搭建,提高開發生命周期效率;

        2,大大簡化運維工作量;

        3,微服務利器;

      1.4 Docker容器與虛擬機區別?

    Docker是一種輕量級的虛擬化技術,比傳統的虛擬機性能更好。

    下圖是虛擬機的體繫結構:

     

    • server – 表示真實電腦。
    • Host OS – 真實電腦的操作系統,例如:Windows,Linux
    • Hypervisor – 虛擬機平台,模擬硬件,如VMWare,VirtualBox
    • Guest OS – 虛擬機平台上安裝的操作系統,例如CentOS Linux
    • App – 虛擬機操作系統上的應用,例如nginx

     

    下圖是Docker的體繫結構:

    • server – 表示真實電腦。
    • Host OS – 真實電腦的操作系統,例如:Windows,Linux
    • Docker Engine – 新一代虛擬化技術,不需要包含單獨的操作系統。
    • App – 所有的應用程序現在都作為Docker容器運行。

     

    這種體繫結構的明顯優勢是,不需要為虛擬機操作系統提供硬件模擬。所有應用程序都作為Docker容器工作,性能更好。

      Docker容器 虛擬機(VM)
    操作系統 與宿主機共享OS 宿主機OS上運行宿主機OS
    存儲大小 鏡像小,便於存儲與傳輸 鏡像龐大(vmdk等)
    運行性能 幾乎無額外性能損失 操作系統額外的cpu、內存消耗
    移植性 輕便、靈活、適用於Linux 笨重、與虛擬化技術耦合度高
    硬件親和性  面向軟件開發者 面向硬件運維者

     

    Docker優點:輕量級,速度快,運行應用隔離,方便維護…

    2,Docker安裝

      2.1 Docker版本介紹

    Docker從1.13版本之後採用時間線的方式作為版本號,分為社區版CE和企業版EE。

    社區版是免費提供給個人開發者和小型團體使用的,企業版會提供額外的收費服務,比如經過官方測試認證過的基礎設施、容器、插件等。

    社區版按照stable和edge兩種方式發布,每個季度更新stable版本,如17.06,17.09;每個月份更新edge版本,如17.09,17.10。

    我們平時用社區版就足夠了。所以我們安裝社區版;

      2.2 Docker安裝官方文檔

    我們主要參考:https://docs.docker.com/install/linux/docker-ce/centos/  來安裝;

      2.3 工具準備

    前置課程:Centos課程  http://www.java1234.com/javaxuexiluxiantu.html

    打包下載: http://pan.baidu.com/s/1i55jJAt

    虛擬機 VMware

    centos7安裝下虛擬機VM上;

    連接工具 才用 FinalShell  官方地址:http://www.hostbuf.com/

      2.4 Docker安裝步驟

    我們切換到root用戶

    1、Docker 要求 CentOS 系統的內核版本高於 3.10 ,查看本頁面的前提條件來驗證你的CentOS 版本是否支持 Docker 。

    通過 uname -r 命令查看你當前的內核版本

     $ uname -r

    2、使用 root 權限登錄 Centos。確保 yum 包更新到最新。

    $ yum update

    3、卸載舊版本(如果安裝過舊版本的話)

    $ yum remove docker  docker-common docker-selinux docker-engine

    4、安裝需要的軟件包, yum-util 提供yum-config-manager功能,另外兩個是devicemapper驅動依賴的

    $ yum install -y yum-utils device-mapper-persistent-data lvm2

    5、設置yum源

    $ yum-config-manager –add-repo https://download.docker.com/linux/centos/docker-ce.repo

    6,安裝最新版本的Docker

    $ yum install docker-ce docker-ce-cli containerd.io

    7,啟動Docker並設置開機啟動

    $ systemctl start docker

    $ systemctl enable docker

    8,驗證Docker

    $ docker version

     

    說明安裝OK;

    9,Docker HelloWorld測試;

    $ docker run hello-world

     

    因為本地沒有這個鏡像,所以從遠程官方倉庫去拉取,下載;

    然後我們再執行一次;

     

    OK了

      2.5 Docker配置阿里雲鏡像倉庫

    Docker默認遠程倉庫是 https://hub.docker.com/

    比如我們下載一個大點的東西,龜速

     

    由於是國外主機,類似Maven倉庫,慢得一腿,經常延遲,破損;

    所以我們一般都是配置國內鏡像,比如阿里雲,網易雲等;推薦阿里雲,穩定點;

    配置步驟如下:

    1,登錄進入阿里雲鏡像服務中心,獲取鏡像地址

    進入阿里雲容器鏡像服務地址:點這裏快速進入

    使用你的淘寶賬號密碼登錄

     

    這裏我們獲取鏡像地址;

    2,在/etc/docker目錄下找到在daemon.json文件(沒有就新建),將下面內容寫入

    {

     “registry-mirrors”: [“https://xxxxxxx.mirror.aliyuncs.com”]

    }

    3,重啟daemon

    systemctl daemon-reload

    4,重啟docker服務

    systemctl restart docker

    5,測試

    由於速度太快,截圖都難;

     

    3,HelloWorld運行原理

    運行  docker run hello-world

    本地倉庫未能找到該鏡像,然後去遠程倉庫尋找以及下載該鏡像;

    然後我們再執行該命令:

    出來了 Hellowold。我們具體來分析下 執行原理和過程;

    從左到右 client客戶端,Docker運行主機,遠程倉庫;

    docker build ,pull,run分別是 構建,拉取,運行命令,後面再細講;

    中間Docker主機里有 Docker daemon主運行線程,以及Containers容器,容器里可以運行很多實例,(實例是從右側Images鏡像實例化出來的)Images是存儲再本地的鏡像文件,比如 Redis,Tomat這些鏡像文件;

    右側是Registry鏡像倉庫,默認遠程鏡像倉庫 https://hub.docker.com/  不過是國外主機,下載很慢,不穩定,所以我們後面要配置成阿里雲倉庫鏡像地址,穩定快捷;

    執行 docker run hello-world的過程看如下圖例:

     

     

     

    4,Docker基本命令

       4.1 啟動Docker

               systemctl start docker

      4.2 停止Docker

             systemctl stop docker

      4.3 重啟Docker

           systemctl restart docker

      4.4 開機啟動Docker

         systemctl enable docker

      4.5 查看Docker概要信息

       docker info

      4.6 查看Docker幫助文檔

       docker –help

      4.7 查看Docker版本信息

         docker version

    5,Docker鏡像

      5.1 docker images 列出本機所有鏡像

     

    REPOSITORY 鏡像的倉庫源
    TAG 鏡像的標籤(版本)同一個倉庫有多個TAG的鏡像,多個版本;我們用REPOSITORY:TAG來定義不同的鏡像;
    IMAGE ID 鏡像ID,鏡像的唯一標識
    CREATE 鏡像創建時間
    SIZE 鏡像大小

    OPTIONS 可選參數:

    -a 显示所有鏡像(包括中間層)
    q 只显示鏡像ID
    -qa 可以組合
    –digests 显示鏡像的摘要信息
    –no-trunc 显示完整的鏡像信息 

     

      5.2 docker search 搜索鏡像

    和 https://hub.docker.com/ 這裏的搜索效果一樣;

    OPTIONS可選參數:

    –no-trunc 显示完整的鏡像描述
    -s 列出收藏數不小於指定值的鏡像
    –automated 只列出Docker Hub自動構建類型的鏡像

     

     

     

      5.3 docker pull 下載鏡像

    docker pull 鏡像名稱:[TAG]

    注意:不加TAG,默認下載最新版本latest

      5.4 docker rmi 刪除鏡像

    1,刪除單個:docker rmi 鏡像名稱:[TAG]

    如果不寫TAG,默認刪除最新版本latest

    有鏡像生成的容器再運行時候,會報錯,刪除失敗;

    我們需要加 -f 強制刪除

    2,刪除多個:docker rmi -f 鏡像名稱1:[TAG] 鏡像名稱2:[TAG]

    中間空格隔開

    3,刪除全部:docker rmi -f $(docker images -qa)

     

     

    ——————————————————————————————————————————

    作者: java1234_小鋒

    出處:https://www.cnblogs.com/java688/p/13132444.html

    版權:本站使用「CC BY 4.0」創作共享協議,轉載請在文章明顯位置註明作者及出處。

    ——————————————————————————————————————————

     

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 一文讀懂:GBDT梯度提升

    一文讀懂:GBDT梯度提升

    先縷一縷幾個關係:

    • GBDT是gradient-boost decision tree
    • GBDT的核心就是gradient boost,我們搞清楚什麼是gradient boost就可以了
    • GBDT是boost中的一種方法,boost還有XGBoost,adaboost。

    基本概念

    【Boost】就是讓多個弱分類器,通過不同的集成方式,來讓多個弱分類器變成一個強分類器。

    【gradient-boost】 梯度提升。簡單的說,先訓練一個弱分類器,然後弱分類器和目標值之間的殘差,作為下一個弱分類器訓練的目標值。這裡有一個非常簡單的例子

    • 第一個模型預測年齡,雖然真實值是30歲,第一個模型只給出了20歲的估計值;
    • 第二棵樹要預測的就是這個10歲的殘差,但是第二棵樹只給出了6歲的估計值;
    • 第三棵樹預測的是第二棵樹的4歲的殘差,但是………………(禁止套娃)

    梯度 or 殘差 ?

    對於GBDT,網上的很多文章都沒有講清楚,學習梯度還是學習殘差?從上面的那個例子來看,是學習殘差的。

    其實,從來GBDT都是學習梯度的,學習殘差只是學習梯度的一個特例!

    如果我們是在做一個回歸任務(就像是上面例子中預測年齡),採用平方損失:\(loss = \frac{1}{2}\sum^n_i{(y_i-\hat{y_i})^2}\)
    其中\(y_i\)是真實數值,\(\hat{y_i}\)是模型預測的值。

    然後想求取這個關於\(\hat{y_i}\)的梯度,那就是:
    \(\frac{\partial loss}{\partial \hat{y^i}}=(-1)(y_i-\hat{y_i})\)

    所以殘差在平方損失的情況下,就是等於負梯度,所以兩者一回事。

    殘差過於敏感

    對於數據不幹凈,沒有清晰掉異常值的數據樣本。使用平方損失對異常值過於敏感了

    所以,這裡在回歸問題中,也可以考慮使用下面的兩個損失函數:

    • Absolute loss:
      \(loss=|y-\hat{y}|\)

    • Huber loss:
      這個是設置一個閾值,當\(|y-\hat{y}|\)小於這個閾值的時候,採用平方損失,當\(|y-\hat{y}|\)大於這個閾值的時候,採用類似於絕對損失的線性損失:

      這裏看一下huber loss的函數圖像:

      就是一個平方損失,一個線性損失。

    然後看一下平方損失,絕對損失,huber損失對於異常值的容忍程度:

    CART回歸樹分裂思路(可不看)

    其實這個問題看起來問的不明所以,其實是問你決策樹如何選擇特徵的。從上面的例子可以看出來,GDBT應該是處理回歸問題的(處理連續數據的)。當然,GDBT也有辦法處理分類問題,只是這裏就不說了,這裏主要說GDBT怎麼處理回歸問題的,回歸問題能處理,那麼總有回歸離散化的辦法的

    這個問題是在問:CART TREE如何選擇特徵的CART TREE就是回歸決策樹,就是之前提到的弱分類器。

    一個決策樹,希望讀者已經有一個大概的理解了。簡單說就是:樣本可以根據的特徵A是否超過某一個閾值劃分成兩部分,然後劃分之後的每一個部分又可以根據某一個特徵是否超過某一個閾值再分成兩部分……

    這樣我們就要做出選擇了:每一個部分是根據哪一個特徵去劃分?根據這個特徵的哪一個數值作為閾值劃分?

    如果我們算力無窮,那麼自然可以遍歷每一個特徵,然後窮舉每一種可能的分割點,然後對比找到最優分割點。

    那麼如何判斷分割的點的好壞呢?得給出一個cost函數,或者叫做loss函數這樣的東西吧。

    \(loss= \sum_{第一部分}{(y_i-me an(y_{第一部分}))^2}+\sum_{第二部分}{(y_i-mean(y_{第二部分}))^2}\)

    看一下這個公式,我把公式寫的太丑了。其實這個公式非常的好理解:現在根據某一個特徵值,根據某一個閾值把樣本分成了兩個部分:第一部分和第二部分。然後計算每一個部分的樣本的label的均值,也就是公式中的:\(mean(y_{第一部分})\),\(mean(y_{第二部分})\),然後計算第一部分中所有樣本的label與第一部分label均值之間的差的平方和,同樣的過程計算第二個部分的,兩個相加起來就是這個loss。選擇能夠讓這個loss最小的分割特徵和分割閾值,就是我們要找的東西。

    其實我在學這一塊的時候,發現這個過程像是什麼?像不像聚類算法,通過上面的loss的最小化的過程,把一堆樣本分成兩類,讓兩類的類內距離最小。那個均值就像是求類中心點,計算每一個label距離類中心點的距離。(這一段看不懂也沒事

    喜歡的話請關注我們的微信公眾號~【你好世界煉丹師】。

    • 公眾號主要講統計學,數據科學,機器學習,深度學習,以及一些參加Kaggle競賽的經驗。
    • 公眾號內容建議作為課後的一些相關知識的補充,飯後甜點。
    • 此外,為了不過多打擾,公眾號每周推送一次,每次4~6篇精選文章。

    微信搜索公眾號:你好世界煉丹師。期待您的關注。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準

  • 熱浪席捲 澳洲全國均溫40.9度創新高

    摘錄自2019年12月18日中央社報導

    澳17日出現有紀錄以來最熱的一天,氣象局測到全國平均氣溫高達攝氏40.9度(華氏105.6度)。先前澳洲高溫紀錄為2013年1月時創下的攝氏40.3度。

    在近日野火肆虐澳洲東岸,又有熱浪席捲各地,氣候狀況更形惡劣的情況下,高溫紀錄預料很快就會再寫新高。

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 氣候暖化影響冬眠 俄羅斯小獾提早甦醒

    摘錄自2020年2月13日公視報導

    受到暖冬影響,冬眠的動物提早甦醒,像是在俄羅斯,動物園裡的獾原本應該睡到月底,卻在月初就爬起來玩。

    在俄羅斯西伯利亞城市伊爾庫茨克的動物園,發現有兩隻獾,在二月的第一週就醒來一起玩,牠們去年這時候還在冬眠,當時還跟往年作息一樣,睡到月底。助理獸醫史達茲卡雅表示:「牠們都是小獾,也就是新生代。成年的獾都還在睡覺,牠們冬眠會比較久。」

    這個月10號,當地氣溫攝氏兩度,但去年的同一天,氣溫為零下24度。此外,動物園內這隻名為藍波的刺蝟,也在上週跑出來活動,園方表示,通常這表示春天就快到了,史達茲卡雅說:「動物比我們的感覺還要強,牠們更能適應大自然的改變,牠們也會比較早感覺得到。」

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※幫你省時又省力,新北清潔一流服務好口碑

    ※回頭車貨運收費標準