最近在做一个CS约战平台的东西,实现类似 HLSW 读取CS服务器信息的工具。
研究了一下CS1.6服务器udp
协议的东西。分享一下。
对UDP协议不熟悉,可能有
错误的地方请大家指点。
这个是协议的的全文
http://developer.valvesoftware.com/wiki/Server_Queries
协议提供了 5 个请求:
* The server responds to 5 queries:
* A2A_PING
* Ping the server.
// ping 服务器
* A2S_SERVERQUERY_GETCHALLENGE
* Returns a challenge number for use in the player and rules query.
//challenge number 应该是用于表示一个用户请求。
* A2S_INFO
* Basic information about the server.
//服务器信息
* A2S_PLAYER
* Details about each player on the server.
//列出服务器所有用户
* A2S_RULES
* The rules the server is using.
//服务器的规则
* Queries should be sent in UDP packets to the listen port of the server, which is typically port 27015.
//UDP 的数据包,服务器默认端口是 27015
五个请求的内容是:
private static final byte[] A2S_INFO_BYTE =
new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
0x54,//T
'S', 'o', 'u', 'r', 'c', 'e', ' ', 'E', 'n', 'g', 'i', 'n', 'e', ' ', 'Q', 'u', 'e', 'r', 'y',
0x00};
private static final byte[] A2S_PLAYER_BYTE =
new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
0x55,//U
(byte) 0xFF,(byte) 0xFF,(byte) 0xFF,(byte) 0xFF,//四位 0xFF 可能被替换为challenge number
0x00};
private static final byte[] A2S_RULES_BYTE =
new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
0x56,//V
(byte) 0xFF,(byte) 0xFF,(byte) 0xFF,(byte) 0xFF,//四位 0xFF 可能被替换为challenge number
0x00};
private static final byte[] A2S_SERVERQUERY_GETCHALLENGE_BYTE =
new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
0x57,//W
0x00};
private static final byte[] A2A_PING_BYTE =
new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF,
0x69,//i
0x00};
服务器返回的数据包格式:
数据包除去IP/UDP 头部,最大长度为 1400 bytes。
如果单个数据包传输数据则开始的四个字节是 -1 (0xFF,0xFF,0xFF,0xFF)后面是数据。
如果多个数据包传输数据则开始的四个字节是 -2 (0xFE,0xFF,0xFF,0xFF),后面是四个字节的Request ID(对一次请求返回的多个数据包是相同的且唯一),之后是一个字节的数据包大小和编号,低位的4bit是表示请求返回的包的总数,高位的4bit是表示当前包的编号(从零开始)(协议中说 Source Engine 引擎,会用两个字节来标识,但未
发现这种服务器,而且后面有两个字节的 Split Length 也没有发现)。第0个数据包开头有四个字节 (0xFF,0xFF,0xFF,0xFF),其他数据包没有。
socket.receive(datapack);
Frame f = new Frame(HelpUtil.getSubBytes(datapack.getData(), 0, datapack.getLength()));
if(f.isHasNext()){
log.debug("has next!");
//if(type == SOURCE_OR_SHIP_SERVERS) throw new UnsupportedOperationException(" do not have implient for source agent! ");
byte[][] bts = new byte[f.getDataLength()][];
bts[f.getIndex()] = f.getData();
for(int i=1; i < bts.length; i++){
socket.receive(datapack);
Frame next = new Frame(HelpUtil.
getSubBytes(datapack.getData(), 0, datapack.getLength()));
if(!next.isHasNext() || next.getId() != f.getId()){
i--;
log.warn("one package frame is discard!");
//不应该丢弃包,应该根据第一个字节判断返回的类型。
continue;
}else{
bts[next.getIndex()] = next.getData();
if(i >= bts.length ) break;
}
}
ByteArrayOutputStream array = new ByteArrayOutputStream();
for(int i=0; i<bts.length; i++){
array.write(bts[i]);
}
服务器返回的数据起始的一个字节都是标识位(除去开头的 0xFF,0xFF,0xFF,0xFF),这个是后来才发现的,所以现在的设计上有很大问题。如:ping 第一个字节是 'j',A2S_PLAYER 是 'D' ......
如果服务器返回的数据中是字符串 则是以 UTF-8
编码, 并且以 0x00 做为字符串结尾标识。
A2A_PING 请求:
用户发现服务器是不是可以连接(或检查服务器类型)。
第一个字节 是 'j'(0x6A)
Goldsource servers 引擎的服务器返回
0x6A,0x00
Source servers 引擎的服务器返回(这种服务器暂时未发现)
0x6A,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
A2S_SERVERQUERY_GETCHALLENGE 请求:
第一个字节 是 'A' (0x41)
接下来是 4 个字节的 challenge number 用于 A2S_PLAYER 和 A2S_RULES 这个请求的时候替换请求的后四个字节。
很多服务器不支持该请求。
A2S_INFO 请求:
区分两种服务器
第一个字节分别是 'I','m'(貌似区分是
正版服务器,还是盗版服务器,这个不确定)
m 类型:
int i = 0, t;
info.setType(bts[i]); //m / I
t = i + 1;
if(info.getType() == 'm'){
i = HelpUtil.find(bts, t, (byte) 0x00);//ip:port
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00);//server name
if(i >= 0){
info.setName(new String(HelpUtil.getSubBytes(bts, t, i-t), DEFAULT_CHARSET));
}
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00);//map
if(i >= 0){
info.setMap(new String(HelpUtil.getSubBytes(bts, t, i-t), DEFAULT_CHARSET));
}
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00);//Game Directory
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00);//Game Description
if(i >= 0){
info.setDescription(new String(HelpUtil.getSubBytes(bts, t, i-t), DEFAULT_CHARSET));
}
t = i + 1;
info.setCurrentPlayers(bts[t]);
t += 1;
info.setMaxPlayers(bts[t]);
t += 1; //version
t += 1; //Dedicated
t += 1; //OS
info.setNeedPassword(bts[t++] != 0);//Password
t += 1; //IsMod
i = t;
if(bts[t] == 0x01){
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00); //URLInfo
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00); //URLDL
t = i + 1; //Nul
t += 4; //ModVersion
t += 4; //ModSize
t += 1; //SvOnly
t += 1; //ClDLL
}
t += 1; //Secure
t += 1;//Number of bots
byte end = bts[t];//不应该溢出
}
对于'I'类型
if(info.type == 'I'){
int version = bts[t++];//version
i = HelpUtil.find(bts, t, (byte) 0x00);//server name
if(i >= 0){
info.setName(new String(HelpUtil.getSubBytes(bts, t, i-t), DEFAULT_CHARSET));
}
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00);//Map
if(i >= 0){
info.setMap(new String(HelpUtil.getSubBytes(bts, t, i-t), DEFAULT_CHARSET));
}
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00);//Map
if(i >= 0){//Game Directory
//info.setMap(new String(HelpUtil.getSubBytes(bts, t, i-t), DEFAULT_CHARSET));
}
t = i + 1;
i = HelpUtil.find(bts, t, (byte) 0x00);//Map
if(i >= 0){//Game Description
info.setDescription(new String(HelpUtil.getSubBytes(bts, t, i-t), DEFAULT_CHARSET));
}
t = i + 1;
HelpUtil.toShort(bts[t++],bts[t++]); //AppID
info.setCurrentPlayers(bts[t++]);
info.setMaxPlayers(bts[t++]);
t++; //Number of bots
t++; //Dedicated byte 'l' for listen, 'd' for dedicated, 'p' for SourceTV
t++; //Host operating system. 'l' for Linux, 'w' for Windows
t++; //Password
t++; //Secure
//后面的数据不清楚做什么用的 所以忽略掉了。
}else{
throw new RuntimeException("donot support type [" + info.type + "]");
}
A2S_PLAYER 请求:
玩家的列表,名字,得分,
在线时间
int t = 0;
byte type = bts[t++];//type Should be equal to 'D' (0x44)
int len = bts[t++];
for(int i=0; i<len; i++){
Player p = new Player();
p.setIndex(bts[t++]);//序号
int u = HelpUtil.find(bts, t, (byte) 0x00);
p.setName(new String(HelpUtil.getSubBytes(bts, t, u - t), DEFAULT_CHARSET));//name
t = u + 1;
p.setKill(Integer.reverseBytes(HelpUtil.toInt(bts[t++],bts[t++],bts[t++],bts[t++])));
//Number of kills this player has
p.setConnectedTime(
Float.intBitsToFloat(//浮点类型 秒数,不清楚为什么用浮点数
Integer.reverseBytes(//比较奇怪为什么是反向的字节
HelpUtil.toInt(bts[t++],bts[t++],bts[t++],bts[t++])))); //(x)
//The time in seconds this player has been connected
list.add(p);
}
A2S_RULES 请求:
这个结构比较简单 字符串的键值对
int t = 0;
int type = bts[t++];//Should be equal to 'E' (0x45)
short len = HelpUtil.toShort(bts[t++], bts[t++]);//The number of rules reported in this response
// top is error!
for(short i=0; ; i++){
int u = HelpUtil.find(bts, t, (byte) 0x00);
if(u == -1) break;
String key = new String(HelpUtil.getSubBytes(bts, t, u-t));
t = u + 1;
u = HelpUtil.find(bts, t, (byte) 0x00);
String value = new String(HelpUtil.getSubBytes(bts, t, u-t));
t = u + 1;
map.put(key, value);
}
源码SVN地址:https://lineblog.googlecode.com/svn/trunk/
目录:httpAnalysis/cs/ 下
[完]
转载请保留原文地址: http://lchshu001.iteye.com/blog/1207956 , 谢谢