Websocket (Professional)
此功能在 realvirtual 6.3 (Professional) 中添加。
此接口需要 REALVIRTUAL_JSON 编译器定义。有关设置说明,请参见 Newtonsoft JSON。
概述
WebSocket 实时接口 (WebSocket Realtime Interface) 通过 WebSocket 连接提供高性能、双向的信号交换。它既可作为服务器也可作为客户端运行,支持两个 Unity 实例、外部应用程序、PLC 或任何支持 WebSocket 通信的系统之间的连接。
基于 FastInterface 框架构建,提供线程安全的后台通信、自动重连、变化检测和低延迟信号传输。
核心能力:
- 单组件中的服务器和客户端模式
- 扁平 JSON 通信协议 (v2),便于从任何语言集成
- 从远端导入信号,自动反转方向
- SSL/TLS 支持 (wss\://),通过反向代理实现安全连接
- 基于模式的信号方向检测,适用于没有 PLC 类型前缀的外部系统
- 订阅筛选,使客户端只接收所需的信号
设置
添加接口
- 将 WebsocketRealtimeInterface 预制体拖入场景,或将组件添加到现有 GameObject 上
- 接口显示为 realvirtual 根对象的子对象
- 在接口下创建 PLC 信号对象 (PLCInputBool, PLCOutputFloat 等) 作为子对象

服务器模式
将 Is Server 设置为 true(在 Connection Settings 折叠面板中)。服务器在配置的地址和端口上监听,并向所有已连接的客户端广播信号数据。
- Address: 要绑定的 IP (
127.0.0.1用于本机,0.0.0.0用于所有接口) - Port: TCP 端口(默认:1234)
客户端模式
将 Is Server 设置为 false。客户端连接到远程 WebSocket 服务器。
- Address: 服务器的 IP 地址
- Port: 服务器的 TCP 端口
- Use SSL: 启用以使用
wss://连接(如通过反向代理) - Path: 可选的 URL 路径(如
/my-bridge/ws用于反向代理路由) - Client Name: 握手期间发送的标识名称
信号导入
客户端模式导入(编辑模式)
在客户端模式下,您可以从远程服务器导入信号而无需手动创建:
- 确保远程服务器正在运行
- 在 Inspector 中点击 Import Signals(在编辑模式下工作 — 无需进入播放模式)
- 接口连接、请求信号目录、创建方向反转的本地镜像信号,然后断开连接
您也可以使用 Test Connection 来验证连接而不进行导入。
服务器模式导入(播放模式)
在服务器模式下,您可以在播放模式期间从已连接的客户端导入信号:
- 启动仿真,使服务器运行且客户端已连接
- 在 Inspector 中点击 Import Signals from Clients
- 服务器向所有已连接的客户端广播导入请求,并创建方向反转的本地镜像信号
这对于发现远程客户端提供的信号而无需手动定义非常有用。
属性
配置
| 属性 | 描述 |
|---|---|
| Update Cycle Ms | 通信循环间隔(毫秒)(默认:10) |
| Only Transmit Changed Inputs | 仅发送自上次周期以来值发生变化的信号 |
| Auto Reconnect | 连接断开时自动重连 |
| Reconnect Interval Seconds | 重连尝试之间的间隔(秒) |
| Max Reconnect Attempts | 放弃前的最大重连尝试次数(-1 = 无限) |
| Debug Mode | 启用详细日志记录以便故障排除 |
连接设置
| 属性 | 描述 |
|---|---|
| Is Server | true = WebSocket 服务器,false = WebSocket 客户端 |
| Address | 要绑定的 IP(服务器)或要连接的 IP(客户端) |
| Port | 通信 TCP 端口(默认:1234) |
| Use SSL | (仅客户端)使用 wss:// 进行加密连接 |
| Path | (仅客户端)追加在 host:port 之后的可选 URL 路径 |
| Client Name | (仅客户端)初始化握手期间发送的名称 |
| Connect On Play | 进入播放模式时自动连接 |
状态
| 属性 | 描述 |
|---|---|
| Connected Clients | (仅服务器)当前已连接的客户端数量 |
信号导入
| 属性 | 描述 |
|---|---|
| Default Signal Direction | 无模式匹配时导入信号的方向(Input 或 Output) |
| Use Pattern Matching | 启用基于名称的方向检测 |
| Input Patterns | 标识 Unity 写入信号的名称子串(如 "Unity") |
| Output Patterns | 标识 PLC 写入信号的名称子串 |
用例
- Unity 到 Unity 的分布式仿真,带有信号镜像
- 外部应用程序集成(Web 仪表板、自定义 HMI、测试框架)
- PLC 桥接连接(如通过运行在 ctrlX CORE 或其他边缘设备上的桥接应用程序)
- WebGL 构建,其中基于 TCP 的协议不可用
通信协议 v2 参考
WebSocket 实时接口使用扁平 JSON 协议。所有消息都是 UTF-8 编码的 JSON 对象,以 WebSocket 文本帧发送。没有双重编码 — 所有字段都是原生 JSON 类型。
消息信封
每条消息都有一个 type 字段来确定其用途:
{
"type": "<message_type>",
"version": 2,
...
}
消息类型
init - 客户端标识
客户端在 WebSocket 连接建立后立即发送。
{
"type": "init",
"version": 2,
"name": "MyClient"
}
| 字段 | 类型 | 描述 |
|---|---|---|
type | string | 始终为 "init" |
version | int | 协议版本 (2) |
name | string | 客户端标识名称 |
data - 周期性信号交换
双向周期性发送。包含信号名称及其当前值。
{
"type": "data",
"signals": {
"Sensor1": true,
"DriveSpeed": 150.5,
"MotorOn": false,
"Counter": 42,
"Status": "Running"
}
}
| 字段 | 类型 | 描述 |
|---|---|---|
type | string | 始终为 "data" |
signals | object | 信号名称到当前值的映射。值为原生 JSON 类型:true/false 用于布尔值,数字用于整数/浮点数,字符串用于文本。 |
import_request - 信号发现请求
从一端发送到另一端,请求完整的信号目录。
{
"type": "import_request",
"version": 2
}
import_answer - 信号目录响应
对 import_request 的响应。包含所有信号名称、其当前值和 PLC 类型字符串。
{
"type": "import_answer",
"version": 2,
"signals": {
"Sensor1": true,
"DriveSpeed": 0.0,
"Counter": 0
},
"signalTypes": {
"Sensor1": "PLCInputBool",
"DriveSpeed": "PLCInputFloat",
"Counter": "PLCOutputInt"
}
}
| 字段 | 类型 | 描述 |
|---|---|---|
signals | object | 信号名称到当前值 |
signalTypes | object | 信号名称到类型字符串。识别的类型:PLCInputBool、PLCOutputBool、PLCInputInt、PLCOutputInt、PLCInputFloat、PLCOutputFloat、PLCInputText、PLCOutputText。也接受不带前缀的中性类型(如 Bool、Float)。 |
导入时的方向反转: 当接收端导入信号时,方向会被反转:
- 远端
PLCInputBool(远端从 PLC 读取)变为本地PLCOutputBool(本地写入 Unity) - 远端
PLCOutputBool(远端写入 PLC)变为本地PLCInputBool(本地从 Unity 读取)
subscribe - 筛选服务器输出
客户端发送,告知服务器它希望接收哪些信号。服务器随后在 data 消息中只发送匹配的信号。如果未发送订阅消息,服务器发送所有信号。
{
"type": "subscribe",
"version": 2,
"subscribe": ["Sensor1", "DriveSpeed", "Counter"]
}
snapshot - 即时完整状态
服务器在收到 subscribe 消息后发送。包含所有已订阅信号的当前值。
{
"type": "snapshot",
"signals": {
"Sensor1": true,
"DriveSpeed": 0.0,
"Counter": 0
}
}
config_get - 读取服务器配置
客户端发送,从服务器请求当前配置(如桥接应用程序)。
{
"type": "config_get",
"version": 2
}
config_answer - 配置响应
服务器响应 config_get 发送。包含作为键值映射的当前配置。
{
"type": "config_answer",
"version": 2,
"config": {
"publishIntervalMs": 5,
"loggingLevel": "Information",
"browsePaths": [
{ "prefix": "plc/app/Application/sym", "direction": "input", "recursive": true }
]
}
}
| 字段 | 类型 | 描述 |
|---|---|---|
config | object | 应用程序特定的键值配置映射。结构取决于服务器实现。 |
config_set - 写入服务器配置
客户端发送,更新服务器上的配置值。服务器应持久化并应用新配置。
{
"type": "config_set",
"version": 2,
"config": {
"publishIntervalMs": 10,
"loggingLevel": "Debug",
"browsePaths": [
{ "prefix": "plc/app/Application/sym", "direction": "input", "recursive": true }
]
}
}
config_result - 配置更新结果
服务器响应 config_set 发送。报告配置是否已成功应用。
{
"type": "config_result",
"version": 2,
"success": true,
"message": "Configuration saved and applied"
}
| 字段 | 类型 | 描述 |
|---|---|---|
success | boolean | true 表示配置已应用,false 表示出错 |
message | string | 人类可读的结果消息 |
config 消息是可选的且特定于应用程序。它们被 ctrlX 桥接器用于远程配置发布间隔、日志级别和 Data Layer 浏览路径。如果您实现自己的服务器,可以为 config 字段定义任何键值结构,或完全忽略这些消息。
信号值类型
| realvirtual 类型 | JSON 类型 | 示例 |
|---|---|---|
PLCInputBool / PLCOutputBool | boolean | true, false |
PLCInputInt / PLCOutputInt | integer | 42, -1, 0 |
PLCInputFloat / PLCOutputFloat | number | 3.14, 0.0, -100.5 |
PLCInputText / PLCOutputText | string | "Running", "" |
实现另一端
本节描述如何构建您自己的 WebSocket 应用程序,使其与 realvirtual WebSocket 实时接口通信。您可以使用任何支持 WebSocket 连接的编程语言。
连接序列
连接到 realvirtual 服务器时:
1. 打开到 ws://address:port 的 WebSocket 连接
2. 发送带有客户端名称的 "init" 消息
3. (可选)发送 "import_request" 以发现可用信号
4. (可选)发送 "subscribe" 以筛选要接收的信号
5. 开始 "data" 消息的周期性交换
运行您自己的服务器,realvirtual 作为客户端连接时:
1. 在您的 address:port 上启动 WebSocket 服务器
2. 接收 realvirtual 发送的 "init" 消息(客户端标识自身)
3. (可选)接收列出客户端所需信号的 "subscribe" 消息
4. (可选)发送已订阅信号当前值的 "snapshot"
5. 开始 "data" 消息的周期性交换
示例:Python 客户端
一个最小化的 Python 客户端,连接到 realvirtual 服务器、导入信号并交换数据:
import asyncio
import json
import websockets
async def main():
uri = "ws://127.0.0.1:8080"
async with websockets.connect(uri) as ws:
# 1. 发送 init
await ws.send(json.dumps({
"type": "init",
"version": 2,
"name": "PythonClient"
}))
print("Connected and init sent")
# 2. 请求信号目录
await ws.send(json.dumps({
"type": "import_request",
"version": 2
}))
# 3. 等待 import_answer
while True:
raw = await ws.recv()
msg = json.loads(raw)
if msg.get("type") == "import_answer":
print(f"Available signals: {list(msg['signalTypes'].keys())}")
break
# 4. 订阅特定信号(可选)
await ws.send(json.dumps({
"type": "subscribe",
"version": 2,
"subscribe": ["Sensor1", "DriveSpeed"]
}))
# 5. 周期性数据交换
while True:
# 从 realvirtual 接收数据
raw = await asyncio.wait_for(ws.recv(), timeout=5.0)
msg = json.loads(raw)
if msg.get("type") in ("data", "snapshot"):
signals = msg.get("signals", {})
print(f"Received: {signals}")
# 向 realvirtual 发送数据
await ws.send(json.dumps({
"type": "data",
"signals": {
"MyOutput": True,
"SetSpeed": 100.5
}
}))
asyncio.run(main())
示例:Python 服务器
一个 realvirtual 作为客户端连接的服务器。当您的应用程序是"主机"而 realvirtual 是连接的客户端时非常有用:
import asyncio
import json
import websockets
# 信号状态
signals = {
"PLC_Running": True,
"PLC_Speed": 0.0,
"PLC_Counter": 0
}
signal_types = {
"PLC_Running": "PLCOutputBool",
"PLC_Speed": "PLCOutputFloat",
"PLC_Counter": "PLCOutputInt"
}
async def handle_client(ws):
client_name = "unknown"
subscribed_signals = None # None = 发送全部
async for raw in ws:
msg = json.loads(raw)
msg_type = msg.get("type")
if msg_type == "init":
client_name = msg.get("name", "unknown")
print(f"Client connected: {client_name}")
elif msg_type == "import_request":
# 发送信号目录
await ws.send(json.dumps({
"type": "import_answer",
"version": 2,
"signals": signals,
"signalTypes": signal_types
}))
elif msg_type == "subscribe":
subscribed_signals = msg.get("subscribe", [])
# 发送已订阅信号的快照
snapshot = {k: v for k, v in signals.items()
if k in subscribed_signals}
await ws.send(json.dumps({
"type": "snapshot",
"signals": snapshot
}))
elif msg_type == "data":
# 从 realvirtual 接收值
received = msg.get("signals", {})
print(f"From {client_name}: {received}")
# 在您的应用程序中处理接收到的值...
# 发送更新后的值
to_send = signals
if subscribed_signals is not None:
to_send = {k: v for k, v in signals.items()
if k in subscribed_signals}
await ws.send(json.dumps({
"type": "data",
"signals": to_send
}))
async def main():
async with websockets.serve(handle_client, "0.0.0.0", 8080):
print("WebSocket server running on ws://0.0.0.0:8080")
await asyncio.Future() # 永久运行
asyncio.run(main())
示例:C# 客户端 (.NET)
适用于 .NET 应用程序(如 PLC 或边缘设备上的桥接应用程序):
using System;
using System.Collections.Generic;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
public class RealvirtualWebSocketClient
{
private ClientWebSocket ws;
private readonly string uri;
private readonly string clientName;
public RealvirtualWebSocketClient(string uri, string clientName = "DotNetClient")
{
this.uri = uri;
this.clientName = clientName;
}
public async Task ConnectAsync(CancellationToken ct = default)
{
ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri(uri), ct);
// 发送 init
await SendAsync(new
{
type = "init",
version = 2,
name = clientName
}, ct);
}
public async Task<Dictionary<string, JsonElement>> ReceiveDataAsync(
CancellationToken ct = default)
{
var buffer = new byte[8192];
var result = await ws.ReceiveAsync(buffer, ct);
var json = Encoding.UTF8.GetString(buffer, 0, result.Count);
var msg = JsonSerializer.Deserialize<JsonElement>(json);
var msgType = msg.GetProperty("type").GetString();
if (msgType == "data" || msgType == "snapshot")
{
var signals = new Dictionary<string, JsonElement>();
foreach (var prop in msg.GetProperty("signals").EnumerateObject())
signals[prop.Name] = prop.Value;
return signals;
}
return null;
}
public async Task SendDataAsync(Dictionary<string, object> signals,
CancellationToken ct = default)
{
await SendAsync(new { type = "data", signals }, ct);
}
public async Task RequestImportAsync(CancellationToken ct = default)
{
await SendAsync(new { type = "import_request", version = 2 }, ct);
}
private async Task SendAsync(object message, CancellationToken ct)
{
var json = JsonSerializer.Serialize(message);
var bytes = Encoding.UTF8.GetBytes(json);
await ws.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
}
}
示例:JavaScript / TypeScript(浏览器或 Node.js)
const ws = new WebSocket("ws://127.0.0.1:8080");
ws.onopen = () => {
// 发送 init
ws.send(JSON.stringify({
type: "init",
version: 2,
name: "WebClient"
}));
// 请求信号目录
ws.send(JSON.stringify({
type: "import_request",
version: 2
}));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case "import_answer":
console.log("Available signals:", Object.keys(msg.signalTypes));
// 订阅特定信号
ws.send(JSON.stringify({
type: "subscribe",
version: 2,
subscribe: ["Sensor1", "DriveSpeed"]
}));
break;
case "data":
case "snapshot":
console.log("Received signals:", msg.signals);
// 发送数据
ws.send(JSON.stringify({
type: "data",
signals: {
"MyOutput": true,
"SetSpeed": 100.5
}
}));
break;
}
};
方向映射指南
实现另一端时,理解信号方向至关重要:
| 您的应用程序发送... | realvirtual 创建... | 含义 |
|---|---|---|
signalTypes: {"Speed": "PLCOutputFloat"} | 本地 PLCInputFloat,名称为 "Speed" | 您的应用写入 Speed,Unity 读取它 |
signalTypes: {"Enable": "PLCInputBool"} | 本地 PLCOutputBool,名称为 "Enable" | 您的应用读取 Enable,Unity 写入它 |
signalTypes: {"Value": "Float"} | 方向由模式匹配或默认值决定 | 中性类型 - 无方向前缀 |
经验法则: 如果您的应用程序写入值,将其声明为 PLCOutput。如果您的应用程序从 Unity 读取值,将其声明为 PLCInput。realvirtual 在导入时反转方向。
实现者提示
- 仅 JSON - 所有消息都是 JSON 文本帧。不使用二进制帧。
- 无需心跳 - 周期性的
data消息充当隐式心跳。如果停止发送数据,连接保持打开。 - 部分更新 -
data消息不需要包含所有信号。您可以只发送已变化的信号。 - 类型转换 - realvirtual 会尝试转换 JSON 数字类型。为浮点信号发送
1或为整数信号发送1.0均可。 - 信号名称 - 在两端使用一致的名称。名称区分大小写且精确匹配。
- SSL/TLS - 对于通过反向代理 (nginx, traefik) 的生产部署,配置
useSSL=true并设置path以匹配您的代理路由。
故障排除
接口无法连接:
- 检查服务器是否正在运行且在配置的地址和端口上可访问
- 对于客户端模式,验证地址、端口、SSL 和路径设置
- 使用 Test Connection 按钮验证连接
- 启用 Debug Mode 获取详细日志
信号不更新:
- 确保信号是接口的子 GameObject
- 检查信号方向(Input = Unity 写入,Output = Unity 读取)
- 验证信号名称在两端是否完全匹配
- 检查发送的 data 消息是否包含预期的信号名称
导入创建错误的方向:
- 检查
import_answer中的signalTypes-PLCInput和PLCOutput在导入时会反转 - 对于中性类型,配置 Input Patterns 和 Output Patterns 或设置 Default Signal Direction
另请参见
- 自定义接口 (FastInterface) - 如何构建您自己的接口
- 信号管理器 - 管理 PLC 信号
- 接口概述 - 所有可用接口