返回博客

gRPC 自定义服务开发指南

coomia-dip 的内部通信全部基于 gRPC。这不仅仅是一个技术选型决策,更是平台设计哲学的体现——强类型契约、高性能序列化、双向流式通信。当你需要扩展平台功能时,编写自定义 gRPC 服务是最自然的方式。

Coomia发布于 2026年1月24日17 分钟阅读
分享本文Twitter / X

系列:S12 开发者教程 · 第 14 篇 | 难度:中级 | 阅读时间:15 分钟

gRPC 自定义服务开发指南

#引言

coomia-dip 的内部通信全部基于 gRPC。这不仅仅是一个技术选型决策,更是平台设计哲学的体现——强类型契约、高性能序列化、双向流式通信。当你需要扩展平台功能时,编写自定义 gRPC 服务是最自然的方式。

本教程将从 Protocol Buffers 定义开始,手把手带你完成一个自定义 gRPC 服务的全生命周期:定义接口、生成代码、实现服务、集成到平台、编写测试、部署上线。

#1. 为什么选择 gRPC

#1.1 REST vs gRPC:性能对比

在 coomia-dip 的早期原型中,我们也尝试过 REST/JSON。但在以下场景中,REST 的劣势非常明显:

维度REST/JSONgRPC/Protobuf
序列化大小100% (基准)约 30-50%
序列化速度100% (基准)约 5-10x 快
类型安全运行时验证编译时保证
流式通信需要 WebSocket原生支持
代码生成手动或 OpenAPI原生 protoc
错误处理HTTP 状态码 + 自定义标准 Status Code

在平台内部,每秒可能有成千上万条消息在不同 Layer 之间流转。gRPC 的低开销让这一切成为可能。

#1.2 coomia-dip 的 gRPC 约定

平台定义了统一的 gRPC 约定:

Code
proto/
├── onto/
│   ├── common/v1/         # 公共类型定义
│   │   ├── types.proto    # RID, Timestamp, Pagination 等
│   │   └── errors.proto   # 统一错误码
│   ├── ontology/v1/       # 本体服务
│   ├── action/v1/         # Action 服务
│   ├── query/v1/          # 查询服务
│   ├── subscription/v1/   # 订阅服务
│   └── custom/v1/         # 自定义扩展服务

