继上一篇介绍了skynet的网络部分之后,这一篇以网关gate.lua为例,简单分析下其串接和处理流程。

在官方给出的范例中,是以examples/main.lua作为启动脚本的,在此过程中会创建watchdog服务:

  1. local watchdog = skynet.newservice("watchdog")
  2. skynet.call(watchdog, "lua", "start", {
  3. port = ,
  4. maxclient = max_client,
  5. nodelay = true,
  6. })

首先加载watchdog.lua脚本。而在watchdog.lua的加载过程中,创建了gate服务。加载gate.lua过程中,调用gateserver.start(gate),gateserver会向skynet注册socket协议的处理:

  1. skynet.register_protocol {
  2. name = "socket",
  3. id = skynet.PTYPE_SOCKET, -- PTYPE_SOCKET = 6
  4. unpack = function ( msg, sz )
  5. return netpack.filter( queue, msg, sz)
  6. end,
  7. dispatch = function (_, _, q, type, ...)
  8. queue = q
  9. if type then
  10. MSG[type](...)
  11. end
  12. end
  13. }

另外gateserver也会注册lua协议的处理,这里就不展开了。gateserver中会拦截skynet_socket_message;也会拦截部分lua消息(一般是由watchdog转发而来),并调用gate注册进来的回调。注意gateserver才是skynet消息的入口,gate只不过是个回调而已。至此,gate服务加载完毕。

watchdog服务加载完毕后,main.lua中接着调用watchdog的start方法,其参数分别指定了侦听的端口、最大客户端连接数、是否延迟等。看下watchdog的start方法:

  1. function CMD.start(conf)
  2. skynet.call(gate, "lua", "open" , conf)
  3. end

其紧接着调用gate的open方法,而这个方法在gateserver中被拦截了:

  1. function CMD.open( source, conf )
  2. assert(not socket)
  3. local address = conf.address or "0.0.0.0"
  4. local port = assert(conf.port)
  5. maxclient = conf.maxclient or
  6. nodelay = conf.nodelay
  7. skynet.error(string.format("Listen on %s:%d", address, port))
  8. socket = socketdriver.listen(address, port)
  9. socketdriver.start(socket)
  10. if handler.open then
  11. return handler.open(source, conf)
  12. end
  13. end

可以看到,在open方法中创建了socket并开始了侦听过程。回忆上篇,socket操作的LuaAPI作为socketdriver被实现在lua-socket.c文件中,看一眼这里的listen是如何交互的:

  1. static int
  2. llisten(lua_State *L) {
  3. const char * host = luaL_checkstring(L,);
  4. int port = luaL_checkinteger(L,);
  5. int backlog = luaL_optinteger(L,,BACKLOG);
  6. struct skynet_context * ctx = lua_touserdata(L, lua_upvalueindex());
  7. int id = skynet_socket_listen(ctx, host,port,backlog);
  8. if (id < ) {
  9. return luaL_error(L, "Listen error");
  10. }
  11.  
  12. lua_pushinteger(L,id);
  13. return ;
  14. }

析取参数,获取关联的skynet-context之后,调用skynet_socket.c的skynet_socket_listen:

  1. int
  2. skynet_socket_listen(struct skynet_context *ctx, const char *host, int port, int backlog) {
  3. uint32_t source = skynet_context_handle(ctx);
  4. return socket_server_listen(SOCKET_SERVER, source, host, port, backlog);
  5. }

拿到context-handle,这个handle在后续创建socket时会被关联起来。handle作为参数opaque传递入socket_server.c中的socket_server_listen方法中:

  1. int
  2. socket_server_listen(struct socket_server *ss, uintptr_t opaque, const char * addr, int port, int backlog) {
  3. int fd = do_listen(addr, port, backlog);
  4. if (fd < ) {
  5. return -;
  6. }
  7. struct request_package request;
  8. int id = reserve_id(ss);
  9. if (id < ) {
  10. close(fd);
  11. return id;
  12. }
  13. request.u.listen.opaque = opaque;
  14. request.u.listen.id = id;
  15. request.u.listen.fd = fd;
  16. send_request(ss, &request, 'L', sizeof(request.u.listen));
  17. return id;
  18. }

