天眼系统接入API

文档版本:v2.2.2 2019-08-02

By: Fly

1. 接入HOST

目前有4种接口方式,可以依照具体的场景使用对应的方式。

2. TCP连接方式

每日超过20万的数据建议使用TCP协议发送数据。

2.1. 传输数据结构

2.1.1. Send的数据结构体

游戏ID 正文长度 Length 正文(见下文)Content
byte(2) uint32
byte(4)
Big-Endian
byte(N)
天眼后台生成的2 bytesID 表示后面正文的长度
1 ~ FFFFFFFF
Protobuf结构的数据,见下文.proto文件

2.1.2. .proto文件

Protobuf3为例(兼容Protobuf2

// ChatUserV3 是下面类的子类
message ChatUserV3 {
    string playerId = 1;  // 游戏角色ID 必要
    string userId = 2;  // 用户ID, 必要
    string nickname = 3; // 角色昵称 必要
    int32 level = 4; // 角色等级,必要
    int32 vipLevel = 5; // 角色VIP等级,为了减少误封,需要此字段
    int64 power = 6; // 角色战力值 非必要
    string zoneId = 7; // 区服ID 这里是指游戏里面选择的区服,必要
    string zoneName = 8; //区服名字 必要
    string extra = 10; // JSON字符串,其它信息iag
    string serverId = 11; // 发行渠道ID(如包含英文字母,必须全小写),比如代表的意义:发行商1、发行商2、小米专服,一定要看下文解释,一定。
}

//主类,您将此类序列化二进制就是上文提到的「正文Content」
message ChatV3 {
    string id = 10; // ID 每条消息的ID,每条消息都不一样。必要
    string channel = 1;  //消息频道,比如私聊、世界所对应的ID 必要
    ChatUserV3 from = 2; // 发言人
    ChatUserV3 to = 3; // 发送给谁,私聊时必要,其它为null
    string content = 4;  //内容 必要
    string ip = 5; // 当前发言人的IP 必要
    string extra = 7; // JSON字符串,其它信息
    int32 status = 8; // 禁言状态由我方回传
}

2.2 实现方式

先实现聊天主类,PHP伪代码

$user = new ChatUserV3();
$user->setPlayerId(...);
$user->setExtra("{\"hasExtra\": true}");
...

$chat = new ChatV3();
$chat->setId("uuid");
$chat->setContent("聊天内容");
$chat->setFrom($user);
...

然后组合二进制流发送(基于Swoole 4)

TcpPool.php 文件在: https://github.com/fly-studio/laravel/blob/5.8/vendor/addons/server/src/Client/TcpPool.php

include "TcpPool.php";

class SendTo {
    private $pool;

    public function __construct()
    {
        $this->pool = new \Addons\Server\Client\TcpPool(ip, port, [
            'open_length_check' => true,
            'package_length_type' => 'N',
            'package_length_offset' => 2,
            'package_body_offset' => 6,
        ]); // 默认开10个连接池
    }

    public function connect()
    {
        $pool->connect(); // 启用连接池,内部实现了意外断开会自动会重连
    }

    // 简单的用法,一次发一条
    public function send(ChatV3 $chat)
    {
        $bytes = $chat->serializeToString(); // 上文的$chat
        $pool->call("\x4a\xd1".pack('N', strlen($bytes)).$bytes); //同步发送
    }

    /**
     * 一次发送多条
     *
     * @param array   $list         $chat的数组
     * @param boolean $waitFor      是否等待这些异步任务执行结束,阻塞当前协程
     */
    public function sendList(array $list, boolean $waitFor = true)
    {
        foreach($list as $chat)
        {
            // 异步发送,效率高
            $bytes = $chat->serializeToString(); // 上文的$chat
            $pool->callAsync("\x4a\xd1".pack('N', strlen($bytes)).$bytes);
        }

        if ($waitFor) $pool->waitFor();
    }
}

//上面的类执行运行在go中

$chatQueue = new \SplQueue();

go(function() use ($chatQueue){
    $sendTo = new SendTo();
    $sendTo->connect();

    while($chatQueue->isEmpty())
        \Co::sleep(1); // sleep 1s when queue is empty

    $chat = $chatQueue->pop();
    $sendTo->send($chat);
});

// 在聊天监听中
$chatQueue->push(A chat instance);

注意: 您只需要将ChatV3数据转换二进制流即可,ChatUserV3ChatV3的子类

2.3. 测试结构完整性

可以使用 http://admin.gamegs.cn/tests/proto 来测试您待发送数据的完整性

2.4. recv的数据结构

天眼会将您发送的protobuf数据完全返回,不过会更新ChatV3中的status字段用于前置禁言判断。

recv和发送结构一样,即

| ID 2bytes | LENGTH 4bytes | protobuf 正文 |

status的解释

字段 类型 含义
status int 状态
0:正常
1:当前聊天违规
2:当前聊天疑似违规

注:游戏需要根据返回结果,将status为1、2的聊天进行隐言处理,对其它玩家不可见。

提醒:游戏需做好异常和超时处理逻辑,已免影响正常游戏体验。

3. HTTP连接方式

HTTP协议接入方式会简单很多,但是建议用于20万条/日以下的场景

众所周知,HTTP/1.1的传输效率较低,会话周期短,建立多个http通道时,就需要多次tcp握手;并且一个会话中,会重复的发送、接受多余的头部信息;然后因为本接口设置的是POSTJSONJSON中键名不能省略),浪费了大量流量在传输上。

