Gsky游戏服务器框架介绍 2

First Post:

Last Update:

Word Count:
1.9k

Read Time:
8 min

Gsky游戏服务器框架介绍 2

github

最近跟新了许多代码,目前代码框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
gsky
├── crypto
│ ├── pe.cc
│ ├── pe.hh
│ ├── pmd5.cc
│ └── pmd5.hh
├── gsky.cc
├── gsky.hh
├── log
│ ├── log.cc
│ ├── log.hh
│ ├── log_thread.cc
│ └── log_thread.hh
├── net
│ ├── channel.cc
│ ├── channel.hh
│ ├── epoll.cc
│ ├── epoll.hh
│ ├── eventloop.cc
│ ├── eventloop.hh
│ ├── eventloop_thread.cc
│ ├── eventloop_thread.hh
│ ├── eventloop_threadpool.cc
│ ├── eventloop_threadpool.hh
│ ├── http
│ ├── net.cc
│ ├── net.hh
│ ├── pp
│ │ ├── pp.hh
│ │ ├── request.cc
│ │ ├── request.hh
│ │ ├── response.cc
│ │ ├── response.hh
│ │ ├── socket.cc
│ │ └── socket.hh
│ ├── socket.cc
│ ├── socket.hh
│ ├── util.cc
│ └── util.hh
├── server.cc
├── server.hh
├── thread
│ ├── condition.hh
│ ├── count_down_latch.hh
│ ├── mutex_lock.hh
│ ├── noncopyable.hh
│ ├── thread.cc
│ └── thread.hh
└── util
├── firewall.cc
├── firewall.hh
├── json.hh
├── url.hh
├── util.cc
├── util.hh
└── vessel.hh

这几天主要是完善 pp (pwnsky protocol)二进制加密传输协议,还有该框架的拓展性。

pp 协议,全称为 pwnsky protocol, 是一款吸收http部分特性的一款二进制传输协议,主要用于游戏长连接交互协议,目前基于tcp来实现。

该协议头部只占16字节,相对与http更小,由于协议字段都在固定位置,解析起来更快速。

pp协议中定义有状态码,数据类型,数据长度,请求路由。

采用 pwnsky encryption进行数据加密,由服务端随机生成8字节密钥返回给客户端,客户端接收到之后,在断开之前传输数据都采用该密钥进行加解密。

pp协议是我自己根据http特点来压缩而来的,头部大小只有16字节,目前头部定义字段如下:

1
2
3
4
5
--------------------------------------------------------------------------
| magic 2字节 | status 1字节| type 1字节 | length 4 字节 |
--------------------------------------------------------------------------
| route 6 字节 | code 2字节 |
--------------------------------------------------------------------------

magic: 协议标识,两字节为 “\x50\x50”

status: 状态码,包含客户端请求状态码与服务端响应状态码。

type: 传输数据类型,类似与http中的Content-Type

length: 数据长度

route: 请求路由,类似于http url中的path

code: 校验码,用于检测传输内容是否符合加密规范。

pp协议目前 c++ 定义如下,后面不断完善协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
namespace pp {
enum class status {

// 客户端请求码
connect = 0x10, // 建立连接,请求密钥的过程
data_transfer = 0x11, // 传输数据

// 服务端响应码
protocol_error = 0x20, // 协议解析错误
too_big = 0x21, // 传输数据过长
invalid_transfer = 0x22, // 无效传输

ok = 0x30, // 请求成功
send_key = 0x31, // 发送密钥

redirct = 0x40, // 重置路由

};

// 数据类型
enum class data_type {
binary_stream = 0x00, // 二进制数据流
image = 0x01, // 图片
video = 0x02, // 视频
music = 0x03, // 音乐

text = 0x10, // 文本
json = 0x11, // json 数据
xml = 0x12, // xml 数据
};

// 协议头,只占16 字节
struct header {
unsigned short magic; // 协议标识,"PP" 值为0x5050,
unsigned char status; // 客户端请求码与服务端响应码
unsigned char type; // 数据类型
unsigned int length; // 数据长度
unsigned char route[6]; // 请求路由,代替http url中的path
unsigned char code[2]; // 数据校验码
};

}