在作了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过程中,会检查是否有网络事件产生(以下是简化的代码):

  1. int socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {
  2. for (;;) {
  3. // 管道select
  4. ......
  5.  
  6. // socket event check
  7. ......
  8.  
  9. // s: socket
  10. switch (s->type) {
  11. case SOCKET_TYPE_CONNECTING:
  12. return report_connect(ss, s, result);
  13. case SOCKET_TYPE_LISTEN: {
  14. int ok = report_accept(ss, s, result);
  15. if (ok > ) { return SOCKET_ACCEPT; }
    17 if (ok < ) { return SOCKET_ERROR; }
  16. // when ok == 0, retry
  17. break;
  18. }
    23 // other
    24 ......25 }
  19. 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:

  1. function MSG.open(fd, msg)
  2. if client_number >= maxclient then
  3. socketdriver.close(fd)
  4. return
  5. end
  6. if nodelay then
  7. socketdriver.nodelay(fd)
  8. end
  9. connection[fd] = true
  10. client_number = client_number +
  11. handler.connect(fd, msg)
  12. end

回调到gate.lua中的connect:

  1. function handler.connect(fd, addr)
  2. local c = {
  3. fd = fd,
  4. ip = addr,
  5. }
  6. connection[fd] = c
  7. skynet.send(watchdog, "lua", "socket", "open", fd, addr)
  8. end

watchdog.lua中的SOCKET.open:

  1. function SOCKET.open(fd, addr)
  2. skynet.error("New client from : " .. addr)
  3. agent[fd] = skynet.newservice("agent")
  4. skynet.call(agent[fd], "lua", "start", { gate = gate, client = fd, watchdog = skynet.self() })
  5. end

此时,会创建玩家agent服务,并调用start方法:

  1. function CMD.start(conf)
  2. local fd = conf.client
  3. local gate = conf.gate
  4. WATCHDOG = conf.watchdog
  5. -- slot 1,2 set at main.lua
  6. host = sprotoloader.load():host "package"
  7. send_request = host:attach(sprotoloader.load())
  8. skynet.fork(function()
  9. while true do
  10. send_package(send_request "heartbeat")
  11. skynet.sleep()
  12. end
  13. end)
  14.  
  15. client_fd = fd
  16. skynet.call(gate, "lua", "forward", fd)
  17. end

agent拿到gate服务的标识后,调用forward将自己的标识注册到gate服务中来:

  1. function CMD.forward(source, fd, client, address)
  2. local c = assert(connection[fd])
  3. unforward(c)
  4. c.client = client or
  5. c.agent = address or source
  6. forwarding[c.agent] = c
  7. gateserver.openclient(fd)
  8. end

gateserver.openclient开始侦听此socket的事件:

  1. function gateserver.openclient(fd)
  2. if connection[fd] then
  3. socketdriver.start(fd)
  4. end
  5. end

至此,一个新连接的建立流程就结束了。那么连接建立后网络数据又是如何转发进来的呢?流程依然是一致的,socket_server_poll侦测到READ后读取数据并转发到gateserver中来,后者调用netpack对数据粘包,此时会调用MSG.more或MSG.data将数据转交给gate.message:

  1. function handler.message(fd, msg, sz)
  2. -- recv a package, forward it
  3. local c = connection[fd]
  4. local agent = c.agent
  5. if agent then
  6. skynet.redirect(agent, c.client, "client", , msg, sz)
  7. else
  8. skynet.send(watchdog, "lua", "socket", "data", fd, netpack.tostring(msg, sz))
  9. end
  10. end

直接将数据转发给之前通过forward注册进来的agent,采用的协议是"client"。agent中需注册此协议的处理,拿到字节流并根据上层的业务协议对数据转码,做进一步的处理。这样,数据接收和转发的流程就结束了。其它方面,比如数据发送,关闭socket等等,流程上都是一致的,具体细节不再详述。

skynet源码阅读<3>--网关分析的更多相关文章

  1. skynet源码阅读<1>--lua与c的基本交互

    阅读skynet的lua-c交互部分代码时,可以看到如下处理: struct skynet_context * context = lua_touserdata(L, lua_upvalueindex ...

  2. skynet源码阅读<5>--协程调度模型

    注:为方便理解,本文贴出的代码部分经过了缩减或展开,与实际skynet代码可能会有所出入.    作为一个skynet actor,在启动脚本被加载的过程中,总是要调用skynet.start和sky ...

  3. skynet源码阅读<6>--线程调度

    相比于上节我们提到的协程调度,skynet的线程调度从逻辑流程上来看要简单很多.下面我们就来具体做一分析.首先自然是以skynet_start.c为入口: static void start(int ...

  4. skynet源码阅读<4>--定时器实现

    昨天和三石公聊天,他提到timer的实现原理,我当时迟疑了一下,心想timer不是系统底层时钟中断驱动上层进程/线程,累积计时实现的么?他简述了timer的实现,什么堆排序,优先级队列等,与我想象的不 ...

  5. skynet 源码阅读笔记 bootstrap.lua

    最近几周粗略看了 skynet 代码的 C 部分.遇到很多知识点以前只是知道,但并不十分了解,所以这是一个学习的过程. 从 main 函数开始,闷头一阵看下来,着实蛋疼. 当看了 skynet_mq. ...

  6. skynet源码阅读<7>--死循环检测

    在使用skynet开发时,你也许会碰到类似这样的警告:A message from [ :0100000f ] to [ :0100000a ] maybe in an endless loop (v ...

  7. skynet源码阅读<2>--网络部分

    先来看下socket_server的数据结构,这里简称为ss: struct socket_server { int recvctrl_fd; int sendctrl_fd; int checkct ...

  8. HTTP请求库——axios源码阅读与分析

    概述 在前端开发过程中,我们经常会遇到需要发送异步请求的情况.而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率. axios是一个在近些年来非常火的 ...

  9. JDK源码阅读(一):Object源码分析

    最近经过某大佬的建议准备阅读一下JDK的源码来提升一下自己 所以开始写JDK源码分析的文章 阅读JDK版本为1.8 目录 Object结构图 构造器 equals 方法 getClass 方法 has ...

随机推荐

  1. set_include_path() &&get_include_path()用法

    function initialize(){    set_include_path(get_include_path().PATH_SEPARATOR . "core/");   ...

  2. 【Android】状态栏通知Notification、NotificationManager详解(转)

    在Android系统中,发一个状态栏通知还是很方便的.下面我们就来看一下,怎么发送状态栏通知,状态栏通知又有哪些参数可以设置? 首先,发送一个状态栏通知必须用到两个类:  NotificationMa ...

  3. Louvain algorithm for community detection

    主要理解Louvain 算法中对于模块度的定义:模块度是评估一个社区网络划分好坏的度量方法,它的物理含义是社区内节点的连边数与随机情况下的边数只差,它的取值范围是 [−1/2,1).可以简单地理解为社 ...

  4. 你知道在springboot中如何使用WebSocket吗

    一.背景   我们都知道 http 协议只能浏览器单方面向服务器发起请求获得响应,服务器不能主动向浏览器推送消息.想要实现浏览器的主动推送有两种主流实现方式: 轮询:缺点很多,但是实现简单 webso ...

  5. 使用Swagger生成Spring Boot REST客户端(支持Feign)(待实践)

    如果项目上使用了Swagger做RESTful的文档,那么也可以通过Swagger提供的代码生成器生成客户端代码,同时支持Feign客户端. 但是经过测试,生成Feign代码和REST客户端有些臃肿. ...

  6. js上传文件研究

    https://github.com/shengulong/javascript-file-upload

  7. PostgreSQL触发器的使用

    原文: https://www.yiibai.com/postgresql/postgresql-trigger.html -------------------------------------- ...

  8. ElasticSearch集群本机搭建

    ElasticSearch集群本机搭建 elasticsearch elasticsearch -Ehttp.port=8200 -Epath.data=node2 elasticsearch -Eh ...

  9. ng-options bug解决方案(示例)

    情况: 无法获取 ng-model 的值 解决方案: 绑定到对象的属性值上 1.页面 <ion-view hide-nav-bar="true"> <ion-co ...

  10. Effective C++ 条款九、十 绝不在构造和析构过程中调用virtual函数|令operator=返回一个reference to *this

      1.当在一个子类当中调用构造函数,其父类构造函数肯定先被调用.如果此时父类构造函数中有一个virtual函数,子类当中也有,肯定执行父类当中的virtual函数,而此时子类当中的成员变量并未被初始 ...