50行Python代碼實現視頻中物體顏色識別和跟蹤(必須以紅色為例)

目前計算機視覺(CV)與自然語言處理(NLP)及語音識別並列為人工智能三大熱點方向,而計算機視覺中的對象檢測(objectdetection)應用非常廣泛,比如自動駕駛、視頻監控、工業質檢、醫療診斷等場景。

目標檢測的根本任務就是將圖片或者視頻中感興趣的目標提取出來,目標的識別可以基於顏色、紋理、形狀。其中顏色屬性運用十分廣泛,也比較容易實現。下面就向大家分享一個我做的小實驗———通過OpenCV的Python接口來實現從視頻中進行顏色識別和跟蹤。

下面就是我們完整的代碼實現(已調試運行):

import numpy as np
import cv2
font = cv2.FONT_HERSHEY_SIMPLEX
lower_green = np.array([35, 110, 106])  # 綠色範圍低閾值
upper_green = np.array([77, 255, 255])  # 綠色範圍高閾值
lower_red = np.array([0, 127, 128])  # 紅色範圍低閾值
upper_red = np.array([10, 255, 255])  # 紅色範圍高閾值
#需要更多顏色,可以去百度一下HSV閾值!
# cap = cv2.VideoCapture('1.mp4')  # 打開視頻文件
cap = cv2.VideoCapture(0)#打開USB攝像頭
if (cap.isOpened()):  # 視頻打開成功
    flag = 1
else:
    flag = 0
num = 0
if (flag):
    while (True):
        ret, frame = cap.read()  # 讀取一幀
       
        if ret == False:  # 讀取幀失敗
            break
        hsv_img = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
        mask_green = cv2.inRange(hsv_img, lower_green, upper_green)  # 根據顏色範圍刪選
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red) 
 # 根據顏色範圍刪選
        mask_green = cv2.medianBlur(mask_green, 7)  # 中值濾波
        mask_red = cv2.medianBlur(mask_red, 7)  # 中值濾波
        mask = cv2.bitwise_or(mask_green, mask_red)
        mask_green, contours, hierarchy = cv2.findContours(mask_green, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
        mask_red, contours2, hierarchy2 = cv2.findContours(mask_red, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

        for cnt in contours:
            (x, y, w, h) = cv2.boundingRect(cnt)
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 255), 2)
            cv2.putText(frame, "Green", (x, y - 5), font, 0.7, (0, 255, 0), 2)

        for cnt2 in contours2:
            (x2, y2, w2, h2) = cv2.boundingRect(cnt2)
            cv2.rectangle(frame, (x2, y2), (x2 + w2, y2 + h2), (0, 255, 255), 2)
            cv2.putText(frame, "Red", (x2, y2 - 5), font, 0.7, (0, 0, 255), 2)
        num = num + 1
        cv2.imshow("dection", frame)
        cv2.imwrite("imgs/%d.jpg"%num, frame)
        if cv2.waitKey(20) & 0xFF == 27:
            break
cv2.waitKey(0)
cv2.destroyAllWindows()

如圖所示,我們將會檢測到紅色區域

最終的效果圖:

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

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

020.掌握Pod-Pod基礎使用

一 Pod定義詳解

1.1 完整Pod定義文件

  1 apiVersion: v1			#必選,版本號,例如v1,版本號必須可以用 kubectl api-versions 查詢到
  2 kind: Pod				#必選,Pod
  3 metadata:				#必選,元數據
  4   name: string			#必選,Pod名稱,需符合RFC 1035規範
  5   namespace: string			#必選,Pod所屬的命名空間,默認為"default"
  6   labels:				#自定義標籤
  7     - name: string			#自定義標籤名字
  8   annotations:			#自定義註釋列表
  9     - name: string
 10 spec:				#必選,Pod中容器的詳細定義
 11   containers:			#必選,Pod中容器列表
 12   - name: string			#必選,容器名稱,需符合RFC 1035規範
 13     image: string			#必選,容器的鏡像名稱
 14     imagePullPolicy: [ Always|Never|IfNotPresent ]	#獲取鏡像的策略,Alawys表示每次都嘗試下載鏡像,IfnotPresent表示優先使用本地鏡像,否則下載鏡像,Nerver表示僅使用本地鏡像
 15     command: [string]		#容器的啟動命令列表,如不指定,使用打包時使用的啟動命令
 16     args: [string]			#容器的啟動命令參數列表
 17     workingDir: string		#容器的工作目錄
 18     volumeMounts:			#掛載到容器內部的存儲卷配置
 19     - name: string			#引用pod定義的共享存儲卷的名稱,需用volumes[]部分定義的的卷名
 20       mountPath: string		#存儲卷在容器內mount的絕對路徑,應少於512字符
 21       readOnly: boolean		#是否為只讀模式,默認為讀寫模式
 22     ports:				#需要暴露的端口庫號列表
 23     - name: string			#端口的名稱
 24       containerPort: int		#容器需要監聽的端口號
 25       hostPort: int		        #容器所在主機需要監聽的端口號,默認與Container相同
 26       protocol: string		#端口協議,支持TCP和UDP,默認TCP
 27     env:				#容器運行前需設置的環境變量列表
 28     - name: string			#環境變量名稱
 29       value: string		        #環境變量的值
 30     resources:			#資源限制和請求的設置
 31       limits:			#資源限制的設置
 32         cpu: string		        #CPU的限制,單位為core數,將用於docker run --cpu-shares參數
 33         memory: string		#內存限制,單位可以為Mib/Gib,將用於docker run --memory參數
 34       requests:			#資源請求的設置
 35         cpu: string		        #CPU請求,容器啟動的初始可用數量
 36         memory: string		#內存請求,容器啟動的初始可用數量
 37     livenessProbe:			#對Pod內各容器健康檢查的設置,當探測無響應幾次后將自動重啟該容器,檢查方法有exec、httpGet和tcpSocket,對一個容器只需設置其中一種方法即可
 38       exec:			        #對Pod容器內檢查方式設置為exec方式
 39         command: [string]		#exec方式需要制定的命令或腳本
 40       httpGet:			#對Pod內個容器健康檢查方法設置為HttpGet,需要制定Path、port
 41         path: string
 42         port: number
 43         host: string
 44         scheme: string
 45         HttpHeaders:
 46         - name: string
 47           value: string
 48       tcpSocket:			#對Pod內個容器健康檢查方式設置為tcpSocket方式
 49          port: number
 50        initialDelaySeconds: 0	#容器啟動完成后首次探測的時間,單位為秒
 51        timeoutSeconds: 0		#對容器健康檢查探測等待響應的超時時間,單位秒,默認1秒
 52        periodSeconds: 0		#對容器監控檢查的定期探測時間設置,單位秒,默認10秒一次
 53        successThreshold: 0
 54        failureThreshold: 0
 55        securityContext:
 56          privileged: false
 57     restartPolicy: [Always | Never | OnFailure]	#Pod的重啟策略,Always表示一旦不管以何種方式終止運行,kubelet都將重啟,OnFailure表示只有Pod以非0退出碼退出才重啟,Nerver表示不再重啟該Pod
 58     nodeSelector: obeject		#設置NodeSelector表示將該Pod調度到包含這個label的node上,以key:value的格式指定
 59     imagePullSecrets:		#Pull鏡像時使用的secret名稱,以key:secretkey格式指定
 60     - name: string
 61     hostNetwork: false		#是否使用主機網絡模式,默認為false,如果設置為true,表示使用宿主機網絡
 62     volumes:			#在該pod上定義共享存儲卷列表
 63     - name: string			#共享存儲卷名稱 (volumes類型有很多種)
 64       emptyDir: {}			#類型為emtyDir的存儲卷,與Pod同生命周期的一個臨時目錄。為空值
 65       hostPath: string		#類型為hostPath的存儲卷,表示掛載Pod所在宿主機的目錄
 66         path: string		#Pod所在宿主機的目錄,將被用於同期中mount的目錄
 67       secret:			#類型為secret的存儲卷,掛載集群與定義的secre對象到容器內部
 68         scretname: string
 69         items:
 70         - key: string
 71           path: string
 72       configMap:			#類型為configMap的存儲卷,掛載預定義的configMap對象到容器內部
 73         name: string
 74         items:
 75         - key: string
 76           path: string

二 Pod的基本用法

2.1 創建Pod


Pod可以由1個或多個容器組合而成,通常對於緊耦合的兩個應用,應該組合成一個整體對外提供服務,則應該將這兩個打包為一個pod。

屬於一個Pod的多個容器應用之間相互訪問只需要通過localhost即可通信,這一組容器被綁定在一個環境中。

  1 [root@k8smaster01 study]# vi frontend-localredis-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: redis-php
  6   label:
  7     name: redis-php
  8 spec:
  9   containers:
 10   - name: frontend
 11     image: kubeguide/guestbook-php-frontend:localredis
 12     ports:
 13     - containersPort: 80
 14   - name: redis-php
 15     image: kubeguide/redis-master
 16     ports:
 17     - containersPort: 6379
 18 
 19 [root@k8smaster01 study]# kubectl create -f frontend-localredis-pod.yaml
 20 


2.2 查看Pod

  1 [root@k8smaster01 study]# kubectl get pods	                #READY為2/2,表示此Pod中運行了yaml定義的兩個容器
  2 NAME        READY   STATUS    RESTARTS   AGE
  3 redis-php   2/2     Running   0          7m45s
  4 [root@k8smaster01 study]# kubectl describe pod redis-php	#查看詳細信息
  5 


三 靜態Pod

3.1 靜態Pod概述


靜態pod是由kubelet進行管理的僅存在於特定Node的Pod上,他們不能通過API Server進行管理,無法與ReplicationController、Deployment或者DaemonSet進行關聯,並且kubelet無法對他們進行健康檢查。靜態Pod總是由kubelet進行創建,並且總是在kubelet所在的Node上運行。

創建靜態Pod有兩種方式:配置文件或者HTTP方式。

3.2 配置文件方式創建

  1 [root@k8snode01 ~]# mkdir -p /etc/kubelet.d
  2 [root@k8snode01 ~]# vi /etc/kubelet.d/static-web.yaml
  3 apiVersion: v1
  4 kind: Pod
  5 metadata:
  6   name: static-web
  7   label:
  8     name: static-web
  9 spec:
 10   containers:
 11   - name: static-web
 12     image: nginx
 13     ports:
 14     - name: web
 15       containersPort: 80
 16 
 17 [root@k8snode01 ~]# vi /etc/systemd/system/kubelet.service
 18 ……
 19   --config=/etc/kubelet.d/ \·				#加入此參數
 20 ……
 21 [root@k8snode01 ~]# systemctl daemon-reload
 22 [root@k8snode01 ~]# systemctl restart kubelet.service	#重啟kubelet
 23 [root@k8snode01 ~]# docker ps				#查看創建的pod