命名约定:

  • 包名:onto.{domain}.v1
  • 服务名:{Domain}Service
  • 方法名:动词开头(Get, List, Create, Update, Delete, Stream
  • 消息名:{Method}Request / {Method}Response

#2. 定义 Protobuf 接口

#2.1 场景:设备监控服务

假设你需要为 IoT 设备监控场景构建一个自定义服务,提供设备状态查询、告警触发和实时数据流功能。

PROTOBUF
// proto/onto/device/v1/device_service.proto
syntax = "proto3";

package onto.device.v1;

import "onto/common/v1/types.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";

option java_package = "com.onto.device.v1";
option java_multiple_files = true;
option python_package = "onto.device.v1";

// 设备监控服务
service DeviceMonitorService {
    // 单个设备查询
    rpc GetDevice(GetDeviceRequest) returns (GetDeviceResponse);

    // 批量设备查询
    rpc ListDevices(ListDevicesRequest) returns (ListDevicesResponse);

    // 上报设备遥测数据
    rpc ReportTelemetry(ReportTelemetryRequest) returns (ReportTelemetryResponse);

    // 批量上报(客户端流式)
    rpc BatchReportTelemetry(stream TelemetryDataPoint) returns (BatchReportResponse);

    // 实时遥测数据流(服务端流式)
    rpc StreamTelemetry(StreamTelemetryRequest) returns (stream TelemetryDataPoint);

    // 双向流:实时命令与响应
    rpc DeviceCommandChannel(stream DeviceCommand) returns (stream CommandResponse);

    // 触发设备告警
    rpc TriggerAlert(TriggerAlertRequest) returns (TriggerAlertResponse);

    // 查询告警历史
    rpc ListAlerts(ListAlertsRequest) returns (ListAlertsResponse);
}

// ---- 消息定义 ----

message Device {
    string device_rid = 1;
    string device_name = 2;
    string device_type = 3;          // SENSOR, ACTUATOR, GATEWAY
    string location = 4;
    DeviceStatus status = 5;
    map<string, string> metadata = 6;
    google.protobuf.Timestamp last_seen = 7;
    google.protobuf.Timestamp registered_at = 8;
}

enum DeviceStatus {
    DEVICE_STATUS_UNSPECIFIED = 0;
    DEVICE_STATUS_ONLINE = 1;
    DEVICE_STATUS_OFFLINE = 2;
    DEVICE_STATUS_MAINTENANCE = 3;
    DEVICE_STATUS_ERROR = 4;
}

message TelemetryDataPoint {
    string device_rid = 1;
    string metric_name = 2;
    double value = 3;
    string unit = 4;
    google.protobuf.Timestamp timestamp = 5;
    map<string, string> tags = 6;
}

message GetDeviceRequest {
    string device_rid = 1;
    bool include_latest_telemetry = 2;
}

message GetDeviceResponse {
    Device device = 1;
    repeated TelemetryDataPoint latest_telemetry = 2;
}

message ListDevicesRequest {
    string device_type = 1;         // 可选过滤
    DeviceStatus status = 2;        // 可选过滤
    string location_prefix = 3;     // 位置前缀匹配
    onto.common.v1.Pagination pagination = 4;
}

message ListDevicesResponse {
    repeated Device devices = 1;
    onto.common.v1.PaginationInfo pagination_info = 2;
}

message ReportTelemetryRequest {
    repeated TelemetryDataPoint data_points = 1;
}

message ReportTelemetryResponse {
    int32 accepted_count = 1;
    int32 rejected_count = 2;
    repeated string rejected_reasons = 3;
}

message BatchReportResponse {
    int64 total_received = 1;
    int64 total_accepted = 2;
    int64 total_rejected = 3;
}

message StreamTelemetryRequest {
    string device_rid = 1;          // 可选,空则订阅所有设备
    repeated string metric_names = 2; // 可选,空则订阅所有指标
    int32 throttle_ms = 3;          // 节流间隔
}

message DeviceCommand {
    string command_id = 1;
    string device_rid = 2;
    string command_type = 3;        // REBOOT, CONFIG_UPDATE, FIRMWARE_UPDATE
    map<string, string> parameters = 4;
    int32 timeout_seconds = 5;
}

message CommandResponse {
    string command_id = 1;
    string device_rid = 2;
    CommandStatus status = 3;
    string message = 4;
    map<string, string> result = 5;
}

enum CommandStatus {
    COMMAND_STATUS_UNSPECIFIED = 0;
    COMMAND_STATUS_RECEIVED = 1;
    COMMAND_STATUS_EXECUTING = 2;
    COMMAND_STATUS_SUCCESS = 3;
    COMMAND_STATUS_FAILED = 4;
    COMMAND_STATUS_TIMEOUT = 5;
}

message TriggerAlertRequest {
    string device_rid = 1;
    string alert_type = 2;
    string severity = 3;            // INFO, WARNING, CRITICAL
    string message = 4;
    map<string, string> context = 5;
}

message TriggerAlertResponse {
    string alert_id = 1;
    google.protobuf.Timestamp triggered_at = 2;
}

message ListAlertsRequest {
    string device_rid = 1;
    string severity = 2;
    google.protobuf.Timestamp from_time = 3;
    google.protobuf.Timestamp to_time = 4;
    onto.common.v1.Pagination pagination = 5;
}

message ListAlertsResponse {
    repeated Alert alerts = 1;
    onto.common.v1.PaginationInfo pagination_info = 2;
}

message Alert {
    string alert_id = 1;
    string device_rid = 2;
    string alert_type = 3;
    string severity = 4;
    string message = 5;
    map<string, string> context = 6;
    bool acknowledged = 7;
    google.protobuf.Timestamp triggered_at = 8;
    google.protobuf.Timestamp acknowledged_at = 9;
}

#3. 生成代码

#3.1 Python 代码生成

Bash
# 安装 gRPC 工具
pip install grpcio grpcio-tools

# 生成 Python 代码
python -m grpc_tools.protoc \
    --proto_path=proto/ \
    --python_out=python-sdk/generated/ \
    --grpc_python_out=python-sdk/generated/ \
    --pyi_out=python-sdk/generated/ \
    proto/onto/device/v1/device_service.proto

# 生成的文件:
# python-sdk/generated/onto/device/v1/
# ├── device_service_pb2.py       # 消息类
# ├── device_service_pb2.pyi      # 类型存根
# └── device_service_pb2_grpc.py  # 服务存根和 Servicer

#3.2 Java 代码生成(Gradle)

GROOVY
// build.gradle.kts
plugins {
    id("com.google.protobuf") version "0.9.4"
}

dependencies {
    implementation("io.grpc:grpc-netty-shaded:1.62.2")
    implementation("io.grpc:grpc-protobuf:1.62.2")
    implementation("io.grpc:grpc-stub:1.62.2")
    compileOnly("org.apache.tomcat:annotations-api:6.0.53")
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.25.3"
    }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:1.62.2"
        }
    }
    generateProtoTasks {
        all().forEach { task ->
            task.plugins {
                id("grpc")
            }
        }
    }
}

