TCP 是基于数据流的,但一般需要以带长度信息的数据包来做数据交换,skynet 提供了一个通用模板 lualib/snax/gateserver.lua 来启动一个网关服务器,gateserver 做的就是这个工作。
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注册网络事件处理
gateserver.start(handler)
main.lua
local skynet = require "skynet"
skynet.start(function()
skynet.error("Server start")
local gateserver = skynet.newservice("myservice/mygateserver") --启动前面写的网关服务
skynet.call(gateserver, "lua", "open", { --需要给网关服务发送open消息,来启动监听
port = 8002, --监听的端口
maxclient = 64, --客户端最大连接数
nodelay = true, --是否延迟TCP
})
skynet.error("gate server setup on", 8002)
skynet.exit()
end)
socketclient.c
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 128
#define SERV_PORT 8002
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[])
{
struct sockaddr_in servaddr;
int sockfd;
char buf[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(SERV_PORT);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
pthread_t thid;
pthread_create(&thid, NULL, readthread, (void*)sockfd);
while (fgets(buf, MAXLINE, stdin) != NULL)
write(sockfd, buf, strlen(buf));
close(sockfd);
return 0;
}
脚本的功能是:从控制台读取用户的输入,并将输入的数据发送到服务端。
编译测试脚本:
gcc socketclient.c -lpthread -o socketclient
/root/skynet-master/skynet ./config/config
启动客户端,并在控制台输入“123”,回车,终止客户端进程。
可以看到:客户端连接与断开连接,网关服务都有收到,但 handler.message 并没有执行。这是由于 snax.gateserver 基于TCP协议包装了一个两字节数据长度的协议,而客户端 socketclient 并没有按照这种协议发送数据。
gateserver 应用协议是基于TCP协议的简单封装,前两个字节表示数据包的长度len(不计算这两个表示长度的字节),高字节在前低字节在后(大端序),后面紧跟len字节数的数据。例如:
\x00\x05 \x31\x32\x33\x34\x35
| |
len data
由于数据包的长度用两个字节表示,因此 data 部分最大可到 65535 个字节,这种协议包方式可以解决TCP粘包的问题。
因此,若想通过TCP与gateserver通信,则必须要按照这种协议进行组包解包。否则gateserver无法识别。
封包/解包TCP网路数据可使用skynet.netpack库:
local netpack = require "skynet.netpack"
--打包数据str,返回一个C指针msg,sz,申请内存
netpack.pack(str)
--解包数据,返回一个lua的字符串,会释放内存
netpack.tostring(msg, sz)
修改第一章节网关服务 handler.message() 函数:
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)
注意:
msg是一个指向一块堆空间的C指针,即使不进行任何操作,始终需要调用 skynet.trash 来释放底层的内存。
修改3.1小节中,客户端测试脚本发包部分:
#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;
}
解决方法:在 linux 上以编辑模式打开脚本(vim),可能看到比 windows 多出了一些字符或者格式完全乱套,删除异常字符并对格式重新编辑后即可解决。
这是因为代码里有非法 Ascll 码字符、非法空格所造成的,解决方法同问题一,用 vim 命令打开脚本后,删除非法字符即可。