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)
三、測試
3.1 客戶端測試腳本
socketclient.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#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
3.2 啓動網關
/root/skynet-master/skynet ./config/config
3.3 測試
啓動客戶端,並在控制檯輸入“123”,回車,終止客戶端進程。
可以看到:客戶端連接與斷開連接,網關服務都有收到,但 handler.message 並沒有執行。這是由於 snax.gateserver 基於TCP協議包裝了一個兩字節數據長度的協議,而客戶端 socketclient 並沒有按照這種協議發送數據。
四、gateserver 應用協議
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)
六、改良
6.1、修改網關服務
修改第一章節網關服務 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 來釋放底層的內存。
6.2、修改客戶端測試腳本
修改3.1小節中,客戶端測試腳本發包部分:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#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;
}
6.3、測試
啓動網關服務與客戶端:
問題一、lua 腳本從 windows 拷貝到 linux 後,運行時出現錯誤:unexpected symbol near ‘<\194>’。
解決方法:在 linux 上以編輯模式打開腳本(vim),可能看到比 windows 多出了一些字符或者格式完全亂套,刪除異常字符並對格式重新編輯後即可解決。
問題二、C 腳本在 linux 上編譯是,出現錯誤:socketclient.c:32:1: error: stray 鈥榎200鈥in program。
這是因爲代碼裏有非法 Ascll 碼字符、非法空格所造成的,解決方法同問題一,用 vim 命令打開腳本後,刪除非法字符即可。