其次,鉴于HTTP/1.1的原理,客户端在没有得到HTTP回复前,会一直阻塞,这会影响发送效率。

建立多条HTTP虽然能够回避阻塞的问题,但是因为服务性能是有限的,并发太多也会排队。

TCPWebsocket的长连接链路方式,只需一次握手,其次使用Protobuf压缩之后,传输的字节数大大的减少,并且TCP无阻塞,可以持续的发送数据,所以在传输效率上要优于HTTP的方式。

如果在技术上没有实现难度,建议使用TCP的方式。

如果接口 2s 没有返回结果,请及时放行聊天,以免因为阻塞或等原因导致用户聊天故障

3.1. 发送API

POST http://api.gamegs.cn:8743/api/v1/chat/receive

字段 类型 解释
id string 我们给您的ID的16进制字符串,比如 4ad1,无需0x
这点上和TCP的不一致,请务必注意
data[] string JSON的文本 见下文结构
data[] 第二条, 没有可忽略 见下文结构
data[] 第N条, 没有可忽略 见下文结构

data[]建议每次100条以内,以免HTTP阻塞,

3.1.1. data的JSON结构

注意:

如果没有以下的某字段信息,可以设置为null,或者没有该字段,注意不是字符串的 "null"

字段 类型 解释
id string 消息的ID,必要,每条消息不一样
channel string 消息频道,必要
from User的JSON结构 发言人,必要
to User的JSON结构,或null 发送给谁,私聊时必要
content string 发言内容,必要
ip string 发言人的IP,必要
extra 任意JSON,或null 额外信息

3.1.2. User的JSON结构

字段 类型 解释
playerId string 游戏角色ID 必要
userId string 用户ID 必要
nickname string 用户昵称 必要
level int32 角色等级 必要
vipLevel int32 角色VIP等级 必要,为了减少误封,需要此字段
power int64 角色战力值,非必要
zoneId string 区服ID,指游戏里面的区服 必要
zoneName string 区服名字,非必要
serverId 服务器标识ID 发行渠道ID(如包含英文字母,必须全小写)
比如代表的意义:发行商1、发行商2、小米专服详细解释
extra 任意JSON,或null 额外信息

HTTP接口专门对extrauser.extra做了处理,和TCPProtobuf结构有区别,不关心ProtoBuf可以忽略本条

如下:

var extra1 = {
    "hasExtra": true
};

// 正确
var obj = {
    "id": "...",
    "content": "....",
    "extra": extra1; // 有效
    "from": {
        ...
        "extra": { // 有效
            "foo": [
                "any",
                "thing"
            ]
        },
    }
    ...

}

// 错误
var obj = {
    "content": "...."
    "extra": JSON.stringify(extra1); // 错误
    "from": {
        ...
        "extra" : "{\"hasExtra\":true}" // 错误
    }
    ...
}

3.1.3. POST JSON 的方式

如果您习惯将JSON作为Body发送过去,可以使用类似下面的结构

  1. data 使用正常的json结构, 无需转换为字符串
  2. 一定要设置头:Content-Type: application/json
  3. 无论是一次传输一条或多条data[]都必须是数组