#4. 实现服务端

#4.1 Python 实现(FastAPI + gRPC 混合部署)

Python
# intelligence-Layer/device_monitor/grpc_server.py
import grpc
from concurrent import futures
from datetime import datetime, timezone
from typing import AsyncIterator
import asyncio
import logging

from onto.device.v1 import device_service_pb2 as pb2
from onto.device.v1 import device_service_pb2_grpc as pb2_grpc
from google.protobuf.timestamp_pb2 import Timestamp

logger = logging.getLogger(__name__)


class DeviceMonitorServicer(pb2_grpc.DeviceMonitorServiceServicer):
    """设备监控 gRPC 服务实现"""

    def __init__(self, device_repo, telemetry_store, alert_manager):
        self.device_repo = device_repo
        self.telemetry_store = telemetry_store
        self.alert_manager = alert_manager
        self._telemetry_subscribers: dict[str, asyncio.Queue] = {}

    async def GetDevice(self, request, context):
        """查询单个设备"""
        device = await self.device_repo.get(request.device_rid)

        if device is None:
            context.set_code(grpc.StatusCode.NOT_FOUND)
            context.set_details(f"Device {request.device_rid} not found")
            return pb2.GetDeviceResponse()

        response = pb2.GetDeviceResponse(
            device=self._to_proto_device(device),
        )

        if request.include_latest_telemetry:
            telemetry = await self.telemetry_store.get_latest(
                request.device_rid, limit=10
            )
            for t in telemetry:
                response.latest_telemetry.append(self._to_proto_telemetry(t))

        return response

    async def ListDevices(self, request, context):
        """批量查询设备"""
        filters = {}
        if request.device_type:
            filters["device_type"] = request.device_type
        if request.status != pb2.DEVICE_STATUS_UNSPECIFIED:
            filters["status"] = request.status
        if request.location_prefix:
            filters["location_prefix"] = request.location_prefix

        page_size = request.pagination.page_size or 20
        page_token = request.pagination.page_token or ""

        devices, next_token, total = await self.device_repo.list(
            filters=filters,
            page_size=page_size,
            page_token=page_token,
        )

        return pb2.ListDevicesResponse(
            devices=[self._to_proto_device(d) for d in devices],
            pagination_info=pb2.onto.common.v1.PaginationInfo(
                next_page_token=next_token,
                total_count=total,
            ),
        )

    async def ReportTelemetry(self, request, context):
        """上报遥测数据"""
        accepted = 0
        rejected = 0
        rejected_reasons = []

        for dp in request.data_points:
            try:
                await self.telemetry_store.write(
                    device_rid=dp.device_rid,
                    metric_name=dp.metric_name,
                    value=dp.value,
                    unit=dp.unit,
                    timestamp=dp.timestamp.ToDatetime(),
                    tags=dict(dp.tags),
                )
                accepted += 1

                # 通知流式订阅者
                await self._notify_subscribers(dp)

            except ValueError as e:
                rejected += 1
                rejected_reasons.append(f"{dp.device_rid}/{dp.metric_name}: {e}")

        return pb2.ReportTelemetryResponse(
            accepted_count=accepted,
            rejected_count=rejected,
            rejected_reasons=rejected_reasons,
        )

    async def BatchReportTelemetry(self, request_iterator, context):
        """客户端流式:批量上报遥测数据"""
        total_received = 0
        total_accepted = 0
        total_rejected = 0
        batch = []
        batch_size = 100

        async for data_point in request_iterator:
            total_received += 1
            batch.append(data_point)

            if len(batch) >= batch_size:
                accepted, rejected = await self._flush_batch(batch)
                total_accepted += accepted
                total_rejected += rejected
                batch.clear()

        # 刷新最后一批
        if batch:
            accepted, rejected = await self._flush_batch(batch)
            total_accepted += accepted
            total_rejected += rejected

        return pb2.BatchReportResponse(
            total_received=total_received,
            total_accepted=total_accepted,
            total_rejected=total_rejected,
        )

    async def StreamTelemetry(self, request, context):
        """服务端流式:实时推送遥测数据"""
        subscriber_id = f"stream-{id(context)}"
        queue = asyncio.Queue(maxsize=1000)
        self._telemetry_subscribers[subscriber_id] = queue

        logger.info(f"New telemetry subscriber: {subscriber_id}")

        try:
            while not context.cancelled():
                try:
                    data_point = await asyncio.wait_for(queue.get(), timeout=30.0)

                    # 应用过滤
                    if request.device_rid and data_point.device_rid != request.device_rid:
                        continue
                    if request.metric_names and data_point.metric_name not in request.metric_names:
                        continue

                    yield data_point

                    # 节流
                    if request.throttle_ms > 0:
                        await asyncio.sleep(request.throttle_ms / 1000.0)

                except asyncio.TimeoutError:
                    # 发送心跳(空数据点)
                    yield pb2.TelemetryDataPoint()

        finally:
            del self._telemetry_subscribers[subscriber_id]
            logger.info(f"Telemetry subscriber disconnected: {subscriber_id}")

    async def DeviceCommandChannel(self, request_iterator, context):
        """双向流式:设备命令通道"""
        async for command in request_iterator:
            logger.info(f"Command received: {command.command_type} -> {command.device_rid}")

            # 确认收到
            yield pb2.CommandResponse(
                command_id=command.command_id,
                device_rid=command.device_rid,
                status=pb2.COMMAND_STATUS_RECEIVED,
                message="Command received, executing...",
            )

            # 异步执行命令
            try:
                result = await self._execute_command(command)
                yield pb2.CommandResponse(
                    command_id=command.command_id,
                    device_rid=command.device_rid,
                    status=pb2.COMMAND_STATUS_SUCCESS,
                    message="Command executed successfully",
                    result=result,
                )
            except TimeoutError:
                yield pb2.CommandResponse(
                    command_id=command.command_id,
                    device_rid=command.device_rid,
                    status=pb2.COMMAND_STATUS_TIMEOUT,
                    message=f"Command timed out after {command.timeout_seconds}s",
                )
            except Exception as e:
                yield pb2.CommandResponse(
                    command_id=command.command_id,
                    device_rid=command.device_rid,
                    status=pb2.COMMAND_STATUS_FAILED,
                    message=str(e),
                )

    async def TriggerAlert(self, request, context):
        """触发告警"""
        alert_id = await self.alert_manager.trigger(
            device_rid=request.device_rid,
            alert_type=request.alert_type,
            severity=request.severity,
            message=request.message,
            context=dict(request.context),
        )

        now = Timestamp()
        now.GetCurrentTime()

        return pb2.TriggerAlertResponse(
            alert_id=alert_id,
            triggered_at=now,
        )

    # ---- 内部方法 ----

    async def _notify_subscribers(self, data_point):
        """通知所有流式订阅者"""
        dead_subscribers = []
        for sub_id, queue in self._telemetry_subscribers.items():
            try:
                queue.put_nowait(data_point)
            except asyncio.QueueFull:
                logger.warning(f"Subscriber {sub_id} queue full, dropping data point")

    async def _flush_batch(self, batch):
        """批量写入遥测数据"""
        accepted = 0
        rejected = 0
        for dp in batch:
            try:
                await self.telemetry_store.write(
                    device_rid=dp.device_rid,
                    metric_name=dp.metric_name,
                    value=dp.value,
                    unit=dp.unit,
                    timestamp=dp.timestamp.ToDatetime(),
                    tags=dict(dp.tags),
                )
                accepted += 1
            except Exception:
                rejected += 1
        return accepted, rejected

    async def _execute_command(self, command):
        """执行设备命令"""
        # 实际实现中会通过 MQTT/CoAP 下发到设备
        await asyncio.sleep(0.1)  # 模拟
        return {"status": "ok"}

    def _to_proto_device(self, device) -> pb2.Device:
        """将领域模型转为 Protobuf 消息"""
        ts = Timestamp()
        ts.FromDatetime(device.last_seen)

        reg_ts = Timestamp()
        reg_ts.FromDatetime(device.registered_at)

        return pb2.Device(
            device_rid=device.rid,
            device_name=device.name,
            device_type=device.device_type,
            location=device.location,
            status=device.status,
            metadata=device.metadata,
            last_seen=ts,
            registered_at=reg_ts,
        )

    def _to_proto_telemetry(self, t) -> pb2.TelemetryDataPoint:
        ts = Timestamp()
        ts.FromDatetime(t.timestamp)
        return pb2.TelemetryDataPoint(
            device_rid=t.device_rid,
            metric_name=t.metric_name,
            value=t.value,
            unit=t.unit,
            timestamp=ts,
            tags=t.tags,
        )

#4.2 启动 gRPC 服务器

Python
# intelligence-Layer/device_monitor/server.py
import grpc
from grpc_reflection.v1alpha import reflection
import asyncio
import signal
import logging

from onto.device.v1 import device_service_pb2 as pb2
from onto.device.v1 import device_service_pb2_grpc as pb2_grpc
from device_monitor.grpc_server import DeviceMonitorServicer
from device_monitor.repositories import DeviceRepository
from device_monitor.stores import TelemetryStore
from device_monitor.alerts import AlertManager

logger = logging.getLogger(__name__)


async def serve():
    """启动 gRPC 服务器"""

    # 初始化依赖
    device_repo = DeviceRepository()
    telemetry_store = TelemetryStore()
    alert_manager = AlertManager()

    # 创建 gRPC 服务器
    server = grpc.aio.server(
        futures.ThreadPoolExecutor(max_workers=10),
        options=[
            ("grpc.max_send_message_length", 50 * 1024 * 1024),    # 50MB
            ("grpc.max_receive_message_length", 50 * 1024 * 1024),  # 50MB
            ("grpc.keepalive_time_ms", 30000),                      # 30s
            ("grpc.keepalive_timeout_ms", 10000),                   # 10s
            ("grpc.keepalive_permit_without_calls", True),
        ],
    )

    # 注册服务
    servicer = DeviceMonitorServicer(device_repo, telemetry_store, alert_manager)
    pb2_grpc.add_DeviceMonitorServiceServicer_to_server(servicer, server)

    # 启用 gRPC 反射(方便调试)
    service_names = (
        pb2.DESCRIPTOR.services_by_name["DeviceMonitorService"].full_name,
        reflection.SERVICE_NAME,
    )
    reflection.enable_server_reflection(service_names, server)

    # 绑定端口
    listen_addr = "[::]:50051"
    server.add_insecure_port(listen_addr)

    logger.info(f"Starting gRPC server on {listen_addr}")
    await server.start()

    # 优雅关闭
    async def shutdown(sig):
        logger.info(f"Received {sig.name}, shutting down...")
        await server.stop(grace=5)  # 5 秒优雅关闭

    loop = asyncio.get_event_loop()
    for sig in (signal.SIGTERM, signal.SIGINT):
        loop.add_signal_handler(sig, lambda s=sig: asyncio.create_task(shutdown(s)))

    await server.wait_for_termination()


