分類: 3C資訊

  • 一次找出範圍內的所有素數,埃式篩法是什麼神仙算法?

    一次找出範圍內的所有素數,埃式篩法是什麼神仙算法?

    本文始發於個人公眾號:TechFlow,原創不易,求個關注

    今天這篇是算法與數據結構專題的第23篇文章,我們繼續數論相關的算法,來看看大名鼎鼎的埃式篩法。

    我們都知道在數學領域,素數非常重要,有海量的公式和研究關於素數,比如那個非常著名至今沒有人解出來的哥德巴赫猜想。和數學領域一樣,素數在信息領域也非常重要,有着大量的應用。舉個簡單的例子,很多安全加密算法也是利用的質數。我們想要利用素數去進行各種計算之前,總是要先找到素數。所以這就有了一個最簡單也最不簡單的問題,我們怎麼樣來尋找素數呢?

    判斷素數

    尋找素數最樸素的方法當然是一個一個遍歷,我們依次遍歷每一個數,然後分別判斷是否是素數。所以問題的核心又回到了判斷素數上,那麼怎麼判斷一個數是不是素數呢?

    素數的性質只有一個,就是只有1和它本身這兩個因數,我們要判斷素數也只能利用這個性質。所以可以想到,假如我們要判斷n是否是素數,可以從2開始遍歷到n-1,如果這n-1個數都不能整除n,那麼說明n就是素數。這個我沒記錯在C語言的練習題當中出現過,總之非常簡單,可以說是最簡單的算法了。

    def is_prime(n):
        for i in range(2, n):
            if n % i == 0:
                return False
        return n != 1
    

    顯然,這個算法是可以優化的,比如當n是偶數的時候,我們根本不需要循環,除了2意外的偶數一定是合數。再比如,我們循環的上界其實也沒有必要到n-1,到就可以了。因為因數如果存在一定是成對出現的,如果存在小於根號n的因數,那麼n除以它一定大於根號n。

    這個改進也很簡單,稍作改動即可:

    def is_prime(n):
        if n % 2 == 0 and n != 2:
            return False
        for i in range(3, int(math.sqrt(n) + 1)):
            if n % i == 0:
                return False
        return n != 1
    

    這樣我們把O(n)的算法優化到了O()也算是有了很大的改進了,但是還沒有結束,我們還可以繼續優化。數學上有一個定理,只有形如6n-1和6n+1的自然數可能是素數,這裏的n是大於等於1的整數。

    這個定理乍一看好像很高級,但其實很簡單,因為所有自然數都可以寫成6n,6n+1,6n+2,6n+3,6n+4,6n+5這6種,其中6n,6n+2,6n+4是偶數,一定不是素數。6n+3可以寫成3(2n+1),顯然也不是素數,所以只有可能6n+1和6n+5可能是素數。6n+5等價於6n-1,所以我們一般寫成6n-1和6n+1。

    利用這個定理,我們的代碼可以進一步優化:

    def is_prime(n):
        if n % 6 not in (1, 5) and n not in (2, 3):
            return False
        for i in range(3, int(math.sqrt(n) + 1)):
            if n % i == 0:
                return False
        return n != 1
    

    雖然這樣已經很快了,但仍然不是最優的,尤其是當我們需要尋找大量素數的時候,仍會消耗大量的時間。那麼有沒有什麼辦法可以批量查找素數呢?

    有,這個方法叫做埃拉托斯特尼算法。這個名字念起來非常拗口,這是一個古希臘的名字。此人是個古希臘的大牛,是大名鼎鼎的阿基米德的好友。他雖然沒有阿基米德那麼出名,但是也非常非常厲害,在數學、天文學、地理學、文學、歷史學等多個領域都有建樹。並且還自創方法測量了地球直徑、地月距離、地日距離以及黃赤交角等諸多數值。要知道他生活的年代是兩千五百多年前,那時候中國還是春秋戰國時期,可以想見此人有多厲害。

    埃式篩法

    我們今天要介紹的埃拉托斯特尼算法就是他發明的用來篩選素數的方法,為了方便我們一般簡稱為埃式篩法或者篩法。埃式篩法的思路非常簡單,就是用已經篩選出來的素數去過濾所有能夠被它整除的數。這些素數就像是篩子一樣去過濾自然數,最後被篩剩下的數自然就是不能被前面素數整除的數,根據素數的定義,這些剩下的數也是素數。

    舉個例子,比如我們要篩選出100以內的所有素數,我們知道2是最小的素數,我們先用2可以篩掉所有的偶數。然後往後遍歷到3,3是被2篩剩下的第一個數,也是素數,我們再用3去篩除所有能被3整除的數。篩完之後我們繼續往後遍歷,第一個遇到的數是7,所以7也是素數,我們再重複以上的過程,直到遍歷結束為止。結束的時候,我們就獲得了100以內的所有素數。

    如果還不太明白,可以看下下面這張動圖,非常清楚地還原了這整個過程。

    不見圖 請翻牆

    這個思想非常簡單,理解了之後寫出代碼來真的很容易:

    def eratosthenes(n):
        primes = []
        is_prime = [True] * (n + 1)
        for i in range(2, n+1):
            if is_prime[i]:
                primes.append(i)
                # 用當前素數i去篩掉所有能被它整除的數
                for j in range(i * 2, n+1, i):
                    is_prime[j] = False
        return primes
    

    我們運行一次代碼看看:

    和我們的預期一樣,獲得了小於100的所有素數。我們來分析一下篩法的複雜度,從代碼當中我們可以看到,我們一共有了兩層循環,最外面一層循環固定是遍歷n次。而裏面的這一層循環遍歷的次數一直在變化,並且它的運算次數和素數的大小相關,看起來似乎不太方便計算。實際上是可以的,根據素數分佈定理以及一系列複雜的運算(相信我,你們不會感興趣的),我們是可以得出篩法的複雜度是

    極致優化

    篩法的複雜度已經非常近似了,因為即使在n很大的時候,經過兩次ln的計算,也非常近似常數了,實際上在絕大多數使用場景當中,上面的算法已經足夠應用了。

    但是仍然有大牛不知滿足,繼續對算法做出了優化,將其優化到了的複雜度。雖然從效率上來看並沒有數量級的提升,但是應用到的思想非常巧妙,值得我們學習。在我們理解這個優化之前,先來看看之前的篩法還有什麼可以優化的地方。比較明顯地可以看出來,對於一個合數而言,它可能會被多個素數篩去。比如38,它有2和19這兩個素因數,那麼它就會被置為兩次False,這就帶來了額外的開銷,如果對於每一個合數我們只更新一次,那麼是不是就能優化到了呢?

    怎麼樣保證每個合數只被更新一次呢?這裏要用到一個定理,就是每個合數分解質因數只有的結果是唯一的。既然是唯一的,那麼一定可以找到最小的質因數,如果我們能夠保證一個合數只會被它最小的質因數更新為False,那麼整個優化就完成了。

    那我們具體怎麼做呢?其實也不難,我們假設整數n的最小質因數是m,那麼我們用小於m的素數i乘上n可以得到一個合數。我們將這個合數消除,對於這個合數而言,i一定是它最小的質因數。因為它等於i * n,n最小的質因數是m,i 又小於m,所以i是它最小的質因數,我們用這樣的方法來生成消除的合數,這樣來保證每個合數只會被它最小的質因數消除。

    根據這一點,我們可以寫出新的代碼:

    def ertosthenes(n):
        primes = []
        is_prime = [True] * (n+1)
        for i in range(2, n+1):
            if is_prime[i]:
                primes.append(i)
            for j, p in enumerate(primes):
                # 防止越界
                if p > n // i:
                    break
                # 過濾
       is_prime[i * p] = False
                # 當i % p等於0的時候說明p就是i最小的質因數
                if i % p == 0:
                    break
                    
        return primes
    

    總結

    到這裏,我們關於埃式篩法的介紹就告一段落了。埃式篩法的優化版本相對來說要難以記憶一些,如果記不住的話,可以就只使用優化之前的版本,兩者的效率相差並不大,完全在可以接受的範圍之內。

    篩法看着代碼非常簡單,但是非常重要,有了它,我們就可以在短時間內獲得大量的素數,快速地獲得一個素數表。有了素數表之後,很多問題就簡單許多了,比如因數分解的問題,比如信息加密的問題等等。我每次回顧篩法算法的時候都會忍不住感慨,這個兩千多年前被發明出來的算法至今看來非但不過時,仍然還是那麼巧妙。希望大家都能懷着崇敬的心情,理解算法當中的精髓。

    如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

    本文使用 mdnice 排版

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

    【其他文章推薦】

    網頁設計最專業,超強功能平台可客製化

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

    ※回頭車貨運收費標準

    ※推薦評價好的iphone維修中心

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

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    台中搬家公司費用怎麼算?

  • 容器技術之Docker私有鏡像倉庫docker-distribution

    容器技術之Docker私有鏡像倉庫docker-distribution

      在前邊的博客中我們說到docker的架構由docker客戶端、服務端以及倉庫組成;docker倉庫就是用來存放鏡像的地方;其實docker registry我們理解為存放docker鏡像倉庫的倉庫比較準確吧;因為docker的鏡像倉庫通常是把同一類的鏡像用不同的版本來區別,而registry則是用來存放這些倉庫的倉庫;默認安裝docker都是從dockerhub鏡像倉庫下載鏡像;其實在生產環境中,我們很少去公有倉庫上下載鏡像,原因之一是公有倉庫中的鏡像在生產環境中使用,有些不適配,通常我們是去公有倉庫下載基礎鏡像,然後基於基礎鏡像構建適合自己生產環境中的鏡像;其次公有倉庫鏡像有很多都不是安全的鏡像,這麼說吧,我們不確定自己下載的鏡像是否有後門,是否有挖礦代碼,所以基於種種因素,我們還是有必要搭建自己私有的鏡像倉庫;今天我們就來聊一聊docker的私有鏡像倉庫的搭建;

      1、查看docker-distribution包簡介

    [root@docker_registry ~]# yum info docker-distribution
    Loaded plugins: fastestmirror
    Loading mirror speeds from cached hostfile
     * base: mirrors.aliyun.com
     * extras: mirrors.aliyun.com
     * updates: mirrors.aliyun.com
    Available Packages
    Name        : docker-distribution
    Arch        : x86_64
    Version     : 2.6.2
    Release     : 2.git48294d9.el7
    Size        : 3.5 M
    Repo        : extras/7/x86_64
    Summary     : Docker toolset to pack, ship, store, and deliver content
    URL         : https://github.com/docker/distribution
    License     : ASL 2.0
    Description : Docker toolset to pack, ship, store, and deliver content
    
    [root@docker_registry ~]# 
    

      提示:docker-distribution這個包就是提供簡單倉庫服務軟件實現;

      2、安裝docker-distribution

    [root@docker_registry ~]# yum install -y docker-distribution
    Loaded plugins: fastestmirror
    Loading mirror speeds from cached hostfile
     * base: mirrors.aliyun.com
     * extras: mirrors.aliyun.com
     * updates: mirrors.aliyun.com
    Resolving Dependencies
    There are unfinished transactions remaining. You might consider running yum-complete-transaction, or "yum-complete-transaction --cleanup-only" and "yum history redo last", first to finish them. If those don't work you'll have to try removing/installing packages by hand (maybe package-cleanup can help).
    The program yum-complete-transaction is found in the yum-utils package.
    --> Running transaction check
    ---> Package docker-distribution.x86_64 0:2.6.2-2.git48294d9.el7 will be installed
    --> Finished Dependency Resolution
    
    Dependencies Resolved
    
    ===================================================================================================================
     Package                         Arch               Version                               Repository          Size
    ===================================================================================================================
    Installing:
     docker-distribution             x86_64             2.6.2-2.git48294d9.el7                extras             3.5 M
    
    Transaction Summary
    ===================================================================================================================
    Install  1 Package
    
    Total download size: 3.5 M
    Installed size: 12 M
    Downloading packages:
    docker-distribution-2.6.2-2.git48294d9.el7.x86_64.rpm                                       | 3.5 MB  00:00:03     
    Running transaction check
    Running transaction test
    Transaction test succeeded
    Running transaction
      Installing : docker-distribution-2.6.2-2.git48294d9.el7.x86_64                                               1/1 
      Verifying  : docker-distribution-2.6.2-2.git48294d9.el7.x86_64                                               1/1 
    
    Installed:
      docker-distribution.x86_64 0:2.6.2-2.git48294d9.el7                                                              
    
    Complete!
    [root@docker_registry ~]# 
    

      3、查看docker-distribution安裝了那些文件

    [root@docker_registry ~]# rpm -ql docker-distribution
    /etc/docker-distribution/registry/config.yml
    /usr/bin/registry
    /usr/lib/systemd/system/docker-distribution.service
    /usr/share/doc/docker-distribution-2.6.2
    /usr/share/doc/docker-distribution-2.6.2/AUTHORS
    /usr/share/doc/docker-distribution-2.6.2/CONTRIBUTING.md
    /usr/share/doc/docker-distribution-2.6.2/LICENSE
    /usr/share/doc/docker-distribution-2.6.2/MAINTAINERS
    /usr/share/doc/docker-distribution-2.6.2/README.md
    /var/lib/registry
    [root@docker_registry ~]# 
    

      提示:/etc/docker-distribution/registry/config.yml這個文件用於配置registry的配置文件;/usr/bin/registry是二進制應用程序;/usr/lib/systemd/system/docker-distribution.service 這個文件是docker-distribution的unit file;/var/lib/registry這個目錄用於存放我們上傳到registry上的鏡像存放地;

      4、查看配置文件

    [root@docker_registry ~]# cat /etc/docker-distribution/registry/config.yml
    version: 0.1
    log:
      fields:
        service: registry
    storage:
        cache:
            layerinfo: inmemory
        filesystem:
            rootdirectory: /var/lib/registry
    http:
        addr: :5000
    [root@docker_registry ~]# 
    

      提示:這個配置文件是一個yml語法的配置文件,從上面的信息可以看到,默認情況docker-distribution監聽在tcp的5000端口;存放鏡像的目錄是/var/lib/registry/目錄下;

      5、啟動docker-distribution

    [root@docker_registry ~]# systemctl start docker-distribution
    [root@docker_registry ~]# ss -tnl
    State       Recv-Q Send-Q            Local Address:Port                           Peer Address:Port              
    LISTEN      0      128                           *:22                                        *:*                  
    LISTEN      0      100                   127.0.0.1:25                                        *:*                  
    LISTEN      0      128                          :::22                                       :::*                  
    LISTEN      0      100                         ::1:25                                       :::*                  
    LISTEN      0      128                          :::5000                                     :::*                  
    [root@docker_registry ~]# 
    

      提示:可以看到5000端口已經處於監聽狀態了;到此docker-distribution就啟動起來了;這個倉庫服務很簡陋,沒有用戶認證功能,默認是基於http通信而非https,所以從某些角度講,不是一個安全的倉庫;所以一般不見在互聯網上使用,在自己的內外環境中可以使用;

      這裏補充一點,docker的鏡像通常是 registry地址加repository名稱加版本這三部分組成,registry可以是域名,可以是ip地址加端口,也可以說域名加端口,默認https是443端口,http是80端口,如果不寫端口默認是443而非80(原因是docker默認不支持從http協議的倉庫下載/上傳鏡像);例如 quay.io/coreos/flannel:v0.12.0-s390x  從這個鏡像名我們就可以知道registry是https://quay.io;repository名稱為coreos/flannel 版本是v0.12.0-s390x;

      示例:下載第三方倉庫鏡像到本地

      提示:可以看到下載下來的鏡像名稱就是我們剛才說的registry+repository+版本;從上面的信息我們可以總結一點,docker鏡像的名稱(標籤)反應了該鏡像來自哪個registry的那個倉庫;所以我們要下載私有鏡像倉庫中的鏡像就需要把加上私有registry的名稱或地址+repository+版本來下載私有鏡像倉庫中的鏡像;同理上傳鏡像也需要寫明上傳到那個registry中的那個repository中去;

      示例:上傳本地鏡像到私有倉庫

      提示:要把本地倉庫鏡像傳到私有倉庫中去,首先我們要把本地鏡像打一個新的標籤,按照我們剛才上面說的邏輯,然後在上傳新打到標籤的鏡像到私有倉庫就可以了;從上面的信息我們看到當我們打好標籤后,上傳鏡像時報錯了,提示我們倉庫不是https的;默認情況docker不支持http明文上傳/下載鏡像;如果我們非要用http上傳下載鏡像我們需要在配置文件中明確的告訴docker非安全倉庫地址;

      配置docker支持私有倉庫上傳下載鏡像

    [root@docker_registry ~]# cat /etc/docker/daemon.json
    {
            "registry-mirrors": ["https://registry.docker-cn.com","https://cyr1uljt.mirror.aliyuncs.com"],
            "insecure-registries": ["192.168.0.99:5000"]
    }
    
    [root@docker_registry ~]# systemctl daemon-reload    
    [root@docker_registry ~]# systemctl restart docker 
    

      提示:我們通過在配置文件中配置insecure-registries來告訴docker192.168.0.99:5000這個registry是不安全的,但是我們信任這個倉庫,大概就是這個意思嘛;通常我們是寫主機名然後配合hosts文件來解析的方式來對registry解析;從而把鏡像命名為主機名+倉庫名+版本的形式;如下所示;這裏還需要注意一點insecure-registries後面的列表中的倉庫如果有域名,域名不能有下劃線(“_”),否則重啟docker會起不來;

    [root@docker_registry ~]# cat /etc/docker/daemon.json 
    {
            "registry-mirrors": ["https://registry.docker-cn.com","https://cyr1uljt.mirror.aliyuncs.com"],
            "insecure-registries": ["192.168.0.99:5000","docker-registry.io:5000"]
    
    }
    [root@docker_registry ~]# cat /etc/hosts
    127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
    ::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
    192.168.0.99 docker-registry.io registry
    192.168.0.22 docker-node01.io node01
    192.168.0.23 docker-node02.io node02
    [root@docker_registry ~]# systemctl restart docker
    [root@docker_registry ~]# docker info
    Client:
     Debug Mode: false
    
    Server:
     Containers: 0
      Running: 0
      Paused: 0
      Stopped: 0
     Images: 1
     Server Version: 19.03.11
     Storage Driver: overlay2
      Backing Filesystem: xfs
      Supports d_type: true
      Native Overlay Diff: true
     Logging Driver: json-file
     Cgroup Driver: cgroupfs
     Plugins:
      Volume: local
      Network: bridge host ipvlan macvlan null overlay
      Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
     Swarm: inactive
     Runtimes: runc
     Default Runtime: runc
     Init Binary: docker-init
     containerd version: 7ad184331fa3e55e52b890ea95e65ba581ae3429
     runc version: dc9208a3303feef5b3839f4323d9beb36df0a9dd
     init version: fec3683
     Security Options:
      seccomp
       Profile: default
     Kernel Version: 3.10.0-693.el7.x86_64
     Operating System: CentOS Linux 7 (Core)
     OSType: linux
     Architecture: x86_64
     CPUs: 4
     Total Memory: 1.785GiB
     Name: docker_registry
     ID: R34V:IG2F:23I6:6WG6:FFQ4:75SV:3UKZ:RFH7:DGCO:QS7V:CS7K:NSH6
     Docker Root Dir: /var/lib/docker
     Debug Mode: false
     Registry: https://index.docker.io/v1/
     Labels:
     Experimental: false
     Insecure Registries:
      192.168.0.99:5000
      docker-registry.io:5000
      127.0.0.0/8
     Registry Mirrors:
      https://registry.docker-cn.com/
      https://cyr1uljt.mirror.aliyuncs.com/
     Live Restore Enabled: false
    
    [root@docker_registry ~]#
    

      提示:重啟docker后,如果在docker info 中能夠看到我們配置的內容說明配置生效了;現在我們再來傳我們新打的標籤的鏡像,看看是否能夠傳到我們的私有倉庫呢?

    [root@docker_registry ~]# docker images
    REPOSITORY                       TAG                 IMAGE ID            CREATED             SIZE
    docker-registry.io:5000/centos   7                   b5b4d78bc90c        4 weeks ago         203MB
    centos                           7                   b5b4d78bc90c        4 weeks ago         203MB
    192.168.0.99:5000/flannel        v0.12.0-s390x       57eade024bfb        2 months ago        56.9MB
    quay.io/coreos/flannel           v0.12.0-s390x       57eade024bfb        2 months ago        56.9MB
    [root@docker_registry ~]# docker push 192.168.0.99:5000/flannel:v0.12.0-s390x
    The push refers to repository [192.168.0.99:5000/flannel]
    b67de7789e55: Pushed 
    4c4bfa1b47e6: Pushed 
    3b7ae8a9c323: Pushed 
    fbd88a276dca: Pushed 
    271ca11ef489: Pushed 
    1f106b41b4d6: Pushed 
    v0.12.0-s390x: digest: sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6 size: 1579
    [root@docker_registry ~]# docker push docker-registry.io:5000/centos:7
    The push refers to repository [docker-registry.io:5000/centos]
    edf3aa290fb3: Pushed 
    7: digest: sha256:c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941 size: 529
    [root@docker_registry ~]# 
    

      提示:可以看到我們上傳的兩個鏡像都完成了上傳沒有報錯,接下來我們去/var/lib/registry/這個目錄,看看是否有這兩個鏡像相關目錄?

    [root@docker_registry ~]# tree /var/lib/registry/
    /var/lib/registry/
    └── docker
        └── registry
            └── v2
                ├── blobs
                │   └── sha256
                │       ├── 13
                │       │   └── 13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
                │       │       └── data
                │       ├── 17
                │       │   └── 176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
                │       │       └── data
                │       ├── 1b
                │       │   └── 1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
                │       │       └── data
                │       ├── 26
                │       │   └── 266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
                │       │       └── data
                │       ├── 3c
                │       │   └── 3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
                │       │       └── data
                │       ├── 42
                │       │   └── 42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
                │       │       └── data
                │       ├── 52
                │       │   └── 524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
                │       │       └── data
                │       ├── 57
                │       │   └── 57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
                │       │       └── data
                │       ├── 85
                │       │   └── 85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6
                │       │       └── data
                │       ├── b5
                │       │   └── b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d
                │       │       └── data
                │       └── c2
                │           └── c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
                │               └── data
                └── repositories
                    ├── centos
                    │   ├── _layers
                    │   │   └── sha256
                    │   │       ├── 524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
                    │   │       │   └── link
                    │   │       └── b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d
                    │   │           └── link
                    │   ├── _manifests
                    │   │   ├── revisions
                    │   │   │   └── sha256
                    │   │   │       └── c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
                    │   │   │           └── link
                    │   │   └── tags
                    │   │       └── 7
                    │   │           ├── current
                    │   │           │   └── link
                    │   │           └── index
                    │   │               └── sha256
                    │   │                   └── c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
                    │   │                       └── link
                    │   └── _uploads
                    └── flannel
                        ├── _layers
                        │   └── sha256
                        │       ├── 13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
                        │       │   └── link
                        │       ├── 176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
                        │       │   └── link
                        │       ├── 1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
                        │       │   └── link
                        │       ├── 266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
                        │       │   └── link
                        │       ├── 42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
                        │       │   └── link
                        │       ├── 57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
                        │       │   └── link
                        │       └── 85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6
                        │           └── link
                        ├── _manifests
                        │   ├── revisions
                        │   │   └── sha256
                        │   │       └── 3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
                        │   │           └── link
                        │   └── tags
                        │       └── v0.12.0-s390x
                        │           ├── current
                        │           │   └── link
                        │           └── index
                        │               └── sha256
                        │                   └── 3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
                        │                       └── link
                        └── _uploads
    
    65 directories, 26 files
    [root@docker_registry ~]# 
    

      提示:可以看到對應目錄下有兩個子目錄就是以我們上傳的鏡像名稱命名的;

      示例:查看私有倉庫中存在的進行列表

    [root@docker_registry ~]# curl docker-registry.io:5000/v2/_catalog
    {"repositories":["centos","flannel"]}
    [root@docker_registry ~]# 

      示例:下載私有倉庫中的鏡像到本地

    [root@docker_node01 ~]# ip a l ens33
    2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
        link/ether 00:0c:29:22:36:7f brd ff:ff:ff:ff:ff:ff
        inet 192.168.0.22/24 brd 192.168.0.255 scope global ens33
           valid_lft forever preferred_lft forever
        inet6 fe80::20c:29ff:fe22:367f/64 scope link 
           valid_lft forever preferred_lft forever
    [root@docker_node01 ~]# docker images
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    linux1874/myimg     v0.1                e408b1c6e04f        2 weeks ago         1.22MB
    wordpress           latest              c3fa1c8546fb        5 weeks ago         540MB
    mysql               5.7                 f965319e89de        5 weeks ago         448MB
    alpine              v3                  f70734b6a266        6 weeks ago         5.61MB
    nginx               1.14-alpine         8a2fb25a19f5        14 months ago       16MB
    httpd               2.4.37-alpine       dfd436f9a5d8        17 months ago       91.8MB
    [root@docker_node01 ~]# docker pull 192.168.0.99:5000/flannel:v0.12.0-s390x
    v0.12.0-s390x: Pulling from flannel
    176bad61a3a4: Pull complete 
    13b80a37370b: Pull complete 
    42d8e66fa893: Pull complete 
    266247e2e603: Pull complete 
    1b56fbc8a8e1: Pull complete 
    85ecb68de469: Pull complete 
    Digest: sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6
    Status: Downloaded newer image for 192.168.0.99:5000/flannel:v0.12.0-s390x
    192.168.0.99:5000/flannel:v0.12.0-s390x
    [root@docker_node01 ~]# docker images
    REPOSITORY                  TAG                 IMAGE ID            CREATED             SIZE
    linux1874/myimg             v0.1                e408b1c6e04f        2 weeks ago         1.22MB
    wordpress                   latest              c3fa1c8546fb        5 weeks ago         540MB
    mysql                       5.7                 f965319e89de        5 weeks ago         448MB
    alpine                      v3                  f70734b6a266        6 weeks ago         5.61MB
    192.168.0.99:5000/flannel   v0.12.0-s390x       57eade024bfb        2 months ago        56.9MB
    nginx                       1.14-alpine         8a2fb25a19f5        14 months ago       16MB
    httpd                       2.4.37-alpine       dfd436f9a5d8        17 months ago       91.8MB
    [root@docker_node01 ~]# 
    

      提示:下載私有倉庫中的鏡像,默認情況docker也是不支持直接訪問http協議的倉庫,需要我們手動去配置insecure-registries,然後重啟docker才可以;

      示例:刪除私有倉庫中的鏡像

      1、獲取對應鏡像的sha256的值 curl –header “Accept:application/vnd.docker.distribution.manifest.v2+json” -I -X GET http://<registry addr>/v2/<image name>/manifests/<image tag>

      2、刪除對應鏡像版本元數據 curl -I -X DELETE http://<registry addr>/v2/<image name>/manifests/<image digest>

      提示:如果響應405方法不被允許;我們需要修改私有倉庫的配置文件,將其配置為允許刪除;如下

    [root@docker_registry ~]# cat /etc/docker-distribution/registry/config.yml
    version: 0.1
    log:
      fields:
        service: registry
    storage:
        delete:
            enabled: true
        cache:
            layerinfo: inmemory
        filesystem:
            rootdirectory: /var/lib/registry
    http:
        addr: :5000
    [root@docker_registry ~]# systemctl restart docker-distribution           
    [root@docker_registry ~]# curl -IX DELETE http://docker-registry.io:5000/v2/centos/manifests/sha256:c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
    HTTP/1.1 202 Accepted
    Docker-Distribution-Api-Version: registry/2.0
    Date: Sat, 06 Jun 2020 19:55:52 GMT
    Content-Length: 0
    Content-Type: text/plain; charset=utf-8
    
    [root@docker_registry ~]#
    

      提示:degest值包含”sha256:”

      3、垃圾回收清理

    [root@docker_registry ~]# registry garbage-collect /etc/docker-distribution/registry/config.yml 
    centos
    flannel
    flannel: marking manifest sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6 
    flannel: marking blob sha256:57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
    flannel: marking blob sha256:176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
    flannel: marking blob sha256:13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
    flannel: marking blob sha256:42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
    flannel: marking blob sha256:266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
    flannel: marking blob sha256:1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
    flannel: marking blob sha256:85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6
    myweb
    myweb: marking manifest sha256:aaf04cf567a776e36eb3b0bafaec17ed8d9e0a743bdb897dca13f251250ae493 
    myweb: marking blob sha256:4f406abeaab7f848178867409142090d1a551b22b968be6a6dae733c8403738e
    myweb: marking blob sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
    myweb: marking blob sha256:c941076f9075280c41b502283f37ab8bafef3a66f4a7ba299838dce07641a48d
    test
    test: marking manifest sha256:5ecad23ab8a52e55f93968f708d325261032dd613287aec92e7cf8ddd6426635 
    test: marking blob sha256:370e3a843c3cb12700301e3f87f929939146cd8b676260bedcd83aaa7fcc2939
    test: marking blob sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
    test: marking manifest sha256:da8b53210bf1f4dc4873bbd5589abad616663cda45205ae3a4fffb0729d2730d 
    test: marking blob sha256:461f6ceabc885e2e90b5f9ee82aefc9a30a39510c40e7cd8fb7436a4d340fe1d
    test: marking blob sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
    test: marking blob sha256:035e026f1d6b0acba3413ba616dcbabf75d20e945778c52716e601255452b7b7
    
    17 blobs marked, 2 blobs eligible for deletion
    blob eligible for deletion: sha256:b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/b5/b5b4d78bc90ccd15806443fb881e35b5ddba924e2f475c1071a38a3094c3081d  go.version=go1.9.4 instance.id=b3029d7f-99e8-4941-8c87-989514b584ea
    blob eligible for deletion: sha256:c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/c2/c2f1d5a9c0a81350fa0ad7e1eee99e379d75fe53823d44b5469eb2eb6092c941  go.version=go1.9.4 instance.id=b3029d7f-99e8-4941-8c87-989514b584ea
    [root@docker_registry ~]# 
    

      測試:下載docker-registry.io:5000/centos:7 看看是否還能下載?

    [root@docker_node01 ~]# docker pull 192.168.0.99:5000/centos:7
    Error response from daemon: manifest for 192.168.0.99:5000/centos:7 not found: manifest unknown: manifest unknown
    [root@docker_node01 ~]# 
    

      提示:以上提示告訴我們沒有對應鏡像的元數據信息;說明我們私有倉庫沒有對應鏡像;以上方法適合精準刪除某個鏡像的某個版本,如果是刪除一個倉庫,直接刪除 /var/lib/registry/docker/registry/v2/repositories/下對應倉庫的目錄,然後在用registry命令做垃圾回收;如下

    [root@docker_registry ~]# ll /var/lib/registry/docker/registry/v2/repositories/
    total 0
    drwxr-xr-x 5 root root 55 Jun  6 14:16 centos
    drwxr-xr-x 5 root root 55 Jun  6 14:15 flannel
    drwxr-xr-x 5 root root 55 Jun  6 15:25 myweb
    drwxr-xr-x 5 root root 55 Jun  6 15:24 test
    [root@docker_registry ~]# rm -rf /var/lib/registry/docker/registry/v2/repositories/test
    [root@docker_registry ~]# rm -rf /var/lib/registry/docker/registry/v2/repositories/myweb
    [root@docker_registry ~]# ll /var/lib/registry/docker/registry/v2/repositories/
    total 0
    drwxr-xr-x 5 root root 55 Jun  6 14:16 centos
    drwxr-xr-x 5 root root 55 Jun  6 14:15 flannel
    [root@docker_registry ~]# registry garbage-collect /etc/docker-distribution/registry/config.yml 
    centos
    flannel
    flannel: marking manifest sha256:3ce5b8d40451787e1166bf6b207c7834c13f7a0712b46ddbfb591d8b5906bfa6 
    flannel: marking blob sha256:57eade024bfbd48c45ef2bad996c4d6a0fa41b692247294745265af738066813
    flannel: marking blob sha256:176bad61a3a435da03ec603d2bd8f7a69286d92f21f447b17f21f0bc4e085bde
    flannel: marking blob sha256:13b80a37370b57f558a2e06092c39224e5d1ebac50e48df0afdeb43cf2303e60
    flannel: marking blob sha256:42d8e66fa893de4beb5d136b787cf182b24b7f4972c4212b9493b661ad1d7e85
    flannel: marking blob sha256:266247e2e603e1c840f97cb4d97a08b9184344e9802966cb42c93d21c407839f
    flannel: marking blob sha256:1b56fbc8a8e10830867455c0794a70f5469c154cdc013554daf501aeda3f30fe
    flannel: marking blob sha256:85ecb68de4693bb4093d923f6d1062766e4fa7cbb3bf456a2bc19dd3e6c5e6c6
    
    8 blobs marked, 9 blobs eligible for deletion
    blob eligible for deletion: sha256:370e3a843c3cb12700301e3f87f929939146cd8b676260bedcd83aaa7fcc2939
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/37/370e3a843c3cb12700301e3f87f929939146cd8b676260bedcd83aaa7fcc2939  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/52/524b0c1e57f8ee5fee01a1decba2f301c324a6513ca3551021264e3aa7341ebc  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:5ecad23ab8a52e55f93968f708d325261032dd613287aec92e7cf8ddd6426635
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/5e/5ecad23ab8a52e55f93968f708d325261032dd613287aec92e7cf8ddd6426635  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:aaf04cf567a776e36eb3b0bafaec17ed8d9e0a743bdb897dca13f251250ae493
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/aa/aaf04cf567a776e36eb3b0bafaec17ed8d9e0a743bdb897dca13f251250ae493  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:035e026f1d6b0acba3413ba616dcbabf75d20e945778c52716e601255452b7b7
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/03/035e026f1d6b0acba3413ba616dcbabf75d20e945778c52716e601255452b7b7  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:461f6ceabc885e2e90b5f9ee82aefc9a30a39510c40e7cd8fb7436a4d340fe1d
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/46/461f6ceabc885e2e90b5f9ee82aefc9a30a39510c40e7cd8fb7436a4d340fe1d  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:4f406abeaab7f848178867409142090d1a551b22b968be6a6dae733c8403738e
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/4f/4f406abeaab7f848178867409142090d1a551b22b968be6a6dae733c8403738e  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:c941076f9075280c41b502283f37ab8bafef3a66f4a7ba299838dce07641a48d
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/c9/c941076f9075280c41b502283f37ab8bafef3a66f4a7ba299838dce07641a48d  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    blob eligible for deletion: sha256:da8b53210bf1f4dc4873bbd5589abad616663cda45205ae3a4fffb0729d2730d
    INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/da/da8b53210bf1f4dc4873bbd5589abad616663cda45205ae3a4fffb0729d2730d  go.version=go1.9.4 instance.id=1a15f1e7-194f-4a82-b79d-9f437d975f6e
    [root@docker_registry ~]# 
    

      提示:這種方式比較粗暴簡單,通常是一個倉庫里只有一個版本鏡像可以使用這種方式刪除,如果一個倉庫有多個版本,那麼還是建議使用第一種方式;

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

    【其他文章推薦】

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

    ※回頭車貨運收費標準

    ※推薦評價好的iphone維修中心

    ※超省錢租車方案

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

    ※推薦台中搬家公司優質服務,可到府估價

  • 用雲開發Cloudbase,實現小程序多圖片內容安全監測

    用雲開發Cloudbase,實現小程序多圖片內容安全監測

    前言

    相比於文本的安全檢測,圖片的安全檢測要稍微略複雜一些,當您讀完本篇,將get到

    • 圖片安全檢測的應用場景
    • 解決圖片的安全校驗的方式
    • 使用雲調用方式對圖片進行檢測
    • 如何對上傳圖片大小進行限制
    • 如何解決多圖上傳覆蓋問題

    示例效果

    當用戶上傳敏感違規圖片時,禁止用戶上傳發布,並且做出相對應的用戶友好提示

    應用場景

    通常,在校驗一張圖片是否含有違法違規內容相比於文本安全的校驗,同樣重要,有如下應用

    • 圖片智能鑒黃:涉及拍照的工具類應用(如美拍,識圖類應用)用戶拍照上傳檢測;電商類商品上架圖片檢測;媒體類用戶文章里的圖片檢測等
    • 敏感人臉識別:用戶頭像;媒體類用戶文章里的圖片檢測;社交類用戶上傳的圖片檢測等,凡是有用戶自發生產內容的都應當提前做檢測

    解決圖片的安全手段

    在小程序開發中,提供了兩種方式

    • HTTPS調用
    • 雲調用

    HTTPS 調用的請求接口地止

    https://api.weixin.qq.com/wxa/img_sec_check?access_token=ACCESS_TOKEN
    

    檢測圖片審核,根據官方文檔得知,需要兩個必傳的參數:分別是:access_token(接口調用憑證),media(要檢測的圖片文件)

    對於HTTPS調用方式,願意折騰的小夥伴可以參考文本內容安全檢測(上篇)的處理方式,處理大同小異,本篇主要以雲開發的雲調用為主

    功能實現:小程序端邏輯

    對於wxml與wxss,大家可以自行任意修改,本文重點在於圖片安全的校驗

    <view class="image-list">
    <!-- 显示圖片 -->
       <block wx:for="{{images}}" wx:key="*this"><view class="image-wrap">
           <image class="image" src="{{item}}" mode="aspectFill" bind:tap="onPreviewImage" data-imgsrc="{{item}}"></image><i class="iconfont icon-shanchu" bind:tap="onDelImage" data-index="{{index}}"></i></view>
       </block>
       <!-- 選擇圖片 -->
       <view class="image-wrap selectphoto" hidden="{{!selectPhoto}}" bind:tap="onChooseImage"><i class="iconfont icon-add"></i></view>
       </view>
       <view class="footer"><button class="send-btn"  bind:tap="send">發布</button>
       </view>
    

    對應的wxss代碼

    .footer {
      display: flex;
      align-items: center;
      width: 100%;
      box-sizing: border-box;
      background: #34bfa3;
    }
    
    .send-btn {
      width: 100%;
      color: #fff;
      font-size: 32rpx;
      background: #34bfa3;
    }
    
    button {
      border-radius: 0rpx;
    }
    
    button::after {
      border-radius: 0rpx !important;
    }
    
    /* 圖片樣式 */
    .image-list {
      display: flex;
      flex-wrap: wrap;
      margin-top: 20rpx;
    }
    
    .image-wrap {
      width: 220rpx;
      height: 220rpx;
      margin-right: 10rpx;
      margin-bottom: 10rpx;
      position: relative;
      overflow: hidden;
      text-align: center;
    }
    
    .image {
      width: 100%;
      height: 100%;
    }
    
    .icon-shanchu {
      position: absolute;
      top: 0;
      right: 0;
      width: 40rpx;
      height: 40rpx;
      background-color: #000;
      opacity: 0.4;
      color: #fff;
      text-align: center;
      line-height: 40rpx;
      font-size: 38rpx;
      font-weight: bolder;
    }
    
    .selectphoto {
      border: 2rpx dashed #cbd1d7;
      position: relative;
    }
    
    .icon-add {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: #cbd1d7;
      font-size: 60rpx;
    }
    

    最終呈現的UI,由於只是用於圖片檢測演示,UI方面可忽略,如下所示

    對應的JS代碼

    /*
    * 涉及到的API:wx.chooseImage  從本地相冊選擇圖片或使用相機拍照
    *(https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html)
    
    
    *
    *
    */// 最大上傳圖片數量
    const MAX_IMG_NUM = 9;
    
    const db = wx.cloud.database(); // 初始化雲數據庫
    Page({
    
      /**
       * 頁面的初始數據
       */
      data: {
        images: [],  // 把上傳的圖片存放在一個數組對象裏面
        selectPhoto: true, // 添加+icon元素是否显示
      },
    
      /**
       * 生命周期函數--監聽頁面加載
       */
      onLoad: function (options) {
    
      },
    
      // 選擇圖片
      onChooseImage() {
        // 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
        let max = MAX_IMG_NUM - this.data.images.length; 
        wx.chooseImage({
          count: max,               // count表示最多可以選擇的圖片張數
          sizeType: ['original', 'compressed'], //  所選的圖片的尺寸
          sourceType: ['album', 'camera'],  // 選擇圖片的來源
          success: (res) => {                     // 接口調用成功的回調函數console.log(res)
            this.setData({                       // tempFilePath可以作為img標籤的src屬性显示圖片,下面是將后添加的圖片與之前的圖片給追加起來
              images: this.data.images.concat(res.tempFilePaths)
            })
            // 還能再選幾張圖片
            max = MAX_IMG_NUM - this.data.images.length
            this.setData({
              selectPhoto: max <= 0 ? false : true  // 當超過9張時,加號隱藏
            })
          },
        })
      },
    
      // 點擊右上方刪除圖標,刪除圖片操作
      onDelImage(event) {
        const index = event.target.dataset.index;
        // 點擊刪除當前圖片,用splice方法,刪除一張,從數組中移除一個                                                       
        this.data.images.splice(index, 1)
        this.setData({
          images: this.data.images
        })
        // 當添加的圖片達到設置最大的數量時,添加按鈕隱藏,不讓新添加圖片
        if (this.data.images.length == MAX_IMG_NUM - 1) {
          this.setData({
            selectPhoto: true,
          })
        }
      },
    })
    

    最終實現的前端UI效果如下所是:

    您現在看到的效果,沒有任何雲函數代碼,只是前端的純靜態展示,對於一些涉嫌敏感圖片,是有必要進行做過濾處理的

    功能實現:雲函數側邏輯

    在cloudfunctions目錄文件夾下創建雲函數imgSecCheck

    並在該目錄下創建config.json,配置參數如下所示

    {
      "permissions": {
        "openapi": [
          "security.imgSecCheck"
        ]
      }
    }
    

    配置完后,在主入口index.js中,如下所示,通過security.imgSecCheck接口,並傳入media對象

    // 雲函數入口文件
    const cloud = require('wx-server-sdk');
    cloud.init({
      env: cloud.DYNAMIC_CURRENT_ENV
    })
    
    // 雲函數入口函數
    exports.main = async (event, context) => {
      const wxContext = cloud.getWXContext()
      try {
        const result = await cloud.openapi.security.imgSecCheck({
          media: {
            contentType: 'image/png',
            value: Buffer.from(event.img)   // 這裏必須要將小程序端傳過來的進行Buffer轉化,否則就會報錯,接口異常
          }
          
        })
    
        if (result && result.errCode.toString() === '87014') {
          return { code: 500, msg: '內容含有違法違規內容', data: result }
        } else {
          return { code: 200, msg: '內容ok', data: result }
        }
      } catch (err) {
        // 錯誤處理
        if (err.errCode.toString() === '87014') {
          return { code: 500, msg: '內容含有違法違規內容', data: err }
        }
        return { code: 502, msg: '調用imgSecCheck接口異常', data: err }
      }
    }
    

    您會發現在雲函數端,就這麼幾行代碼,就完成了圖片安全校驗

    而在小程序端,代碼如下所示

    // miniprogram/pages/imgSecCheck/imgSecCheck.js
    // 最大上傳圖片數量
    const MAX_IMG_NUM = 9;
    
    const db = wx.cloud.database()
    Page({
    
      /**
       * 頁面的初始數據
       */
      data: {
        images: [],
        selectPhoto: true, // 添加圖片元素是否显示
      },
    
      /**
       * 生命周期函數--監聽頁面加載
       */
      onLoad: function (options) {
    
      },
      // 選擇圖片
      onChooseImage() {
        // const that = this;  // 如果下面用了箭頭函數,那麼這行代碼是不需要的,直接用this就可以了的// 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
        let max = MAX_IMG_NUM - this.data.images.length; 
        wx.chooseImage({
          count: max,
          sizeType: ['original', 'compressed'],
          sourceType: ['album', 'camera'],
          success: (res) => {  // 這裏若不是箭頭函數,那麼下面的this.setData的this要換成that上面的臨時變量,作用域的問題,不清楚的,可以看下this指向相關的知識
           console.log(res)
           // tempFilePath可以作為img標籤的src屬性显示圖片
            const  tempFiles = res.tempFiles;
            this.setData({
              images: this.data.images.concat(res.tempFilePaths)
            })
            // 在選擇圖片時,對本地臨時存儲的圖片,這個時候,進行圖片的校驗,當然你放在最後點擊發布時,進行校驗也是可以的,只不過是一個前置校驗和後置校驗的問題,我個人傾向於在選擇圖片時就進行校驗的,選擇一些照片時,就應該在選擇時階段做安全判斷的, 小程序端請求雲函數方式// 圖片轉化buffer后,調用雲函數
            console.log(tempFiles);
            tempFiles.forEach(items => {
              console.log(items);
              // 圖片轉化buffer后,調用雲函數
              wx.getFileSystemManager().readFile({
                filePath: items.path,
                success: res => {
                      console.log(res);
                       wx.cloud.callFunction({  // 小程序端請求imgSecCheck雲函數,並傳遞img參數進行檢驗
                        name: 'imgSecCheck',
                        data: {
                          img: res.data
                        }
                })
                .then(res => {
                   console.log(res);
                   let { errCode } = res.result.data;
                   switch(errCode) {
                     case 87014:
                       this.setData({
                          resultText: '內容含有違法違規內容'
                       })
                       break;
                     case 0:
                       this.setData({
                         resultText: '內容OK'
                       })
                       break;
                     default:
                       break;
                   }
     
                })
                .catch(err => {
                   console.error(err);
                })
                },
                fail: err => {
                  console.error(err);
                }
              })
            })
            
                
            // 還能再選幾張圖片
            max = MAX_IMG_NUM - this.data.images.length
            this.setData({
              selectPhoto: max <= 0 ? false : true  // 當超過9張時,加號隱藏
            })
          },
        })
      },
    
      // 刪除圖片
      onDelImage(event) {
        const index =  event.target.dataset.index;
        // 點擊刪除當前圖片,用splice方法,刪除一張,從數組中移除一個
        this.data.images.splice(index, 1);
        this.setData({
          images: this.data.images
        })
        // 當添加的圖片達到設置最大的數量時,添加按鈕隱藏,不讓新添加圖片
        if (this.data.images.length == MAX_IMG_NUM - 1) {
          this.setData({
            selectPhoto: true,
          })
        }
      },
    })
    

    示例效果如下所示:

    至此,關於圖片安全檢測就已經完成了,您只需要根據檢測的結果,做一些友好的用戶提示,或者做一些自己的業務邏輯判斷即可

    常見問題

    如何對上傳的圖片大小進行限制

    有時候,您需要對用戶上傳圖片的大小進行限制,限制用戶任意上傳超大圖片,那怎麼處理呢,在微信小程序裏面,主要藉助的是wx.chooseImage這個接口成功返回后臨時路徑的res.tempFiles中的size大小判斷即可進行處理

    具體實例代碼如下所示

    // 選擇圖片
      onChooseImage() {
        // 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
        let max = MAX_IMG_NUM - this.data.images.length; 
        wx.chooseImage({
          count: max,
          sizeType: ['original', 'compressed'],
          sourceType: ['album', 'camera'],
          success: (res) => {
            console.log(res)
            const  tempFiles = res.tempFiles;
            this.setData({
              images: this.data.images.concat(res.tempFilePaths)  // tempFilePath可以作為img標籤的src屬性显示圖片
            })
            // 在選擇圖片時,對本地臨時存儲的圖片,這個時候,進行圖片的校驗,當然你放在最後點擊發布時,進行校驗也是可以的,只不過是一個前置校驗和後置校驗的問題,我個人傾向於在選擇圖片時就進行校驗的,選擇一些照片時,就應該在選擇時階段做安全判斷的, 小程序端請求雲函數方式// 圖片轉化buffer后,調用雲函數
            console.log(tempFiles);
            tempFiles.forEach(items => {
              if (items && items.size > 1 * (1024 * 1024)) {  // 限製圖片的大小
                wx.showToast({
                  icon: 'none',
                  title: '上傳的圖片超過1M,禁止用戶上傳',
                  duration: 4000
                })
                // 超過1M的圖片,禁止用戶上傳
              }
              console.log(items);
              // 圖片轉化buffer后,調用雲函數
              wx.getFileSystemManager().readFile({
                filePath: items.path,
                success: res => {
                      console.log(res);
                       wx.cloud.callFunction({   // 請求調用雲函數imgSecCheck
                        name: 'imgSecCheck',
                        data: {
                          img: res.data
                        }
                })
                .then(res => {
                   console.log(res);
                   let { errCode } = res.result.data;
                   switch(errCode) {
                     case 87014:
                       this.setData({
                          resultText: '內容含有違法違規內容'
                       })
                       break;
                     case 0:
                       this.setData({
                         resultText: '內容OK'
                       })
                       break;
                     default:
                       break;
                   }
                })
                .catch(err => {
                   console.error(err);
                })
                },
                fail: err => {
                  console.error(err);
                }
              })
            })
           
            // 還能再選幾張圖片
            max = MAX_IMG_NUM - this.data.images.length
            this.setData({
              selectPhoto: max <= 0 ? false : true  // 當超過9張時,加號隱藏
            })
          },
        })
      },
    

    注意: 使用微信官方的圖片內容安全接口進行校驗,限製圖片大小限制:1M,否則的話就會報錯

    也就是說,對於超過1M大小的違規圖片,微信官方提供的這個圖片安全接口是無法進行校驗的

    這個根據自己的業務而定,在小程序端對用戶上傳圖片的大小進行限制如果您覺得微信官方提供的圖片安全接口滿足不了自己的業務需求,那麼可以選擇一些其他的圖片內容安全校驗的接口的

    這個圖片安全校驗是非常有必要的,用戶一旦上傳非法圖片,一旦通過網絡進行傳播,產生了社會影響,平台是有責任的,這種前車之鑒是有的

    如何解決多圖上傳覆蓋的問題

    對於上傳圖片來說,這個wx.cloud.uploadFileAPI接口只能上傳一張圖片,但是很多時候,是需要上傳多張圖片到雲存儲當中的,當點擊發布的時候,我們是希望將多張圖片都上傳到雲存儲當中去的

    這個API雖然只能每次上傳一張,但您可以循環遍歷多張圖片,然後一張一張的上傳的

    在cloudPath上傳文件的參數當中,它的值:需要注意:文件的名稱

    那如何保證上傳的圖片不被覆蓋,文件不重名的情況下就不會被覆蓋

    而在選擇圖片的時候,不應該上傳,因為用戶可能有刪除等操作,如果直接上傳的話會造成資源的浪費

    而應該在點發布按鈕的時候,才執行上傳操作,文件不重名覆蓋的示例代碼如下所示

          let promiseArr = []
          let fileIds = []      // 將圖片的fileId存放到一個數組中
          let imgLength = this.data.images.length;
          // 圖片上傳
          for (let i = 0; i < imgLength; i++) {
            let p = new Promise((resolve, reject) => {
            let item = this.data.images[i]
              // 文件擴展名
              let suffix = /\.\w+$/.exec(item)[0]; // 取文件后拓展名
              wx.cloud.uploadFile({      // 利用官方提供的上傳接口
                cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix,  // 雲存儲路徑,您也可以使用es6中的模板字符串進行拼接的
                filePath: item,   // 要上傳文件資源的路徑
                success: (res) => {
                  console.log(res);
                  console.log(res.fileID)
                  fileIds = fileIds.concat(res.fileID)       // 將新上傳的與之前上傳的給拼接起來
                  resolve()
                },
                fail: (err) => {
                  console.error(err)
                  reject()
                }
              })
            })
            promiseArr.push(p)
          }
          // 存入到雲數據庫,其中這個Promise.all(),等待裏面所有的任務都執行之後,在去執行後面的任務,也就是等待上傳所有的圖片上傳完后,才能把相對應的數據存到數據庫當中,具體與promise相關問題,可自行查漏
          Promise.all(promiseArr).then((res) => {
              db.collection('blog').add({ // 查找blog集合,將img,時間等數據添加到這個集合當中
                data: {
                  img: fileIds,
                  createTime: db.serverDate(), // 服務端的時間
                }
              }).then((res) => {
                console.log(res);
                this._hideToastTip();
                this._successTip();
              })
            })
            .catch((err) => {
              // 發布失敗console.error(err);
            })
    

    上面通過利用當前時間+隨機數的方式進行了一個區分,規避了上傳文件同名的問題

    因為這個上傳接口,一次性只能上傳一張圖片,所以需要循環遍歷圖片,然後一張張的上傳

    一個是上傳到雲存儲中,另一個是添加到雲數據庫集合當中,要分別注意下這兩個操作,雲數據庫中的圖片是從雲存儲中拿到的,然後再添加到雲數據庫當中去的

    示例效果如下所示:

    將上傳的圖片存儲到雲數據庫中

    注意:添加數據到雲數據庫中,需要手動創建集合,不然是無法上傳不到雲數據庫當中的,會報錯

    至此,關於敏感圖片的檢測,以及多圖片的上傳到這裏就已經完成了

    如下是完整的小程序端邏輯示例代碼

    // miniprogram/pages/imgSecCheck/imgSecCheck.js
    // 最大上傳圖片數量
    const MAX_IMG_NUM = 9;
    const db = wx.cloud.database()
    Page({
    
      /**
       * 頁面的初始數據
       */
      data: {
        images: [],
        selectPhoto: true, // 添加圖片元素是否显示
      },
    
      /**
       * 生命周期函數--監聽頁面加載
       */
      onLoad: function (options) {
    
      },
    
      // 選擇圖片
      onChooseImage() {
        // 還能再選幾張圖片,初始值設置最大的數量-當前的圖片的長度
        let max = MAX_IMG_NUM - this.data.images.length;
        wx.chooseImage({
          count: max,
          sizeType: ['original', 'compressed'],
          sourceType: ['album', 'camera'],
          success: (res) => {
            console.log(res)
            const tempFiles = res.tempFiles;
            this.setData({
              images: this.data.images.concat(res.tempFilePaths) // tempFilePath可以作為img標籤的src屬性显示圖片
            })
            // 在選擇圖片時,對本地臨時存儲的圖片,這個時候,進行圖片的校驗,當然你放在最後點擊發布時,進行校驗也是可以的,只不過是一個前置校驗和後置校驗的問題,我個人傾向於在選擇圖片時就進行校驗的,選擇一些照片時,就應該在選擇時階段做安全判斷的, 小程序端請求雲函數方式
            // 圖片轉化buffer后,調用雲函數
            console.log(tempFiles);
            tempFiles.forEach(items => {
              if (items && items.size > 1 * (1024 * 1024)) {
                wx.showToast({
                  icon: 'none',
                  title: '上傳的圖片超過1M,禁止用戶上傳',
                  duration: 4000
                })
                // 超過1M的圖片,禁止上傳
              }
              console.log(items);
              // 圖片轉化buffer后,調用雲函數
              wx.getFileSystemManager().readFile({
                filePath: items.path,
                success: res => {
                  console.log(res);
                  this._checkImgSafe(res.data); // 檢測圖片安全校驗
                },
                fail: err => {
                  console.error(err);
                }
              })
            })
    
    
            // 還能再選幾張圖片
            max = MAX_IMG_NUM - this.data.images.length
            this.setData({
              selectPhoto: max <= 0 ? false : true // 當超過9張時,加號隱藏
            })
          },
        })
      },
    
      // 刪除圖片
      onDelImage(event) {
        const index = event.target.dataset.index;
        // 點擊刪除當前圖片,用splice方法,刪除一張,從數組中移除一個
        this.data.images.splice(index, 1);
        this.setData({
          images: this.data.images
        })
        // 當添加的圖片達到設置最大的數量時,添加按鈕隱藏,不讓新添加圖片
        if (this.data.images.length == MAX_IMG_NUM - 1) {
          this.setData({
            selectPhoto: true,
          })
        }
      },
    
      // 點擊發布按鈕,將圖片上傳到雲數據庫當中
      send() {
        const images = this.data.images.length;
        if (images) {
          this._showToastTip();
          let promiseArr = []
          let fileIds = []
          let imgLength = this.data.images.length;
          // 圖片上傳
          for (let i = 0; i < imgLength; i++) {
            let p = new Promise((resolve, reject) => {
              let item = this.data.images[i]
              // 文件擴展名
              let suffix = /\.\w+$/.exec(item)[0]; // 取文件后拓展名
              wx.cloud.uploadFile({   // 上傳圖片至雲存儲,循環遍歷,一張張的上傳
                cloudPath: 'blog/' + Date.now() + '-' + Math.random() * 1000000 + suffix,
                filePath: item,
                success: (res) => {
                  console.log(res);
                  console.log(res.fileID)
                  fileIds = fileIds.concat(res.fileID)
                  resolve()
    
                },
                fail: (err) => {
                  console.error(err)
                  reject()
                }
              })
            })
            promiseArr.push(p)
          }
          // 存入到雲數據庫
          Promise.all(promiseArr).then((res) => {
              db.collection('blog').add({ // 查找blog集合,將數據添加到這個集合當中
                data: {
                  img: fileIds,
                  createTime: db.serverDate(), // 服務端的時間
                }
              }).then((res) => {
                console.log(res);
                this._hideToastTip();
                this._successTip();
              })
            })
            .catch((err) => {
              // 發布失敗
              console.error(err);
            })
        } else {
          wx.showToast({
            icon: 'none',
            title: '沒有選擇任何圖片,發布不了',
          })
        }
    
      },
    
      // 校驗圖片的安全
      _checkImgSafe(data) {
        wx.cloud.callFunction({
            name: 'imgSecCheck',
            data: {
              img: data
            }
          })
          .then(res => {
            console.log(res);
            let {
              errCode
            } = res.result.data;
            switch (errCode) {
              case 87014:
                this.setData({
                  resultText: '內容含有違法違規內容'
                })
                break;
              case 0:
                this.setData({
                  resultText: '內容OK'
                })
                break;
              default:
                break;
            }
          })
          .catch(err => {
            console.error(err);
          })
      },
    
      _showToastTip() {
        wx.showToast({
          icon: 'none',
          title: '發布中...',
        })
      },
    
      _hideToastTip() {
        wx.hideLoading();
      },
    
      _successTip() {
        wx.showToast({
          icon: 'none',
          title: '發布成功',
        })
      },
    })
    

    完整的示例wxml,如下所示

    <view class="image-list">
    <!-- 显示圖片 -->
    <block wx:for="{{images}}" wx:key="*this">
         <view class="image-wrap"><image class="image" src="{{item}}" mode="aspectFill" bind:tap="onPreviewImage" data-imgsrc="{{item}}"></image><i class="iconfont icon-shanchu" bind:tap="onDelImage" data-index="{{index}}"></i>
         </view>
    </block>
    <!-- 選擇圖片 -->
    <view class="image-wrap selectphoto" hidden="{{!selectPhoto}}" bind:tap="onChooseImage"><i class="iconfont icon-add"></i></view>
    </view>
    <view class="footer">
       <button class="send-btn"  bind:tap="send">發布</button>
    </view>
    <view>
        檢測結果显示: {{ resultText }}
    </view>
    

    您可以根據自己的業務邏輯需要,一旦檢測到圖片違規時,禁用按鈕狀態,或者給一些用戶提示,都是可以的,在發布之前或者點擊發布時,進行圖片內容安全的校驗都可以,一旦發現圖片有違規時,就不讓繼續後面的操作的

    結語

    本文主要通過藉助官方提供的圖片security.imgSecCheck

    接口,實現了對圖片安全的校驗,實現起來,是相當的方便的,對於基礎性的校驗,利用官方提供的這個接口,已經夠用了的,但是如果想要更加嚴格的檢測,可以引入一些第三方的內容安全強強校驗,確保內容的安全

    實現了如何對上傳的圖片大小進行限制,以及解決同名圖片上傳覆蓋的問題

    如果大家對文本內容安全校驗以及圖片安全校驗仍然有什麼問題,可以在下方留言,一起探討。

    雲開發公眾號:騰訊云云開發

    雲開發技術文檔:https://cloudbase.net

    雲開發技術交流加Q群:601134960

    更多精彩
    掃描二維碼了解更多

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

    【其他文章推薦】

    ※回頭車貨運收費標準

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

    ※推薦評價好的iphone維修中心

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

    台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

  • c#中的值類型和引用類型

    c#中的值類型和引用類型

    值類型和引用類型,是c#比較基礎,也必須掌握的知識點,但是也不是那麼輕易就能掌握,今天跟着老胡一起來看看吧。
     

    典型類型

    首先我們看看這兩種不同的類型有哪些比較典型的代表。
     

    典型值類型

    int, long, float, double等原始類型中表示数字的類型都是值類型,表示時間的datatime也是值類型,除此之外我們還可以通過關鍵字struct自定義值類型。
     

    典型引用類型

    原始類型中,array, list, dictionary, queue, stack和string都是引用類型,除此之外我們通過關鍵字class自定義引用類型。
     

    基類

    c#中所有的類型都最終繼承自Object,這是沒有疑問的,但是這其中還有些微區別。
     

    值類型基類

    對於值類型來說,除了最終繼承自Object,還繼承自ValueType,繼承鏈如下

    但是請不要誤解,這裏僅僅指的是值類型天然是ValueType,但是不代表值類型能夠這麼聲明

    struct Struct1 : ValueType
    {
    
    }
    

    這樣是會引起編譯錯誤的,值類型不能繼承任何其他類型,值類型只能實現接口,不能繼承自其它類型。只有引用類型既可以實現接口也能繼承自其它類型。順便說一下,還有一點比較重要的是,ValueType重寫了Object基類的Equals方法和GetHashCode方法,所以當使用Equals比較兩個值類型的時候,系統會比較兩個值類型的各個屬性是否相等,再返回結果,這就是所謂的相等性。與此相對,引用類型在使用Equals的時候,會在後台調用object.ReferenceEquals,換言之,引用類型在比較相等性的時候會考慮同一性
     

    引用類型基類

    對於引用類型就沒有那麼麻煩,引用類型不會繼承自ValueType。引用類型可以繼承其他類型。
     

    在內存中的表現

    我們都知道,C#將內存分為了兩部分,一個是Stack,另外一個是Managed Heap。一般來說,用於函數調用進棧,函數返回出棧,用的是Stack,而當創造一個新的實例時,會根據創建的實例屬於值類型還是引用類型決定使用Stack還是Managed Heap。
     

    值類型在內存中

    當創建一個值類型對象時,c#會在Stack上面創建一塊空間,這塊空間就存放這個值類型對象。
    int是一個典型的值類型,如下語句

    int age = 10;
    

    會存在於內存中的Stack上面。

    如果把值類型的實例賦值給另外一個值類型,那麼效果就是複製一個新的值類型實例。

    int myAge = age;
    

     

    引用類型在內存中

    與值類型在內存中的表現不一樣,創建一個引用類型的實例,不但會在Stack上面新建一個引用,還會在Heap上面劃分出內存以容納該引用類型實例。用戶在使用的時候通過Stack上面的變量間接引用該實例。

    class Author
    {
    	public string Name{get;set;}
    	public int Age{get;set;}
    }
    
    Author author = new Author(){Name="deatharthas", Age= 32};
    

    注意看和值類型在內存中的區別,引用類型通過Stack上的變量訪問位於Heap上面的實例。
    在賦值的時候,拷貝的僅僅是Stack上面的變量,新拷貝出來的對象和舊的對象指向的是同一塊內存。

    Author myAuthor = author;
    

    這個時候,author和myAuthor指向同一塊內存,稱為同一性,通過調用

    object.ReferenceEquals(myAuthor, author);
    

    可以得到驗證。
     
    但可能有細心的朋友會有疑問了,不是說int是值類型,值類型是存在於Stack上面的嗎?為什麼在author類裏面,它會在Heap裏面呢?贊一個細心!值類型一般存在於Stack上面,但如果某個值類型包含於引用類型,那麼它也會隨着那個引用類型存放在Heap上面。
     

    當參數時的行為區別

    c#中的參數傳遞默認都是傳值(by value),但是根據所傳遞對象是值類型還是引用類型,它們的行為還是有所區別,現在我們來看看。

    值類型當參數

    值類型當參數的時候,傳遞到函數內部的是一份值類型的拷貝,所以在函數內部修改這個拷貝不會影響原對象。除非我們在傳遞參數的時候使用了ref或者out。
     

    引用類型當參數

    如果參數是引用類型,傳遞到函數內部的依然是一份拷貝,但是這個拷貝是其在Stack上面的變量的拷貝,就像上面的賦值那個例子。所以這個時候這份拷貝其實和原對象指向同一塊內存(指向同一性),修改這個對象可以反映到原對象上面。
     

    謹慎返回引用類型

    編程是一項需要謹慎的工作,有時候我們經常會犯一些錯誤,而這些錯誤又是那麼的不明顯以至於不摔坑幾次,我們根本察覺不了,考慮下面一個例子。

        class People
        {
            public string Name { get; set; }
            public int Age { get; set; }
            private People _Father = null;
            public People Father { get { return _Father; } }
            public People(People father)
            {
                _Father = father;
            }
            public void ShowFather()
            {
                Console.WriteLine("father's name is " + Father.Name + " and his age is " + Father.Age);
            }
        }
    
        class Program
        {        
            static void Main(string[] args)
            {
                People father = new People(null) { Name = "father", Age = 60 };
                People son = new People(father);
                son.ShowFather();
                Console.ReadLine();
            }
        }
    

    看起來沒什麼問題,對吧?Father沒有提供setter,似乎是安全的。但是我們試試下面的代碼。

    	static void Main(string[] args)
            {
                People father = new People(null) { Name = "father", Age = 60 };
                People son = new People(father);
                var f = son.Father;
                f.Name="Changed";
                son.ShowFather();
                Console.ReadLine();
            }
    

    看,發現了什麼,外部改變了本來應該被封裝所保護的Father屬性,封裝被破壞了!
    稍微一想我們應該能明白這個道理,Father屬性返回的拷貝的變量和原Father變量指向同一塊實例。要想解決這個問題,我們要麼返回一個值類型,要麼返回一個全新的對象。修改Father屬性如下:

    public People Father { get { return new People(_Father._Father) { Name = _Father.Name, Age = _Father.Age }; } }
    

    再次測試,

    這次封裝就沒問題了。
     

    總結

    我們大概知道了值類型和引用類型的區別,包括它們的行為,在內存的居住方式,以及使用引用類型時可能會遇到的暗坑,希望大家通過閱讀這篇文章,能夠加深一些對它們的了解,少走一些彎路。
    今天也簡單的提到了比較時的同一性,和預防封裝被破壞所採用的返回一個新的實例拷貝的策略(這個時候適合使用DeepCopy),我們之後有機會再詳細聊。

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

    【其他文章推薦】

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

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

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

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

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

    ※別再煩惱如何寫文案,掌握八大原則!

  • 系統化學習多線程(一)

    系統化學習多線程(一)

    大綱

    ————————-學前必讀———————————-

    學習不能快速成功,但一定可以快速入門
    整體課程思路:
    1.實踐為主,理論化偏少
    2.課程筆記有完整的案例和代碼,(為了學習效率)再開始之前我會簡單粗暴的介紹知識點案例思路,
    有基礎的同學聽了之後可以直接結合筆記寫代碼,
    如果沒聽懂再向下看視頻,我會手把手編寫代碼和演示測試結果;
    3.重要提示,學編程和學游泳一樣,多實踐學習效率才高,理解才透徹;
    4.編碼功底差的建議每個案例代碼寫三遍,至於為什麼…<<賣油翁>>…老祖宗的智慧

    ————————————————————————-

     1.線程

    1.1.什麼是線程

    線程(英語:thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程并行執行不同的任務。在Unix System V及SunOS中也被稱為輕量進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱為線程。(來自百度百科)

    一個進程可以有很多線程,每條線程并行執行不同的任務。

    1.2.多線程hello word

    需求:模擬在計算上一邊聽歌一邊打遊戲

    三種實現方案如下:

    TestDemo

     1 package com.wfd360.thread;
     2 
     3 import com.wfd360.thread.demo01.GameRunnable;
     4 import com.wfd360.thread.demo01.MusicRunnable;
     5 import com.wfd360.thread.demo02.GameThread;
     6 import com.wfd360.thread.demo02.MusicThread;
     7 import org.junit.Test;
     8 
     9 /**
    10  * @author 姿勢帝-博客園
    11  * @address https://www.cnblogs.com/newAndHui/
    12  * @WeChat 851298348
    13  * @create 05/03 5:27
    14  * @description 需求分析:
    15  * 1.模擬一邊打遊戲一邊聽音樂,在控制台打印輸出模擬
    16  * 2.把兩個業務封裝成獨立的線程,實現接口Runnable或繼承Thread,通過看源碼你會發現Thread類實現了接口Runnable,使用本質上這兩種方法時一樣的。
    17  * 3.Thread類提供兩個方法,線程主題方法run,啟動線程方法start
    18  */
    19 public class TestDemo {
    20     /**
    21      * 方式1:實現接口Runnable
    22      */
    23     @Test
    24     public void testRunnable() throws InterruptedException {
    25         System.out.println("-------test start-------");
    26         // 實例對象
    27         MusicRunnable music = new MusicRunnable();
    28         GameRunnable game = new GameRunnable();
    29         // 創建線程
    30         Thread musicThread = new Thread(music);
    31         Thread gameThread = new Thread(game);
    32         // 啟動線程
    33         musicThread.start();
    34         gameThread.start();
    35         System.out.println("--------等待其他線程執行--------------");
    36         Thread.sleep(5 * 1000);
    37         System.out.println("-------test end-------");
    38     }
    39 
    40     /**
    41      * 方式2:繼承Thread
    42      */
    43     @Test
    44     public void testThread() throws InterruptedException {
    45         System.out.println("-------test start-------");
    46         // 創建線程
    47         MusicThread musicThread = new MusicThread();
    48         GameThread gameThread = new GameThread();
    49         // 啟動線程
    50         musicThread.start();
    51         gameThread.start();
    52         System.out.println("--------等待其他線程執行--------------");
    53         Thread.sleep(5 * 1000);
    54         System.out.println("-------test end-------");
    55     }
    56 
    57     /**
    58      * 方式3:簡寫,這種寫法一般我們在做模擬測試的使用,在正式代碼中建議不使用,可讀性較差
    59      */
    60     @Test
    61     public void testThreadSimple() throws InterruptedException {
    62         System.out.println("-------test start-------");
    63         // 創建線程
    64         Thread musicThread = new Thread(() -> {
    65             for (int i = 0; i < 100; i++) {
    66                 System.out.println("=======聽音樂中============" + i);
    67             }
    68         });
    69         Thread gameThread = new Thread(() -> {
    70             for (int i = 0; i < 100; i++) {
    71                 System.out.println("=======打遊戲中============" + i);
    72             }
    73         });
    74         // 啟動線程
    75         musicThread.start();
    76         gameThread.start();
    77         System.out.println("--------等待其他線程執行--------------");
    78         Thread.sleep(5 * 1000);
    79         System.out.println("-------test end-------");
    80     }
    81 }


     實現接口Runnable

     1 package com.wfd360.thread.demo01;
     2 
     3 /**
     4  * @author 姿勢帝-博客園
     5  * @address https://www.cnblogs.com/newAndHui/
     6  * @WeChat 851298348
     7  * @create 05/03 5:31
     8  * @description
     9  */
    10 public class GameRunnable implements Runnable {
    11     @Override
    12     public void run() {
    13         for (int i = 0; i < 100; i++) {
    14             System.out.println("=======打遊戲中============" + i);
    15         }
    16     }
    17 }

    GameRunnable

     1 package com.wfd360.thread.demo01;
     2 
     3 /**
     4  * @author 姿勢帝-博客園
     5  * @address https://www.cnblogs.com/newAndHui/
     6  * @WeChat 851298348
     7  * @create 05/03 5:29
     8  * @description
     9  */
    10 public class MusicRunnable implements Runnable {
    11     @Override
    12     public void run() {
    13         for (int i = 0; i < 100; i++) {
    14             System.out.println("=======聽音樂中============"+i);
    15         }
    16     }
    17 }

    MusicRunnable

     繼承Thread

     1 package com.wfd360.thread.demo02;
     2 
     3 /**
     4  * @author 姿勢帝-博客園
     5  * @address https://www.cnblogs.com/newAndHui/
     6  * @WeChat 851298348
     7  * @create 05/03 6:00
     8  * @description
     9  */
    10 public class GameThread extends Thread {
    11     @Override
    12     public void run() {
    13         for (int i = 0; i < 100; i++) {
    14             System.out.println("-------遊戲中----------"+i);
    15         }
    16     }
    17 }

    GameThread GameThread

     1 package com.wfd360.thread.demo02;
     2 
     3 /**
     4  * @author 姿勢帝-博客園
     5  * @address https://www.cnblogs.com/newAndHui/
     6  * @WeChat 851298348
     7  * @create 05/03 6:00
     8  * @description
     9  */
    10 public class MusicThread extends Thread {
    11     @Override
    12     public void run() {
    13         for (int i = 0; i < 100; i++) {
    14             System.out.println("-------音樂中----------" + i);
    15         }
    16     }
    17 }


     總結

    啟動線程兩種方式:

        1.通過繼承Thread類

        2.實現Runnable接口

     使用哪種方式更好?

    區別: 

    一個類如果繼承了其他類,就無法在繼承Thread類,在Java中,一個類只能繼承一個類,而一個類如果實現了一個接口,還可以實現其他接口,接口是可以多實現的,所以說

    Runable的擴展性更強,但是繼承的方式更簡單,個人建議一般情況下使用Thread;

    實現接口Runnable或繼承Thread,通過看源碼你會發現Thread類實現了接口Runnable,使用本質上這兩種方法是一樣的

    啟動線程流程:

        創建啟動線程的方式一:繼承Thread類

           1.將業務方法封裝成線程對象,自定義類t extends Thread類; 

           2.覆寫run方法: 覆寫第一步中的run方法;

           3.創建自定義對象t

           4.啟動線程 t.start();

       創建啟動線程方式二:實現Runnable接口

          1.將業務方法封裝成線程對象,自定義類t implements Runnable接口;

          2.實現第一步中的run方法

          3.創建自定義對象t

          4.啟動線程 new Thread(t).start();

    1.3.對主線程與創建線程執行順序的理解

    問題:
    直接寫一個簡單的HelloWorld 程序,有沒有線程?
    ==>有一個主線程,在垃圾回收的時候,有gc 線程。

     1 package com.wfd360.thread;
     2 
     3 import org.junit.Test;
     4 
     5 /**
     6  * @author 姿勢帝-博客園
     7  * @address https://www.cnblogs.com/newAndHui/
     8  * @WeChat 851298348
     9  * @create 05/04 11:09
    10  * @description <p>
    11  * 問題:
    12  * 直接寫一個簡單的HelloWorld 程序,有沒有線程?
    13  * ==>有一個主線程,在垃圾回收的時候,有gc 線程。
    14  * 結論:一旦線程啟動起來之後就是獨立的,和創建環境沒有關係;
    15  * 啟動線程不能直接調用run方法,必須調用start方法;
    16  * </p>
    17  */
    18 public class TestDemo02 {
    19     /**
    20      * 如果把創建線程放在循環語句的 下 面,會交替出現嗎
    21      * ==>否,因為主線程執行完成后才會啟動hello線程
    22      *
    23      * @throws Exception
    24      */
    25     @Test
    26     public void test1() throws Exception {
    27         System.out.println("---test start-------");
    28         // 執行主線程
    29         for (int i = 0; i < 100; i++) {
    30             System.out.println("-----test1--------" + i);
    31         }
    32         // 啟動hello線程
    33         new HelloThread().start();
    34         System.out.println("=======等待執行完成===========");
    35         Thread.sleep(5 * 1000);
    36         System.out.println("---test end-------");
    37     }
    38 
    39     /**
    40      * 如果把創建線程放在循環語句的 上 面,會交替出現嗎
    41      * ==>可能會,可能不會,可能出現for循環完之後,線程還沒有啟動完;
    42      *
    43      * @throws Exception
    44      */
    45     @Test
    46     public void test2() throws Exception {
    47         System.out.println("---test start-------");
    48         // 啟動hello線程
    49         new HelloThread().start();
    50         // 執行主線程
    51         for (int i = 0; i < 100; i++) {
    52             System.out.println("-----test1--------" + i);
    53         }
    54         System.out.println("=======等待執行完成===========");
    55         Thread.sleep(5 * 1000);
    56         System.out.println("---test end-------");
    57     }
    58 
    59     /**
    60      * 採用內部類的方式定義一個hello線程對象
    61      */
    62     class HelloThread extends Thread {
    63         @Override
    64         public void run() {
    65             for (int i = 0; i < 100; i++) {
    66                 System.out.println("-----HelloThread--------" + i);
    67             }
    68         }
    69     }
    70 }

    TestDemo02

    結論:一旦線程啟動起來之後就是獨立的,和創建環境沒有關係;
    啟動線程不能直接調用run方法,必須調用start方法;

     1.4.對sleep方法的理解

    package com.wfd360.thread;
    
    /**
     * @author 姿勢帝-博客園
     * @address https://www.cnblogs.com/newAndHui/
     * @WeChat 851298348
     * @create 05/04 11:34
     * @description <p>
     * Thread類的方法:
     * static void sleep(long millis) 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行);
     * </p>
     */
    public class TestSleep {
        /**
         * 做一個簡易倒計時,10秒鐘,控制台每一秒輸出一個数字,如10,9,8,7.....0
         */
        public static void main(String[] args) throws Exception {
            System.out.println("---test start-------");
            for (int i = 10; i >= 0; i--) {
                Thread.sleep(1 * 1000);
                System.out.println(i);
            }
            System.out.println("---test end-------");
        }
    }

    1.5.線程名稱的設置與獲取

    繼承方式

    簡單需求:使用多線程模擬多窗口售票

     1 package com.wfd360.thread.demo03Ticket;
     2 
     3 /**
     4  * @author 姿勢帝-博客園
     5  * @address https://www.cnblogs.com/newAndHui/
     6  * @WeChat 851298348
     7  * @create 05/04 11:55
     8  * @description <p>
     9  * 模擬多線程售票
    10  * </p>
    11  */
    12 public class TicketThread extends Thread {
    13     // 假定票總是100張
    14     private static Integer num = 100;
    15 
    16     @Override
    17     public void run() {
    18         // 只要有票就一直售票
    19         while (num > 0) {
    20             System.out.println("正在出售第" + num + "張票");
    21             --num;
    22         }
    23         System.out.println("===售票結束===");
    24     }
    25 }

    TicketThread

    test

    /**
         * 測試模擬三個窗口售票
         * @throws InterruptedException
         */
        @Test
        public void testTicketThread() throws InterruptedException {
            System.out.println("---test start-------");
            // 模擬多3個窗口售票
            TicketThread ticketThread1 = new TicketThread();
            TicketThread ticketThread2 = new TicketThread();
            TicketThread ticketThread3 = new TicketThread();
            // 啟動線程售票
            ticketThread1.start();
            ticketThread2.start();
            ticketThread3.start();
            System.out.println("======等待售票============");
            Thread.sleep(5 * 1000);
            System.out.println("---test end-------");
        }

    結果:

    1.在售票過程中不能區分售出的票是那個窗口售出的,解決通過線程名稱判斷

    2.有重複售出的票(後面的線程同步解決)

    解決第一個問題,設置獲取線程名稱,通過Thread對象裏面自帶的getName,setName方法

     具體代碼

    設置線程名稱

     獲取線程名稱

     上面講了繼承的方式獲取線程名稱,那麼實現接口Runnable的方式怎麼獲取設置勒

    繼承Thread的方式,可以通過getName的方式獲取當前線程的名稱?
    那使用Runnable的方式,能通過getName獲取嘛?

    getName方法是Thread類的,但是TicketThread現在並沒有繼承Thread類,而是實現了Runnable接口.

    問題:如果實現Runnable接口,怎麼獲取線程名稱?

     思考:TicketThread類裏面的代碼要執行,它肯定存在於某個線程中, 就比如寫個helloword打印語句,是不是也處於一個主線程中,那這裏怎麼獲取線程名稱?
    通過動態獲取,當程序正在執行的時候,獲取當前正在執行的線程名稱。怎麼獲取?
    在Thread類裏面有個靜態的方法currentThread() 方法,返回當前正在執行的線程引用;

    Thread.currentThread().getName

    那怎麼設置線程名稱?

    Thread類裏面有個name字段,相當於Thread類把它包裝了一下:

    通過源碼可以發現,構造方法裏面還有可以傳一個名字:

    具體實現代碼如下

     

     總結:

    繼承方式設置\獲取線程名稱通過 Thread對象裏面的 setName,getName方法;

    實現接口方式設置名稱通過 new Thread(‘線程實例對象’, “線程名稱”),獲取線程名稱通過:Thread.currentThread().getName

    1.6.Thread的join方法

    void join() 方法 :等待該線程終止
    void join(long millis) 方法 :等待該線程終止的時間最長為millis毫秒

    需求: 當主線程運行到20的時候(i =20)的時候,讓JoinThread線程加進來直到執行完成,在執行主線程.

     1 package com.wfd360.thread;
     2 
     3 import org.junit.Test;
     4 
     5 /**
     6  * @author 姿勢帝-博客園
     7  * @address https://www.cnblogs.com/newAndHui/
     8  * @WeChat 851298348
     9  * @create 05/04 6:31
    10  * @description
    11  */
    12 public class Test05Join {
    13     /**
    14      * 需求:
    15      * 當主線程for循環到i=20時,等JoinThread線程執行完成后,在執行for循環的線程
    16      * @throws InterruptedException
    17      */
    18     @Test
    19     public void testJoinThread() throws InterruptedException {
    20         System.out.println("---test start-------");
    21         // 開啟線程
    22         JoinThread thread = new JoinThread();
    23         thread.start();
    24         // 循環打印線程
    25         for (int i = 0; i < 100; i++) {
    26             System.out.println("======testJoinThread=========="+i);
    27             Thread.sleep(1);
    28             if (i==20){
    29                 // 等線程JoinThread執行完成
    30                 thread.join();
    31             }
    32         }
    33         System.out.println("=============等待線程執行完成===================");
    34         Thread.sleep(10*1000);
    35         System.out.println("---test end-------");
    36     }
    37     
    38     class JoinThread extends Thread {
    39         @Override
    40         public void run() {
    41             for (int i = 0; i < 100; i++) {
    42                 System.out.println("=====JoinThread=======" + i);
    43                 // 模擬處理很多業務耗時1毫秒
    44                 try {
    45                     Thread.sleep(1);
    46                 } catch (InterruptedException e) {
    47                     e.printStackTrace();
    48                 }
    49             }
    50         }
    51     }
    52 }

    Test05Join

    1.7.線程優先級

    直接上代碼

     1 package com.wfd360.thread;
     2 
     3 /**
     4  * @author 姿勢帝-博客園
     5  * @address https://www.cnblogs.com/newAndHui/
     6  * @WeChat 851298348
     7  * @create 05/05 8:17
     8  * @description <p>
     9  * 1.==>線程優先級的理解:
    10  * 線程的優先級和生活中類似,高優先級線程的執行優先於低優先級線程;
    11  * 並不是絕對的,可能優先級高的線程優先 比 優先級低的線程先執行,只能說,高優先級的線程優先執行的幾率更多;
    12  * (比如兩個線程,一個優先級高,一個優先級低,如果一共運行一個小時,優先級高的線程執行遠遠大於優先級低的但是並不是說優先級高的先執行完,
    13  * 在執行優先級低的)
    14  * 2.==>重新設置線程優先級
    15  * int getPriority() 返回線程的優先級。
    16  * void setPriority(int newPriority) 更改線程的優先級。Java線程的優先級從1到10級別,值越大優先級越高.
    17  * 3.==>線程的默認優先級受創建線程的環境影響,默認值5,自定義線程的默認優先級和創建它的環境的線程優先級一致
    18  * </p>
    19  */
    20 public class Test06Priority {
    21     /**
    22      * 測試獲取線程優先級,設置線程優先級,驗證線程優先級受創建環境影響
    23      * @param args
    24      */
    25     public static void main(String[] args) {
    26         Thread threadMain = Thread.currentThread();
    27         // 獲取默認優先級数字
    28         System.out.println("main線程默認優先級:" +threadMain.getPriority());// 5
    29         // 重新設置默認優先級数字
    30         threadMain.setPriority(8);
    31         // 再次重新獲取優先級数字
    32         System.out.println("main線程修改后的優先級:" +threadMain.getPriority());// 8
    33         // 創建一個線程查看優先級
    34         Thread thread = new Thread();
    35         System.out.println("thread線程的優先級:" +thread.getPriority());// 8 受創建環境影響
    36     }
    37 }

    1.8.後台線程,即守護線程

    直接看代碼

     1 package com.wfd360.thread;
     2 
     3 import com.wfd360.thread.demo04Daemon.DaemonThreaad;
     4 
     5 /**
     6  * @author 姿勢帝-博客園
     7  * @address https://www.cnblogs.com/newAndHui/
     8  * @WeChat 851298348
     9  * @create 05/05 9:12
    10  * @description <p>
    11  * 後台線程,即守護線程
    12  * 後台線程:指為其他線程提供服務的線程,也稱為守護線程。JVM的垃圾回收線程就是一個後台線程。
    13  * 需求:嘗試把線程標記為後台線程或者標記為(前台)線程;
    14  * Thread類提供的方法:
    15  * 方法1: void setDaemon(boolean on) 將該線程標記為守護線程或用戶線程,true為後台線程,false為用戶線程(前台線下)
    16  * 怎樣測試該線程是否是守護線程?
    17  * 方法2:isDaemon()  測試該線程是否為守護線程. true為後台線程,false為用戶線程(前台線下)
    18  * <p>
    19  * 結論1:活動的線程(已經在執行的線程t.start())不能設置後台線程,即主線程不能設置為後台線程。
    20  * 結論2: 自定義線程的默認狀態和環境有關,後台線程中創建的線程默認是後台線程,前台線程中創建的線程為前台線程.
    21  * 結論3: 前台線程執行完后,會直接關閉後台線程,即自定義的後台線程不一定能執行完成
    22  * </p>
    23  */
    24 public class Test07Daemon {
    25     /**
    26      * 測試1
    27      * 查看主線程的狀態,嘗試更改
    28      * 結論:活動的線程不能設置為後台線程
    29      *
    30      * @param args
    31      */
    32     public static void main1(String[] args) {
    33         Thread threadMain = Thread.currentThread();
    34         System.out.println("是後台線程么:" + threadMain.isDaemon());// false
    35         threadMain.setDaemon(true); // 報錯,活動的線程不能設置為後台線程
    36         System.out.println("修改后是後台線程么:" + threadMain.isDaemon());
    37     }
    38 
    39     /**
    40      * 測試2
    41      * 查看主線程中 創建線程的狀態,嘗試更改;
    42      *
    43      * @param args
    44      */
    45     public static void main2(String[] args) {
    46         Thread thread = new Thread();
    47         // false
    48         System.out.println("是後台線程么:" + thread.isDaemon());
    49         // 修改為後台線程
    50         thread.setDaemon(true);
    51         System.out.println("修改后是後台線程么:" + thread.isDaemon());
    52     }
    53 
    54     /**
    55      * 測試3
    56      * 查看主線程中 創建線程的狀態,嘗試更改,讓線程處於活動狀態在修改->報錯;
    57      *
    58      * @param args
    59      */
    60     public static void main3(String[] args) {
    61         DaemonThread thread = new DaemonThread();
    62         // 讓線程處於活躍狀態
    63         thread.start();
    64         // false
    65         System.out.println("是後台線程么:" + thread.isDaemon());
    66         // 修改為後台線程,報錯,當前已經是活躍狀態(thread.start())不能修改為後台線程
    67         thread.setDaemon(true);
    68         System.out.println("修改后是後台線程么:" + thread.isDaemon());
    69     }
    70 
    71     /**
    72      * 測試4
    73      * 前台線程執行完后,會直接關閉後台線程,即如果後台線程不一定能執行完成
    74      * 可以通過修改等待執行時間來觀察DaemonThread線程的數組輸出變化
    75      *
    76      * @param args
    77      */
    78     public static void main(String[] args) throws InterruptedException {
    79         DaemonThread thread = new DaemonThread();
    80         // 修改為後台線程
    81         thread.setDaemon(true);
    82         // 讓線程處於活躍狀態
    83         thread.start();
    84         System.out.println("========等待後台線程執行============");
    85         Thread.sleep(5 * 1000);
    86     }
    87 }
     1 package com.wfd360.thread.demo04Daemon;
     2 
     3 /**
     4  * @author 姿勢帝-博客園
     5  * @address https://www.cnblogs.com/newAndHui/
     6  * @WeChat 851298348
     7  * @create 05/05 9:27
     8  * @description
     9  */
    10 public class DaemonThread extends Thread {
    11     @Override
    12     public void run() {
    13         for (int i = 0; i < 10; i++) {
    14             System.out.println("===="+i);
    15             try {
    16                 Thread.sleep(1000);
    17             } catch (InterruptedException e) {
    18                 e.printStackTrace();
    19             }
    20         }
    21     }
    22 }

    DaemonThread

    線程基礎相關的方法定義就先到這裏,下一篇我們將進入線程同步.

    https://www.cnblogs.com/newAndHui/p/12831089.html

    系統化的在線學習:點擊進入學習

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

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※台北網頁設計公司全省服務真心推薦

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※推薦評價好的iphone維修中心

    網頁設計最專業,超強功能平台可客製化

    ※別再煩惱如何寫文案,掌握八大原則!

  • WebService之Spring+CXF整合示例

    WebService之Spring+CXF整合示例

    一、Spring+CXF整合示例

    WebService是一種跨編程語言、跨操作系統平台的遠程調用技術,它是指一個應用程序向外界暴露一個能通過Web調用的API接口,我們把調用這個WebService的應用程序稱作客戶端,把提供這個WebService的應用程序稱作服務端。

    環境

    win10+Spring5.1+cxf3.3.2

    下載

    • 官網下載:https://archive.apache.org/dist/cxf/
    • 百度網盤:
      鏈接:https://pan.baidu.com/s/1nsUweTFG_6CcZKaVBCQ7uQ
      提取碼:4qp7

    服務端

    • 新建web項目
    • 放入依賴
      apache-cxf-3.3.2\lib中的jar包全部copy至項目WEB-INF\lib目錄下(偷個懶,這些jar包中包含了Spring所需的jar包)
    • web.xml中添加webService的配置攔截
    <!--webService  -->
    <servlet>
        <servlet-name>CXFService</servlet-name>
        <servlet-class>org.apache.cxf.transport.servlet.CXFServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>CXFService</servlet-name>
        <url-pattern>/webservice/*</url-pattern>
    </servlet-mapping>
    
    • webservice服務接口
      在項目src目錄下新建pms.inface.WebServiceInterface
    package pms.inface;
    
    import javax.jws.WebMethod;
    import javax.jws.WebParam;
    import javax.jws.WebResult;
    import javax.jws.WebService;
    
    @WebService(targetNamespace = "http://spring.webservice.server", name = "WebServiceInterface")
    public interface WebServiceInterface {
    
    	@WebMethod
        @WebResult(name = "result", targetNamespace = "http://spring.webservice.server")
    	public String sayBye(@WebParam(name = "word", targetNamespace = "http://spring.webservice.server") String word);
    
    }
    
    
    • 接口實現類
      在項目src目錄下新建pms.impl.WebServiceImpl
    package pms.impl;
    
    import javax.jws.WebService;
    
    import pms.inface.WebServiceInterface;
    
    @WebService
    public class WebServiceImpl implements WebServiceInterface{
    
    	@Override
    	public String sayBye(String word) {
    		return word + "當和這個真實的世界迎面撞上時,你是否找到辦法和自己身上的慾望講和,又該如何理解這個鋪面而來的人生?";
    	}
    
    }
    
    
    • webservice配置文件
      WEB-INF目錄下新建webservice配置文件cxf-webService.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:jaxws="http://cxf.apache.org/jaxws"
    	xmlns:cxf="http://cxf.apache.org/core"
    	xmlns:http-conf="http://cxf.apache.org/transports/http/configuration"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
           http://cxf.apache.org/jaxws
           http://cxf.apache.org/schemas/jaxws.xsd
           http://cxf.apache.org/core
    	   http://cxf.apache.org/schemas/core.xsd
    	   http://cxf.apache.org/transports/http/configuration
    	   http://cxf.apache.org/schemas/configuration/http-conf.xsd
    	   ">
    	   
    	<import resource="classpath:META-INF/cxf/cxf.xml" />
    
    	<!-- 使用jaxws:server標籤發布WebService服務 ,設置address為訪問地址, 和web.xml文件中配置的CXF配合為一個完整的路徑 -->
    	<!-- serviceClass為實現類的接口 serviceBean引用配置好的WebService實現類 -->
    	<jaxws:server address="/webServiceInterface"
    		serviceClass="pms.inface.WebServiceInterface">
    		<jaxws:serviceBean>
    			<ref bean="WebServiceImpl" />
    		</jaxws:serviceBean>
    	</jaxws:server>
    	
    	<!-- 為所有的WS設置超時時間 ,此時為默認值 連接時間30s,等待回復時間為60s-->	
    	<http-conf:conduit name="*.http-conduit">
    		<http-conf:client ConnectionTimeout="60000" ReceiveTimeout="120000"/>
    	</http-conf:conduit>
    
    </beans>
    
    • spring配置文件
      WEB-INF目錄下新建spring配置文件applicationContext.xml
    <?xml version="1.0" encoding="UTF-8"?>
    
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    
       <bean id="WebServiceImpl" class="pms.impl.WebServiceImpl"></bean>
    	
    	<import resource="cxf-webService.xml" />
    
    </beans>
    

          在web.xml中配置applicationContext.xml

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>
    		    /WEB-INF/applicationContext.xml
    		</param-value>
      </context-param>
      <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
      </listener>
    
    • 將項目放至tomcat中啟動
      啟動后訪問地址:localhost:PORT/項目名/webservice/webServiceInterface?wsdl,如下圖所示,webservice接口發布成功

    二、SoapUI測試

    SoapUI是一個開源測試工具,通過soap/http來檢查、調用、實現Web Service的功能/負載/符合性測試。

    下載

    • 百度網盤
      鏈接:https://pan.baidu.com/s/1N2RTqhvrkuzx7YJvmDeY7Q
      提取碼:e1w3

    測試

    • 打開SoapUI,新建一個SOAP項目,將剛才的發布地址copyInitial WSDL欄,點擊OK按鈕
    • 發起接口請求

    三、客戶端

    使用wsdl2java工具生成webservice客戶端代碼

    • 該工具在剛才下載的apache-cxf-3.3.2\bin目錄下
    • 配置環境變量
      設置CXF_HOME,並添加%CXF_HOME %/binpath環境變量。
    • CMD命令行輸入wsdl2java -help,有正常提示說明環境已經正確配置
    • wsdl2java.bat用法:
    wsdl2java –p 包名 –d 存放目錄 -all wsdl地址
    
    -p 指定wsdl的命名空間,也就是要生成代碼的包名
    
    -d 指令要生成代碼所在目錄
    
    -client 生成客戶端測試web service的代碼
    
    -server 生成服務器啟動web service代碼
    
    -impl 生成web service的實現代碼,我們在方式一用的就是這個
    
    -ant 生成build.xml文件
    
    -all 生成所有開始端點代碼
    
    • 生成客戶端代碼
    wsdl2java -p pms.inface -d ./ -all http://localhost:8080/spring_webservice_server/webservice/webServiceInterface?wsdl
    

    客戶端調用

    • 新建web項目
    • 放入依賴
      apache-cxf-3.3.2\lib中的jar包全部copy至項目WEB-INF\lib目錄下
    • wsdl2java生成的代碼放至src.pms.inface目錄下
    調用方法一:
    • 新建webServiceClientMain測試
    package pms;
    
    import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
    import pms.inface.WebServiceInterface;
    
    public class webServiceClientMain {
    	public static void main(String[] args) {
    		JaxWsProxyFactoryBean svr = new JaxWsProxyFactoryBean();
    		svr.setServiceClass(WebServiceInterface.class);
    		svr.setAddress("http://localhost:8080/spring_webservice_server/webservice/webServiceInterface?wsdl");
    		WebServiceInterface webServiceInterface = (WebServiceInterface) svr.create();
    
    		System.out.println(webServiceInterface.sayBye("honey,"));
    	}
    }
    
    • 運行webServiceClientMain
    調用方法二:
    • 在src目錄下新建applicationContext.xml
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:jaxws="http://cxf.apache.org/jaxws"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans
    		http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    		http://www.springframework.org/schema/context
    		http://www.springframework.org/schema/context/spring-context-3.0.xsd
    		http://cxf.apache.org/jaxws
    		http://cxf.apache.org/schemas/jaxws.xsd">
    
    	<jaxws:client id="webServiceInterface"
    		serviceClass="pms.inface.WebServiceInterface"
    		address="http://localhost:8080/spring_webservice_server/webservice/webServiceInterface?wsdl" >
    	</jaxws:client>	
    </beans>
    
    • 新建webServiceClientTest測試
    package pms;
    
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    import pms.inface.WebServiceInterface;
    
    public class webServiceClientTest {
    
    	public static void main(String[] args) {
    		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
    		WebServiceInterface webServiceInterface = context.getBean(WebServiceInterface.class);
    		String result = webServiceInterface.sayBye("honey,");
    		System.out.println(result);
    	}
    	
    }
    
    • 運行webServiceClientTest

    四、服務端攔截器

    • 需求場景:服務提供方安全驗證,也就是webservice自定義請求頭的實現,服務接口在身份認證過程中的密碼字段滿足SM3(哈希函數算法標準)的加密要求
    • SM3加密所需jar包:commons-lang3-3.9.jarbcprov-jdk15on-1.60.jar,這兩個jar包在剛才下載的apache-cxf-3.3.2\lib下就有
    • 請求頭格式
    <security>
    	<username></username>
    	<password></password>
    </auth>
    
    • src.pms.interceptor下新建WebServiceInInterceptor攔截器攔截請求,解析頭部
    package pms.interceptor;
    
    import java.util.List;
    import javax.servlet.http.HttpServletRequest;
    import javax.xml.namespace.QName;
    import org.apache.cxf.binding.soap.SoapMessage;
    import org.apache.cxf.headers.Header;
    import org.apache.cxf.interceptor.Fault;
    import org.apache.cxf.phase.AbstractPhaseInterceptor;
    import org.apache.cxf.phase.Phase;
    import org.apache.cxf.transport.http.AbstractHTTPDestination;
    import org.w3c.dom.Element;
    import org.w3c.dom.Node;
    import org.w3c.dom.NodeList;
    import pms.support.Sm3Utils;
    import pms.support.StringUtils;
    
    /**
     * WebService的輸入攔截器
     * @author coisini
     * @date May 2020, 13
     *
     */
    public class WebServiceInInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
    	
        private static final String USERNAME = "admin";
        private static final String PASSWORD = "P@ssw0rd";
        
        /**
         * 允許訪問的IP
         */
        private static final String ALLOWIP = "127.0.0.1;XXX.XXX.XXX.XXX";
    
    	public WebServiceInInterceptor() {
    		/*
    		 * 攔截器鏈有多個階段,每個階段都有多個攔截器,攔截器在攔截器鏈的哪個階段起作用,可以在攔截器的構造函數中聲明
    		 * RECEIVE 接收階段,傳輸層處理
    		 * (PRE/USER/POST)_STREAM 流處理/轉換階段
    		 * READ SOAPHeader讀取 
    		 * (PRE/USER/POST)_PROTOCOL 協議處理階段,例如JAX-WS的Handler處理 
    		 * UNMARSHAL SOAP請求解碼階段 
    		 * (PRE/USER/POST)_LOGICAL SOAP請求解碼處理階段 
    		 * PRE_INVOKE 調用業務處理之前進入該階段 
    		 * INVOKE 調用業務階段 
    		 * POST_INVOKE 提交業務處理結果,並觸發輸入連接器
    		 */
    		super(Phase.PRE_INVOKE);
    	}
    
    	/**
    	  * 客戶端傳來的 soap 消息先進入攔截器這裏進行處理,客戶端的賬目與密碼消息放在 soap 的消息頭<security></security>中,
    	  * 類似如下:
         * <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
         * <soap:Header><security><username>admin</username><password>P@ssw0rd</password></security></soap:Header>
         * <soap:Body></soap:Body></soap:Envelope>
         * 現在只需要解析其中的 <head></head>標籤,如果解析驗證成功,則放行,否則這裏直接拋出異常,
         * 服務端不會再往後運行,客戶端也會跟着拋出異常,得不到正確結果
         *
         * @param message
         * @throws Fault
         */
    	@Override
        public void handleMessage(SoapMessage message) throws Fault {
    		System.out.println("PRE_INVOKE");
    		
    		HttpServletRequest request = (HttpServletRequest)message.get(AbstractHTTPDestination.HTTP_REQUEST);
    	    String ipAddr=request.getRemoteAddr();
    	    System.out.println("客戶端訪問IP----"+ipAddr);
    	    
    	    if(!ALLOWIP.contains(ipAddr)) {
    			throw new Fault(new IllegalArgumentException("非法IP地址"), new QName("0009"));
    		}
    		
    		/**
    		 * org.apache.cxf.headers.Header
             * QName :xml 限定名稱,客戶端設置頭信息時,必須與服務器保持一致,否則這裏返回的 header 為null,則永遠通不過的
             */
    		Header authHeader = null;
    		//獲取驗證頭
    		List<Header> headers = message.getHeaders();
    		for(Header h:headers){
    			if(h.getName().toString().contains("security")){
    				authHeader=h;
    				break;
    			}
    		}
    		System.out.println("authHeader");
    		System.out.println(authHeader);
    		
    		if(authHeader !=null) {
    			Element auth = (Element) authHeader.getObject();
    			NodeList childNodes = auth.getChildNodes();
    			String username = null,password = null;
    			for(int i = 0, len = childNodes.getLength(); i < len; i++){
    					Node item = childNodes.item(i);
    					if(item.getNodeName().contains("username")){
    						username = item.getTextContent();
    						System.out.println(username);
    					}
    					if(item.getNodeName().contains("password")){
    						password = item.getTextContent();
    						System.out.println(password);
    					}
    			}
    			
    			if(StringUtils.isBlank(username) || StringUtils.isBlank(password)) { 
    		    	throw new Fault(new IllegalArgumentException("用戶名或密碼不能為空"), new QName("0001")); 
    		    }
    			
    			if(!Sm3Utils.verify(USERNAME, username) || !Sm3Utils.verify(PASSWORD,password)) { 
    		    	throw new Fault(new IllegalArgumentException("用戶名或密碼錯誤"), new QName("0008")); 
    		    }
    		  
    		    if (Sm3Utils.verify(USERNAME, username) && Sm3Utils.verify(PASSWORD,password)) { 
    		    	System.out.println("webService 服務端自定義攔截器驗證通過...."); 
    		    	return;//放行
    		    } 
    		}else {
    			throw new Fault(new IllegalArgumentException("請求頭security不合法"), new QName("0010"));
    		}
    	}
    
    	// 出現錯誤輸出錯誤信息和棧信息
    	public void handleFault(SoapMessage message) {
    		Exception exeption = message.getContent(Exception.class);
    		System.out.println(exeption.getMessage());
    	}
    	
    }
    
    • src.pms.support下新建Sm3Utils加密類
    package pms.support;
    
    import java.io.UnsupportedEncodingException;
    import java.security.Security;
    import java.util.Arrays;
    import org.bouncycastle.crypto.digests.SM3Digest;
    import org.bouncycastle.crypto.macs.HMac;
    import org.bouncycastle.crypto.params.KeyParameter;
    import org.bouncycastle.jce.provider.BouncyCastleProvider;
    import org.bouncycastle.pqc.math.linearalgebra.ByteUtils;
    
    /**
     * SM3加密
     * @author coisini
     * @date May 2020, 13
     */
    public class Sm3Utils {
    	 private static final String ENCODING = "UTF-8";
         static {
             Security.addProvider(new BouncyCastleProvider());
         }
    	    
        /**
         * sm3算法加密
         * @explain
         * @param paramStr
         * 待加密字符串
         * @return 返回加密后,固定長度=32的16進制字符串
         */
        public static String encrypt(String paramStr){
            // 將返回的hash值轉換成16進制字符串
            String resultHexString = "";
            try {
                // 將字符串轉換成byte數組
                byte[] srcData = paramStr.getBytes(ENCODING);
                // 調用hash()
                byte[] resultHash = hash(srcData);
                // 將返回的hash值轉換成16進制字符串
                resultHexString = ByteUtils.toHexString(resultHash);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return resultHexString;
        }
        
        /**
         * 返回長度=32的byte數組
         * @explain 生成對應的hash值
         * @param srcData
         * @return
         */
        public static byte[] hash(byte[] srcData) {
            SM3Digest digest = new SM3Digest();
            digest.update(srcData, 0, srcData.length);
            byte[] hash = new byte[digest.getDigestSize()];
            digest.doFinal(hash, 0);
            return hash;
        }
        
        /**
         * 通過密鑰進行加密
         * @explain 指定密鑰進行加密
         * @param key
         *            密鑰
         * @param srcData
         *            被加密的byte數組
         * @return
         */
        public static byte[] hmac(byte[] key, byte[] srcData) {
            KeyParameter keyParameter = new KeyParameter(key);
            SM3Digest digest = new SM3Digest();
            HMac mac = new HMac(digest);
            mac.init(keyParameter);
            mac.update(srcData, 0, srcData.length);
            byte[] result = new byte[mac.getMacSize()];
            mac.doFinal(result, 0);
            return result;
        }
        
        /**
         * 判斷源數據與加密數據是否一致
         * @explain 通過驗證原數組和生成的hash數組是否為同一數組,驗證2者是否為同一數據
         * @param srcStr
         *            原字符串
         * @param sm3HexString
         *            16進制字符串
         * @return 校驗結果
         */
        public static boolean verify(String srcStr, String sm3HexString) {
            boolean flag = false;
            try {
                byte[] srcData = srcStr.getBytes(ENCODING);
                byte[] sm3Hash = ByteUtils.fromHexString(sm3HexString);
                byte[] newHash = hash(srcData);
                if (Arrays.equals(newHash, sm3Hash))
                    flag = true;
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }
            return flag;
        }
        
        public static void main(String[] args) {
            // 測試二:account
            String account = "admin";
            String passoword = "P@ssw0rd";
            String hex = Sm3Utils.encrypt(account);
            System.out.println(hex);//dc1fd00e3eeeb940ff46f457bf97d66ba7fcc36e0b20802383de142860e76ae6
            System.out.println(Sm3Utils.encrypt(passoword));//c2de40449a2019db9936381fa9810c22c8548a8635ed2b7fb3c7ec362e37429d
            //驗證加密后的16進制字符串與加密前的字符串是否相同
            boolean flag =  Sm3Utils.verify(account, hex);
            System.out.println(flag);// true
        }
    }
    
    • StringUtils工具類
    package pms.support;
    
    /**
     * 字符串工具類
     * @author coisini
     * @date Nov 27, 2019
     */
    public class StringUtils {
    
    	/**
    	 * 判空操作
    	 * @param value
    	 * @return
    	 */
    	public static boolean isBlank(String value) {
    		return value == null || "".equals(value) || "null".equals(value) || "undefined".equals(value);
    	}
    
    }
    
    • cxf-webService.xml添加攔截器配置
    <!-- 在此處引用攔截器 -->
    <bean id="InInterceptor"
    	class="pms.interceptor.WebServiceInInterceptor" >
    </bean>
    
    <cxf:bus>
    	<cxf:inInterceptors>
    		<ref bean="InInterceptor" />
    	</cxf:inInterceptors>
    </cxf:bus> 
    
    • SoapUI調用
    • java調用

      服務端攔截器到此結束,由上圖可以看出攔截器配置生效

    五、客戶端攔截器

    • src.pms.support下新建AddHeaderInterceptor攔截器攔截請求,添加自定義認證頭部
    package pms.support;
    
    import java.util.List;
    import javax.xml.namespace.QName;
    import org.apache.cxf.binding.soap.SoapHeader;
    import org.apache.cxf.binding.soap.SoapMessage;
    import org.apache.cxf.headers.Header;
    import org.apache.cxf.helpers.DOMUtils;
    import org.apache.cxf.interceptor.Fault;
    import org.apache.cxf.phase.AbstractPhaseInterceptor;
    import org.apache.cxf.phase.Phase;
    import org.w3c.dom.Document;
    import org.w3c.dom.Element;
    
    public class AddHeaderInterceptor extends AbstractPhaseInterceptor<SoapMessage>{ 
        
        private String userName; 
        private String password; 
           
        public AddHeaderInterceptor(String userName, String password) { 
            super(Phase.PREPARE_SEND); 
            this.userName = userName; 
            this.password = password;  
        } 
       
        @Override 
        public void handleMessage(SoapMessage msg) throws Fault { 
        	   System.out.println("攔截...");
            
               /**
                * 生成的XML文檔
                * <authHeader>
                *      <userName>admin</userName>
                *      <password>P@ssw0rd</password>
                * </authHeader>
                */ 
            
            	// SoapHeader部分待添加的節點
         		QName qName = new QName("security");
         		Document doc = DOMUtils.createDocument();
    
         		Element pwdEl = doc.createElement("password");
         		pwdEl.setTextContent(password);
         		Element userEl = doc.createElement("username");
         		userEl.setTextContent(userName);
         		Element root = doc.createElement("security");
         		root.appendChild(userEl);
         		root.appendChild(pwdEl);
         		// 創建SoapHeader內容
         		SoapHeader header = new SoapHeader(qName, root);
         		// 添加SoapHeader內容
         		List<Header> headers = msg.getHeaders();
         		headers.add(header); 
        } 
    }
    
    • java調用,修改webServiceClientMain調用代碼如下
    public class webServiceClientMain {
    	public static void main(String[] args) {
    		JaxWsProxyFactoryBean svr = new JaxWsProxyFactoryBean();
    		svr.setServiceClass(WebServiceInterface.class);
    		svr.setAddress("http://localhost:8081/spring_webservice_server/webservice/webServiceInterface?wsdl");
    		WebServiceInterface webServiceInterface = (WebServiceInterface) svr.create();
    		
    		// jaxws API 轉到 cxf API 添加日誌攔截器
    		org.apache.cxf.endpoint.Client client = org.apache.cxf.frontend.ClientProxy
    				.getClient(webServiceInterface);
    		org.apache.cxf.endpoint.Endpoint cxfEndpoint = client.getEndpoint();
    		//添加自定義的攔截器
    		cxfEndpoint.getOutInterceptors().add(new AddHeaderInterceptor("dc1fd00e3eeeb940ff46f457bf97d66ba7fcc36e0b20802383de142860e76ae6", "c2de40449a2019db9936381fa9810c22c8548a8635ed2b7fb3c7ec362e37429d"));
    		
    		System.out.println(webServiceInterface.sayBye("honey,"));
    	}
    }
    

    • SoapUI調用

    六、代碼示例

    服務端:https://github.com/Maggieq8324/spring_webservice_server.git
    客戶端:https://github.com/Maggieq8324/spring_webservice_client.git

    .end

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

    【其他文章推薦】

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

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

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

    ※回頭車貨運收費標準

    網頁設計最專業,超強功能平台可客製化

    ※別再煩惱如何寫文案,掌握八大原則!

  • 疫情下的奇景!孟買市區湧入大量紅鶴 數量估創紀錄

    摘錄自2020年5月2日自由時報綜合報導

    根據《CNN》報導,每年9月至翌年5月都會觀測到紅鶴族群遷徙至孟買覓食,然而,今(2020)年在人類活動大幅下降的狀況下,遷徙至當地的紅鶴數量預估將超過13萬4000隻,創下歷史新高。

    孟買自然歷史學會(BNHS)副主任科特(Rahul Khot)表示,在人類社交活動暫停後,當地不僅出現破紀錄數量的紅鶴,牠們選定的棲地也與往常相異,已有族群擴展至以往少見紅鶴蹤跡的濕地。

    印度境內陸續傳出野生動物受益於武漢肺炎疫情的消息,不只德里湧入大量猴群,極瀕危的恆河江豚也在多年來首度被觀測到活體行為;顯示出人類活動暫停,讓我們的地球鄰居們產生明顯變化。

    生物多樣性
    生態保育
    國際新聞
    印度
    孟買
    紅鶴
    江豚
    動物與大環境變遷
    武漢肺炎

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

    【其他文章推薦】

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

    ※別再煩惱如何寫文案,掌握八大原則!

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

    ※超省錢租車方案

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※回頭車貨運收費標準

  • 山地大猩猩的家園不平靜 剛果維龍加國家公園12名護管員遭殺害

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

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

    【其他文章推薦】

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※超省錢租車方案

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

    網頁設計最專業,超強功能平台可客製化

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

  • 墨西哥穩定電力擺第一 新能源測試喊卡

    摘錄自2020年5月4日經濟日報報導

    在疫情蔓延下,墨西哥電力系統主管機關宣布,對乾淨能源新計畫的關鍵測試無限期喊卡,另採取措施,以提高全國電力系統的穩定性,但批評者擔心,這項措施將傷害再生能源業者。

     

    能源議題
    能源轉型
    國際新聞
    墨西哥
    乾淨能源
    武漢肺炎
    綠電
    疫情看氣候與能源
    新能源

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

    ※別再煩惱如何寫文案,掌握八大原則!

    ※超省錢租車方案

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

    ※推薦台中搬家公司優質服務,可到府估價

  • 法下週解除封鎖令 巴黎交通主幹道規劃給自行車

    摘錄自2020年5月5日自由時報報導

    法國為遏止武漢肺炎(新型冠狀病毒疾病,COVID-19)疫情擴散,自3月17日起實施全國封鎖,期間2度延長封鎖禁令至5月11日;面對下週即將解除的封鎖令,巴黎市長伊達戈(Anne Hidalgo)將把最繁忙的主要交通幹道規劃給自行車,以減少民眾對大眾運輸工具的依賴,進而避免群聚感染。

    伊達戈今(5日)指出,城市解封後共將保留50公里原先的汽車道給自行車使用,另外將有30條街道將被設置為行人專用道,她強調,「特別是在學校周圍,以避免人群聚集」。

    法國政府也宣布了一項2000萬歐元(約新台幣6.5億元)的自行車計畫,用以刺激民眾在封鎖解除後對自行車的使用度,其中包括每人50歐元(約新台幣1620元)的自行車維修或調整補貼。

    生活環境
    國際新聞
    法國
    檢疫封鎖
    解除
    自行車道

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

    【其他文章推薦】

    ※超省錢租車方案

    ※別再煩惱如何寫文案,掌握八大原則!

    ※回頭車貨運收費標準

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

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

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

    網頁設計最專業,超強功能平台可客製化