至少会启动两个进程,gate 和 game,可以分属不同集群,因为他们之间是直接通过 socket 通信。
gate 进程会启动两个关键服务,client_gate 和 svr_gate。
game 进程启动一个 gameweb 服务,该服务代码把 examples/simpleweb.lua 拿过来修改下,向 svr_gate 发起一个连接,把连接 id 发给内置的 agent 服务。agent 服务启动该连接的数据传输,然后调用 skynet lua 层自带的 http 解析方法,解析 http 报文,这里我们主要是在读取 http 报文前手动读取 8 字节,解析出 包长 和 客户端的sid,然后在回复 http 报文前,写入客户端sid。
目录结构
启动 gate: ./skynet game/etc/config_web_gate
启动 game:./skynet game/etc/config_web_game
工作流:
怎么知道当前接收的 http 报文的长度是多少?
该 gate 服务只支持 [包长 + 包体] 的消息解析,包长支持 2 字节和 4 字节两种大小,我们正常的游戏服务器自己定义数据格式,完全够用了。
可是 http 报文格式非常奇葩,没有明确的包体长度提供,格式也不像普通字节流一样明确,总算知道为啥它叫超文本协议了,它的报文格式就是文本组成的,用回车换行来分割模块,用字符串来标识模块…
总之,当 client_gate 收到数据时,我不知道一个 http 报文何时接收完毕,那么我就不能为一个 http 报文拼接一个客户端的连接 id 之后再发送给 game,那么当同时有多个客户端发起访问的时候,会有两个问题出现,一个是 svr_gate 服务不知道当前数据包是上一个连接未发完的数据包还是后一个连接的数据包,只能一股脑发给 game 进程,这里就可能出现 game 读取数据出错; 二是 game 运气好处理对了消息,然后想回复客户端的时候,不知道客户端的连接 id,最终发到 client_gate 的时候,client_gate 也不知道该回复给那个客户端。
我搭建 http 服务器的初衷是为了学习熟悉 skynet 的底层上层各种机制,为我后面移植游戏服务器打基础,所以没有时间去造轮子解析 http 报文。搜索到一个 github 上高星的 http 解析库,纯 c 写的,又小巧,就拿来用一下。库地址 别人翻译的博客地址
回到我们要解决的问题上来,我需要这个解析库的目的是做什么,得到当前 http 报文的长度,让我知道应该打包多少数据发给后面的服务。
这里我将 http-parser.c 跟 gate 服务一起编译,为连接池中的每个连接的 buffer 对象初始化一个 http-parser 解析器 struct http_parser,在 service-src/databuffer.h 中增加 http_databuffer_len 接口来获取 http 报文长度,该接口在连接收到远端数据时,尝试获取一个完整 http 报文的长度,如果返回大于 0 的值则表示已经有一个完整的 http 报文的数据量可以进行转发了。
代码可能有点冗杂,好不容易调试过了,还没来得及优化。
C 部分
// service-src/databuffer.h
...
struct databuffer {
int header;
int offset;
int size;
struct message * head;
struct message * tail;
/*增加以下内容*/
struct message * http_head; // 指向 parser 当前解析到的位置
http_parser *parser; // 解析器对象
int http_offset; // 指向 http_head 中当前解析到的偏移值
int http_len; // 当前报文已经解析出来的长度
};
/* game服务回复消息的时候要在 http 报文前插入连接id 信息,
* svr_gate 服务需要先读取四字节的id信息后,重置解析数据
* */
static void
http_databuffer_reset(struct databuffer *db) {
db->http_head = db->head;
db->http_offset = db->offset;
db->http_len = 0;
// http_parser_init(db->parser, HTTP_BOTH); 由于一个连接拥有一个解析器,如果没有解析出错,无须重置解析器;如果出错,连接都该断掉,解析器会在 databuffer_clear() 中被重置
}
/* 尝试获取当前报文的长度
* 1. 读取完一个消息,则返回消息总长度
* 2. 数据读取完,但是一个消息都没读完
* 3. 数据读取完,刚好一个完整 http 消息
* */
static int
http_databuffer_len(struct databuffer *db) {
struct message *head = db->http_head;
int http_offset = db->http_offset;
if (!head) {
db->http_head = db->head;
head = db->http_head;
http_offset = db->offset;
db->http_offset = db->offset;
} else if (head->size == http_offset) {
head = head->next;
http_offset = 0;
}
if (!head) {
return 0;
}
int len = db->http_len;
db->parser->user_flag = 0;
while (head) {
struct message *current = head;
int bsz = current->size - http_offset;
int http_len = http_parser_execute(db->parser, http_settings, current->buffer + http_offset, bsz);
len += http_len;
db->http_offset = http_offset + http_len;
db->http_len = len;
if (db->parser->user_flag) {
// 一个 http 消息结束
db->http_head = head;
db->http_len = 0;
return len;
} else if (http_len == bsz) {
head = head->next;
http_offset = 0;
} else {
// 出错
db->http_head = NULL;
db->http_offset = 0;
db->http_len = 0;
http_parser_init(db->parser, HTTP_BOTH);
return -1;
}
}
if (!head) {
db->http_head = db->tail;
} else {
db->http_head = head;
}
return 0;
}
static void
databuffer_clear(struct databuffer *db, struct messagepool *mp) {
while (db->head) {
_return_message(db,mp);
}
http_parser *parser = db->parser;
http_parser_init(db->parser, HTTP_BOTH); // 重置解析器
memset(db, 0, sizeof(*db));
db->parser = parser; // 将解析器保存下来复用,不需要重新分配内存
}
/* 解析器解析完成一个报文时的回调函数
* 返回 0 表示正常,解析器会继续工作
* 这里特地返回 1 来中断解析器,
* 解析器中断返回时的返回值是当前解析到的位置,
* 这样我们就可以计算到一个完整报文的长度了
* */
static int
databuffer_http_complete(http_parser* parser) {
parser->user_flag = 1; // 我在 struct http_parser 中添加的一个自定义数据,用于 http_databuffer_len 判断当前报文是否完整
return 1;
}
/* 初始化解析器的回调设置结构,注册解析完一个 http 报文的回调函数 */
static void
databuffer_http_setting_init() {
if (!http_settings) {
http_settings = skynet_malloc(sizeof(*http_settings));
memset((char*)http_settings, 0, sizeof(*http_settings));
http_settings->on_message_complete = databuffer_http_complete;
}
}
// 3rd/http-parser/http_parser.h
...
struct http_parser {
...
int user_flag; // 添加自定义数据,之前不熟悉不敢直接用 data 来存数据,
// 其实 data 应该就是用于存放自定义数据的,后面再优化啦,
// 不侵入式的使用外部的库是最好的
/** PUBLIC **/
void *data; /* A pointer to get hook to the "connection" or "socket" object */
};
// service-src/service_gate.c
...
struct gate {
struct skynet_context *ctx;
int listen_id;
uint32_t watchdog;
uint32_t broker; // 客户端连接的gate服务地址 .client_gate
uint32_t broker_http; // 服务器连接的gate服务地址 .svr_gate
int broker_sid; // 第一个连接上 .svr_gate 的 game 连接 id
// 用于标识客户端不指定 game id 时的消息转发对象
int client_tag;
int header_size;
int max_connection;
struct hashid hash;
struct connection *conn;
// todo: save message pool ptr for release
struct messagepool mp;
};
/* 服务消息处理接口修改 */
static void
_ctrl(struct gate * g, const void * msg, int sz) {
...
if (memcmp(command,"broker",i)==0) {
_parm(tmp, sz, i);
g->broker = skynet_queryname(ctx, command);
databuffer_http_setting_init(); // 初始化 http-parser 的回调设置
return;
}
if (memcmp(command,"broker_http",i)==0) {
_parm(tmp, sz, i);
g->broker_http = skynet_queryname(ctx, command);
skynet_error(ctx, "broker_http: %u", g->broker_http);
databuffer_http_setting_init(); // 初始化 http-parser 的回调设置
return;
}
...
}
/* socket 消息的处理接口修改
* 这里的 size 就是一个完整的 http 报文的长度
* 1. 在报文的最后拼接的四字节是要发往的客户端id, gate 服务将收到的服务消息转发给指定客户端的时候会用
* 我们在发往 broker_http(svr_gate) 时这里设置为0,让 svr_gate 转发给默认的 game 连接
* 2. 在报文前拼接四字节 fd 是消息来源,客户端与 client_gate 的连接 id,用于 game 处理完消息
* 回复用户的时候,client_gate 可以将消息发给指定用户
* 3. 因为报文最后拼接的发送对象fd,在 svr_gate 转发消息前会截取掉,所以包长只记录 客户端id + http报文长度
* */
static void
_forward(struct gate *g, struct connection * c, int size, int sid) {
...
if (g->broker_http) {
// 四字节包长 + [四字节fd + 包体] + 四字节发送对象fd
int len = size + 4; // 四字节fd + 包体的长度
void * temp = skynet_malloc(len + 4 + 4);
databuffer_read(&c->buffer,&g->mp,(char *)(temp + 8), size);
*((int *)(temp + 4 + len)) = 0;
char * csize = (char*)temp;
char * client_id = (char*)temp + 4;
for (int i = 0; i < 4; ++i) {
*(csize + i) = (size >> ((3 - i) * 8)) & 0xFF;
*(client_id + i) = (fd >> ((3 - i) * 8)) & 0xFF;
}
skynet_error(ctx, "gate -> svr_gate, fd,%d, cfd,%d, size,%d, csize,%d", fd, *(int*)client_id, size, *(int*)csize);
skynet_send(ctx, 0, g->broker_http, g->client_tag | PTYPE_TAG_DONTCOPY, fd, temp, len + 4 + 4);
return;
}
}
/* gate 服务默认的网络消息转发器 */
static void
dispatch_message(struct gate *g, struct connection *c, int id, void * data, int sz) {
databuffer_push(&c->buffer,&g->mp, data, sz);
for (;;) {
int size = databuffer_readheader(&c->buffer, &g->mp, g->header_size);
if (size < 0) {
return;
} else if (size > 0) {
if (size >= 0x1000000) {
struct skynet_context * ctx = g->ctx;
databuffer_clear(&c->buffer,&g->mp);
skynet_socket_close(ctx, id);
skynet_error(ctx, "Recv socket message > 16M");
return;
} else {
_forward(g, c, size, 0);
databuffer_reset(&c->buffer);
}
}
}
}
/* client_gate 网络消息转发器,处理 http 协议,转发给 svr_gate */
static void
dispatch_message_http(struct gate *g, struct connection *c, int id, void * data, int sz) {
databuffer_push(&c->buffer,&g->mp, data, sz); // 这里将网络数据存入连接对象持有的消息池
// 保持不变
for (;;) {
int http_len = http_databuffer_len(&c->buffer); // 尝试获取完整报文的长度
// 0 正常解析中,但是数据量不够
// -1 解析失败(非法的http报文)
// >0 完整报文的长度
skynet_error(g->ctx, "[gate] dispatch_message_http, sid,%d, http_len,%d, all_len,%d", c->id, http_len, c->buffer.size);
if (http_len == 0) {
return;
} else if (http_len < 0) {
struct skynet_context * ctx = g->ctx;
databuffer_clear(&c->buffer,&g->mp);
skynet_socket_close(ctx, id);
skynet_error(ctx, "[client_gate] http msg parser failed, id,%s", id);
return;
} else if (http_len >= 0x1000000) {
struct skynet_context * ctx = g->ctx;
databuffer_clear(&c->buffer,&g->mp);
skynet_socket_close(ctx, id);
skynet_error(ctx, "Recv socket message > 16M");
return;
} else {
_forward(g, c, http_len, 0);
databuffer_reset(&c->buffer);
http_databuffer_reset(&c->buffer);
}
}
}
/* svr_gate 网络消息转发器,处理 http 协议,转发给 client_gate
* 这里接收到的是 game 进程的回包,格式为 客户端id + http报文
* 所以需要先解析前面四字节作为客户端id,然后才能进行http报文长度计算
* */
static void
dispatch_message_proxy(struct gate *g, struct connection *c, int id, void * data, int sz) {
skynet_error(g->ctx, "[svr_gate] dispatch_message_proxy, sz,%d", sz);
databuffer_push(&c->buffer,&g->mp, data, sz);
for (;;) {
static int sid = -1;
skynet_error(g->ctx, "[svr_gate] dispatch_message_proxy send msg begin, sid,%d", sid);
if (sid == -1) {
if (c->buffer.size < 4) {
return;
}
databuffer_read(&c->buffer, &g->mp, (char*)&sid, 4);
http_databuffer_reset(&c->buffer); // 读取到 sid 后,需要重置下 parser 参数
}
int http_len = http_databuffer_len(&c->buffer);
skynet_error(g->ctx, "[svr_gate] dispatch_message_proxy, sid,%d, httplen,%d, all,%d, httpoffset,%d, msgoffset,%d, db->httplen,%d, msg->head,%x, http_head,%x, flag,%d", sid, http_len, c->buffer.size, c->buffer.http_offset, c->buffer.offset, c->buffer.http_len, c->buffer.head, c->buffer.http_head, c->buffer.parser->user_flag);
if (http_len == 0) {
return;
} else if (http_len < 0) {
struct skynet_context * ctx = g->ctx;
databuffer_clear(&c->buffer,&g->mp);
skynet_socket_close(ctx, id);
skynet_error(ctx, "[svr_gate] http msg parser failed, id,%s", id);
return;
} else if (http_len >= 0x1000000) {
struct skynet_context * ctx = g->ctx;
databuffer_clear(&c->buffer,&g->mp);
skynet_socket_close(ctx, id);
skynet_error(ctx, "Recv socket message > 16M");
return;
} else {
_forward(g, c, http_len, sid);
databuffer_reset(&c->buffer);
sid = -1;
skynet_error(g->ctx, "[svr_gate] dispatch_message_proxy send msg done, sid,%d", sid);
}
}
}
/* skynet gate 服务的服务消息处理接口
* PTYPE_TEXT 类型的消息为控制指令,可以进行 gate 服务的一些参数设置
* PTYPE_CLIENT 类型的消息为需要发往远端的消息,会将消息写入 skynet socket
* PTYPE_SOCKET 类型的消息为网络消息
* 我们在这里只略微修改了下 PTYPE_CLIENT 类型的处理,当消息没有指定远端 sid 的时候,设置为默认的 broker_sid
* */
static int
_cb(struct skynet_context * ctx, void * ud, int type, int session, uint32_t source, const void * msg, size_t sz) {
struct gate *g = ud;
switch(type) {
case PTYPE_TEXT:
_ctrl(g , msg , (int)sz);
break;
case PTYPE_CLIENT: {
if (sz <=4 ) {
skynet_error(ctx, "Invalid client message from %x",source);
break;
}
// The last 4 bytes in msg are the id of socket, write following bytes to it
const uint8_t * idbuf = msg + sz - 4;
uint32_t uid = idbuf[0] | idbuf[1] << 8 | idbuf[2] << 16 | idbuf[3] << 24;
if (uid == 0 && g->broker_sid) {
uid = g->broker_sid;
}
int id = hashid_lookup(&g->hash, uid);
skynet_error(ctx, "ptype_client, broker_sid,%d, uid,%u, id,%d, size,%d, csize,%d", g->broker_sid, uid, id, sz, *(int*)msg);
if (id>=0) {
// don't send id (last 4 bytes)
skynet_socket_send(ctx, uid, (void*)msg, sz-4);
// return 1 means don't free msg
return 1;
} else {
skynet_error(ctx, "Invalid client id %d from %x",(int)uid,source);
break;
}
}
case PTYPE_SOCKET:
// recv socket message from skynet_socket
dispatch_socket_message(g, msg, (int)(sz-sizeof(struct skynet_socket_message)));
break;
}
return 0;
}
/* skynet gate 服务的初始化接口
* 在初始化连接池的时候,为每个连接对象初始化 http 解析器
* */
int
gate_init(struct gate *g , struct skynet_context * ctx, char * parm) {
if (parm == NULL)
return 1;
...
int n = sscanf(parm, "%c %s %s %d %d", &header, watchdog, binding, &client_tag, &max);
/* 这里云风大哥写的 n<4,总觉得不妥,要么这个检查不要,要么就应该参数齐全 */
if (n<5) {
skynet_error(ctx, "Invalid gate parm %s",parm);
return 1;
}
...
int i;
for (i=0;i<max;i++) {
g->conn[i].id = -1;
/* 为解析器分配内存并初始化 */
g->conn[i].buffer.parser = skynet_malloc(sizeof(*g->conn[i].buffer.parser));
http_parser_init(g->conn[i].buffer.parser, HTTP_BOTH);
}
switch(header) {
case 'S':
g->header_size = 2;
break;
case 'L':
g->header_size = 4;
break;
case 'A': // all 全额转发
// 一开始是想到 http 报文就全额转发
// game 服务的 skynet 提供的 lua层http解析接口解析即可
// 但是这样对于多个客户端连接就会出现问题了,后面就没用这个方式了
g->header_size = 0;
break;
default:
break;
}
skynet_callback(ctx,g,_cb);
return start_listen(g,binding);
}
Lua 部分
-- game/myservice/service_web/gateweb.lua
local skynet = require "skynet"
local table = table
local string = string
require "skynet.manager"
skynet.register_protocol({
name = "gate",
id = skynet.PTYPE_TEXT, -- gate 服务的控制指令消息类型
pack = function(...) return ... end,
unpack = skynet.unpack,
})
skynet.start(function()
-- 启动 client_gate 服务
local client_gate = skynet.launch("gate", "A", "!", "0.0.0.0:8001", "0", "10000")
skynet.name(".client_gate", client_gate)
-- 启动 svr_gate 服务
local svr_gate = skynet.launch("gate", "A", "!", "0.0.0.0:8002", "0", "10")
skynet.name(".svr_gate", svr_gate)
-- 设置 client_gate 服务的 broker_http 为 svr_gate
skynet.send(client_gate, "gate", "broker_http .svr_gate")
-- 设置 svr_gate 服务的 broker 为 client_gate
skynet.send(svr_gate, "gate", "broker .client_gate")
INFO("client_gate: %d, svr_gate: %d", client_gate, svr_gate)
skynet.exit() -- 将两个 gate 服务启动后就可以退出了
end)
-- game/myservice/service_web/gameweb.lua
local mode, protocol = ...
protocol = protocol or "http"
if mode == "agent" then
-- 小端编码,因为 gate 服务解析 PTYPE_CLIENT 消息的时候,是按小端方式解码的
-- 低位数据存在低位地址
local function wrap_int(val)
local str = ""
for i = 1, 4 do
str = str .. string.char((val >> ((i - 1) * 8)) & 0xFF)
end
return str
end
skynet.start(function()
skynet.dispatch("lua", function (_,_,id)
socket.start(id)
local size, sid
local interface = gen_interface(protocol, id)
if interface.init then
interface.init()
end
-- local readbytes = sockethelper.readfunc(id)
local writebytes = sockethelper.writefunc(id)
while true do
-- limit request body size to 8192 (you can pass nil to unlimit)
INFO("prepare to receive http msg, id,%s", id)
-- 这里修改了下 lualib/http/internal.lua 的
-- function M.recvheader(readbytes, lines, header, has_size) 接口
-- 通过这里传递的第三个参数 true 来返回 size 和 sid
local code, url, method, header, body, size, sid = httpd.read_request(interface.read, 8192, true)
INFO("receive http msg, code,%s, url,%s, size,%s, sid,%s", code, url, size, sid)
if code then
if code ~= 200 then
response(id, interface.write, code)
else
local tmp = {}
if header.host then
table.insert(tmp, string.format("host: %s", header.host))
end
local path, query = urllib.parse(url)
table.insert(tmp, string.format("path: %s", path))
if query then
local q = urllib.parse_query(query)
for k, v in pairs(q) do
table.insert(tmp, string.format("query: %s= %s", k,v))
end
end
table.insert(tmp, "-----header----")
for k,v in pairs(header) do
table.insert(tmp, string.format("%s = %s",k,v))
end
table.insert(tmp, "-----body----\n" .. body)
writebytes(wrap_int(sid)) -- 先将 sid 写入,再写入完整的 http 报文
response(id, interface.write, code, table.concat(tmp,"\n"))
end
else
if url == sockethelper.socket_error then
skynet.error("socket closed")
else
skynet.error(url)
end
break
end
end
socket.close(id)
if interface.close then
interface.close()
end
end)
end)
else
skynet.start(function()
local protocol = "http"
-- 启动内置的 agent 服务,代码就在本文件上面一个分支
local agent = skynet.newservice(SERVICE_NAME, "agent", protocol)
-- 连接 svr_gate
local svr_gate_id = socket.open("127.0.0.1:8002")
INFO("connect svr_gate_id completed, svr_gate_id,%s", svr_gate_id)
-- 将连接套接字发给 agent 服务处理具体消息
skynet.send(agent, "lua", svr_gate_id)
-- 由于是本服务发起的连接,所以不能退掉
-- skynet.exit()
end)
end
学习过程中的一些难点和知识点记录
还有待完善的部分
后续实现方案三来搭建网关,熟悉 skynet 的 cluster 服务。