if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    asyncio.run(serve())

#5. 实现客户端

#5.1 同步客户端

Python
# python-sdk/ontology_sdk/grpc_client/device_client.py
import grpc
from onto.device.v1 import device_service_pb2 as pb2
from onto.device.v1 import device_service_pb2_grpc as pb2_grpc


class DeviceMonitorClient:
    """设备监控 gRPC 客户端"""

    def __init__(self, endpoint: str = "localhost:50051"):
        self.channel = grpc.insecure_channel(endpoint)
        self.stub = pb2_grpc.DeviceMonitorServiceStub(self.channel)

    def get_device(self, device_rid: str, include_telemetry: bool = False):
        """查询单个设备"""
        request = pb2.GetDeviceRequest(
            device_rid=device_rid,
            include_latest_telemetry=include_telemetry,
        )
        try:
            response = self.stub.GetDevice(request, timeout=5.0)
            return response
        except grpc.RpcError as e:
            if e.code() == grpc.StatusCode.NOT_FOUND:
                return None
            raise

    def list_devices(self, device_type: str = "", status=None, page_size: int = 20):
        """批量查询设备"""
        request = pb2.ListDevicesRequest(
            device_type=device_type,
            pagination=pb2.onto.common.v1.Pagination(page_size=page_size),
        )
        if status is not None:
            request.status = status

        return self.stub.ListDevices(request, timeout=10.0)

    def report_telemetry(self, data_points: list[dict]):
        """上报遥测数据"""
        proto_points = []
        for dp in data_points:
            ts = Timestamp()
            ts.FromDatetime(dp["timestamp"])
            proto_points.append(pb2.TelemetryDataPoint(
                device_rid=dp["device_rid"],
                metric_name=dp["metric_name"],
                value=dp["value"],
                unit=dp.get("unit", ""),
                timestamp=ts,
                tags=dp.get("tags", {}),
            ))

        request = pb2.ReportTelemetryRequest(data_points=proto_points)
        return self.stub.ReportTelemetry(request, timeout=10.0)

    def close(self):
        self.channel.close()

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()

#5.2 异步客户端

Python
# python-sdk/ontology_sdk/grpc_client/async_device_client.py
import grpc
import asyncio
from typing import AsyncIterator

from onto.device.v1 import device_service_pb2 as pb2
from onto.device.v1 import device_service_pb2_grpc as pb2_grpc


class AsyncDeviceMonitorClient:
    """设备监控异步 gRPC 客户端"""

    def __init__(self, endpoint: str = "localhost:50051"):
        self.channel = grpc.aio.insecure_channel(endpoint)
        self.stub = pb2_grpc.DeviceMonitorServiceStub(self.channel)

    async def stream_telemetry(
        self,
        device_rid: str = "",
        metric_names: list[str] | None = None,
    ) -> AsyncIterator[pb2.TelemetryDataPoint]:
        """流式接收遥测数据"""
        request = pb2.StreamTelemetryRequest(
            device_rid=device_rid,
            metric_names=metric_names or [],
            throttle_ms=100,
        )

        async for data_point in self.stub.StreamTelemetry(request):
            if data_point.device_rid:  # 跳过心跳
                yield data_point

    async def batch_report(
        self,
        data_points: AsyncIterator[pb2.TelemetryDataPoint],
    ) -> pb2.BatchReportResponse:
        """客户端流式批量上报"""
        return await self.stub.BatchReportTelemetry(data_points)

    async def command_channel(self, commands: AsyncIterator[pb2.DeviceCommand]):
        """双向流式命令通道"""
        async for response in self.stub.DeviceCommandChannel(commands):
            yield response

    async def close(self):
        await self.channel.close()

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        await self.close()


# 使用示例
async def main():
    async with AsyncDeviceMonitorClient("localhost:50051") as client:
        # 流式接收温度传感器数据
        async for dp in client.stream_telemetry(
            device_rid="ri.device.sensor-001",
            metric_names=["temperature"],
        ):
            print(f"[{dp.timestamp}] {dp.metric_name}: {dp.value}{dp.unit}")

            # 温度异常时触发告警
            if dp.value > 80.0:
                await client.stub.TriggerAlert(pb2.TriggerAlertRequest(
                    device_rid=dp.device_rid,
                    alert_type="TEMPERATURE_HIGH",
                    severity="CRITICAL",
                    message=f"Temperature {dp.value}°C exceeds threshold 80°C",
                    context={"current_value": str(dp.value)},
                ))

asyncio.run(main())

#6. 拦截器(Interceptors)

#6.1 认证拦截器