提示:由於靜態pod不能通過API Server進行管理,因此在Master節點執行刪除操作後會變為Pending狀態,且無法刪除。刪除該pod只能在其運行的node上,將定義POD的yaml刪除。

3.3 HTTP方式


通過設置kubelet的啟動參數–mainfest-url,會定期從該URL下載Pod的定義文件,並以.yaml或.json文件的格式進行解析,從而創建Pod。

四 Pod容器共享Volume

4.1 共享Volume


在同一個Pod中的多個容器能夠共享Pod級別的存儲就Volume。Volume可以被定義為各種類型,多個容器各自進行掛載操作,將一個Volume掛載為容器內部需要的目錄。


示例1:

Pod級別設置Volume “app-logs”,同時Pod包含兩個容器,Tomcat向該Volume寫日誌,busybox讀取日誌文件。

  1 [root@k8smaster01 study]# vi pod-volume-applogs.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: volume-pod
  6 spec:
  7   containers:
  8   - name: tomcat
  9     image: tomcat
 10     ports:
 11     - containerPort: 8080
 12     volumeMounts:
 13     - name: app-logs
 14       mountPath: /usr/local/tomcat/logs
 15   - name: logreader
 16     image: busybox
 17     command: ["sh","-c","tail -f /logs/catalina*.log"]
 18     volumeMounts:
 19     - name: app-logs
 20       mountPath: /logs
 21   volumes:
 22   - name: app-logs
 23     emptyDir: {}

解釋:

Volume名:app-logs;

emptyDir:為Pod分配到Node的時候創建。無需指定宿主機的目錄文件,為Kubernetes自動分配的目錄。

  1 [root@k8smaster01 study]# kubectl create -f pod-volume-applogs.yaml	#創建
  2 [root@k8smaster01 study]# kubectl get pods				#查看
  3 [root@k8smaster01 study]# kubectl logs volume-pod -c busybox	#讀取log




  1 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- ls /usr/local/tomcat/logs
  2 catalina.2019-06-29.log      localhost_access_log.2019-06-29.txt
  3 host-manager.2019-06-29.log  manager.2019-06-29.log
  4 localhost.2019-06-29.log
  5 [root@k8smaster01 study]# kubectl exec -it volume-pod -c tomcat -- tail /usr/local/tomcat/logs/catalina.2019-06-29.log



提示:通過tomcat容器可查看日誌,對比busybox通過共享Volume查看的日誌是否一致。

五 Pod配置管理

5.1 Pod配置概述


應用部署的一個最佳實踐是將應用所需的配置信息與程序進行分離,使程序更加靈活。將相應的應用打包為鏡像,可以通過環境變量或者外掛volume的方式在創建容器的時候進行配置注入,從而實現更好的復用。

Kubernetes提供一種統一的應用配置管理方案:ConfigMap。

5.2 ConfigMap概述


ConfigMap供容器使用的主要場景:

  • 生成容器內部的環境變量;
  • 設置容器的啟動命令的參數(需設置為環境變量);
  • 以volume的形式掛載為容器內部的文件或者目錄。


ConfigMap以一個或多個key:value的形式定義。value可以是string也可以是一個文件內容,可以通過yaml配置文件或者通過kubectl create configmap 的方式創建configMap。

5.3 創建ConfigMap資源對象——yaml方式

  1 [root@k8smaster01 study]# vi cm-appvars.yaml
  2 apiVersion: v1
  3 kind: ConfigMap
  4 metadata:
  5   name: cm-appvars
  6 data:
  7   apploglevel: info
  8   appdatadir: /var/data
  9 
 10 [root@k8smaster01 study]# kubectl create -f cm-appvars.yaml
 11 configmap/cm-appvars created
 12 [root@k8smaster01 study]# kubectl get configmaps
 13 NAME         DATA   AGE
 14 cm-appvars   2      8s
 15 [root@k8smaster01 study]# kubectl describe configmaps cm-appvars



  1 [root@k8smaster01 study]# kubectl get configmaps cm-appvars -o yaml


5.4 創建ConfigMap資源對象——命令行方式


語法1

  1 # kubectl create configmap NAME --from-file=[key=]source --from-file=[key=]source



解釋:通過–from-file參數從文件中創建,可以指定key名稱,也可以制定多個key。

語法2

  1 # kubectl create configmap NAME --from-file=config-files-dir



解釋:通過–from-file參數從目錄中創建,該目錄下的每個配置文件名都被設置為key,文件的內容被設置為value。

語法3

  1 # kubectl create configmap NAME --from-literal=key1=value1 --from-literal=key2=value2



解釋:通過–from-literal參數從文本中創建,直接將指定的key#=value#創建為ConfigMap的內容。

5.5 Pod使用ConfigMap


容器應用使用ConfigMap有兩種方式:

  • 通過環境變量獲取ConfigMap中的內容;
  • 通過Volume掛載的方式將ConfigMap中的內容掛載為容器內容的文件或目錄。

  1 [root@k8smaster01 study]# vi cm-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: cm-test-pod
  6 spec:
  7   containers:
  8   - name: cm-test
  9     image: busybox
 10     command: ["/bin/sh","-c","env|grep APP"]	#容器里執行查看環境變量的命令
 11     env:
 12     - name: APPLOGLEVEL				#定義容器環境變量名稱
 13       valueFrom:
 14         configMapKeyRef:			#環境變量的值來自ConfigMap
 15           name: cm-appvars			#指定來自cm-appvars的ConfigMap
 16           key: apploglevel			#key為apploglevel
 17     - name: APPDATADIR
 18       valueFrom:
 19         configMapKeyRef:
 20           name: cm-appvars
 21           key: appdatadir
 22 
 23 [root@k8smaster01 study]# kubectl create -f cm-test-pod.yaml
 24 [root@k8smaster01 study]# kubectl get pods
 25 NAME          READY   STATUS      RESTARTS   AGE
 26 cm-test-pod   0/1     Completed   2          24s



【掛載形式-待補充】

5.6 ConfigMap限制


  • Configmap必須在pod創建之間創建;
  • ConfigMap受到namespace的限制,只有同一個命名空間下才能引用;
  • ConfigMap暫時無法配置配額;
  • 靜態的pod無法使用ConfigMap;
  • 在使用volumeMount掛載的時候,configMap基於items創建的文件會整體的將掛載數據卷的容器的目錄下的文件全部覆蓋。

六 Pod獲取自身信息

6.1 Downward API


pod擁有唯一的名字、IP地址,並且處於某個Namespace中。pod的容器內獲取pod的信息科通過Downward API實現。具體有以下兩種方式:

  • 環境變量:用於單個變量,可以將pod信息和container信息注入容器內部;
  • volume掛載:將數組類信息生成為文件,掛載至容器內部。


舉例1:通過Downward API將Pod的IP、名稱和所在的Namespace注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       command: [ "/bin/sh", "-c", "env" ]
 11       env:
 12         - name: MY_POD_NAME
 13           valueFrom:
 14             fieldRef:
 15               fieldPath: metadata.name
 16         - name: MY_POD_NAMESPACE
 17           valueFrom:
 18             fieldRef:
 19               fieldPath: metadata.namespace
 20         - name: MY_POD_IP
 21           valueFrom:
 22             fieldRef:
 23               fieldPath: status.podIP
 24   restartPolicy: Never



提示:Downward API提供如下變量:

metadata.name:Pod的名稱,當Pod通過RC生成時,其名稱是RC隨機產生的唯一名稱;

status.podIP:Pod的IP地址,POd的IP屬於狀態數據,而非元數據;

metadata.namespace:Pod所在的namespace。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod | grep MY_POD
  3 MY_POD_NAMESPACE=default
  4 MY_POD_IP=172.30.240.4
  5 MY_POD_NAME=dapi-test-pod
  6 



舉例2:通過Downward API將Container的自願請求和限制信息注入容器的環境變量。

  1 [root@k8smaster01 study]# vi dapi-test-pod-container-vars.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-container-vars
  6 spec:
  7   containers:
  8     - name: test-container
  9       image: busybox
 10       imagePullPolicy: Never
 11       command: [ "/bin/sh", "-c" ]
 12       args:
 13       - while true; do
 14           echo -en '\n';
 15           printenv MY_CPU_REQUEST MY_CPU_LIMIT;
 16           printenv MY_MEM_REQUEST MY_MEM_LIMIT;
 17           sleep 3600;
 18         done;
 19       resources:
 20         requests:
 21           memory: "32Mi"
 22           cpu: "125m"
 23         limits:
 24           memory: "64Mi"
 25           cpu: "250m"
 26       env:
 27         - name: MY_CPU_REQUEST
 28           valueFrom:
 29             resourceFieldRef:
 30               containerName: test-container
 31               resource: requests.cpu
 32         - name: MY_CPU_LIMIT
 33           valueFrom:
 34             resourceFieldRef:
 35               containerName: test-container
 36               resource: limits.cpu
 37         - name: MY_MEM_REQUEST
 38           valueFrom:
 39             resourceFieldRef:
 40               containerName: test-container
 41               resource: requests.memory
 42         - name: MY_MEM_LIMIT
 43           valueFrom:
 44             resourceFieldRef:
 45               containerName: test-container
 46               resource: limits.memory
 47   restartPolicy: Never



提示:Downward API提供如下變量:

requests.cpu:容器的CPU請求值;

limits.cpu:容器的CPU限制值;

requests.memory:容器的內存請求值;

limits.memory:容器的內存限制值。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-container-vars.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-container-vars
  3 1
  4 1
  5 33554432
  6 67108864



舉例3:通過Downward API將Pod的Label、Annotation列表通過Volume掛載為容器內的一個文件。

  1 [root@k8smaster01 study]# vi dapi-test-pod-volume.yaml
  2 apiVersion: v1
  3 kind: Pod
  4 metadata:
  5   name: dapi-test-pod-volume
  6   labels:
  7     zone: us-est-coast
  8     cluster: test-cluster1
  9     rack: rack-22
 10   annotations:
 11     build: two
 12     builder: john-doe
 13 spec:
 14   containers:
 15     - name: test-container
 16       image: busybox
 17       imagePullPolicy: Never
 18       command: [ "/bin/sh", "-c" ]
 19       args:
 20       - while true; do
 21           if [[ -e /etc/labels ]]; then
 22             echo -en '\n\n'; cat /etc/labels; fi;
 23           if [[ -e /etc/annotations ]]; then
 24             echo -en '\n\n'; cat /etc/annotations; fi;
 25           sleep 3600;
 26         done;
 27       volumeMounts:
 28         - name: podinfo
 29           mountPath: /etc
 30           readOnly: false
 31   volumes:
 32     - name: podinfo
 33       downwardAPI:
 34         items:
 35           - path: "labels"
 36             fieldRef:
 37               fieldPath: metadata.labels
 38           - path: "annotations"
 39             fieldRef:
 40               fieldPath: metadata.annotations



