skynet 提供了一个通用模板 lualib/snax/gateserver.lua 来启动一个网关服务器,通过 TCP 连接和客户端交换数据。
TCP 基于数据流,但一般我们需要以带长度信息的数据包的结构来做数据交换。gateserver 做的就是这个工作,把数据流切割成包的形式转发到可以处理它的地址。
local gateserver = require "snax.gateserver" local handler = {} --必须提供一张表,表里面定义connect、message等相关回调函数 -- register handlers here gateserver.start(handler) --网关服务的入口函数
示例代码: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)
可以使用普通服务创建方式来创建一个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并没有按照这种协议发送。
gateserver应用协议是基于TCP协议做了一层简单的封装,前两个字节表示数据包的长度len(不计算这两个表示长度的字节),高字节在前低字节在后(大端序),后面紧跟len字节数的数据。例如:
\x00\x05 \x31\x32\x33\x34\x35 | | len data
由于只用两字节表示数据长度,那么这个包的data最大只能是65535字节。这种协议包方式可以解决TCP粘包的问题,也是TCP通信当中最常用的一种应用层协议包定义方式。
所以如果想通过TCP与gateserver通信必须要按照这种协议进行组包解包。否则gateserver肯定是不识别的。
打包与解包TCP网路数据可以使用skynet.netpack库
local netpack = require "skynet.netpack" --使用netpack --打包数据str,返回一个C指针msg,sz,申请内存 netpack.pack(str) --解包数据,返回一个lua的字符串,会释放内存 netpack.tostring(msg, sz)
下面我们通过改写socketclient.c文件来组包发送数据:
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
上面的实验中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来释放底层的内存。
网关服务最重要的任务就是控制客户端连接数,避免大量客户登录到这个服务上。
修改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 #第三个客户端连接成功后,马上关闭
--如果你希望在监听端口打开的时候,做一些初始化操作,可以提供 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
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关闭收到反馈。
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
前面几节我们讲到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
skynet有自带的网关服务代码,在service/gate.lua,这个网关代码写的足够好了,完全可以直接使用,
使用这个网关服务的例子代码在examples/watchdog.lua,watchdog.lua中使用到examples/agent.lua文件。
三个代码需要结合则去看。启动watchdog服务的代码在main.lua。
它们的大致关系:
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。