先说清楚这玩意到底解决什么问题
做工业数据对接的人,应该都经历过那种“设备各说各话”的崩溃时刻。产线上可能同时有PLC、变频器、温控表,各自走Modbus、Profinet、自定义串口协议,上位机要去取数据,就得给每一类设备单独写驱动。驱动一多,维护就是噩梦:今天换了个设备型号,明天改了个寄存器地址,后天某个仪表协议版本升级,整套系统全都跟着遭殃。
OPC DA的出现,本质上就是给这个混乱现场立规矩。OPC DA Server所干的事情非常聚焦:把各种异构设备的数据通过各自的驱动收上来,统一塞进一套标准的COM接口模型里,再对外暴露一个统一的“数据仓库”。客户端不需要知道底层是PLC还是仪表,只需要按照 OPC DA 2.05a 规范去枚举标签、建立组、读写Item,就完事了。
这篇文章围绕的核心就是:用C#或C++开发一款OPC DA Server软件,会碰到哪些设计问题、实现难点和部署里的坑。我做一个自研的OPC DA Server前前后后折腾了很长时间,头尾踩过不少坑,也有些个人心得。如果你也是被设备驱动搞到头大的工程师,或者正在考虑自研一套数据采集中间层、想摆脱老外商用Server的授权限制,这篇文章应该能帮你少走不少弯路。
需要先声明一点:OPC DA是基于COM/DCOM的老协议,不是OPC UA那种现代架构。所以下面聊的所有东西,都离不开Windows、注册表、COM组件、DCOM配置这些老派技术栈。别嫌它老,工业现场就是这样的,能稳定跑十年的东西就是好东西。
1. 先理解OPC DA Server的结构,再做代码
1.1 一个现场数据对接的真实场景
我最早遇到自研需求,是在一个模拟项目X里。车间里有三套设备,一套西门子系的PLC,一套国产的温控模块,还有一套采集历史数据的无纸记录仪。上位可视化系统是个自行开发的组态页面,客户要求所有数据必须能实时刷新到界面上,还要能反写下发参数。
当时最省事的办法是买商业OPC Server,但问题也很现实:一是授权费按点位算,点位一多价格就离谱;二是商业产品的协议驱动经常和现场非标设备不兼容,尤其是国产走私有Modbus扩展命令的设备,官方不提供驱动就只能干瞪眼。于是我决定自己写一个OPC DA Server,把三套设备的数据都收进来,再通过标准OPC方式供上层系统读取。
这个需求是所有自研Server的典型模板:底层是多类异构设备,上层是标准数据消费方,中间夹着的就是我们开发的Server。理解这个模板,你就明白为什么Server的内部设计必须是“设备驱动可插拔、统一数据模型、对外标准接口”这三层结构。
1.2 OPC DA的数据模型:Server、Group、Item三层结构
OPC DA的数据模型非常清晰,就三层。最顶层叫OPCServer,代表一个独立的服务器实例;中间层叫Group,是客户端批量组织Item的容器;最底层叫Item,对应一个真实的测点或者参数,比如“1号线的温度”“电机的当前转速”。
这里有个不少新手容易理解错的地方:OPC DA的Group不是挂在Server下面的真实设备组,它本质上是一份“订阅契约”。客户端按自己关心的一组Item建一个Group,告诉服务器“这些点我感兴趣,更新周期多少、死区多少、数据变化了要回调我”。同一个物理Item可以被不同客户端放进各自的Group,互不干扰。设计Server的时候,Group只是一个动态生成的上下文对象,不需要跟真实设备产生任何绑定关系。
Item则是Server内部标签表的一个公开窗口。Server内部可能管着一个很大的结构体数组,每项对应一个采集点,但客户端不能直接摸到内部结构体,只能通过Item的字符串ID(比如“PLC1.Temperature”)来访问。这种设计让底层真实数据源和客户端之间有一层很好的隔离,设备换了、寄存器地址变了,客户端代码基本不用动。
1.3 数据从设备到客户端的完整流向
理解数据流是关键。一个OPC DA Server内部通常跑着两类线程:一类是采集线程,按固定周期去轮询或者接收底层设备的主动上报,把最新数值写到内存里的“标签表”;另一类是COM服务线程,响应客户端的接口调用,从标签表里取数。
打个生活化的比方,标签表就是一间快递仓库,采集线程不停地把最新货物搬进仓库,客户端接口则是仓库柜台,客户来问“某某货在不在”,柜台直接从仓库货架上拿,不需要让客户自己跑去工厂车间看生产。这个比喻很好地解释了缓存与实时采集的关系。
所以OPC DA Server里的Read操作,绝大多数时候读的不是设备实时值,而是标签表里最新一次采集周期的缓存值。这个设计理念和OPC UA一模一样,它牺牲了一丁点实时性,换来了极高的并发访问能力,因为客户端再多,也只是对着内存仓库取数,不会直接把设备冲崩掉。
写操作则是另一条路径:客户端发起Write后,Server立刻去调对应设备的驱动,把值实际写下去,同时更新标签表里的缓存。写操作一定要走真实通道,不能只改内存里的值。
2. 技术选型:C#、C++,还是混合方案
2.1 三条路线的横向对比
先摆一个大方向的结论:C#和C++都能做出能用的OPC DA Server,但实现的底层逻辑和踩坑点完全不同。我做过纯C++的版本,也做过托管代码为主的版本,还做过两者混合的架构。特意用表格把三条路线的主要差异列了一下。
| 选型路线 | 开发效率 | 底层DCOM兼容性 | 跨进程稳定性 | 维护成本 | 适合场景 |
|---|---|---|---|---|---|
| 纯C++(ATL/原生COM) | 低 | 高 | 高 | 高 | 老工控环境、极大规模现场、对性能极致敏感 |
| 纯C#(COM互操作) | 高 | 中 | 中高 | 低 | 新项目、团队熟悉.NET、点位规模可控 |
| C#管理端 + C++底层驱动 | 中高 | 高 | 高 | 中 | 协议复杂、设备多样、需要可维护又追求性能 |
这里说的兼容性,特指OPC DA在Windows环境里对DCOM调用的适应能力。C++直接做COM组件,和系统底层的COM运行库是同构的,DCOM权限、代理桩、身份验证这些机制都发生在原生层,问题少。C#虽然也能通过[ComVisible]做COM可见组件,但托管代码和COM对象之间的转换层多了一层,一旦涉及跨机器访问,身份模拟、令牌复制、线程套间这些问题会更棘手。
2.2 C++方案的核心优势与使用场景
如果你面对的是老掉牙的工控现场,比如Windows XP/Server 2003的老机器、只有32位系统、必须用旧式Delphi或VB6客户端去连,那纯C++方案是最稳的。这些老环境往往缺少完整的.NET运行库,装上.NET框架费劲不说,COM互操作层还可能被安全策略拦着。C++写的ATL/COM组件就是原生的,直接往注册表写CLSID就能用,不依赖任何托管运行时。
C++方案还有一个隐藏优势:线程模型完全可控。OPC DA Server要面对大量并发读取和异步回调,托管环境下要小心GC压缩导致回调对象被意外回收,而C++里裸指针和引用计数都由自己掌控,稳定性上限更高。代价也很沉重:所有接口方法都要手写HRESULT返回,所有BSTR、VARIANT都要自己管理内存。一个AddItem方法内部要处理的东西,足够C#版写三个完整功能模块了。
2.3 C#方案的核心优势与使用场景
如果是新建项目,现场机器都是Win10/Server 2019以后的环境,团队又普遍熟悉.NET,那纯C#是完全可行的。C#写COM可见组件并不困难,关键是抓住几个机制:
一是类必须标记[ComVisible(true)]和[Guid],方法要用显式接口实现来防止COM接口编号错乱;二是进程内和进程外的宿主要分开设计,Server本体建议做成独立进程或Windows服务,不要做成被客户端加载的进程内组件;三是所有被COM对象使用的内部数据结构必须注意线程安全,因为COM的客户端调用可能来自任意线程套间。
C#的另一个优势在于写管理工具。Server做出来后,配置设备驱动、导入点位表、设置采集周期、查看运行日志这些周边功能,在.NET生态里非常好写,WinForms/WPF随便挑。纯C++要写这些界面,工作量直接翻好几倍。所以如果你的Server需要经常改配置、调参、排查问题,C#方案能帮你省很多维护阶段的痛苦。
2.4 混合架构的常见设计
我在另一个项目里尝试过混合方案:C#写宿主机、配置界面、日志系统和管理工具,C++写设备驱动层和核心协议解析,两者通过P/Invoke调C API接口通信。C++DLL只负责一件事:把来自不同设备的原始数据转换成统一结构体,塞到一块共享内存环形缓冲区里,同时提供“写入设备”的函数入口。
C#宿主负责创建COM对象、注册到Windows、实现IOPCServer等接口,当客户端调用Read时,C#直接读共享缓冲区里的最新值。这种设计的核心好处是:底层协议栈跑在原生层,性能有保障;上层业务逻辑跑在托管层,开发效率高、出问题好排查。
但代价也很现实:C#和C++之间的数据交换要避免频繁跨边界拷贝,否则性能优势全没了。现场调通后,性能真的非常能打,标签规模上万甚至几万都能稳定撑住。如果你的设备协议种类特别繁杂,建议认真考虑这种混合架构,它是性能与可维护性的平衡点。
3. 核心开发环节拆解:接口、内存与数据流
3.1 必须吃透的COM接口体系
OPC DA 2.05a规范定义了一套严格的COM接口。Server级别要实现的IOPCServer、IOPCBrowseServerAddressSpace,Group级别的IOPCItemMgt、IOPCGroupStateMgt、IOPCSyncIO、IOPCAsyncIO2,以及回调用的IOPCDataCallback。规范原文不复杂,但全部手动实现一遍工作量非常大。
新手容易犯的错误是试图“绕开接口体系”,自己定义一套简化的C#接口然后暴露出去。客户端如果是你自己写的没问题,但市面上所有标准OPC客户端都只认规范里的固定CLSID和接口ID,你自定义的接口它们根本解析不了。所以做Server的第一步就是老老实实按规范来,不要发挥想象力。
建议把所有接口的声明按照规范原样整理成一份项目内的公共头文件,C++版本和C#版本都用它做底层参照,保证和规范完全一致。我甚至在项目里专门放了一个只读文件夹,里面存着OPC DA 2.05a的PDF、接口定义和测试客户端的源码,出了分歧直接翻原始规范,不要靠记忆。
3.2 C++实现:采集线程、标签内存表与ATL Server
先看一下C++方案里最核心的标签内存表设计。因为C++里没有托管的并发容器,需要自己管理锁和内存布局。一个基础的结构体设计长这样:
typedef struct _TAG_ITEM { WCHAR szItemID[128]; // 标签路径,如 L"PLC1.Temperature" VARIANT vValue; // 最新采集值 FILETIME ftTimestamp; // 采集时间戳 WORD wQuality; // OPC质量码 CRITICAL_SECTION csLock; // 每个标签的写保护 BOOL bChanged; // 变化标记,供异步回调判断 } TAG_ITEM;这个结构体之所以重要,是因为它直接决定了并发性能。每个标签自带一个锁,采集线程更新某个标签时只锁该标签,不会影响其他标签的读取;COM线程读取时也只锁当前标签,锁的粒度很小。如果把锁加在全局标签表上,采集线程一写整个表,所有客户端读取就全都等着,那个性能是真的不能看。
C++方案最稳妥的宿主形式是做成独立的进程内DLL,再配合一个exe宿主,用LocalServer32方式注册。直接做一个进程外组件好处是,客户端崩溃了不会把Server拖死,Server的采集线程也能一直跑。注意ATL项目默认生成的是进程内组件,需要手动调整,否则生产环境一跑就各种莫名其妙的问题。
3.3 C#实现:互操作定义与托管组件的生命周期
C#方案里,核心挑战是COM互操作定义。通过[ComVisible(true)]和显式接口实现保证了COM接口编号正确。一个典型的类骨架是这样:
[ComVisible(true)] [Guid("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx")] [ProgId("MyOpcServer.Server.1")] [ClassInterface(ClassInterfaceType.None)] public class OpcServer : IOPCServer { private readonly ConcurrentDictionary<string, TagItem> _tagTable = new(); private readonly ConcurrentDictionary<int, OpcGroup> _groups = new(); public void Read(OPCHANDLE hGroup, OPCHANDLE[] hItems, int count, out OPCITEMSTATE[] values, out int[] errors) { values = new OPCITEMSTATE[count]; errors = new int[count]; for (int i = 0; i < count; i++) { // 从 _tagTable 拿到最新缓存值和时间戳、质量码 // 注意此方法可能被COM线程池并发调用,必须保证线程安全 } } }上面的代码是示意用的,真实接口签名要以互操作头文件为准。关键要领是:所有内部集合都必须用ConcurrentDictionary或者自建锁保护,因为COM客户端调用可能从任何线程进来,没有并发保护很容易出现偶发的数据错乱。
C#方案还有一个容易忽略的生命周期问题:如果Server宿主进程被GC回收了托管对象,COM组件却还被外部客户端持有引用,进程就会崩或者接口直接变僵尸。处理办法是把宿主进程的生命周期设为常驻,不让GC因为“没有托管引用”就去回收还在被COM调用的对象。我甚至专门在Server宿主里写过一个心跳对象,每30秒让托管层和COM层互相握一次手,确保两边都不会被回收。
3.4 时间戳与质量码:两个被忽略的关键字段
很多新手做OPC DA Server时,注意力全放在数值上报上,时间戳和质量码直接写死或者随便填,这种做法会非常坑。OPC客户端上层往往有报警、报表、分析功能,全部依赖时间戳和质量码判断数据能不能用。时间戳填错,报表曲线直接乱掉,质量码填错,上层系统会把错误数据当成有效值去做决策。
时间戳应该反映的是“这个值是什么时候从设备采集到的”,不是“Server什么时候写入内存的”。如果你在采集线程里拿到的设备数据自带时间戳,优先用设备时间;如果没有,就在数据到达采集线程的时刻打上FILETIME。
质量码是高8位固定为状态大类,0xC0是Good,0x00是Bad,0x40是Uncertain。现场最常见的一个错误是:设备通信断开了,采集线程读不到数据,却把上一次缓存的Good值继续返回给客户端。这在语义上就错了。设备断线之后,标签质量应该切成Bad,并且保留一个“最后已知值”的副本,只有在客户端明确要求读旧值时才会用到。后来我养成了一个习惯:采集线程里任何一条赋值语句,都强制同时修改值、时间戳、质量码三个字段,绝不分开做。
3.5 异步回调:把设备变化主动推给客户端
OPC DA最强的能力之一就是异步通知。客户端注册了一个Group,设置好更新周期和死区,Server会在数据变化时主动调客户端的OnDataChange回调接口。这样客户端不需要高频轮询,数据一有风吹草动就会收到。
实现这个机制,核心问题是“怎么判断数据变了”。比较稳妥的做法是:采集线程每次更新标签后,检查新值跟旧值的差异是否超过死区(PercentDeadband),超过就给自己所在Group标记一个脏位。Server的内部调度器每个更新周期扫一遍所有Group的脏位,给有脏位且有客户端订阅的Group派发回调。
这里有一个效率陷阱:不要每来一个变化就立刻调一次COM回调。COM调用本身是有开销的,如果现场同时有几十个标签高频抖动,回调是会被打爆的。我在实际项目里就是给回调增加了一个极小的攒批窗口,把同一批次的变化事件按组打包,一次性发给客户端。OpcGroup里维护一个待通知的Item句柄列表,攒够一定数量或者到时间就统一调用OnDataChange。实测下来不仅客户端负载下降,Server本身也更少出现COM回调重入导致的死锁。
4. 部署与联网配置:DCOM是最大的坑
4.1 单机部署与跨机器部署的差异
OPC DA在单机场景下部署相对简单,因为它本质上是进程内调用,只要COM组件注册成功,客户端和Server跑在同一台机器上,连身份验证都可能不太严格。真正的噩梦在跨机器场景,也就是客户端在一台机器,Server在另一台机器,两者通过DCOM通信。这种情况下,身份验证、权限配置、防火墙端口、代理桩注册,一个不处理好就是连不上。
跨机器部署对Server的线程模型要求也会突然变高。单机时你随便从线程池回调客户端,问题可能不暴露;跨机器时COM要跨网络执行RPC,服务器端会开辟额外的RPC线程来等待客户端返回值,这时候如果Server端阻塞了RPC线程,客户端很快就会报RPC失败。这也是我特别强调C++和C#都要保持回调调用快速返回、不执行重活的原因。
4.2 DCOM权限配置操作清单
DCOM权限配置是OPC DA部署里出现频率最高的故障点。下面这份清单是我在实际项目里反复配出来的,建议直接照抄:
- 在Server机器上运行 dcomcnfg 打开组件服务。
- 找到自己的OPC Server组件,进入属性设置。
- “常规”页确认身份验证级别是“默认”或“无”。
- “位置”页勾选“在数据所在的那台计算机上运行”。
- “安全”页分别配置“启动权限”“激活权限”“访问权限”,把需要连接的客户端账户加进去并给足权限。
- “标识”页选“交互式用户”方便调试,正式环境建议指定一个专用服务账户,别用“系统账户”或“管理员”。
这里重点解释一下“标识”页的含义。它决定Server进程以哪个Windows账户的身份运行,这直接影响了Server访问网络设备、读取文件、写日志的能力。我们在调试时把标识设为“交互式用户”,好处是Server弹出的各种错误框能直接看到,坏处是一旦有人远程桌面断开,Server可能跟着歇菜。正式环境还是应该用专门的域账户或本地账户,把密码设成永不过期。
4.3 固定RPC动态端口:让防火墙真正可控
DCOM默认会使用TCP 135端口来协商通信端口,协商完之后,实际数据流走的是系统动态范围内的某个TCP端口。在完全没有防火墙的局域网里这当然无所谓,但一旦客户现场有安全策略,防火墙必须放行动态端口范围,不然客户端请求刚到135就被掐死,或者135通了但后续数据流被拦。
行业通用的做法是给OPC DA Server锁定一段RPC端口范围,然后在防火墙里只放行135加这一段。注册表配置如下:
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Rpc\Internet] "Ports"="4000-4100" "PortsInternetAvailable"="Y" "UseInternetPorts"="Y"这段配置的意思是,RPC通信可以使用4000到4100这101个端口,并在默认情况下允许这些端口用于互联网通信。配置完成后要重启Server机器才生效。如果在重启后拿netstat观察,你会发现Server进程的TCP监听端口都落在4000到4100这个区间内,这就说明配置成功了。
4.4 32位/64位与代理桩的注册问题
OPC DA 2.05a毕竟是COM时代的产物,32位和64位的问题特别现实。一个常见情况是客户端是32位程序,Server是64位服务,两边用DCOM互访时,系统COM层会自动跨位数转发,但前提是两边都装了对应的代理桩动态库。
OPC Foundation官方提供的ps-opcda代理桩是必须有的,而且32位和64位版本都要注册。千万不要以为只装一个就行,我就是在这个坑上摔过一次。某次部署时Server机器只注册了64位的代理桩,32位的Delphi客户端怎么都报接口找不到,折腾了很久才发现32位版本的DLL没有在SysWOW64下注册。这种问题非常隐蔽,因为本地调试时通常64位客户端连64位Server,一切正常,到了客户现场换32位客户端才出事。
5. 常见问题排查与性能调优实录
5.1 高频报错与自查清单
自己做Server开发和部署,最怕的就是客户端连不上、回调收不到、读写数据乱套。我把高频问题整理成了一张某公司内部流传的排查表,走在路上删减一些信息不算泄密,但核心内容值得保留:
| 现象 | 典型错误码 | 直接原因 | 优先排查点 |
|---|---|---|---|
| 客户端找不到Server | REGDB_E_CLASSNOTREG (0x80040154) | 组件未注册 | 检查CLSID注册表项、位数是否匹配 |
| 权限不足拒绝访问 | E_ACCESSDENIED (0x80070005) | DCOM权限配置有误 | 检查“安全”页是否授权客户端账户 |
| RPC服务器不可用 | RPC_S_SERVER_UNAVAILABLE (0x800706BA) | 通信链路断 | 检查Server进程是否存活、防火墙端口 |
| 回调收不到 | 无显式报错 | 回调对象生命周期或线程模型 | 检查客户端是否保持回调引用、身份验证级别 |
| 数值读出来全是0 | 无报错 | 采集线程未启动 | 查看Server日志,确认设备驱动通道 |
这张表的价值在于,它把“现象”和“原因”对应起来了。很多同学看到错误码就慌,看到0x80070005以为程序有bug,实际上配置层面就能解决。建议把这张表打印出来贴在工位上。
5.2 客户端怎么都连不上:按这个顺序排查
连不上这个问题百分之九十出在网络和权限,而不是功能逻辑。我自己排查时有一个固定顺序,分享给你参考。
第一步,用服务器本机的测试客户端去连本地Server。如果本机都连不上,问题在Server自身:服务没起、组件没注册、接口实现崩溃。本机能连上,Go下一步。
第二步,换一台同网段的机器用标准OPC客户端去连。如果连不上,先看防火墙,确认135端口通不通,动态RPC端口范围有没有放行。Windows自带telnet测试端口、用ping测试连通性都试一遍。
第三步,确认客户端和Server的归属域情况,不要在非域环境里用域账户身份验证。很多现场是工作组环境,DCOM身份验证默认要求Kerberos,非域环境下很难走通。这时候要把DCOM的身份验证级别调成“无”或“连接级”,再配合“标识”页里的账户设置,往往马上就通了。
这三步走完,大部分连不上问题都已经解决了。如果还不行,就去看系统事件日志,COM和DCOM的报错一般都会在事件查看器里留下一条很详细的记录,直接按记录里的错误码对照排查表处理。
5.3 回调延迟高、收不到:线程模型与生命周期检查
回调收不到是最容易让人挠头的问题。客户端确实建了Group、注册了回调接口,Server采集线程也看到数据在变,但客户端就是没有反应。这种问题要从两个方向查。
一个方向是生命周期。C#客户端如果把回调对象写成一个局部变量,函数执行完变量就被GC回收了,COM回调自然就断了。我在项目里要求所有客户端代码必须把IOPCDataCallback回调对象保存在一个成员变量里,防止被垃圾回收。别笑,工业现场的第三方客户端系统里真的有人犯过这种错。
另一个方向是线程模型。COM回调是服务器进程在自己线程池里执行回调函数,如果服务器端的宿主是STA线程,调用任何慢方法都可能造成串行堵塞。C++版本的Server最好明确走MTA模型,C#版本则要注意不要让回调线程去等待一个被主线程占用的锁,否则一旦发生锁竞争,表现就是偶发性的“回调一切正常,但延迟几秒甚至几十秒”。
我还遇到过一种情况:客户端在低配置老机器上跑,正常回调还行,一旦系统CPU占用率窜到100%,回调就开始丢。这个问题本质上是COM的RPC超时和客户端窗口消息队列阻塞,把客户端的身份验证级别适当调整,并在Server端每个回调批次降量,症状会缓解不少。
5.4 性能瓶颈:同步读与异步读的取舍、缓存策略
OPC DA Server的性能瓶颈很少出在“某一次读取”上,而是出在“高频反复读取”上。做个简单的计算:如果20个客户端同时每100毫秒同步读一次,每次读500个Item,那服务器每秒就要响应10万次Item读取。这个量级在C#里如果每次都查字典刷新缓存,很容易顶不住。
优化方向非常明确。首先,客户端能用异步读就尽量别用同步读,服务器对异步读的攒批优化收益远大于同步读。其次,服务器端要把Item地址解析做成快路径:客户端在AddItem阶段已经拿到了服务器分配的数字句柄,Read操作用句柄直接索引到标签内存表,不走字符串查找。
如果你做的Server点位规模上了万级,建议把标签内存表从普通的字典改成数组加哈希索引,并且把多个标签的值缓存在一个连续内存块里,减少缓存行的频繁颠簸。这里补充一个判断方法:如果服务器一上量CPU就飙升,但采集线程占用很小,基本可以断定是COM接口层做了太多的字符串转换或者锁竞争,优先从这两个地方找问题。我实测发现,通过把组内的Item句柄按升序排列,让批量读操作尽量顺序访问内存,性能能再涨一截。
6. 写在最后的几条选型建议与维护心得
真去做OPC DA Server,选C#还是选C++,与其跟着技术潮流走,不如先想清楚三个问题:一是团队里有没有人能稳定维护COM层的原生代码,二是现场运行环境是否允许方便地装.NET运行库,三是客户端那边有没有历史包袱(比如只能用32位老客户端)。
我的个人建议是:如果是新项目、新环境,优先选C#,把精力集中在协议驱动和业务逻辑上,别把时间耗在COM内存管理里;如果是老项目、老机器、老客户端,优先选C++,虽然开发慢一些,但部署稳定性的确更让人放心;如果团队两边都有能人,混合架构是长期最舒服的选择,只是项目初期的接口约定一定要设计好,不然沟通成本很高。
最后有一个维护心得特别想分享:OPC DA Server做出来只是开始,真正的价值体现在现场稳定运行半年以上不重启。开发阶段就把日志系统做扎实,采集线程的每一次异常、COM接口的每一次错误返回、DCOM身份的每一次切换,都尽量留下痕迹。现场出了问题,没有日志,所有人都在猜;有了完整日志,大部分问题都能在半小时内定位。我自己后来做OPC DA相关项目,几乎都是先写日志框架再写业务逻辑,这个习惯救了我不知道多少次(有一次夜里两点半被叫起来处理的紧急故障,就是靠日志里的连续超时记录锁定了外围交换机问题,而这个交换机问题又是因为线缆氧化导致的偶发丢包)。工业场景就是这样,稳定压倒一切,能跑几年不重启的Server,比功能炫酷但三天两头拉胯的Server值钱得多。