SRC(Send Recv Cmds)语言是一种描述各种命令格式和字段的简单语言,主要用于快速开发网络测试程序,同时可以作为命令文档来查阅。关于SRC语言的详细语法,请参考“SRC Language”,本文主要介绍如何使用SRC语言和编译器。
SRC编译器
完整的SRC编译器发布版本包括:
s 可执行文件srcc
s 静态库libsrc.a
s 动态库libsrc.so
s 头文件SRC_language.h。
对于没有调用“FUN”函数的SRC源文件,可以使用srcc程序直接编译并运行。
使用srcc
srcc程序的使用方式为:
srcc FILE
其中,FILE是SRC源文件的文件名。目前,srcc一次只能读取一个SRC源文件进行处理。
使用src库
如果命令需要使用FUN函数,那么必须使用src库。
使用src库的代码需要包含头文件“SRC_language.h”,里面声明了如下的函数和变量:
s SRC_version
当前SRC编译器的版本信息,例如“1.0.67”;
s void SRC_init()
初始化SRC编译环境。在使用其他库函数之前,请先调用SRC_init()函数。
s bool SRC_compile(const std::string & filename)
编译SRC源文件“filename”,返回值表示是否成功。如果返回值为false,应该退出程序,并按照错误提示修改SRC源代码。
s bool SRC_run()
运行编译的SRC源代码,包括发送和接收命令数据,返回值表示是否成功。如果返回false,应该退出程序,根据错误提示检查SRC源代码或对方服务器的可用性。
s bool SRC_register_function(const std::string & func_name,
bool (*func_ptr)(std::vector<char> & src,std::vector<char> & dst))
注册自定义的函数。SRC_register_function函数接受2个参数:第一个是字符串,表示自定义函数的名字,这个名字在SRC源代码里可以作为FUN函数的第一个参数;第二个参数是函数指针,指向用户自定义的函数,函数的格式是:
bool func(std::vector<char> & src, std::vector<char> & dst)
其中,参数src是由SRC库提供的当前命令的数据,而参数dst是函数进行处理后返回给SRC库的数据。函数返回值表示是否处理成功,注意:
1. 如果自定义函数返回true,SRC库会使用参数dst里的数据作为命令数据,并继续后面的处理;
2. 如果自定义函数返回false,SRC库报告一个运行时错误,如果这个错误不足以终止程序,那么SRC使用参数src里的数据作为命令数据,继续后面的处理。
下面是一个使用src库的例子。
#include <SRC_language.h>
//安装SRC库之后,SRC_language.h会在/usr/local/include目录下
bool func(std::vector<char> & src, std::vector<char> & dst)
{ //自定义函数
dst.swap(src);
return true;
}
int main(int argc, const char ** argv)
{
SRC_init();
if(!SRC_register_function("my_func", func))
return 1;
if(!SRC_compile(argv[1]))
return 1;
if(!SRC_run())
return 1;
return 0;
}
这里的用户自定义函数func不对数据进行任何修改,只是简单的转移给dst。在SRC源代码里,用户可以通过:
FUN(my_func)
调用自定义函数func。当然,实际中的用户自定义函数可以做更多的事情。
编译上述代码,在链接时加入“-lsrc”标志,例如:
g++ -o test.out test.cpp -lsrc
应用SRC语言
编写SRC代码其实是一件很简单的事情,你只要对照着命令的最初说明,写出所有的字段就可以了。而且我相信,当你习惯了SRC语言的简洁明快,你就在也不会想着自己写测试程序了。
我们从一个简单的例子开始。
例1:简单命令
假设我们要测试一个服务器,发送的命令格式如下:
类型 | 变量名 | 名称 |
U16 | magic | 魔数(0x11DE) |
U8 | version | 协议版本号(1) |
U16 | type | 包类型(0) |
U32 | seq_num | 包序号 |
U32 | body_len | 后续包体长度 |
U32 | program_version | 程序版本 |
在正常情况下,服务器会回复一个如下格式的命令:
类型 | 变量名 | 名称 |
U16 | magic | 魔数(0x11DE) |
U8 | version | 协议版本号 |
U16 | type | 包类型(1) |
U32 | seq_num | 包序号 |
U32 | body_len | 后续包体长度 |
U32 | error_code | 错误码,当为0时才有后面的字段 |
U64 | tablet_id | 子表ID |
String | table_name | 表名称 |
String | start_row | 起始行关键字 |
String | end_row | 结束行关键字 |
U16 | status | 状态(位) |
String | host_name | 主机名 |
U16 | port | 端口号 |
U64 | start_code | 起始码(启动时间) |
虽然这2个命令看起来很复杂,但是在SRC语言里,它们都是最简单的命令。我们根据命令的字段说明,可以直接写出以下的SRC代码:
TCP("192.168.13.89",20080) //指定服务器地址和端口
NBO //指定命令编码解码的字节序
CMD Query SEND //发送的命令,以及每个字段的测试值
U16 magic := 0x11DE
U8 version := 1
U16 type := 0
U32 seq_num := 1234
U32 body_len
BEGIN(body_len)
U32 program_version := 1
END CMD
CMD Resp RECV //服务器应该回复的命令
U16 magic
U8 version
U16 type
U32 seq_num
U32 body_len
U32 error_code == 0 //断言声明
U64 tablet_id
STR table_name
STR start_row
STR end_row
U16 status
STR host_name
U16 port
U64 start_code
END CMD
根据协议说明,只有当回复命令的error_code字段等于0的时候,才有后面的字段,所以使用了断言声明来保证。
下面是使用srcc程序编译运行这段代码后的结果:
SEND command 'Query' data =
0000h: 11 DE 01 00 00 00 00 04 D2 00 00 00 04 00 00 00 ; ................
0010h: 01 ; .
RECV command 'Resp'
magic = 4574
version = 1
type = 1
seq_num = 1234
body_len = 71
error_code = 0
tablet_id = 0
table_name = (19).METADATA_1ST_LEVEL
start_row = (0)
end_row = (0)
status = 1
host_name = (12)58.61.39.244
port = 20088
start_code = 1227369904024166
RECV data =
0000h: 11 DE 01 00 01 00 00 04 D2 00 00 00 47 00 00 00 ; ............G...
0010h: 00 00 00 00 00 00 00 00 00 00 00 00 13 2E 4D 45 ; ..............ME
0020h: 54 41 44 41 54 41 5F 31 53 54 5F 4C 45 56 45 4C ; TADATA_1ST_LEVEL
0030h: 00 00 00 00 00 00 00 00 00 01 00 00 00 0C 35 38 ; ..............58
0040h: 2E 36 31 2E 33 39 2E 32 34 34 4E 78 00 04 5C 49 ; .61.39.244Nx../I
0050h: 53 23 2A 66 ; S#*f
srcc程序打印出了实际发送的数据,服务器回应的数据,以及回应命令里每个字段的值。
例2:HTTP头信息
有些命令使用HTTP协议进行传输,于是需要在命令数据的前面加上HTTP头信息,而这些信息里,“Content-Length:”字段是难点。
假设这次我们需要测试的服务器,接受如下格式的查询命令:
类型 | 变量名 | 名称 |
U16 | version | 协议版本号(>=100,100以上使用加密协议传输) |
U16 | cmdtype | 命令类型(161) |
U32 | seq | 包序号 |
U32 | body_len | 后续包体长度 |
String | file_hash | 文件的hash标识 |
String | client_hash | 客户端的hash标识 |
String | peer_id | 客户端标识 |
回应的命令是如下格式:
类型 | 变量名 | 名称 |
U16 | version | 协议版本号(>=1) |
U16 | cmdtype | 命令类型(162) |
U32 | seq | 包序号 |
U32 | body_len | 后续包体长度 |
String | file_hash | 文件的hash标识 |
String | client_hash | 客户端的hash标识 |
String数组 | peer_id_array | peer资源列表 |
命令虽然比前一个例子简单,但是这次的命令需要使用HTTP协议传输,即需要编码和解码HTTP头信息。对于编码HTTP头信息,SRC语言采用非常简单的方法解决这个问题,即自由变量和流输出声明。SRC代码如下:
TCP("127.0.0.1",9531) //指定服务器地址和端口
NBO //指定命令编码解码的字节序
CMD QueryCommand SEND //发送的命令
//HTTP wrap
RAW http1 := "POST http://127.0.0.1:12345/ HTTP/1.1/r/n/
Content-Length: "
DEF U32 pack_len //使用自由变量计算整个数据包的长度
RAW pack_len_str << pack_len //使用流输出变量将长度写入HTTP头信息里
RAW http2 := "/r/nContent-Type: application/octet-stream/r/n/
Connection: Close/r/n/r/n"
BEGIN(pack_len) //开始计算整个数据包的长度
//cmd data
U16 ver := 100
U16 cmd_type:(161)
U32 seq = 1234
U32 len(0)
BEGIN(len)
STR file_hash := "http://www.xunlei.com"
STR client_hash := UNHEX("1A2B3C4D5E6F")
STR peer_id := "ABCDEF"
END CMD
发送命令的确比前一个例子复杂了些,总共增加了5条语句,来实现添加HTTP头信息,但是相比通过C++程序实现,这里的复杂度简直是小儿科。
服务器返回的命令同样也会使用HTTP传输,所以我们还需要解码HTTP头信息。SRC语言使用断言声明和流输入声明解决这个问题。
CMD RespCommand RECV
//http wrap
RAW http1 == "HTTP/1.1 200 OK/r/n/
Content-Length: " //使用断言声明读取RAW字符串
RAW pack_len_str >> U32 //使用流输入声明读取数值字符串
RAW http2 == "/r/nContent-Type: Application/
/octet-stream/r/nConnection: Close/r/n/r/n"
//cmd data
U16 ver >= 100
U16 cmd_type == 162
U32 seq
U32 len
len < 32k
U8 result
!result
STR fileHash;
STR clientHash;
STR[] peer_id_array; //array
END CMD
接收命令使用3条语句,来解码HTTP头信息。
下面是使用srcc程序编译运行这段代码后的结果:
SEND command 'QueryCommand' data =
0000h: 50 4F 53 54 20 68 74 74 70 3A 2F 2F 31 32 37 2E ; POST http://127.
0010h: 30 2E 30 2E 31 3A 31 32 33 34 35 2F 20 48 54 54 ; 0.0.1:12345/ HTT
0020h: 50 2F 31 2E 31 0D 0A 43 6F 6E 74 65 6E 74 2D 4C ; P/1.1..Content-L
0030h: 65 6E 67 74 68 3A 20 35 37 0D 0A 43 6F 6E 74 65 ; ength: 57..Conte
0040h: 6E 74 2D 54 79 70 65 3A 20 61 70 70 6C 69 63 61 ; nt-Type: applica
0050h: 74 69 6F 6E 2F 6F 63 74 65 74 2D 73 74 72 65 61 ; tion/octet-strea
0060h: 6D 0D 0A 43 6F 6E 6E 65 63 74 69 6F 6E 3A 20 43 ; m..Connection: C
0070h: 6C 6F 73 65 0D 0A 0D 0A 00 64 00 A1 00 00 04 D2 ; lose.....d......
0080h: 00 00 00 2D 00 00 00 15 68 74 74 70 3A 2F 2F 77 ; ...-....http://w
0090h: 77 77 2E 78 75 6E 6C 65 69 2E 63 6F 6D 00 00 00 ; ww.xunlei.com...
00a0h: 06 1A 2B 3C 4D 5E 6F 00 00 00 06 41 42 43 44 45 ; ..+<M^o....ABCDE
00b0h: 46 ; F
RECV command 'RespCommand'
http1 = (33)HTTP/1.1 200 OK/r/nContent-Length:
pack_len_str = (2)88
http2 = (63)/r/nContent-Type: Application/octet-stream/r/nConnection: Close/r/n/r/n
ver = 100
cmd_type = 162
seq = 1234
len = 76
result = 0
fileHash = (21)http://www.xunlei.com
clientHash = (6)/032+<M^o
peer_id_array.size() = 3
peer_id_array[0] = (6)ABCDEF
peer_id_array[1] = (9)ABCDEFabc
peer_id_array[2] = (9)ABCDEFefg
RECV data =
0000h: 48 54 54 50 2F 31 2E 31 20 32 30 30 20 4F 4B 0D ; HTTP/1.1 200 OK.
0010h: 0A 43 6F 6E 74 65 6E 74 2D 4C 65 6E 67 74 68 3A ; .Content-Length:
0020h: 20 38 38 0D 0A 43 6F 6E 74 65 6E 74 2D 54 79 70 ; 88..Content-Typ
0030h: 65 3A 20 41 70 70 6C 69 63 61 74 69 6F 6E 2F 6F ; e: Application/o
0040h: 63 74 65 74 2D 73 74 72 65 61 6D 0D 0A 43 6F 6E ; ctet-stream..Con
0050h: 6E 65 63 74 69 6F 6E 3A 20 43 6C 6F 73 65 0D 0A ; nection: Close..
0060h: 0D 0A 00 64 00 A2 00 00 04 D2 00 00 00 4C 00 00 ; ...d.........L..
0070h: 00 00 15 68 74 74 70 3A 2F 2F 77 77 77 2E 78 75 ; ...http://www.xu
0080h: 6E 6C 65 69 2E 63 6F 6D 00 00 00 06 1A 2B 3C 4D ; nlei.com.....+<M
0090h: 5E 6F 00 00 00 03 00 00 00 06 41 42 43 44 45 46 ; ^o........ABCDEF
00a0h: 00 00 00 09 41 42 43 44 45 46 61 62 63 00 00 00 ; ....ABCDEFabc...
00b0h: 09 41 42 43 44 45 46 65 66 67 ; .ABCDEFefg
忽略其他数据,我们只关心发送命令里的“Content-Length:”字段部分是否正确,事实说明,“Content-Length: 57”是正确的。同时,服务器的正确回复和srcc的正确解码,也证明了SRC语言的能力。
例3:加密解密
虽然到目前为止,SRC语言已经支持了大部分命令的编码和解码,但是还不够,SRC语言需要支持更多的命令,包括加密和解密的命令。
对命令数据进行加密或者解密,或者其他处理,都可以抽象为同一类型的操作:对输入数据进行处理,并把结果放进输出数据里。所以SRC语言使用了如下形式的C++函数来代表这一类型的操作:
bool func(std::vector<char> & src, std::vector<char> & dst)
其中函数参数和返回值的意义在“使用src库”一节里已有详细介绍。要使用自定义的函数,必须使用SRC编译器提供的库,这是因为:
1. srcc编译程序srcc没有内置任何自定义函数;
2. 自定义函数往往涉及用户机密,不应该包含在SRC公共库里;
3. 只有用户才最清楚自定义函数的细节
用户根据自己的需求,写好自定义函数,然后使用SRC库生成新的SRC编译器衍生品,就可以编译运行调用自定义函数的SRC代码了。当然,这个衍生品也包含原始SRC编译器的一切功能。
这次我们需要测试的命令,与“例2:HTTP头信息”里的相同,只是这次我们把版本号字段version的值设置为101。根据协议说明,当版本号字段大于100时,需要采用加密协议传输。
加密和解密函数的细节不必追究,这里给出函数的大概模样:
bool aes_encrypt(std::vector<char> & src,std::vector<char> & dst)
{
…… //加密函数实现
}
bool aes_decrypt(std::vector<char> & src,std::vector<char> & dst)
{
…… //解密函数实现
}
定义了这2个函数,我们的主程序main里还需要做如下事情:
#include "SRC_language.h" //包含SRC头文件
int main(int argc, const char ** argv)
{
SRC_init(); //初始化SRC编译环境
if(!SRC_register_function("aes_encrypt",aes_encrypt))
//注册加密函数
return 1;
if(!SRC_register_function("aes_decrypt",aes_decrypt))
//注册解密函数
return 1;
if(!SRC_compile(argv[1])) //编译SRC源文件
return 1;
if(!SRC_run()) //运行代码,发送和接收命令
return 1;
return 0;
}
将这段代码编译,与SRC库链接成新的程序后,我们得到了SRC编译程序的一个衍生品,这个衍生品支持“aes_encrypt”和“aes_decrypt”这2个自定义函数。
接下来我们需要写出发送和接收加密命令的SRC源代码,也许你认为可能会很复杂,恰恰相反,这回的SRC源代码比起例2,总共只有三处修改:
TCP("127.0.0.1",9531) //指定服务器地址和端口
NBO //指定命令编码解码的字节序
CMD QueryCommand SEND //发送的命令
//HTTP wrap
RAW http1 := "POST http://127.0.0.1:12345/ HTTP/1.1/r/n/
Content-Length: "
DEF U32 pack_len //使用自由变量计算整个数据包的长度
RAW pack_len_str << pack_len //使用流输出变量将长度写入HTTP头信息里
RAW http2 := "/r/nContent-Type: application/octet-stream/r/n/
Connection: Close/r/n/r/n"
BEGIN(pack_len) //开始计算整个数据包的长度
//cmd data
U16 ver := 101 //这是第一处修改,将版本号设置为101
U16 cmd_type:(161)
U32 seq = 1234
U32 len(0)
BEGIN(len)
STR file_hash := "http://www.xunlei.com"
STR client_hash := UNHEX("1A2B3C4D5E6F")
STR peer_id := "ABCDEF"
FUN(aes_encrypt) //这是第二处修改,调用自定义加密函数
END CMD
CMD RespCommand RECV //接收的命令
//http wrap
RAW http1 == "HTTP/1.1 200 OK/r/n/
Content-Length: " //使用断言声明读取RAW字符串
RAW pack_len_str >> U32 //使用流输入声明读取数值字符串
RAW http2 == "/r/nContent-Type: Application/
/octet-stream/r/nConnection: Close/r/n/r/n"
//cmd data
U16 ver >= 100
U16 cmd_type == 162
U32 seq
U32 len
len < 32k
FUN(aes_decrypt,len) //第三处修改,接收后续len字节数据,并调用解密函数
U8 result
!result
STR fileHash;
STR clientHash;
STR[] peer_id_array; //array
END CMD
运行这段SRC代码的结果与例2大致相同,这里不再赘述。
例4:结构体数组
从1.1版本开始,SRC语言增加了一个重要的特性:支持结构体数组。有一些命令,不能仅仅当作简单字段的集合,它有自己的逻辑结构,特别是这些逻辑结构内部又会有更小的逻辑结构。SRC语言可以通过一层层解构这些结构,把有些命令还原成为简单字段的集合。但是当命令里含有逻辑结构的数组时,这种解构就变得不可能了。所以必须有一种描述这种结构体数组的方法,这就是SRC语言里的“BEGIN ARRAY”函数和“END ARRAY”函数。
我们用一个复杂的命令,来展示SRC语言的真正实力。发送的命令格式如下:
字段 | 类型 | 描述 |
ProtocolVersion | U32 | 协议版本号 |
Seq | U32 | 命令的序列号 |
CommandLength | U32 | 命令长度,指跟在这个字段后面的所有命令字段长度的总和 |
QUERY_PLUS | U8 | 59 |
Peer ID | STR |
|
CID | STR | 三段CID |
File size | U64 | 文件大小 |
GCID | STR | Gcid |
Peer capability flag | U8 |
|
Interal_ip | U32 | 内部IP地址 |
NAT_TYPE | U32 | 网络类型,类型检测 |
Level_resource | U8 | 期望等级资源个数 |
服务器的回复命令格式如下:
字段 | 类型 | 引入版本 | 描述 |
ProtocolVersion | U32 | 53 | 协议版本号 |
Seq | U32 | 53 | 命令的序列号 |
CommandLength | U32 | 53 | 命令长度,指跟在这个字段后面的所有命令字段长度的总和 |
QUERYRESP | U8 | 53 | 60 |
Result | U8 | 53 | 是否成功: 0:成功;其他值代表错误码 失败时忽略其后任何字段 |
CID | STR | 53 | 三段CID |
File size | U64 | 53 | 文件大小 |
GCID | STR | 53 | 全文CID |
Level_resource | U8 | 53 | 实际等级资源个数 |
PeerResource | ARRAY | 53 | 资源数组 |
数组每个元素的格式如下:
自定义类型PeerResource
字段 | 类型 | 引入版本 | 描述 |
Len | U32 | 53 | 数组元素长,指后面字段的长度和 |
PeerID | STR | 53 | 资源的peerID |
File_name | STR | 53 | 资源名称 |
U32ernalIP | U32 | 53 | 资源的内部IP |
Port | U16 | 53 | 资源监听的Tcp端口 |
CapabilityFlag | UU8 | 53 |
|
所有对命令需要使用HTTP协议传输,并且使用加密解密算法。此外,最难部分是回复命令最后的PeerResource结构体数组。
我不再重复写出使用SRC库的C++代码,直接给出命令的SRC源代码,除了前面说过的HTTP头定义方式,自定义函数的使用方式,真正的革新只有2条语句:
STR server_ip := "192.168.13.112"
U16 server_port := 80
TCP(server_ip,server_port)
HBO
CMD Query SEND //发送的命令
//http wrap
RAW http1 := "POST http://127.0.0.1:12345/ HTTP/1.1/r/n/
Content-Length: "
DEF U32 pack_len
RAW pack_len_str << pack_len //stream output
RAW http2 := "/r/nContent-Type: application/octet-stream/
/r/nConnection: Close/r/n/r/n"
BEGIN(pack_len)
//cmd data
U32 ProtocolVersion := 53 //协议版本号
U32 Seq := 1234 //命令的序列号
U32 CmdLength //命令长度
BEGIN(CmdLength)
U8 CmdType := 59 //命令类型
STR PeerID := "3456789012345"
STR Cid := UNHEX("B2F4F2869EAEEFEC231FAF81E46AD085ACB0742F ")
U64 FileSz := 0
STR Gcid := UNHEX("")
U8 PeerCapability := 106
U32 Inter_ip := IP NBO("192.168.85.45") //内部IP地址
U32 NAT_TYPE := 39 //网络类型,类型检测
U8 Level_resource := 100 //期望等级资源个数
FUN(aes_encrypt)
END CMD
发送命令没有什么新意,全是前面介绍过的内容。
CMD Resp RECV //接收的命令
//http wrap
RAW http1 == "HTTP/1.1 200 OK/r/nContent-Length: "
RAW pack_len_str >> U32 //stream input
RAW http2 == "/r/nContent-Type: application/octet-stream/
/r/nConnection: Close/r/n/r/n"
//cmd data
U32 ProtocolVersion
U32 Seq
U32 CmdLength
FUN(aes_decrypt,CmdLength)
U8 CmdType == 60
U8 Result == 0
STR Cid
U64 FileSz
STR Gcid
U8 Level_resource
BEGIN ARRAY //革新之一:读取数组元素个数,并标识结构体的起始位置
U32 rc_len_1
STR PeerID
STR Filename
U32 InterIP
PRINT(" ",IP NBO(InterIP))
U16 Port
U8 CapabilityFlag
END ARRAY //革新之二:标识结构体的结束位置。如果需要,重复读取结构体
END CMD
使用2条语句,解决了复杂的命令结构体数组,这就是SRC语言遵循的设计原则:用最少的代码,解决最多的问题。这段SRC代码需要使用包含了自定义函数“aes_encrypt”和“aes_decrypt”的SRC编译器衍生品来执行,如果回复的命令里,真的有结构体数组元素,那么SRC编译器会打印出如下的信息:
…… //省略前面的信息
ARRAY_SIZE = 28
rc_len_1[0] = 31
PeerID[0] = (16)00016C035AC3V024
Filename[0] = (0)
InterIP[0] = 1677830336
192.168.1.100
Port[0] = 80
CapabilityFlag[0] = 107
rc_len_1[1] = 31
PeerID[1] = (16)00016C063E674AM4
Filename[1] = (0)
InterIP[1] = 1749973885
125.123.78.104
Port[1] = 23516
CapabilityFlag[1] = 106
rc_len_1[2] = 31
PeerID[2] = (16)00016C9A04A8AKM4
Filename[2] = (0)
InterIP[2] = 2641227388
124.238.109.157
Port[2] = 80
CapabilityFlag[2] = 107
…… //省略后面的信息
SRC编译器先打印出数组的总长度,然后一次显示每个元素的每个字段,并以下标值区分。
后记
本文介绍了SRC语言和编译器的使用,通过实例展示了SRC语言的能力。如果你还在使用C++,Python,Java或者其他语言编写网络测试程序,那么不妨试试SRC。我相信一旦你知道了SRC语言的魅力,就会在一切可能的情况下尽量使用它。
目前SRC编译器是开源项目,所有源代码和发布版本文件都可从以下网址获取:
http://code.google.com/p/client-model/
最后欢迎大家提出宝贵的建议和意见,请将邮件发送至daidodo AT gmail.com,谢谢!