skynet源码阅读<3>--网关分析
继上一篇介绍了skynet的网络部分之后,这一篇以网关gate.lua为例,简单分析下其串接和处理流程。
在官方给出的范例中,是以examples/main.lua作为启动脚本的,在此过程中会创建watchdog服务:
- local watchdog = skynet.newservice("watchdog")
- skynet.call(watchdog, "lua", "start", {
- port = ,
- maxclient = max_client,
- nodelay = true,
- })
首先加载watchdog.lua脚本。而在watchdog.lua的加载过程中,创建了gate服务。加载gate.lua过程中,调用gateserver.start(gate),gateserver会向skynet注册socket协议的处理:
- skynet.register_protocol {
- name = "socket",
- id = skynet.PTYPE_SOCKET, -- PTYPE_SOCKET = 6
- unpack = function ( msg, sz )
- return netpack.filter( queue, msg, sz)
- end,
- dispatch = function (_, _, q, type, ...)
- queue = q
- if type then
- MSG[type](...)
- end
- end
- }
另外gateserver也会注册lua协议的处理,这里就不展开了。gateserver中会拦截skynet_socket_message;也会拦截部分lua消息(一般是由watchdog转发而来),并调用gate注册进来的回调。注意gateserver才是skynet消息的入口,gate只不过是个回调而已。至此,gate服务加载完毕。
watchdog服务加载完毕后,main.lua中接着调用watchdog的start方法,其参数分别指定了侦听的端口、最大客户端连接数、是否延迟等。看下watchdog的start方法:
- function CMD.start(conf)
- skynet.call(gate, "lua", "open" , conf)
- end
其紧接着调用gate的open方法,而这个方法在gateserver中被拦截了:
- function CMD.open( source, conf )
- assert(not socket)
- local address = conf.address or "0.0.0.0"
- local port = assert(conf.port)
- maxclient = conf.maxclient or
- nodelay = conf.nodelay
- skynet.error(string.format("Listen on %s:%d", address, port))
- socket = socketdriver.listen(address, port)
- socketdriver.start(socket)
- if handler.open then
- return handler.open(source, conf)
- end
- end
可以看到,在open方法中创建了socket并开始了侦听过程。回忆上篇,socket操作的LuaAPI作为socketdriver被实现在lua-socket.c文件中,看一眼这里的listen是如何交互的:
- static int
- llisten(lua_State *L) {
- const char * host = luaL_checkstring(L,);
- int port = luaL_checkinteger(L,);
- int backlog = luaL_optinteger(L,,BACKLOG);
- struct skynet_context * ctx = lua_touserdata(L, lua_upvalueindex());
- int id = skynet_socket_listen(ctx, host,port,backlog);
- if (id < ) {
- return luaL_error(L, "Listen error");
- }
- lua_pushinteger(L,id);
- return ;
- }
析取参数,获取关联的skynet-context之后,调用skynet_socket.c的skynet_socket_listen:
- int
- skynet_socket_listen(struct skynet_context *ctx, const char *host, int port, int backlog) {
- uint32_t source = skynet_context_handle(ctx);
- return socket_server_listen(SOCKET_SERVER, source, host, port, backlog);
- }
拿到context-handle,这个handle在后续创建socket时会被关联起来。handle作为参数opaque传递入socket_server.c中的socket_server_listen方法中:
- int
- socket_server_listen(struct socket_server *ss, uintptr_t opaque, const char * addr, int port, int backlog) {
- int fd = do_listen(addr, port, backlog);
- if (fd < ) {
- return -;
- }
- struct request_package request;
- int id = reserve_id(ss);
- if (id < ) {
- close(fd);
- return id;
- }
- request.u.listen.opaque = opaque;
- request.u.listen.id = id;
- request.u.listen.fd = fd;
- send_request(ss, &request, 'L', sizeof(request.u.listen));
- return id;
- }
在作了bind和listen之后,将此socket描述符打包为request写入socket_server的读管道,由socket_server_poll轮循处理。至此,gate服务中listen的流程已经非常清晰了。
再回到gateserver.CMD.open方法中,在socketdriver.listen之后,紧接着调用socketdriver.start开始网络事件的处理(具体细节请按上述流程参照源码),最后调用gate.lua的回调handler.open。在这个过程中,gate服务的skynet-context-handle已经与对应的socket绑定了,后续关于此socket的SOCKET消息都会转发到gate服务中来,具体到代码则是由gateserver接收过滤后,再做进一步的分发处理。
那么一个新的客户端连接是如何被接收创建的呢?
在socket_server_poll过程中,会检查是否有网络事件产生(以下是简化的代码):
- int socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
- for (;;) {
- // 管道select
- ......
- // socket event check
- ......
- // s: socket
- switch (s->type) {
- case SOCKET_TYPE_CONNECTING:
- return report_connect(ss, s, result);
- case SOCKET_TYPE_LISTEN: {
- int ok = report_accept(ss, s, result);
- if (ok > ) { return SOCKET_ACCEPT; }
17 if (ok < ) { return SOCKET_ERROR; }- // when ok == 0, retry
- break;
- }
23 // other
24 ......25 }- 26 }
对于正在listen的socket(SOCKET_TYPE_LISTEN),当发生事件(即侦测到有新的连接)时,会在report_accept中accept得连接描述符fd并创建socket结构,然后返回SOCKET_ACCEPT交由上层的skynet_socket.c:skynet_socket_poll处理,后者会封装类型为SKYNET_SOCKET_TYPE_ACCEPT的skynet-message并推入到gate服务的队列中去,最终转发到gateserver中所注册的SOCKET协议入口。
回到gateserver中来(见上述gateserver摘录的代码),在接收到网络消息时,先是在unpack中通过netpack(见lua-netpack.c)合并过滤消息,比如TCP消息粘包等(注意skynet_socket_message如果其padding为true,则表示非数据的命令,比如SKYNET_SOCKET_TYPE_ACCEPT)。对于SKYNET_SOCKET_TYPE_ACCEPT命令,netpack会解析并转换为open命令,最后gateserver会调用到MSG.open:
- function MSG.open(fd, msg)
- if client_number >= maxclient then
- socketdriver.close(fd)
- return
- end
- if nodelay then
- socketdriver.nodelay(fd)
- end
- connection[fd] = true
- client_number = client_number +
- handler.connect(fd, msg)
- end
回调到gate.lua中的connect:
- function handler.connect(fd, addr)
- local c = {
- fd = fd,
- ip = addr,
- }
- connection[fd] = c
- skynet.send(watchdog, "lua", "socket", "open", fd, addr)
- end
watchdog.lua中的SOCKET.open:
- function SOCKET.open(fd, addr)
- skynet.error("New client from : " .. addr)
- agent[fd] = skynet.newservice("agent")
- skynet.call(agent[fd], "lua", "start", { gate = gate, client = fd, watchdog = skynet.self() })
- end
此时,会创建玩家agent服务,并调用start方法:
- function CMD.start(conf)
- local fd = conf.client
- local gate = conf.gate
- WATCHDOG = conf.watchdog
- -- slot 1,2 set at main.lua
- host = sprotoloader.load():host "package"
- send_request = host:attach(sprotoloader.load())
- skynet.fork(function()
- while true do
- send_package(send_request "heartbeat")
- skynet.sleep()
- end
- end)
- client_fd = fd
- skynet.call(gate, "lua", "forward", fd)
- end
agent拿到gate服务的标识后,调用forward将自己的标识注册到gate服务中来:
- function CMD.forward(source, fd, client, address)
- local c = assert(connection[fd])
- unforward(c)
- c.client = client or
- c.agent = address or source
- forwarding[c.agent] = c
- gateserver.openclient(fd)
- end
gateserver.openclient开始侦听此socket的事件:
- function gateserver.openclient(fd)
- if connection[fd] then
- socketdriver.start(fd)
- end
- end
至此,一个新连接的建立流程就结束了。那么连接建立后网络数据又是如何转发进来的呢?流程依然是一致的,socket_server_poll侦测到READ后读取数据并转发到gateserver中来,后者调用netpack对数据粘包,此时会调用MSG.more或MSG.data将数据转交给gate.message:
- function handler.message(fd, msg, sz)
- -- recv a package, forward it
- local c = connection[fd]
- local agent = c.agent
- if agent then
- skynet.redirect(agent, c.client, "client", , msg, sz)
- else
- skynet.send(watchdog, "lua", "socket", "data", fd, netpack.tostring(msg, sz))
- end
- end
直接将数据转发给之前通过forward注册进来的agent,采用的协议是"client"。agent中需注册此协议的处理,拿到字节流并根据上层的业务协议对数据转码,做进一步的处理。这样,数据接收和转发的流程就结束了。其它方面,比如数据发送,关闭socket等等,流程上都是一致的,具体细节不再详述。
skynet源码阅读<3>--网关分析的更多相关文章
- skynet源码阅读<1>--lua与c的基本交互
阅读skynet的lua-c交互部分代码时,可以看到如下处理: struct skynet_context * context = lua_touserdata(L, lua_upvalueindex ...
- skynet源码阅读<5>--协程调度模型
注:为方便理解,本文贴出的代码部分经过了缩减或展开,与实际skynet代码可能会有所出入. 作为一个skynet actor,在启动脚本被加载的过程中,总是要调用skynet.start和sky ...
- skynet源码阅读<6>--线程调度
相比于上节我们提到的协程调度,skynet的线程调度从逻辑流程上来看要简单很多.下面我们就来具体做一分析.首先自然是以skynet_start.c为入口: static void start(int ...
- skynet源码阅读<4>--定时器实现
昨天和三石公聊天,他提到timer的实现原理,我当时迟疑了一下,心想timer不是系统底层时钟中断驱动上层进程/线程,累积计时实现的么?他简述了timer的实现,什么堆排序,优先级队列等,与我想象的不 ...
- skynet 源码阅读笔记 bootstrap.lua
最近几周粗略看了 skynet 代码的 C 部分.遇到很多知识点以前只是知道,但并不十分了解,所以这是一个学习的过程. 从 main 函数开始,闷头一阵看下来,着实蛋疼. 当看了 skynet_mq. ...
- skynet源码阅读<7>--死循环检测
在使用skynet开发时,你也许会碰到类似这样的警告:A message from [ :0100000f ] to [ :0100000a ] maybe in an endless loop (v ...
- skynet源码阅读<2>--网络部分
先来看下socket_server的数据结构,这里简称为ss: struct socket_server { int recvctrl_fd; int sendctrl_fd; int checkct ...
- HTTP请求库——axios源码阅读与分析
概述 在前端开发过程中,我们经常会遇到需要发送异步请求的情况.而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率. axios是一个在近些年来非常火的 ...
- JDK源码阅读(一):Object源码分析
最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...
随机推荐
- set_include_path() &&get_include_path()用法
function initialize(){ set_include_path(get_include_path().PATH_SEPARATOR . "core/"); ...
- 【Android】状态栏通知Notification、NotificationManager详解(转)
在Android系统中,发一个状态栏通知还是很方便的.下面我们就来看一下,怎么发送状态栏通知,状态栏通知又有哪些参数可以设置? 首先,发送一个状态栏通知必须用到两个类: NotificationMa ...
- Louvain algorithm for community detection
主要理解Louvain 算法中对于模块度的定义:模块度是评估一个社区网络划分好坏的度量方法,它的物理含义是社区内节点的连边数与随机情况下的边数只差,它的取值范围是 [−1/2,1).可以简单地理解为社 ...
- 你知道在springboot中如何使用WebSocket吗
一.背景 我们都知道 http 协议只能浏览器单方面向服务器发起请求获得响应,服务器不能主动向浏览器推送消息.想要实现浏览器的主动推送有两种主流实现方式: 轮询:缺点很多,但是实现简单 webso ...
- 使用Swagger生成Spring Boot REST客户端(支持Feign)(待实践)
如果项目上使用了Swagger做RESTful的文档,那么也可以通过Swagger提供的代码生成器生成客户端代码,同时支持Feign客户端. 但是经过测试,生成Feign代码和REST客户端有些臃肿. ...
- js上传文件研究
https://github.com/shengulong/javascript-file-upload
- PostgreSQL触发器的使用
原文: https://www.yiibai.com/postgresql/postgresql-trigger.html -------------------------------------- ...
- ElasticSearch集群本机搭建
ElasticSearch集群本机搭建 elasticsearch elasticsearch -Ehttp.port=8200 -Epath.data=node2 elasticsearch -Eh ...
- ng-options bug解决方案(示例)
情况: 无法获取 ng-model 的值 解决方案: 绑定到对象的属性值上 1.页面 <ion-view hide-nav-bar="true"> <ion-co ...
- Effective C++ 条款九、十 绝不在构造和析构过程中调用virtual函数|令operator=返回一个reference to *this
1.当在一个子类当中调用构造函数,其父类构造函数肯定先被调用.如果此时父类构造函数中有一个virtual函数,子类当中也有,肯定执行父类当中的virtual函数,而此时子类当中的成员变量并未被初始 ...