Python
class AuthInterceptor(grpc.aio.ServerInterceptor):
    """服务端认证拦截器"""

    def __init__(self, token_validator):
        self.token_validator = token_validator
        self._public_methods = {"/grpc.reflection.v1alpha.ServerReflection/"}

    async def intercept_service(self, continuation, handler_call_details):
        method = handler_call_details.method

        # 公开方法不需要认证
        for public in self._public_methods:
            if method.startswith(public):
                return await continuation(handler_call_details)

        # 从 metadata 提取 token
        metadata = dict(handler_call_details.invocation_metadata or [])
        token = metadata.get("authorization", "")

        if not token.startswith("Bearer "):
            return self._unauthenticated("Missing or invalid Authorization header")

        token = token[7:]  # 去掉 "Bearer " 前缀

        try:
            user_context = await self.token_validator.validate(token)
            # 将用户信息注入 context(通过 metadata 传递)
            handler_call_details.invocation_metadata.append(
                ("x-user-id", user_context.user_id)
            )
        except InvalidTokenError as e:
            return self._unauthenticated(str(e))

        return await continuation(handler_call_details)

    def _unauthenticated(self, details: str):
        async def abort(ignored_request, context):
            await context.abort(grpc.StatusCode.UNAUTHENTICATED, details)
        return grpc.unary_unary_rpc_method_handler(abort)

#6.2 日志与指标拦截器

Python
import time
from prometheus_client import Counter, Histogram

grpc_requests_total = Counter(
    "grpc_requests_total",
    "Total gRPC requests",
    ["method", "status"],
)

grpc_request_duration = Histogram(
    "grpc_request_duration_seconds",
    "gRPC request duration",
    ["method"],
)


class MetricsInterceptor(grpc.aio.ServerInterceptor):
    """指标收集拦截器"""

    async def intercept_service(self, continuation, handler_call_details):
        method = handler_call_details.method
        start = time.perf_counter()

        try:
            response = await continuation(handler_call_details)
            grpc_requests_total.labels(method=method, status="OK").inc()
            return response
        except grpc.aio.AioRpcError as e:
            grpc_requests_total.labels(method=method, status=e.code().name).inc()
            raise
        finally:
            duration = time.perf_counter() - start
            grpc_request_duration.labels(method=method).observe(duration)

#7. 测试

#7.1 单元测试

Python
import pytest
import grpc
from unittest.mock import AsyncMock, MagicMock

from device_monitor.grpc_server import DeviceMonitorServicer
from onto.device.v1 import device_service_pb2 as pb2


@pytest.fixture
def servicer():
    return DeviceMonitorServicer(
        device_repo=AsyncMock(),
        telemetry_store=AsyncMock(),
        alert_manager=AsyncMock(),
    )


@pytest.fixture
def context():
    ctx = MagicMock()
    ctx.cancelled.return_value = False
    return ctx


@pytest.mark.asyncio
async def test_get_device_found(servicer, context):
    """测试设备查询——存在的设备"""
    servicer.device_repo.get.return_value = MagicMock(
        rid="ri.device.001",
        name="Temperature Sensor",
        device_type="SENSOR",
        location="Building A / Floor 3",
        status=pb2.DEVICE_STATUS_ONLINE,
        metadata={"firmware": "2.1.0"},
        last_seen=datetime.now(timezone.utc),
        registered_at=datetime.now(timezone.utc),
    )

    request = pb2.GetDeviceRequest(
        device_rid="ri.device.001",
        include_latest_telemetry=False,
    )

    response = await servicer.GetDevice(request, context)
    assert response.device.device_rid == "ri.device.001"
    assert response.device.device_name == "Temperature Sensor"


@pytest.mark.asyncio
async def test_get_device_not_found(servicer, context):
    """测试设备查询——不存在的设备"""
    servicer.device_repo.get.return_value = None

    request = pb2.GetDeviceRequest(device_rid="ri.device.999")
    response = await servicer.GetDevice(request, context)

    context.set_code.assert_called_once_with(grpc.StatusCode.NOT_FOUND)


@pytest.mark.asyncio
async def test_report_telemetry(servicer, context):
    """测试遥测数据上报"""
    ts = Timestamp()
    ts.GetCurrentTime()

    request = pb2.ReportTelemetryRequest(
        data_points=[
            pb2.TelemetryDataPoint(
                device_rid="ri.device.001",
                metric_name="temperature",
                value=25.5,
                unit="°C",
                timestamp=ts,
            ),
        ]
    )

    response = await servicer.ReportTelemetry(request, context)
    assert response.accepted_count == 1
    assert response.rejected_count == 0

#7.2 集成测试

Python
import pytest
import grpc
import asyncio

from onto.device.v1 import device_service_pb2 as pb2
from onto.device.v1 import device_service_pb2_grpc as pb2_grpc


