skynet学习笔记之http服务搭建3

文章目录

    • 前言
    • 架构介绍
    • 问题
    • 解决办法
    • 关键代码
    • 结语
    • 参考文章

前言

  • 上周用 master/slave 机制搭建了个带有网关的 http 服务器,那个是方案一,gate 和 game 在一个集群。经过几天折腾,总算调试好了方案二实现的 gate ,gate 和 game 分属不同集群。
  • 本文总结用 skynet 自带的 c 服务 gate 来搭建 http 服务器的网关,并且做到在 c 层转发完整的 http 数据包。
  • 我对该 gate 服务作为基础的 c 服务放在那却没有被 example 下的例子用到感到很奇怪,如果有人知道哪个服务用到了可以告诉我一下吗,对它的详细介绍放到下一篇文章。
  • 为了实现 c 层代理 http 协议,引入了 nodejs 的 http 解析库 https://github.com/nodejs/http-parser 来测量 http 报文长度。该解析库十分小巧,且纯用 c 写的,只需要引入一个头文件,一个 .c 文件,编译连接到 gate 服务中即可。

架构介绍

图画的很烂,将就看看吧
skynet学习笔记之http服务搭建3_第1张图片
服务搭建:

  • 至少会启动两个进程,gate 和 game,可以分属不同集群,因为他们之间是直接通过 socket 通信。

  • gate 进程会启动两个关键服务,client_gate 和 svr_gate。

    • client_gate :开放端口接纳客户端连接,转发客户端消息给 svr_gate;接收 svr_gate 消息回复给客户端
    • svr_gate :开放端口接纳 game 服务器连接,转发 client_gate 消息给 game 服务器;接收 game 服务器的消息回复给 client_gate
  • game 进程启动一个 gameweb 服务,该服务代码把 examples/simpleweb.lua 拿过来修改下,向 svr_gate 发起一个连接,把连接 id 发给内置的 agent 服务。agent 服务启动该连接的数据传输,然后调用 skynet lua 层自带的 http 解析方法,解析 http 报文,这里我们主要是在读取 http 报文前手动读取 8 字节,解析出 包长 和 客户端的sid,然后在回复 http 报文前,写入客户端sid。

目录结构
skynet学习笔记之http服务搭建3_第2张图片
启动 gate: ./skynet game/etc/config_web_gate
启动 game:./skynet game/etc/config_web_game

消息流:
skynet学习笔记之http服务搭建3_第3张图片

  • 浏览器发给 client_gate 的消息格式:http报文
  • client_gate 发给 svr_gate 的消息格式:四字节长度 + [ 四字节sid + 完整http报文 ] + 四字节uid,长度为 [ 四字节sid + 完整http报文 ] 的长度
  • svr_gate 发给 game 的消息格式:四字节长度 + [ 四字节sid + 完整http报文 ] ,截取掉了末尾的uid
  • game 发给 svr_gate 的消息格式:四字节sid + game回复的http报文
  • svr_gate 发给 client_gate 的消息格式:完整http报文 + 四字节sid
  • client_gate 发给浏览器的消息格式:完整http报文

工作流:

  1. 客户端,也就是浏览器,输入 服务器 ip:8001 就向 gate 进程的 client_gate 服务发起了连接。
  2. client_gate 进程的 8001 端口接收到客户端连接后,client_gate 服务创建一个 skynet socket 结构,从连接池里分配一个连接对象用于接收后续数据。
  3. 客户端连接成功后,往 client_gate 分配的连接发送 http 报文。
  4. skynet 的网络事件监听器 epoll 将收到的数据 struct skynet_socket_message 通过消息队列发往 client_gate 服务,消息类型为 PTYPE_SOCKET。
  5. client_gate 服务收到 PTYPE_SOCKET 消息后,将数据存放进连接对象的 databuffer 结构中,然后尝试从该 buffer 中读取一个完整的 http 报文,如果读取成功,则会将读取到的数据拼接上消息来源的客户端连接 id 后通过消息队列转发给 svr_gate 服务,消息类型为 PTYPE_CLIENT。
  6. svr_gate 服务收到类型为 PTYPE_CLIENT 的消息后,将消息转发给远端,也就是与它相连的 game 进程的 http 消息处理服务。
  7. game 用 skynet 自带的 lua 层 http 解析接口处理完消息后,回复消息时,先压入消息来源的客户端与 client_gate 的连接 id,然后写入 http 报文,发送给 svr_gate。
  8. svr_gate 接收到 game 的回包后,将解析得到的客户端与 client_gate 的连接 id 和完整的 http 报文通过消息队列转发给 client_gate 服务,消息类型为 PTYPE_CLIENT。
  9. client_gate 最终将 game 回复的 http 报文转发给客户端。

问题

怎么知道当前接收的 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

结语

学习过程中的一些难点和知识点记录

  • http-parser 解析器工作流程
  • http 和 自定义协议区别,以及他们与 tcp 协议,套接字的关系
  • skynet http 服务详细拆解
  • skynet gate 服务收包,转发流程详细拆解,引申通用套接字编程
  • int 转换,大小端问题
  • skynet.pack/unpack 工作方式

还有待完善的部分

  • 客户端与 client_gate 的连接关闭后,要丢弃关闭前发给 game 消息而关闭后才接收到的回包,否则会出现将之前用户的回包发错给其他用户。
  • 侵入式的修改 http_parser.h 代码,为解析器增加了个 user_flag 字段,这可以优化成使用解析器提供的 void *data 字段。
  • 为了方便 gdb 调试,将 http-parser 代码跟 skynet 进程一起编译了,优化成只编译到 gate 服务中。

后续实现方案三来搭建网关,熟悉 skynet 的 cluster 服务。

参考文章

  • nodejs/http-parser 库的 readme

你可能感兴趣的:(skynet,c语言,lua)