注意:Volume中的ddownwardAPI的items語法,將會以path的名稱生成文件。如上所示,會在容器內生產/etc/labels和/etc/annotations兩個文件,分別包含metadata.labels和metadata.annotations的全部Label。

  1 [root@k8smaster01 study]# kubectl create -f dapi-test-pod-volume.yaml
  2 [root@k8smaster01 study]# kubectl logs dapi-test-pod-volume
  3 



提示:DownwardAPI意義:

在某些集群中,集群中的每個節點需要將自身的標識(ID)及進程綁定的IP地址等信息事先寫入配置文件中,進程啟動時讀取此類信息,然後發布到某個類似註冊服務中心。此時可通過DowanwardAPI,將一個預啟動腳本或Init Container,通過環境變量或文件方式獲取Pod自身的信息,然後寫入主程序配置文件中,最後啟動主程序。本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

Appium+python自動化(四十一)-Appium自動化測試框架綜合實踐 – 即將落下帷幕(超詳解)

1.簡介

  今天我們緊接着上一篇繼續分享Appium自動化測試框架綜合實踐 – 代碼實現。到今天為止,大功即將告成;框架所需要的代碼實現都基本完成。

2.data數據封裝

2.1使用背景

在實際項目過程中,我們的數據可能是存儲在一個數據文件中,如txt,excel、csv文件類型。我們可以封裝一些方法來讀取文件中的數據來實現數據驅動。

2.2案例

將測試賬號存儲在account.csv文件,內容如下:

account.csv

hg2018

hg2018

hg2019

zxw2019

666

222

參考代碼

2.3enumerate()簡介

enumerate()是python的內置函數

  • enumerate在字典上是枚舉、列舉的意思
  • 對於一個可迭代的(iterable)/可遍歷的對象(如列表、字符串),enumerate將其組成一個索引序列,利用它可以同時獲得索引和值
  • enumerate多用於在for循環中得到計數。

2.4enumerate()使用

如果對一個列表,既要遍歷索引又要遍曆元素時,首先可以這樣寫:

參考代碼
list = ["", "", "一個", "測試","數據"]

for i in range(len(list)):

    print(i,list[i])

上述方法有些累贅,利用enumerate()會更加直接和優美:

參考代碼
list1 = ["", "", "一個", "測試","數據"]

for index, item in enumerate(list1):

        print(index,item)

3.數據讀取方法封裝

  數據讀取方法也屬於公共方法,這裏我們首先實現一下,然後將其封裝到裡邊即可。

3.1數據讀取方法實現的參考代碼

import csv


     def get_csv_data(csv_file,line):

        with open(csv_file, 'r', encoding='utf-8-sig') as file:

            reader=csv.reader(file)

            for index, row in enumerate(reader,1):

                if index == line:

                    return row

 

    csv_file='../data/account.csv'

    data=get_csv_data(csv_file,3)

    print(data)

3.2封裝

將其封裝在公共方法中,在其他地方用到的時候,直接導入調用即可。

4.utf-8與utf-8-sig兩種編碼格式的區別

UTF-8以字節為編碼單元,它的字節順序在所有系統中都是一樣的,沒有字節序的問題,也因此它實際上並不需要BOM(“ByteOrder Mark”)。但是UTF-8 with BOM即utf-8-sig需要提供BOM。

5.config文件配置

各種配置文件都放在這個目錄下。

5.1日誌文件配置 

主要是一些日誌信息的配置。

log.config

 參考代碼
[loggers]
keys=root,infoLogger

[logger_root]
level=DEBUG
handlers=consoleHandler,fileHandler

[logger_infoLogger]
handlers=consoleHandler,fileHandler
qualname=infoLogger
propagate=0

[handlers]
keys=consoleHandler,fileHandler

[handler_consoleHandler]
class=StreamHandler
level=INFO
formatter=form02
args=(sys.stdout,)

[handler_fileHandler]
class=FileHandler
level=INFO
formatter=form01
args=('../logs/runlog.log', 'a')

[formatters]
keys=form01,form02

[formatter_form01]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s

[formatter_form02]
format=%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s

6.測試用例封裝

這裏宏哥舉例給小夥伴們演示:封裝註冊和登錄兩個測試用例。

6.1測試用例執行開始結束操作封裝

測試用例執行開始和結束的封裝,其他模塊用到直接導入,調用即可。

myunit.py

參考代碼
# coding=utf-8
# 1.先設置編碼,utf-8可支持中英文,如上,一般放在第一行

# 2.註釋:包括記錄創建時間,創建人,項目名稱。
'''
Created on 2019-11-20
@author: 北京-宏哥   QQ交流群:707699217
Project:Appium自動化測試框架綜合實踐 - 代碼實現
'''
# 3.導入模塊
import unittest
from kyb_testProject.common.desired_caps import appium_desired
import logging
from time import sleep

class StartEnd(unittest.TestCase):
    def setUp(self):
        logging.info('=====setUp====')
        self.driver=appium_desired()

    def tearDown(self):
        logging.info('====tearDown====')
        sleep(5)
        self.driver.close_app()

6.2註冊用例

開始註冊用例代碼邏輯的實現。

test_register.py

參考代碼
# coding=utf-8
# 1.先設置編碼,utf-8可支持中英文,如上,一般放在第一行

# 2.註釋:包括記錄創建時間,創建人,項目名稱。
'''
Created on 2019-11-20
@author: 北京-宏哥   QQ交流群:707699217
Project:Appium自動化測試框架綜合實踐 - 代碼實現
'''
# 3.導入模塊
from kyb_testProject.common.myunit import StartEnd
from kyb_testProject.businessView.registerView import RegisterView
import logging,random,unittest

class RegisterTest(StartEnd):
    def test_user_register(self):
        logging.info('======test_user_register======')
        r=RegisterView(self.driver)

        username = 'bjhg2019' + 'fly' + str(random.randint(1000, 9000))
        password = 'bjhg2020' + str(random.randint(1000, 9000))
        email = 'bjhg' + str(random.randint(1000, 9000)) + '@163.com'

        self.assertTrue(r.register_action(username,password,email))

if __name__ == '__main__':
    unittest.main()

6.3登錄用例

開始登錄用例代碼邏輯的實現。

test_login.py

參考代碼
# coding=utf-8
# 1.先設置編碼,utf-8可支持中英文,如上,一般放在第一行

# 2.註釋:包括記錄創建時間,創建人,項目名稱。
'''
Created on 2019-11-13
@author: 北京-宏哥   QQ交流群:707699217
Project:Appium自動化測試框架綜合實踐 - 代碼實現
'''
# 3.導入模塊
from kyb_testProject.common.myunit import StartEnd
from kyb_testProject.businessView.loginView import LoginView
import unittest
import logging

class TestLogin(StartEnd):
    csv_file='../data/account.csv'

    @unittest.skip('test_login_zxw2018')
    def test_login_zxw2018(self):
        logging.info('======test_login_zxw2018=====')
        l=LoginView(self.driver)
        data=l.get_csv_data(self.csv_file,2)

        l.login_action(data[0],data[1])
        self.assertTrue(l.check_loginStatus())

    # @unittest.skip('skip test_login_zxw2017')
    def test_login_zxw2017(self):
        logging.info('======test_login_zxw2017=====')
        l=LoginView(self.driver)
        data = l.get_csv_data(self.csv_file, 1)

        l.login_action(data[0], data[1])
        self.assertTrue(l.check_loginStatus())

    @unittest.skip('test_login_error')
    def test_login_error(self):
        logging.info('======test_login_error=====')
        l = LoginView(self.driver)
        data = l.get_csv_data(self.csv_file, 3)

        l.login_action(data[0], data[1])
        self.assertTrue(l.check_loginStatus(),msg='login fail!')

if __name__ == '__main__':
    unittest.main()

7.小結

到此,Appium自動化測試框架就差下一篇就全部完成了,聰明的你都懂了嗎???嘿嘿!慢慢地來吧。

下節預告

下一篇,講解執行測試用例,生成測試報告,以及自動化平台,請關注宏哥,敬請期待!!!

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

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

Netty學習篇⑤–編、解碼源碼分析

前言

學習Netty也有一段時間了,Netty作為一個高性能的異步框架,很多RPC框架也運用到了Netty中的知識,在rpc框架中豐富的數據協議及編解碼可以讓使用者更加青睞;
Netty支持豐富的編解碼框架,其本身內部提供的編解碼也可以應對各種業務場景;
今天主要就是學習下Netty中提供的編、解碼類,之前只是簡單的使用了下Netty提供的解碼類,今天更加深入的研究下Netty中編、解碼的源碼及部分使用。

編、解碼的概念

  • 編碼(Encoder)

    編碼就是將我們發送的數據編碼成字節數組方便在網絡中進行傳輸,類似Java中的序列化,將對象序列化成字節傳輸
  • 解碼(Decoder)

    解碼和編碼相反,將傳輸過來的字節數組轉化為各種對象來進行展示等,類似Java中的反序列化
    如:
    // 將字節數組轉化為字符串
    new String(byte bytes[], Charset charset)

編、解碼超類

ByteToMessageDecoder: 解碼超類,將字節轉換成消息

解碼解碼一般用於將獲取到的消息解碼成系統可識別且自己需要的數據結構;因此ByteToMessageDecoder需要繼承ChannelInboundHandlerAdapter入站適配器來獲取到入站的數據,在handler使用之前通過channelRead獲取入站數據進行一波解碼;
ByteToMessageDecoder類圖

源碼分析

通過channelRead獲取入站數據,將數據緩存至cumulation數據緩衝區,最後在傳給decode進行解碼,在read完成之後清空緩存的數據

1. 獲取入站數據

