分類
發燒車訊

005.OpenShift訪問控制-權限-角色

一 Kubetcl namespace

1.1 namespace描述


Kubernetes namespace提供了將一組相關資源組合在一起的機制。在Red Hat OpenShift容器平台中,project是一個帶有附加註釋的Kubernetes namespace。

namespace提供以下特性:

  1. 命名資源,以避免基本的命名衝突;
  2. 將管理權限授予受信任的用戶;
  3. 限制用戶資源消耗的能力;
  4. 用戶和用戶組隔離。

1.2 project


project提供了一種機制,通過這種機制可以管理普通用戶對資源的訪問。project允許一組用戶獨立於其他組組織和管理其內容,必須允許用戶訪問項目。如果允許創建項目,用戶將自動訪問自己的項目。

項目可以有單獨的name、display name和description。

name是項目的唯一標識符,在使用CLI工具或API時都是基於name,name的最大長度為63個字符。

display name是項目在web控制台中显示的方式(默認為name)。

description是項目的更詳細描述,並且在web控制台中也可見。

以下組件適用於項目:

  • Object:pod、service、rc等;
  • Policies:決定用戶可以或不能對對象執行哪些操作的規則;
  • Constraints:可以限制的每種對象的配額。

1.3 cluster管理


集群管理員可以創建項目並將項目的管理權限委託給任何用戶。在OpenShift容器平台中,項目用於對相關對象進行分組和隔離。

管理員可以讓用戶訪問某些項目,允許他們創建自己的項目,並在單個項目中賦予他們管理權限。

管理員可以將角色應用於允許或限制其創建項目能力的用戶和組,同時可以在用戶初始登錄之前分配角色。

限制項目創建:從通過身份驗證的用戶和組中刪除self-provisioning集群角色,將拒絕任何新項目的權限。

[root@master ~]$ oc adm policy remove-cluster-role-from-group \

self-provisioner \

system:authenticated \

system:authenticated:oauth

授予項目創建:項目創建授予具有self-供應者角色和self-provisione集群角色綁定的用戶。默認情況下,所有經過身份驗證的用戶都可以使用這些角色。

[root@master ~]$ oc adm policy add-cluster-role-to-group \

self-provisioner \

system:authenticated \

system:authenticated:oauth

1.4 創建project


如果項目創建權限被授予用戶,則可以使用oc命令創建project。

[root@master ~]$ oc new-project demoproject \

–description=”Demonstrate project creation” \

–display-name=”demo_project”

二 OpenShift角色

2.1 角色概述


role具有不同級別的訪問和策略,包括集群和本地策略。user和group可以同時與多個role關聯。運行oc description命令查看角色及其綁定的詳細信息。

在集群策略中具有cluster-admin缺省角色的用戶可以查看集群策略和所有本地策略。在給定的本地策略中具有admin缺省角色的用戶可以基於per-project查看策略。

可通過以下命令查看當前的集群綁定集,其中显示綁定到不同角色的用戶和組。

[root@demo ~]# oc describe clusterPolicyBindings :default

2.2 查看本地policy


儘管本地角色列表及其關聯的規則集在本地策略中是不可查看的,但是所有缺省角色仍然適用,並且可以添加到用戶或組中,cluster-admin缺省角色除外。但是,本地綁定是可見的。

可通過以下命令查看當前的本地綁定,其中显示綁定到不同角色的用戶和組。

[root@demo ~]# oc describe policyBindings :default

提示:默認情況下,在本地策略中,只會列出admin角色的綁定。但是,如果將其他默認角色添加到本地策略中的用戶和組,也會列出它們。

2.3 管理role綁定


向用戶或組添加或綁定角色,從而實現向用戶或組提供角色授予的相關訪問權限。可以使用oc adm policy命令在用戶和組之間添加和刪除角色。

當使用以下操作管理本地策略的用戶和組角色時,可以使用-n選項指定項目。如果沒有指定,則使用當前項目。

常見管理本地策略操作:





























命令 描述
oc adm policy who-can verb resource 設置哪些用戶可以對資源執行操作
oc adm policy add-role-to-user role username 將指定角色綁定到指定用戶
oc adm policy remove-role-from-user role username 從指定用戶中移除給定角色
oc adm policy remove-user username 刪除指定的用戶及其所有角色
oc adm policy add-role-to-group role groupname 將指定的角色綁定到指定的組
oc adm policy remove-role-fromgroup role groupname 從指定組中移除給定角色
oc adm policy remove-group groupname 刪除指定的組及其所有角色


還可以使用如下所示的的操作管理cluster policy的role binding,這類命令不需要-n選項,因為cluster policy不在namespace級別上操作。

常見管理cluster policy操作:




















命令 描述
oc adm policy add-cluster-role-to-user role username 將集群中所有項目的指定角色綁定到指定用戶
oc adm policy remove-cluster-role-from-user role username 為集群中的所有項目從指定用戶中刪除指定角色
oc adm policy add-cluster-role-togroup role groupname 為集群中的所有項目將指定的角色綁定到指定的組
oc adm policy remove-cluster-role-from-group role groupname 從集群中所有項目的指定組中移除給定角色


提示:oc policy命令應用於當前項目,而oc adm policy命令應用於集群範圍的操作。

示例:在example項目中為developer用戶提供admin角色。

[root@demo ~]# oc adm policy add-role-to-user admin developer -n example

[root@demo ~]# oc describe policybindings :default -n example #檢查綁定

三 安全上下文約束(SCCS)

3.1 SCCS概述


OpenShift提供安全上下文約束(SCCS),它控制pod可以執行的操作和它可以訪問的資源。默認情況下,任何容器的執行都只授予受限制的SCC定義的功能。

SCCS相關命令:

  1 [user@demo ~]$ oc get scc			                        #列出可用的SCC
  2 [user@demo ~]$ oc describe scc scc_name		                #現實特定SCC詳細信息
  3 [user@demo ~]$ oc adm policy add-scc-to-user scc_name user_name
  4 [user@demo ~]$ oc adm policy add-scc-to-group scc_name group_name	#要授予用戶或組特定的SCC
  5 [user@demo ~]$ oc adm policy remove-scc-from-user scc_name user_name
  6 [user@demo ~]$ oc adm policy remove-scc-from-group scc_name group_name	#從特定的SCC中刪除用戶或組


四 服務賬戶

4.1 服務賬戶


service account提供了一種靈活的方法來控制API訪問,而無需共享常規用戶的憑據。如果應用程序需要訪問受限制的SCC未授予的功能,可創建一個新的、特定的service account並將其添加到適當的SCC中。

例如,在缺省情況下,OpenShift不支持部署需要提升特權的應用程序。若有此需求,可創建一個service account,修改dc,然後添加service account至SCC。

示例:將anyuid配置為在容器中作為root用戶運行。

[user@demo ~]$ oc create serviceaccount useroot #創建一個名為useroot的新服務帳戶

[user@demo ~]$ oc patch dc/demo-app \

–patch ‘{“spec”:{“template”:{“spec”:{“serviceAccountName”: “useroot”}}}}’ #修改應用程序的DC

[user@demo ~]$ oc adm policy add-scc-to-user anyuid -z useroot #將useroot服務帳戶添加到anyuid SCC中,作為容器中的根用戶運行

4.2 Web管理user成員


OCP平台的默認配置是,在用戶首次登錄成功時,自動創建該用戶對象。

要管理允許訪問項目的用戶,請以項目管理員或集群管理員的身份登錄到web控制台,並選擇要管理的項目。在左側窗格中,單擊Resources——>membership進入項目member頁面。

在Users列中,在突出显示的文本框中輸入用戶名。在“添加另一個角色”列中,從用戶所在行的列表中選擇一個角色,然後單擊“添加”。

4.3 Cli管理user成員


CLI中如果自動創建對象功能被關閉,集群管理員可通過如下方式創建新用戶:

[root@master ~]$ oc create user demo-user

同時還需要在身份認證軟件中創建用戶,如為HTPasswdIdentityProvider才用戶命令如下:

[root@master ~]$ htpasswd /etc/origin/openshift-passwd demo-user

要向用戶添加項目角色,首先使用oc project命令輸入項目,然後使用oc policy add-role-to-user命令:

[root@master ~]$ oc policy add-role-to-user edit demo-user

要從用戶中刪除項目角色,使用oc policy remove-role-from-user命令:

[root@master ~]$ oc policy remove-role-from-user edit demo-user

並不是所有OpenShift角色都由項目限定範圍。要分配這些規則,請使用oc adm policy command命令。

[root@master ~]$ oc adm policy add-cluster-role-to-user cluster-admin admin

4.4 身份驗證和授權


身份驗證層標識與對OpenShift容器平台API的請求相關聯的用戶,然後授權層使用關於請求用戶的身份信息來確定是否應該允許該請求。

  • user和group


OCP容器平台中的用戶是一個可以向OpenShift API發出請求的實體。通常,這表示與OpenShift交互的develop或administrator的帳戶。

可以將用戶分配給一個或多個組,每個組表示一組特定的角色(或權限)。當需要通過管理授權策略給多個客戶授權時候,group會比較合適。例如允許訪問項目中的對象,而不是單獨授予用戶。

  • Authentication Tokens


API調用必須使用訪問令牌或X.509證書進行身份驗證,會話token表示用戶,並且是短期的,默認情況下在24小時內到期。

可以通過運行oc whoami命令來驗證經過身份驗證的用戶。

[root@master ~]$ oc login -u demo-user

[root@master ~]$ oc whoami

demo-user

4.5 身份驗證類型


本環境中,身份驗證由HTPasswdIdentityProvider模塊提供,該模塊根據使用htpasswd命令生成的文件驗證用戶名和密碼。

OpenShift容器平台支持的其他認證類型包括:

  • Basic Authentication (Remote)


一種通用的後端集成機制,允許用戶使用針對遠程標識提供者驗證的憑據登錄到OpenShift容器平台。用戶將他們的用戶名和密碼發送到OpenShift容器平台,OpenShift平台通過到服務器的請求驗證這些憑據,並將憑據作為基本的Auth頭傳遞。這要求用戶在登錄過程中向OpenShift容器平台輸入他們的憑據。

  • Request Header Authentication


用戶使用請求頭值(如X-RemoteUser)登錄到OpenShift容器平台。它通常與身份驗證代理結合使用,身份驗證代理對用戶進行身份驗證,然後通過請求頭值為OpenShift容器平台提供用戶標識。

  • Keystone Authentication


Keystone是一個OpenStack項目,提供標識、令牌、目錄和策略服務。OpenShift容器平台與Keystone集成,通過配置OpenStack Keystone v3服務器將用戶存儲在內部數據庫中,從而支持共享身份驗證。這種配置允許用戶使用Keystone憑證登錄OpenShift容器平台。

  • LDAP Authentication


用戶使用他們的LDAP憑證登錄到OpenShift容器平台。在身份驗證期間,LDAP目錄將搜索與提供的用戶名匹配的條目。如果找到匹配項,則嘗試使用條目的專有名稱(DN)和提供的密碼進行簡單綁定。

  • GitHub Authentication


GitHub使用OAuth,它允許與OpenShift容器平台集成使用OAuth身份驗證來促進令牌交換流。這允許用戶使用他們的GitHub憑證登錄到OpenShift容器平台。為了防止使用GitHub用戶id的未授權用戶登錄到OpenShift容器平台集群,可以將訪問權限限制在特定的GitHub組織中。

五 管理項目及賬戶

5.1 前置準備


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

5.2 本練習準備


[student@workstation ~]$ lab secure-resources setup

5.3 創建htpasswd賬戶

  1 [kiosk@foundation0 ~]$ ssh root@master
  2 [root@master ~]# htpasswd -b /etc/origin/master/htpasswd user1 redhat
  3 [root@master ~]# htpasswd -b /etc/origin/master/htpasswd user2 redhat
  4 #添加基於htpasswd形式的user1和user2,密碼都為redhat。


5.4 設置策略

  1 [student@workstation ~]$ oc login -u admin -p redhat https://master.lab.example.com	#使用管理員登錄
  2 [student@workstation ~]$ oc adm policy remove-cluster-role-from-group \
  3 self-provisioner system:authenticated:oauth
  4 #刪除所有賦予普通創建項目的功能,該命令可參考本環境如下目錄中的命令。
  5 [student@workstation ~]$ cat /home/student/DO280/labs/secure-resources/configure-policy.sh
  6 #!/bin/bash
  7 oc adm policy remove-cluster-role-from-group \
  8     self-provisioner system:authenticated system:authenticated:oauth


5.5 驗證策略

  1 [student@workstation ~]$ oc login -u user1 -p redhat https://master.lab.example.com	#使用普通用戶user1登錄
  2 [student@workstation ~]$ oc new-project test					#測試創建project
  3 Error from server (Forbidden): You may not request a new project via this API.


5.6 創建項目

  1 [student@workstation ~]$ oc login -u admin -p redhat https://master.lab.example.com	#使用集群管理員登錄
  2 [student@workstation ~]$ oc new-project project-user1				#創建兩個項目
  3 [student@workstation ~]$ oc new-project project-user2


5.7 將項目與user關聯

  1 #選擇項目1
  2 Now using project "project-user1" on server "https://master.lab.example.com:443".
  3 [student@workstation ~]$ oc policy add-role-to-user admin user1		#將user1添加為項目1的管理員
  4 role "admin" added: "user1"
  5 [student@workstation ~]$ oc policy add-role-to-user edit user2		#將user2添加為項目1的開發員
  6 role "edit" added: "user2"
  7 
  8 [student@workstation ~]$ oc project project-user2			        #選擇項目2
  9 Now using project "project-user2" on server "https://master.lab.example.com:443".
 10 [student@workstation ~]$ oc policy add-role-to-user edit user2		#將user2添加為項目2的開發員
 11 role "edit" added: "user2"


5.8 驗證訪問

  1 [student@workstation ~]$ oc login -u user1 -p redhat https://master.lab.example.com	#使用user1登錄
  2 [student@workstation ~]$ oc project project-user1					#驗證項目1的訪問
  3 Already on project "project-user1" on server "https://master.lab.example.com:443".
  4 [student@workstation ~]$ oc project project-user2					#驗證項目2的訪問
  5 error: You are not a member of project "project-user2".
  6 You have one project on this server: project-user1
  7 
  8 [student@workstation ~]$ oc login -u user2 -p redhat https://master.lab.example.com	#使用user2登錄
  9 [student@workstation ~]$ oc project project-user1
 10 Already on project "project-user1" on server "https://master.lab.example.com:443".	#驗證項目1的訪問
 11 [student@workstation ~]$ oc project project-user2
 12 Now using project "project-user2" on server "https://master.lab.example.com:443".	#驗證項目2的訪問


5.9 部署特權應用

  1 [student@workstation ~]$ oc login -u user2 -p redhat https://master.lab.example.com
  2 [student@workstation ~]$ oc project project-user1
  3 Now using project "project-user1" on server "https://master.lab.example.com:443".
  4 [student@workstation ~]$ oc new-app --name=nginx --docker-image=registry.lab.example.com/nginx:latest
  5 #使用在項目1上不具備admin權限的用戶user2登錄,並部署應用,會出現如下提示:





5.10 驗證部署

  1 [student@workstation ~]$ oc get pods




結論:由上可知,部署失敗是因為容器映像需要root用戶,pod以CrashLoopBackOff或錯誤狀態結束。

5.11 故障排除


若要解決此故障需要減少特定項目的安全限制。

要使用特權訪問運行容器,可創建一個允許pod使用操作系統普通用戶運行的service account。

如下部分需要具有項目管理員特權的用戶執行,而另一些操作需要具有集群管理員特權的用戶執行。

本環境中,相關操作命令可以從/home/student/DO280/labs/secure-resources文件夾中的configure-sc.sh腳本運行或複製。

  1 [student@workstation ~]$ oc login -u user1 -p redhat https://master.lab.example.com	#使用項目1的admin賬戶登錄 
  2 [student@workstation ~]$ oc create serviceaccount useroot		  #創建服務賬戶
  3 serviceaccount "useroot" created
  4 [student@workstation ~]$ oc login -u admin -p redhat https://master.lab.example.com	#使用集群管理員登錄
  5 [student@workstation ~]$ oc project project-user1			  #選擇項目1
  6 Already on project "project-user1" on server "https://master.lab.example.com:443".
  7 [student@workstation ~]$ oc adm policy add-scc-to-user anyuid -z useroot  #設置SCC策略
  8 scc "anyuid" added to: ["system:serviceaccount:project-user1:useroot"]    #將服務帳戶與anyuid安全上下文關聯,此操作需要集群管理員用戶。
  9 [student@workstation ~]$ oc login -u user2 -p redhat https://master.lab.example.com	#切換user2用戶
 10 [student@workstation ~]$ oc project project-user1
 11 Already on project "project-user1" on server "https://master.lab.example.com:443".
 12 [student@workstation ~]$ oc patch dc nginx --patch='{"spec":{"template":{"spec":{"serviceAccountName": "useroot"}}}}'



#更新負責管理nginx的dc資源,任何開發人員用戶都可以執行此操作。本環境中,相關操作命令可以從/home/student/DO280/labs/secure-resources文件夾中的configure-sc.sh腳本運行或複製。

5.12 驗證確認

  1 [student@workstation ~]$ oc get pods
  2 NAME            READY     STATUS    RESTARTS   AGE
  3 nginx-2-98k8f   1/1       Running   0          3m
  4 

5.13 暴露服務

  1 [student@workstation ~]$ oc expose svc nginx
  2 route "nginx" exposed
  3 [student@workstation ~]$ oc get svc
  4 NAME      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
  5 nginx     ClusterIP   172.30.118.63   <none>        80/TCP    13m
  6 [student@workstation ~]$ oc get route
  7 NAME      HOST/PORT                                  PATH      SERVICES   PORT      TERMINATION   WILDCARD
  8 nginx     nginx-project-user1.apps.lab.example.com             nginx      80-tcp                  None


5.14 測試訪問

  1 [student@workstation ~]$ curl -s http://nginx-project-user1.apps.lab.example.com

5.15 策略刪除演示

  1 [student@workstation ~]$ oc login -u admin -p redhat
  2 [student@workstation ~]$ oc adm policy add-cluster-role-to-group self-provisioner system:authenticated system:authenticated:oauth
  3 cluster role "self-provisioner" added: ["system:authenticated" "system:authenticated:oauth"]
  4 [student@workstation ~]$ oc delete project project-user1
  5 project "project-user1" deleted
  6 [student@workstation ~]$ oc delete project project-user2
  7 [root@master ~]# htpasswd -D /etc/origin/master/htpasswd user1 
  8 [root@master ~]# htpasswd -D /etc/origin/master/htpasswd user2


#為所有常規用戶重新啟用項目創建,即重置為初始狀態。本環境中,相關操作命令可以從/home/student/DO280/labs/secure-resources文件夾中的restore-policy.sh腳本運行或複製。

六 管理加密信息

6.1 secret特性


Secret對象類型提供了一種機制來保存敏感信息,如密碼、OCP客戶端配置文件、Docker配置文件和私有倉庫憑據。Secrets將敏感內容與Pod解耦。可以使用Volume插件將Secrets掛載到容器上,或者系統可以使用Secrets代表pod執行操作。

Secrets的主要特徵包括:

  • Secrets data可以獨立於其定義引用。
  • Secrets data Volume由臨時文件存儲支持。
  • 可以在名稱空間中共享Secrets data。

6.2 創建Secrets


在依賴於該Secrets的pod之前創建一個Secrets。

  1 [user@demo ~]$ oc create secret generic secret_name \
  2 --from-literal=key1=secret1 \
  3 --from-literal=key2=secret2	#用secret data創建secret對象
  4 [user@demo ~]$ oc secrets add --for=mount serviceaccount/serviceaccount-name \
  5 secret/secret_name		#更新pod的服務帳戶,允許引用該secrets。

例如,允許一個運行在指定服務帳戶下的pod掛載一個secrets

創建一個pod,該pod使用環境變量或數據卷作為文件的方式使用該secret,通常使用模板完成。

6.3 使用secret暴露Pod


secrets可以作為數據卷掛載,也可以作為環境變量以便供pod中的容器使用。

例如,要向pod公開一個secrets,首先創建一個secrets並將username和password以k/v形式配置,然後將鍵名分配給pod的YAML文件env定義。

示例:創建名為demo-secret的secrets,它定義用戶名和密碼為username/demo-user。

[user@demo ~]$ oc create secret generic demo-secret \

–from-literal=username=demo-user

要使用前面的secret作為MySQL數據庫pod的數據庫管理員密碼,請定義環境變量,並引用secret名稱和密碼。

  1 env:
  2   - name: MYSQL_ROOT_PASSWORD
  3     valueFrom:
  4       secretKeyRef:
  5        key: username
  6        name: demo-secret


6.4 web端管理secret


從web控制台管理secret:

1. 以授權用戶身份登錄到web控制台。

2. 創建或選擇一個項目來承載secret。

3. 導航到resource——>secrets。

6.5 Secret使用場景


  • password和user names


敏感信息(如password和user name)可以存儲在一個secret中,該secret被掛載為容器中的數據卷。數據显示為位於容器的數據卷目錄中的文件中的內容。然後,應用程序(如數據庫)可以使用這些secret對用戶進行身份驗證。

  • 傳輸層安全性(TLS)和密鑰對


通過讓集群將簽名證書和密鑰對生成到項目名稱空間中的secret中,可以實現對服務的通信的保護。證書和密鑰對使用PEM格式存儲以類似tls.crt和tls.key的格式存儲在secret的pod中。

七 ConfigMap對象

7.1 ConfigMap概述


ConfigMaps對象類似於secret,但其設計目的是支持處理不包含敏感信息的字符串。ConfigMap對象持有配置數據的鍵值對,這些配置數據可以在pods中使用,或者用於存儲系統組件(如控制器)的配置數據。

ConfigMap對象提供了將配置數據注入容器的機制。ConfigMap存儲精細的粒度信息,比如單個屬性,或者詳細信息,比如整個配置文件或JSON blob。

7.2 CLI創建ConfigMap


可以使用–from-literal選項從CLI創建ConfigMap對象。

示例:創建一個ConfigMap對象,該對象將IP地址172.20.30.40分配給名為serverAddress的ConfigMap密鑰。

  1 [user@demo ~]$ oc create configmap special-config \
  2 --from-literal=serverAddress=172.20.30.40
  3 [user@demo ~]$ oc get configmaps special-config -o yaml		#查看configMap
  4 apiVersion: v1
  5 data:
  6   key1: serverAddress=172.20.30.40
  7 kind: ConfigMap
  8 metadata:
  9   creationTimestamp: 2017-07-10T17:13:31Z
 10   name: special-config
 11 ……
 12 在配置映射的pod定義中填充環境變量APISERVER。
 13 env:
 14   - name: APISERVER
 15       valueFrom:
 16         configMapKeyRef:
 17           name: special-config
 18           key: serverAddress


7.3 web管理ConfigMap


從web控制台管理ConfigMap對象:

1. 以授權用戶身份登錄到web控制台。

2. 創建或選擇一個項目來承載ConfigMap。

3. 導航到資源→配置映射。

八 加密練習

8.1 前置準備


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

8.2 本練習準備


[student@workstation ~]$ lab secure-secrets setup

8.3 創建項目

  1 [student@workstation ~]$ oc login -u developer -p redhat
  2 [student@workstation ~]$ cd /home/student/DO280/labs/secure-secrets/
  3 [student@workstation secure-secrets]$ less mysql-ephemeral.yml		#導入本環境MySQL模板







模板解讀:

該mysql-ephemeral.yml模板文件,包含openshift項目中的mysql臨時模板,pod所需的其他環境變量由模板參數初始化,並具有默認值。

但沒有secret定義,後續操作將手動創建模板所需的secret。

根據模板的要求,創建一個包含MySQL容器image使用的憑證的secret,將這個secret命名為mysql。

  • 應用程序訪問的數據庫用戶名由database-user定義。
  • 數據庫用戶的密碼由database-password定義。
  • 數據庫管理員密碼由database-root-password定義

8.4 創建新secret

  1 [student@workstation secure-secrets]$ oc create secret generic mysql \
  2 --from-literal='database-user'='mysql' \
  3 --from-literal='database-password'='redhat' \
  4 --from-literal='database-root-password'='do280-admin'
  5 [student@workstation secure-secrets]$ oc get secret mysql -o yaml	#確認secret




8.5 創建應用

  1 [student@workstation secure-secrets]$ oc new-app --file=mysql-ephemeral.yml
  2 [student@workstation secure-secrets]$ oc get pods		#確認應用
  3 NAME            READY     STATUS    RESTARTS   AGE
  4 mysql-1-j4fnz   1/1       Running   0          1m


8.6 端口轉發

  1 [student@workstation secure-secrets]$ cd
  2 [student@workstation ~]$ oc port-forward mysql-1-j4fnz 3306:3306



提示:驗證完成之前forward不要關閉。

8.7 確認驗證

  1 [student@workstation ~]$ mysql -uroot -pdo280-admin -h127.0.0.1	#新開終端測試MySQL



九 管理security policy

9.1 OCP authorization授權


OCP定義了用戶可以執行的兩組主要操作:

與項目相關的操作(也稱為本地策略):project-related

與管理相關的操作(也稱為集群策略):administration-related

由於這兩種策略都有大量可用的操作,所以將一些操作分組並定義為角色。














默認角色 描述
cluster-admin 此角色中的所有用戶都可以管理OpenShift集群。
cluster-status 此角色中的所有用戶都提供對集群信息的只讀訪問。


為管理本地政策,OCP提供以下角色:




















默認角色 描述
edit 角色中的用戶可以從項目中創建、更改和刪除公共應用程序資源,比如service和dc。 但是不能對限制範圍和配額等管理資源採取行動,也不能管理對項目的訪問權限。
basic-user 角色中的用戶具有對項目的讀訪問權。
self-provisioner 角色中的用戶可以創建新項目。這是一個集群角色,而不是項目角色。
admin 角色中的用戶可以管理項目中的所有資源,包括授予對項目的其他用戶的訪問權


除了能夠創建新應用程序之外,admin角色還允許用戶訪問項目資源,比如配額和限制範圍。

edit角色允許用戶在項目中充當開發人員,但要在項目管理員配置的約束下工作。

9.2 相關命令

  1 向集群用戶添加角色
  2 $ oc adm policy add-cluster-role-to-user cluster-role username
  3 示例:將普通用戶更改為集群管理員。
  4 $ oc adm policy add-cluster-role-to-user cluster-role username
  5 從用戶中刪除集群角色
  6 $ oc adm policy remove-cluster-role-from-user cluster-role username
  7 示例:將集群管理員更改為普通用戶。
  8 $ oc adm policy remove-cluster-role-from-user cluster-admin username
  9 將指定的用戶綁定到項目中的角色
 10 $ oc adm policy add-role-to-user role-name username -n project
 11 示例:在WordPress項目中dev用戶綁定basic-user角色。
 12 $ oc adm policy add-role-to-user basic-user dev -n wordpress


9.3 權限及規則


OpenShift將一組規則集合成一個角色,規則由謂詞和資源定義。如create user是OpenShift中的一條規則,它是一個名為cluster-admin的角色的所擁有的權限的一部分。

  1 $ oc adm policy who-can delete user

9.4 user類型


與OCP的交互基於用戶,OCP的user對象表示可以通過向該用戶或用戶組添加角色來從而實現相應權限的授予。

Regular users:通常以這種用戶類型與OCP交互,常規用戶用User對象表。例如,user1,user2。

System users:通常在安裝OCP中定義基礎設施時自動創建的,主要目的是使基礎設施能夠安全地與API交互。包括集群管理員(可以訪問所有內容)、每個節點的用戶、路由器和內部倉庫使用的用戶,以及各種其他用戶。還存在一個匿名系統用戶,默認情況下,該用戶用於未經身份驗證的請求。system user主要包括:system:admin、system:openshift-registry和system:node:node1.example.com。

Service accounts:這些是與項目關聯的特殊系統用戶。有些是在第一次創建項目時自動創建的,項目管理員可以創建多個,以便定義對每個項目內容的訪問。Service accounts由ServiceAccount對象表示。Service accounts主要包括:system:serviceaccount:default:deployer和system:serviceaccount:foo:builder。

每個用戶在訪問OpenShift容器平台之前必須進行身份驗證。沒有身份驗證或身份驗證無效的API請求將使用匿名系統用戶身份驗證來請求服務。身份驗證成功后,策略確定用戶被授權做什麼。

9.5 安全上下文約束(SCCS)


OpenShift提供了一種名為安全上下文約束的安全機制,它限制對資源的訪問,但不限制OpenShift中的操作。

SCC限制從OpenShift中運行的pod到主機環境的訪問:

  • 運行特權容器
  • 請求容器的額外功能
  • 使用主機目錄作為卷
  • 更改容器的SELinux上下文
  • 更改用戶ID


社區開發的一些容器可能需要放鬆安全上下文約束,因為它們可能需要訪問默認禁止的資源,例如文件系統、套接字或訪問SELinux上下文。

OpenShift定義的安全上下文約束(SCCs)可以使用以下命令作為集群管理員列出。

$ oc get scc

SCC通常有以下7中SCCS:

  • anyuid
  • hostaccess
  • hostmount-anyuid
  • nonroot
  • privileged
  • restricted(默認)


$ oc describe scc anyuid #查看某一種SCC詳情

OpenShift創建的所有容器都使用restricted類型的SCC,它提供了對OpenShift外部資源的有限訪問。

對於anyuid安全上下文,run as user策略被定義為RunAsAny,表示pod可以作為容器中可用的任何用戶ID運行。這允許需要特定用戶使用特定用戶ID運行命令的容器。

要將容器更改為使用不同的SCC運行,需要創建綁定到pod的服務帳戶。

$ oc create serviceaccount service-account-name #首先創建服務賬戶

$ oc adm policy add-scc-to-user SCC -z service-account #將服務帳戶與SCC關聯

要確定哪個帳戶可以創建需要更高安全性要求的pod,可以使用scc-subject-review子命令。

$ oc export pod pod-name > output.yaml

$ oc adm policy scc-subject-review -f output.yaml

9.6 OpenShift與SELinux


OpenShift要求在每個主機上啟用SELinux,以便使用強制訪問控制提供對資源的安全訪問。同樣,由OpenShift管理的Docker容器需要管理SELinux上下文,以避免兼容性問題。

為了最小化在不支持SELinux的情況下運行容器的風險,可以創建SELinux上下文策略。

為了更新SELinux上下文,可以使用現有的SCC作為起點生成一個新的SCC。

$ oc export scc restricted > custom_selinux.yml #導出默認的SCC

編輯導出的YAML文件以更改SCC名稱和SELinux上下文。

$ oc create -f yaml_file #使用修改后的ymal重新創建一個SCC

9.7 特權容器


有些容器可能需要訪問主機的運行時環境。S2I構建器容器需要訪問宿主docker守護進程來構建和運行容器。

例如,S2I構建器容器是一類特權容器,它要求訪問超出其自身容器的限制。這些容器可能會帶來安全風險,因為它們可以使用OpenShift節點上的任何資源。通過創建具有特權訪問權的服務帳戶,可以使用SCCs啟用特權容器的訪問。

十 資源訪問控制綜合實驗

10.1 前置準備


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

10.2 本練習準備

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

10.3 創建用戶

  1 [root@master ~]# htpasswd /etc/origin/master/htpasswd user-review
  2 New password: 【redhat】
  3 Re-type new password: 【redhat】


10.4 修改策略

  1 [student@workstation ~]$ oc login -u admin -p redhat
  2 [student@workstation ~]$ oc adm policy remove-cluster-role-from-group \
  3 self-provisioner system:authenticated system:authenticated:oauth
  4 禁用所有常規用戶的項目創建功能


10.5 確認驗證

  1 [student@workstation ~]$ oc login -u user-review -p redhat
  2 [student@workstation ~]$ oc new-project test			#普通用戶無法創建項目
  3 Error from server (Forbidden): You may not request a new project via this API.


10.6 創建項目

  1 [student@workstation ~]$ oc login -u admin -p redhat
  2 [student@workstation ~]$ oc new-project secure-review		#使用管理員創建項目


10.7 授權用戶

  1 [student@workstation ~]$ oc project secure-review
  2 Already on project "secure-review" on server "https://master.lab.example.com:443".
  3 [student@workstation ~]$ oc policy add-role-to-user edit user-review	#將edit的role和user-review進行關聯


10.8 測試訪問

  1 [student@workstation ~]$ oc login -u user-review -p redhat
  2 [student@workstation ~]$ oc project secure-review		#測試訪問
  3 Already on project "secure-review" on server "https://master.lab.example.com:443".


10.9 檢查模板

  1 [student@workstation ~]$ cd /home/student/DO280/labs/secure-review/
  2 [student@workstation secure-review]$ less mysql-ephemeral.yml





模板解讀:

該mysql-ephemeral.yml模板文件,包含openshift項目中的mysql臨時模板,pod所需的其他環境變量由模板參數初始化,並具有默認值。

但沒有secret定義,後續操作將手動創建模板所需的secret。

根據模板的要求,創建一個包含MySQL容器image使用的憑證的secret,將這個secret命名為mysql。

  • 應用程序訪問的數據庫用戶名由database-user定義。
  • 數據庫用戶的密碼由database-password定義。
  • 數據庫管理員密碼由database-root-password定義


使用user-review developer用戶創建一個名為mysql的secret。這個secret應該存儲用戶名mysql、密碼redhat和數據庫管理員密碼do280-admin。

數據庫用戶名由database-user定義。此用戶的密碼由mysql secret密鑰定義。

數據庫管理員密碼由database-root-password定義。

10.10 創建secret

  1 [student@workstation secure-review]$ oc create secret generic mysql \
  2 --from-literal='database-user'='mysql' \
  3 --from-literal='database-password'='redhat' \
  4 --from-literal='database-root-password'='do280-admin'
  5 [student@workstation secure-review]$ oc get secret mysql -o yaml	#確認驗證secret


10.11 部署應用

  1 [student@workstation secure-review]$ oc new-app --file=mysql-ephemeral.yml
  2 [student@workstation secure-review]$ oc get pods
  3 NAME            READY     STATUS    RESTARTS   AGE
  4 mysql-1-2lr7t   1/1       Running   0          31s


10.12 轉發端口

  1 [student@workstation ~]$ oc port-forward mysql-1-2lr7t 3306:3306

10.13 測試訪問

  1 [student@workstation ~]$ mysql -umysql -predhat -h127.0.0.1

10.14 部署phpmyadmin應用


使用內部倉庫registry.lab.example.com的image部署phpmyadmin:4.7容器。phpmyadmin:4.7容器需要名為PMA_HOST的環境變量來提供MySQL服務器的IP地址。

使用模板創建一個基於FQND的MySQL pod的service。

為使用模板創建的MySQL服務器pod使用服務FQDN,該模板是mysql.secure-review.svc.cluster.local。

  1 [student@workstation ~]$ oc new-app --name=phpmyadmin \
  2 --docker-image=registry.lab.example.com/phpmyadmin/phpmyadmin:4.7 \
  3 -e PMA_HOST=mysql.secure-review.svc.cluster.local






結論:該命令會發出警告,提示需要root特權。默認情況下,OpenShift不支持使用操作系統的root用戶運行容器。

10.15 查看pod

  1 [student@workstation ~]$ oc get pods
  2 NAME                 READY     STATUS    RESTARTS   AGE
  3 mysql-1-2lr7t        1/1       Running   0          8m
  4 phpmyadmin-1-v7tl7   0/1       Error     2          1m
  5 因為沒有root權限,因此部署失敗,需要提權。


10.16 授予權限

  1 [student@workstation ~]$ oc login -u admin -p redhat			#使用管理員登錄
  2 [student@workstation ~]$ oc create serviceaccount phpmyadmin-account	#首先創建服務賬戶
  3 [student@workstation ~]$ oc adm policy add-scc-to-user anyuid -z phpmyadmin-account
  4 scc "anyuid" added to: ["system:serviceaccount:secure-review:phpmyadmin-account"]	#將服務帳戶與anyuid安全上下文關聯


10.17 更新應用

  1 [student@workstation ~]$ oc patch dc phpmyadmin --patch='{"spec":{"template":{"spec":{"serviceAccountName": "phpmyadmin-account"}}}}'


#更新負責管理phpmyadmin的dc資源,任何開發人員用戶都可以執行此操作。

本環境中,相關操作命令可以從/home/student/DO280/labs/secure-review文件夾中的patch-dc.sh腳本運行或複製。

10.18 確認驗證

  1 [student@workstation ~]$ oc login -u user-review -p redhat
  2 [student@workstation ~]$ oc get pods			#確認pod是否正常
  3 NAME                 READY     STATUS    RESTARTS   AGE
  4 mysql-1-2lr7t        1/1       Running   0          13m
  5 phpmyadmin-2-bdjvq   1/1       Running   0          1m


10.19 暴露服務

  1 [student@workstation ~]$ oc expose svc phpmyadmin --hostname=phpmyadmin.apps.lab.example.com

10.20 訪問測試

  1 [student@workstation ~]$ curl -s http://phpmyadmin.apps.lab.example.com

10.21 確認及刪除

  1 [student@workstation ~]$ lab secure-review grade		#環境腳本判斷
  2 [student@workstation ~]$ oc login -u admin -p redhat
  3 [student@workstation ~]$ oc adm policy add-cluster-role-to-group \
  4 self-provisioner system:authenticated system:authenticated:oauth
  5 [student@workstation ~]$ oc delete project secure-review
  6 [student@workstation ~]$ ssh root@master htpasswd -D \
  7 /etc/origin/master/htpasswd user-review			#刪除用戶
  8 [student@workstation ~]$ oc delete user user-review		#刪除項目

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

分類
發燒車訊

Linux 進程間通信(IPC)總結

概述

一個大型的應用系統,往往需要眾多進程協作,進程(Linux進程概念見附1)間通信的重要性顯而易見。本系列文章闡述了 Linux 環境下的幾種主要進程間通信手段。

進程隔離

進程隔離是為保護操作系統中進程互不干擾而設計的一組不同硬件和軟件的技術。這個技術是為了避免進程A寫入進程B的情況發生。 進程的隔離實現,使用了虛擬地址空間。進程A的虛擬地址和進程B的虛擬地址不同,這樣就防止進程A將數據信息寫入進程B。

虛擬地址空間

當創建一個進程時,操作系統會為該進程分配一個 4GB 大小的虛擬進程地址空間。之所以是 4GB ,是因為在 32 位的操作系統中,一個指針長度是 4 字節,而 4 字節指針的尋址能力是從 0x00000000~0xFFFFFFFF ,最大值 0xFFFFFFFF 表示的即為 4GB 大小的容量。與虛擬地址空間相對的,還有一個物理地址空間,這個地址空間對應的是真實的物理內存。要注意的是這個 4GB 的地址空間是“虛擬”的,並不是真實存在的,而且每個進程只能訪問自己虛擬地址空間中的數據,無法訪問別的進程中的數據,通過這種方法實現了進程間的地址隔離。

針對 Linux 操作系統,將最高的1G字節(從虛擬地址 0xC0000000 到 0xFFFFFFFF )供內核使用,稱為內核空間,而較低的 3G 字節(從虛擬地址 0x00000000 到0xBFFFFFFF),供各個進程使用,稱為用戶空間。每個進程都可以通過系統調用進入到內核。其中在 Linux 系統中,進程的用戶空間是獨立的,而內核空間是共有的,進程切換時,用戶空間切換,內核空間不變

創建虛擬地址空間目的是為了解決進程地址空間隔離的問題。但程序要想執行,必須運行在真實的內存上,所以,必須在虛擬地址與物理地址間建立一種映射關係。這樣,通過映射機制,當程序訪問虛擬地址空間上的某個地址值時,就相當於訪問了物理地址空間中的另一個值。人們想到了一種分段、分頁的方法,它的思想是在虛擬地址空間和物理地址空間之間做一一映射。這種思想理解起來並不難,操作系統保證不同進程的地址空間被映射到物理地址空間中不同的區域上,這樣每個進程最終訪問到的物理地址空間都是彼此分開的。通過這種方式,就實現了進程間的地址隔離。

 

系統調用/內核態/用戶態

雖然從邏輯上抽離出用戶空間和內核空間;但是不可避免的的是,總有那麼一些用戶空間需要訪問內核的資源;比如應用程序訪問文件,網絡是很常見的事情,怎麼辦呢?

用戶空間訪問內核空間的唯一方式就是系統調用;通過這個統一入口接口,所有的資源訪問都是在內核的控制下執行,以免導致對用戶程序對系統資源的越權訪問,從而保障了系統的安全和穩定。用戶軟件良莠不齊,要是它們亂搞把系統玩壞了怎麼辦?因此對於某些特權操作必須交給安全可靠的內核來執行。

當一個任務(進程)執行系統調用而陷入內核代碼中執行時,我們就稱進程處於內核運行態(或簡稱為內核態)此時處理器處於特權級最高的(0級)內核代碼中執行。當進程在執行用戶自己的代碼時,則稱其處於用戶運行態(用戶態)。即此時處理器在特權級最低的(3級)用戶代碼中運行。處理器在特權等級高的時候才能執行那些特權CPU指令。

IPC 通信原理

理解了上面的幾個概念,我們再來看看進程之間是如何實現通信的。

通常的做法是消息發送方將要發送的數據存放在內存緩存區中,通過系統調用進入內核態。然後內核程序在內核空間分配內存,開闢一塊內核緩存區,調用 copy_from_user() 函數將數據從用戶空間的內存緩存區拷貝到內核空間的內核緩存區中。同樣的,接收方進程在接收數據時在自己的用戶空間開闢一塊內存緩存區,然後內核程序調用 copy_to_user() 函數將數據從內核緩存區拷貝到接收進程的內存緩存區。這樣數據發送方進程和數據接收方進程就完成了一次數據傳輸,我們稱完成了一次進程間通信。如下圖:

進程間通信方式

Linux 進程間基本的通信方式主要有:管道(pipe) (包括匿名管道和命名管道)、信號(signal)、消息隊列(queue)、共享內存、信號量和套接字。

管道

管道的實質是一個內核緩衝區(調用 pipe 函數來開闢),管道的作用正如其名,需要通信的兩個進程在管道的兩端,進程利用管道傳遞信息。管道對於管道兩端的進程而言,就是一個文件,但是這個文件比較特殊,它不屬於文件系統並且只存在於內存中。 Linux一切皆文件,操作系統為管道提供操作的方法:文件操作,用 fork 來共享管道原理。

管道依據是否有名字分為匿名管道和命名管道(有名管道),這兩種管道有一定的區別。

匿名管道有幾個重要的限制:

  1. 管道是半雙工的,數據只能在一個方向上流動,A進程傳給B進程,不能反向傳遞
  2. 管道只能用於父子進程或兄弟進程之間的通信,即具有親緣關係的進程。

命名管道允許沒有親緣關係的進程進行通信。命名管道不同於匿名管道之處在於它提供了一個路徑名與之關聯,這樣一個進程即使與創建有名管道的進程不存在親緣關係,只要可以訪問該路徑,就能通過有名管道互相通信。

pipe 函數接受一個參數,是包含兩個整數的數組,如果調用成功,會通過 pipefd[2] 傳出給用戶程序兩個文件描述符,需要注意 pipefd[0] 指向管道的讀端, pipefd[1] 指向管道的寫端,那麼此時這個管道對於用戶程序就是一個文件,可以通過 read(pipefd [0]);或者 write(pipefd [1]) 進行操作。pipe 函數調用成功返回 0,否則返回 -1.

那麼再來看看通過管道進行通信的步驟:

  • 父進程創建管道,得到兩個文件描述符指向管道的兩端
  • 利用fork函數創建齣子進程,則子進程也得到兩個文件描述符指向同一管道
  • 父進程關閉讀端(pipe[0]),子進程關閉寫端pipe[1],則此時父進程可以往管道中進行寫操作,子進程可以從管道中讀,從而實現了通過管道的進程間通信。

     

管道的特點:

  • 只能單向通信

兩個文件描述符,用一個,另一個不用,不用的文件描述符就要 close

  • 只能血緣關係的進程進行通信

  • 依賴於文件系統

  • 生命周期隨進程

  • 面向字節流的服務

面向字節流:數據無規則,沒有明顯邊界,收發數據比較靈活:對於用戶態,可以一次性發送也可以分次發送,當然接受數據也如此;而面向數據報:數據有明顯邊界,數據只能整條接受 

  • 管道內部提供了同步機制

臨界資源: 大家都能訪問到的共享資源

臨界區: 對臨界資源進行操作的代碼

同步: 臨界資源訪問的可控時序性(一個操作完另一個才可以操作)

互斥: 對臨界資源同一時間的唯一訪問性(保護臨界資源安全)

說明:因為管道通信是單向的,在上面的例子中我們是通過子進程寫父進程來讀,如果想要同時父進程寫而子進程來讀,就需要再打開另外的管道;

管道的讀寫端通過打開的文件描述符來傳遞,因此要通信的兩個進程必須從它們的公共祖先那裡繼承管道的件描述符。 上面的例子是父進程把文件描述符傳給子進程之後父子進程之 間通信,也可以父進程fork兩次,把文件描述符傳給兩個子進程,然後兩個子進程之間通信, 總之 需要通過fork傳遞文件描述符使兩個進程都能訪問同一管道,它們才能通信。

四個特殊情況:

  1. 如果所有指向管道寫端的文件描述符都關閉了,而仍然有進程從管道的讀端讀數據,那麼管道中剩餘的數據都被讀取后,再次read會返回0,就像讀到文件末尾一樣

  2. 如果有指向管道寫端的文件描述符沒關閉,而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那麼管道中剩餘的數據都被讀取后,再次read會阻塞,直到管道中有數據可讀了才讀取數據並返回。

  3. 如果所有指向管道讀端的文件描述符都關閉了,這時有進程指向管道的寫端write,那麼該進程會收到信號SIGPIPE,通常會導致進程異常終止。

  4. 如果有指向管道讀端的文件描述符沒關閉,而持有管道寫端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那麼在管道被寫滿時再write會阻塞,直到管道中有空位置了才寫入數據並返回。

命名管道FIFO

在管道中,只有具有血緣關係的進程才能進行通信,對於後來的命名管道,就解決了這個問題。FIFO 不同於管道之處在於它提供一個路徑名與之關聯,以 FIFO 的文件形式存儲於文件系統中。命名管道是一個設備文件,因此,即使進程與創建FIFO的進程不存在親緣關係,只要可以訪問該路徑,就能夠通過 FIFO 相互通信。值得注意的是, FIFO (first input first output) 總是按照先進先出的原則工作,第一個被寫入的數據將首先從管道中讀出。

命名管道的創建

創建命名管道的系統函數有兩個: mknod 和 mkfifo。兩個函數均定義在頭文件 sys/stat.h,
函數原型如下:

#include <sys/types.h>
#include <sys/stat.h>
int mknod(const char *path,mode_t mod,dev_t dev);
int mkfifo(const char *path,mode_t mode);

函數 mknod 參數中 path 為創建的命名管道的全路徑名: mod 為創建的命名管道的模指明其存取權限; dev 為設備值,該值取決於文件創建的種類,它只在創建設備文件時才會用到。這兩個函數調用成功都返回 0,失敗都返回 -1。

命名管道打開特性:

  1. 如果用只讀打開命名管道,open 函數將阻塞等待直至有其他進程以寫的方式打開這個命名管道,如果沒有進程以寫的方式發開這個命名管道,程序將停在此處

  2. 如果用只寫打開命名管道,open 函數將阻塞等到直至有其他進程以讀的方式打開這個命名管道,如果沒有進程以讀的方式發開這個命名管道,程序將停在此處;

  3. 如果用讀寫打開命名管道,則不會阻塞(但是管道是單向)

System V IPC   

IPC(Inter-Process Communication)是指多個進程之間相互通信,交換信息的方法,System V 是 Unix 操作系統最早的商業發行版,由 AT&T(American Telephone & Telegraph)開發。System V IPC 是指 Linux 引入自 System V 的進程通信機制,一共有三種:

  • 信號量,用來管理對共享資源的訪問;

  • 共享內存,用來高效地實現進程間的數據共享;

  • 消息隊列,用來實現進程間數據的傳遞。

這三種統稱 IPC 資源,每個 IPC 資源都是請求時動態創建的,都是永駐內存,除非被進程显示釋放,都是可以被任一進程使用。每個 IPC 資源都使用一個 32 位的 IPC 關鍵字和 32 位的 IPC 標識符,前者類似文件系統中的路徑名,由程序自由定製,後者類似打開文件的文件描述符,由內核統一分配,在系統內部是唯一的,當多個進程使用同一個IPC資源通信時需要該資源的 IPC 標識符。     

創建新的 IPC 資源時需要指定 IPC 關鍵字,如果沒有與之關聯的 IPC 資源,則創建一個新的 IPC 資源;如果已經存在,則判斷當前進程是否具有訪問權限,是否超過資源使用限制等,如果符合條件則返回該資源的 IPC 標識符。為了避免兩個不同的 IPC 資源使用相同的 IPC 關鍵字,創建時可以指定IPC關鍵字為 IPC_PRIVATE,由內核負責生成一個唯一的關鍵字。   

創建新的 IPC 資源時最後一個參數可以包括三個標誌,PC_CREAT 說明如果IPC資源不存在則必須創建它,IPC_EXCL 說明如果資源已經存在且設置了 PC_CREAT 標誌則創建失敗,IPC_NOWAIT 說明訪問 IPC 資源時進程從不阻塞。 

信號量

信號量(semaphore)是一種用於提供不同進程之間或者一個給定的不同線程間同步手段的原語。信號量多用於進程間的同步與互斥,簡單的說一下同步和互斥的意思:

同步:處理競爭就是同步,安排進程執行的先後順序就是同步,每個進程都有一定的先後執行順序。

互斥:互斥訪問不可共享的臨界資源,同時會引發兩個新的控制問題(互斥可以說是特殊的同步)。

競爭:當併發進程競爭使用同一個資源的時候,我們就稱為競爭進程。

共享資源通常分為兩類:一類是互斥共享資源,即任一時刻只允許一個進程訪問該資源;另一類是同步共享資源,即同一時刻允許多個進程訪問該資源;信號量是解決互斥共享資源的同步問題而引入的機制。

下面說一下信號量的工作機制,可以直接理解成計數器(當然其實加鎖的時候肯定不能這麼簡單,不只只是信號量了),信號量會有初值(>0),每當有進程申請使用信號量,通過一個 P 操作來對信號量進行-1操作,當計數器減到 0 的時候就說明沒有資源了,其他進程要想訪問就必須等待(具體怎麼等還有說法,比如忙等待或者睡眠),當該進程執行完這段工作(我們稱之為臨界區)之後,就會執行 V 操作來對信號量進行 +1 操作。

  • 臨界區:臨界區指的是一個訪問共用資源(例如:共用設備或是共用存儲器)的程序片段,而這些共用資源又無法同時被多個線程訪問的特性。

  • 臨界資源:只能被一個進程同時使用(不可以多個進程共享),要用到互斥。

我們可以說信號量也是進程間通信的一種方式,比如互斥鎖的簡單實現就是信號量,一個進程使用互斥鎖,並通知(通信)其他想要該互斥鎖的進程,阻止他們的訪問和使用。

當有進程要求使用共享資源時,需要執行以下操作:

  1. 系統首先要檢測該資源的信號量;

  2. 若該資源的信號量值大於 0,則進程可以使用該資源,此時,進程將該資源的信號量值減1;

  3. 若該資源的信號量值為 0,則進程進入休眠狀態,直到信號量值大於 0 時進程被喚醒,訪問該資源;

       當進程不再使用由一個信號量控制的共享資源時,該信號量值增加 1,如果此時有進程處於休眠狀態等待此信號量,則該進程會被喚醒

每個信號量集都有一個與其相對應的結構,該結構定義如下:

/* Data structure describing a set of semaphores.  */  
struct semid_ds  
{  
    struct ipc_perm sem_perm;   /* operation permission struct */  
    struct sem *sem_base;       /* ptr to array of semaphores in set */  
    unsigned short sem_nsems;   /* # of semaphores in set */  
    time_t sem_otime;           /* last-semop() time */  
    time_t sem_ctime;           /* last-change time */  
};  
  
/* Data structure describing each of semaphores.  */  
struct sem  
{  
    unsigned short semval;  /* semaphore value, always >= 0 */  
    pid_t          sempid;  /* pid for last successful semop(), SETVAL, SETALL */  
    unsigned short semncnt; /* # processes awaiting semval > curval */  
    unsigned short semzcnt; /* # processes awaiting semval == 0 */  
};  

信號量集的結構圖如下所示:

消息隊列

消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列 ID)來標識。其具有以下特點:

  1. 消息隊列是面向記錄的,其中的消息具有特定的格式以及特定的優先級。

  2. 消息隊列獨立於發送與接收進程。進程終止時,消息隊列及其內容並不會被刪除。

  3. 消息隊列可以實現消息的隨機查詢,消息不一定要以先進先出的次序讀取,也可以按消息的類型讀取。

原型

1 #include <sys/msg.h>
2 // 創建或打開消息隊列:成功返回隊列ID,失敗返回-1
3 int msgget(key_t key, int flag);
4 // 添加消息:成功返回0,失敗返回-1
5 int msgsnd(int msqid, const void *ptr, size_t size, int flag);
6 // 讀取消息:成功返回消息數據的長度,失敗返回-1
7 int msgrcv(int msqid, void *ptr, size_t size, long type,int flag);
8 // 控制消息隊列:成功返回0,失敗返回-1
9 int msgctl(int msqid, int cmd, struct msqid_ds *buf);

在以下兩種情況下,msgget 將創建一個新的消息隊列:

  • 如果沒有與鍵值key相對應的消息隊列,並且flag中包含了IPC_CREAT標誌位。

  • key參數為IPC_PRIVATE

函數msgrcv在讀取消息隊列時,type參數有下面幾種情況:

  • type == 0,返回隊列中的第一個消息;

  • type > 0,返回隊列中消息類型為 type 的第一個消息;

  • type < 0,返回隊列中消息類型值小於或等於 type 絕對值的消息,如果有多個,則取類型值最小的消息。

可以看出,type 值非 0 時用於以非先進先出次序讀消息。也可以把 type 看做優先級的權值。

共享內存

共享內存是 System V 版本的最後一個進程間通信方式。共享內存,顧名思義就是允許兩個不相關的進程訪問同一個邏輯內存,共享內存是兩個正在運行的進程之間共享和傳遞數據的一種非常有效的方式。不同進程之間共享的內存通常為同一段物理內存。進程可以將同一段物理內存連接到他們自己的地址空間中,所有的進程都可以訪問共享內存中的地址。如果某個進程向共享內存寫入數據,所做的改動將立即影響到可以訪問同一段共享內存的任何其他進程。

特別提醒:共享內存並未提供同步機制,也就是說,在第一個進程結束對共享內存的寫操作之前,並無自動機制可以阻止第二個進程開始對它進行讀取,所以我們通常需要用其他的機制來同步對共享內存的訪問,例如信號量。

共享內存的通信原理
在 Linux 中,每個進程都有屬於自己的進程控制塊(PCB)和地址空間(Addr Space),並且都有一個與之對應的頁表,負責將進程的虛擬地址與物理地址進行映射,通過內存管理單元(MMU)進行管理。兩個不同的虛擬地址通過頁表映射到物理空間的同一區域,它們所指向的這塊區域即共享內存。

共享內存的通信原理示意圖:

 

對於上圖我的理解是:當兩個進程通過頁表將虛擬地址映射到物理地址時,在物理地址中有一塊共同的內存區,即共享內存,這塊內存可以被兩個進程同時看到。這樣當一個進程進行寫操作,另一個進程讀操作就可以實現進程間通信。但是,我們要確保一個進程在寫的時候不能被讀,因此我們使用信號量來實現同步與互斥。

對於一個共享內存,實現採用的是引用計數的原理,當進程脫離共享存儲區后,計數器減一,掛架成功時,計數器加一,只有當計數器變為零時,才能被刪除。當進程終止時,它所附加的共享存儲區都會自動脫離。

為什麼共享內存速度最快?

藉助上圖說明:Proc A 進程給內存中寫數據, Proc B 進程從內存中讀取數據,在此期間一共發生了兩次複製

(1)Proc A 到共享內存       (2)共享內存到 Proc B

因為直接在內存上操作,所以共享內存的速度也就提高了。

共享內存的接口函數以及指令

查看系統中的共享存儲段

ipcs -m

刪除系統中的共享存儲段

ipcrm -m [shmid]

shmget ( ):創建共享內存

int shmget(key_t key, size_t size, int shmflg);

[參數key]:由ftok生成的key標識,標識系統的唯一IPC資源。

[參數size]:需要申請共享內存的大小。在操作系統中,申請內存的最小單位為頁,一頁是4k字節,為了避免內存碎片,我們一般申請的內存大小為頁的整數倍。

[參數shmflg]:如果要創建新的共享內存,需要使用IPC_CREAT,IPC_EXCL,如果是已經存在的,可以使用IPC_CREAT或直接傳0。

[返回值]:成功時返回一個新建或已經存在的的共享內存標識符,取決於shmflg的參數。失敗返回-1並設置錯誤碼。

shmat ( ):掛接共享內存

void *shmat(int shmid, const void *shmaddr, int shmflg);

[參數shmid]:共享存儲段的標識符。

[參數*shmaddr]:shmaddr = 0,則存儲段連接到由內核選擇的第一個可以地址上(推薦使用)。

[參數shmflg]:若指定了SHM_RDONLY位,則以只讀方式連接此段,否則以讀寫方式連接此段。

[返回值]:成功返回共享存儲段的指針(虛擬地址),並且內核將使其與該共享存儲段相關的shmid_ds結構中的shm_nattch計數器加1(類似於引用計數);出錯返回-1。

shmdt ( ):去關聯共享內存:當一個進程不需要共享內存的時候,就需要去關聯。該函數並不刪除所指定的共享內存區,而是將之前用shmat函數連接好的共享內存區脫離目前的進程。

int shmdt(const void *shmaddr);

[參數*shmaddr]:連接以後返回的地址。

[返回值]:成功返回0,並將shmid_ds結構體中的 shm_nattch計數器減1;出錯返回-1。

shmctl ( ):銷毀共享內存

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

[參數shmid]:共享存儲段標識符。

[參數cmd]:指定的執行操作,設置為IPC_RMID時表示可以刪除共享內存。

[參數*buf]:設置為NULL即可。

[返回值]:成功返回0,失敗返回-1。

POSIX 消息隊列   

POSIX 消息隊列是 POSIX 標準在 2001 年定義的一種 IPC 機制,與 System V 中的消息隊列相比有如下差異:

  • 更簡單的基於文件的應用接口,Linux 通過 mqueue 的特殊文件系統來實現消息隊列,隊列名跟文件名類似,必須以”/”開頭,每個消息隊列在文件系統內都有一個對應的索引節點,返回的隊列描述符實際是一個文件描述符

  • 完全支持消息優先級,消息在隊列中是按照優先級倒序排列的(即0表示優先級最低)。當一條消息被添加到隊列中時,它會被放置在隊列中具有相同優先級的所有消息之後。如果一個應用程序無需使用消息優先級,那麼只需要將msg_prio指定為0即可。

  • 完全支持消息到達的異步通知,當新消息到達且當前隊列為空時會通知之前註冊過表示接受通知的進程。在任何一個時刻都只有一個進程能夠向一個特定的消息隊列註冊接收通知。如果一個消息隊列上已經存在註冊進程了,那麼後續在該隊列上的註冊請求將會失敗。可以給進程發送信號或者另起一個線程調用通知函數完成通知。當通知完成時,註冊即被撤銷,進程需要繼續接受通知則必須重新註冊。

  • 用於阻塞發送與接收操作的超時機制,可以指定阻塞的最長時間,超時自動返回 

套接字:

套接字是更為基礎的進程間通信機制,與其他方式不同的是,套接字可用於不同機器之間的進程間通信。

有兩種類型的套接字:基於文件的和面向網絡的。

  • Unix 套接字是基於文件的,並且擁有一個“家族名字”–AF_UNIX,它代表地址家族 (address family):UNIX。

  • 第二類型的套接字是基於網絡的,它也有自己的家族名字–AF_INET,代表地址家族 (address family):INTERNET

不管採用哪種地址家族,都有兩種不同的套接字連接:面向連接的和無連接的。

  • 面向連接的套接字 (SOCK_STREAM):進行通信前必須建立一個連接,面向連接的通信提供序列化的、可靠地和不重複的數據交付,而沒有記錄邊界。

這意味着每條信息可以被拆分成多個片段,並且每個片段都能確保到達目的地,然後在目的地將信息拼接起來。

實現這種連接類型的主要協議是傳輸控制協議 (TCP)。

  • 無連接的套接字 (SOCK_DGRAM):在通信開始之前並不需要建立連接,在數據傳輸過程中並無法保證它的順序性、可靠性或重複性。

然而,數據報確實保存了記錄邊界,這就意味着消息是以整體發送的,而並非首先分成多個片段。

由於面向連接的套接字所提供的保證,因此它們的設置以及對虛擬電路連接的維護需要大量的開銷。然而,數據報不需要這些開銷,即它的成本更加“低廉”

實現這種連接類型的主要協議是用戶數據報協議 (UDP)。

信號

信號是軟件層次上對中斷機制的一種模擬,是一種異步通信方式,進程不必通過任何操作來等待信號的到達。信號可以在用戶空間進程和內核之間直接交互,內核可以利用信號來通知用戶空間的進程發生了哪些系統事件。

信號來源:

信號事件的發生有兩個來源:硬件來源,比如我們按下了鍵盤或者其它硬件故障;軟件來源,最常用發送信號的系統函數是 kill, raise, alarm 和 setitimer 以及 sigqueue 函數,軟件來源還包括一些非法運算等操作。

進程對信號的響應:

進程可以通過三種方式來響應信號:

  • 忽略信號,即對信號不做任何處理,但是有兩個信號是不能忽略的:SIGKLL 和 SIGSTOP;

  • 捕捉信號,定義信號處理函數,當信號發生時,執行相應的處理函數;

  • 執行缺省操作,Linux 對每種信號都規定了默認操作。

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

【K8s學習筆記】K8s是如何部署應用的?

本文內容

本文致力於介紹K8s一些基礎概念與串聯部署應用的主體流程,使用Minikube實操

基礎架構概念回顧

溫故而知新,上一節【K8S學習筆記】初識K8S 及架構組件 我們學習了K8s的發展歷史、基礎架構概念及用途,本節講的內容建立在其上,有必要把之前的架構小節提出來回顧下:

K8s架構分為控制平台(位於的Master節點)與執行節點Node

控制平台包含:

  • kube-apiserver(訪問入口,接收命令)

  • etcd(KV數據庫,保存集群狀態與數據)

  • kube-scheduler(監控節點狀態,調度容器部署)

  • kube-controller-manager(監控集群狀態,控制節點、副本、端點、賬戶與令牌)

  • cloud-controller-manager(控制與雲交互的節點、路由、服務、數據卷)

執行節點包含:

  • kubelet(監控與實際操作容器)

  • kube-proxy(每個節點上運行的網絡代理,維護網絡轉發規則,實現了Service)

  • 容器運行時環境CRI(支持多種實現K8s CRI的容器技術)

接下來需要引入 Pod 與 Service 的概念,這也是在上一篇文章中未給出的

Pod、Service與Label概念

Pod概念與結構

Pod 是 K8s最重要的基本概念,官網給出概念:Pod是Kubernates可調度的最小的、可部署的單元。怎麼理解呢?最簡單的理解是,Pod是一組容器。

再詳細些,Pod是一組容器組成的概念,這些容器都有共同的特點:

  • 都有一個特殊的被稱為“根容器”的Pause容器。Pause容器鏡像屬於K8s平台的一部分
  • 包含一個或多個緊密相關的用戶業務容器。假設個場景:業務容器需要獨立的redis提供服務,這裏就可以將它們兩個部署在同一Pod中。

下邊是Pod的組成示意圖:

為什麼Kubernetes會設計出一個全新的概念與Pod會有這樣特殊的結構呢?

  • 原因之一:K8s需要將一組容器視為一個單元處理。當其中某些容器死亡了,此時無法判定這組容器的狀態。而當成一個單元,只要其中有一個容器掛了,這個單元就判定掛了。
  • 原因之二:通過Pause共享容器IP,共享Pause掛接的Volume,簡化密切關聯的業務容器之間的通信問題和文件共享問題

K8s為每個Pod都分配了唯一的IP地址,稱為Pod IP,一個Pod里的多個容器共享Pod IP地址。需要牢記的一點是:在 kubernetes 里,一個 Pod 里的容器與另外主機上的 Pod 容器能夠直接通信。

Pod的創建流程

當一個普通的Pod被創建,就會被放入etcd中存儲,隨後被 K8s Master節點調度到某個具體的Node上並進行綁定(Binding),隨後該Pod被對應的Node上的kubelet進程實例化成一組相關的Docker容器並啟動。

當Pod中有容器停止時,K8s 會自動檢測到這個問題並重啟這個 Pod(Pod里所有容器);如果Pod所在的Node宕機,就會將這個Node上的所有Pod重新調度到其他節點上。

細心的讀者是否發現:

當Pod越來越多,Pod IP 也越來越多,那是如何從茫茫IP地址中找到需要的服務呢?換句話來說,是否有一種提供唯一入口的機制,來完成對Pod的訪問,而無需關心訪問到具體是哪個Pod(who care :happy:)?

Kubernetes 提供了這種機制,稱之為 Service。

Service概念

Service服務是Kubernetes里的核心資源對象之一,從名稱上來看,理解其為一個”服務“也不為過,Service的作用是為相同標識的一系列Pod提供唯一的訪問地址。

Service使用的唯一地址稱為ClusterIP,僅當在集群內的容器才能通過此IP訪問到Service

它具體實現對一系列Pod綁定,需要再引入Label的概念,才能做出解答。

Label概念

Kubernetes 提供了Label(標籤)來對Pod、Service、RC、Node等進行標記。相當於打標籤,Label可以是一組KV鍵值對,也可以是一個set

一個資源對象可以定義任意數量的Label,同一個Label可以添加到任意數量的資源對象上。通常由定義資源對象時添加,創建后亦可動態添加或刪除。

Service如何動態綁定Pods?

原來,在定義 Pod 時,設置一系列 Label 標籤,Service 創建時定義了 Label Selector(Label Selector 可以類比為對 Label 進行 SQL 的 where 查詢),kube-proxy 進程通過 Service的Label Selector 來選擇對應的 Pod,自動建立每個 Service 到對應 Pod 的請求轉發路由表,從而實現 Service 的智能負載均衡機制。

小結:Pod是K8s最小的執行單元,包含一個Pause容器與多個業務容器,每個Pod中容器共享Pod IP,容器之間可直接作用Pod IP通信;Label是一種標籤,它將標籤打在Pod上時,Service可以通過定義Label Selector(Label查詢規則)來選擇Pod,具體實現路由表建立的是kube-proxy

部署應用實踐(Minikube)

安裝kubectl需要安裝成本地服務,這裡是debian10,更多參考https://developer.aliyun.com/mirror/kubernetes

sudo su -
apt-get update && apt-get install -y apt-transport-https
curl https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | apt-key add - 
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main
EOF  
apt-get update
apt-get install -y kubelet kubectl
exit

下載安裝Minikube(阿里雲修改版):

curl -Lo minikube-linux-amd64-1.11.0-aliyuncs http://kubernetes.oss-cn-hangzhou.aliyuncs.com/minikube/releases/v1.11.0/minikube-linux-amd64
sudo install minikube-linux-amd64-1.11.0-aliyuncs /usr/local/bin/minikube

使用魔改版是因為官方代碼里有些地方寫死了地址,而國內無法訪問

部署k8s集群:

minikube start --driver docker --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers --kubernetes-version v1.18.3

本地有docker時使用此driver,其他可選的虛擬化技術參考https://minikube.sigs.k8s.io/docs/drivers/ 選擇

#部署一個Pod,Pod的deployment是一種默認的無狀態多備份的部署類型
kubectl create deployment hello-minikube --image=registry.cn-hangzhou.aliyuncs.com/google_containers/echoserver:1.4
#查看集群中當前的Pod列表
kubectl get pods
#創建的NodePort類型Service,將所有Node開放一個端口號,通過這個端口將流量轉發到Service以及下游Pods
kubectl expose deployment hello-minikube --type=NodePort --port=8080
#獲取暴露 Service 的 URL 以查看 Service 的詳細信息:
minikube service hello-minikube --url
#minikube提供的特色功能,直接通過瀏覽器打開剛才創建的Service的外部訪問地址
minikube service hello-minikube

自動打開瀏覽器訪問服務(Minikube特色功能)

提示:這張圖中的request-uri的解釋是不正確的,但是與 <minikube這個唯一Node的IP>:<NodePort><Service的ClusterIP>:<ServicePort>都不是完全匹配的,不大理解這塊提示,有緣人希望可解我所惑,在此先行感謝

查看Pod的描述信息

kubectl describe pod hello-minikube

最下方可以清楚得看到K8s集群default-scheduler成功指派我們要求的Pod在節點minikube上創建,kubelet收到信息后,拉取鏡像並啟動了容器

部署流程原理簡述

  1. kubectl 發送創建 deployment 類型、名為hello-minikube的Pod 請求到 kube-apiserver
  2. kube-apiserver 將這條描述信息存到 etcd
  3. kube-scheduler 監控 etcd 得到描述信息,監測Node負載情況,創建Pod部署描述信息到etcd
  4. 待部署Node的kubelet監聽到 etcd 中有自己的部署Pod信息,取出信息並按圖拉取業務鏡像,創建pause容器與業務容器(此時集群外部無法訪問)
  5. kubectl 執行expose命令,同時將創建Service與NodePort信息存到etcd中
  6. 各節點kube-proxy監聽到etcd變化,創建邏輯上的Service與路由信息,開闢NodePort
  7. 當請求到達<任意Node節點IP>:<NodePort> 時,根據路由錶轉發到指定的Service上,負載均衡到Pod,提供服務

集群外部訪問:

參考

  • https://kubernetes.io/docs/concepts/workloads/pods/pod-overview/
  • https://kubernetes.io/docs/concepts/services-networking/
  • https://minikube.sigs.k8s.io/docs/start/
  • https://minikube.sigs.k8s.io/docs/handbook/controls/
  • 《Kubernetes權威指南》第 4 版

行文過程中難免出現錯誤,還請讀者評論幫忙改正,大家共同進步,在此感謝

轉載請註明出處,文章中概念引入《Kubernetes權威指南》很多,侵權改。

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

【其他文章推薦】

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

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

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

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

async/await剖析

async/await剖析

JavaScript是單線程的,為了避免同步阻塞可能會帶來的一些負面影響,引入了異步非阻塞機制,而對於異步執行的解決方案從最早的回調函數,到ES6Promise對象以及Generator函數,每次都有所改進,但是卻又美中不足,他們都有額外的複雜性,都需要理解抽象的底層運行機制,直到在ES7中引入了async/await,他可以簡化使用多個Promise時的同步行為,在編程的時候甚至都不需要關心這個操作是否為異步操作。

分析

首先使用async/await執行一組異步操作,並不需要回調嵌套也不需要寫多個then方法,在使用上甚至覺得這本身就是一個同步操作,當然在正式使用上應該將await語句放置於 try...catch代碼塊中,因為await命令後面的Promise對象,運行結果可能是rejected

function promise(){
    return new Promise((resolve, reject) => {
       var rand = Math.random() * 2; 
       setTimeout(() => resolve(rand), 1000);
    });
}

async function asyncFunct(){
    var r1 = await promise();
    console.log(1, r1);
    var r2 = await promise();
    console.log(2, r2);
    var r3 = await promise();
    console.log(3, r3);
}

asyncFunct();

async/await實際上是Generator函數的語法糖,如Promises類似於結構化回調,async/await在實現上結合了Generator函數與Promise函數,下面使用Generator函數加Thunk函數的形式實現一個與上邊相同的例子,可以看到只是將async替換成了*放置在函數右端,並將await替換成了yield,所以說async/await實際上是Generator函數的語法糖,此處唯一不同的地方在於實現了一個流程的自動管理函數run,而async/await內置了執行器,關於這個例子的實現下邊會詳述。對比來看,asyncawait,比起*yield,語義更清楚,async表示函數里有異步操作,await表示緊跟在後面的表達式需要等待結果。

function thunkFunct(index){
    return function f(funct){
        var rand = Math.random() * 2;
        setTimeout(() => funct(rand), 1000)
    }
}

function* generator(){ 
    var r1 = yield thunkFunct();
    console.log(1, r1);
    var r2 = yield thunkFunct();
    console.log(2, r2);
    var r3 = yield thunkFunct();
    console.log(3, r3);
}

function run(generator){
    var g = generator();

    var next = function(data){
        var res = g.next(data);
        if(res.done) return ;
        // console.log(res.value);
        res.value(next);
    }

    next();
}

run(generator);

實現

async函數內置了執行器,能夠實現函數執行的自動流程管理,通過Generator yield ThunkGenerator yield Promise實現一個自動流程管理,只需要編寫Generator函數以及Thunk函數或者Promise對象並傳入自執行函數,就可以實現類似於async/await的效果。

Generator yield Thunk

自動流程管理run函數,首先需要知道在調用next()方法時,如果傳入了參數,那麼這個參數會傳給上一條執行的yield語句左邊的變量,在這個函數中,第一次執行next時並未傳遞參數,而且在第一個yield上邊也並不存在接收變量的語句,無需傳遞參數,接下來就是判斷是否執行完這個生成器函數,在這裏並沒有執行完,那麼將自定義的next函數傳入res.value中,這裏需要注意res.value是一個函數,可以在下邊的例子中將註釋的那一行執行,然後就可以看到這個值是f(funct){...},此時我們將自定義的next函數傳遞后,就將next的執行權限交予了f這個函數,在這個函數執行完異步任務后,會執行回調函數,在這個回調函數中會觸發生成器的下一個next方法,並且這個next方法是傳遞了參數的,上文提到傳入參數後會將其傳遞給上一條執行的yield語句左邊的變量,那麼在這一次執行中會將這個參數值傳遞給r1,然後在繼續執行next,不斷往複,直到生成器函數結束運行,這樣就實現了流程的自動管理。

function thunkFunct(index){
    return function f(funct){
        var rand = Math.random() * 2;
        setTimeout(() => funct(rand), 1000)
    }
}

function* generator(){ 
    var r1 = yield thunkFunct();
    console.log(1, r1);
    var r2 = yield thunkFunct();
    console.log(2, r2);
    var r3 = yield thunkFunct();
    console.log(3, r3);
}

function run(generator){
    var g = generator();

    var next = function(data){
        var res = g.next(data);
        if(res.done) return ;
        // console.log(res.value);
        res.value(next);
    }

    next();
}

run(generator);

Generator yield Promise

相對於使用Thunk函數來做流程自動管理,使用Promise來實現相對更加簡單,Promise實例能夠知道上一次回調什麼時候執行,通過then方法啟動下一個yield,不斷繼續執行,這樣就實現了流程的自動管理。

function promise(){
    return new Promise((resolve,reject) => {
        var rand = Math.random() * 2;
        setTimeout( () => resolve(rand), 1000);
    })
}

function* generator(){ 
    var r1 = yield promise();
    console.log(1, r1);
    var r2 = yield promise();
    console.log(2, r2);
    var r3 = yield promise();
    console.log(3, r3);
}

function run(generator){
    var g = generator();

    var next = function(data){
        var res = g.next(data);
        if(res.done) return ;
        res.value.then(data => next(data));
    }

    next();
}

run(generator);
// 比較完整的流程自動管理函數
function promise(){
    return new Promise((resolve,reject) => {
        var rand = Math.random() * 2;
        setTimeout( () => resolve(rand), 1000);
    })
}

function* generator(){ 
    var r1 = yield promise();
    console.log(1, r1);
    var r2 = yield promise();
    console.log(2, r2);
    var r3 = yield promise();
    console.log(3, r3);
}

function run(generator){
    return new Promise((resolve, reject) => {
        var g = generator();
        
        var next = function(data){
            var res = null;
            try{
                res = g.next(data);
            }catch(e){
                return reject(e);
            }
            if(!res) return reject(null);
            if(res.done) return resolve(res.value);
            Promise.resolve(res.value).then(data => {
                next(data);
            },(e) => {
                throw new Error(e);
            });
        }
        
        next();
    })
   
}

run(generator).then( () => {
    console.log("Finish");
});

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://segmentfault.com/a/1190000007535316
http://www.ruanyifeng.com/blog/2015/05/co.html
http://www.ruanyifeng.com/blog/2015/05/async.html

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

分類
發燒車訊

Tensorflow 中(批量)讀取數據的案列分析及TFRecord文件的打包與讀取

內容概要:

單一數據讀取方式:

  第一種:slice_input_producer()

# 返回值可以直接通過 Session.run([images, labels])查看,且第一個參數必須放在列表中,如[...]
[images, labels] = tf.train.slice_input_producer([images, labels], num_epochs=None, shuffle=True)

  第二種:string_input_producer()

# 需要定義文件讀取器,然後通過讀取器中的 read()方法來獲取數據(返回值類型 key,value),再通過 Session.run(value)查看
file_queue = tf.train.string_input_producer(filename, num_epochs=None, shuffle=True)

reader = tf.WholeFileReader() # 定義文件讀取器
key, value = reader.read(file_queue)    # key:文件名;value:文件中的內容

  !!!num_epochs=None,不指定迭代次數,這樣文件隊列中元素個數也不限定(None*數據集大小)。

  !!!如果不是None,則此函數創建本地計數器 epochs,需要使用local_variables_initializer()初始化局部變量

  !!!以上兩種方法都可以生成文件名隊列。

(隨機)批量數據讀取方式:

batchsize=2  # 每次讀取的樣本數量
tf.train.batch(tensors, batch_size=batchsize)
tf.train.shuffle_batch(tensors, batch_size=batchsize, capacity=batchsize*10, min_after_dequeue=batchsize*5) # capacity > min_after_dequeue

  !!!以上所有讀取數據的方法,在Session.run()之前必須開啟文件隊列線程 tf.train.start_queue_runners()

 TFRecord文件的打包與讀取

 

 一、單一數據讀取方式

第一種:slice_input_producer()

def slice_input_producer(tensor_list, num_epochs=None, shuffle=True, seed=None, capacity=32, shared_name=None, name=None)

案例1:

import tensorflow as tf

images = ['image1.jpg', 'image2.jpg', 'image3.jpg', 'image4.jpg']
labels = [1, 2, 3, 4]

# [images, labels] = tf.train.slice_input_producer([images, labels], num_epochs=None, shuffle=True)

# 當num_epochs=2時,此時文件隊列中只有 2*4=8個樣本,所有在取第9個樣本時會出錯
# [images, labels] = tf.train.slice_input_producer([images, labels], num_epochs=2, shuffle=True)

data = tf.train.slice_input_producer([images, labels], num_epochs=None, shuffle=True)
print(type(data))   # <class 'list'>

with tf.Session() as sess:
    # sess.run(tf.local_variables_initializer())
    sess.run(tf.local_variables_initializer())
    coord = tf.train.Coordinator()  # 線程的協調器
    threads = tf.train.start_queue_runners(sess, coord)  # 開始在圖表中收集隊列運行器

    for i in range(10):
        print(sess.run(data))

    coord.request_stop()
    coord.join(threads)

"""
運行結果:
[b'image2.jpg', 2]
[b'image1.jpg', 1]
[b'image3.jpg', 3]
[b'image4.jpg', 4]
[b'image2.jpg', 2]
[b'image1.jpg', 1]
[b'image3.jpg', 3]
[b'image4.jpg', 4]
[b'image2.jpg', 2]
[b'image3.jpg', 3]
"""

  !!!slice_input_producer() 中的第一個參數需要放在一個列表中,列表中的每個元素可以是 List 或 Tensor,如 [images,labels],

  !!!num_epochs設置

 

 第二種:string_input_producer()

def string_input_producer(string_tensor, num_epochs=None, shuffle=True, seed=None, capacity=32, shared_name=None, name=None, cancel_op=None)

文件讀取器

  不同類型的文件對應不同的文件讀取器,我們稱為 reader對象

  該對象的 read 方法自動讀取文件,並創建數據隊列,輸出key/文件名,value/文件內容;

reader = tf.TextLineReader()      ### 一行一行讀取,適用於所有文本文件

reader = tf.TFRecordReader() ### A Reader that outputs the records from a TFRecords file
reader = tf.WholeFileReader() ### 一次讀取整個文件,適用圖片

 案例2:讀取csv文件

iimport tensorflow as tf

filename = ['data/A.csv', 'data/B.csv', 'data/C.csv']

file_queue = tf.train.string_input_producer(filename, shuffle=True, num_epochs=2)   # 生成文件名隊列
reader = tf.WholeFileReader()           # 定義文件讀取器(一次讀取整個文件)
# reader = tf.TextLineReader()            # 定義文件讀取器(一行一行的讀)
key, value = reader.read(file_queue)    # key:文件名;value:文件中的內容
print(type(file_queue))

