最近一個項目中須要用到OPC client,從OPC Server中獲取數據。主要的編程語言使用Java實現。實際開發中遇到了各類坑,其實也和本身沒有這方面的經驗有關,如今寫一篇文章分享下整個項目中遇到的一些問題。html
開發OPC Client以前須要一些準備知識,須要一些知識儲備,不然根本搞不清楚裏面的門道。如今對一些預先準備的知識點作一律述。OPC是什麼就不說了。java
OPC Server端目前常見的有如下幾種協議:數據庫
COM
口訪問,大大簡化了編程的難度。基於OPC UA的開源客戶端很是多。不過因爲誕生時間較晚,目前在國內工業上未大規模應用,而且這個協議自己就跟舊的DA協議不兼容,客戶端無法通用。咱們的目標環境絕大多數是OPC DA 2.0的Server,極個別可能有OPC DA 3.0。當時找到的不少類庫實現的都是OPC UA的。編程
第一坑: 基於JAVA開發的OPC Client很是少,大部分是商業的,售價不菲。現場環境又是OPC DA的Server,開源client只有兩個可選,找工具和評估就花了很多時間。windows
OPC存儲和傳統的關係型數據庫存儲格式有很大的不一樣,不一樣於關係型數據庫的表存儲,OPC存儲格式是樹形結構,Server端的存儲格式以下:api
host `-- OPC Server Name `-- tag1: value, type, timestamp, ..., `-- tag2: value, type, timestamp, ..., `-- tag3: ... ...
每一個主機上可能存在多個OPC Server,每一個Server下面有若干個tag
,就是各個數據收集點當前的值,會按期更新。每一個tag
包含的內容大體有當前值,值類型,時間戳等等數據。是一種樹形結構。因此客戶端鏈接的時候須要指明服務器的ip或主機名,須要鏈接的OPC服務名,以及監聽哪些tag
的數據。服務器
Client端存儲的格式以下:網絡
Group1 `-- tag1 `-- tag2 `-- tag3 Group2 `-- tag4 `-- tag5 ...
這個就比較有意思了,Client是能夠本身維護一個存儲層級Group
。也就是服務端存儲的都是一個個tag
,客戶端能夠本身維護一個個Group
,分類存放這些tag
。因此OPC的Client就和傳統的關係型數據庫有很大的不一樣。客戶端除了指明上述Server端的信息以外,還須要建立一個個Group
,將Server端的tag
一個個放到這些Group
中,而後對應的tag
才能持續的得到數據。架構
第二坑: 這種存儲格式在其餘數據庫十分罕見,當時這裏就迷茫了好一陣子,經過了解協議的人講解,才明白原來客戶端還能夠維護一套存儲結構。當時沒理清楚Group和tag的關係,從服務端看不到Group,客戶端卻要填一個Group,不知道這個Group從哪來。後來才搞清楚。app
Component Object Model對象組件模型,是微軟定義的一套軟件的二進制接口,能夠實現跨編程語言的進程間通訊,進而實現複用。
Microsoft Distributed Component Object Model,坑最多的一個玩意。字面意思看起來是分佈式的COM,簡單理解就是能夠利用網絡傳輸數據的COM協議,客戶端也能夠經過互聯網分佈在各個角落,再也不限制在同一臺主機上了。
上面描述來看這玩意好像挺美好是吧?實際操做開發中才發現,這玩意簡直是坑王之王,對於不熟悉的人來講充滿了坑,十分折騰。配置過程能夠參考一些文章
progID
不管無何也通不了,始終報錯,不得不改用CLSID
這種方法,十分坑。神坑: DCOM。從配置開始就充滿了陷阱和坑。不但配置繁瑣複雜,還會受到各類權限以及防火牆規則的影響。最噁心的是這玩意隨時可能報各類奇葩的錯誤,因爲缺少足夠的錯誤信息,很難解決,基本憑藉經驗解決DCOM的故障。
收集到足夠的準備知識後,就能夠開工了。OPC Server是DA 2.0的,所以找到了如下兩個開源類庫。
JEasyOPC Client
DCOM
實現(劃重點)這兩個類庫都試過,JEasyOPC底層用了JNI,調用代碼量倒不是很大,使用也足夠簡單,坑也遇到了點,就是64位的JRE運行會報錯,說dll是ia32架構的,不能運行於AMD64平臺下,換了32位版本的JRE以後運行起來了,可是一直報錯Unknown Error,從JNI報出來的,不明因此,實在無力解決,只能放棄。
只剩下Utgard一種選擇了,也慶幸目標Server是DA 2.0的,用這個類庫徹底夠用。這個類庫所有使用DCOM協議鏈接OPC Server,因此對於本地鏈接OPC Server,理論上不須要COM口,可是這個類庫所有使用DCOM協議鏈接,因此依舊須要配置主機名,以及登陸的用戶名密碼。使用以前必須先配置DCOM,其中痛苦不足爲外人道也,在上面準備知識部分已經寫道了。
通過一番折騰,總算將項目跑起來了,最終參考的工程代碼以下(項目實用Gradle構建,代碼使用Utgard官方的tutorial範例):
build.gradle:
apply plugin: 'java' apply plugin: 'application' repositories { maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } jcenter() maven { url 'http://neutronium.openscada.org/maven/' } } dependencies { compile 'org.openscada.utgard:org.openscada.opc.lib:1.3.0-SNAPSHOT' compile 'org.openscada.utgard:org.openscada.opc.dcom:1.2.0-SNAPSHOT' compile 'org.jinterop:j-interop:2.0.4' compile 'ch.qos.logback:logback-core:1.2.3' compile 'org.slf4j:slf4j-api:1.7.25' } mainClassName = 'UtgardTutorial1'
src/main/java/UtgardTutorial1.java:
import org.jinterop.dcom.common.JIException; import org.openscada.opc.lib.common.ConnectionInformation; import org.openscada.opc.lib.da.AccessBase; import org.openscada.opc.lib.da.Server; import org.openscada.opc.lib.da.SyncAccess; import java.util.concurrent.Executors; public class UtgardTutorial1 { public static void main(String[] args) throws Exception { // create connection information final ConnectionInformation ci = new ConnectionInformation(); ci.setHost("localhost"); ci.setUser("Administrator"); ci.setPassword("mypassword"); ci.setProgId("TLSvrRDK.OPCTOOLKIT.DEMO"); // ci.setClsid("08a3cc25-5953-47c1-9f81-efe3046f2d8c"); // if ProgId is not working, try it using the Clsid instead final String itemId = "tag1"; // create a new server final Server server = new Server(ci, Executors.newSingleThreadScheduledExecutor()); try { // connect to server server.connect(); // add sync access, poll every 500 ms final AccessBase access = new SyncAccess(server, 500); access.addItem(itemId, (item, state) -> System.out.println("Resut: " + state.toString())); // start reading access.bind(); // wait a little bit Thread.sleep(10 * 1000); // stop reading access.unbind(); } catch (final JIException e) { System.out.println(String.format("%08X: %s", e.getErrorCode(), server.getErrorMessage(e.getErrorCode()))); e.printStackTrace(); } } }
最終項目運行輸出以下:
Recieved RESPONSE Resut: Value: [[]]], Timestamp: 星期三 七月 05 00:32:29 CST 2017, Quality: 192, ErrorCode: 00000000 七月 05, 2017 12:32:27 上午 rpc.DefaultConnection processOutgoing 信息: Sending REQUEST 七月 05, 2017 12:32:27 上午 rpc.DefaultConnection processIncoming 信息: Recieved RESPONSE Resut: Value: [[]]], Timestamp: 星期三 七月 05 00:32:29 CST 2017, Quality: 192, ErrorCode: 00000000 七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processOutgoing 信息: Sending REQUEST 七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processIncoming 信息: Recieved RESPONSE Resut: Value: [[U]], Timestamp: 星期三 七月 05 00:32:30 CST 2017, Quality: 192, ErrorCode: 00000000 七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processOutgoing 信息: Sending REQUEST 七月 05, 2017 12:32:28 上午 rpc.DefaultConnection processIncoming 信息: Recieved RESPONSE Resut: Value: [[U]], Timestamp: 星期三 七月 05 00:32:30 CST 2017, Quality: 192, ErrorCode: 00000000 七月 05, 2017 12:32:29 上午 rpc.DefaultConnection processOutgoing 信息:
總算跑起來了。