/**
*  通過重寫channelRead方法來獲取入站數據
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    // 檢測是否是byteBuf對象格式數據
    if (msg instanceof ByteBuf) {
        // 實例化字節解碼成功輸出集合 即List<Object> out
        CodecOutputList out = CodecOutputList.newInstance();
        try {
            // 獲取到的請求的數據
            ByteBuf data = (ByteBuf) msg;
            // 如果緩衝數據區為空則代表是首次觸發read方法
            first = cumulation == null;
            if (first) {
                // 如果是第一次read則當前msg數據為緩衝數據
                cumulation = data;
            } else {
                // 如果不是則觸發累加,將緩衝區的舊數據和新獲取到的數據通過        expandCumulation 方法累加在一起存入緩衝區cumulation
                // cumulator 累加類,將緩衝池中數據和新數據進行組合在一起
                // private Cumulator cumulator = MERGE_CUMULATOR;
                cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
            }
            // 將緩衝區數據cumulation進行解碼
            callDecode(ctx, cumulation, out);
        } catch (DecoderException e) {
            throw e;
        } catch (Throwable t) {
            throw new DecoderException(t);
        } finally {
            // 在解碼完畢后釋放引用和清空全局字節緩衝區
            if (cumulation != null && !cumulation.isReadable()) {
                numReads = 0;
                cumulation.release();
                cumulation = null;
                // discardAfterReads為netty中設置的讀取多少次后開始丟棄字節 默認值16
                // 可通過setDiscardAfterReads(int n)來設置值不設置默認16次
            } else if (++ numReads >= discardAfterReads) {
                // We did enough reads already try to discard some bytes so we not risk to see a OOME.
                // 在我們讀取了足夠的數據可以嘗試丟棄一些字節已保證不出現內存溢出的異常
                // 
                // See https://github.com/netty/netty/issues/4275
                // 讀取次數重置為0
                numReads = 0;
                // 重置讀寫指針或丟棄部分已讀取的字節
                discardSomeReadBytes();
            }
            // out為解碼成功的傳遞給下一個handler
            int size = out.size();
            decodeWasNull = !out.insertSinceRecycled();
            // 結束當前read傳遞到下個ChannelHandler
            fireChannelRead(ctx, out, size);
            // 回收響應集合 將insertSinceRecycled設置為false;
            // insertSinceRecycled用於channelReadComplete判斷使用
            out.recycle();
        }
    } else {
        // 不是的話直接fire傳遞給下一個handler
        ctx.fireChannelRead(msg);
    }
}
2. 初始化字節緩衝區計算器: Cumulator主要用於全局字節緩衝區和新讀取的字節緩衝區組合在一起擴容
public static final Cumulator MERGE_CUMULATOR = new Cumulator() {
    
    /**
    * alloc ChannelHandlerContext分配的字節緩衝區
    * cumulation 當前ByteToMessageDecoder類全局的字節緩衝區
    * in 入站的字節緩衝區
    **/
    @Override
    public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
        final ByteBuf buffer;
        // 如果全局ByteBuf寫入的字節+當前入站的字節數據大於全局緩衝區最大的容量或者全局緩衝區的引用數大於1個或全局緩衝區只讀
        if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes()
            || cumulation.refCnt() > 1 || cumulation.isReadOnly()) {
            // Expand cumulation (by replace it) when either there is not more room in the buffer
            // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or
            // duplicate().retain() or if its read-only.
            //
            // See:
            // - https://github.com/netty/netty/issues/2327
            // - https://github.com/netty/netty/issues/1764
            // 進行擴展全局字節緩衝區(容量大小 = 新數據追加到舊數據末尾組成新的全局字節緩衝區)
            buffer = expandCumulation(alloc, cumulation, in.readableBytes());
        } else {
            buffer = cumulation;
        }
        // 將新數據寫入緩衝區
        buffer.writeBytes(in);
        // 釋放當前的字節緩衝區的引用
        in.release();
        
        return buffer;
    }
};


/**
* alloc 字節緩衝區操作類
* cumulation 全局累加字節緩衝區
* readable 讀取到的字節數長度
*/
// 字節緩衝區擴容方法
static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) {
    // 舊數據
    ByteBuf oldCumulation = cumulation;
    // 通過ByteBufAllocator將緩衝區擴大到oldCumulation + readable大小
    cumulation = alloc.buffer(oldCumulation.readableBytes() + readable);
    // 將舊數據重新寫入到新的字節緩衝區
    cumulation.writeBytes(oldCumulation);
    // 舊字節緩衝區引用-1
    oldCumulation.release();
    return cumulation;
}
3. ByteBuf釋放當前字節緩衝區的引用: 通過調用ReferenceCounted接口中的release方法來釋放
@Override
public boolean release() {
    return release0(1);
}

@Override
public boolean release(int decrement) {
    return release0(checkPositive(decrement, "decrement"));
}

/**
* decrement 減量
*/
private boolean release0(int decrement) {
    for (;;) {
        int refCnt = this.refCnt;
        // 當前引用小於減量
        if (refCnt < decrement) {
            throw new IllegalReferenceCountException(refCnt, -decrement);
        }
        // 這裏就利用里線程併發中的知識CAS,線程安全的設置refCnt的值
        if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) {
            // 如果減量和引用量相等
            if (refCnt == decrement) {
                // 全部釋放
                deallocate();
                return true;
            }
            return false;
        }
    }
}

4. 將全局字節緩衝區進行解碼

/**
* ctx ChannelHandler的上下文,用於傳輸數據與下一個handler來交互
* in 入站數據
* out 解析之後的出站集合 (此出站不是返回給客戶端的而是傳遞給下個handler的)
*/
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    try {
        // 如果入站數據還有沒解析的
        while (in.isReadable()) {
            // 解析成功的出站集合長度
            int outSize = out.size();
            // 如果大於0則說明解析成功的數據還沒被消費完,直接fire掉給通道中的後續handler繼續                消費
            if (outSize > 0) {
                fireChannelRead(ctx, out, outSize);
                out.clear();

                // Check if this handler was removed before continuing with decoding.
                // 在這個handler刪除之前檢查是否還在繼續解碼
                // If it was removed, it is not safe to continue to operate on the buffer.
                // 如果移除了,它繼續操作緩衝區是不安全的
                //
                // See:
                // - https://github.com/netty/netty/issues/4635
                if (ctx.isRemoved()) {
                    break;
                }
                outSize = 0;
            }
            // 入站數據字節長度
            int oldInputLength = in.readableBytes();
            // 開始解碼數據
            decodeRemovalReentryProtection(ctx, in, out);

            // Check if this handler was removed before continuing the loop.
            // 
            // If it was removed, it is not safe to continue to operate on the buffer.
            //
            // See https://github.com/netty/netty/issues/1664
            if (ctx.isRemoved()) {
                break;
            }

            // 解析完畢跳出循環
            if (outSize == out.size()) {
                if (oldInputLength == in.readableBytes()) {
                    break;
                } else {
                    continue;
                }
            }

            if (oldInputLength == in.readableBytes()) {
                throw new DecoderException(
                    StringUtil.simpleClassName(getClass()) +
                    ".decode() did not read anything but decoded a message.");
            }

            if (isSingleDecode()) {
                break;
            }
        }
    } catch (DecoderException e) {
        throw e;
    } catch (Throwable cause) {
        throw new DecoderException(cause);
    }
}

final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // 設置解碼狀態為正在解碼  STATE_INIT = 0; STATE_CALLING_CHILD_DECODE = 1;             STATE_HANDLER_REMOVED_PENDING = 2; 分別為初始化; 解碼; 解碼完畢移除
        decodeState = STATE_CALLING_CHILD_DECODE;
        try {
            // 具體的解碼邏輯(netty提供的解碼器或自定義解碼器中重寫的decode方法)
            decode(ctx, in, out);
        } finally {
            // 此時decodeState為正在解碼中 值為1,返回false
            boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING;
            // 在設置為初始化等待解碼
            decodeState = STATE_INIT;
            // 解碼完成移除當前ChannelHandler標記為不處理
            // 可以看看handlerRemoved源碼。如果緩衝區還有數據直接傳遞給下一個handler
            if (removePending) {
                handlerRemoved(ctx);
            }
        }
    }
5. 執行channelReadComplete
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    // 讀取次數重置
    numReads = 0;
    // 重置讀寫index
    discardSomeReadBytes();
    // 在channelRead meth中定義賦值 decodeWasNull = !out.insertSinceRecycled();
    // out指的是解碼集合List<Object> out; 咱們可以點進
    if (decodeWasNull) {
        decodeWasNull = false;
        if (!ctx.channel().config().isAutoRead()) {
            ctx.read();
        }
    }
    // fire掉readComplete傳遞到下一個handler的readComplete
    ctx.fireChannelReadComplete();
}

/**
*  然後我們可以搜索下insertSinceRecucled在什麼地方被賦值了
* Returns {@code true} if any elements where added or set. This will be reset once {@link #recycle()} was called.
*/
boolean insertSinceRecycled() {
    return insertSinceRecycled;
}


// 搜索下insert的調用我們可以看到是CodecOutputList類即為channelRead中的out集合,眾所周知在    decode完之後,解碼數據就會被調用add方法,此時insertSinceRecycled被設置為true
private void insert(int index, Object element) {
    array[index] = element;
    insertSinceRecycled = true;
}


/**
* 清空回收數組內部的所有元素和存儲空間
* Recycle the array which will clear it and null out all entries in the internal storage.
*/
// 搜索recycle的調用我么可以知道在channelRead的finally邏輯中 調用了out.recycle();此時        insertSinceRecycled被設置為false
void recycle() {
    for (int i = 0 ; i < size; i ++) {
        array[i] = null;
    }
    clear();
    insertSinceRecycled = false;
    handle.recycle(this);
}

至此ByteToMessageDecoder解碼類應該差不多比較清晰了!!!

MessageToByteEncoder: 編碼超類,將消息轉成字節進行編碼發出

何謂編碼,就是將發送數據轉化為客戶端和服務端約束好的數據結構和格式進行傳輸,我們可以在編碼過程中將消息體body的長度和一些頭部信息有序的設置到ByteBuf字節緩衝區中;方便解碼方靈活的運用來判斷(是否完整的包等)和處理業務;解碼是繼承入站數據,反之編碼應該繼承出站的數據;接下來我們看看編碼類是怎麼進行編碼的;
MessageToByteEncoder類圖如下

源碼分析

既然是繼承出站類,我們直接看看write方法是怎麼樣的

