文档版本: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,此对应关系需要对接给我们
仅为参考: