2. SOME/IP服务发现进阶:订阅与取消订阅
任务目标
在SOME/IP服务发现(SD)模块中,实现客户端对服务端事件组的订阅(Subscribe)与取消订阅(Unsubscribe)功能,并正确处理订阅确认(SubscribeAck)与错误响应(SubscribeNack)。本案例聚焦于SD协议层状态机与报文处理逻辑,不涉及具体传输层实现。
核心概念
SOME/IP-SD中,订阅流程使用SubscribeEventgroup报文,服务端以SubscribeEventgroupAck确认或SubscribeEventgroupNack拒绝。客户端需维护订阅状态,处理重试与超时。取消订阅使用UnsubscribeEventgroup报文,服务端以UnsubscribeEventgroupAck确认。
关键实现
以下代码实现SD客户端订阅管理核心逻辑,包含:订阅请求发送、确认/错误处理、取消订阅、超时重试。代码使用C++11编写,不依赖特定操作系统API,便于移植。
#include <cstdint>
#include <cstring>
#include <map>
#include <functional>
// SD报文类型常量
enum SDMessageType : uint8_t {
SUBSCRIBE = 0x06,
SUBSCRIBE_ACK = 0x07,
SUBSCRIBE_NACK = 0x08,
UNSUBSCRIBE = 0x09,
UNSUBSCRIBE_ACK = 0x0A
};
// 订阅状态
enum class SubState : uint8_t {
NOT_SUBSCRIBED = 0,
SUBSCRIBE_PENDING = 1,
SUBSCRIBED = 2,
UNSUBSCRIBE_PENDING = 3
};
// 订阅条目
struct Subscription {
uint16_t service_id;
uint16_t instance_id;
uint16_t eventgroup_id;
SubState state;
uint8_t retry_count;
uint32_t ttl_sec; // 有效时间(秒)
uint32_t expiry_timestamp; // 过期时间戳(秒)
};
// SD报文头(简化)
struct SDHeader {
uint8_t msg_type;
uint16_t service_id;
uint16_t instance_id;
uint16_t eventgroup_id;
uint32_t ttl_sec;
};
// 回调接口
struct SDCallbacks {
std::function<void(const Subscription&)> on_subscribed;
std::function<void(const Subscription&, uint8_t error_code)> on_subscribe_failed;
std::function<void(const Subscription&)> on_unsubscribed;
};
class SDClient {
public:
explicit SDClient(SDCallbacks cb) : cb_(std::move(cb)) {}
// 发送订阅请求
bool subscribe(uint16_t service_id, uint16_t instance_id,
uint16_t eventgroup_id, uint32_t ttl_sec = 30) {
Subscription sub{service_id, instance_id, eventgroup_id,
SubState::SUBSCRIBE_PENDING, 0, ttl_sec, 0};
auto key = make_key(service_id, instance_id, eventgroup_id);
if (subs_.count(key) && subs_[key].state == SubState::SUBSCRIBED) {
return false; // 已订阅
}
subs_[key] = sub;
send_sd_message(SUBSCRIBE, service_id, instance_id, eventgroup_id, ttl_sec);
return true;
}
// 取消订阅
bool unsubscribe(uint16_t service_id, uint16_t instance_id,
uint16_t eventgroup_id) {
auto key = make_key(service_id, instance_id, eventgroup_id);
auto it = subs_.find(key);
if (it == subs_.end() || it->second.state == SubState::NOT_SUBSCRIBED) {
return false;
}
it->second.state = SubState::UNSUBSCRIBE_PENDING;
send_sd_message(UNSUBSCRIBE, service_id, instance_id, eventgroup_id, 0);
return true;
}
// 处理接收到的SD报文(由网络层调用)
void handle_sd_message(const SDHeader& hdr) {
auto key = make_key(hdr.service_id, hdr.instance_id, hdr.eventgroup_id);
auto it = subs_.find(key);
if (it == subs_.end()) return;
Subscription& sub = it->second;
switch (hdr.msg_type) {
case SUBSCRIBE_ACK:
if (sub.state == SubState::SUBSCRIBE_PENDING) {
sub.state = SubState::SUBSCRIBED;
sub.ttl_sec = hdr.ttl_sec;
sub.expiry_timestamp = now_sec() + hdr.ttl_sec;
if (cb_.on_subscribed) cb_.on_subscribed(sub);
}
break;
case SUBSCRIBE_NACK:
if (sub.state == SubState::SUBSCRIBE_PENDING) {
sub.state = SubState::NOT_SUBSCRIBED;
if (cb_.on_subscribe_failed) cb_.on_subscribe_failed(sub, 0x01);
}
break;
case UNSUBSCRIBE_ACK:
if (sub.state == SubState::UNSUBSCRIBE_PENDING) {
subs_.erase(it);
if (cb_.on_unsubscribed) cb_.on_unsubscribed(sub);
}
break;
default:
break;
}
}
// 定时器驱动:处理超时重试与过期(每秒调用一次)
void tick() {
uint32_t now = now_sec();
for (auto& kv : subs_) {
Subscription& sub = kv.second;
if (sub.state == SubState::SUBSCRIBE_PENDING) {
if (sub.retry_count < 3) {
sub.retry_count++;
send_sd_message(SUBSCRIBE, sub.service_id, sub.instance_id,
sub.eventgroup_id, sub.ttl_sec);
} else {
sub.state = SubState::NOT_SUBSCRIBED;
if (cb_.on_subscribe_failed) cb_.on_subscribe_failed(sub, 0x02);
}
} else if (sub.state == SubState::SUBSCRIBED) {
if (now >= sub.expiry_timestamp) {
sub.state = SubState::NOT_SUBSCRIBED;
if (cb_.on_subscribe_failed) cb_.on_subscribe_failed(sub, 0x03);
}
}
}
}
private:
static uint64_t make_key(uint16_t s, uint16_t i, uint16_t eg) {
return (uint64_t)s << 32 | (uint64_t)i << 16 | eg;
}
void send_sd_message(uint8_t type, uint16_t s, uint16_t i,
uint16_t eg, uint32_t ttl) {
SDHeader hdr{type, s, i, eg, ttl};
// 实际工程中在此处调用网络发送接口
// 本示例仅构造报文,不实际发送
(void)hdr;
}
static uint32_t now_sec() {
// 简化时间获取,实际应使用系统时间
static uint32_t counter = 0;
return ++counter;
}
SDCallbacks cb_;
std::map<uint64_t, Subscription> subs_;
};
验证方法
编写以下测试代码验证订阅状态机转换:
#include <cassert>
#include <iostream>
int main() {
bool sub_ok = false, unsub_ok = false, nack_ok = false;
SDCallbacks cb;
cb.on_subscribed = [&](const Subscription&) { sub_ok = true; };
cb.on_unsubscribed = [&](const Subscription&) { unsub_ok = true; };
cb.on_subscribe_failed = [&](const Subscription&, uint8_t) { nack_ok = true; };
SDClient client(cb);
// 测试1:正常订阅-确认流程
client.subscribe(0x1234, 0x5678, 0x01);
SDHeader ack{SUBSCRIBE_ACK, 0x1234, 0x5678, 0x01, 30};
client.handle_sd_message(ack);
assert(sub_ok);
// 测试2:取消订阅-确认流程
client.unsubscribe(0x1234, 0x5678, 0x01);
SDHeader unsub_ack{UNSUBSCRIBE_ACK, 0x1234, 0x5678, 0x01, 0};
client.handle_sd_message(unsub_ack);
assert(unsub_ok);
// 测试3:订阅被拒绝(Nack)
client.subscribe(0x1111, 0x2222, 0x03);
SDHeader nack{SUBSCRIBE_NACK, 0x1111, 0x2222, 0x03, 0};
client.handle_sd_message(nack);
assert(nack_ok);
std::cout << "All tests passed" << std::endl;
return 0;
}
编译与运行:
g++ -std=c++11 -o sd_test sd_test.cpp
./sd_test
通过条件:程序输出All tests passed,且无断言失败。测试覆盖三条核心路径:订阅确认、取消订阅确认、订阅被拒绝。
关键说明
send_sd_message函数中仅构造报文头,实际发送需接入网络层(如DoIP或UDP socket),本案例聚焦SD逻辑。- 重试策略为最多3次,超时后回调
on_subscribe_failed并携带错误码0x02;订阅过期(TTL到期)错误码为0x03。 - 实际工程中
now_sec()应替换为系统时钟(如std::time),此处用计数器模拟以便测试。