/**
* 通過write方法獲取到出站的數據即要發送出去的數據
* ctx channelHandler上下文
* msg 發送的數據 Object可以通過繼承類指定的泛型來指定
* promise channelPromise異步監聽,類似ChannelFuture,只不過promise可以設置監聽的結果,future只能通過獲取監聽的成功失敗結果;可以去了解下promise和future的區別
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        // 檢測發送數據的類型 通過TypeParameterMatcher類型匹配器
        if (acceptOutboundMessage(msg)) {
            @SuppressWarnings("unchecked")
            I cast = (I) msg;
            // 分配字節緩衝區 preferDirect默認為true
            buf = allocateBuffer(ctx, cast, preferDirect);
            try {
                // 進行編碼
                encode(ctx, cast, buf);
            } finally {
                // 完成編碼后釋放對象的引用
                ReferenceCountUtil.release(cast);
            }
            // 如果緩衝區有數據則通過ctx發送出去,promise可以監聽數據傳輸並設置是否完成
            if (buf.isReadable()) {
                ctx.write(buf, promise);
            } else {
                // 如果沒有數據則釋放字節緩衝區的引用併發送一個empty的空包
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            // 非TypeParameterMatcher類型匹配器匹配的類型直接發送出去
            ctx.write(msg, promise);
        }
    } catch (EncoderException e) {
        throw e;
    } catch (Throwable e) {
        throw new EncoderException(e);
    } finally {
        if (buf != null) {
            buf.release();
        }
    }
}

// 初始化設置preferDirect為true
protected MessageToByteEncoder() {
    this(true);
}
protected MessageToByteEncoder(boolean preferDirect) {
    matcher = TypeParameterMatcher.find(this, MessageToByteEncoder.class, "I");
    this.preferDirect = preferDirect;
}

編碼: 重寫encode方法,根據實際業務來進行數據編碼

// 此處就是我們需要重寫的編碼方法了,我們和根據約束好的或者自己定義好想要的數據格式發送給對方

// 下面是我自己寫的demo的編碼方法;頭部設置好body的長度,服務端可以根據長度來判斷是否是完整的包,僅僅自學寫的簡單的demo非正常線上運營項目的邏輯
public class MyClientEncode extends MessageToByteEncoder<String> {

    @Override
    protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
        if (null != msg) {
            byte[] request = msg.getBytes(Charset.forName("UTF-8"));
            out.writeInt(request.length);
            out.writeBytes(request);
        }
    }
}

編碼類相對要簡單很多,因為只需要將發送的數據序列化,按照一定的格式進行發送數據!!!

項目實戰

項目主要簡單的實現下自定義編解碼器的運用及LengthFieldBasedFrameDecoder的使用

  • 項目結構如下
    │  hetangyuese-netty-06.iml
    │  pom.xml
    │
    ├─src
    │  ├─main
    │  │  ├─java
    │  │  │  └─com
    │  │  │      └─hetangyuese
    │  │  │          └─netty
    │  │  │              ├─client
    │  │  │              │      MyClient06.java
    │  │  │              │      MyClientChannelInitializer.java
    │  │  │              │      MyClientDecoder.java
    │  │  │              │      MyClientEncode.java
    │  │  │              │      MyClientHandler.java
    │  │  │              │      MyMessage.java
    │  │  │              │
    │  │  │              └─server
    │  │  │                      MyChannelInitializer.java
    │  │  │                      MyServer06.java
    │  │  │                      MyServerDecoder.java
    │  │  │                      MyServerDecoderLength.java
    │  │  │                      MyServerEncoder.java
    │  │  │                      MyServerHandler.java
    │  │  │
    │  │  └─resources
    │  └─test
    │      └─java
    
  • 服務端

    Serverhandler: 只是簡單的將解碼的內容輸出

    public class MyServerHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("客戶端連接成功 time: " + new Date().toLocaleString());
        }
    
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("客戶端斷開連接 time: " + new Date().toLocaleString());
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            String body = (String) msg;
            System.out.println("content:" + body);
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            // 出現異常關閉通道
            cause.printStackTrace();
            ctx.close();
        }
    }

    解碼器

    public class MyServerDecoder extends ByteToMessageDecoder {
    
        // 此處我頭部只塞了長度字段佔4個字節,別問為啥我知道,這是要客戶端和服務端約束好的
        private static int min_head_length = 4;
    
        @Override
        protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
            // 解碼的字節長度
            int size = in.readableBytes();
            if(size < min_head_length) {
                System.out.println("解析的數據長度小於頭部長度字段的長度");
                return ;
            }
            // 讀取的時候指針已經移位到長度字段的尾端
            int length = in.readInt();
            if (size < length) {
                System.out.println("解析的數據長度與長度不符合");
                return ;
            }
    
            // 上面已經讀取到了長度字段,後面的長度就是body
            ByteBuf decoderArr = in.readBytes(length);
            byte[] request = new byte[decoderArr.readableBytes()];
            // 將數據寫入空數組
            decoderArr.readBytes(request);
            String body = new String(request, Charset.forName("UTF-8"));
            out.add(body);
        }
    }

    將解碼器加入到channelHandler中:記得加到業務handler的前面否則無效

    public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
    //                .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0))
    //                .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0))
                    .addLast(new MyServerDecoder())
                    .addLast(new MyServerHandler())
            ;
        }
    }
  • 客戶端

    ClientHandler

    public class MyClientHandler extends ChannelInboundHandlerAdapter {
    
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("與服務端連接成功");
            for (int i = 0; i<10; i++) {
                ctx.writeAndFlush("hhhhh" + i);
            }
        }
    
        @Override
        public void channelInactive(ChannelHandlerContext ctx) throws Exception {
            System.out.println("與服務端斷開連接");
        }
    
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("收到服務端消息:" +msg+ " time: " + new Date().toLocaleString());
        }
    
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            cause.printStackTrace();
            ctx.close();
        }
    }

    編碼器

    public class MyClientEncode extends MessageToByteEncoder<String> {
    
        @Override
        protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception {
            if (null != msg) {
                byte[] request = msg.getBytes(Charset.forName("UTF-8"));
                out.writeInt(request.length);
                out.writeBytes(request);
            }
        }
    }

    將編碼器加到ClientHandler的前面

    public class MyClientChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
                    .addLast(new MyClientDecoder())
                    .addLast(new MyClientEncode())
                    .addLast(new MyClientHandler())
            ;
    
        }
    }
  • 服務端運行結果
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 16:35:47
    content:hhhhh0
    content:hhhhh1
    content:hhhhh2
    content:hhhhh3
    content:hhhhh4
    content:hhhhh5
    content:hhhhh6
    content:hhhhh7
    content:hhhhh8
    content:hhhhh9
  • 如果不用自定義的解碼器怎麼獲取到body內容呢

    將自定義編碼器換成LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)

    public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
    
        @Override
        protected void initChannel(SocketChannel ch) throws Exception {
            ch.pipeline()
    //                .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0))
                    .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0))
    //                .addLast(new MyServerDecoder())
                    .addLast(new MyServerHandler())
            ;
        }
    }
    
    // 怕忘記的各個參數的含義在這在說明一次,自己不斷的修改每個值觀察結果就可以更加深刻的理解
    /**
    * maxFrameLength:消息體的最大長度,好像默認最大值為1024*1024
    * lengthFieldOffset 長度字段所在字節數組的下標 (我這是第一個write的所以下標是0)
    * lengthFieldLength 長度字段的字節長度(int類型佔4個字節)
    * lengthAdjustment 長度字段補償的數值 (lengthAdjustment =  數據包長度 - lengthFieldOffset - lengthFieldLength - 長度域的值),解析需要減去對應的數值
    * initialBytesToStrip 是否去掉長度字段(0不去除,對應長度域字節長度)
    */
    public LengthFieldBasedFrameDecoder(
                int maxFrameLength,
                int lengthFieldOffset, int lengthFieldLength,
                int lengthAdjustment, int initialBytesToStrip)
    結果: 前都帶上了長度
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh0, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh1, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh2, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh3, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh4, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh5, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh6, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh7, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh8, time: 2019-11-19 17:53:42
    收到客戶端發來的消息:   hhhhh9, time: 2019-11-19 17:53:42

    如果我們在客戶端的長度域中做手腳 LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)

    舊: out.writeInt(request.length);
    新: out.writeInt(request.length + 1);
    // 看結果就不正常,0後面多了一個0;但是不知道為啥只解碼了一次??? 求解答
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 17:56:55
    收到客戶端發來的消息:   hhhhh0 , time: 2019-11-19 17:56:55
    
    // 正確修改為 LengthFieldBasedFrameDecoder(10240, 0, 4, -1, 0)
    // 結果:
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh0, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh1, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh2, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh3, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh4, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh5, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh6, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh7, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh8, time: 2019-11-19 18:02:18
    收到客戶端發來的消息:   hhhhh9, time: 2019-11-19 18:02:18

    捨棄長度域 :LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 4)

    // 結果
    MyServer06 is start ...................
    客戶端連接成功 time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh0, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh1, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh2, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh3, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh4, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh5, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh6, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh7, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh8, time: 2019-11-19 18:03:44
    收到客戶端發來的消息:hhhhh9, time: 2019-11-19 18:03:44
    分析源碼示例中的 lengthAdjustment = 消息字節長度 – lengthFieldOffset-lengthFieldLength-長度域中的值
  • 源碼中的示例
     * <pre>
     * lengthFieldOffset   =  0
     * lengthFieldLength   =  2
     * <b>lengthAdjustment</b>    = <b>-2</b> (= the length of the Length field)
     * initialBytesToStrip =  0
     *
     * BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
     * +--------+----------------+      +--------+----------------+
     * | Length | Actual Content |----->| Length | Actual Content |
     * | 0x000E | "HELLO, WORLD" |      | 0x000E | "HELLO, WORLD" |
     * +--------+----------------+      +--------+----------------+
     * </pre>
    長度域中0x000E為16進制,轉換成10進制是14,說明消息體長度為14;根據公式:14-0-2-14 = -2
    * <pre>
     * lengthFieldOffset   = 0
     * lengthFieldLength   = 3
     * <b>lengthAdjustment</b>    = <b>2</b> (= the length of Header 1)
     * initialBytesToStrip = 0
     *
     * BEFORE DECODE (17 bytes)                      AFTER DECODE (17 bytes)
     * +----------+----------+----------------+      +----------+----------+----------------+
     * |  Length  | Header 1 | Actual Content |----->|  Length  | Header 1 | Actual Content |
     * | 0x00000C |  0xCAFE  | "HELLO, WORLD" |      | 0x00000C |  0xCAFE  | "HELLO, WORLD" |
     * +----------+----------+----------------+      +----------+----------+----------------+
     * </pre>
    從上的例子可以知道;lengthAdjustment(2) = 17- 12(00000C)-lengthFieldOffset(0) - lengthFieldLength(3);

    …….等等

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

Graphviz 畫圖的一些總結

Graphviz 是一個自動排版的作圖軟件,可以生成 png pdf 等格式。

一切以官方文檔為準,博客只是參考。這裏做一個自己學習的記錄。

dot 語言

Graphviz 構建組件為 圖,節點,邊,用屬性對其進行描述。

以下是定義DOT語言的抽象語法,約束的規則如下:

  • 元素的終止以 粗體 显示
  • 文字字符用單引號 引起來
  • 圓括號 () 的內容為必選項
  • 方括號 [] 為可選項目
  • 豎杠 | 為擇一選擇
聲明 結構
graph [ strict ] (graph | digraph) [ ID ] ‘{‘ stmt_list ‘}’
stmt_list [ stmt [ ‘;‘ ] stmt_list ]
stmt node_stmt | edge_stmt | attr_stmt | ID ‘=‘ ID | subgraph
attr_stmt (graph | node | edge) attr_list
attr_list ‘[‘** [ a_list ] **’]’ [ attr_list ]
a_list ID ‘=’ ID [ (‘;’ | ‘,’) ] [ a_list ]
edge_stmt (node_id | subgraph) edgeRHS [ attr_list ]
edgeRHS edgeop (node_id | subgraph) [ edgeRHS ]
node_stmt node_id [ attr_list ]
node_id ID [ port ]
port :‘ ID [ ‘:‘ compass_pt ] | ‘:‘ compass_pt
subgraph [ subgraph [ ID ] ] ‘{‘ stmt_list ‘}
compass_pt (n | ne | e | se | s | sw | w | nw | c | _)