POST http://api.gamegs.cn:8743/api/v1/chat/receive
{
    "id": "4ad1",
    "data": [
        {
            "id": "当前聊天的ID",
            "from": {
                "playerId": "123456789",
                "userId": "987654321",
                "nickname": "张三",
                "level": 200,
                "vipLevel": 8,
                "zoneId": "S10001",
                "serverId": null,
                "extra": [ // 可以为任何有效的JSON,而不是JSON字符串
                    "abc",
                    2,
                    3,
                    {
                        "extra": true
                    }
                ]
            },
            "to": null, // null或者和上面的from类似
            "channel": "123",
            "content": "聊天的内容",
            "ip": "127.0.0.1",
            "extra": null // 或任何有效的JSON
        },
        {
            "content": "3",
            "from": {
                "nickname": "4"
                ...
            }
        },
    ]
}
cUrl 例子
curl -X POST -H "application/json; charset=utf-8" --data "{...上面JSON文本...}" http://api.gamegs.cn:8743/api/v1/chat/receive
jQuery 例子
var json = {
    "id": "4ad1",
    "data": [
        {
            "id": "当前聊天ID",
            "content": "内容1",
            "from": {
                "nickname": "2"
                ...
            }
        },
        {
            "id": "当前聊天ID",
            "content": "内容3",
            "from": {
                "nickname": "4"
                ...
            }
        },
    ]
};
$.ajax('http://api.gamegs.cn:8743/api/v1/chat/receive',
    {
        'data': JSON.stringify(json),
        'type': 'POST',
        'processData': false,
        'contentType': 'application/json'
    }
);
Java 例子
JSONObject json = new JSONObject();
json.put("id", "");
json.put("data": Arrays.asList(数据1, 数据2));

OkHttpClient client = new OkHttpClient();
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json.toString());
Request request = new Request.Builder()
  .url("http://api.gamegs.cn:8743/api/v1/chat/receive")
  .post(body)
  .build();
Response response = client.newCall(request).execute();
String result = response.body().string();

// 判断result的结果

3.1.4. POST 表单的方式(可选)

根据实现环境,自行选择POST JSONPOST 表单的方式

表单结构

<form action="http://api.gamegs.cn:8743/api/v1/chat/receive" method="POST">
<input name="id" value="4ad1"/>
<input name="data[]" value="{....JSON 1....}"/>
<input name="data[]" value="{....JSON 2....}"/>
<input name="data[]" value="{....JSON 3....}"/>
<input name="data[]" value="{....JSON 4....}"/>
<input name="data[]" value="{....JSON 5....}"/>
</form>

换算成form-urlencoded结果类似于:id=4ad1&data[]=...&data[]=...&data[]=...

如果开发语言的HTTP组件受限,不允许存在相同的KEY名,也可以用&data[0]=...&data[1]=...&data[2]=...

jQuerya ajax的例子

可以看到data[]字段的值是字符串,也就是JSON.stringify之后的内容

var json1 = {
    "id": "当前聊天ID",
    "content": "1",
    "from": {
        "nickname": "2"
        ...
    }
};

var json2 = {...};

$.ajax({
    "url": "http://api.gamegs.cn:8743/api/v1/chat/receive",
    "method": "POST",
    "data": {
        "id": "4ad1",
        "data": [
            JSON.stringify(json1),
            JSON.stringify(json2)
        ]
    },
    "success": function(response) {
        if (response && (response.result == 'success' || response.result == 'api'))
        {
            alert('发送成功');
        }
    }
});
PHPGuzzleHttp的例子
$data = [
    [
        'id': '当前聊天id0',
        'content': '...',
        'from': [
            'nickname': '...',
            ...
        ],
    ],
    [
        'id': '当前聊天id1',
        'content': '...',
        'from': [
            'nickname': '...',
            ...
        ],
    ],
];

$client = new \GuzzleHttp\Client();
//在服务端使用异步请求Promise的方式,能有效的预防阻塞
$promise = $client->requestAsync('POST', 'http://api.gamegs.cn:8743/api/v1/chat/receive', [
    'multipart' => [
        [
            'name'     => 'id',
            'contents' => '4ad1'
        ],
        [
            'name'     => 'data[]',
            'contents' => json_encode($data[0]),
        ],
        [
            'name'     => 'data[]',
            'contents' => json_encode($data[1]),
        ]
    ]
]).then(function($response) {

    $json = json_decode($response->getBody()->getContents(), true);

    if ($json['result'] == 'success' || $json['result'] == 'api')
    {
        echo 'ok';
    }

}, function($exception){
    echo 'http fail'.
});
Java例子
OkHttpClient client = new OkHttpClient();
FormBody formBody = new FormBody.Builder()
        .add("id", "4ad1")
        .add("data[]", "{...JSON 1...}")
        .add("data[]", "{...JSON 2...}")
        .build();

final Request request = new Request.Builder()
    .url("http://api.gamegs.cn:8743/api/v1/chat/receive")
    .post(formBody)
    .build();

client.newCall(request)
   .enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
        //错误
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        final String content = response.body().string();
        // json decode
        Map<String, Object> json = decode(content);
        if (json.result == "api" || json.result == "success")
        {
            // 正确
        }
    }
});

3.2. Response

Http接口返回如下JSON格式的字符串

{
    "result": "success", // 值为success或api为正确的返回,其它一律为错误
    "message": {  // 返回api时,没有此字段
        "title": "标题,可能没有",
        "content": "正确或错误的信息"
    },
    "data": [ //当前聊天返回的结果,字段解释见下文
        {
            "id": "您传入消息0的ID",
            "status": 1,
        },
        {
            "id": "您传入消息1的ID",
            "status": 2,
        }
    ],

    //其它字段您无需在意
}

data返回的结果的顺序与发送data顺序绝对会保持一致。

3.2.1. data结果

字段 类型 含义
id string 您传入的该条消息的ID
status int 状态
0:正常
1:当前聊天违规
2:当前聊天疑似违规

注:游戏需要根据返回结果,将status为1、2的聊天进行隐言处理,对其它玩家不可见。

提醒:游戏需做好异常和超时处理逻辑,已免影响正常游戏体验。

4. serverId zoneId 的区别

zoneId 是游戏里面区服的意思, 比如是:梦幻西游的华东1、华南1、华南2,或者是传奇1区、传奇7区,这是必填字段,

serverId是表示发行渠道ID,比如代表的意义:发行商1、发行商2、小米专服

5. 修改用户资料

6. 注意

7. 您需要提供的

7.1. 禁言接口URL

您需要提供一个禁言的API接口,以便天眼系统在识别到违规信息之后,调取该API封禁用户,比如A用户发送拉人广告,系统在判断后,将会调取该API,并发送回用户的相关信息,以及禁言时长(分钟),您的API会对该用户做出处罚。

接口以HTTP为宜。

禁言时长覆盖的问题:针对大VIP大等级用户,系统的引入了阶梯禁言机制防止误封,比如1、5、15、60、9999分钟。但是随着用户的恶意度增加,可能会在上一次禁言条件未失效的情况下,发送新的禁言时间,此时,您应该是覆盖之前的禁言时长,而不是累加或拒绝。

7.1.1. 我们会以您的API发送如下字段:

POST application/x-www-form-urlencodedPOST JSON 方式发送如下数据 请确定使用哪种方式

nickname content 不参与签名

php为例,字符一定是utf-8编码

function verify()
{
    $playerId = $_POST['playerId'];
    ...
    $sign = $_POST['sign'];

    $secretKey = "abcdefg"; // 我们给您的SecretKey
    return strcasecmp($sign, md5($secretKey.$playerId.$userId.$zoneId.$serverId.$time)) == 0;
}

java为例

boolean verify(Request request)
{
    String playerId = request.getParameter('playerId');
    ...
    String sign = request.getParameter('sign');

    try {
        String secretKey = "abcdefg";
        byte[] bytes = (secretKey + playerId + userId + zoneId + serverId + time).getBytes("UTF-8"); // 输出其utf-8的bytes

        java.security.MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
        digest.update(bytes)

        byte[] md5sum = digest.digest();
        BigInteger bigInt = new BigInteger(1, md5sum);

        return String.format("%032X", bigInt).equalsIgnoreCase(sign); // MD5前面会补零

    } catch (NoSuchAlgorithmException | UnsupportedEncodingException e)
    {
        return false;
    }
}

7.1.2. 您需要回执的结果为JSON

{
    "code": 0,
    "message" : "some message"
}

code0 则成功,其它一律失败

7.2. Channel频道的ID对应关系

在上文proto文件中,有一个channel字段,这表示消息的发送的频道ID,此对应关系需要对接给我们

仅为参考:

8. 接入建议

接入荐读:关于天眼对接的几点建议



Table of Contents