init = [tf.global_variables_initializer(), tf.local_variables_initializer()]
with tf.Session() as sess:
    sess.run(init)
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(sess=sess, coord=coord)
    try:
        while not coord.should_stop():
            for i in range(6):
                print(sess.run([key, value]))
            break
    except tf.errors.OutOfRangeError:
        print('read done')
    finally:
        coord.request_stop()
    coord.join(threads)

"""
reader = tf.WholeFileReader()           # 定義文件讀取器(一次讀取整個文件)
運行結果:
[b'data/C.csv', b'7.jpg,7\n8.jpg,8\n9.jpg,9\n']
[b'data/B.csv', b'4.jpg,4\n5.jpg,5\n6.jpg,6\n']
[b'data/A.csv', b'1.jpg,1\n2.jpg,2\n3.jpg,3\n']
[b'data/A.csv', b'1.jpg,1\n2.jpg,2\n3.jpg,3\n']
[b'data/B.csv', b'4.jpg,4\n5.jpg,5\n6.jpg,6\n']
[b'data/C.csv', b'7.jpg,7\n8.jpg,8\n9.jpg,9\n']
"""
"""
reader = tf.TextLineReader()           # 定義文件讀取器(一行一行的讀)
運行結果:
[b'data/B.csv:1', b'4.jpg,4']
[b'data/B.csv:2', b'5.jpg,5']
[b'data/B.csv:3', b'6.jpg,6']
[b'data/C.csv:1', b'7.jpg,7']
[b'data/C.csv:2', b'8.jpg,8']
[b'data/C.csv:3', b'9.jpg,9']
"""

案例3:讀取圖片(每次讀取全部圖片內容,不是一行一行)

import tensorflow as tf

filename = ['1.jpg', '2.jpg']
filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=1)
reader = tf.WholeFileReader()              # 文件讀取器
key, value = reader.read(filename_queue)   # 讀取文件 key:文件名;value:圖片數據,bytes

with tf.Session() as sess:
    tf.local_variables_initializer().run()
    coord = tf.train.Coordinator()      # 線程的協調器
    threads = tf.train.start_queue_runners(sess, coord)

    for i in range(filename.__len__()):
        image_data = sess.run(value)
        with open('img_%d.jpg' % i, 'wb') as f:
            f.write(image_data)
    coord.request_stop()
    coord.join(threads)

 

 二、(隨機)批量數據讀取方式:

  功能:shuffle_batch() 和 batch() 這兩個API都是從文件隊列中批量獲取數據,使用方式類似;

案例4:slice_input_producer() 與 batch()

import tensorflow as tf
import numpy as np

images = np.arange(20).reshape([10, 2])
label = np.asarray(range(0, 10))
images = tf.cast(images, tf.float32)  # 可以註釋掉,不影響運行結果
label = tf.cast(label, tf.int32)     # 可以註釋掉,不影響運行結果

batchsize = 6   # 每次獲取元素的數量
input_queue = tf.train.slice_input_producer([images, label], num_epochs=None, shuffle=False)
image_batch, label_batch = tf.train.batch(input_queue, batch_size=batchsize)

# 隨機獲取 batchsize個元素,其中,capacity:隊列容量,這個參數一定要比 min_after_dequeue 大
# image_batch, label_batch = tf.train.shuffle_batch(input_queue, batch_size=batchsize, capacity=64, min_after_dequeue=10)

with tf.Session() as sess:
    coord = tf.train.Coordinator()      # 線程的協調器
    threads = tf.train.start_queue_runners(sess, coord)     # 開始在圖表中收集隊列運行器
    for cnt in range(2):
        print("第{}次獲取數據,每次batch={}...".format(cnt+1, batchsize))
        image_batch_v, label_batch_v = sess.run([image_batch, label_batch])
        print(image_batch_v, label_batch_v, label_batch_v.__len__())

    coord.request_stop()
    coord.join(threads)

"""
運行結果:
第1次獲取數據,每次batch=6...
[[ 0.  1.]
 [ 2.  3.]
 [ 4.  5.]
 [ 6.  7.]
 [ 8.  9.]
 [10. 11.]] [0 1 2 3 4 5] 6
第2次獲取數據,每次batch=6...
[[12. 13.]
 [14. 15.]
 [16. 17.]
 [18. 19.]
 [ 0.  1.]
 [ 2.  3.]] [6 7 8 9 0 1] 6
"""

 案例5:從本地批量的讀取圖片 — string_input_producer() 與 batch()

 1 import tensorflow as tf
 2 import glob
 3 import cv2 as cv
 4 
 5 def read_imgs(filename, picture_format, input_image_shape, batch_size=1):
 6     """
 7     從本地批量的讀取圖片
 8     :param filename: 圖片路徑(包括圖片的文件名),[]
 9     :param picture_format: 圖片的格式,如 bmp,jpg,png等; string
10     :param input_image_shape: 輸入圖像的大小; (h,w,c)或[]
11     :param batch_size: 每次從文件隊列中加載圖片的數量; int
12     :return: batch_size張圖片數據, Tensor
13     """
14     global new_img
15     # 創建文件隊列
16     file_queue = tf.train.string_input_producer(filename, num_epochs=1, shuffle=True)
17     # 創建文件讀取器
18     reader = tf.WholeFileReader()
19     # 讀取文件隊列中的文件
20     _, img_bytes = reader.read(file_queue)
21     # print(img_bytes)    # Tensor("ReaderReadV2_19:1", shape=(), dtype=string)
22     # 對圖片進行解碼
23     if picture_format == ".bmp":
24         new_img = tf.image.decode_bmp(img_bytes, channels=1)
25     elif picture_format == ".jpg":
26         new_img = tf.image.decode_jpeg(img_bytes, channels=3)
27     else:
28         pass
29     # 重新設置圖片的大小
30     # new_img = tf.image.resize_images(new_img, input_image_shape)
31     new_img = tf.reshape(new_img, input_image_shape)
32     # 設置圖片的數據類型
33     new_img = tf.image.convert_image_dtype(new_img, tf.uint8)
34 
35     # return new_img
36     return tf.train.batch([new_img], batch_size)
37 
38 
39 def main():
40     image_path = glob.glob(r'F:\demo\FaceRecognition\人臉庫\ORL\*.bmp')
41     image_batch = read_imgs(image_path, ".bmp", (112, 92, 1), 5)
42     print(type(image_batch))
43     # image_path = glob.glob(r'.\*.jpg')
44     # image_batch = read_imgs(image_path, ".jpg", (313, 500, 3), 1)
45 
46     sess = tf.Session()
47     sess.run(tf.local_variables_initializer())
48     tf.train.start_queue_runners(sess=sess)
49 
50     image_batch = sess.run(image_batch)
51     print(type(image_batch))    # <class 'numpy.ndarray'>
52 
53     for i in range(image_batch.__len__()):
54         cv.imshow("win_"+str(i), image_batch[i])
55     cv.waitKey()
56     cv.destroyAllWindows()
57 
58 def start():
59     image_path = glob.glob(r'F:\demo\FaceRecognition\人臉庫\ORL\*.bmp')
60     image_batch = read_imgs(image_path, ".bmp", (112, 92, 1), 5)
61     print(type(image_batch))    # <class 'tensorflow.python.framework.ops.Tensor'>
62 
63 
64     with tf.Session() as sess:
65         sess.run(tf.local_variables_initializer())
66         coord = tf.train.Coordinator()      # 線程的協調器
67         threads = tf.train.start_queue_runners(sess, coord)     # 開始在圖表中收集隊列運行器
68         image_batch = sess.run(image_batch)
69         print(type(image_batch))    # <class 'numpy.ndarray'>
70 
71         for i in range(image_batch.__len__()):
72             cv.imshow("win_"+str(i), image_batch[i])
73         cv.waitKey()
74         cv.destroyAllWindows()
75 
76         # 若使用 with 方式打開 Session,且沒加如下2行語句,則會出錯
77         # ERROR:tensorflow:Exception in QueueRunner: Enqueue operation was cancelled;
78         # 原因:文件隊列線程還處於工作狀態(隊列中還有圖片數據),而加載完batch_size張圖片會話就會自動關閉,同時關閉文件隊列線程
79         coord.request_stop()
80         coord.join(threads)
81 
82 
83 if __name__ == "__main__":
84     # main()
85     start()

從本地批量的讀取圖片案例

 

案列6:TFRecord文件打包與讀取

 1 def write_TFRecord(filename, data, labels, is_shuffler=True):
 2     """
 3     將數據打包成TFRecord格式
 4     :param filename: 打包後路徑名,默認在工程目錄下創建該文件;String
 5     :param data: 需要打包的文件路徑名;list
 6     :param labels: 對應文件的標籤;list
 7     :param is_shuffler:是否隨機初始化打包后的數據,默認:True;Bool
 8     :return: None
 9     """
10     im_data = list(data)
11     im_labels = list(labels)
12 
13     index = [i for i in range(im_data.__len__())]
14     if is_shuffler:
15         np.random.shuffle(index)
16 
17     # 創建寫入器,然後使用該對象寫入樣本example
18     writer = tf.python_io.TFRecordWriter(filename)
19     for i in range(im_data.__len__()):
20         im_d = im_data[index[i]]    # im_d:存放着第index[i]張圖片的路徑信息
21         im_l = im_labels[index[i]]  # im_l:存放着對應圖片的標籤信息
22 
23         # # 獲取當前的圖片數據  方式一:
24         # data = cv2.imread(im_d)
25         # # 創建樣本
26         # ex = tf.train.Example(
27         #     features=tf.train.Features(
28         #         feature={
29         #             "image": tf.train.Feature(
30         #                 bytes_list=tf.train.BytesList(
31         #                     value=[data.tobytes()])), # 需要打包成bytes類型
32         #             "label": tf.train.Feature(
33         #                 int64_list=tf.train.Int64List(
34         #                     value=[im_l])),
35         #         }
36         #     )
37         # )
38         # 獲取當前的圖片數據  方式二:相對於方式一,打包文件佔用空間小了一半多
39         data = tf.gfile.FastGFile(im_d, "rb").read()
40         ex = tf.train.Example(
41             features=tf.train.Features(
42                 feature={
43                     "image": tf.train.Feature(
44                         bytes_list=tf.train.BytesList(
45                             value=[data])), # 此時的data已經是bytes類型
46                     "label": tf.train.Feature(
47                         int64_list=tf.train.Int64List(
48                             value=[im_l])),
49                 }
50             )
51         )
52 
53         # 寫入將序列化之後的樣本
54         writer.write(ex.SerializeToString())
55     # 關閉寫入器
56     writer.close()

TFRecord文件打包案列

 

 1 import tensorflow as tf
 2 import cv2
 3 
 4 def read_TFRecord(file_list, batch_size=10):
 5     """
 6     讀取TFRecord文件
 7     :param file_list: 存放TFRecord的文件名,List
 8     :param batch_size: 每次讀取圖片的數量
 9     :return: 解析後圖片及對應的標籤
10     """
11     file_queue = tf.train.string_input_producer(file_list, num_epochs=None, shuffle=True)
12     reader = tf.TFRecordReader()
13     _, ex = reader.read(file_queue)
14     batch = tf.train.shuffle_batch([ex], batch_size, capacity=batch_size * 10, min_after_dequeue=batch_size * 5)
15 
16     feature = {
17         'image': tf.FixedLenFeature([], tf.string),
18         'label': tf.FixedLenFeature([], tf.int64)
19     }
20     example = tf.parse_example(batch, features=feature)
21 
22     images = tf.decode_raw(example['image'], tf.uint8)
23     images = tf.reshape(images, [-1, 32, 32, 3])
24 
25     return images, example['label']
26 
27 
28 
29 def main():
30     # filelist = ['data/train.tfrecord']
31     filelist = ['data/test.tfrecord']
32     images, labels = read_TFRecord(filelist, 2)
33     with tf.Session() as sess:
34         sess.run(tf.local_variables_initializer())
35         coord = tf.train.Coordinator()
36         threads = tf.train.start_queue_runners(sess=sess, coord=coord)
37 
38         try:
39             while not coord.should_stop():
40                 for i in range(1):
41                     image_bth, _ = sess.run([images, labels])
42                     print(_)
43 
44                     cv2.imshow("image_0", image_bth[0])
45                     cv2.imshow("image_1", image_bth[1])
46                 break
47         except tf.errors.OutOfRangeError:
48             print('read done')
49         finally:
50             coord.request_stop()
51         coord.join(threads)
52         cv2.waitKey(0)
53         cv2.destroyAllWindows()
54 
55 if __name__ == "__main__":
56     main()

TFReord文件的讀取案列

 

,

內容概要:

單一數據讀取方式:

  第一種:slice_input_producer()

# 返回值可以直接通過 Session.run([images, labels])查看,且第一個參數必須放在列表中,如[...]
[images, labels] = tf.train.slice_input_producer([images, labels], num_epochs=None, shuffle=True)

  第二種:string_input_producer()

# 需要定義文件讀取器,然後通過讀取器中的 read()方法來獲取數據(返回值類型 key,value),再通過 Session.run(value)查看
file_queue = tf.train.string_input_producer(filename, num_epochs=None, shuffle=True)

reader = tf.WholeFileReader() # 定義文件讀取器
key, value = reader.read(file_queue)    # key:文件名;value:文件中的內容

  !!!num_epochs=None,不指定迭代次數,這樣文件隊列中元素個數也不限定(None*數據集大小)。

  !!!如果不是None,則此函數創建本地計數器 epochs,需要使用local_variables_initializer()初始化局部變量

  !!!以上兩種方法都可以生成文件名隊列。

(隨機)批量數據讀取方式:

batchsize=2  # 每次讀取的樣本數量
tf.train.batch(tensors, batch_size=batchsize)
tf.train.shuffle_batch(tensors, batch_size=batchsize, capacity=batchsize*10, min_after_dequeue=batchsize*5) # capacity > min_after_dequeue

  !!!以上所有讀取數據的方法,在Session.run()之前必須開啟文件隊列線程 tf.train.start_queue_runners()

 TFRecord文件的打包與讀取

 

 一、單一數據讀取方式

第一種:slice_input_producer()

def slice_input_producer(tensor_list, num_epochs=None, shuffle=True, seed=None, capacity=32, shared_name=None, name=None)

案例1:

import tensorflow as tf

images = ['image1.jpg', 'image2.jpg', 'image3.jpg', 'image4.jpg']
labels = [1, 2, 3, 4]

# [images, labels] = tf.train.slice_input_producer([images, labels], num_epochs=None, shuffle=True)

# 當num_epochs=2時,此時文件隊列中只有 2*4=8個樣本,所有在取第9個樣本時會出錯
# [images, labels] = tf.train.slice_input_producer([images, labels], num_epochs=2, shuffle=True)

data = tf.train.slice_input_producer([images, labels], num_epochs=None, shuffle=True)
print(type(data))   # <class 'list'>

with tf.Session() as sess:
    # sess.run(tf.local_variables_initializer())
    sess.run(tf.local_variables_initializer())
    coord = tf.train.Coordinator()  # 線程的協調器
    threads = tf.train.start_queue_runners(sess, coord)  # 開始在圖表中收集隊列運行器

    for i in range(10):
        print(sess.run(data))

    coord.request_stop()
    coord.join(threads)

"""
運行結果:
[b'image2.jpg', 2]
[b'image1.jpg', 1]
[b'image3.jpg', 3]
[b'image4.jpg', 4]
[b'image2.jpg', 2]
[b'image1.jpg', 1]
[b'image3.jpg', 3]
[b'image4.jpg', 4]
[b'image2.jpg', 2]
[b'image3.jpg', 3]
"""

  !!!slice_input_producer() 中的第一個參數需要放在一個列表中,列表中的每個元素可以是 List 或 Tensor,如 [images,labels],

  !!!num_epochs設置

 

 第二種:string_input_producer()

def string_input_producer(string_tensor, num_epochs=None, shuffle=True, seed=None, capacity=32, shared_name=None, name=None, cancel_op=None)

文件讀取器

  不同類型的文件對應不同的文件讀取器,我們稱為 reader對象

  該對象的 read 方法自動讀取文件,並創建數據隊列,輸出key/文件名,value/文件內容;

reader = tf.TextLineReader()      ### 一行一行讀取,適用於所有文本文件

reader = tf.TFRecordReader() ### A Reader that outputs the records from a TFRecords file
reader = tf.WholeFileReader() ### 一次讀取整個文件,適用圖片

 案例2:讀取csv文件

iimport tensorflow as tf

filename = ['data/A.csv', 'data/B.csv', 'data/C.csv']

file_queue = tf.train.string_input_producer(filename, shuffle=True, num_epochs=2)   # 生成文件名隊列
reader = tf.WholeFileReader()           # 定義文件讀取器(一次讀取整個文件)
# reader = tf.TextLineReader()            # 定義文件讀取器(一行一行的讀)
key, value = reader.read(file_queue)    # key:文件名;value:文件中的內容
print(type(file_queue))

init = [tf.global_variables_initializer(), tf.local_variables_initializer()]
with tf.Session() as sess:
    sess.run(init)
    coord = tf.train.Coordinator()
    threads = tf.train.start_queue_runners(sess=sess, coord=coord)
    try:
        while not coord.should_stop():
            for i in range(6):
                print(sess.run([key, value]))
            break
    except tf.errors.OutOfRangeError:
        print('read done')
    finally:
        coord.request_stop()
    coord.join(threads)

"""
reader = tf.WholeFileReader()           # 定義文件讀取器(一次讀取整個文件)
運行結果:
[b'data/C.csv', b'7.jpg,7\n8.jpg,8\n9.jpg,9\n']
[b'data/B.csv', b'4.jpg,4\n5.jpg,5\n6.jpg,6\n']
[b'data/A.csv', b'1.jpg,1\n2.jpg,2\n3.jpg,3\n']
[b'data/A.csv', b'1.jpg,1\n2.jpg,2\n3.jpg,3\n']
[b'data/B.csv', b'4.jpg,4\n5.jpg,5\n6.jpg,6\n']
[b'data/C.csv', b'7.jpg,7\n8.jpg,8\n9.jpg,9\n']
"""
"""
reader = tf.TextLineReader()           # 定義文件讀取器(一行一行的讀)
運行結果:
[b'data/B.csv:1', b'4.jpg,4']
[b'data/B.csv:2', b'5.jpg,5']
[b'data/B.csv:3', b'6.jpg,6']
[b'data/C.csv:1', b'7.jpg,7']
[b'data/C.csv:2', b'8.jpg,8']
[b'data/C.csv:3', b'9.jpg,9']
"""

案例3:讀取圖片(每次讀取全部圖片內容,不是一行一行)

import tensorflow as tf

filename = ['1.jpg', '2.jpg']
filename_queue = tf.train.string_input_producer(filename, shuffle=False, num_epochs=1)
reader = tf.WholeFileReader()              # 文件讀取器
key, value = reader.read(filename_queue)   # 讀取文件 key:文件名;value:圖片數據,bytes

with tf.Session() as sess:
    tf.local_variables_initializer().run()
    coord = tf.train.Coordinator()      # 線程的協調器
    threads = tf.train.start_queue_runners(sess, coord)

    for i in range(filename.__len__()):
        image_data = sess.run(value)
        with open('img_%d.jpg' % i, 'wb') as f:
            f.write(image_data)
    coord.request_stop()
    coord.join(threads)

 

 二、(隨機)批量數據讀取方式:

  功能:shuffle_batch() 和 batch() 這兩個API都是從文件隊列中批量獲取數據,使用方式類似;

案例4:slice_input_producer() 與 batch()

import tensorflow as tf
import numpy as np

images = np.arange(20).reshape([10, 2])
label = np.asarray(range(0, 10))
images = tf.cast(images, tf.float32)  # 可以註釋掉,不影響運行結果
label = tf.cast(label, tf.int32)     # 可以註釋掉,不影響運行結果

batchsize = 6   # 每次獲取元素的數量
input_queue = tf.train.slice_input_producer([images, label], num_epochs=None, shuffle=False)
image_batch, label_batch = tf.train.batch(input_queue, batch_size=batchsize)

# 隨機獲取 batchsize個元素,其中,capacity:隊列容量,這個參數一定要比 min_after_dequeue 大
# image_batch, label_batch = tf.train.shuffle_batch(input_queue, batch_size=batchsize, capacity=64, min_after_dequeue=10)

with tf.Session() as sess:
    coord = tf.train.Coordinator()      # 線程的協調器
    threads = tf.train.start_queue_runners(sess, coord)     # 開始在圖表中收集隊列運行器
    for cnt in range(2):
        print("第{}次獲取數據,每次batch={}...".format(cnt+1, batchsize))
        image_batch_v, label_batch_v = sess.run([image_batch, label_batch])
        print(image_batch_v, label_batch_v, label_batch_v.__len__())

    coord.request_stop()
    coord.join(threads)

"""
運行結果:
第1次獲取數據,每次batch=6...
[[ 0.  1.]
 [ 2.  3.]
 [ 4.  5.]
 [ 6.  7.]
 [ 8.  9.]
 [10. 11.]] [0 1 2 3 4 5] 6
第2次獲取數據,每次batch=6...
[[12. 13.]
 [14. 15.]
 [16. 17.]
 [18. 19.]
 [ 0.  1.]
 [ 2.  3.]] [6 7 8 9 0 1] 6
"""

 案例5:從本地批量的讀取圖片 — string_input_producer() 與 batch()

 1 import tensorflow as tf
 2 import glob
 3 import cv2 as cv
 4 
 5 def read_imgs(filename, picture_format, input_image_shape, batch_size=1):
 6     """
 7     從本地批量的讀取圖片
 8     :param filename: 圖片路徑(包括圖片的文件名),[]
 9     :param picture_format: 圖片的格式,如 bmp,jpg,png等; string
10     :param input_image_shape: 輸入圖像的大小; (h,w,c)或[]
11     :param batch_size: 每次從文件隊列中加載圖片的數量; int
12     :return: batch_size張圖片數據, Tensor
13     """
14     global new_img
15     # 創建文件隊列
16     file_queue = tf.train.string_input_producer(filename, num_epochs=1, shuffle=True)
17     # 創建文件讀取器
18     reader = tf.WholeFileReader()
19     # 讀取文件隊列中的文件
20     _, img_bytes = reader.read(file_queue)
21     # print(img_bytes)    # Tensor("ReaderReadV2_19:1", shape=(), dtype=string)
22     # 對圖片進行解碼
23     if picture_format == ".bmp":
24         new_img = tf.image.decode_bmp(img_bytes, channels=1)
25     elif picture_format == ".jpg":
26         new_img = tf.image.decode_jpeg(img_bytes, channels=3)
27     else:
28         pass
29     # 重新設置圖片的大小
30     # new_img = tf.image.resize_images(new_img, input_image_shape)
31     new_img = tf.reshape(new_img, input_image_shape)
32     # 設置圖片的數據類型
33     new_img = tf.image.convert_image_dtype(new_img, tf.uint8)
34 
35     # return new_img
36     return tf.train.batch([new_img], batch_size)
37 
38 
39 def main():
40     image_path = glob.glob(r'F:\demo\FaceRecognition\人臉庫\ORL\*.bmp')
41     image_batch = read_imgs(image_path, ".bmp", (112, 92, 1), 5)
42     print(type(image_batch))
43     # image_path = glob.glob(r'.\*.jpg')
44     # image_batch = read_imgs(image_path, ".jpg", (313, 500, 3), 1)
45 
46     sess = tf.Session()
47     sess.run(tf.local_variables_initializer())
48     tf.train.start_queue_runners(sess=sess)
49 
50     image_batch = sess.run(image_batch)
51     print(type(image_batch))    # <class 'numpy.ndarray'>
52 
53     for i in range(image_batch.__len__()):
54         cv.imshow("win_"+str(i), image_batch[i])
55     cv.waitKey()
56     cv.destroyAllWindows()
57 
58 def start():
59     image_path = glob.glob(r'F:\demo\FaceRecognition\人臉庫\ORL\*.bmp')
60     image_batch = read_imgs(image_path, ".bmp", (112, 92, 1), 5)
61     print(type(image_batch))    # <class 'tensorflow.python.framework.ops.Tensor'>
62 
63 
64     with tf.Session() as sess:
65         sess.run(tf.local_variables_initializer())
66         coord = tf.train.Coordinator()      # 線程的協調器
67         threads = tf.train.start_queue_runners(sess, coord)     # 開始在圖表中收集隊列運行器
68         image_batch = sess.run(image_batch)
69         print(type(image_batch))    # <class 'numpy.ndarray'>
70 
71         for i in range(image_batch.__len__()):
72             cv.imshow("win_"+str(i), image_batch[i])
73         cv.waitKey()
74         cv.destroyAllWindows()
75 
76         # 若使用 with 方式打開 Session,且沒加如下2行語句,則會出錯
77         # ERROR:tensorflow:Exception in QueueRunner: Enqueue operation was cancelled;
78         # 原因:文件隊列線程還處於工作狀態(隊列中還有圖片數據),而加載完batch_size張圖片會話就會自動關閉,同時關閉文件隊列線程
79         coord.request_stop()
80         coord.join(threads)
81 
82 
83 if __name__ == "__main__":
84     # main()
85     start()

從本地批量的讀取圖片案例

 

案列6:TFRecord文件打包與讀取

 1 def write_TFRecord(filename, data, labels, is_shuffler=True):
 2     """
 3     將數據打包成TFRecord格式
 4     :param filename: 打包後路徑名,默認在工程目錄下創建該文件;String
 5     :param data: 需要打包的文件路徑名;list
 6     :param labels: 對應文件的標籤;list
 7     :param is_shuffler:是否隨機初始化打包后的數據,默認:True;Bool
 8     :return: None
 9     """
10     im_data = list(data)
11     im_labels = list(labels)
12 
13     index = [i for i in range(im_data.__len__())]
14     if is_shuffler:
15         np.random.shuffle(index)
16 
17     # 創建寫入器,然後使用該對象寫入樣本example
18     writer = tf.python_io.TFRecordWriter(filename)
19     for i in range(im_data.__len__()):
20         im_d = im_data[index[i]]    # im_d:存放着第index[i]張圖片的路徑信息
21         im_l = im_labels[index[i]]  # im_l:存放着對應圖片的標籤信息
22 
23         # # 獲取當前的圖片數據  方式一:
24         # data = cv2.imread(im_d)
25         # # 創建樣本
26         # ex = tf.train.Example(
27         #     features=tf.train.Features(
28         #         feature={
29         #             "image": tf.train.Feature(
30         #                 bytes_list=tf.train.BytesList(
31         #                     value=[data.tobytes()])), # 需要打包成bytes類型
32         #             "label": tf.train.Feature(
33         #                 int64_list=tf.train.Int64List(
34         #                     value=[im_l])),
35         #         }
36         #     )
37         # )
38         # 獲取當前的圖片數據  方式二:相對於方式一,打包文件佔用空間小了一半多
39         data = tf.gfile.FastGFile(im_d, "rb").read()
40         ex = tf.train.Example(
41             features=tf.train.Features(
42                 feature={
43                     "image": tf.train.Feature(
44                         bytes_list=tf.train.BytesList(
45                             value=[data])), # 此時的data已經是bytes類型
46                     "label": tf.train.Feature(
47                         int64_list=tf.train.Int64List(
48                             value=[im_l])),
49                 }
50             )
51         )
52 
53         # 寫入將序列化之後的樣本
54         writer.write(ex.SerializeToString())
55     # 關閉寫入器
56     writer.close()

TFRecord文件打包案列

 

 1 import tensorflow as tf
 2 import cv2
 3 
 4 def read_TFRecord(file_list, batch_size=10):
 5     """
 6     讀取TFRecord文件
 7     :param file_list: 存放TFRecord的文件名,List
 8     :param batch_size: 每次讀取圖片的數量
 9     :return: 解析後圖片及對應的標籤
10     """
11     file_queue = tf.train.string_input_producer(file_list, num_epochs=None, shuffle=True)
12     reader = tf.TFRecordReader()
13     _, ex = reader.read(file_queue)
14     batch = tf.train.shuffle_batch([ex], batch_size, capacity=batch_size * 10, min_after_dequeue=batch_size * 5)
15 
16     feature = {
17         'image': tf.FixedLenFeature([], tf.string),
18         'label': tf.FixedLenFeature([], tf.int64)
19     }
20     example = tf.parse_example(batch, features=feature)
21 
22     images = tf.decode_raw(example['image'], tf.uint8)
23     images = tf.reshape(images, [-1, 32, 32, 3])
24 
25     return images, example['label']
26 
27 
28 
29 def main():
30     # filelist = ['data/train.tfrecord']
31     filelist = ['data/test.tfrecord']
32     images, labels = read_TFRecord(filelist, 2)
33     with tf.Session() as sess:
34         sess.run(tf.local_variables_initializer())
35         coord = tf.train.Coordinator()
36         threads = tf.train.start_queue_runners(sess=sess, coord=coord)
37 
38         try:
39             while not coord.should_stop():
40                 for i in range(1):
41                     image_bth, _ = sess.run([images, labels])
42                     print(_)
43 
44                     cv2.imshow("image_0", image_bth[0])
45                     cv2.imshow("image_1", image_bth[1])
46                 break
47         except tf.errors.OutOfRangeError:
48             print('read done')
49         finally:
50             coord.request_stop()
51         coord.join(threads)
52         cv2.waitKey(0)
53         cv2.destroyAllWindows()
54 
55 if __name__ == "__main__":
56     main()

TFReord文件的讀取案列

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

分類
發燒車訊

10.DRF-認證租車,如何寫文案,貨運,銷售文案,FB行銷

Django rest framework源碼分析(1)—-認證

一、基礎

1.1.安裝

兩種方式:

  • github
  • pip直接安裝
pip install djangorestframework

1.2.需要先了解的一些知識

理解下面兩個知識點非常重要,django-rest-framework源碼中到處都是基於CBV和面向對象的封裝

(1)面向對象封裝的兩大特性

把同一類方法封裝到類中

將數據封裝到對象中

(2)CBV

基於反射實現根據請求方式不同,執行不同的方法

原理:url–>view方法–>dispatch方法(反射執行其它方法:GET/POST/PUT/DELETE等等)

二、簡單實例

2.1.settings

先創建一個project和一個app(我這裏命名為API)

首先要在settings的app中添加

INSTALLED_APPS = [
    'rest_framework',
]

2.2.url

from django.contrib import admin
from django.urls import path
from API.views import AuthView

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/auth/',AuthView.as_view()),
]

2.3.models

一個保存用戶的信息

一個保存用戶登錄成功后的token

from django.db import models

class UserInfo(models.Model):
    USER_TYPE = (
        (1,'普通用戶'),
        (2,'VIP'),
        (3,'SVIP')
    )

    user_type = models.IntegerField(choices=USER_TYPE)
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=64)

class UserToken(models.Model):
    user = models.OneToOneField(UserInfo,on_delete=models.CASCADE)
    token = models.CharField(max_length=64)

2.4.views

用戶登錄(返回token並保存到數據庫)

from django.shortcuts import render
from django.http import JsonResponse
from rest_framework.views import APIView
from API import models

def md5(user):
    import hashlib
    import time
    #當前時間,相當於生成一個隨機的字符串
    ctime = str(time.time())
    m = hashlib.md5(bytes(user,encoding='utf-8'))
    m.update(bytes(ctime,encoding='utf-8'))
    return m.hexdigest()

class AuthView(object):
    def post(self,request,*args,**kwargs):
        ret = {'code':1000,'msg':None}
        try:
            user = request._request.POST.get('username')
            pwd = request._request.POST.get('password')
            obj = models.UserInfo.objects.filter(username=user,password=pwd).first()
            if not obj:
                ret['code'] = 1001
                ret['msg'] = '用戶名或密碼錯誤'
            #為用戶創建token
            token = md5(user)
            #存在就更新,不存在就創建
            models.UserToken.objects.update_or_create(user=obj,defaults={'token':token})
            ret['token'] = token
        except Exception as e:
            ret['code'] = 1002
            ret['msg'] = '請求異常'
        return JsonResponse(ret)

2.5.利用postman發請求

如果用戶名和密碼正確的話 會生成token值,下次該用戶再登錄時,token的值就會更新

數據庫中可以看到token的值

當用戶名或密碼錯誤時,拋出異常

三、添加認證

基於上面的例子,添加一個認證的類

3.1.url

path('api/v1/order/',OrderView.as_view()),

3.2.views

from django.shortcuts import render,HttpResponse
from django.http import JsonResponse
from rest_framework.views import APIView
from API import models
from rest_framework.request import Request
from rest_framework import exceptions
from rest_framework.authentication import BasicAuthentication

ORDER_DICT = {
    1:{
        'name':'apple',
        'price':15
    },
    2:{
        'name':'dog',
        'price':100
    }
}

def md5(user):
    import hashlib
    import time
    #當前時間,相當於生成一個隨機的字符串
    ctime = str(time.time())
    m = hashlib.md5(bytes(user,encoding='utf-8'))
    m.update(bytes(ctime,encoding='utf-8'))
    return m.hexdigest()

class AuthView(object):
    '''用於用戶登錄驗證'''
    def post(self,request,*args,**kwargs):
        ret = {'code':1000,'msg':None}
        try:
            user = request._request.POST.get('username')
            pwd = request._request.POST.get('password')
            obj = models.UserInfo.objects.filter(username=user,password=pwd).first()
            if not obj:
                ret['code'] = 1001
                ret['msg'] = '用戶名或密碼錯誤'
            #為用戶創建token
            token = md5(user)
            #存在就更新,不存在就創建
            models.UserToken.objects.update_or_create(user=obj,defaults={'token':token})
            ret['token'] = token
        except Exception as e:
            ret['code'] = 1002
            ret['msg'] = '請求異常'
        return JsonResponse(ret)


class Authentication(APIView):
    '''認證'''
    def authenticate(self,request):
        token = request._request.GET.get('token')
        token_obj = models.UserToken.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed('用戶認證失敗')
        #在rest framework內部會將這兩個字段賦值給request,以供後續操作使用
        return (token_obj.user,token_obj)

    def authenticate_header(self, request):
        pass

class OrderView(APIView):
    '''訂單相關業務'''

    authentication_classes = [Authentication,]    #添加認證
    def get(self,request,*args,**kwargs):
        #request.user
        #request.auth
        ret = {'code':1000,'msg':None,'data':None}
        try:
            ret['data'] = ORDER_DICT
        except Exception as e:
            pass
        return JsonResponse(ret)

3.3用postman發get請求

請求的時候沒有帶token,可以看到會显示“用戶認證失敗”

這樣就達到了認證的效果,django-rest-framework的認證是怎麼實現的呢,下面基於這個例子來剖析drf的源碼。

四、drf的認證源碼分析

源碼流程圖

請求先到dispatch

dispatch()主要做了兩件事

  • 封裝request
  • 認證  

具體看我寫的代碼裏面的註釋

def dispatch(self, request, *args, **kwargs):
	"""
	`.dispatch()` is pretty much the same as Django's regular dispatch,
	but with extra hooks for startup, finalize, and exception handling.
	 """
	self.args = args
    self.kwargs = kwargs
    #對原始request進行加工,豐富了一些功能
    #Request(
    #     request,
    #     parsers=self.get_parsers(),
    #     authenticators=self.get_authenticators(),
    #     negotiator=self.get_content_negotiator(),
    #     parser_context=parser_context
    # )
    #request(原始request,[BasicAuthentications對象,])
    #獲取原生request,request._request
    #獲取認證類的對象,request.authticators
    #1.封裝request
    request = self.initialize_request(request, *args, **kwargs)
    self.request = request
    self.headers = self.default_response_headers  # deprecate?

    try:
        #2.認證
        self.initial(request, *args, **kwargs)

        # Get the appropriate handler method
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed

        response = handler(request, *args, **kwargs)

	except Exception as exc:
        response = self.handle_exception(exc)

	self.response = self.finalize_response(request, response, *args, **kwargs)
    return self.response

4.1.reuqest

(1)initialize_request()

可以看到initialize()就是封裝原始request

def initialize_request(self, request, *args, **kwargs):
    """
    Returns the initial request object.
    """
    parser_context = self.get_parser_context(request)

    return Request(
        request,
        parsers=self.get_parsers(),
        #[BasicAuthentication(),],把對象封裝到request裏面了
        authenticators=self.get_authenticators(),    
        negotiator=self.get_content_negotiator(), parser_context=parser_context )

(2)get_authenticators()

通過列表生成式,返回對象的列表

def get_authenticators(self):
    """
    Instantiates and returns the list of authenticators that this view can use.
    """
    return [auth() for auth in self.authentication_classes]

(3)authentication_classes

APIView裏面有個 authentication_classes 字段

可以看到默認是去全局的配置文件找(api_settings)

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
    content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
    metadata_class = api_settings.DEFAULT_METADATA_CLASS
    versioning_class = api_settings.DEFAULT_VERSIONING_CLASS

4.2.認證

self.initial(request, *args, **kwargs)

def dispatch(self, request, *args, **kwargs):
    """
    `.dispatch()` is pretty much the same as Django's regular dispatch,
    but with extra hooks for startup, finalize, and exception handling.
    """
    self.args = args
    self.kwargs = kwargs
    #對原始request進行加工,豐富了一些功能
    #Request(
    #     request,
    #     parsers=self.get_parsers(),
    #     authenticators=self.get_authenticators(),
    #     negotiator=self.get_content_negotiator(),
    #     parser_context=parser_context
    # )
    #request(原始request,[BasicAuthentications對象,])
    #獲取原生request,request._request
    #獲取認證類的對象,request.authticators
    #1.封裝request
    request = self.initialize_request(request, *args, **kwargs)
    self.request = request
    self.headers = self.default_response_headers  # deprecate?

    try:
        #2.認證
        self.initial(request, *args, **kwargs)

        # Get the appropriate handler method
        if request.method.lower() in self.http_method_names:
            handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
        else:
            handler = self.http_method_not_allowed

        response = handler(request, *args, **kwargs)

	except Exception as exc:
        response = self.handle_exception(exc)

	self.response = self.finalize_response(request, response, *args, **kwargs)
    return self.response

(1)initial()

主要看 self.perform_authentication(request),實現認證

def initial(self, request, *args, **kwargs):
    """
    Runs anything that needs to occur prior to calling the method handler.
    """
    self.format_kwarg = self.get_format_suffix(**kwargs)

    # Perform content negotiation and store the accepted info on the request
    neg = self.perform_content_negotiation(request)
    request.accepted_renderer, request.accepted_media_type = neg

    # Determine the API version, if versioning is in use.
    version, scheme = self.determine_version(request, *args, **kwargs)
    request.version, request.versioning_scheme = version, scheme

    # Ensure that the incoming request is permitted
    #3.實現認證
    self.perform_authentication(request)
    self.check_permissions(request)
    self.check_throttles(request)

(2)perform_authentication()

調用了request.user

def perform_authentication(self, request):
    """
    Perform authentication on the incoming request.

	Note that if you override this and simply 'pass', then authentication
	will instead be performed lazily, the first time either
	`request.user` or `request.auth` is accessed.
	"""
    request.user

(3)user

request.user的request的位置

點進去可以看到Request有個user方法,加 @property 表示調用user方法的時候不需要加括號“user()”,可以直接調用:request.user

@property
def user(self):
    """
    Returns the user associated with the current request, as authenticated
    by the authentication classes provided to the request.
    """
    if not hasattr(self, '_user'):
        with wrap_attributeerrors():
            #獲取認證對象,進行一步步的認證
            self._authenticate()
    return self._user

(4)_authenticate()

循環所有authenticator對象

def _authenticate(self):
    """
    Attempt to authenticate the request using each authentication instance
    in turn.
    """
    #循環認證類的所有對象
    #執行對象的authenticate方法
    for authenticator in self.authenticators:
        try:
            #執行認證類的authenticate方法
            #這裏分三種情況
            #1.如果authenticate方法拋出異常,self._not_authenticated()執行
            #2.有返回值,必須是元組:(request.user,request.auth)
            #3.返回None,表示當前認證不處理,等下一個認證來處理
            user_auth_tuple = authenticator.authenticate(self)
        except exceptions.APIException:
            self._not_authenticated()
            raise

        if user_auth_tuple is not None:
            self._authenticator = authenticator
            self.user, self.auth = user_auth_tuple
            return

	self._not_authenticated()

返回值就是例子中的:

token_obj.user-->>request.user
token_obj-->>request.auth
#在rest framework內部會將這兩個字段賦值給request,以供後續操作使用
return (token_obj.user,token_obj)     #例子中的return

當都沒有返回值,就執行self._not_authenticated(),相當於匿名用戶,沒有通過認證

def _not_authenticated(self):
    """
    Set authenticator, user & authtoken representing an unauthenticated request.

	Defaults are None, AnonymousUser & None.
	"""
    self._authenticator = None

    if api_settings.UNAUTHENTICATED_USER:
        self.user = api_settings.UNAUTHENTICATED_USER()   #AnonymousUser匿名用戶
    else:
        self.user = None

	if api_settings.UNAUTHENTICATED_TOKEN:
        self.auth = api_settings.UNAUTHENTICATED_TOKEN()  #None
	else:
        self.auth = None

面向對象知識:

子類繼承 父類,調用方法的時候:

  • 優先去自己裏面找有沒有這個方法,有就執行自己的
  • 只有當自己裏面沒有這個方法的時候才會去父類找

因為authenticate方法我們自己寫,所以當執行authenticate()的時候就是執行我們自己寫的認證

父類中的authenticate方法

def authenticate(self, request):
    return (self.force_user, self.force_token)

我們自己寫的

class Authentication(APIView):
    '''用於用戶登錄驗證'''
    def authenticate(self,request):
        token = request._request.GET.get('token')
        token_obj = models.UserToken.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed('用戶認證失敗')
        #在rest framework內部會將這兩個字段賦值給request,以供後續操作使用
        return (token_obj.user,token_obj)

認證的流程就是上面寫的,弄懂了原理,再寫代碼就更容易理解為什麼了。

4.3.配置文件

繼續解讀源碼

默認是去全局配置文件中找,所以我們應該在settings.py中配置好路徑

api_settings源碼

api_settings = APISettings(None, DEFAULTS, IMPORT_STRINGS)

def reload_api_settings(*args, **kwargs):
    setting = kwargs['setting']
    if setting == 'REST_FRAMEWORK':
        api_settings.reload()

setting中‘REST_FRAMEWORK’中找

全局配置方法:

API文件夾下面新建文件夾utils,再新建auth.py文件,裏面寫上認證的類

settings.py

#設置全局認證
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES":['API.utils.auth.Authentication',]   #裏面寫你的認證的類的路徑
}

auth.py

# API/utils/auth.py

from rest_framework import exceptions
from API import models


class Authentication(object):
    '''用於用戶登錄驗證'''
    def authenticate(self,request):
        token = request._request.GET.get('token')
        token_obj = models.UserToken.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed('用戶認證失敗')
        #在rest framework內部會將這兩個字段賦值給request,以供後續操作使用
        return (token_obj.user,token_obj)

    def authenticate_header(self, request):
        pass

在settings裏面設置的全局認證,所有業務都需要經過認證,如果想讓某個不需要認證,只需要在其中添加下面的代碼:

authentication_classes = []    #裏面為空,代表不需要認證
from django.shortcuts import render,HttpResponse
from django.http import JsonResponse
from rest_framework.views import APIView
from API import models
from rest_framework.request import Request
from rest_framework import exceptions
from rest_framework.authentication import BasicAuthentication

ORDER_DICT = {
    1:{
        'name':'apple',
        'price':15
    },
    2:{
        'name':'dog',
        'price':100
    }
}

def md5(user):
    import hashlib
    import time
    #當前時間,相當於生成一個隨機的字符串
    ctime = str(time.time())
    m = hashlib.md5(bytes(user,encoding='utf-8'))
    m.update(bytes(ctime,encoding='utf-8'))
    return m.hexdigest()

class AuthView(APIView):
    '''用於用戶登錄驗證'''

    authentication_classes = []    #裏面為空,代表不需要認證

    def post(self,request,*args,**kwargs):
        ret = {'code':1000,'msg':None}
        try:
            user = request._request.POST.get('username')
            pwd = request._request.POST.get('password')
            obj = models.UserInfo.objects.filter(username=user,password=pwd).first()
            if not obj:
                ret['code'] = 1001
                ret['msg'] = '用戶名或密碼錯誤'
            #為用戶創建token
            token = md5(user)
            #存在就更新,不存在就創建
            models.UserToken.objects.update_or_create(user=obj,defaults={'token':token})
            ret['token'] = token
        except Exception as e:
            ret['code'] = 1002
            ret['msg'] = '請求異常'
        return JsonResponse(ret)




class OrderView(APIView):
    '''訂單相關業務'''


    def get(self,request,*args,**kwargs):
        # self.dispatch
        #request.user
        #request.auth
        ret = {'code':1000,'msg':None,'data':None}
        try:
            ret['data'] = ORDER_DICT
        except Exception as e:
            pass
        return JsonResponse(ret)

API/view.py代碼

再測試一下我們的代碼

不帶token發請求

帶token發請求

五、drf的內置認證

rest_framework裏面內置了一些認證,我們自己寫的認證類都要繼承內置認證類 “BaseAuthentication”

4.1.BaseAuthentication源碼:

class BaseAuthentication(object):
    """
    All authentication classes should extend BaseAuthentication.
    """

    def authenticate(self, request):
        """
        Authenticate the request and return a two-tuple of (user, token).
        """
        #內置的認證類,authenticate方法,如果不自己寫,默認則拋出異常
        raise NotImplementedError(".authenticate() must be overridden.")

    def authenticate_header(self, request):
        """
        Return a string to be used as the value of the `WWW-Authenticate`
        header in a `401 Unauthenticated` response, or `None` if the
        authentication scheme should return `403 Permission Denied` responses.
        """
        #authenticate_header方法,作用是當認證失敗的時候,返回的響應頭
        pass

4.2.修改自己寫的認證類

自己寫的Authentication必須繼承內置認證類BaseAuthentication

# API/utils/auth/py

from rest_framework import exceptions
from API import models
from rest_framework.authentication import BaseAuthentication


class Authentication(BaseAuthentication):
    '''用於用戶登錄驗證'''
    def authenticate(self,request):
        token = request._request.GET.get('token')
        token_obj = models.UserToken.objects.filter(token=token).first()
        if not token_obj:
            raise exceptions.AuthenticationFailed('用戶認證失敗')
        #在rest framework內部會將這兩個字段賦值給request,以供後續操作使用
        return (token_obj.user,token_obj)

    def authenticate_header(self, request):
        pass

4.3.其它內置認證類

rest_framework裏面還內置了其它認證類,我們主要用到的就是BaseAuthentication,剩下的很少用到

六、總結

自己寫認證類方法梳理

(1)創建認證類

  • 繼承BaseAuthentication —>>1.重寫authenticate方法;2.authenticate_header方法直接寫pass就可以(這個方法必須寫)

(2)authenticate()返回值(三種)

  • None —–>>>當前認證不管,等下一個認證來執行
  • raise exceptions.AuthenticationFailed(‘用戶認證失敗’) # from rest_framework import exceptions
  • 有返回值元祖形式:(元素1,元素2) #元素1複製給request.user; 元素2複製給request.auth

(3)局部使用

  • authentication_classes = [BaseAuthentication,]

(4)全局使用

#設置全局認證
REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES":['API.utils.auth.Authentication',]
}

源碼流程

—>>dispatch

    –封裝request

       —獲取定義的認證類(全局/局部),通過列表生成式創建對象 

     —initial

       —-peform_authentication

         —–request.user (每部循環創建的對象)

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

SpringMVC之文件上傳

一、環境搭建

1、新建項目

(1)在” main”目錄下新建” java”與” resources”目錄

(2)將” java”設置為”Sources Root”

(3)將” resources”設置為”Resources Root”

(4)在”java”目錄下新建”StudyProject.Controller”包

(5)在”StudyProject.Controller”包下新建”TestController”類

(6)在”resources”目錄下新建”Spring.xml”

(7)在”WEB-INF”目錄下新建文件夾”Pages”

(8)在“Pages”目錄下新建”success.jsp”

2、整體框架

3、TestController類和success.jsp

(1)TestController類

原代碼

1 package StudyProject.Controller;
2 
3 public class TestController {
4 }

編寫前端控制器及路徑

修改后

1 package StudyProject.Controller;
2 
3 import org.springframework.stereotype.Controller;
4 import org.springframework.web.bind.annotation.RequestMapping;
5 
6 @Controller
7 @RequestMapping(path="/testController")
8 public class TestController {
9 }

(2)success.jsp

原代碼

1 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
2 <html>
3 <head>
4     <title>Title</title>
5 </head>
6 <body>
7 
8 </body>
9 </html>

添加一個跳轉成功提示

修改后

 1 <%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
 2 <html>
 3 <head>
 4     <title>Title</title>
 5 </head>
 6 <body>
 7 
 8     <h3>跳轉成功</h3>
 9 
10 </body>
11 </html>

4、配置文件

(1)pom.xml

原代碼

1   <properties>
2     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
3     <maven.compiler.source>1.7</maven.compiler.source>
4     <maven.compiler.target>1.7</maven.compiler.target>
5   </properties>

修改版本,並且加上spring.version

修改后

1   <properties>
2     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
3     <maven.compiler.source>14.0.1</maven.compiler.source>
4     <maven.compiler.target>14.0.1</maven.compiler.target>
5     <spring.version>5.0.2.RELEASE</spring.version>
6   </properties>

原代碼

1   <dependencies>
2     <dependency>
3       <groupId>junit</groupId>
4       <artifactId>junit</artifactId>
5       <version>4.11</version>
6       <scope>test</scope>
7     </dependency>
8   </dependencies>

在<dependencies></dependency>里加入坐標依賴,原有的可以刪去

修改后

 1   <!-- 導入坐標依賴 -->
 2   <dependencies>
 3     <dependency>
 4       <groupId>org.springframework</groupId>
 5       <artifactId>spring-context</artifactId>
 6       <version>${spring.version}</version>
 7     </dependency>
 8     <dependency>
 9       <groupId>org.springframework</groupId>
10       <artifactId>spring-web</artifactId>
11       <version>${spring.version}</version>
12     </dependency>
13     <dependency>
14       <groupId>org.springframework</groupId>
15       <artifactId>spring-webmvc</artifactId>
16       <version>${spring.version}</version>
17     </dependency>
18     <dependency>
19       <groupId>javax.servlet</groupId>
20       <artifactId>servlet-api</artifactId>
21       <version>2.5</version>
22       <scope>provided</scope>
23     </dependency>
24     <dependency>
25       <groupId>javax.servlet.jsp</groupId>
26       <artifactId>jsp-api</artifactId>
27       <version>2.0</version>
28       <scope>provided</scope>
29     </dependency>
30   </dependencies>

(2)web.xml

原代碼

1 <!DOCTYPE web-app PUBLIC
2  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
3  "http://java.sun.com/dtd/web-app_2_3.dtd" >
4 
5 <web-app>
6   <display-name>Archetype Created Web Application</display-name>
7 </web-app>

配置前段控制器與解決中文亂碼的過濾器

修改后

 1 <!DOCTYPE web-app PUBLIC
 2  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 3  "http://java.sun.com/dtd/web-app_2_3.dtd" >
 4 
 5 <web-app>
 6   <display-name>Archetype Created Web Application</display-name>
 7 
 8   <!--配置解決中文亂碼的過濾器-->
 9   <filter>