采用pp协议的gsky服务器连接与客户端过程:

  1. 客户端发起获取密钥连接请求

  2. 服务端随机生成8字节密钥和2字节code (校验码),并采用PE (Pwnsky Encryption)以全0的8字节的密钥对内容部分进行加密,也对pp协议头部后8字节也进行单独加密。

  3. 客户端收到数据,采用全0 的8字节密钥分别解密协议头部后8字节与内容密钥部分,将其code与密钥储存。

  4. 客户端发送数据,在协议头部的code值设置为之前服务端发送过来的code,再分别对内容与头部后8字节采用服服务端发送过来的密钥进行加密,再发送给服务端。

  5. 服务端接收数据,采用自己的密钥先进行协议头部后8字节解密,检验code值是否正确,正确之后再根据长度接收数据内容与解密数据内容。

大体上连接与传输过程就是这么回事,客户端若不进行密钥获取的话,服务端接收到数据后是直接断开连接的。

那至于为什么要对协议头部后8字节进行加密,协议头部后8字节包含了 6字节的route与2字节的code,route相当于请求路径,也不希望攻击者通过抓包看到的,所以route有必要进行加盟,code是校验值,有一定程度检测数据与密钥的正确性。

上面提到了PE加密,PE加密是自己先暂时写的一个对称加密算法,比较简单,采用密钥轮加变换单字节单字节的异或数据,密码算法简单的目的也是处于服务器的处理效率考虑,目前加解密c++实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace gsky {
namespace crypto {
class pe {
public:
pe();
~pe();

void encode(unsigned char key[8], void *raw_data, size_t length);
void decode(unsigned char key[8], void *raw_data, size_t length);

unsigned char xor_table_[256] = {
0xbe, 0xd1, 0x90, 0x88, 0x57, 0x00, 0xe9, 0x53, 0x10, 0xbd, 0x2a, 0x34, 0x51, 0x84, 0x07, 0xc4,
0x33, 0xc5, 0x3b, 0x53, 0x5f, 0xa8, 0x5d, 0x4b, 0x6d, 0x22, 0x63, 0x5d, 0x3c, 0xbd, 0x47, 0x6d,
0x22, 0x3f, 0x38, 0x4b, 0x7a, 0x4c, 0xb8, 0xcc, 0xb8, 0x37, 0x78, 0x17, 0x73, 0x23, 0x27, 0x71,
0xb1, 0xc7, 0xa6, 0xd1, 0xa0, 0x48, 0x21, 0xc4, 0x1b, 0x0a, 0xad, 0xc9, 0xa5, 0xe6, 0x14, 0x18,
0xfc, 0x7b, 0x53, 0x59, 0x8b, 0x0d, 0x07, 0xcd, 0x07, 0xcc, 0xbc, 0xa5, 0xe0, 0x28, 0x0e, 0xf9,
0x31, 0xc8, 0xed, 0x78, 0xf4, 0x75, 0x60, 0x65, 0x52, 0xb4, 0xfb, 0xbf, 0xac, 0x6e, 0xea, 0x5d,
0xca, 0x0d, 0xb5, 0x66, 0xac, 0xba, 0x06, 0x30, 0x95, 0xf4, 0x96, 0x42, 0x7a, 0x7f, 0x58, 0x6d,
0x83, 0x8e, 0xf6, 0x61, 0x7c, 0x0e, 0xfd, 0x09, 0x6e, 0x42, 0x6b, 0x1e, 0xb9, 0x14, 0x22, 0xf6,

0x16, 0xd2, 0xd2, 0x60, 0x29, 0x23, 0x32, 0x9e, 0xb4, 0x82, 0xee, 0x58, 0x3a, 0x7d, 0x1f, 0x74,
0x98, 0x5d, 0x17, 0x64, 0xe4, 0x6f, 0xf5, 0xad, 0x94, 0xaa, 0x89, 0xe3, 0xbe, 0x98, 0x91, 0x38,
0x70, 0xec, 0x2f, 0x5e, 0x9f, 0xc9, 0xb1, 0x26, 0x3a, 0x64, 0x48, 0x13, 0xf1, 0x1a, 0xc5, 0xd5,
0xe5, 0x66, 0x11, 0x11, 0x3a, 0xaa, 0x79, 0x45, 0x42, 0xb4, 0x57, 0x9d, 0x3f, 0xbc, 0xa3, 0xaa,
0x98, 0x4e, 0x6b, 0x7a, 0x4a, 0x2f, 0x3e, 0x10, 0x7a, 0xc5, 0x33, 0x8d, 0xac, 0x0b, 0x79, 0x33,
0x5d, 0x09, 0xfc, 0x9d, 0x9b, 0xe5, 0x18, 0xcd, 0x1c, 0x7c, 0x8b, 0x0a, 0xa8, 0x95, 0x56, 0xcc,
0x4e, 0x34, 0x31, 0x33, 0xf5, 0xc1, 0xf5, 0x03, 0x0a, 0x4a, 0xb4, 0xd1, 0x90, 0xf1, 0x8f, 0x57,
0x20, 0x05, 0x0d, 0xa0, 0xcd, 0x82, 0xb3, 0x25, 0xd8, 0xd2, 0x20, 0xf3, 0xc5, 0x96, 0x35, 0x35,
};
};

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <gsky/crypto/pe.hh>

gsky::crypto::pe::pe() {

}

gsky::crypto::pe::~pe() {

}

// key length is 8 bytes
// 加密概述
// 采用密钥重叠循环,查表来进行异或。
//
void gsky::crypto::pe::encode(unsigned char key[8], void *raw_data, size_t length) {
unsigned char keys[8];
memcpy(keys, key, 8);
char *data = (char *)raw_data;
for(int i = 0; i < length; i ++) {
data[i] ^= keys[i % 8];
unsigned char n = ((keys[i % 8] + keys[(i + 1) % 8]) * keys[(i + 2) % 8]) & 0xff;
data[i] ^= n ^ xor_table_[n];
keys[i % 8] = (n * 2 + 3) % 0x100;
}
}

// 解密
void gsky::crypto::pe::decode(unsigned char key[8], void *raw_data, size_t length) {
unsigned char keys[8];
memcpy(keys, key, 8);
char *data = (char *)raw_data;
for(int i = 0; i < length; i ++) {
char t_key = keys[i % 8];
unsigned char n = ((keys[i % 8] + keys[(i + 1) % 8]) * keys[(i + 2) % 8]) & 0xff;
data[i] ^= n ^ xor_table_[n];
data[i] ^= t_key;
keys[i % 8] = (n * 2 + 3) % 0x100;
}
}

协议拓展部分,为了更方便的自定义传输协议,我对框架进行了比较大的整改,在net模块目前有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
├── net
│ ├── channel.cc
│ ├── channel.hh
│ ├── epoll.cc
│ ├── epoll.hh
│ ├── eventloop.cc
│ ├── eventloop.hh
│ ├── eventloop_thread.cc
│ ├── eventloop_thread.hh
│ ├── eventloop_threadpool.cc
│ ├── eventloop_threadpool.hh
│ ├── http // http解析,有待实现
│ ├── net.cc
│ ├── net.hh
│ ├── pp // pp解析
│ │ ├── pp.hh
│ │ ├── request.cc
│ │ ├── request.hh
│ │ ├── response.cc
│ │ ├── response.hh
│ │ ├── socket.cc
│ │ └── socket.hh
│ ├── socket.cc
│ ├── socket.hh
│ ├── util.cc
│ └── util.hh

上面部分主要是从net::socket类进行协议的分支,该类主要是相当与一个epoll架构的单纯tcp套接子处理,基于tcp之上,再对数据进行协议解析,为了让库更好的拓展,我特意模仿了golang语言中的http库写了两个类request类和response类,request类是存储客户端请求信息,response类是让数据发送给客户端的封装接口。

目前来说pp协议服务端已经基本差不多了,只是pp协议客户端还有待实现一下sdk,方便接入gsky服务器。

打赏点小钱
支付宝 | Alipay
微信 | WeChat