文档版本:v2.2.2 2019-08-02
By: Fly
目前有4种接口方式,可以依照具体的场景使用对应的方式。
HOST:api.gamegs.cn
TCP端口:8741
HTTP端口:8743
HTTPS端口:443(非必要,请选择其它接入方式)
HTTP端口:7000 测试端口
香港、台湾地区请使用域名:hk.gamegs.cn 端口同上
每日超过20万的数据建议使用TCP协议发送数据。
| 游戏ID | 正文长度 Length | 正文(见下文)Content |
|---|---|---|
| byte(2) | uint32 byte(4) Big-Endian |
byte(N) |
天眼后台生成的2 bytes的ID |
表示后面正文的长度 1 ~ FFFFFFFF |
Protobuf结构的数据,见下文.proto文件 |
ID:为天眼后台生成的一个2字节数据,比如 0x12 0x34,请向管理员索取Length:是后面正文的长度,而不是整个包的长度,对于sizeof(Content) != Length的包会直接丢弃Length并不能真正达到FFFFFFFF,鉴于系统处理的是聊天信息,理论上也不会太长。如果您的游戏有特长文本传输需求(比如>512K),双方可以先联调测试一下。2 + 4 + N以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; // 禁言状态由我方回传
}
先实现聊天主类,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数据转换二进制流即可,ChatUserV3是ChatV3的子类
可以使用 http://admin.gamegs.cn/tests/proto 来测试您待发送数据的完整性
天眼会将您发送的protobuf数据完全返回,不过会更新ChatV3中的status字段用于前置禁言判断。
recv和发送结构一样,即
| ID 2bytes | LENGTH 4bytes | protobuf 正文 |
| 字段 | 类型 | 含义 |
|---|---|---|
| status | int | 状态 0:正常 1:当前聊天违规 2:当前聊天疑似违规 |
注:游戏需要根据返回结果,将status为1、2的聊天进行隐言处理,对其它玩家不可见。
提醒:游戏需做好异常和超时处理逻辑,已免影响正常游戏体验。
| 游戏ID | 正文长度 Length | 正文 Content |
|---|---|---|
| byte(2) | uint32 byte(4) Big-Endian |
byte(N) |
天眼后台生成的2 bytes的ID |
表示后面正文的长度 1 ~ FFFFFFFF |
16进制 0xf |
众所周知,HTTP/1.1的传输效率较低,会话周期短,建立多个http通道时,就需要多次tcp握手;并且一个会话中,会重复的发送、接受多余的头部信息;然后因为本接口设置的是POST的JSON(JSON中键名不能省略),浪费了大量流量在传输上。
其次,鉴于HTTP/1.1的原理,客户端在没有得到HTTP回复前,会一直阻塞,这会影响发送效率。
而TCP、Websocket的长连接链路方式,只需一次握手,其次使用Protobuf压缩之后,传输的字节数大大的减少,并且TCP无阻塞,可以持续的发送数据,所以在传输效率上要优于HTTP的方式。
如果在技术上没有实现难度,建议使用
TCP的方式。
如果接口 2s 没有返回结果,请及时放行聊天,以免因为阻塞或等原因导致用户聊天故障
POST http://api.gamegs.cn:8743/api/v1/chat/receive
jQuery.ajax、axios、okhttp、GuzzleHttp等data是json的字符串| 字段 | 类型 | 解释 |
|---|---|---|
| id | string | 我们给您的ID的16进制字符串,比如 4ad1,无需0x 这点上和TCP的不一致,请务必注意 |
| data[] | string JSON的文本 | 见下文结构 |
| data[] | 第二条, 没有可忽略 | 见下文结构 |
| data[] | 第N条, 没有可忽略 | 见下文结构 |
data[]建议每次100条以内,以免HTTP阻塞,
注意:
如果没有以下的某字段信息,可以设置为null,或者没有该字段,注意不是字符串的 "null"
| 字段 | 类型 | 解释 |
|---|---|---|
| id | string | 消息的ID,必要,每条消息不一样 |
| channel | string | 消息频道,必要 |
| from | User的JSON结构 | 发言人,必要 |
| to | User的JSON结构,或null | 发送给谁,私聊时必要 |
| content | string | 发言内容,必要 |
| ip | string | 发言人的IP,必要 |
| extra | 任意JSON,或null | 额外信息 |
| 字段 | 类型 | 解释 |
|---|---|---|
| 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接口专门对extra、user.extra做了处理,和TCP的Protobuf结构有区别,不关心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}" // 错误
}
...
}
如果您习惯将JSON作为Body发送过去,可以使用类似下面的结构
- data 使用正常的
json结构, 无需转换为字符串- 一定要设置头:
Content-Type: application/json- 无论是一次传输一条或多条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的结果
根据实现环境,自行选择
POST JSON或POST 表单的方式
表单结构
<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('发送成功');
}
}
});
PHP的GuzzleHttp的例子$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")
{
// 正确
}
}
});
Http接口返回如下JSON格式的字符串
{
"result": "success", // 值为success或api为正确的返回,其它一律为错误
"message": { // 返回api时,没有此字段
"title": "标题,可能没有",
"content": "正确或错误的信息"
},
"data": [ //当前聊天返回的结果,字段解释见下文
{
"id": "您传入消息0的ID",
"status": 1,
},
{
"id": "您传入消息1的ID",
"status": 2,
}
],
//其它字段您无需在意
}
data返回的结果的顺序与发送data顺序绝对会保持一致。
| 字段 | 类型 | 含义 |
|---|---|---|
| id | string | 您传入的该条消息的ID |
| status | int | 状态 0:正常 1:当前聊天违规 2:当前聊天疑似违规 |
注:游戏需要根据返回结果,将status为1、2的聊天进行隐言处理,对其它玩家不可见。
提醒:游戏需做好异常和超时处理逻辑,已免影响正常游戏体验。
serverId zoneId 的区别zoneId 是游戏里面区服的意思, 比如是:梦幻西游的华东1、华南1、华南2,或者是传奇1区、传奇7区,这是必填字段,
而serverId是表示发行渠道ID,比如代表的意义:发行商1、发行商2、小米专服
公会公告、礼物赠言、口令红包等可以文本输入的地方,也是广告的高发地带,这些信息也需要传输过来,并且需要对这类发言指定对应的channel ID。
关于TCP、WebSocket接口的方式的注意事项
OOM崩溃。您需要提供一个禁言的API接口,以便天眼系统在识别到违规信息之后,调取该API封禁用户,比如A用户发送拉人广告,系统在判断后,将会调取该API,并发送回用户的相关信息,以及禁言时长(分钟),您的API会对该用户做出处罚。
接口以HTTP为宜。
禁言时长覆盖的问题:针对大VIP、大等级用户,系统的引入了阶梯禁言机制防止误封,比如1、5、15、60、9999分钟。但是随着用户的恶意度增加,可能会在上一次禁言条件未失效的情况下,发送新的禁言时间,此时,您应该是覆盖之前的禁言时长,而不是累加或拒绝。
POST application/x-www-form-urlencoded 或 POST JSON 方式发送如下数据
请确定使用哪种方式
参与签名的字段 playerId、userId、zoneId、serverId、time
以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;
}
}
{
"code": 0,
"message" : "some message"
}
code 为 0 则成功,其它一律失败
在上文proto文件中,有一个channel字段,这表示消息的发送的频道ID,此对应关系需要对接给我们
仅为参考: