一、获取密钥生成签名
·请求路径:/proxy/gateway/application/GenerateToken
·请求方法:POST
·请求参数:
·nonce:随机字符串,不可重复,建议使用 uuid。
·timestamp:当前时间戳(毫秒),前后五分钟有效。
注意:tenantId:应用被下发到其他租户后,会生成新的 appId(应用 ID) 和 appSecret,所以请求时需要使用新生成的 appId(应用 ID)和 appSecret,或者使用原始 appId(应用 ID)+ 原始 appSecret + 对应租户的 tenantId,来为不同的租户生成 token。
二、应用签名生成规则
拼接字符串:
appId=10001&appSecret=xxxxxxxxxxxxxxxxxxxxxxxx&nonce=VlghmWSvnod7MvcC×tamp=1640783576118
对该字符串进行 md5 加密生成32位签名(小写)
sign = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
JavaGoimport org.apache.commons.lang3.RandomStringUtils;import java.nio.charset.StandardCharsets;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.util.Objects;public class SignUtil { /** * 生成签名 * * @param appId 应用ID * @param appSecret 应用密钥 * @return 生成的签名 */ public static String generateSign(Long appId, String appSecret) throws NoSuchAlgorithmException { Objects.requireNonNull(appId, "应用ID不能为空"); Objects.requireNonNull(appSecret, "应用密钥不能为空"); long timestamp = System.currentTimeMillis(); String nonce = RandomStringUtils.randomAlphanumeric(32); String strToMD5 = "appId=" + appId + "&appSecret=" + appSecret + "&nonce=" + nonce + "×tamp=" + timestamp; return calculateMD5(strToMD5); } /** * 计算字符串的MD5哈希值 * * @param plainText 待计算的字符串 * @return 字符串的MD5哈希值 */ public static String calculateMD5(String plainText) throws NoSuchAlgorithmException { Objects.requireNonNull(plainText, "输入字符串不能为空"); MessageDigest md = MessageDigest.getInstance("MD5"); byte[] hashBytes = md.digest(plainText.getBytes(StandardCharsets.UTF_8)); StringBuilder md5code = new StringBuilder(); for (byte b : hashBytes) { String hex = Integer.toHexString(0xFF & b); if (hex.length() == 1) { md5code.append("0"); } md5code.append(hex); } return md5code.toString(); }}message genTokenReq { string appId = 1; int64 timestamp = 2; string nonce = 3; string sign = 4; int32 tenantId = 5;} secret := "xxxx" req := &apigateway.GenTokenReq{ AppId: "xxx", Nonce: uuid.New().String(), TenantId: xxxx, Timestamp: time.Now().UnixMilli(), } req.Sign = GenSign(secret, req.AppId, req.Nonce, req.Timestamp)func GenSign(secret, appid, nonce string, timestamp int64) string { // 创建一个map params := map[string]string{ "appId": appid, "appSecret": secret, "nonce": nonce, "timestamp": strconv.FormatInt(timestamp, 10), } // 对map的键进行字典序排序 keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) // 拼接map的键值对 var str string for i, k := range keys { str += k + "=" + params[k] if i != len(keys)-1 { str += "&" } } // 使用md5算法对字符串进行签名 hash := md5.Sum([]byte(str)) signature := hex.EncodeToString(hash[:]) return signature}
三、HTTP/HTTPS
OPEN API Doc 声明定义包含接口名称、接口描述、接口定义、输入参数、请求示例、返回参数、错误码。
请求路径:/proxy/*
请求方法:*
请求头携带参数:
| 字段 | 数据类型 | 是否必须 | 释义 | 示例 |
| Dtgw-Token | string(16) | 是 | 登录后由应用中心返回,之后访问需要携带至请求头中。用于验证身份,并确认该请求所属应用 | D6heVfeQT7RnINhM |
四、WebSocket
下面以孪生底座公共租户下的设备通知 API 和巡检应用为例,讲述应用在某个 WS API 下建立 WebSocket 链接的过程。
设备通知 API,API ID 为:bff0bda8-95ed-4034-b36b-0c533bf4a077。
巡检应用,应用 appid 是:10097。
应用请求和 WS API 建立 WS 连接和请求普通 API 是一样的流程,第0步,均需要获取应用请求 API 的 token,详情可参见 获取密钥生成签名。
1. 应用请求 WS API 建立 WS 连接:
示例如下:
wscat -c 'ws://ws.dtwin.tencent.com/proxy/bff0bda8-95ed-4034-b36b-0c533bf4a077/weiling-pubsub/test?Dtgw-Token=gi9UyVNnSK3tpbTekBBryqnbmJ3Ta5PT'
或:
wscat -c 'ws://ws.dtwin.tencent.com/proxy/bff0bda8-95ed-4034-b36b-0c533bf4a077/weiling-pubsub/test' -H "Dtgw-Token: gi9UyVNnSK3tpbTekBBryqnbmJ3Ta5PT"
其中:gi9UyVNnSK3tpbTekBBryqnbmJ3Ta5PT,是第0步获取的应用请求 API 的 token。token 可以放在请求头,也可以放在 url 的 query string 中。
注意:说明:
注意:(ws://ws.dtwin.tencent.com/proxy/bff0bda8-95ed-4034-b36b-0c533bf4a077/weiling-pubsub/test) WS API 要和资源中心看到的一样不能篡改。
2. WS 连接建立后,要进行具体的订阅操作:
WS 通道,消息传递格式是统一的 json 格式,格式定义如下:
注意:参数说明:
注意:1. ver:版本号,当前版本统一为1。
注意:2. operation:操作,是一个枚举值,每个枚举值表达是一种类型的命令,不同的命令 body 是不同的 json 结构。
注意:3. seq:请求响应序列,递增即可,请求和响应一一对应。
注意:4. topic:只有在 operation 为9的时候有意义,代表 client 收到的消息是来自哪个 topic。有这个字段的原因是,订阅 topic 支持2级,通配订阅,详情请参见 订阅示例说明。
注意:5. body:不同 operation 命令下是不同的 json 结构,可参见如下示例。
注意:6. 具体推送的消息唯一 id,通常是 traceid,下行消息才有。
具体 operation 命令枚举定义如下,需要用到的是1 ~9和100:
其中 topic 支持两级目录结构的形式: /first/second,并支持*表示通配某一级,例如:
·订阅 topic: /device/*,那么/device/add,/device/del,/device/xx 等 topic 上的消息都能收到,并且在推送的消息结构里面会有具体是哪个 topic 的消息字段。
·订阅 topic:/*/*,则关于某个 ws api 的所有 topic 消息都能收到。
以下是各个操作的示例:
·订阅
请求:
{"ver":1,"operation":1,"seq_id":10001,"body":"{\\"sub_topic\\":[\\"/citybase/*\\",\\"/cim/add\\"]}"}
响应:
{"ver":1,"operation":2,"seq_id":10001,"body":"{\\"message\\":\\"ok\\"}"}
·更新 topic
请求:
{"ver":1,"operation":3,"seq_id":10002,"body":"{\\"sub_topic\\":[\\"/cim/del\\",\\"/citybase/get\\"]}"}
响应:
{"ver":1,"operation":4,"seq_id":10001,"body":"{\\"message\\":\\"ok\\"}"}
·取消订阅主题
请求:
{"ver":1,"operation":5,"seq_id":10003,"body":"{\\"sub_topic\\":[\\"/citybase/get\\"]}"}
响应:
{"ver":1,"operation":6,"seq_id":10003,"body":"{\\"message\\":\\"ok\\"}"}
·心跳,维持 WS 连接,需要每隔45s发送一个心跳包,否则连接空闲,会关闭 WS 连接
请求:
{"ver":1,"operation":7,"seq_id":100011,"body":""}
响应:
{"ver":1,"operation":8,"seq_id":10001,"body":"{\\"gap\\":45}"}
·接收业务推送的消息
{"ver":1,"operation":9,"topic":"/cim/add","body":"{\\"uid\\": 102002, \\"name\\": \\"dtwin test 4444\\"}"}
·错误回复,如果发送错误,会发送一个错误的 WS 回复,错误回复的 operation 是100
{"ver":1,"operation":100,"seq_id":10001,"body":"{\\"code\\":110011,\\"message\\":\\"duplicate Subscribe operation\\"}"}
3. 示例:
以下是用 wscat 客户端,进行的一个完整流程的示例,白色是发送的请求,蓝色是收到的消息/响应。
注意:说明:
注意:WS 连接后的,第一个请求包一定要是订阅请求,operation 是1。
注意:具体要订阅的 topic,应用根据具体的 WS API 的相关说明文档来进行订阅即可。
五、平台回调
考虑到适配的孪生应用支持多租户和多项目空间的产品形态,每个租户下的应用 ID 和 apiID 相对独立,孪生应用为了适配一次,达到万千租户开箱即用,注册应用时填写“消息通知服务回调地址”,并为应用授权“应用中心通知 API”这个消息通知服务接口,开通租户时,孪生底座通过调用应用所提供“消息通知服务回调地址”服务,通知孪生应用新租户下的应用 ID,达到不同租户下应用 ID 和 apiID 的应用权限系统相互隔离。
1. 孪生应用适配孪生底座的业务逻辑图:
2. 填写孪生应用“消息通知服务回调地址”。
3. 授权孪生应用消息通知服务接口“应用中心通知 API”
4. 应用创建回调消息:
business_type 为10001表示是来自应用中心的消息。
eventType 为1表示是应用创建的消息,不同 eventType 对应的 data 协议不同。
{ "business_type": 10001, "data": "{\\"eventType\\":1,\\"data\\":\\"{\\\\\\"appId \\\\\\":26826,\\\\\\"appSecret\\\\\\":\\\\\\"***\\\\\\",\\\\\\"name\\\\\\":\\\\\\"智能资产管理\\\\\\",\\\\\\"originAppId\\\\\\":10041,\\\\\\"tenantId\\\\\\":100215}\\"}", "event_id": "GBKHb4bbCFSSBlpicE4a37nban4bJeQt"}
上图代码示例协议内容说明:
| 字段名 | 数据类型 | 说明 |
| tenantId | integer | 租户 id |
| appId | integer | 新生成的 appId |
| appSecret | string | 新生成的 appSecret |
| name | string | 应用名称 |
| originAppId | integer | 原始应用 id |
5. 应用下发到项目空间回调消息:
business_type 为10001表示是来自应用中心的消息。
eventType 为2表示是应用下发到项目空间的消息,不同 eventType 对应的 data 协议不同。
{ "business_type": 10001, "data": "{\\"eventType\\":2,\\"data\\":\\"{\\\\\\"tenantName\\\\\\":\\\\\\"\\\\\\",\\\\\\"username\\\\\\":\\\\\\"dd\\\\\\",\\\\\\"tenantStatus\\\\\\":0,\\\\\\"tenantId\\\\\\":100101,\\\\\\"email\\\\\\":\\\\\\"22@2.cc\\\\\\",\\\\\\"workspaceId\\\\\\":1214,\\\\\\"workspaceName\\\\\\":\\\\\\"空间11\\\\\\",\\\\\\"workspaceDescription\\\\\\":\\\\\\"1\\\\\\",\\\\\\"workspaceStatus\\\\\\":0,\\\\\\"appId\\\\\\":20861,\\\\\\"appSecret\\\\\\":\\\\\\"43MGE0GN3Coc85KSotCn7BlkVHFs7Gf5\\\\\\",\\\\\\"name\\\\\\":\\\\\\"能源管理\\\\\\"}\\"}", "event_id": "GBKHb4bbCFSSBlpicE4a37nban4bJeQt"}
上图代码示例协议内容说明:
| 字段名 | 数据类型 | 说明 |
| tenantName | string | 租户名称 |
| username | string | 租户负责人名称 |
| string | 租户负责人 email | |
| tenantStatus | integer | 租户状态:1:禁用0:启用-1:删除 |
| tenantId | integer | 租户 id |
| workspaceId | integer | 空间 id |
| workspaceName | string | 空间名称 |
| workspaceDescription | string | 空间描述 |
| workspaceStatus | integer | 工作空间状态:0:启用1:停用-1:已删除 |
| appId | integer | 新生成的 appId |
| appSecret | string | 新生成的 appSecret |
| name | string | 应用名称 |
| originAppId | integer | 原始应用 id |