Java OPC client開發踩坑記
來源:https://www.jianshu.com/p/26391f0cbb6f
最近一個專案中需要用到OPC client,從OPC Server中獲取資料。主要的程式語言使用Java實現。實際開發中遇到了各種坑,其實也和自己沒有這方面的經驗有關,現在寫一篇文章分享下整個專案中遇到的一些問題。
準備知識
開發OPC Client之前需要一些準備知識,需要一些知識儲備,否則根本搞不清楚裡面的門道。現在對一些預先準備的知識點做一概述。OPC是什麼就不說了。
OPC Server端的協議
OPC Server端目前常見的有以下幾種協議:
- OPC DA: Data Access協議,是最基本的OPC協議。OPC DA伺服器本身不儲存資料,只負責顯示資料收集點的當前值。客戶端可以設定一個refresh interval,定期重新整理這個值。目前常見的協議版本號為2.0和3.0,兩個協議不完全相容。也就是用OPC DA 2.0協議的客戶端連不上OPC DA 3.0的Server
- OPC HDA: Historical Data Access協議。前面說過DA只顯示當前狀態值,不儲存資料。而HDA協議是由資料庫提供,提供了歷史資料訪問的能力。比如價格昂貴的Historian資料庫,就是提供HDA協議介面訪問OPC的歷史資料。HDA的Java客戶端目前我沒找到免費的。
- OPC UA: Unified Architecture統一架構協議。誕生於2008年,摒棄了前面老的OPC協議繁雜,互不相容等劣勢,並且不再需要
COM
口訪問,大大簡化了程式設計的難度。基於OPC UA的開源客戶端非常多。不過由於誕生時間較晚,目前在國內工業上未大規模應用,並且這個協議本身就跟舊的DA協議不相容,客戶端沒法通用。
我們的目標環境絕大多數是OPC DA 2.0的Server,極個別可能有OPC DA 3.0。當時找到的很多類庫實現的都是OPC UA的。
第一坑: 基於JAVA開發的OPC Client非常少,大部分是商業的,售價不菲。現場環境又是OPC DA的Server,開源client只有兩個可選,找工具和評估就花了不少時間。
OPC儲存格式
OPC儲存和傳統的關係型資料庫儲存格式有很大的不同,不同於關係型資料庫的表儲存,OPC儲存格式是樹形結構,Server端的儲存格式如下:
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從哪來。後來才搞清楚。
COM
Component Object Model物件元件模型,是微軟定義的一套軟體的二進位制介面,可以實現跨程式語言的程序間通訊,進而實現複用。
DCOM
Microsoft Distributed Component Object Model,坑最多的一個玩意。字面意思看起來是分散式的COM,簡單理解就是可以利用網路傳輸資料的COM協議,客戶端也可以通過網際網路分佈在各個角落,不再限制在同一臺主機上了。
上面描述來看這玩意好像挺美好是吧?實際操作開發中才發現,這玩意簡直是坑王之王,對於不熟悉的人來說充滿了坑,十分折騰。配置過程可以參考一些文章
- DCOM是windows上的服務,使用前需要啟用
- DCOM是遠端連線的協議,需要配置相關的許可權,以及防火牆規則放行
- 特別注意這一點,前兩項配置在網上都能找到,這一條是我在經歷無數次痛之後才意識到的。DCOM遠端連線和http不同,是通過本地使用者認證的,需要以本地使用者身份登入伺服器,拿到相應的許可權,才能使用DCOM。有點繞是吧?你可以類比Windows的遠端桌面登入,需要拿到伺服器的使用者名稱密碼才能登入並作業系統,許可權受到登入使用者的許可權所限制。而DCOM就是用的這種方式。關於各種錯誤網上能找出一大堆解決方案,可能還沒一個能解決你的問題的。甚至可能
progID
無論無何也通不了,始終報錯,不得不改用CLSID
這種方法,十分坑。
神坑: DCOM。從配置開始就充滿了陷阱和坑。不但配置繁瑣複雜,還會受到各種許可權以及防火牆規則的影響。最噁心的是這玩意隨時可能報各種奇葩的錯誤,由於缺乏足夠的錯誤資訊,很難解決,基本憑藉經驗解決DCOM的故障。
開發過程
收集到足夠的準備知識後,就可以開工了。OPC Server是DA 2.0的,因此找到了以下兩個開源類庫。
JEasyOPC Client
- 底層依賴JNI,只能跑在windows環境,不能跨平臺
- 整個類庫比較古老,使用的dll是32位的,整個專案只能使用32位的JRE執行
- 同時支援DA 2.0與3.0協議,算是亮點
- OpenSCADA專案底下的子專案
- 純Java編寫,具有跨平臺特性
- 全部基於
DCOM
實現(劃重點) - 目前只支援DA 2.0協議,3.0協議的支援還在開發中
這兩個類庫都試過,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
資訊:
總算跑起來了。
作者:馮宇Ops
連結:https://www.jianshu.com/p/26391f0cbb6f
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。