skynet框架应用 (十三) 网关服务

13 网关服务

​ skynet 提供了一个通用模板 lualib/snax/gateserver.lua 来启动一个网关服务器,通过 TCP 连接和客户端交换数据。

​ TCP 基于数据流,但一般我们需要以带长度信息的数据包的结构来做数据交换。gateserver 做的就是这个工作,把数据流切割成包的形式转发到可以处理它的地址。


local gateserver = require "snax.gateserver"

local handler = {}        --必须提供一张表,表里面定义connect、message等相关回调函数

-- register handlers here

gateserver.start(handler)  --网关服务的入口函数

13.1最简单网关服务

13.1.1 编写mygateserver.lua

示例代码:mygateserver.lua


local skynet = require "skynet"
local gateserver = require "snax.gateserver"

local handler = {}

--当一个客户端链接进来,gateserver自动处理链接,并且调用该函数,必须要有
function handler.connect(fd, ipaddr)   
    skynet.error("ipaddr:",ipaddr,"fd:",fd,"connect")
    gateserver.openclient(fd) --链接成功不代表马上可以读到数据,需要打开这个套接字,允许fd接收数据
end

--当一个客户端断开链接后调用该函数,必须要有
function handler.disconnect(fd)   
    skynet.error("fd:", fd, "disconnect")
end


--当fd有数据到达了,会调用这个函数,前提是fd需要调用gateserver.openclient打开
function handler.message(fd, msg, sz)
    skynet.error("recv message from fd:", fd)

end


gateserver.start(handler)

13.1.2 启动mygateserver

​ 可以使用普通服务创建方式来创建一个mygateserver服务,但是这个服务启动后,并不能马上开始工作,

需要你给mygateserver发送一个lua消息open并且告诉gateserver监听的端口、最大连接数、延时等信息来开启mygateserver服务。

代码如下openmygateserver.lua


local skynet = require "skynet"

skynet.start(function()
    skynet.error("Server start")
    local gateserver = skynet.newservice("mygateserver") --启动刚才写的网关服务
    skynet.call(gateserver, "lua", "open", {   --需要给网关服务发送open消息,来启动监听
        port = 8002,            --监听的端口
        maxclient = 64,         --客户端最大连接数
        nodelay = true,         --是否延迟TCP
    })

    skynet.error("gate server setup on", 8002)
    skynet.exit()
end)

运行结果:


$ ./skynet examples/config
openmygateserver                    #启动openmygateserver
[:01000010] LAUNCH snlua openmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver  #启动mygateserver
[:01000012] Listen on 0.0.0.0:8002     #开始监听端口8002
[:01000010] gate server setup on 8002
[:01000010] KILL self               #openmygateserver退出

重开一个终端,启动一个C语言的socketclient客户端(在代码在9.2中)去连接8002端口,观察skynet服务情况:


$ ./skynet examples/config
openmygateserver  
[:01000010] LAUNCH snlua openmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver
[:01000012] Listen on 0.0.0.0:8002
[:01000010] gate server setup on 8002
[:01000010] KILL self
[:01000012] ipaddr: 127.0.0.1:48008 fd: 9 connect  #连接进入
[:01000012] fd: 9 disconnect                       #断开连接

​ 上面的结果可以看连接与断开连接都能执行,但是handler.message并没有执行。这是由于snax.gateserver

基于TCP协议包装了一个两字节数据长度的协议。而现在版本的socketclient并没有按照这种协议发送。

13.2 gateserver应用协议

13.2.1 两字数据长度协议

​ gateserver应用协议是基于TCP协议做了一层简单的封装,前两个字节表示数据包的长度len(不计算这两个表示长度的字节),高字节在前低字节在后(大端序),后面紧跟len字节数的数据。例如:


\x00\x05    \x31\x32\x33\x34\x35
    |            |
  len           data

​ 由于只用两字节表示数据长度,那么这个包的data最大只能是65535字节。这种协议包方式可以解决TCP粘包的问题,也是TCP通信当中最常用的一种应用层协议包定义方式。

​ 所以如果想通过TCP与gateserver通信必须要按照这种协议进行组包解包。否则gateserver肯定是不识别的。

13.2.2 打包与解包

​ 打包与解包TCP网路数据可以使用skynet.netpack库


local netpack = require "skynet.netpack" --使用netpack
--打包数据str,返回一个C指针msg,sz,申请内存
netpack.pack(str) 

--解包数据,返回一个lua的字符串,会释放内存
netpack.tostring(msg, sz)

13.2.3 client使用长度协议发包