ID 其實就是一個字符串,為該組件的名稱或者屬性的名稱,命名規則如下:

  1. 所有的字母 [a-zA-Z\200-\377] 下劃線,数字 [0-9],数字不能出現在起始位置
  2. 純数字 $[-]^?(.[0-9]^+ | [0-9]^+(.[0-9]*)6? $
  3. 所有用雙引號引用的字符串 "..."
  4. HTML 格式的字符串 <>

dot 語法的關鍵字

  • strict, 嚴格的圖限定,禁止創建多個相同的邊
  • graph, 無向圖. 在圖的創建時必須聲明為有向圖還是無向圖
  • digraph, 有向圖
  • node, 節點
  • edge, 邊
  • subgraph, 子圖

通過 dot 的抽象語法可以看到

  1. 整個 graph 必須使用 graph 或 digraph {} 進行限定說明圖的屬性
  2. 圖裡面的聲明列表可以為空,也可以為多個,每個聲明后的 ; 為可選項
  3. 聲明有幾種類型
    1. 節點 node
    2. edge
    3. 子圖 subgraph
    4. 屬性列表
    5. ID = ID, 這個類型暫時還沒有看到有什麼作用
  4. 屬性列表
    1. 必須使用中括號 [ ] 將列表項括起來
    2. 列表項為可選
  5. 屬性列表項
    1. 以 key = value 的形式存在,列表項可選擇 ‘,‘ 和 ‘;‘ 結尾
    2. 可存在多個列表項
  6. 邊的聲明
    1. 首端為 節點標識符或者子圖,
    2. 右部分由邊連接節點標識符或者子圖構成,右部分可以存在多個
    3. 尾部可選屬性列表
  7. 節點的聲明
    示例 節點的用法 node0 [label = "<postid1> string|<postid2> string|<postid3> string3", height=.5]` node0:head[color=lightblue] // 設置該部分的顏色
    1. 首部為節點標識符 節點部分(post) 方向 組成,其中后兩項為可選項。
    2. 後半部分為可選的屬性列表
方向 說明
n north 北
ne north east
e east 東
se south east 東南
s south 南
sw south west 西南
w west 西
nw north west 西北
c center 中部
_ 任意

一個方向的示例

digraph action {
    node [shape = record,height=.1];

    node0 [label = "<head> head|<body> body|<foot> foot", height=.5]
    node2 [shape = box label="mind"]

    node0:head:n -> node2:n [label = "n"]
    node0:head:ne -> node2:ne [label = "ne"]
    node0:head:e -> node2:e [label = "e"]
    node0:head:se -> node2:se [label = "se"]
    node0:head:s -> node2:s [label = "s"]
    node0:head:sw -> node2:sw [label = "sw"]
    node0:head:w -> node2:w [label = "w"]
    node0:head:nw -> node2:nw [label = "nw"]
    node0:head:c -> node2:c [label = "c"]
    node0:head:_ -> node2:_ [label = "_"]

    node0:body[style=filled color=lightblue]
}

效果如下 圖-1

繪製屬性

一個圖中有非常多的 node 和 edge,如果每次都需要聲明一個節點的屬性會非常麻煩,有一個簡單的方式為聲明一個公共的屬性如

digraph action {
    rankdir = LR // 設置方向
    node [shape=box color=blue]
    edge [color=red]

    node1 // 默認節點屬性
    node2 [color=lightblue] // 屬於該節點的顏色屬性
    node1 -> node2 // 默認邊屬性 
    node2 -> node1 [color=green] // 屬於該變的屬性
}

在聲明位置之後的節點都有一個 默認 的形狀和顏色屬性。

全部的屬性見,這裏列舉部分常用的屬性

  • charset 編碼,一般設置 UTF-8
  • fontname 字體名稱,這個在中文的情況需要設置,否則導出圖片的時候會亂碼,一般設置微軟雅黑(“Microsoft YaHei”), linux 下也是同樣設置系統帶的字體就好,其他字體設置見 屬性
  • fontcolor 字體顏色
  • fontsize 字體大小,用於文本內容
  • fillcolor 用於填充節點或者集群(cluster)的背景顏色。
  • size 圖形的最大寬度和高度
  • label 圖形上的文本標記
  • margin 設置圖形的邊距
  • pad 指定將繪製區域擴展到繪製圖形所需的最小區域的長度(以英寸為單位)
  • style 設置圖形組件的樣式信息。 對於聚類子圖或者節點,如果style = “filled”,則填充聚類框的背景
  • rankdir 設置圖形布局的排列方向 (全局只有一個生效). “TB”, “LR”, “BT”, “RL”, 分別對應於從上到下,從左到右,從下到上和從右到左繪製的有向圖。
  • ranksep 以英寸為單位提供所需的排列間隔
  • ratio 設置生成圖片的縱橫比

節點(node)

節點的默認屬性為 shape = ellipse, width = .75, height = 0.5 並且用節點標識符作為節點的显示文字。

如圖一中所示,聲明兩個節點 node0 和 node2,node0 或 node2 就表示這個節點的節點標識符,後面緊跟的是該節點的屬性列表;另一種用法為 節點標識符:節點部分:方向[屬性列表] node0:body[style=filled color=lightblue], 這個為單一節點聲明的方式。

節點中最基本的屬性為:

  • shape 形狀,全部形狀見,一些常用的圖形有
  • width height, 圖形的寬度和高度,如果設置了 fixedsize 為 true,則寬和高為最終的長度
  • fixedsize, 如果為false,節點的大小由其文本內容所需要的最小值決定
  • rank 子圖中節點上的排列等級約束. 最小等級是最頂部或最左側,最大等級是最底部或最右側。
    • same. 所有節點都位於同一等級
    • min. 所有節點都位於最小等級上
    • source. 所有節點都位於最小等級上,並且最小等級上的唯一節點屬於某個等級 source 或 min 的子圖.
    • max sink. 和上類似

邊 (edge)

有向圖中的的邊用 -> 表示,無向圖用 -- 表示。

可以同時連接多個節點或者子圖,但是只能有一個屬性列表,如下

digraph {
    rankdir = LR
    A -> B -> c[color=green]
}

一些關於邊的屬性如下:

digraph {
    rankdir = LR
    splines = ortho

    A -> B -> C -> D -> F [color = green]
    E -> F -> B -> D [color = blue]
    B -> E -> H[color = red]
}
  • len 首選邊的長度
  • weight 邊的權重, 權重越大越接近邊的長度
  • lhead 邏輯邊緣的頭部(箭頭那個位置),compound 設置為 true 時,邊被裁減到子圖的邊界處
  • ltail 類似 lhead
  • headlabel 邊上靠近箭頭部分的標籤
  • taillabel 邊上靠近尾部部分的標籤
    設置 A->B->C->D->F的權重最大,修改綠色的分支的權重為 100,使其變成主要邏輯分支。

  • splines 控制如何以及是否表示邊緣。其值如下
    • none 或者 “”, 無邊
    • true 或者 spline, 樣條線(無規則,可為直或者曲線)
    • false 或者 line, 直線段
    • polyline, 折線
    • curved, 曲弧線,兩條?
    • ortho, 正直的線(橫豎)
  • dir 設置繪製箭頭的邊緣類型

子圖

subgraph 必須配合 cluster 一起使用,用法為 subgraph cluster* {}

需要設置 compound 為 true,則在群集之間留出邊緣,子圖的邊界關係在 邊 的定義中有給出,這裏直接給個示例。

digraph G {
    compound = true  // 允許子圖間存在邊
    ranksep = 1
    node [shape = record]

    subgraph cluster_hardware {
        label = "hardware"
        color = lightblue
        CPU Memory
    }

    subgraph cluster_kernel {
        label = "kernel"
        color = green
        Init IPC
    }

    subgraph cluster_libc {
        label = "libc"
        color = yellow
        glibc
    }

    CPU -> Init [lhead = cluster_kernel ltail = cluster_hardware]
    IPC -> glibc [lhead = cluster_libc ltail = cluster_kernel]
}

示例

TCP IP 狀態流程圖

展示了兩個版本,怎麼把這些圖形節點稍微規範的显示出來

digraph {
    compound=true
    fontsize=10
    margin="0,0"
    ranksep = .75
    nodesep = .65

    node [shape=Mrecord fontname="Inconsolata, Consolas", fontsize=12, penwidth=0.5]
    edge [fontname="Inconsolata, Consolas", fontsize=10, arrowhead=normal]


    "TCP/IP State Transition" [shape = "plaintext", fontsize = 16]

    // now start server state transition
    "CLOSED" -> "LISTEN" [style = blod, label = "應用:被動打開\n發送:<無>"];
    "LISTEN" -> "SENT_REVD" [style = blod, label = "接收:SYN\n發送:SYN,ACK"]
    "SENT_REVD" -> "ESTABLISHED" [style = blod, label = "接收:ACK\n發送:<無>", weight = 20]
    "ESTABLISHED" -> "CLOSE_WAIT" [style = blod, label = "接收:FIN\n發送:ACK", weight = 20]

    subgraph cluster_passive_close {    
        style = dotted
        margin = 10

        passive_close [shape = plaintext, label = "被動關閉", fontsize = 14]

        "CLOSE_WAIT" -> "LAST_ACK" [style = blod, label = "應用:關閉\n發送:FIN", weight = 10]
    }
    "LAST_ACK" -> "CLOSED" [style = blod, label = "接收:ACK\n發送:<無>"]

    // now start client state transition
    "CLOSED" -> "SYN_SENT" [style = dashed, label = "應用:主動打開\n發送:SYN"]; 
    "SYN_SENT" -> "ESTABLISHED" [style = dashed, label = "接收:SYN,ACK\n發送:ACK", weight = 25]
    "SYN_SENT" -> "SENT_REVD" [style = dotted, label = "接收:SYN\n發送:SYN,ACK\n同時打開"]
    "ESTABLISHED" -> "FIN_WAIT_1" [style = dashed, label = "應用:關閉\n發送:FIN", weight = 20]
    
    subgraph cluster_active_close {
        style = dotted
        margin = 10
        
        active_open [shape = plaintext, label = "主動關閉", fontsize = 14]

        "FIN_WAIT_1" -> "FIN_WAIT_2" [style = dashed, label = "接收:ACK\n發送:<無>"]
        "FIN_WAIT_2" -> "TIME_WAIT" [style = dashed, label = "接收:FIN\n發送:ACK"]
        "FIN_WAIT_1" -> "CLOSING" [style = dotted, label = "接收:ACK\n發送:<無>"]
        "FIN_WAIT_1" -> "TIME_WAIT" [style = dotted, label = "接收:SYN,ACK\n發送:ACK"]
        "CLOSING" -> "TIME_WAIT" [style = dotted]
    }
    
    "TIME_WAIT" -> "CLOSED" [style = dashed, label = "2MSL超時"]
}

這是一個很挫的版本,排版亂飛了。

digraph rankdot {
    compound=true
    margin="0,0"
    ranksep = .75
    nodesep = 1
    pad = .5
    //splines = ortho

    node [shape=Mrecord, charset = "UTF-8" fontname="Microsoft YaHei", fontsize=14]
    edge [charset = "UTF-8" fontname="Microsoft YaHei", fontsize=11, arrowhead = normal]


    CLOSED -> LISTEN [style = dashed, label = "應用:被動打開\n發送:<無>", weight = 100];
    
    "TCP/IP State Transition" [shape = "plaintext", fontsize = 16]

    {
        rank = same
        SYN_RCVD SYN_SENT
        point_1 [shape = point, width = 0]
        
        SYN_SENT -> point_1 [style = dotted, label = "應用關閉或者超時"]
        // SYN_SENT -> SYN_RCVD 這個一行代碼和上一行衝突了,syn_sent 會在syn_rcvd右邊
        SYN_RCVD -> SYN_SENT [style = dotted, dir = back, headlabel = "接收:SYN\n發送:SYN,ACK\n同時打開"]
    }

    LISTEN -> SYN_RCVD [style = dashed, headlabel = "接收:SYN\n發送:SYN,ACK"]
    SYN_RCVD -> LISTEN [style = dotted, headlabel = "接收:RST"]
    CLOSED:es -> SYN_SENT [style = blod, label = "應用:主動打開\n發送:SYN"]

    {
        rank = same
        ESTABLISHED CLOSE_WAIT

        ESTABLISHED -> CLOSE_WAIT [style = dashed, label = "接收:SYN,ACK\n發送:ACK"]
    }

    SYN_RCVD -> ESTABLISHED [style = dashed, label = "接收:ACK\n發送:<無>", weight = 9]
    SYN_SENT -> ESTABLISHED  [style = blod, label = "接收:SYN,ACK\n發送:ACK", weight = 10]

    {
        rank = same

        FIN_WAIT_1
        CLOSING 
        LAST_ACK
        point_2 [shape = point, width = 0]

        FIN_WAIT_1 -> CLOSING [style = dotted, label = "接收:FIN\n發送:ACK"]
        LAST_ACK -> point_2 [style = dashed, label = "接收:ACK\n發送:<無>"]
    }

    CLOSE_WAIT -> LAST_ACK [style = dashed, label = "應用:關閉\n發送:FIN", weight = 10]

    {
        rank = same
        FIN_WAIT_2  TIME_WAIT

        point_3 [shape = point, width = 0]
        TIME_WAIT -> point_3 [style = blod, label = "2MSL超時"]
    }

    ESTABLISHED -> FIN_WAIT_1 [style = blod, label = "應用:關閉\n發送:FIN"]
    FIN_WAIT_1 -> FIN_WAIT_2 [style = blod, headlabel = "接收:ACK\n發送:<無>", weight = 15]
    FIN_WAIT_2 -> TIME_WAIT [style = blod, label = "接收:FIN\n發送:ACK", weight = 10]

    CLOSING -> TIME_WAIT [style = dotted, label = "接收:ACK\n發送:<無>", weight = 15]
    FIN_WAIT_1 -> TIME_WAIT [style = dotted, label = "接收:ACK\n發送:<無>"]

    point_3 -> point_2 [arrowhead = none, style = dotted, weight = 10]
    point_2 -> point_1 [arrowhead = none, style = dotted]
    point_1 -> CLOSED [style = dotted]
}

這個版本看起來有內味了,最最最的主要的原因就是我使用 rank = same 屬性,將一些圖形固定在 同一列,一些需要橫豎的直線的地方使用 weight 來調整權重,達到橫豎的直接的效果,很多地方都是微調的結果。有一個很差的地方是 使用了rank限制若干圖形后,就不能使用 subgraph 屬性了,這樣就不能在若干不同部分的節點周邊畫線(對比關閉的區域)了。

epoll 相關數據結構及關係

digraph rankdot {
    compound=true
    margin="0,0"
    ranksep = .75
    nodesep = 1
    pad = .5
    rankdir = LR

    node [shape=record, charset = "UTF-8" fontname="Microsoft YaHei", fontsize=14]
    edge [style = dashed, charset = "UTF-8" fontname="Microsoft YaHei", fontsize=11]

    epoll [shape = plaintext, label = "epoll 相關結構及部分關係"]

    eventpoll [
        color = cornflowerblue,
        label = "<eventpoll> struct \n eventpoll |
            <lock> spinlock_t lock; |
            <mutex> struct mutex mtx; |
            <wq> wait_queue_head_t wq; |
            <poll_wait> wait_queue_head_t poll_wait; |
            <rdllist> struct list_head rdllist; |
            <ovflist> struct epitem *ovflist; |
            <rbr> struct rb_root_cached rbr; |
            <ws> struct wakeup_source *ws; |
            <user> struct user_struct *user; |
            <file> struct file *file; |
            <visited> int visited; |
            <visited_list_link> struct list_head visited_list_link;"
    ]

    epitem [
        color = sienna,
        label = "<epitem> struct \n epitem  |
            <rb>struct rb_node rbn;\nstruct rcu_head rcu; |
            <rdllink> struct list_head rdllink; |
            <next> struct epitem *next; |
            <ffd> struct epoll_filefd ffd; |
            <nwait> int nwait; |
            <pwqlist> struct list_head pwqlist; |
            <ep> struct eventpoll *ep; |
            <fllink> struct list_head fllink; |
            <ws> struct wakeup_source __rcu *ws; |
            <event> struct epoll_event event;"
    ]

    epitem2 [
        color = sienna,
        label = "<epitem> struct \n epitem |
            <rb>struct rb_node rbn;\nstruct rcu_head rcu; |
            <rdllink> struct list_head rdllink; |
            <next> struct epitem *next; |
            <ep> struct eventpoll *ep; |
             ··· |
             ··· "
    ]

    eppoll_entry [
        color = darkviolet,
        label = "<entry> struct \n eppoll_entry |
            <llink> struct list_head llink; |
            <base> struct epitem *base; |
            <wait> wait_queue_entry_t wait; |
            <whead> wait_queue_head_t *whead;"
    ]

    epitem:ep -> eventpoll:se [color = sienna]
    epitem2:ep -> eventpoll:se [color = sienna]
    eventpoll:ovflist -> epitem:next -> epitem2:next [color = cornflowerblue]
    eventpoll:rdllist -> epitem:rdllink -> epitem2:rdllink [dir = both]
    eppoll_entry:llink -> epitem:pwqlist [color = darkviolet]
    eppoll_entry:base -> epitem:nw  [color = darkviolet]
}

遺留問題

  1. 在以上TCP/IP 狀態變遷圖中,嘗試增加主動關閉方的區域邊框
  2. 嘗試增加 TCP/IP 的時序圖

使用 VSCode 進行預覽生成

  1. 在官網下載graphviz安裝包
  2. 安裝 vscode 插件 Graphviz Preview
  3. 在 settings.json 中添加 "graphvizPreview.dotPath": "graphviz_path\graphviz-2.38\\release\\bin\\dot.exe", graphviz_path 為所在路徑,這些修改一下既可
  4. 新建一個 dot 文件,右上角就會有預覽生成的按鈕了

參考

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

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

美國蒙大拿州密蘇拉(Missoula)聯邦地區法官克里斯坦森(Dana Christensen)與環保人士及美國原住民站在同一陣線

美國蒙大拿州密蘇拉(Missoula)聯邦地區法官克里斯坦森(Dana Christensen)與環保人士及美國原住民站在同一陣線,駁回美國魚類暨野生動物管理局(US Fish and Wildlife Service)將灰熊從瀕危物種名單除名的決定。

環保人士主張,根據瀕臨滅絕物種保護法,對這些灰熊與蒙大拿州和下48州(Lower 48)的其他灰熊族群採取差別待遇,是生物學上靠不住且非法行為,法官也同意這類說法。

環保人士說,儘管灰熊數量有所回升,倘若沒有受到聯邦持續保護,牠們的復育情況就會受到影響。此外,氣候變遷導致灰熊食物供給出現變化和人為死亡率高,也對灰熊生存構成威脅。

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

北極白鯨驚奇現身泰晤士河 再不回家專家憂安危

摘錄自2018年9月26日中央社報導

據英國各大媒體報導,一隻迷途的白鯨現身英國泰晤士河,第一次被目擊是25日在肯特郡(Kent)格雷夫森德(Gravesend)的泰晤士河段,當時白鯨正在駁船附近覓食,被暱稱為「班尼」(Benny),距離其原棲息地北極圈水域達數千公里。26日白鯨再次現身同個地點,引發是否迷航及可能遇險的擔憂。

鯨豚保育協會(WDC)海洋哺乳類動物科學家羅特(Rob Lott)表示,這隻白鯨正受到監控,以防牠擱淺。「但白鯨停留在泰晤士河口的時間愈長,就愈令人擔心。」

海洋生物保育慈善團體ORCA的保育學家巴比(Lucy Babey)表示:「這是白鯨出現在英國的最南端紀錄。」

英國海洋生物救援組織表示,英國最後一次發現白鯨蹤跡是3年前在北英格蘭的諾森伯蘭(Northumberland)海岸,以及北愛爾蘭,但極為罕見。

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

特斯拉擴產,2018目標年產50萬輛車

因應龐大需求,電動車龍頭廠商特斯拉(Tesla)表示將積極擴產,目標在2018年達成年產量50萬輛車。

特斯拉2015年的產量約為5萬輛,但Model 3甫一推出就獲得40萬輛的超大筆訂單。為滿足這些預購車主的需求,特斯拉表示將積極擴產五倍,將原先2020年年產量50萬輛的目標提前到2018年實現,從中可望取得可觀的美國政府補助。而2016年,特斯拉預計將交車8~9萬輛。

特斯拉執行長Elon Musk於5月4日發表公司經營狀況時表示已將自己的辦公桌「移至生產線尾端,整個團隊都全力以赴」,彰顯急速擴張的決心。但他也坦言這個目標極具挑戰,若能成功,全球電動車市場將產生結構性的變化。

特斯拉財報:首季虧損

特斯拉在4日所發表的財報顯示,該公司今年第一季營收較去年同期大幅增加22%,為11.5億美元。但由於公司正在高速擴張,使營業費用比去年同期增加四成,至5億美元;加上Model X交車進度略有落後,使首季出現虧損,淨損為2.83億美元,高於去年第一季的淨損1.54億美元。扣除非常態性項目後,相當於每股虧損0.57美元。

未見起色的財報直接衝擊特斯拉股情。4日當天,特斯拉股價一度下滑4.2%,為222.56美元。不過,當Musk再宣布目標於2018年擴產至50萬輛後,股價再度回彈,最高漲至每股239.72美元。

特斯拉能否如期實現年產能50萬輛的目標,仍待觀察。近期有兩位高階副總即將離職,且特斯拉營運燒錢速度快於盈利回收,也是一隱憂。但特斯拉表示目前仍不需要依靠融資。

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

中國新能源汽車零部件發展規劃成果集中亮相北京10月車展

2016年10月13—16日,以“選擇·行動——未來從現在開始”為主題的2016(第四屆)節能與新能源汽車產業發展規 劃成果展覽會、中國國際汽車新能源及技術應用展覽會將在北京國家會議中心舉辦。展會由中國國家工業和資訊化部支援,科學技術部、中國國際貿易促進委員會批准,是貫徹落實國務院《節能與 新能源汽車產業發展規劃(2012—2020年)》,推動中國節能與新能源汽車產業發展的重要舉措之一。本屆展會將全方面覆蓋節能環保汽車、純電動 汽車、混合動力汽車、插電式混合動力和燃料電池汽車,以及電池、電機、電控等關鍵零部件和充電設施等產品,眾多國際知名汽車企業、零部件和充電設施供應 商將攜旗下新品參展。

汽車產業是國民經濟的重要支柱產業,也是實現新一輪技術革命和產業變革的重要載體。中國汽車產業快速發展,產銷增速連續七年居世界第一位,新能源汽車產業初具規模,15年中國已成為全球最大的新能源汽車生產以及銷售市場.

但同時,據中國國家863“節能與新能源汽車”重大項目監理諮詢專家組王秉剛調研,中國目前具有生產傳統汽車ABS系統能力的工廠主要有亞太、元豐與伯特利等幾家,他們是未來最有希望成為中國自主電動汽車制動系統的生產企業,與國外同類企業相比,他們規模都不大,在同類產品的市場佔有率不足5%,在年產量達2000多萬輛的中國汽車產業裡,如此重要的底盤部件,自主企業生產的比例低到差不多可以忽略不計的程度。中國汽車產業空心化可見一斑!這裡舉的僅是制動系統的例子,其它零部件也存在類似情況,就連備受關注的動力電池也未必樂觀,已經有國外大電池企業,在部分地方政府優惠政策的支援下在國內建廠。

中國新能源汽車零部件企業正加強關鍵核心技術研發,推進技術創新,竭盡全力避免傳統汽車產業存在的核心技術缺失、產業空心化現象,在新能源汽車行業重蹈覆轍。據車展合作單位盛大超越展覽公司嶽巍介紹,“新能源汽車零部件主要包括電機、驅動控制系統和電源系統,目前中國在新能源汽車零部件上的技術創新有序推進,整體研發水平不斷提升,中國企業的電機及驅動控制系統在新能源客車(如宇通、安凱、中通等)及乘用車(比亞迪、北汽、江淮等)上已得到廣泛應用,這些成果大部分將在北京10月新能源汽車展上展出,中國的電機及驅動控制系統參展企業包括:德沃仕、英威騰、中冶南方、超同步、合普動力、合康動力、福工動力、八達等。這些企業有的具有國外及科研院所的專業背景,有的是從電梯控制、機床控制發展而來。比如合普動力是低速電動車電機驅動控制的領軍企業,隨著高速新能源汽車市場的不斷成長,已開發了電動客車及乘用車電機驅動控制產品”。

盛大超越展覽公司電池及BMS展區經理耿忱也對中國企業抱有積極信心,他說,”北京10月新能源汽車成果展上的中國電池企業非常給力,沃特瑪、寧德時代、中航鋰電、力神電池、比克電池、萬向、山東威能、神工光電、內蒙古稀奧科、實聯長宜等知名電池企業展示了車用動力電池技術及市場應用的發展成果,部分電池展商還帶來了實際應用整車配合展示”。他還表示,“環宇賽爾、科隆集團、均勝普瑞等一批新的中國電池及BMS展商將有望加盟2016北京10月新能源汽車成果展,給主機廠提供更加前沿和多元化的產品解決方案“。

展會相關

2016中國國際汽車新能源及技術應用展覽會
節能與新能源汽車產業發展規劃成果展覽會
行業地位:中國最大高速新能源汽車展
展覽規模:30,000平方米(2015年)
觀眾數量:60,000人次(2015年)
展覽週期:每年一屆,2013年首屆
連絡人:岳巍 先生
手機:+86 135 5286 5285
展會網址:
 

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

台灣海運大陸貨務運送流程

兩岸物流進出口一站式服務

abp(net core)+easyui+efcore實現倉儲管理系統——ABP WebAPI與EasyUI結合增刪改查之一(二十七)

 



 

一.前言

       通過前面的文章的學習,我們已經有實現了傳統的ASP.NET Core MVC+EasyUI的增刪改查功能。本篇文章我們要實現了使用ABP提供的WebAPI方式+EasyUI來實現增刪改查的功能。本文中我們將不在使用DataGrid表格控件,而是使用樹形表格(TreeGrid)控件。

二、樹形表格(TreeGrid)介紹

       我先上圖,讓我們來看一下功能完成之後的組織管理信息列表頁面。如下圖。

       這個組織管理列表頁面使用TreeGrid來實現的。我們接下來介紹一下TreeGrid。

     首先、在定義TreeGrid時有兩個屬性必須要有一個是idField,這個要唯一;另一個是treeField的定義,這是樹節點的值,必須要有。 

     其次、easyui加載treegrid的json數據格式有三種,我就介紹我常用的這種。其他兩種方式,請查看easyui的相關文檔。

  {"total":7,"rows":[

           {"id":1,"name":"All Tasks","begin":"3/4/2010","end":"3/20/2010","progress":60,"iconCls":"icon-ok"},      
{"id":2,"name":"Designing","begin":"3/4/2010","end":"3/10/2010","progress":100,"_parentId":1,"state":"closed"},
{"id":21,"name":"Database","persons":2,"begin":"3/4/2010","end":"3/6/2010","progress":100,"_parentId":2},
{"id":22,"name":"UML","persons":1,"begin":"3/7/2010","end":"3/8/2010","progress":100,"_parentId":2}, {"id":23,"name":"Export Document","persons":1,"begin":"3/9/2010","end":"3/10/2010","progress":100,"_parentId":2},
{"id":3,"name":"Coding","persons":2,"begin":"3/11/2010","end":"3/18/2010","progress":80},
{"id":4,"name":"Testing","persons":1,"begin":"3/19/2010","end":"3/20/2010","progress":20} ],"footer":[ {"name":"Total Persons:","persons":7,"iconCls":"icon-sum"} ]}

     下面介紹一下上面數據中的幾個重要屬性:

     1)  _parentId :字段_parentId必不可少,且名稱唯一。記得前面有“_” ,他是用來記錄父級節點,沒有這個屬性,是沒法展示父級節點 其次就是這個父級節點必須存在,不然信息也是展示不出來,在後台遍歷組合的時候,如果父級節點不存在或為0時,此時 _parentId 應該不賦值,或設為“”。如果賦值 “0” 則在表格中不显示數據。

    2) state:是否展開

     3) checked:是否選中(用於複選框)

    4) iconCls:選項前面的圖標,如果自己不設定,父級節點默認為文件夾圖標,子級節點為文件圖標

 

    下面我們來開始實現組織管理頁面的相關功能。首先我們要創建一個組織信息實體。

三、創建Org實體

       1. 在Visual Studio 2017的“解決方案資源管理器”中,右鍵單擊“ABP.TPLMS.Core”項目的“Entitys”文件夾,在彈出菜單中選擇“添加” >

 > “類”。 將類命名為 Org,然後選擇“添加”。

      2.創建Org類繼承自Entity<int>,通過實現審計模塊中的IHasCreationTime來實現保存創建時間。根據TreeGrid所需要的數據格式的要求。代碼如下:

using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
 

namespace ABP.TPLMS.Entitys
{

   public partial class Org : Entity<int>, IHasCreationTime
{
      int m_parentId = 0;
        public Org()
        {

            this.Id = 0;
            this.Name = string.Empty;
            this.HotKey = string.Empty;
            this.ParentId = 0;
            this.ParentName = string.Empty;

            this.IconName = string.Empty;

            this.Status = 0;

            this.Type = 0;

            this.BizCode = string.Empty;
            this.CustomCode = string.Empty;
            this.CreationTime = DateTime.Now;
            this.UpdateTime = DateTime.Now;
            this.CreateId = 0;

            this.SortNo = 0;

        }

        [Required]
        [StringLength(255)]
        public string Name { get; set; }

        [StringLength(255)]
        public string HotKey { get; set; }

        public int ParentId { get { return m_parentId; } set { m_parentId = value; } }

        [Required]
        [StringLength(255)]
        public string ParentName { get; set; }

        public bool IsLeaf { get; set; }

        public bool IsAutoExpand { get; set; }

        [StringLength(255)]
        public string IconName { get; set; } 

        public int Status { get; set; }

        public int Type { get; set; }

        [StringLength(255)]
        public string BizCode { get; set; }

        [StringLength(100)]
        public string CustomCode { get; set; }

        public DateTime CreationTime { get; set; }
        public DateTime UpdateTime { get; set; }
        public int CreateId { get; set; }

        public int SortNo { get; set; }
public int? _parentId {
            get {
                if (m_parentId == 0)                

                {
                    return null;
                }

                return m_parentId;
            }           

        }
    }
}

      3.定義好實體之後,我們去“ABP.TPLMS.EntityFrameworkCore”項目中的“TPLMSDbContext”類中定義實體對應的DbSet,以應用Code First 數據遷移。添加以下代碼

 

using Microsoft.EntityFrameworkCore;
using Abp.Zero.EntityFrameworkCore;
using ABP.TPLMS.Authorization.Roles;
using ABP.TPLMS.Authorization.Users;
using ABP.TPLMS.MultiTenancy;
using ABP.TPLMS.Entitys;
 

namespace ABP.TPLMS.EntityFrameworkCore
{

    public class TPLMSDbContext : AbpZeroDbContext<Tenant, Role, User, TPLMSDbContext>
    {

        /* Define a DbSet for each entity of the application */
    
        public TPLMSDbContext(DbContextOptions<TPLMSDbContext> options)
            : base(options)

        {
        }

        public DbSet<Module> Modules { get; set; }
        public DbSet<Supplier> Suppliers { get; set; }
  public DbSet<Cargo> Cargos { get; set; }
          public DbSet<Org> Orgs { get; set; }

    }
}

 

 

 

      4.從菜單中選擇“工具->NuGet包管理器器—>程序包管理器控制台”菜單。

     5. 在PMC中,默認項目選擇EntityframeworkCore對應的項目后。輸入以下命令:Add-Migration AddEntityOrg,創建遷移。如下圖。

 

       6. 在上面的命令執行完畢之後,創建成功后,會在Migrations文件夾下創建時間_AddEntityOrg格式的類文件,這些代碼是基於DbContext指定的模型。如下圖。

 

     7.在程序包管理器控制台,輸入Update-Database,回車執行遷移。執行成功后,如下圖。

 

     8. 在SQL Server Management Studio中查看數據庫,Orgs表創建成功。

 

 

 

 

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!