10     <filter-name>characterEncodingFilter</filter-name>
11     <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
12     <init-param>
13       <param-name>encoding</param-name>
14       <param-value>UTF-8</param-value>
15     </init-param>
16   </filter>
17   <filter-mapping>
18   <filter-name>characterEncodingFilter</filter-name>
19   <url-pattern>/*</url-pattern>
20   </filter-mapping>
21 
22   <!-- 配置前端控制器 -->
23   <servlet>
24     <servlet-name>dispatcherServlet</servlet-name>
25     <!-- 創建前端控制器DispatcherServlet對象 -->
26     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
27     <!-- 使前端控制器初始化時讀取Spring.xml文件創建Spring核心容器 -->
28     <init-param>
29       <param-name>contextConfigLocation</param-name>
30       <param-value>classpath*:Spring.xml</param-value>
31     </init-param>
32     <!-- 設置該Servlet的優先級別為最高,使之最早創建(在應用啟動時就加載並初始化這個servlet -->
33     <load-on-startup>1</load-on-startup>
34   </servlet>
35   <servlet-mapping>
36     <servlet-name>dispatcherServlet</servlet-name>
37     <url-pattern>/</url-pattern>
38   </servlet-mapping>
39 
40 </web-app>

(3)Spring.xml

原代碼

1 <?xml version="1.0" encoding="UTF-8"?>
2 <beans xmlns="http://www.springframework.org/schema/beans"
3        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
5 
6 </beans>

配置spring創建容器時掃描的包、視圖解析器、開啟spring註解支持等

修改后

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:mvc="http://www.springframework.org/schema/mvc"
 4        xmlns:context="http://www.springframework.org/schema/context"
 5        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 6        xsi:schemaLocation="
 7         http://www.springframework.org/schema/beans
 8         http://www.springframework.org/schema/beans/spring-beans.xsd
 9         http://www.springframework.org/schema/mvc
10         http://www.springframework.org/schema/mvc/spring-mvc.xsd
11         http://www.springframework.org/schema/context
12         http://www.springframework.org/schema/context/spring-context.xsd">
13 
14     <!-- 配置spring創建容器時掃描的包 -->
15     <context:component-scan base-package="StudyProject.Controller"></context:component-scan>
16 
17     <!-- 配置視圖解析器,用於解析項目跳轉到的文件的位置 -->
18     <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
19         <!-- 尋找包的路徑 -->
20         <property name="prefix" value="/WEB-INF/pages/"></property>
21         <!-- 尋找文件的後綴名 -->
22         <property name="suffix" value=".jsp"></property>
23     </bean>
24 
25     <!-- 配置spring開啟註解mvc的支持 -->
26     <mvc:annotation-driven></mvc:annotation-driven>
27 </beans>

5、Tomcat服務器(本地已建SpringMVC項目,Spring_MVC項目僅做示範)

點擊”Add Configurations”配置Tomcat服務器

二、文件上傳

1、傳統方式上傳文件

(1)TestController類

在控制器內部新增”testMethod_Traditional”方法

 1 @Controller
 2 @RequestMapping(path="/testController")
 3 public class TestController {
 4 
 5     @RequestMapping(path="/testMethod_Traditional")
 6     public String testMethod_Traditional(HttpServletRequest request) throws Exception {
 7         System.out.println("執行了testMethod_Traditional方法");
 8 
 9         //獲取文件上傳目錄
10         String path = request.getSession().getServletContext().getRealPath("/uploads");
11         //創建file對象
12         File file = new File(path);
13         //判斷路徑是否存在,若不存在,創建該路徑
14         if (!file.exists()) {
15             file.mkdir();
16         }
17 
18         //創建磁盤文件項工廠
19         DiskFileItemFactory factory = new DiskFileItemFactory();
20         ServletFileUpload fileUpload = new ServletFileUpload(factory);
21         //解析request對象
22         List<FileItem> list = fileUpload.parseRequest(request);
23         //遍歷
24         for (FileItem fileItem:list) {
25             // 判斷文件項是普通字段,還是上傳的文件
26             if (fileItem.isFormField()) {
27                 //普通字段
28             } else {
29                 //上傳文件項
30                 //獲取上傳文件項的名稱
31                 String filename = fileItem.getName();
32                 String uuid = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
33                 filename = uuid+"_"+filename;
34                 //上傳文件
35                 fileItem.write(new File(file,filename));
36                 //刪除臨時文件
37                 fileItem.delete();
38             }
39         }
40 
41         System.out.println("上傳路徑:"+path);
42         System.out.println("上傳成功");
43         return "success";
44     }
45 
46 }

(2)index.jsp

添加form表單

1     <form action="testController/testMethod_Traditional" method="post" enctype="multipart/form-data">
2         圖片 <input type="file" name="uploadfile_Traditional"> <br>
3         <input type="submit" value="傳統方式上傳文件">
4     </form>

(3)結果演示

點擊服務器”SpringMVC”右側的運行按鈕

選擇文件然後進行上傳

點擊上傳按鈕后,執行成功,跳到”success.jsp”界面显示跳轉成功

在IDEA輸出台查看文件路徑

按照路徑查看文件是否上傳成功

2、SpringMVC方式上傳文件

(1)pom.xml添加文件上傳坐標依賴

在pom.xml文件<dependencies></dependencies>內添加文件上傳坐標依賴

 1     <!-- 文件上傳 -->
 2     <dependency>
 3       <groupId>commons-fileupload</groupId>
 4       <artifactId>commons-fileupload</artifactId>
 5       <version>1.3.1</version>
 6     </dependency>
 7     <dependency>
 8       <groupId>commons-io</groupId>
 9       <artifactId>commons-io</artifactId>
10       <version>2.4</version>
11     </dependency>

(2)Spring.xml配置文件解析器對象

在Spring.xml文件<beans></beans>內配置文件解析器對象

1     <!-- 配置文件解析器對象 -->
2     <bean id="multipartResolver"
3           class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
4         <property name="defaultEncoding" value="utf-8"></property>
5         <property name="maxUploadSize" value="10485760"></property>
6     </bean>

(3)TestController類

在控制器內部新增”testMethod_SpringMVC”方法

 1 @Controller
 2 @RequestMapping(path="/testController")
 3 public class TestController {
 4 
 5     @RequestMapping(path="/testMethod_SpringMVC")
 6     public String testMethod_SpringMVC(HttpServletRequest request,MultipartFile uploadfile_SpringMVC) throws Exception {
 7         System.out.println("執行了testMethod_SpringMVC方法");
 8 
 9         //獲取文件上傳目錄
10         String path = request.getSession().getServletContext().getRealPath("/uploads");
11         //創建file對象
12         File file = new File(path);
13         //判斷路徑是否存在,若不存在,創建該路徑
14         if (!file.exists()) {
15             file.mkdir();
16         }
17 
18         //獲取到上傳文件的名稱
19         String filename = uploadfile_SpringMVC.getOriginalFilename();
20         String uuid = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
21         filename = uuid+"_"+filename;
22         //上傳文件
23         uploadfile_SpringMVC.transferTo(new File(file,filename));
24 
25         System.out.println("上傳路徑:"+path);
26         System.out.println("上傳成功");
27         return "success";
28     }
29 
30 }

(4)index.jsp

添加form表單

1     <form action="testController/testMethod_SpringMVC" method="post" enctype="multipart/form-data">
2         圖片 <input type="file" name="uploadfile_SpringMVC"> <br>
3         <input type="submit" value="SpringMVC上傳文件">
4     </form>

(5)結果演示

點擊服務器”SpringMVC”右側的運行按鈕

選擇文件然後進行上傳

點擊上傳按鈕后,執行成功,跳到”success.jsp”界面显示跳轉成功

在IDEA輸出台查看文件路徑

按照路徑查看文件是否上傳成功

3、跨服務器上傳文件

(1)新建”FileuploadServer”項目(過程不再演示)

不需要建立”java””resources”等文件夾,只需要”index.jsp”显示界面即可

“index.jsp”代碼

1 <html>
2 <body>
3 <h2>Hello! FileuploadServer</h2>
4 </body>
5 </html>

(2)配置服務器

點擊”Edit Configurations”配置Tomcat服務器

為與”SpringMVC”服務器區分,修改”HTTP port”為”9090”,修改”JMX port”為”1090”

(3)pom.xml添加跨服務器文件上傳坐標依賴

 1     <!-- 跨服務器文件上傳 -->
 2     <dependency>
 3       <groupId>com.sun.jersey</groupId>
 4       <artifactId>jersey-core</artifactId>
 5       <version>1.18.1</version>
 6     </dependency>
 7     <dependency>
 8       <groupId>com.sun.jersey</groupId>
 9       <artifactId>jersey-client</artifactId>
10       <version>1.18.1</version>
11     </dependency>

(4)TestController類

在控制器內部新增”testMethod_AcrossServer”方法

 1 @Controller
 2 @RequestMapping(path="/testController")
 3 public class TestController {
 4 
 5     @RequestMapping(path="/testMethod_AcrossServer")
 6     public String testMethod_AcrossServer(MultipartFile uploadfile_AcrossServer) throws Exception {
 7         System.out.println("執行了testMethod_AcrossServer方法");
 8 
 9         //定義上傳文件服務器路徑
10         String path = "http://localhost:9090/FileuploadServer_war_exploded/uploads/";
11 
12         //獲取到上傳文件的名稱
13         String filename = uploadfile_AcrossServer.getOriginalFilename();
14         String uuid = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
15         filename = uuid+"_"+filename;
16 
17         //創建客戶端對象
18         Client client = Client.create();
19         //連接圖片服務器
20         WebResource webResourcer = client.resource(path+filename);
21         //向圖片服務器上傳文件
22         webResourcer.put(uploadfile_AcrossServer.getBytes());
23 
24         System.out.println("上傳路徑:"+path);
25         System.out.println("上傳成功");
26         return "success";
27     }
28 
29 }

(5)index.jsp

添加form表單

1     <form action="testController/testMethod_AcrossServer" method="post" enctype="multipart/form-data">
2         圖片 <input type="file" name="uploadfile_AcrossServer"> <br>
3         <input type="submit" value="跨服務器上傳文件">
4     </form>

(6)結果演示

運行”FileuploadServer”服務器

運行”SpringMVC”服務器

在”FileuploadServer”項目的”target/FileuploadServer/”目錄下新建文件夾”uploads”

選擇文件並進行上傳,上傳成功跳轉到”success.jsp”

查看IDEA輸出信息

此時路徑應為”FileuploadServer/target/FileuploadServer/uploads”,在路徑下查看文件是否上傳成功

三、注意事項

1、傳統方式上傳文件

傳統方式上傳時不需要在Spring.xml內配置文件解析器對象使用該方法時需要註釋掉該對象,否則會造成運行成功但上傳文件為空。

1     <!-- 配置文件解析器對象 -->
2     <bean id="multipartResolver"
3           class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
4         <property name="defaultEncoding" value="utf-8"></property>
5         <property name="maxUploadSize" value="10485760"></property>
6     </bean>

即使用傳統方式上傳文件時,應當註釋掉該段代碼

2、跨服務器上傳文件

(1)需要修改Tomcat服務器的web.xml配置文件的權限,增加可以寫入的權限,否則會出現405的錯誤。如我所下載的Tomcat-9.0.36的web.xml路徑為”apache-tomcat-9.0.36/conf/web.xml”

此時IEDA輸出為

web.xml文件修改處原內容

應修改為

修改后的代碼

 1     <servlet>
 2         <servlet-name>default</servlet-name>
 3         <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
 4         <init-param>
 5             <param-name>debug</param-name>
 6             <param-value>0</param-value>
 7         </init-param>
 8         <init-param>
 9             <param-name>listings</param-name>
10             <param-value>false</param-value>
11         </init-param>
12         <init-param>
13             <param-name>readonly</param-name>
14             <param-value>false</param-value>
15         </init-param>
16         <load-on-startup>1</load-on-startup>
17     </servlet>

(2)在跨服務器上傳文件時,需要在”FileuploadServer”項目的”target/FileuploadServer/”目錄下新建文件夾”uploads”,否則會出現409的錯誤

四、完整代碼

1、pom.xml(SpringMVC)

  1 <?xml version="1.0" encoding="UTF-8"?>
  2 
  3 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5   <modelVersion>4.0.0</modelVersion>
  6 
  7   <groupId>org.example</groupId>
  8   <artifactId>SpringMVC</artifactId>
  9   <version>1.0-SNAPSHOT</version>
 10   <packaging>war</packaging>
 11 
 12   <name>SpringMVC Maven Webapp</name>
 13   <!-- FIXME change it to the project's website -->
 14   <url>http://www.example.com</url>
 15 
 16   <properties>
 17     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 18     <maven.compiler.source>14.0.1</maven.compiler.source>
 19     <maven.compiler.target>14.0.1</maven.compiler.target>
 20     <spring.version>5.0.2.RELEASE</spring.version>
 21   </properties>
 22 
 23   <!-- 導入坐標依賴 -->
 24   <dependencies>
 25     <dependency>
 26       <groupId>org.springframework</groupId>
 27       <artifactId>spring-context</artifactId>
 28       <version>${spring.version}</version>
 29     </dependency>
 30     <dependency>
 31       <groupId>org.springframework</groupId>
 32       <artifactId>spring-web</artifactId>
 33       <version>${spring.version}</version>
 34     </dependency>
 35     <dependency>
 36       <groupId>org.springframework</groupId>
 37       <artifactId>spring-webmvc</artifactId>
 38       <version>${spring.version}</version>
 39     </dependency>
 40     <dependency>
 41       <groupId>javax.servlet</groupId>
 42       <artifactId>servlet-api</artifactId>
 43       <version>2.5</version>
 44       <scope>provided</scope>
 45     </dependency>
 46     <dependency>
 47       <groupId>javax.servlet.jsp</groupId>
 48       <artifactId>jsp-api</artifactId>
 49       <version>2.0</version>
 50       <scope>provided</scope>
 51     </dependency>
 52 
 53     <!-- 文件上傳(採用傳統方式上傳時需註釋掉該部分) -->
 54     <dependency>
 55       <groupId>commons-fileupload</groupId>
 56       <artifactId>commons-fileupload</artifactId>
 57       <version>1.3.1</version>
 58     </dependency>
 59     <dependency>
 60       <groupId>commons-io</groupId>
 61       <artifactId>commons-io</artifactId>
 62       <version>2.4</version>
 63     </dependency>
 64 
 65     <!-- 跨服務器文件上傳 -->
 66     <dependency>
 67       <groupId>com.sun.jersey</groupId>
 68       <artifactId>jersey-core</artifactId>
 69       <version>1.18.1</version>
 70     </dependency>
 71     <dependency>
 72       <groupId>com.sun.jersey</groupId>
 73       <artifactId>jersey-client</artifactId>
 74       <version>1.18.1</version>
 75     </dependency>
 76 
 77   </dependencies>
 78 
 79   <build>
 80     <finalName>SpringMVC</finalName>
 81     <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
 82       <plugins>
 83         <plugin>
 84           <artifactId>maven-clean-plugin</artifactId>
 85           <version>3.1.0</version>
 86         </plugin>
 87         <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
 88         <plugin>
 89           <artifactId>maven-resources-plugin</artifactId>
 90           <version>3.0.2</version>
 91         </plugin>
 92         <plugin>
 93           <artifactId>maven-compiler-plugin</artifactId>
 94           <version>3.8.0</version>
 95         </plugin>
 96         <plugin>
 97           <artifactId>maven-surefire-plugin</artifactId>
 98           <version>2.22.1</version>
 99         </plugin>
100         <plugin>
101           <artifactId>maven-war-plugin</artifactId>
102           <version>3.2.2</version>
103         </plugin>
104         <plugin>
105           <artifactId>maven-install-plugin</artifactId>
106           <version>2.5.2</version>
107         </plugin>
108         <plugin>
109           <artifactId>maven-deploy-plugin</artifactId>
110           <version>2.8.2</version>
111         </plugin>
112       </plugins>
113     </pluginManagement>
114   </build>
115 </project>

2、web.xml(SpringMVC)

 1 <!DOCTYPE web-app PUBLIC
 2  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 3  "http://java.sun.com/dtd/web-app_2_3.dtd" >
 4 
 5 <web-app>
 6   <display-name>Archetype Created Web Application</display-name>
 7 
 8   <!--配置解決中文亂碼的過濾器-->
 9   <filter>
10     <filter-name>characterEncodingFilter</filter-name>
11     <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
12     <init-param>
13       <param-name>encoding</param-name>
14       <param-value>UTF-8</param-value>
15     </init-param>
16   </filter>
17   <filter-mapping>
18   <filter-name>characterEncodingFilter</filter-name>
19   <url-pattern>/*</url-pattern>
20   </filter-mapping>
21 
22   <!-- 配置前端控制器 -->
23   <servlet>
24     <servlet-name>dispatcherServlet</servlet-name>
25     <!-- 創建前端控制器DispatcherServlet對象 -->
26     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
27     <!-- 使前端控制器初始化時讀取Spring.xml文件創建Spring核心容器 -->
28     <init-param>
29       <param-name>contextConfigLocation</param-name>
30       <param-value>classpath*:Spring.xml</param-value>
31     </init-param>
32     <!-- 設置該Servlet的優先級別為最高,使之最早創建(在應用啟動時就加載並初始化這個servlet -->
33     <load-on-startup>1</load-on-startup>
34   </servlet>
35   <servlet-mapping>
36     <servlet-name>dispatcherServlet</servlet-name>
37     <url-pattern>/</url-pattern>
38   </servlet-mapping>
39 
40 </web-app>

3、Spring.xml(SpringMVC)

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <beans xmlns="http://www.springframework.org/schema/beans"
 3        xmlns:mvc="http://www.springframework.org/schema/mvc"
 4        xmlns:context="http://www.springframework.org/schema/context"
 5        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 6        xsi:schemaLocation="
 7         http://www.springframework.org/schema/beans
 8         http://www.springframework.org/schema/beans/spring-beans.xsd
 9         http://www.springframework.org/schema/mvc
10         http://www.springframework.org/schema/mvc/spring-mvc.xsd
11         http://www.springframework.org/schema/context
12         http://www.springframework.org/schema/context/spring-context.xsd">
13 
14     <!-- 配置spring創建容器時掃描的包 -->
15     <context:component-scan base-package="StudyProject.Controller"></context:component-scan>
16 
17     <!-- 配置視圖解析器,用於解析項目跳轉到的文件的位置 -->
18     <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
19         <!-- 尋找包的路徑 -->
20         <property name="prefix" value="/WEB-INF/pages/"></property>
21         <!-- 尋找文件的後綴名 -->
22         <property name="suffix" value=".jsp"></property>
23     </bean>
24 
25     <!-- 配置文件解析器對象 -->
26     <bean id="multipartResolver"
27           class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
28         <property name="defaultEncoding" value="utf-8"></property>
29         <property name="maxUploadSize" value="10485760"></property>
30     </bean>
31     
32     <!-- 配置spring開啟註解mvc的支持 -->
33     <mvc:annotation-driven></mvc:annotation-driven>
34 </beans>

4、TestController類(SpringMVC)

  1 package StudyProject.Controller;
  2 
  3 import com.sun.jersey.api.client.Client;
  4 import com.sun.jersey.api.client.WebResource;
  5 import org.apache.commons.fileupload.FileItem;
  6 import org.apache.commons.fileupload.disk.DiskFileItemFactory;
  7 import org.apache.commons.fileupload.servlet.ServletFileUpload;
  8 import org.springframework.stereotype.Controller;
  9 import org.springframework.web.bind.annotation.RequestMapping;
 10 import org.springframework.web.multipart.MultipartFile;
 11 import javax.servlet.http.HttpServletRequest;
 12 import java.io.File;
 13 import java.util.List;
 14 import java.util.UUID;
 15 
 16 @Controller
 17 @RequestMapping(path="/testController")
 18 public class TestController {
 19 
 20     @RequestMapping(path="/testMethod_Traditional")
 21     public String testMethod_Traditional(HttpServletRequest request) throws Exception {
 22         System.out.println("執行了testMethod_Traditional方法");
 23 
 24         //獲取文件上傳目錄
 25         String path = request.getSession().getServletContext().getRealPath("/uploads");
 26         //創建file對象
 27         File file = new File(path);
 28         //判斷路徑是否存在,若不存在,創建該路徑
 29         if (!file.exists()) {
 30             file.mkdir();
 31         }
 32 
 33         //創建磁盤文件項工廠
 34         DiskFileItemFactory factory = new DiskFileItemFactory();
 35         ServletFileUpload fileUpload = new ServletFileUpload(factory);
 36         //解析request對象
 37         List<FileItem> list = fileUpload.parseRequest(request);
 38         //遍歷
 39         for (FileItem fileItem:list) {
 40             // 判斷文件項是普通字段,還是上傳的文件
 41             if (fileItem.isFormField()) {
 42                 //普通字段
 43             } else {
 44                 //上傳文件項
 45                 //獲取上傳文件項的名稱
 46                 String filename = fileItem.getName();
 47                 String uuid = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
 48                 filename = uuid+"_"+filename;
 49                 //上傳文件
 50                 fileItem.write(new File(file,filename));
 51                 //刪除臨時文件
 52                 fileItem.delete();
 53             }
 54         }
 55 
 56         System.out.println("上傳路徑:"+path);
 57         System.out.println("上傳成功");
 58         return "success";
 59     }
 60 
 61     @RequestMapping(path="/testMethod_SpringMVC")
 62     public String testMethod_SpringMVC(HttpServletRequest request,MultipartFile uploadfile_SpringMVC) throws Exception {
 63         System.out.println("執行了testMethod_SpringMVC方法");
 64 
 65         //獲取文件上傳目錄
 66         String path = request.getSession().getServletContext().getRealPath("/uploads");
 67         //創建file對象
 68         File file = new File(path);
 69         //判斷路徑是否存在,若不存在,創建該路徑
 70         if (!file.exists()) {
 71             file.mkdir();
 72         }
 73 
 74         //獲取到上傳文件的名稱
 75         String filename = uploadfile_SpringMVC.getOriginalFilename();
 76         String uuid = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
 77         filename = uuid+"_"+filename;
 78         //上傳文件
 79         uploadfile_SpringMVC.transferTo(new File(file,filename));
 80 
 81         System.out.println("上傳路徑:"+path);
 82         System.out.println("上傳成功");
 83         return "success";
 84     }
 85 
 86     @RequestMapping(path="/testMethod_AcrossServer")
 87     public String testMethod_AcrossServer(MultipartFile uploadfile_AcrossServer) throws Exception {
 88         System.out.println("執行了testMethod_AcrossServer方法");
 89 
 90         //定義上傳文件服務器路徑
 91         String path = "http://localhost:9090/FileuploadServer_war_exploded/uploads/";
 92 
 93         //獲取到上傳文件的名稱
 94         String filename = uploadfile_AcrossServer.getOriginalFilename();
 95         String uuid = UUID.randomUUID().toString().replaceAll("-","").toUpperCase();
 96         filename = uuid+"_"+filename;
 97 
 98         //創建客戶端對象
 99         Client client = Client.create();
100         //連接圖片服務器
101         WebResource webResourcer = client.resource(path+filename);
102         //向圖片服務器上傳文件
103         webResourcer.put(uploadfile_AcrossServer.getBytes());
104 
105         System.out.println("上傳路徑:"+path);
106         System.out.println("上傳成功");
107         return "success";
108     }
109 
110 }

5、index.jsp(SpringMVC)

 1 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
 2 <html>
 3 <head>
 4     <title>Title</title>
 5 </head>
 6 <body>
 7 
 8     <form action="testController/testMethod_Traditional" method="post" enctype="multipart/form-data">
 9         圖片 <input type="file" name="uploadfile_Traditional"> <br>
10         <input type="submit" value="傳統方式上傳文件">
11     </form>
12 
13     <br><br><br>
14 
15     <form action="testController/testMethod_SpringMVC" method="post" enctype="multipart/form-data">
16         圖片 <input type="file" name="uploadfile_SpringMVC"> <br>
17         <input type="submit" value="SpringMVC上傳文件">
18     </form>
19 
20     <br><br><br>
21 
22     <form action="testController/testMethod_AcrossServer" method="post" enctype="multipart/form-data">
23         圖片 <input type="file" name="uploadfile_AcrossServer"> <br>
24         <input type="submit" value="跨服務器上傳文件">
25     </form>
26 
27 </body>
28 </html>

6、success.jsp(SpringMVC)

 1 <%@ page contentType="text/html;charset=UTF-8" language="java" isELIgnored="false" %>
 2 <html>
 3 <head>
 4     <title>Title</title>
 5 </head>
 6 <body>
 7 
 8     <h3>跳轉成功</h3>
 9 
10 </body>
11 </html>

7、index.jsp(FileuploadServer)

1 <html>
2 <body>
3 <h2>Hello! FileuploadServer</h2>
4 </body>
5 </html>

學習資料來源:黑馬程序員

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

【其他文章推薦】

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

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

分類
發燒車訊

印度抗霾害,2030年全面改賣電動車

在川普總統發出豪語,宣布美國將退出「巴黎氣候協議」的同時,印度能源部門卻宣布,為了對抗日益嚴重的空氣污染,預計在2030 年後印度將只賣電動車。

CNNMoney 報導,做為開發中國家之一,印度的經濟以驚人速度成長中,但在產業與交通持續發展的情況下,嚴重的空氣污染也隨之而來,根據研究估計,空污每年約造成印度120 萬人因喪命,甚至有醫師如此形容,「在首都新德里呼吸,就像是每天抽10 根菸」。

不僅是空氣污染,蓬勃發展的經濟也讓印度成為全球第三大石油進口國,每年在石油上花費將近1,500 億美元,電動車發展將能使石油需求大幅下降,因此印度政府宣布,在2030 年後,在印度銷售的每輛汽車都必須仰賴電力,而非石油。

為了達到目標,印度開始推行「全國電動車計畫」(National Electric Mobility Mission Plan),希望至2020 年時,電動車和混合動力車的年銷量能達到600-700 萬輛,能源部長Piyush Goyal 表示,在電動車市場起步階段,政府會透過經費補助來協助成長,但在那之後,車商就得仰賴市場需求去推動產能上升。

對於電動車大廠特斯拉(Tesla)來說,這當然是非常好的消息,儘管特斯拉還並未進入印度市場,但馬斯克(Elon Musk)也隨即在新聞出現後發布了一條推特,稱讚印度政府對於太陽能、電動車等環保能源產業的支持。

在馬斯克發文後,印度當地最大的電動車商馬璽達(Mahindra)執行長也在推特表示,歡迎特斯拉這個強力的競爭對手盡快加入,「馬斯克你該來了,你不會希望把整個市場都拱手讓給馬璽達吧?人多才熱鬧,也會更加環保。」

為了改善空污情況,印度政府一直有在嘗試相關措施,在2016 年1 月時,新德里政府就宣布,男子開車必須「做一休一」,允許開車的日數以車牌尾數的奇偶來決定,單身婦女則每日都允許被開車。

這項規定對減少空污取得了很大成功,但一但市場轉向純電動車,對於環境將會造成更正面的影響,根據世界經濟論壇報導指出,在採取這項計畫後,至2030 年前,印度將有望將碳排放量減少37%。

(本文由《報》授權提供。照片來源: shared by CC 2.0)

 

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

※超省錢租車方案

分類
發燒車訊

寶馬計畫從2013年開始投產i子品牌電動車

寶馬集團計畫從2013年開始將i子品牌車型投入生產,該品牌為專屬品牌。

寶馬集團董事長諾伯特·雷瑟夫(Norbert Reithofer)日前表示,公司將集中精力準備i系列新款電動車的生產工作,按照計畫i子品牌將在2013年下半年投放到市場,而投產時間則早於該節點。此前有消息稱該子品牌旗下的i3車型明年將在德國萊比錫(Leipzig)工廠投產,初步產能3萬輛/年。

2011年2月21日,寶馬正式宣佈發佈i子品牌。根據寶馬注冊商標透露的資訊,車型覆蓋從i1到i9。寶馬官方正式公佈了i3純電動車和i8插電式混動車兩款車型,分別以Megacity Vehicle概念車和Vision EfficientDynamics概念技術為藍本。另外,據悉i1為小型城市電動車,i4為兩座酷派,i5為帶有酷派風格的四門Sedan轎車。i4將在今年11月底洛杉磯車展上亮相。

此前有傳聞指出,寶馬i專案工程浩大,耗費成本甚巨,導致集團考慮放棄該子品牌。不過寶馬方面否認了該說法。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

FB行銷專家,教你從零開始的技巧

分類
發燒車訊

雷諾計畫批量投產電動汽車Zoe 2013年投放

法國汽車製造商雷諾最近宣佈計畫將Zoe批量投產,2013年投入市場。

雷諾最近抱怨其銷售緩慢,人們對Twizy毫無熱情,Zoe也許會是雷諾在電動汽車市場上最後的嘗試。雷諾近日宣佈將Zoe批量投產,於2013年開售。Zoe電動馬達輸出功率可達88馬力(65千瓦),扭矩220牛米(163英尺磅),最大速度可達84 英里/小時。此款車在英國定價為14444英鎊(約合人民幣144296元)。

Zoe是雷諾最新推出的電動汽車,車上將裝配R-Link多媒體平板電腦、電動氣候控制系統和電池充電器等裝置。雷諾R-Link多媒體平板電腦將配置7英寸的觸屏顯示幕,具有語音辨識系統,可以控制一些主要功能,如,音訊系統、電動汽車駕駛指導系統和Carminat TomTom即時導航系統。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

新北清潔公司,居家、辦公、裝潢細清專業服務

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

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

※超省錢租車方案

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

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