下面我们通过改写socketclient.c文件来组包发送数据:


#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXLINE 128

void* readthread(void* arg)
{
    pthread_detach(pthread_self());
    int sockfd = (int)arg;
    int n = 0;
    char buf[MAXLINE];
    while (1) 
        {
        n = read(sockfd, buf, MAXLINE);
        if (n == 0)
        {
            printf("the other side has been closed.\n");
            close(sockfd);
            exit(0);
        }
        else
            write(STDOUT_FILENO, buf, n);
    }   
    return (void*)0;
}

int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        printf("usage:%s port", argv[0]);
        return -1;
    }
    int port = atoi(argv[1]);
    struct sockaddr_in servaddr;
    int sockfd;
    short size, nsize;
    char buf[MAXLINE];
    unsigned char sendbuf[MAXLINE];

    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
    servaddr.sin_port = htons(port);
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    pthread_t thid;
    pthread_create(&thid, NULL, readthread, (void*)sockfd);

    while (fgets(buf, MAXLINE, stdin) != NULL) 
    {
        size = (short)strlen(buf);   //计算需要发送的数据包长度
        nsize = htons(size);        //转换成大端序
        memcpy(sendbuf, &nsize, sizeof(nsize));  //nsize先填入sendbuf
        memcpy(sendbuf+sizeof(nsize), buf, size); //再填入buf内容
        write(sockfd, sendbuf, size + sizeof(nsize));
    }
    close(sockfd);
    return 0;
}

先运行skynet服务,然后客户端运行:


$ gcc  socketclient.c -o socketclient -lpthread
./socketclient 8002

切回skynet运行的终端:


$ ./skynet examples/config
testmygateserver
[:01000010] LAUNCH snlua testmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver
[:01000012] Listen on 0.0.0.0:8002
[:01000010] gate server setup on 8002
[:01000010] KILL self
[:01000012] ipaddr: 127.0.0.1:48012 fd: 9 connect
[:01000012] recv message from fd: 9    #已经调用handler.message

13.2.4 gateserver解包

​ 上面的实验中mygateserver.lua中的handler.message已经被调用,并且告诉fd为9的套接字发来了数据,msg表示C数据指针,sz表示数据长度,在lua中无法直接使用,需要转换成lua可识别的数据,需要调用netpack.tostring函数来解包。下面来改写一下mygateserver.lua:


local skynet = require "skynet"
local gateserver = require "snax.gateserver"
local netpack = require "skynet.netpack" --使用netpack

local handler = {}

--当一个客户端链接进来,gateserver自动处理链接,并且调用该函数
function handler.connect(fd, ipaddr)   
    skynet.error("ipaddr:",ipaddr,"fd:",fd,"connect")
    gateserver.openclient(fd)
end

--当一个客户端断开链接后调用该函数
function handler.disconnect(fd)
    skynet.error("fd:", fd, "disconnect")
end


--接收消息
function handler.message(fd, msg, sz)
    skynet.error("recv message from fd:", fd)
    skynet.error(netpack.tostring(msg, sz))   --把 handler.message 方法收到的 msg,sz 转换成一个 lua string,并释放 msg 占用的 C 内存。
end

gateserver.start(handler)

运行结果:


$ ./skynet examples/config
openmygateserver
[:01000010] LAUNCH snlua openmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver
[:01000012] Listen on 0.0.0.0:8002
[:01000010] gate server setup on 8002
[:01000010] KILL self
[:01000012] ipaddr: 127.0.0.1:48018 fd: 9 connect
[:01000012] recv message from fd: 9
[:01000012] aaaaaaaaaaa     #正常接收到数据。

[:01000012] fd: 9 disconnect

需要注意的是:msg是一个C指针指向了一块堆空间,如果你不进行任何处理,那么也要调用skynet.trash来释放底层的内存。

13.3 控制客户端连接数

​ 网关服务最重要的任务就是控制客户端连接数,避免大量客户登录到这个服务上。

修改openmygateserver.lua


local skynet = require "skynet"

skynet.start(function()
    skynet.error("Server start")
    local gateserver = skynet.newservice("mygateserver") 
    skynet.call(gateserver, "lua", "open", {   
        port = 8002,            --监听的端口
        maxclient = 2,          --客户端最大连接数改为2个
        nodelay = true,         --是否延迟TCP
    })

    skynet.error("gate server setup on", 8002)
    skynet.exit()
end)

运行openmygateserver.lua,再运行三个socketclient,结果如下:


openmygateserver
[:0100000a] LAUNCH snlua openmygateserver
[:0100000a] Server start
[:0100000b] LAUNCH snlua mygateserver
[:0100000b] Listen on 0.0.0.0:8002
[:0100000b] open by  :0100000a
[:0100000b] listen on 8002
[:0100000b] client max 2
[:0100000b] nodelay true
[:0100000a] gate server setup on 8002
[:0100000a] KILL self
[:0100000b] ipaddr: 127.0.0.1:46650 fd: 7 connect  #第一个客户端连接成功
[:0100000b] ipaddr: 127.0.0.1:46652 fd: 8 connect  #第二个客户端连接成功
[:0100000b] fd: 9 disconnect                     #第三个客户端连接成功后,马上关闭

13.4 gateserver其他回调函数


--如果你希望在监听端口打开的时候,做一些初始化操作,可以提供 open 这个方法。
--source 是请求来源地址,conf 是开启 gate 服务的参数表(端口,连接数,是否延迟)。
function handler.open(source, conf)
end


--当一个连接异常(通常意味着断开),error 被调用,除了 fd ,还会拿到错误信息 msg(通常用于 log 输出)。
function handler.error(fd, msg)
end

--当 fd 上待发送的数据累积超过 1M 字节后,将回调这个方法。你也可以忽略这个消息。
function handler.warning(fd, size)
end

代码如下:

mygateserver.lua


local skynet = require "skynet"
local gateserver = require "snax.gateserver"
local netpack = require "skynet.netpack"

local handler = {}

--当一个客户端链接进来,gateserver自动处理链接,并且调用该函数
function handler.connect(fd, ipaddr)   
    skynet.error("ipaddr:",ipaddr,"fd:",fd,"connect")
    gateserver.openclient(fd)
end

--当一个客户端断开链接后调用该函数
function handler.disconnect(fd)
    skynet.error("fd:", fd, "disconnect")
end


--接收数据
function handler.message(fd, msg, sz)
    skynet.error("recv message from fd:", fd)
    --把 handler.message 方法收到的 msg,sz 转换成一个 lua string,并释放 msg 占用的 C 内存。
    skynet.error(netpack.tostring(msg, sz))   
end

--如果报错就关闭该套接字
function handler.error(fd, msg)
    gateserver.closeclient(fd)
end

--fd中待发送数据超过1M时调用该函数,可以不处理
function handler.warning(fd, size)
    skynet.skynet("warning fd=", fd , "unsent data over 1M")
end

--一旦gateserver打开监听成功后就会调用该接口
--testmygateserver.lua通过给mygateserver.lua发送lua消息open触发该函数调用
function handler.open(source, conf) 
    skynet.error("open by ", skynet.address(source))
    skynet.error("listen on", conf.port)
    skynet.error("client max", conf.maxclient)
    skynet.error("nodelay", conf.nodelay)
end

gateserver.start(handler)

运行结果:


$ ./skynet examples/config
openmygateserver
[:01000010] LAUNCH snlua openmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver
[:01000012] Listen on 0.0.0.0:8002
[:01000012] open by  :01000010
[:01000012] listen on 8002
[:01000012] client max 64
[:01000012] nodelay true
[:01000010] gate server setup on 8002
[:01000010] KILL self

13.5 给gateserver发送lua消息

​ gateserver除了能接收socket消息以为,当然也是可以接受skynet的lua消息,并且gateserver还对lua消息注册函数进行了封装,只需提供handler.command回调函数就能处理lua消息,不需要我们自己调用skynet.dispatch来注册。

继续改写mygateserver.lua


local skynet = require "skynet"
local gateserver = require "snax.gateserver"
local netpack = require "skynet.netpack"


local handler = {}
local CMD = {}

--当一个客户端链接进来,gateserver自动处理链接,并且调用该函数
function handler.connect(fd, ipaddr)   
    skynet.error("ipaddr:",ipaddr,"fd:",fd,"connect")
    gateserver.openclient(fd)
end

--当一个客户端断开链接后调用该函数
function handler.disconnect(fd)
    skynet.error("fd:", fd, "disconnect")
end


--
function handler.message(fd, msg, sz)
    skynet.error("recv message from fd:", fd)
    --把 handler.message 方法收到的 msg,sz 转换成一个 lua string,并释放 msg 占用的 C 内存。
    skynet.error(netpack.tostring(msg, sz))   
end


function handler.error(fd, msg)
    skynet.closeclient(fd)
end


function handler.warning(fd, size)
    skynet.skynet("warning fd=", fd , "unsend data over 1M")
end


function handler.open(source, conf) --testmygateserver
    skynet.error("open by ", skynet.address(source))
    skynet.error("listen on", conf.port)
    skynet.error("client max", conf.maxclient)
    skynet.error("nodelay", conf.nodelay)
end


function CMD.kick(source, fd)
    skynet.error("source:", skynet.address(source), "kick fd:", fd)
    gateserver.closeclient(fd)
end


function handler.command(cmd, source, ...)
    local f = assert(CMD[cmd])
    return f(source, ...)
end

gateserver.start(handler)

再编写一个给mygateserver发送命令的服务:kickmygateserver.lua


local skynet = require "skynet"

local gateserver, fd= ...
fd = tonumber(fd) --必须要转换成整形数,skynet命令行传入的参数都是字符串
skynet.start(function()
    skynet.call(gateserver, "lua",  "kick", fd)
    skynet.exit()
end)
  • 运行

1、先运行openmygateserver。

2、再在另一个终端运行socketclient。

3、然后回到skynet这边启动kickmygateserver关闭连接。


$ ./skynet examples/config
openmygateserver   #终端输入
[:01000010] LAUNCH snlua openmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver  #网关启动
[:01000012] Listen on 0.0.0.0:8002
[:01000012] open by  :01000010
[:01000012] listen on 8002
[:01000012] client max 64
[:01000012] nodelay true
[:01000010] gate server setup on 8002
[:01000010] KILL self
[:01000012] ipaddr: 127.0.0.1:49038 fd: 9 connect #一个先的客户端连接进来,fd为9
kickmygateserver :01000012 9   #终端输入给网关服务也就是:01000012发送lua消息关闭9号fd
[:01000020] LAUNCH snlua kickmygateserver :01000012 9
[:01000012] source: :01000020 kick fd: 9
[:01000020] KILL self
[:01000012] fd: 9 disconnect  #fd 9关闭收到反馈。

13.6 open与close两个lua消息

​ gateserver保留了open与close两个lua消息用来打开关闭监听的端口,所以大家在定义命令的时候不要再使用open与close了。通过这两个消息,我们可以轻松的管理gateserver的开关。

​ 其实在openmygateserver.lua中我们已经发送过一个lua消息open给mygateserver。下面我们来试一试发送lua消息close给mygateserver看看。

代码示例:closemygateserver.lua


local skynet = require "skynet"

local gateserver = ...
skynet.start(function()
    skynet.call(gateserver, "lua",  "close")
    skynet.exit()
end)

先运行openmygateserver再运行closemygateserver:


$ ./skynet examples/config
openmygateserver    
[:01000010] LAUNCH snlua openmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver
[:01000012] Listen on 0.0.0.0:8002
[:01000012] open by  :01000010
[:01000012] listen on 8002
[:01000012] client max 64
[:01000012] nodelay true
[:01000010] gate server setup on 8002
[:01000010] KILL self
[:01000012] ipaddr: 127.0.0.1:49048 fd: 9 connect #可以正常连接
closemygateserver :01000012
[:01000019] LAUNCH snlua closemygateserver :01000012 #发送完CLOSE命令后无法连接新的客户端
[:01000019] KILL self

13.7 agent服务

​ 前面几节我们讲到gateserver的网络数据的读取,那么是不是也可以通过gateserver来发送网络数据呢?很遗憾gateserver并没有提供相关的写函数,因为gateserver本身就只负责管理网络连接(即 TCP连接的处理),涉及到请求处理与答复一般是交给一个叫agent的服务,agent可以由一个普通服务来充当。

​ 来写一个简单的agent.lua服务


local skynet = require "skynet"
local netpack = require "skynet.netpack"
local socket = require "skynet.socket"

local client_fd = ...
client_fd = tonumber(client_fd)

skynet.register_protocol {   
    name = "client",
    id = skynet.PTYPE_CLIENT,
    --需要将网路数据转换成lua字符串,不需要打包,所以不用注册pack函数
    unpack = netpack.tostring,  
}

local function task(msg)
    print("recv from fd", client_fd, msg)
    --响应消息的时候直接通过fd发送出去
    socket.write(client_fd, netpack.pack(string.upper(msg))) 
end

skynet.start(function()
    --注册client消息专门用来接收网络数据
    skynet.dispatch("client", function(_,_, msg) 
        task(msg)
    end)

    skynet.dispatch("lua", function(_,_, cmd) --注册lua消息,来退出服务
        if cmd == "quit" then
            skynet.error(fd,"agent quit")
            skynet.exit()
        end
    end)
end)

​ 改写一下mygateserver.lua,让它处理好一个链接后就创建一个agent服务,并且把fd传给agent,一旦收到数据就转发给agent服务。其他与客户端的交流工作都通过agent来解决。


local skynet = require "skynet"
local gateserver = require "snax.gateserver"
local netpack = require "skynet.netpack"

local handler = {}
local CMD = {}
local agents = {}
--注册client消息专门用来将接收到的网络数据转发给agent,不需要解包,也不需要打包
skynet.register_protocol {   
    name = "client",
    id = skynet.PTYPE_CLIENT,
}

function handler.connect(fd, ipaddr)   
    skynet.error("ipaddr:",ipaddr,"fd:",fd,"connect")
    gateserver.openclient(fd)
    local agent = skynet.newservice("myagent", fd) --连接成功就启动一个agent来代理
    agents[fd] = agent
end

function handler.disconnect(fd) --断开连接后,agent服务退出
    skynet.error("fd:", fd, "disconnect")
    local agent = agents[fd]
    if(agent) then
        --通过发送消息的方式来退出不要使用skynet.kill(agent)
        skynet.send(agent, "lua", "quit")
        agents[fd] = nil
    end
end

function handler.message(fd, msg, sz)
    local agent = agents[fd]
    skynet.redirect(agent, 0, "client", 0, msg, sz) --收到消息就转发给agent
end

function handler.error(fd, msg)
    skynet.closeclient(fd)
end

function handler.warning(fd, size)
    skynet.skynet("warning fd=", fd , "unsend data over 1M")
end

function handler.open(source, conf) 
    skynet.error("open by ", skynet.address(source))
    skynet.error("listen on", conf.port)
    skynet.error("client max", conf.maxclient)
    skynet.error("nodelay", conf.nodelay)
end

function CMD.kick(source, fd)
    skynet.error("source:", skynet.address(source), "kick fd:", fd)
    gateserver.closeclient(fd)
end

function handler.command(cmd, source, ...)
    local f = assert(CMD[cmd])
    return f(source, ...)
end

gateserver.start(handler)

直接在skynet上运行openmygateserver,然后再另一个终端启动socketclient:


$ ./skynet examples/config
openmygateserver
[:01000010] LAUNCH snlua openmygateserver
[:01000010] Server start
[:01000012] LAUNCH snlua mygateserver
[:01000012] Listen on 0.0.0.0:8002
[:01000012] open by  :01000010
[:01000012] listen on 8002
[:01000012] client max 64
[:01000012] nodelay true
[:01000010] gate server setup on 8002
[:01000010] KILL self
[:01000012] ipaddr: 127.0.0.1:49076 fd: 9 connect  #socketclient链接进来
[:01000019] LAUNCH snlua myagent 9      #启动一个新的myagent来处理
recv from fd    9   aaaaaaaaaaaaaa

[:01000012] fd: 9 disconnect            #socketclient退出后
[:01000019] 9 agent quit                #对应的agent服务也退出了
[:01000019] KILL self

13.8 skynet自带网关服务

​ skynet有自带的网关服务代码,在service/gate.lua,这个网关代码写的足够好了,完全可以直接使用,

使用这个网关服务的例子代码在examples/watchdog.lua,watchdog.lua中使用到examples/agent.lua文件。

三个代码需要结合则去看。启动watchdog服务的代码在main.lua。

​ 它们的大致关系:

skynet框架应用 (十三) 网关服务_第1张图片

1、newservice watchdog启动看门狗服务,lua start conf给看门狗发送lua消息start参数为conf。

2、newservice gate启动网关服务,lua open conf给网关发送lua消息open参数为 conf。

3、new connection客户端连接进来。

4、lua socket open fd addr给看门狗发送lua消息socket参数为open fd addr。

5、newservice agent启动一个客户端代理服务,lua start {gate,fd, watchdog}给代理发送lua消息start参数为{gate,fd, watchdog}。

6、lua forward fd给网关发送lua消息forward参数为fd。 (到此,连接建立成功,可以进行网络通信了)

7、send requst客户端发送请求给看门狗。

8、client 1 msg sz 看门狗把请求转发成client消息,session为1。

9、send response 代理直接给客户端发送响应。

按照从1到9的依次去查看代码,需要强调的是:

1、gate主要负责的是client连接创建与断开以及接受到消息转发给agent。

2、watchdog主要负责gate的创建,agent的创建与退出。

3、agentt主要负责接受gate转发的请求,处理业务,然后直接把应答发送给client。

你可能感兴趣的:(skynet)