@pytest.fixture(scope="module")
async def grpc_channel():
    """创建测试用 gRPC channel"""
    channel = grpc.aio.insecure_channel("localhost:50051")
    yield channel
    await channel.close()


@pytest.fixture
def stub(grpc_channel):
    return pb2_grpc.DeviceMonitorServiceStub(grpc_channel)


@pytest.mark.integration
@pytest.mark.asyncio
async def test_full_workflow(stub):
    """端到端测试:上报数据 -> 查询设备 -> 触发告警"""

    # 1. 上报遥测数据
    ts = Timestamp()
    ts.GetCurrentTime()

    report_response = await stub.ReportTelemetry(
        pb2.ReportTelemetryRequest(
            data_points=[
                pb2.TelemetryDataPoint(
                    device_rid="ri.device.test-001",
                    metric_name="temperature",
                    value=85.0,
                    unit="°C",
                    timestamp=ts,
                ),
            ]
        )
    )
    assert report_response.accepted_count == 1

    # 2. 查询设备
    device_response = await stub.GetDevice(
        pb2.GetDeviceRequest(
            device_rid="ri.device.test-001",
            include_latest_telemetry=True,
        )
    )
    assert device_response.device.device_rid == "ri.device.test-001"

    # 3. 触发告警
    alert_response = await stub.TriggerAlert(
        pb2.TriggerAlertRequest(
            device_rid="ri.device.test-001",
            alert_type="TEMPERATURE_HIGH",
            severity="CRITICAL",
            message="Temperature exceeds threshold",
        )
    )
    assert alert_response.alert_id

#8. 与 coomia-dip 平台集成

#8.1 注册为平台服务

将自定义 gRPC 服务注册到 coomia-dip 的服务注册中心:

Python
from ontology_sdk import OntoPlatform

platform = OntoPlatform(base_url="http://localhost:8080", token="admin-token")

# 注册自定义服务
platform.services.register(
    name="device-monitor",
    endpoint="device-monitor:50051",
    proto_descriptor="onto.device.v1.DeviceMonitorService",
    health_check_path="/grpc.health.v1.Health/Check",
    metadata={
        "version": "1.0.0",
        "owner": "iot-team",
        "description": "IoT Device Monitoring Service",
    },
)

#8.2 在 Function 中调用

Python
from ontology_sdk.functions import ontology_function
from ontology_sdk.grpc_client import DeviceMonitorClient

@ontology_function(
    name="check_device_health",
    description="检查设备健康状态",
    parameters={"device_rid": "string"},
    returns="DeviceHealthReport",
)
async def check_device_health(device_rid: str) -> dict:
    async with AsyncDeviceMonitorClient("device-monitor:50051") as client:
        device = await client.stub.GetDevice(
            pb2.GetDeviceRequest(
                device_rid=device_rid,
                include_latest_telemetry=True,
            )
        )

        # 分析遥测数据,生成健康报告
        telemetry = device.latest_telemetry
        anomalies = []
        for dp in telemetry:
            if dp.metric_name == "temperature" and dp.value > 80:
                anomalies.append(f"High temperature: {dp.value}°C")
            if dp.metric_name == "cpu_usage" and dp.value > 95:
                anomalies.append(f"High CPU: {dp.value}%")

        return {
            "device_rid": device_rid,
            "status": "HEALTHY" if not anomalies else "UNHEALTHY",
            "anomalies": anomalies,
            "last_seen": device.device.last_seen.ToDatetime().isoformat(),
        }

#总结

本教程覆盖了在 coomia-dip 中开发自定义 gRPC 服务的完整流程:

  • 接口定义:使用 Protocol Buffers 定义强类型的服务契约
  • 代码生成:通过 protoc 为 Python 和 Java 生成客户端/服务端代码
  • 服务实现:实现单次调用、客户端流式、服务端流式、双向流式四种模式
  • 拦截器:认证、日志、指标收集的横切关注点
  • 测试策略:单元测试 Mock 依赖,集成测试验证端到端
  • 平台集成:注册为平台服务,在 Function 中调用

gRPC 是 coomia-dip 生态系统的"通用语言"。掌握它,你就掌握了扩展平台能力的钥匙。

下一篇:[S12-15] Flink CDC 实时数据同步指南 上一篇:[S12-13] 事件订阅与通知