机器人SDK通信架构设计:从TCP服务端到客户端

前言

在机器人SDK开发中,通信架构的设计直接影响系统的实时性、稳定性和可维护性。本文将分享一套经过实践验证的SDK通信架构设计方案,涵盖服务端(机器人控制器侧)与客户端(上位机侧)的完整设计。

整个系统的核心目标:

  • 服务端:运行在机器人控制器上,通过TCP/UDP接收指令并调用底层SDK
  • 客户端:运行在上位机,提供阻塞式的同步调用接口,隐藏网络通信细节

整体架构图

┌─────────────────────────────────────────────────────────────────────────┐
│                          上位机 (Client)                               │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  用户线程1         用户线程2          recv线程                 │   │
│  │  moveJ()           getState()        持续收包分发              │   │
│  │  send + wait       send + wait       id → promise             │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└───────────────────────────────┬─────────────────────────────────────────┘
                                │ TCP/UDP
                                ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                      机器人控制器 (Server)                             │
│                                                                         │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  异步层 (epoll主线程)                                          │   │
│  │  ┌──────────────────────────────────────────────────────────┐  │   │
│  │  │  非阻塞收包 → 命令分类                                    │  │   │
│  │  │  ├── getState/estop/disable  → 主线程立即处理返回        │  │   │
│  │  │  ├── moveJ/moveL/enable...   → 入队                     │  │   │
│  │  │  └── servoJ/servoDualJ       → UDP直接执行              │  │   │
│  │  └──────────────────────────────────────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │  同步层 (Worker线程)                                           │   │
│  │  ┌──────────────────────────────────────────────────────────┐  │   │
│  │  │  串行队列 → 取任务 → 调用SDK → 写回响应                  │  │   │
│  │  └──────────────────────────────────────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                          │
│                              ▼                                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                      底层 SDK                                   │   │
│  │  moveJ() / moveL() / servoJ() / getState()                    │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

服务端架构:SDK调用与网络解耦

为什么需要分层

底层SDK的运动控制接口是阻塞的(moveJ要等运动完成才返回),但网络收包不能阻塞。分层设计解决了这个矛盾:

网络层 → 快速收包(非阻塞)→ 队列缓冲 → SDK层 → 阻塞执行

命令分类

类型命令处理方式
快速命令getState, estop, disable主线程立即返回
阻塞命令moveJ, moveL, moveW, moveJDual, moveLDual, enable, setSpeed, setTool, setModeWorker串行执行
UDP命令servoJ, servoDualJ, servoWaist直接执行无需响应

Worker线程设计

为什么单Worker就够了?
├── servoJ走UDP → 本身并行
├── moveDualJ → SDK内部并行
├── 阻塞命令按顺序执行即可
└── 多Worker → 增加复杂度,收益几乎为零

客户端架构:把异步网络变成同步调用

核心设计目标

客户端的目标很明确:让调用者像调用本地函数一样调用远程SDK。

本地调用:moveJ(targets) → 等待返回 → 继续
远程调用:moveJ(targets) → 等待返回 → 继续(网络细节全部隐藏)

发收分离模型

┌─────────────────────────────────────────────────────────────────┐
│  调用线程A             调用线程B             recv线程          │
│       │                    │                    │              │
│  moveJ(id=1)          getState(id=2)           │              │
│  send(id=1)           send(id=2)               │              │
│  wait(id=1)           wait(id=2)               │              │
│       │                    │              持续recv            │
│       │                    │         收到响应id=1 → 唤醒A     │
│  返回结果               │         收到响应id=2 → 唤醒B     │
│       │                返回结果                              │
└─────────────────────────────────────────────────────────────────┘

核心数据结构

class RobotClient {
private:
    // 待响应的请求表
    std::map<int, std::shared_ptr<std::promise<json>>> pending_;
    std::atomic<int> next_id_{1};

public:
    json sendCommand(const json& req) {
        int id = ++next_id_;
        auto promise = std::make_shared<std::promise<json>>();
        pending_[id] = promise;
        
        send(req_with_id);  // 发送带id的请求
        auto future = promise->get_future();
        
        if (future.wait_for(60s) != ready) {
            pending_.erase(id);
            return 超时错误;
        }
        return future.get();
    }
    
    void recvLoop() {
        while (true) {
            json resp = recv();
            int id = resp["id"];
            auto it = pending_.find(id);
            if (it != pending_.end()) {
                it->second->set_value(resp);
                pending_.erase(it);
            }
        }
    }
};

阻塞风格 vs 回调风格

对比维度回调风格阻塞风格
代码形态每来一个包调一次函数发→等→处理,顺序执行
业务逻辑被切碎,分散在各回调完整连贯,像本地调用
错误处理难统一,易遗漏try-catch集中处理
可维护性

通信协议

TCP协议格式

每条消息:JSON + “/f/b”

请求示例:

{"id":2, "cmd":"moveJ", "params":{"arm":0, "targets":[10,20,30,0,0,0,0]}}/f/b

响应示例:

{"cmd":"moveJ","id":2,"result":0}/f/b

UDP协议格式

{"cmd":"servoJ", "params":{"arm":0, "targets":[10,20,30,0,0,0,0]}}/f/b

核心设计决策

层面设计决策
服务端Half-Sync/Half-Async,epoll + Worker串行队列
客户端发收分离,Promise/Future异步转同步
命令分类快速命令主线程处理,阻塞命令入队,UDP直接执行
编码风格阻塞式顺序调用,优于回调式
并发模型单Worker,不盲目增加复杂度

总结

这套架构兼顾了实时性、稳定性和开发效率。核心思想是:把网络通信的复杂性封装在SDK内部,对外提供像本地调用一样简单的接口。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