Quake3 网络协议规范
【此文约写于2002年。这个是从搜索引擎的cache里翻出来,不完整。有完整版的请联系我,发给我。谢谢。】
Quake III Arena
网络协议规范(非官方)
Version 48
Document Revision #2
By Dunhill Hwang<dunhill@dusk3d.com>
2002 年1月9日
0. 声明 2
1. 介绍 2
2. 概述 3
2.0. 概念 3
2.1. 体系结构 4
2.2. 消息格式 5
3. 带外消息 6
3.0. getinfo (Client->Server) 6
3.1. infoResponse(Server->Client) 7
3.2. getstatus(Client->Server) 8
3.3. statusResponse(Server->Client) 8
3.4. getchallenge(Client->Server) 9
3.5. challengeResponse(Server->Client) 9
3.6. connect(Client->Server) 9
3.7. connectResponse(Server->Client) 10
3.8. disconnect (Server->Client) 11
3.9. echo (Server->Client and Client->Server) 11
3.10. keyAuthorize (Server->Client) 11
3.11. motd (Server->Client) 11
3.12. print (Server->Client) 12
3.13. getserversResponse (Server->Client) 12
4. 一般消息 12
4.0. 服务器到客户端的消息 13
4.1. 客户端到服务器端的消息 14
4.2. svc_bad 14
4.3. svc_nop 14
4.4. svc_gamestate 14
4.5. svc_configstring 14
4.6. svc_baseline 14
4.7. svc_serverCommand 14
4.8. svc_download 15
4.9. svc_snapshot 15
4.10. clc_bad 15
4.11. clc_nop 15
4.12. clc_move 15
4.13. clc_moveNoDelta 15
4.14. clc_clientCommand 15
4.15. 加密与解密 15
4.16. 消息片断加密和解密 16
5. 结束语 16
6. 相关信息 16
0. 声明
本文档介绍Quake III Arena的v48的网络通讯协议规范。但是,这些信息并不是从idsoftware官方的来的,所以我并不保证这里面的信息是准确的。另外,目前Q3A 1.31所使用的协议是v67,它与这个文档所公布的协议有很大的不同,特别是在加密方面。所以,这份文档不会对Q3A这个游戏的安全方面不会有太大的影响,希望不会给idsoftware带来麻烦。如果确实有这个方面的问题,请联系作者。
本文档是我在开发DuSK 3D时,逐步分析总结的来的。国内的开发者应该学学,我想国内没有人会精心设计一个如此完美和认真的网络通讯协议的。
本文档不可用于商业用途。除此之外,使用不受限制。
1. 介绍
应该注意的是,要完全理解本协议的内容,不但需要有一定的网络编程方面的基础知识,还要对Q3A的运行机制和本协议所应该达到的目标有一定的了解。
Q3A是一个高性能网络三维游戏引擎。它天然的支持网络对战。它具有高效率、高安全性的特点。
高效率
众所周知是,FPS游戏以激烈和极强的操作性而著称。当通过网络对战时,要求在低速连接(比如 Modem用户)时能够流畅的玩游戏,并且能够在服务器端合客户端保持高度的同步。举个例子来说,一般在MUD游戏和MMORPG游戏中,服务器的心跳 (帧速率)有0.5Hz~1Hz就可以了,但是在FPS游戏中,服务器一般在10Hz以上,Q3A和DuSK 3D默认是20Hz,所以通讯量和同步性的要求就很高,这对引擎的开发者是一个挑战。
高安全性
在一个网络游戏中,通讯的安全和服务器的安全是相当重要的。网络协议应该有着良好的网络通讯加密技术,保证通讯的安全。并且,应该充分考虑应付Cracker的攻击。另外,虽然对于游戏作弊不能只从通讯协议和网络模块来解决,但是应该尽量考虑这方面的问题。
2. 概述
Q3A和DuSK 3D支持UDP和IPX网络协议,并且支持SOCK5代理。对于通过NAT标准(主要使用在局域网中的代理服务器中)的用户也有一定的考虑。本文档主要描述支持UDP方面的信息。
不采用TCP是因为TCP太慢了。 TCP耗费了很多代宽在ACK、同步上面。
Q3A默认使用UDP端口号27960,如果被占用则使用27961,以此类推。端口号可以通过net_port指定。如果连接到本地服务器(指同一进程内),则并不使用任何网络协议,而是直接memcpy。如果本地服务器启动了,则本地客户端只能连接到本地服务器。如果客户端连接到了外地服务器(指非同一进程内的任何服务器),则本地服务器一定被关闭。反之则不然。所以,Q3A不需要客户端和服务器端各占一个端口,而只要一个端口就可以了。
2.0. 概念
下面先搞清楚几个概念:操作命令,消息,消息包,网络包。
操作命令(Operate command)
网络通讯传送的是操作命令。操作命令是在逻辑上相对于具有一定完整性的数据。
消息 (Message)
游戏的客户端和服务器端的通讯总是通过消息的形式来进行的。一个消息总往往可以封装多个操作命令在里面。但是,消息并不等价与低层网络协议(比如UDP)的网络包。一个消息最大可以到16K字节。没有理由需要更大的消息。
消息包(Message Packet)
消息在传输时,并不一定是一次传送整个消息的。当消息足够大时,总是拆分成一个个的消息片断来传送,可以称为消息包。一个消息片断最大可以到1400字节。这个大小是为了保证在以太网上传送时,一次可以传送一个包而不用再分成片断,要知道,以太网一帧的大小最大是1512个字节。
网络包 (Network Packet)
数据包通过底层网络协议(比如UDP、IPX)的传送时,会被封装成相应协议的网络包。我们不用太关心这个。如果熟悉网络原理的话,对这个应该很了解。
下面的这些基础知识应该先讲述。
信息字符串 (Information String, InfoString)
InfoString是一种以字符串组织信息的形式,在Q3A网络协议中和Q3A引擎内部,经常使用这种方式来组织信息。InfoString中保存着以名字值(namevalue)配对的信息,名字和信息以反斜杠”格开,和其他的namevalue配对也以反斜杠格开。比如,有两个名字值配对,分别是nameK7,ping50,那么,构成的InfoString将会是"ameK7ping50",引号是不需要的。name和value中不能包含字符反斜杠”、分号’ ;’、引号’"’。
控制台变量 (console variables, cvar)
Q3A把很多信息都存储在一种叫做控制台变量的东西里面。它可以通过游戏的控制台交互式更改,并且也可以从配置文件(config file)中(一般是以cfg为后缀的文本文件)读取,有的控制台变量也会写入配置文件中。控制台变量具有以下几种标志。
/* ========================================================== CVARS (console variables) Many variables can be used for cheating purposes, so when cheats is zero, force all unspecified variables to their default values. ========================================================== */ #define CVAR_ARCHIVE 1 // set to cause it to be saved to vars.rc // used for system variables, not // for player specific configurations #define CVAR_USERINFO 2 // sent to server on connect or change #define CVAR_SERVERINFO 4 // sent in response to front end requests #define CVAR_SYSTEMINFO 8 // these cvars will be duplicated on all // clients #define CVAR_INIT 16 // don't allow change from console at // all, but can be set from the command // line #define CVAR_LATCH 32 // will only change when C code next does // a Cvar_Get(), so it can't be changed // without proper initialization. // modified will be set, even though the // value hasn'tchanged yet #define CVAR_ROM 64 // display only, cannot be set by user at all #define CVAR_USER_CREATED 128 // created by a set command #define CVAR_TEMP 256 // can be set even when cheats are // disabled, but is not archived #define CVAR_CHEAT 512 // can not be changed if cheats are disabled #define CVAR_NORESTART 1024 // do not clear when a cvar_restart is issued
2.1. 体系结构
本协议与引擎的工作原理密切相关,所以有必要介绍一点,才能有助于理解。引擎分为客户端和服务器端两个部分。客户端负责处理用户的输入,转换成游戏中的命令,比如走跑开枪等等,发送给服务器,并且,客户端负责根据当前的游戏状态,显示图形的画面,播放声音等。服务器端处理客户端发来的命令,执行完成之后,把当前游戏状态刷新给客户端。
但是,实际上远远不止这么简单。服务器端和客户端是异步的,他们并不运行在同一帧速率。服务器端总是以恒定帧速率运行,这个有控制台变量sv_fps来指定。而客户端的帧速率则跟用户的机器性能密切相关,但是不会超过com_maxfps所指定的值。多个客户端可能会跑在不同的帧速率上。同时,Q3A又是操作性特别强的游戏,对时间相当敏感。它如何处理多个客户端和服务器端之间游戏状态的同步呢?比如说,modem的 ping值一般在200ms左右,一个玩加开了一枪之后,0.2秒之后命令才到达服务器,在有0.2秒之后才把状态返回,总共就有0.4秒钟。在Q3A这么激烈的游戏中,0.4秒可是致命的时延。Q3A如何解决这个问题?
首先介绍一个原则,"服务器为准原则"。游戏的状态是以服务器端的为准的,而不是任何一个客户端。
Q3A引擎采用多种技术来提高网络通讯的性能,提高游戏的响应性和操作性。
时间戳
所有的网络通讯消息上都加有时间戳,并且物理特性的计算都是以这个时间戳为准的。所以,在ping值为200ms的连接上,虽然消息会延迟0.2秒,但是接受方还是认为你是在0.2秒之前发送的。
预测
在一般情况下(比如在古代的引擎Quake 1中),客户端收集到用户命令后发送给服务器,此后就在等待服务器返回新的游戏状态。这是很笨的。在Q3A中,客户端不会傻等,而会预测可能的游戏状态,其实预测状态所用的代码跟服务器端的代码是一样的,所以服务器端的状态和客户端的状态往往是一致的。如果确实不一致,则"服务器为准原则"将生效。
压缩
为了提高网络通讯速度,降低带宽,Q3A中采用了压缩的技术。这并不是指用一些压缩算法来直接压缩数据。而是指,在传送游戏状态数据时,只传送改变了的游戏状态,而不是全部发送过来。一般来说,这个叫做Delta技术。
但是,技术不能解决所有问题。在玩Q3A的时候,拥有高速连接的玩家肯定比低速连接的玩家占有优势。
2.2. 消息格式
前面已经说过,通讯是以消息为单位来进行的。消息分为两种,带外消息(Outband Message)和一般消息(Normal Message)。带外消息是指客户端可服务器端还没有连接上,不在正常的玩的时候的一些通讯消息。比如查询服务器信息,申请连接服务器等消息和相应的响应消息。而一般消息则是指游戏进行中,客户端已经可以玩的时候所进行通讯的消息。
消息中的信息一律采用小端 (Little Endian)字节序,而不是采用TCP/IP的网络字节序,这是因为考虑到大多数用户都是用x86的电脑。所以,如果机器不是小端的,收到的消息中的信息要转换成机器的字节序。
所有消息都由一个顺序号(sequence number,简称seq)开头,顺序号是4个字节的整数。
带外消息的顺序号总是-1,即0xFFFFFFFF。后面跟的是消息的内容,一般是ASCII码文本命令。一般消息是不会这样开头的,所以可以区分开来。带外消息不需要加密,也不需要分段。
一般消息的顺序号是真正意义上的顺序号,从1开始顺序增加。客户端发送到服务器端的顺序号和服务器端发送到客户端的顺序号没有必然联系,他们是独立增加的。如果顺序号是负数,或者说seq最高位被置1,则表示这是分段后的一个消息片断,接受端应该等接收到所有这个seq的片段后,在重组出原来的消息。比如说,前一个消息顺序号是1,现在有一个消息大于1400字节,要分段,那么,现在的消息号是0×80000002,所有的片段都用这个顺序号,接受方收到之后在自行重组。顺序号达到0×70000000大小后将轮回到1。
3. 带外消息
前面已经说过,带外消息是顺序号-1后面跟着是命令。带外消息不需要加密和分段。这一节就介绍这些命令。注意,所有的命令都是大小写不敏感的。有的命令有参数,有的命令没有参数。命令和参数之间总是用一个空格格开。
消息格式定义采用伪BNF来表示。通用格式中的定义:
<QuotedInfoString> ::= <DQUOTE><InfoString><DQUOTE> // 带双引号的信息字符串 <InfoString> ::= <string> // 信息字符串 <string> ::= <char> | <char><string> // 字符串 <numstring> ::= any integer string // 字符串表示的数字 <char> ::= any of the 128 ASCII characters except <CR>,<LF>, <DQUOTE>, , ; <SP> ::= 0x20 // 空格 <CR> ::= 0x0D // 回车 <LF> ::= 0x0A // 换行 <DQUOTE> ::= 0x22 // 双引号 <seq> : 0xFFFFFFFF // 带外消息的顺序号
3.0. getinfo (Client->Server)
格式:
<seq> getinfo <SP> <challenge>
<challenge> ::= <string>
说明:
getinfo用来获得当前服务器运行的一些信息。它可以带一个参数,叫做challenge的字符串。服务器会把这个参数原样返回。服务器收到这个命令后就会返回infoResponse命令给客户端。
这个命令往往是客户端发送的第一个命令。客户端启动时,会向局域网广播这个命令,并且会向主服务器(Master Server,官方的维持活动服务器列表的服务器)发送这个命令。所有接收到这个命令的服务器都会相应 infoResponse给客户端,单人模式运行的服务器除外。
有的工具软件也会使用这个命令来获得服务器信息,比如GameSpy软件,Quake3的 IE Plugin服务器察看软件等等。
举例:
00000020 FF FF FF FF 67 65 ....ge 00000030 74 69 6E 66 6F 20 78 78 78 tinfo.xxx
3.1. infoResponse(Server->Client)
格式:
<seq> infoResponse <LF> <serverinfo>
<serverinfo> ::= <InfoString>
说明:
infoResponse是对getinfo命令的响应。它返回服务器的一些信息给客户端。返回的信息serverinfo封装成 InfoString(参看2.0节的描述)的形式,作为infoResponse命令的参数发送给客户端。
serverinfo中包含了一下信息:
challenge 客户端发送getinfo时发送过来的challenge信息
protocol 服务器运行的网络协议号,比如48
hostname 服务器的名字
mapname 服务器当前地图的名字
clients 服务器当前连接的客户数目
sv_maxclients 服务器允许的作大客户数
gametype 服务器的游戏类型,如以下定义
gametype 定义:
typedef enum { GT_FFA, // free for all GT_TOURNAMENT, // one on one tournament GT_SINGLE_PLAYER, // single player ffa //-- team games go after this -- GT_TEAM, // team deathmatch GT_CTF, // capture the flag GT_1FCTF, GT_OBELISK, GT_HARVESTER, GT_MAX_GAME_TYPE } gametype_t;
pure:服务器是否纯服务器(pure server,校验更多,更加安全),即服务器控制台变量sv_pure的值。
举例:
00000020 FF FF FF FF 69 6E ....in 00000030 66 6F 52 65 73 70 6F 6E 73 65 0A 5C 63 68 61 6C foResponse.chal 00000040 6C 65 6E 67 65 5C 78 78 78 5C 70 72 6F 74 6F 63 lengexxxprotoc 00000050 6F 6C 5C 34 38 5C 68 6F 73 74 6E 61 6D 65 5C 6E ol48hostname 00000060 6F 6E 61 6D 65 5C 6D 61 70 6E 61 6D 65 5C 71 33 onamemapnameq3 00000070 64 6D 31 5C 63 6C 69 65 6E 74 73 5C 31 5C 73 76 dm1clients1sv 00000080 5F 6D 61 78 63 6C 69 65 6E 74 73 5C 38 5C 67 61 _maxclients8ga 00000090 6D 65 74 79 70 65 5C 30 5C 70 75 72 65 5C 30 metype pure
3.2. getstatus(Client->Server)
格式:
<seq> getstatus <SP> <challenge>
<challenge> ::= <string>
说明:
getstatus用来获得当前服务器运行的一些信息。它和getinfo一样,可以带一个参数,叫做challenge的字符串。服务器会把这个参数原样返回。服务器收到这个命令后就会返回statusResponse命令给客户端。
就像getinfo,有的工具软件也会使用这个命令来获得服务器信息,比如GameSpy软件,Quake3的IE Plugin服务器察看软件等等。
举例:
3.3. statusResponse(Server->Client)
格式:
<seq> statusResponse <LF> <serverstatus> [ <LF> <clientinfo> ] [ <LF> <clientinfo> ]
<serverstatus> ::= <InfoString>
<clientinfo> ::= <code> <SP> <ping> <SP> <name>
<code> ::= <numstring>
<pint> ::= <numstring>
<name> ::= <string>
说明:
statusResponse是对getstatus命令的响应。它返回服务器的一些信息给客户端。返回的信息serverstatus以InfoString(参看2.0节的描述)的形式,作为 statusResponse命令的参数发送给客户端。主要是当前连接到服务器中的玩家的状态,比如名字,得分等等。
serverstatus 中包含以下信息:
challenge:客户端发送getstatus时发送过来的challenge信息
clientinfo是各个客户端信息,包括得分,ping值,名字。本消息可以有多个clientinfo信息,他们都用换行格开。
举例:
3.4. getchallenge(Client->Server)
格式:
<seq> getchallenge
说明:
从服务器获得一个挑战号 (challenge number),是一个数字。这是客户端要连接到服务器必经的一步。以后的一般消息的加密算法都跟这个挑战号相关。它的目的主要是让服务器可以应付拒绝服务(DoS, Denial of Service)攻击,DoS攻击往往发送一些源地址不存在的或者非法的包给服务器,而采用了 challenge的方式后,它就要求客户端必须有一个有效的ip地址。
举例:
00000020 FF FF FF FF 67 65 ....ge 00000030 74 63 68 61 6C 6C 65 6E 67 65 tchallenge
3.5. challengeResponse(Server->Client)
格式:
<seq> challengeResponse <SP> <challenge>
<challenge> ::= <numstring>
说明:
返回一个挑战号给客户端,是随机生成的一个大整数。客户端应端应搞保存这个数字,因为游戏的消息加密算法会用到这个挑战号。
举例:
00000020 FF FF FF FF 63 68 ....ch 00000030 61 6C 6C 65 6E 67 65 52 65 73 70 6F 6E 73 65 20 allengeResponse. 00000040 33 36 31 34 34 35 36 31 30 361445610
3.6. connect(Client->Server)
格式:
<seq> connect <SP> <userinfo>
<userinfo> ::= <QuotedInfoString>
说明:
申请连接到服务器。参数是用户信息,包括名字、模型等等。它以带引号的InfoString的格式传送给服务器。其实,它就是把客户端的控制台变量中所有具有
#define CVAR_USERINFO 2 // sent to server on connect or change
标志的变量都一次性的压入userinfo这个 InfoString中。
userinfo中有下列值:
teamtask 未知
predictItems 预测 item的状态
sex 性别:male,female
handicap 让血,你最大的健康值。上手指导下手时可以让血
color 颜色,任务模型都可以选择不同的颜色。好像Q3A没有实现
team_headmodel 组队模式的时候人头的模型名称
team_model 组队模式的时候人物模型的名称
headmodel 人头的模型名称
model 人物模型的名称
snaps 未知
rate 连接速度,单位为字节/秒
name 玩家的名字
protocol 协议版本号,比如48
qport Q3A内部端口号,前面有解释
challenge 挑战后,challengeResponse返回的值。
如果用Q3A的引擎来开发新的游戏,这个列表可能会被增加或者删除项。
举例:
00000020 FF FF FF FF 63 6F ....co 00000030 6E 6E 65 63 74 20 22 5C 74 65 61 6D 74 61 73 6B nnect." eamtask > 00000040 5C 30 5C 63 67 5F 70 72 65 64 69 63 74 49 74 65 cg_predictIte > 00000050 6D 73 5C 31 5C 73 65 78 5C 6D 61 6C 65 5C 68 61 ms1sexmaleha 00000060 6E 64 69 63 61 70 5C 31 30 30 5C 63 6F 6C 6F 72 ndicap100color 00000070 5C 32 5C 74 65 61 6D 5F 68 65 61 64 6D 6F 64 65 2 eam_headmode 00000080 6C 5C 2A 6A 61 6D 65 73 5C 74 65 61 6D 5F 6D 6F l*james eam_mo 00000090 64 65 6C 5C 6A 61 6D 65 73 5C 68 65 61 64 6D 6F deljamesheadmo 000000A0 64 65 6C 5C 73 61 72 67 65 5C 6D 6F 64 65 6C 5C delsargemodel 000000B0 73 61 72 67 65 2F 72 65 64 5C 73 6E 61 70 73 5C sarge/redsnaps 000000C0 32 30 5C 72 61 74 65 5C 32 35 30 30 30 5C 6E 61 20ate25000 a 000000D0 6D 65 5C 4B 37 5C 70 72 6F 74 6F 63 6F 6C 5C 34 meK7protocol4 000000E0 38 5C 71 70 6F 72 74 5C 34 36 32 39 36 5C 63 68 8qport46296ch 000000F0 61 6C 6C 65 6E 67 65 5C 33 36 31 34 34 35 36 31 allenge36144561 00000100 30 22 0"
3.7. connectResponse(Server->Client)
格式:
<seq> connectResponse
说明:
服务器接受了客户端的连接申请。
举例:
00000020 FF FF FF FF 63 6F ....co 00000030 6E 6E 65 63 74 52 65 73 70 6F 6E 73 65 nnectResponse
3.8. disconnect (Server->Client)
格式:
<seq> disconnect
说明:
服务器拒绝了客户端的连接申请。可能是很多原因,比如客户端的 ip在服务器的拒绝列表中,双方的网络协议号不一致等等。
举例:
3.9. echo (Server->Client and Client->Server)
格式:
<seq> echo <stringmsg>
<stringmsg> ::= <string>
说明:
回显。接受方会把这个消息原样不动的转发回去。用途不明。
举例:
3.10. keyAuthorize (Server->Client)
格式:
<seq> keyAuthorize
说明:
未知。
举例:
3.11. motd (Server->Client)
格式:
<seq> motd
说明:
未知。
举例:
3.12. print (Server->Client)
格式:
<seq> print <stringmsg>
<stringmsg> ::= <string>
说明:
服务器发送这个消息给客户端,客户端会在控制台打印出来
举例:
3.13. getserversResponse (Server->Client)
格式:
<seq> connectResponse
说明:
未知。
举例:
4. 一般消息
如前所述,一个消息中可以封装多个操作命令。为了减低带宽,Q3A的通讯协议采用了基于位(bit)的传送,而不象很多协议一般的基于字节,所以采用只采用伪BNF很难描述。所以,为了方便说明问题,我们先定义以下的伪C语言的函数声明。
// 开始读取一个消息,并把消息的当前读取指针设为0 void MSG_BeginReading( msg_t *msg ); // 从当前指向的位开始读取一定数量的位,并把值看位一个32位符号整数返回 int MSG_ReadBits( msg_t *msg, int bits ); // 从当前指向的位开始读取一个八位符号整数,并把值看位一个整数返回 int MSG_ReadChar( msg_t *msg ); // 从当前指向的位开始读取八位无符号整数,并把值看位一个整数返回 int MSG_ReadByte( msg_t *msg ); // 从当前指向的位开始读取一个16位符号整数,并把值看位一个整数返回 int MSG_ReadShort( msg_t *msg ); // 从当前指向的位开始读取一个32位符号整数位,并把值看位一个整数返回 int MSG_ReadLong( msg_t *msg ); // 从当前指向的位开始读取一个浮点数,32位小端IEEE标准格式 float MSG_ReadFloat( msg_t *msg ); // 从当前指向的位开始读取一个' '结尾的字符串 char* MSG_ReadStringLine( msg_t *msg ); // 从当前指向的位开始读取一个以0结尾的字符串 char* MSG_ReadString( msg_t *msg );
Q3A的一般消息中的命令种类并不是太多,服务器端到客户端的真正有效的命令只有六种,客户端到服务器端只有三种。但是,每种命令包含的信息量比较大。
先看看常量定义:
// protocol.h -- communications protocols #define PROTOCOL_VERSION 48 // the svc_strings[] array in cl_parse.c should mirror this // // server to client // enum svc_ops_e { svc_bad, svc_nop, // to keep connection? svc_gamestate, svc_configstring, // [short] [string] only in gamestate messages svc_baseline, // only in gamestate messages svc_serverCommand, // [string] to be executed by client game module svc_download, // [short] size [size bytes] svc_snapshot }; // // client to server // enum clc_ops_e { clc_bad, clc_nop, // to keep connection? clc_move, // [usercmd_t] clc_moveNoDelta, // [usercmd_t] clc_clientCommand // [string] message };
4.0. 服务器到客户端的消息
格式:
<seq><currentClientCmdNumber><cmd_0><cmd_0_data>…<cmd_n><cmd_n_data>
其中:
<seq> ::= 32bits singed int
<currentClientCmdNumber> ::= 32bits singed int
<cmd_0>…<cmd_n> ::= 1 byte, always is one of svc_ops_e value
<cmd_0_data>…<cmd_n_data> ::= data for <cmd_0>…<cmd_n>
说明:
服务器到客户端的消息格式如上所示。<seq>是顺序号,<currentClientCmdNumber>是当前客户端的命令号。后面接着各个操作命令和它们的数据。
<currentClientCmdNumber>应该详细讲一下。客户端会发送一些文本信息的命令给服务器端,这是通过客户端的操作命令clc_clientCommand来达到这个目的的,这些命令在服务器端被执行。服务器端执行时,先在引擎中(quake3.exe) 中查找执行此种命令的入口,如果没有则转发给qagame.qvm(或者是dll)来执行。服务器端指定的这个<currentClientCmdNumber>就是告诉客户端,它是用这个号码的客户端文本命令来加密这条消息的,所以,客户端就同样用这个号码的文本命令来解密这条消息。
加密和解密算法是一样的,也就是说,它是自逆的。它的C伪代码是这样的:
/* ============ ServerToClient_Msg_Encode_Decode ============ */ void ServerToClient_Msg_Encode_Decode( msg_t *msg ) { int seq, cmdNum, cnt, ofs; byte mask; char *cmd; char c; seq = Msg_ReadLong( msg ); cmdNum = Msg_ReadLong( msg ); cmd = getClientCmdStringByNumber( cmdNum ); mask = seq ^ challenge; // challenge就是getChallenge得到的挑战号 ofs = 0; for( cnt = msg->readcount; cnt < msg->cursize; cnt++ ) { if( !cmd[ofs] ) { ofs = 0; } c = cmd[ofs]; if( c > 127 || c == '%' ) <br />c = '.'; mask ^= c << (cnt & 1); <br />msg->data[cnt] ^= mask; ofs++; } }
后面是操作命令和操作命令的数据。操作命令是一个字节的数,总是svc_ops_e定义的一种。每种操作命令都有其自身的格式,这些在后面有详细定义。
4.1. 客户端到服务器端的消息
格式:
<seq><net_qport><serverid><serverMessageNum><currentServerCmdNumber><cmd_0><cmd_0_data>…<cmd_n><cmd_n_data>
其中: <seq> ::= 32bits singed int
<qport> ::= 16bits unsigned int
<serverMessageNum> ::= 32bits singed int
<currentServerCmdNumber> ::= 32bits singed int
<cmd_0>…<cmd_n> ::= 1 byte, always is one of clc_ops_e value
<cmd_0_data>…<cmd_n_data> ::= data for cmd_x
<seq是顺序号>。
<net_qport>是指 Q3A内部用的端口号。其实,这个说话是很让人迷惑的。本质上说,这只不过是游戏启动时产生的一个随机数。它主要用来分辨一个NAT代理服务器后面的各个客户端。
<serverid>是服务器的id。也是一个随机生成的数。当客户端接收到了svc_gamestate之后,才会知道服务器的id。如果这个id不和服务器的一致,服务器将会重新发送sv_gamestate这个操作命令给客户端。
<serverMessageNum> 是指客户端接收到的最新的服务器消息的seq。
<currentServerCmdNumber>的含义与服务器发送给客户端的<currentClientCmdNumber>很相似。服务器端也会发送一些文本信息的命令给客户端,这是通过服务器端的操作命令 svc_serverCommand来达到这个目的的,这些命令在客户端被执行。客户端执行时,先在引擎中(quake3.exe)中查找执行此种命令的入口,如果没有则转发给cgame.qvm(或者是dll)来执行。指定的这个<currentClientCmdNumber>就是告诉服务器端,它是用这个号码的服务器端文本命令来加密这条消息的,所以,服务器端就同样用这个号码的文本命令来解密这条消息。
加密算法和服务器端发送给客户端的时候的加密算法差不多。它也是自逆的。如下:
/* ============ ClientToServer_Msg_Encode_Decode ============ */ void ServerToClient_Msg_Encode_Decode( msg_t *msg ) { int seq, net_qport, serverid, cmdNum, cnt, ofs; byte mask; char *cmd; char c; seq = Msg_ReadLong( msg ); net_qport = Msg_ReadShort( msg ); serverid = Msg_ReadLong( msg ); cmdNum = Msg_ReadLong( msg ); cmd = getServerCmdStringByNumber( cmdNum ); mask=seq^serverid^challenge; // challenge就是getChallenge得到的挑战号 ofs = 0; for( cnt = msg->readcount; cnt < msg->cursize; cnt++ ) { if( !cmd[ofs] ) { ofs = 0; } c = cmd[ofs]; if( c > 127 || c == '%' ) c = '.'; mask ^= c << (cnt & 1); msg->data[cnt] ^= mask; ofs++; } }
后面是操作命令和操作命令的数据。操作命令是一个字节的数,总是clc_ops_e定义的一种。每种操作命令都有其自身的格式,这些在后面有详细定义。
// to be continue…
链接:PDF Version

Recent Comments