RakNet 已经成功地在Android平台上测试成功。RakNet的文档很多,实现起来很简单,下面对Raknet功能细节进行详细了解。
结构文件 | 描述 |
DS_BinarySearchTree.h | 二叉搜索树,以及AVL平衡二叉搜索树 |
DS_BPlusTree.h | B+树,用于快速查询,删除,和插入 |
DS_BytePool.h | 返回某个大小门限的数据块,减少内存碎片 |
DS_ByteQueue.h | 用于读写字节的队列 |
DS_Heap.h | 堆数据结构体,包括最小堆和最大堆 |
DS_HuffmanEncodingTree.h | 胡夫曼编码树,以给定的频率表用于查找最小按位显示 |
DS_HuffmanEncodingTreeFactory.h | 创建胡夫曼编码树实例 |
DS_HuffmanEncodingTreeNode.h | 胡夫曼编码树中的节点 |
DS_LinkedList.h | 标准链接链表 |
DS_List.h | 动态数组(有时不适宜地成为向量)。双向时作为一个栈 |
DS_Map.h | 关联数组,每一个元素带有分类键值的有序列表 |
DS_MemoryPool.h | 分配和释放固定大小的重用的实例,用于减少内存碎片 |
DS_Multilist.h | 将列表,栈和游戏列表整合成为一个带有通用接口的类 |
DS_OrderedChannelHeap.h | 最大堆返回一个基于关系权重的节点的相关信道,用于带有属性的任务调度 |
DS_OrderedList.h | 通过快排以一个任意键值排序的列表 |
DS_Queue.h | 用数组实现的标准队列 |
DS_QueueLinkedList.h | 用一个链表实现的标准队列 |
DS_RangeList.h | 存储一个列表的数字值,数字是顺序的,以一个序列代表他们。当存储许多序列值时比较有用。 |
DS_Table.h | 带有行列,以及表上的操作 |
DS_Tree.h | 非循环图 |
DS_WeightedGraph.h | 带有权重边得图,用于使用Dijkstra的算法进行路由 |
项目源码总共有268个文件,其中头文件有157个,C++中头文件一般都是描述类的,而实现都放在.cpp文件中,笔者将根据头文件列出相应的类名以及作用。
头文件 | 描述 |
_FindFirst.h | 查找数据结构,函数有:_findfirst、_findnext、_findclose。 |
AutopatcherPatchContext.h | 补丁枚举结构,成员有补丁哈希值、文件、失败原因、通知类。 |
AutopatcherRepositoryInterface.h | 补丁服务器接口,可以获取补丁文件、日期、错误等消息。 |
Base64Encoder.h | 实现一个编码函数Base64Map。 |
BitStream.h | 定义了一个可写入、读取比特流的类。 |
CCRakNetSlidingWindow.h | |
CCRakNetUDT.h | 封装了UDT阻塞控制。 |
CheckSum.h | 生成验证信息。 |
CloudClient.h | 顾名思义,云端客户端,实现拓扑结构网络结构。 |
CloudCommon.h | 云端辅助类,包含了云端信息生成、检索等功能。 |
CloudServer.h | 云服务端,存储了客户端的信息并提供跨服务检索信息。 |
CommandParserInterface.h | 命令解析接口。 |
ConnectionGraph2.h | 连接信息图,提供检索结构。 |
ConsoleServer.h | 服务器远程控制台实现。 |
DataCompressor.h | 数据压缩,就两个方法。 |
DirectoryDeltaTransfer.h | 目录文件传输,一般用于补丁、皮肤等。 |
DR_SHA1.h | 安全散列算法,用于记录文件是否修改。 |
DS_Hash.h | 哈希数据结构 |
DS_ThreadsafeAllocatingQueue.h | 线程安全队列,维护多线程。 |
DynDNS.h | 动态域名 |
EmailSender.h | 发送email |
EmptyHeader.h | 头信息,空文件 |
EpochTimeToString.h | 时间转为字符串 |
Export.h | 导出 |
FileList.h | 文件列表 |
FileListNodeContext.h | 文件节点句柄 |
FileListTransfer.h | 文件传输 |
FileListTransferCBInterface.h | 文件传输接口 |
FileOperations.h | 文件操作类 |
FormatString.h | 格式输出 |
FullyConnectedMesh2.h | 连接网络插件,负责连接所有的节点。 |
Getche.h | 获取字符 |
Gets.h | 获取字符 |
GetTime.h | 获取时间 |
gettimeofday.h | 获取日期 |
GridSectorizer.h | 网格 |
HTTPConnection.h | 封装了Http连接 |
HTTPConnection2.h | 同上 |
IncrementalReadInterface.h | 文件增加部分读取 |
InternalPacket.h | 定义了内部包结构 |
Itoa.h | 整形转换 |
Kbhit.h | 敲击键盘,获取响应的数据。 |
LinuxStrings.h | 字符串操作 |
LocklessTypes.h | 数据锁,增加和减少操作。 |
LogCommandParser.h | 日志命令解析 |
MessageFilter.h | 消息过滤 |
MessageIdentifiers.h | 包含了一个巨大的枚举数据,表示了RakNet用于发送消息的标识符,例如断开连接通知。 |
MTUSize.h | 定义MTU消息大小,最大值、最小值。 |
NativeFeatureIncludes.h | 定义本地功能,宏定义需要哪些功能。 |
NativeFeatureIncludesOverrides.h | 空文件 |
NativeTypes.h | 本地基本类型定义 |
NatPunchthroughClient.h | 穿透客户端配置 |
NatPunchthroughServer.h | 穿透服务端配置 |
NatTypeDetectionClient.h | 客户端匹配穿透方式 |
NatTypeDetectionCommon.h | 穿透方式 |
NatTypeDetectionServer.h | 服务端匹配穿透方式 |
NetworkIDManager.h | 网络标识管理 |
NetworkIDObject.h | 网络标识对象 |
PacketConsoleLogger.h | 网络日志控制,传入和传出过程日志解析。 |
PacketFileLogger.h | 文件数据包日志 |
PacketizedTCP.h | Tcp数据包 |
PacketLogger.h | 数据包日志 |
PacketOutputWindowLogger.h | 数据包输出日志 |
PacketPool.h | 空 |
PacketPriority.h | 枚举,包含优先级和可靠性。 |
PluginInterface2.h | 扩展插件接口,例如:声音插件、补丁更新插件。 |
PS3Includes.h | 空 |
PS4Includes.h | 空 |
Rackspace.h | 辅助管理服务器 |
RakAlloca.h | 定义申请内存函数 |
RakAssert.h | 空 |
RakMemoryOverride.h | 定义申请内存函数 |
RakNetCommandParser.h | 网络命令解析 |
RakNetDefines.h | 预定义 |
RakNetDefinesOverrides.h | 空 |
RakNetSmartPtr.h | 引用计数 |
RakNetSocket.h | 内部套接字 |
RakNetSocket2.h | 内部套接字 |
RakNetStatistics.h | 相关网络信息的统计数据 |
RakNetTime.h | 定义时间类型 |
RakNetTransport2.h | 安全的控制台连接 |
RakNetTypes.h | 定义了在RakNet中使用的结构体,包括SystemAddress结构体——系统的唯一标识符,以及当你需要接收数据或需要发送数据时,API返回给你的数据包。 |
RakNetVersion.h | 网络版本 |
RakPeer.h | 连接管理,例如流量控制。 |
RakPeerInterface.h | 连接管理接口。 |
RakSleep.h | 睡眠函数 |
RakString.h | 字符串的实现,比std::string速度高4.5倍 |
RakThread.h | 封装线程 |
RakWString.h | 封装宽字符操作 |
Rand.h | 随机数 |
RandSync.h | 随机数 |
ReadyEvent.h | 定义事件,端对端的事件。 |
RefCountedObj.h | 引用计数 |
RelayPlugin.h | 消息传递标识 |
ReliabilityLayer.h | 数据包控制,可靠的、有序的、无序的、流量控制等。 |
ReplicaEnums.h | 复制枚举类型 |
ReplicaManager3.h | 复制管理 |
Router2.h | 路由器插件,负责穿透时连接目标路由器。 |
RPC4Plugin.h | C函数调用插件 |
SecureHandshake.h | 握手协议 |
SendToThread.h | 向线程发送消息 |
SignaledEvent.h | 线程事件信号 |
SimpleMutex.h | 封装一个互斥类 |
SimpleTCPServer.h | 封装一个TCP服务器类 |
SingleProducerConsumer.h | 通过使用一个循环缓冲区队列中读写指针线程之间的数据 |
SocketDefines.h | 空 |
SocketIncludes.h | 定义socket需要的头文件 |
SocketLayer.h | Socket布局实现 |
StatisticsHistory.h | 统计历史 |
StringCompressor.h | 字符串压缩 |
StringTable.h | 字符串表 |
SuperFastHash.h | 哈希快速查找 |
TableSerializer.h | 表实序列化 |
TCPInterface.h | Tcp接口 |
TeamBalancer.h | 网络组的选择 |
TeamManager.h | 网络组管理 |
TelnetTransport.h | 远程传输 |
ThreadPool.h | 封装线程操作类 |
ThreadsafePacketLogger.h | 用户线程数据包记录。 |
TransportInterface.h | 传输接口 |
TwoWayAuthentication.h | 双向认证 |
UDPForwarder.h | 封装的UDP数据包 |
UDPProxyClient.h | UDP代理客户端 |
UDPProxyCommon.h | UDP代理通用类 |
UDPProxyCoordinator.h | UDP服务器状态管理 |
UDPProxyServer.h | UDP代理服务器 |
VariableDeltaSerializer.h | |
VariableListDeltaTracker.h | |
VariadicSQLParser.h | |
VitaIncludes.h | 空 |
WindowsIncludes.h | Win下需要的头文件 |
WSAStartupSingleton.h | 启动计数 |
XBox360Includes.h | 空 |
#include "MessageIdentifiers.h" //包含了一个巨大的枚举数据,表示了RakNet用于发送消息的标识符,例如断开连接通知。
#include "RakPeerInterface.h" //一个RakPeer类得接口
#include "RakNetTypes.h" //定义了在RakNet中使用的结构体,包括SystemAddress结构体——系统的唯一标识符,以及当你需要接收数据或需要发送数据时,API返回给你的数据包。
RakNet::RakPeerInterface* peer = RakNet::RakPeerInterface::GetInstance();
peer->Startup(1, &SocketDescriptor(), 1)//1参数用于设置连接的最大值。2参数是一个线程休眠定时器。3参数描述了监听的端口/地址。
peer ->Connect(serverIP, serverPort, 0, 0);//1参数用于设置服务器的IP地址或域地址。2参数是服务器端口。3、4 输入0。
peer->Startup(maxConnectionsAllowed, &SocketDescriptor(serverPort,0), 1);
peer->SetMaximumIncomingConnections(maxPlayersPerServer);//设置允许有多少连接
RakNet::SocketDescriptor sd(60000,0);
peer->Startup(10, &sd, 1);
peer->SetMaximumIncomingConnections(4);
RakNet::Packet *packet = peer->Receive();
RakNet::Packet *packet;//通常要在一个循环中调用这个函数 for (packet=peer->Receive(); packet; peer->DeallocatePacket(packet), packet=peer->Receive()) { }
//其中 DeallocatePacket //数据包释放掉
const char* message = "Hello World";
对所有连接的系统:
peer->Send((char*)message, strlen(message)+1, HIGH_PRIORITY, RELIABLE, 0, UNASSIGNED_RAKNET_GUID, true); //1、字节流 2、有多少字节要发送 3、数据包的优先级 4、获取数据的序列和子串 5、使用哪个有序流 6、要发送到的远端系统(UNASSIGNED_RAKNET_GUID) 7、表明是否广播到所有的连接系统或不广播
somePeer->Shutdown(300);
RakNet::RakPeerInterface::DestroyInstance(rakPeer);
RakNet大致上说定义了3个库:网络通信库、网络通信的插件模块、扩展支持功能。
网络通信是用两个类来提供的。RakPeer和TCPInterface。RakPeer是游戏使用的主要的类,它基于UDP。它提供了连接,连接管理,拥塞控制,远程服务器检测,带外数据,连接统计,延迟,丢包仿真,阻止列表和安全连接功能。
TCPInterface是一个TCP的包装类,用于和基于TCP的外部系统通信。例如,EmailSender类,用于报告远程系统的本亏消息。有一些插件也支持它,建议用于文件传输,例如自动补丁升级系统。
RakNet中的插件模块是附加到RakPeer或PakcktizedTCP实例的类。基类更新或过滤或注入消息到网络流的时候,附加的插件自动更新。插件提供了自动功能,例如在端到端环境下的主机确定,文件传输,NAT跨越,语音通信,远程调用,游戏对象复制。
扩展支持的功能包括崩溃报告,通过Gmail的pop服务器发送邮件,SQL日志服务器,和基于服务器列表的PHP。
每个数据报:
1字节的位标记
4字节的时间戳,用于计算RTT进行拥塞控制
3字节用于序列号,用于查询数据报的ACKs
每一条消息
1字节用于位标记
2字节用于消息长度
if(RELIABLE, RELIABLE_SEQUENCED, RELIABLE_ORDERED)
A. 3字节用于序列号,用于防止返回到用户重复的消息
If(UNRELIABLE_SEQUENCED,RELIABLE_SEQUENCED,RELIABLE_ORDERED)
A 3字节用于序列号,用于在相同信道按序识别消息
B 1字节用于信道排序
If(message over MTU)
A 4字节用于分片序号,为提高性能不需要压缩
B 2字节用于表示这段数据是哪一片
C 4字节用于分片好的索引,为了提高性能不进行压缩
在调用Startup()之前,通常仅可以使用原始UDP功能,包括Ping(),AdvertiseSystem()和SendOutOfBand()。
StartupResult RakPeer::Startup( unsigned short maxConnections, SocketDescriptor *socketDescriptors, unsigned
socketDescriptorCount, int threadPriority );
该函数会做完成如下的工作:
1、生成RakNetGUID,用于唯一标识RakPeerInterface实例。
RakNetGUID g = rakPeer->GetGuidFromSystemAddress(UNASSIGNED_SYSTEM_ADDRESS);//获取guid
2、分配一组可靠连接槽,由maxConnections参数定义。这个数字可能是游戏的最大玩家数,也可以分配一些额外的缓存,手工控制
进入游戏的人。
3、创建一个或多个sockets,这个使用socketDescriptors参数描述的变量。
maxConnections 参数
threadPriority 参数
对于窗口程序,这个是RakPeer更新线程的优先级,传递给_beginthreadex()。对于Linux,这个参数传递给pthread_attr_setschedparam()用于pthread_create()方法。默认的参数是-99999,在Windows上使用0(NORMAL_PRIORITY),在Linux意味着使用优先权1000。Windows下,默认的参数就不错。而Linux下,可以将这个值设置为正常优先权线程应该设置的值。
其实可以创建一组Socket的描述符,代码如下:
SocketDescriptor sdArray[2];
sdArray[0].port=SERVER_PORT_1;
strcpy(sdArray[0].hostAddress, "192.168.0.1");
sdArray[1].port=SERVER_PORT_2;
strcpy(sdArray[1].hostAddress, "192.168.0.2");
if (rakPeer->Startup( 32, 30, &sdArray, 2 )
OnRakNetStarted();
这个是高级用户想要绑定多个网卡时使用。例如一个网卡连接到LAN后的安全服务器,另外一个网卡连接到因特网。访问不同的绑定组,可以将binding的索引传递给有参数connectionSocketIndex的RakPeerInterface接口的函数。
连接到其他的系统的方法,其实有五种方式来发现要连接到的系统,如下:
1、直接输入IP地址(这个广为人知)
2、LAN广播
3、使用ClientServer/CloudClient插件
4、使用游戏大厅服务器或房间插件
5、使用目录服务器DirectoryServer
方法一:直接输入IP地址
从编码的角度看,最简单,最容易的方式就是将IP地址或域名硬编码,或使用GUI询问用户,让他们来输入他们想要连接的系统的IP地址。很多例子使用这种方法。游戏刚刚出来的时候支持这种方式,这种方式是唯一可用的方式。
优势:
1. 对于编程人员和美工的要求较少,GUI可以很简单。
2. 如果IP地址或域名是固定的,例如运行的是一个专用服务器,这个就是最好的解决方案。
不足:
1. 缺乏灵活性
2. 用户仅仅可以与他们知道的人们玩游戏。
注意:要连接到本机上的RakPeer实例或其他相同的应用程序,IP地址要使用127.0.0.1或localhost。
方式二:LAN广播
RakNet支持在局域网中广播一个数据报发现其他的系统的功能,使用可选的数据来发送和检索相似的应用程序。例子LANServerDiscovery说明了这项技术。
在RakPeerInterface中,Ping函数可以做到这些,如下所述:
rakPeer->Ping("255.255.255.255", REMOTE_GAME_PORT, onlyReplyOnAcceptingConnections);
REMOTE_GAME_PORT应该是其他系统上你关心的应用程序运行的端口。
onlyReplyOnAcceptConnections是一个布尔值,来标识其他系统是否需要回复,即使你没有可用连接连接到该系统。
开放系统会回复ID_UNCONNECTED_PONG,例如下面的例子:
if (p->data[0]==ID_UNCONNECTED_PONG)
{
RakNet::TimeMS time;
RakNet::BitStream bsIn(packet->data,packet->length,false);
bsIn.IgnoreBytes(1);
bsIn.Read(time);
printf("Got pong from %s with time %i\n", p->systemAddress.ToString(), RakNet::GetTime() - time);
}
为了发送用户数据,调用RakPeer::SetOfflinePingResponse(customUserData, lengthInBytes);,RakNet会拷贝传递给它的数据,然后将数据返回回来追加到ID_UNCONNECTED_PONG。
方式三:使用CloudServer/CloudClient插件
不用修改,CloudServer/CloudClient插件直接就可以作为目录服务器。
方式四:使用游戏大厅服务器或房间插件
游戏大厅服务器提供了一个数据库驱动服务器,用于交互和开始游戏。它提供了一些功能,例如好友,配对,邮件,排名,即时通信,快速配对,房间,或房间协调。
参考Lobby2Server_PGSQL和Lobby2Client中对这项功能的使用方法。
优势:
1. 玩家加入游戏最灵活的处理方式
2. 允许用户在开始游戏之前进行交互
3. 建立社区
4. 支持多个标题
不足:
1. 需要一个分离的专用服务器来承载这个插件,服务器需要有数据库支持。
2. 功能相对于简单的游戏列表较大,且复杂,需要时间和编程方面投入更多。
方式五:DirectoryServer.php
DirectoryServer.php和相关的代码可以在Samples\PHPDirectoryServer2中找到。这种方式是给出游戏列表比较廉价的方式,游戏上线后使用web服务器来存储,游戏信息是使用字符串来给出。获得更多信息,参考这个功能的参考手册。
优点:
1. 不需要专用的服务器,仅需要一个web页
缺点:
1. 不灵活
2. 有时不可用(需要多次访问)
发起连接尝试代码如下:
一旦知道了想要连接的远端系统的IP地址,使用RakPeerInterface::Connect()方法初始化一个异步的连接尝试,连接参数如下:
ConnectionAttemptResult Connect( const char* host, unsigned short remotePort, const char *passwordData, int passwordDataLength, PublicKey *publicKey=0, unsigned connectionSocketIndex=0, unsigned sendConnectionAttemptCount=6, unsigned timeBetweenSendConnectionAttemptsMS=1000, RakNet::TimeMS timeoutTime=0 )
1. host是一个IP地址,或域名
2. remotePort是远端系统监听的端口,传递给Startup()函数的端口参数。
3. passwordData是随着连接请求发送的二进制数据。如果这个参数与传递给RakPeerInterface::SetPassword()的参数不同,远端系统会回复ID_INVALID_PASSWORD。
4. passwordDataLength是passwordData的长度,单位是字节。
5. publicKey 是远端系统上传递给InitializeSecurity()函数的公用密钥参数。如果你不适用,传递0。
6. connectionSocketINdex是你要发送的客户端的Socket在传递给RakPeer::Startup()函数的socket描述符的数组中的索引。
7. sendConnectionAttemptCount是在确定无法连接前要做出的发送尝试次数。这个也用于MTU检测,使用3个不同的MTU大小。默认的值12意味着发送每个MTU四次,这对于容忍任何原因的包丢失也是足够的了。更低的值意味着ID_CONNECTION_ATTEMPT_FAILED会更快返回。
8. timeBetweenSendConnectionAttemptsMS是进行另外一次连接尝试要等待的毫秒数。比较好的值是4倍的ping值。
9. 如果消息不能发送,在丢掉远端系统之前,为这次连接,timeoutTime指出了要等待多少毫秒。默认值是0,意味着使用SetTimeoutTime()方法中的全局值。
连接尝试成功Connect()会返回CONNECTION_ATTEMPT_STARTED值,如果失败会返回其他的值。
注意:Connect()返回TRUE并不意味着已经连接成功。如果连接成功,应该会返回ID_CONNECTION_REQUEST_ACCEPTED。否则,会收到一条错误消息。
其中连接消息作为Packet::data结构的第一个字节返回,如下:
连接关闭:
ID_DISCONNECTION_NOTIFICATION 丢失通知
ID_CONNECTION_LOST 连接关闭
新的连接:
ID_NEW_INCOMING_CONNECTION 新的连接
ID_CONNECTION_REQUEST_ACCEPTED 请求接受
连接尝试失败:
ID_CONNECTION_ATTEMPT_FAILED 连接失败
ID_REMOTE_SYSTEM_REQUIRES_PUBLIC_KEY 公钥
ID_OUR_SYSTEM_REQUIRES_SECURITY 安全请求
ID_PUBLIC_KEY_MISMATCH
ID_ALREADY_CONNECTED 已经存在
ID_NO_FREE_INCOMING_CONNECTIONS 未释放连接
ID_CONNECTION_BANNED
ID_INVALID_PASSWORD 无效密码
ID_INCOMPATIBLE_PROTOCOL_VERSION 无效协议
ID_IP_RECENTLY_CONNECTED 已连接
ID_CONNECTION_ATTEMPT_FAILED是一条概述性消息,意味着与远端系统没有建立连接。可能的原因包括如下几方面:
如何将游戏数据编码到数据包中?
运行RakNet的系统,事实上所有在因特网上的系统,都是通过人们所熟知的数据包进行通信。或更加准确点在UDP下,它用的是数据报。每一个数据报由RakNet创建,并且包含了一条或多条消息。消息可以由你创建,例如位置或健康(health这个词确实不知道如何翻译好),或者有时由RakNet内部创建的数据,例如pings。按照惯例,消息的第一个字节包含了一个从0到255的数字标识符,它用于表明消息的类型。RakNet有一大组内部使用的消息,或者插件使用的标识符。这些可以在文件MessageIdentifiers.h查看到详细信息。
使用结构体或位流?
任何时候发送数据都是发送一个字符流。有两种很容易的方法将数据编码成为这种格式:一种是创建、一种结构体,然后将它转化为(char *),另外一种就是使用内置的BitStream类。
创建结构体进行转化的优点是很容易修改结构,并且可以看到你事实上正在发送的数据。由于发送者和接收者能够共享定义了结构体的文件,避免了转化的错误。也没有让数据乱序,或者使用错误类型的危险。创建结构体的不足就是常常不得不改变和重新编译文件。并且丧失了使用Bitstream类进行自动压缩的便利。并且RakNet不能自动转换结构体成员的字节序。
使用Bitstream的优点是不需要改变任何外部文件。仅仅需要一个bitstream,在其中写入你想要写入的数据,然后发送即可。可以使用“Compressed”版本的read和write方法写入相对较少的数据,例如使用它写入bool类型,仅仅需要一位。可以动态写入数据,在某些确定情况下写的值是true或者false。使用Serialize(),Write(), Read()等方法写的数据,Bitstream会自动进行网络字节序的转换。Bitstream的不足就是很容易出现数据处理错误。读取数据的方式与写入的方式不完全相同-错误的序列,或者一个字节的错误数据,或者其他的错误。
下面将介绍两种方法创建数据包:
使用结构体创建数据包
没有时间戳的情况
#pragma pack(push, 1)//强制编译器(在VC++下)按照字节对齐的方式填充数据结构体。
struct structName
{
unsigned char typeId; // 数据类型(一个单字节的枚举类型数据)
//+ 放置数据
//+时间戳的数据包
//+数据包数据类型的标识
//+传输的实际数据
};
#pragma pack(pop)
带有时间戳
#pragma pack(push, 1)
struct structName
{
unsigned char useTimeStamp; // 赋值 ID_TIMESTAMP值
RakNet::Time timeStamp; // 将由RakNet::GetTime()返回的系统时间值或其他方式返回的类似值
unsigned char typeId; // 你的类型放到这里
// 这里放数据
};
#pragma pack(pop)
注意:发送数据的时候,RakNet假设timeStamp是网络字节序。必须使用timeStamp域的函数BitStream::EndianSwapBytes()实现字节序的变换。在接收系统上读取时间戳,使用if (bitStream->DoEndianSwap()) bitStream->ReverseBytes(timeStamp, sizeof(timeStamp)获得时间戳。如果使用的是BitStream这一步就不需要了。
使用bitstream可以写入更少的数据,例如:
unsigned char useTimeStamp; //赋值为 ID_TIMESTAMP
RakNet::Time timeStamp; // 将RakNet::GetTime()返回的系统时间值放到这里
unsigned char typeId; //这里赋值一个在ID_USER_PACKET_ENUM定义的枚举类型,例如ID_SET_TIMED_MINE
useTimeStamp = ID_TIMESTAMP;
timeStamp = RakNet::GetTime();
typeId=ID_SET_TIMED_MINE;
Bitstream myBitStream;
myBitStream.Write(useTimeStamp);
myBitStream.Write(timeStamp);
myBitStream.Write(typeId);
// 假设有一个地雷对象 Mine* mine
// 如果雷的位置是0,0,0, 可以使用1位代替
if (mine->GetPosition().x==0.0f && mine->GetPosition().y==0.0f && mine->GetPosition().z==0.0f)
{
myBitStream.Write(true);
}
else
{
myBitStream.Write(false);
myBitStream.Write(mine->GetPosition().x);
myBitStream.Write(mine->GetPosition().y);
myBitStream.Write(mine->GetPosition().z);
}
myBitStream.Write(mine->GetNetworkID()); // 在结构体中此处为 NetworkID networkId
myBitStream.Write(mine->GetOwner()); //在结构体中此处为SystemAddress systemAddress
1、在写入第一个字节的时候,确保将它转换为(MessageID)或(unsigned char)。例如:
bitStream->write((MessageID)ID_SET_TIMED_MINE);
2、在写入字符串的时候,可以使用BitStream的数组写入字符串。一种方法是先写入长度,然后写入数据,例如:
void WriteStringToBitStream(char *myString, BitStream *output)
{
output->Write((unsigned short) strlen(myString));
output->Write(myString, strlen(myString);
}
编解码如下:
void WriteStringToBitStream(char *myString, BitStream *output)
{
stringCompressor->EncodeString(myString, 256, output);
}
void WriteBitStreamToString(char *myString, BitStream *input)
{
stringCompressor->DecodeString(myString, 256, input);
}
256是读取和写入的最大的字节数。在EncodeString中,如果字符串少于256,它会写入整个字符串。如果大于256个字符,将截断字符串
,那么将解码为256个字符的数组,包括结束符。
RakNet也包含一个字符串类,RakNet::RakString,可以在RakString找到。
RakNet::RakString rakString("The value is %i", myInt);
bitStream->write(rakString);
RakString比std::string的速度快3倍。
RakString支持Unicode。
3、可以直接将结构体写入BitsStream,只需要将结构体转化为(char *)。它会使用内存拷贝memcpy拷贝结构体。使用了结构体,就会将指针废弃,因此不要将指针写入bitstream。
第一步:确定数据
正如在Creating Pakcets中描述的,找出你需要使用的数据类型,使用bitstream或结构体。
第二部:确定授权
你通常会发送动作的触发数据,而不是一系列动作的结果。
通常来讲,数据源分为如下三类:
1、来自做出动作的函数
2、来自做出动作的函数的触发器。
3、来自于数据监视器。
来自于做出动作的函数:
例子:
我有一个称为ShootBullet方法,它带有各种参数,例如子弹的类型,射击源以及射击的方向。每一次进入ShootBullet发送中,目的就是发送一个数据报来告诉网络这个射击事件发生了。
优势:
这种方式很容易维护。ShootBullet或许从许多不同的地方调用(鼠标输入,键盘输入,AI)。并且不用担心跟踪每一个发送数据的地方。在已有的单人游戏很容易实现。
不足:
编程很难。如果我用ShootBullet初始化数据报,那么当网络想要执行这个函数的时候,它要调用这个方法的时候,如果ShootBullet初始化数据报,网络会调用ShootBullet方法,然后会发送另外一个数据报,成为一个反馈循环。那么解决方法有两种,或者另外写一个函数,例如DoShootBullet(sloppy)来专门处理网络发来的数据,或传递一个参数到ShootBullet来告诉它是否是要发送一个数据报。还有就是要考虑授权(authority)。客户端是否可以立刻射击,或者客户端需要来自服务器的授权?如果他需要服务器授权,那么ShootBullet方法需要发送数据包,然后立即返回。除非由网络调用,否则不应该发送数据而是仅仅执行射击的动作。网络也需要额外的数据,例如子弹剩余数,而ShootBullet方法却没有这些数据。有时可以从上下文获得这些数据,但是不是所有时候都可以。用这种方式编程需要一些时间和经验,并且有时很容易产生bug。
从动作函数的触发器获得数据:
例如:
还是使用ShootBullet()方法作为例子。但是这次并不是从ShootBullet方法内部发送数据。这次数据由ShootBullet方法的触发器来发送。例如,当用户点击鼠标时,AI决定射击,或者按下空格等等。
优点:
可以从网络上调用ShootBullet函数,而不用担心形成反馈环。这种情况下,从函数外通常有更多可用的信息。如果网络需要这个数据时,就很容易可以将数据发送出去。
不足:
需要更多的维护。如果我后来加入了其他的方式来射击子弹,那可能会忘记为它发送数据。
从数据解释器发送数据:
例子:
玩家的血量每一次到达0时,发送一个数据报。然而,然后,在血量到达0的地方并没有做这项工作。将它加入到每一个框架都运行的函数中来做,或许是在更新玩家的代码中。当这些代码得知血量到达0时,它会发送数据。然后它会做记录该数据已经发送,不再发送它。
优势:
从网络角度看,逻辑非常清楚。不需要担心反馈,不需要修改做出动作的函数。不需要维护,除非有人修改了我监视的数据。可以实现有效的网络算法,例如每一秒不要发送多次该数据包。
不足:
从设计的角度看很是粗略。仅仅能用于某些类型的数据。当监视的对象重置后,需要加入额外的代码来重置监视代码。要求项目内的其他的编程人员了解这种机制,以防他们修改你所监视的数据。
第三步:确定需要何种可靠性,以及需要的有序流类型。
PakcetPriority.h包含了这些枚举类型。有四个优先级可以选择:IMMEDIATE_PRIORITY, HIGH_PRIORITY, MEDIUM_PRIORITY, LOW_PRIORITY。
每一种优先级的发送次数大约是比它优先级低的快两倍。例如,如果HIGH_PRIORITY发送2条消息,在大致相同的时间内只会发送一条IMMEDIATE_PRIORITY消息。奇怪的是IMMEDIATE_PRIORITY可能会首先到达目的端。
Reliability类型在Detailed Implementation一节介绍了。通常使用RELIABLE_ORDERED作为数据包的可靠性类型。对于所有的有序类型,使用有序流,下面会介绍到。
第四步:调用RakPeerInterface.h中的Send方法。
发送方法不会改变数据,仅仅只做一个数据拷贝,因此从编程人员的角度,到这一步就做完了发送工作。
什么是有序流?
有32个有序流用于有序数据包,32有序流用于序列化数据包。可以认为stream是一个相对有序的流,同一个有序类型的数据包相互之间是相对有序的。使用一个例子说明这一点。假设你想要排序所有的聊天消息,排序所有的玩家运动的数据包,排序玩家的开火的数据包,以及序列化所有剩余弹药的数据包。你可能想要所有的聊天数据包按序到达,却不想聊天数据挂起,因为你并没有得到更早发送的玩家运动数据包。玩家运动数据包与聊天消息并没有关系,因此你不会关心他们的到达顺序。因此最好对它们使用不同的有序流,可以将0用于聊天消息,1用于玩家运动数据包。然而,我们认为玩家的开火数据包必须要相对于玩家的运动数据包要有序,谁也不想看到子弹从错误的位置发出。要处理这个问题可以将开火的数据包和玩家的运动数据包放到同一个流(stream),那么如果一个运动数据包比子弹数据包早到达接受方,由于实际上子弹数据包发送的要比运动数据包早,那么运动数据包会在子弹数据包到达并提交上层后才会提交运动数据包。
对于有序的数据包应该丢掉比较老的数据包。例如,如果接收到了数据包2,然后1,最后3,那么结果可能是接收到了2,丢掉1,然后接到3。这中处理对于弹药数据包是比较好的方式,因为弹药仅仅能下降,不会增加。如果你接收了比较老的数据包,那么会看到某个玩家的弹药在射击中增加了,明显是一个错误。因为有序的数据包都是在一个不同的流集合上,那么对有序数据包可以使用任何的流数字,例如0。只要清楚它与聊天数据包没有关系即可,因为聊天数据包使用有序流集合,而不是序列化流。
没有排序,或序列化的数据包,例如UNRELIABLE 和RELIABLE,不会有序列。这些类型的数据包会忽略这个参数。
当一个数据包到来时,例如Receive返回一个非零,处理这个数据包需要三步:
1、确定数据包类型。使用如下的代码可以返回这个类型值。
unsigned char GetPacketIdentifier(Packet *p)
{
if ((unsigned char)p->data[0] == ID_TIMESTAMP)
return (unsigned char) p->data[sizeof(unsigned char) + sizeof(unsigned long)];
else
return (unsigned char) p->data[0];
}
2、处理数据
if (GetPacketIdentifier(packet)==/* 在这里使用赋值的数据包标识符 */)
DoMyPacketHandler(packet);
// 可以将这个函数放到任何位置,在处理游戏的状态类中比较好
void DoMyPacketHandler(Packet *packet)
{
// 将数据转化为适合类型的结构体
MyStruct *s = (MyStruct *) packet->data;
assert(p->length == sizeof(MyStruct)); // 如果传输的是结构体这块这样处理比较好
if (p->length != sizeof(MyStruct))
return;
// 在这里调用函数处理结构体 MyStruct *s
}
使用注释:
接收一个位流数据(BitStream),如果你最初发送的是一个Bitstream,那就需要创建一个BitStream,按照我们的写入顺序来解析数据。使用数据和数据包的长度来创建一个BitStream。我们写入数据的时候,使用的是Write函数,那么就使用Read函数读取数据。如果前面使用的WriteCompressed函数,那读取数据就要使用ReadCompressed函数。如果我们条件性的写入任何数据,依据这个逻辑分支。在接下来的例子中给出了处理在Creating packets中的地雷的数据:
void DoMyPacketHandler(Packet *packet)
{
Bitstream myBitStream(packet->data, packet->length, false); // false指定不拷贝数据,提高效率
myBitStream.Read(useTimeStamp);
myBitStream.Read(timeStamp);
myBitStream.Read(typeId);
bool isAtZero;
myBitStream.Read(isAtZero);
if (isAtZero==false)
{
x=0.0f;
y=0.0f;
z=0.0f;
}
else
{
myBitStream.Read(x);
myBitStream.Read(y);
myBitStream.Read(z);
}
myBitStream.Read(networkID); // 在结构体中这里是 NetworkID networkId
myBitStream.Read(systemAddress); // 在结构体中这里是SystemAddress systemAddress
}
3、通过将数据包传递给RakPeerInterface实例的DeallocatePakcet(Packet *packet)释放数据包。
SystemAddress是包含了网络上系统的二进制的IP地址和端口的结构体。结构体在RakNetTypes.h中定义。
在一些情况下需要使用SystemAddress,例如:
1. 服务器从一个特殊的客户端获取一个消息,想要中继(转发)给所有的其他客户端。你需要在Send函数中指定发送者的SystemAddress(在RakNet::systemAddress域中给出),并且将广播设置为true(就是在Send函数中将广播标志设置为true,将不转发的客户端指定为发送者的SystemAddress)。在游戏世界的一些项目,例如地雷,属于一个特定玩家,这个地雷杀死人之后也会给放置者分数。
2. 在一个端到端的网络上要发送一个消息到任何的端。
功能函数:
ToString() – 指定一个系统地址结构体,返回一个点分的IP地址
FromString – 指定一个点分的IP地址,填充结构体的binaryAddress部分
重要的注意点:
1. 数据包的接受方会自己知道发送数据包的系统的SystemAddress,因为它可以从发送者的IP/Port结合体中来获得这个值。如果仅仅需要服务器知道SystemAddress是什么,那么发送者不需要将它自己的SystemAddress编码到数据包中。原始发送者的SystemAddress在数据包结构体中自动传递给编程人员,Receive会返回它!
2. 当使用客户端/服务器模型时,客户端不知道发送数据包的原始发送者的SystemAddress。只要客户端连接了服务器,所有的数据包都来自服务器。因此如果客户端需要知道另外一个客户端的SystemAddress,则要将数据包中加入一个SystemAddress数据结构。可以让发送客户端填充这个数据域,或者也可以让服务器从原始发送者那里接收到数据包时来填充这个数据结构体。
3. 在连接期间一个特定的RakPeer实例的系统地址不会发生变化,Router2插件除外。然而并不是所有的系统都是这样的(例如在对称的NAT后面的系统就不是这样的情况)。需要一个唯一的标识符,因为在数据包结构体重有一个唯一的标识符,例如rakNetGUID。RakPeerInterface 有一些函数来操作RakNetGUID。
4. 通过RakNetGUID来指向远端系统是非常好的方法,而SystemAddress则不是特别好的选择。RakNetGUID对于一个RakPeer实例来说是唯一的,然后SystemAddress却不一定是唯一的。如果在系统中要使用Router2插件,唯一地使用RakNetGUID很有必要。
BitStream类是在RakNet命名空间下的一个辅助类,用一个封装的动态数组来打包和解包bits。它具有如下的四个优势:
1. 动态创建数据报。
2. 数据压缩。
3. 写入Bits。
4. 数据字节序转换。
使用结构体打包数据,需要提前预定义结构体,并且将它们转化为(char *)。使用BitStream,可以在运行时根据上下文有选择地写入数据块。BitStream可以使用SerializeBitsFromIntegerRange方法和SerializeFloat16()方法压缩内置类型的数据。
使用它写入位数据。大多数时候不需要关心这个问题。然而,当写入一个Boolean类型的数据时,bitstream仅仅自动写入一位数据。这种处理对加密也很有效,因为写入的数据不再是字节对齐的了,一次如果数据遭到窃听,截取,也无法按照正常的字节对齐查看输入内容!
写入数据
Bitstream是作为模板类,可以容纳任何类型数据。如果这是一个内置的类型,例如一个NetwordIDObject,它使用部分模板实现使得类型写入更加有效。如果是局部类型(这块理解不好,应该是自己定义的一种类型),或一个结构体,bitstream写入单独的一位数据,类似于memcpy。可以传递一个包涵了多个数据成员的结构体到bitstream。但是有时你需要要单独序列化每一个元素以纠正字节序问题(例如在PCs和Macs之间的通讯需要这样来实现)。
struct MyVector
{
float x,y,z;
} myVector;
// 没有字节序交换
bitStream.Write(myVector);
// 带有字节序交换
#undef __BITSTREAM_NATIVE_END
bitStream.Write(myVector.x);
bitStream.Write(myVector.y);
bitStream.Write(myVector.z);
// 也可以重写操作符
// Shift 操作符必须在RakNet命名空间中,或者可以使用BitStream.h中默认的命名空间。错误会在
// std::string发生
namespace RakNet
{
RakNet::BitStream& operator << (RakNet::BitStream& out, MyVector& in)
{
out.WriteNormVector(in.x,in.y,in.z);
return out;
}
RakNet::BitStream& operator >> (RakNet::BitStream& in, MyVector& out)
{
bool success = in.ReadNormVector(out.x,out.y,out.z);
assert(success);
return in;
}
} // 命名空间 RakNet
// 从bitstream读取数据
myVector << bitStream;
// 向bitstream写入数据
myVector >> bitStream;
可选—其中的一个构造函数是以长度作为参数。如果大概知道数据的大小,在构造Bitstream对象的时候可以将这个参数传递给Bitstream的构造函数,可以避免在生成bitstream对象后在动态重新分配内存。
读取数据
// 假设我们接收到一个数据包Packet *
BitStream myBitStream(packet->data, packet->length, false);
struct MyVector
{
float x,y,z;
} myVector;
// 没有字节序转换
bitStream.Read(myVector);
// 要转换字节序(__BITSTREAM_NATIVE_END在RakNetDefines.h中要注释掉)
#undef __BITSTREAM_NATIVE_END
#include "BitStream.h"
bitStream.Read(myVector.x);
bitStream.Read(myVector.y);
bitStream.Read(myVector.z);
序列化数据
struct MyVector
{
float x,y,z;
// 如果ToBitstream==true,则是写入数据, 如果ToBitstream==false,则是读取数据
void Serialize(bool writeToBitstream, BitStream *bs)
{
bs->Serialize(writeToBitstream, x);
bs->Serialize(writeToBitstream, y);
bs->Serialize(writeToBitstream, z);
}
} myVector;
有用函数,参考BitStream.h查看完整的函数列表,如下:
Rese t函数
重置bitstream,清除所有的数据。
Write 函数
Write函数在bitstream的最后写入数据。应该使用类似的Read函数从bitstream中将数据读取出来。
Read函数
Read函数用来读取已经存在在bitstream中的数据,从头到尾按照顺序读取。如果读到了bitstream的结尾处了,Read函数会返回false值。
WriteCasted,ReadCasted
写一种类型的数据就像是它被转化为了其他类型的数据。例如WriteCasted(5),等价于写入char c=5; Write(c);
WriteNormVector, ReadNormVector
写入一个通常的向量,其中每一个元素的范围都是-1 — 1。每一个元素有16位。
WriteFloat16,ReadFloat16
给出一个floating指针数字的最大值和最小值,除以范围65535,将结果以16个字节写入。
WriteNormQuat,ReadNormQuat
在16*3 + 4位中,写入一个四元组。
WriteOrthMatrix,ReadOrthMatrix
将一个正交矩阵转换为四元组,然后调用WriteNormQuat,ReadNormQuat写入和读取数据
GetNumberOfBitsUsed,GetNumberOfBytesUsed
返回写入的字节数或位数。
GetData
返回一个指向Bitstream内部数据的指针。这个数据是用(char *)类型使用malloc分配的,在你需要直接访问bitstream的数据时使用。
// 发送数据的时候,使用这些枚举类型设置数据类型
enum PacketPriority
{
// 最高优先级。这些0消息立即发送,通常不会进行缓存或与其他数据包聚集
// 为一个数据报。 在HIGH_PRIORITY优先级的数据或者更低优先级的
// 数据进行缓存,并且是在10毫秒的时间间隔后发送数据。
IMMEDIATE_PRIORITY,
// 每发送两个IMMEDIATE_PRIORITY消息,才会发送一个HIGH_PRIORITY消息
HIGH_PRIORITY,
// 每发送两个HIGH_PRIORITY, 才会发送一条MEDIUM_PRIORITY优先级消息.
MEDIUM_PRIORITY,
// 每发送两条MEDIUM_PRIORITY消息, 才会发送一条LOW_PRIORITY。
LOW_PRIORITY,
NUMBER_OF_PRIORITIES
};
注:上述的情况都是在缓存中有高优先级的消息存在时才会如此,否则如果没有缓存,则到来的数据直接发送。
数据包优先级非常简单。高优先级数据包在中级优先级数据包之前发送,中级优先级数据包在低优先级数据包之前发送。最初提出数据包的优先权花了很长时间才设计清楚,但是实际使用中优先级会扰乱游戏,因为要发送到一些新连接的不太重要的数据(例如地图数据)会占据了游戏数据。
// 这些枚举类型描述了数据包如何传送
enum PacketReliability
{
UNRELIABLE,
UNRELIABLE_SEQUENCED,
RELIABLE,
RELIABLE_ORDERED,
RELIABLE_SEQUENCED,
UNRELIABLE_WITH_ACK_RECEIPT,
UNRELIABLE_SEQUENCED_WITH_ACK_RECEIPT,
RELIABLE_WITH_ACK_RECEIPT,
RELIABLE_ORDERED_WITH_ACK_RECEIPT,
RELIABLE_SEQUENCED_WITH_ACK_RECEIPT
};
不可靠(UNRELIABLE)
packet = rakPeer->Receive();
while (packet)
{
switch(packet->data[0])
{
case ID_SND_RECEIPT_ACKED:
memcpy(&msgNumber, packet->data+1, 4);
printf("Msg #%i was delivered.\n", msgNumber);
break;
case ID_SND_RECEIPT_LOSS:
memcpy(&msgNumber, packet->data+1, 4);
printf("Msg #%i was probably not delivered.\n", msgNumber);
break;
}
sender->DeallocatePacket(packet);
packet = sender->Receive();
}
使用这个值得原因就是了解不可靠类型消息是否到达接受方。有时候如果不可靠消息丢失,要重发不可靠消息,由于这些数据都是时新(实时的)数据,不能使用可靠类型。要实现
这样的功能,在发送不可靠数据的时候,需要创建一个Send()或SendList()返回的值到接受方返回值之间的一个映射。如果接受方的返回值为ID_SND_RECEIPT_LOSS,那么就需要重新发送
本条返回消息值所对应的数据。
高级发送类型
typedef unsigned char SequenceNumberType;
bool GreaterThan(SequenceNumberType a, SequenceNumberType b)
{
// a > b?
const SequenceNumberType halfSpan =(SequenceNumberType) (((SequenceNumberType)(const SequenceNumberType)-1)/(SequenceNumberType)2);
return b!=a && b-a>halfSpan;
}
序列化的数据,而不是使用序列化的消息
// 保留类型—不要修改这些类型定义
// 所有的类型来自于RakPeer
// 这些类型不会返回给用户
// 来自于一个连接的系统的Ping。更新时间戳(仅仅内部使用)
ID_CONNECTED_PING, // 来自于一个未连接系统的Ping。回复,但是不要更新时间戳(仅仅内部使用)
ID_UNCONNECTED_PING,// 来自于未连接系统的Ping,如果已经打开了连接,则回复,不要更新时间戳(仅用于内部)
ID_UNCONNECTED_PING_OPEN_CONNECTIONS,// 来自连接系统的Pong,更新时间戳(仅内部内部)
ID_CONNECTED_PONG,// 一个可靠数据包,用于检测连接丢失(仅仅用于内部)
ID_DETECT_LOST_CONNECTIONS,// C2S: 初始化查询: Header(1), OfflineMesageID(16), Protocol number(1), Pad(toMTU), 发送
// 不用分片,如果在服务器上协议失败,返回ID_INCOMPATIBLE_PROTOCOL_VERSION
// 到客户端
ID_OPEN_CONNECTION_REQUEST_1,
// S2C: Header(1), OfflineMesageID(16), server GUID(8), HasSecurity(1),
// Cookie(4, 如果设置了HasSecurity), public key (如果doSecurity设置为true),
// MTU(2). 如果公钥在客户端使用失败,返回ID_PUBLIC_KEY_MISMATCH
ID_OPEN_CONNECTION_REPLY_1,
// C2S: Header(1), OfflineMesageID(16), Cookie(4, 如果在服务器HasSecurity为true),
// clientSupportsSecurity(1 bit), handshakeChallenge (如果在服务器和客户端设置了security),
// remoteBindingAddress(6), MTU(2), client GUID(8)
// 如果cookie有效则分配连接间隙,服务器没有满,GUID和IP没有使用
ID_OPEN_CONNECTION_REQUEST_2,
// S2C: Header(1), OfflineMesageID(16), 服务器GUID(8), MTU(2),
// doSecurity(1位), handshakeAnswer (如果doSecurity值为true)
ID_OPEN_CONNECTION_REPLY_2,
/// C2S: Header(1), GUID(8), Timestamp, HasSecurity(1), Proof(32)
ID_CONNECTION_REQUEST,
// RakPeer –远端系统要求安全连接,给RakPeerInterface::Connect()公有密钥
ID_REMOTE_SYSTEM_REQUIRES_PUBLIC_KEY,
// RakPeer–给RakPeerInterface::Connect()传递了公共密钥,但是系统没有开启安全检测
ID_OUR_SYSTEM_REQUIRES_SECURITY,
// RakPeer- 传递给RakPeerInterface::Connect()的公钥密钥是错误的
ID_PUBLIC_KEY_MISMATCH,
// RakPeer-与ID_ADVERTISE_SYSTEM相同,但是仅仅是系统内部使用,不会返回给用户
// 第二个字节指明类型,当前用于NAT 穿透的端口广播。
// 参考ID_NAT_ADVERTISE_RECIPIENT_PORT
ID_OUT_OF_BAND_INTERNAL,
// 如果调用了RakPeerInterface::Send(),其中PacketReliability中有 _WITH_ACK_RECEIPT,
// 然后稍迟一点调用RakPeerInterface::Receive(),可以得到ID_SND_RECEIPT_ACKED或
// ID_SND_RECEIPT_LOSS。消息有5字节长,并且1-4字节包含了一个本地有序的号码,
//它标识了这条消息。这个数字会由RakPeerInterface::Send()或RakPeerInterface::SendList()
//返回. ID_SND_RECEIPT_ACKED意味着这条消息到达了接受方
ID_SND_RECEIPT_ACKED,
// 如果用PacketReliability包含_WITH_ACK_RECEIPT 调用的RakPeerInterface::Send()
// 然后调用RakPeerInterface::Receive(),会得到一个ID_SND_RECEIPT_ACKED或
// ID_SND_RECEIPT_LOSS。这条消息会有5字节长,并且1-4字节会包含一个标识这条消
// 息的数字。这个数字由RakPeerInterface::Send()或RakPeerInterface::SendList()返回
// ID_SND_RECEIPT_LOSS意味着消息没有达到的确认(这条消息发送了,或许没有发送,
// 可能没有).在连接断开或关闭时,对于没有发送的消息会得到ID_SND_RECEIPT_LOSS
// 标识,应该将这些消息当做是完全丢失了
ID_SND_RECEIPT_LOSS,
// 用户类型-不要修改这些定义
// RakPeer-在客户端/服务器环境下,我们的连接要求服务器已经接受
ID_CONNECTION_REQUEST_ACCEPTED,
// RakPeer-如果连接请求无法完成时,给玩家回复这样一个消息
ID_CONNECTION_ATTEMPT_FAILED,
// RakPeer-向你当前要连接到的系统发送连接请求。
ID_ALREADY_CONNECTED,
// RakPeer-远端系统已经成功连接。
ID_NEW_INCOMING_CONNECTION,
// RakPeer-试图连接的系统不接受新的连接。
ID_NO_FREE_INCOMING_CONNECTIONS,
// RakPeer-系统在Packet::systemAddress中指定的已经从服务器断开的。对于客户端,这个
// 标识意味着服务器已经关闭
ID_DISCONNECTION_NOTIFICATION,
// RakPeer-可靠数据包不能传递到Packet::systemAddress指定系统。到该系统连接已经断开
ID_CONNECTION_LOST,
// RakPeer-被要连接到的系统禁止掉了
ID_CONNECTION_BANNED,
// RakPeer-远端系统使用了密码,因为设置密码不正确拒绝了连接请求
ID_INVALID_PASSWORD,
// 在 RakNetVersion.h中的RAKNET_PROTOCOL_VERSION与远端系统上的本值不匹配
// 这意味这两个系统无法通信
// 消息的第二个字节包含了远端系统的RAKNET_PROTOCOL_VERSION值
ID_INCOMPATIBLE_PROTOCOL_VERSION,
// 意味着这个IP最近连接到了系统,作为安全连接,已经无法再建立连接
// 参考RakPeer::SetLimitIPConnectionFrequency()
ID_IP_RECENTLY_CONNECTED,
// RakPeer- sizeof(RakNetTime)个字节大小的值,紧跟着它的一个字节代表了自动修改的
// 发送方和接收方系统的差值,需要调用用SetOccasionalPing方法获取这个值。
ID_TIMESTAMP,
// RakPeer-来自未连接的系统的Pong。第一个字节是ID_UNCONNECTED_PONG,
// 第二个 sizeof(RakNet::TimeMS)大小字节的值是ping。紧跟着这个字节的是系统指定的
// 枚举数据,使用bitstreams读取。
ID_UNCONNECTED_PONG,
// RakPeer- 通知远端系统我们的IP/Port。
// 在接受方,所有的传递的ID_ADVERTISE_SYSTEM数据是传递的数据参数。
ID_ADVERTISE_SYSTEM,
// RakPeer-下载一个大的消息,格式是ID_DOWNLOAD_PROGRESS (MessageID),
// partCount (unsigned int),partTotal (unsigned int),partLength (unsigned int),
// 第一个部分数据 (length <= MAX_MTU_SIZE)。参见文件FileListTransferCBInterface.h中
// 的OnFileProgress的三个参数 partCount, partTotal和partLength
ID_DOWNLOAD_PROGRESS,
// ConnectionGraph2插件-在客户端/服务器环境中,一个客户端已经断开了连接,
// 修改Packet::systemAddress以反映这个断开的客户端的systemAddress
ID_REMOTE_DISCONNECTION_NOTIFICATION,
// ConnectionGraph2插件-在客户端/服务器环境,客户端被迫断开了连接
// 修改Packet::systemAddress来反映这个已经断开连接的客户端的systemAddress
ID_REMOTE_CONNECTION_LOST,
// ConnectionGraph2 产检: 1-4字节 = count。
// 对于 (count items)包含了{SystemAddress, RakNetGUID}
ID_REMOTE_NEW_INCOMING_CONNECTION,
/// FileListTransfer插件 – 设置数据
ID_FILE_LIST_TRANSFER_HEADER,
// FileListTransfer plugin – 一个文件
ID_FILE_LIST_TRANSFER_FILE,
// 请求加入文件,发送多个文件。
ID_FILE_LIST_REFERENCE_PUSH_ACK,
// DirectoryDeltaTransfer 插件-从远端系统请求要下载的目录
ID_DDT_DOWNLOAD_REQUEST,
// RakNetTransport plugin – 用于远端控制台的提供者消息
ID_TRANSPORT_STRING,
// ReplicaManager plugin – 创建一个对象
ID_REPLICA_MANAGER_CONSTRUCTION,
// ReplicaManager plugin – 改变对象的范围
ID_REPLICA_MANAGER_SCOPE_CHANGE,
// ReplicaManager plugin – 序列化对象的数据
ID_REPLICA_MANAGER_SERIALIZE,
// ReplicaManager plugin – 新的连接,要发送所有的对象
ID_REPLICA_MANAGER_DOWNLOAD_STARTED,
// ReplicaManager plugin –完成了所有序列化对象的下载
ID_REPLICA_MANAGER_DOWNLOAD_COMPLETE,
// 已经存在于远端系统对象的序列化构造
ID_REPLICA_MANAGER_3_SERIALIZE_CONSTRUCTION_EXISTING,
ID_REPLICA_MANAGER_3_LOCAL_CONSTRUCTION_REJECTED,
ID_REPLICA_MANAGER_3_LOCAL_CONSTRUCTION_ACCEPTED,
// RakVoice plugin – 打开通信信道
ID_RAKVOICE_OPEN_CHANNEL_REQUEST,
// RakVoice plugin – 接收通信信道
ID_RAKVOICE_OPEN_CHANNEL_REPLY,
// RakVoice plugin – 关闭通信信道
ID_RAKVOICE_CLOSE_CHANNEL,
// RakVoice plugin – 语音数据
ID_RAKVOICE_DATA,
// Autopatcher plugin – 获取一个从某个时间开始的修改过的文件
ID_AUTOPATCHER_GET_CHANGELIST_SINCE_DATE,
// Autopatcher plugin – 要创建的文件的列表
ID_AUTOPATCHER_CREATION_LIST,
// Autopatcher plugin – 要删除的文件的列表
ID_AUTOPATCHER_DELETION_LIST,
// Autopatcher plugin – 要升级的文件的列表
ID_AUTOPATCHER_GET_PATCH,
// Autopatcher plugin – 用于一个文件列表的补丁列表
ID_AUTOPATCHER_PATCH_LIST,
// Autopatcher plugin –返回到用户:一个补丁系统数据库的错误
ID_AUTOPATCHER_REPOSITORY_FATAL_ERROR,
// Autopatcher plugin –从补丁系统下载的所有文件已经完成下载
ID_AUTOPATCHER_FINISHED_INTERNAL,
ID_AUTOPATCHER_FINISHED,
// Autopatcher plugin – 返回到用户: 需要重启完成补丁过程。
ID_AUTOPATCHER_RESTART_APPLICATION,
// NATPunchthrough plugin: 内部使用
ID_NAT_PUNCHTHROUGH_REQUEST,
// NATPunchthrough plugin: internal
ID_NAT_CONNECT_AT_TIME,
// NATPunchthrough plugin: internal
ID_NAT_GET_MOST_RECENT_PORT,
// NATPunchthrough plugin: internal
ID_NAT_CLIENT_READY,
// NATPunchthrough plugin:目的系统没有连接到服务器,偏移量为1的字节包含了
// RakNetGUID, NatPunchthroughClient::OpenNAT()的目的域。
ID_NAT_TARGET_NOT_CONNECTED,
// NATPunchthrough plugin:目的系统没有对ID_NAT_GET_MOST_RECENT_PORT做出
//反应,或许插件没有安装,从偏移量为1的字节开始
// 包含了NatPunchthroughClient::OpenNAT()目的域的RakNetGUID。
ID_NAT_TARGET_UNRESPONSIVE,
// NATPunchthrough plugin: 在建立设置穿透时,服务器丢失了到目的系统的连接
// 可能消息没有安装。从偏移量为1的字节开始
// 包含了NatPunchthroughClient::OpenNAT()目的域的RakNetGUID。
ID_NAT_CONNECTION_TO_TARGET_LOST,
// NATPunchthrough plugin: 穿透工作正在进行,可能该插件没有安装。从偏移量为1的字节
//开始包含了NatPunchthroughClient::OpenNAT()目的域的RakNetGUID。
ID_NAT_ALREADY_IN_PROGRESS,
// NATPunchthrough plugin: 这条消息是本地系统生成的,并不是来自网络,
// packet::guid 包含了NatPunchthroughClient::OpenNAT()的目的域。如果自己是发送方,
// 第一个字节为1,否则是0
ID_NAT_PUNCHTHROUGH_FAILED,
// NATPunchthrough plugin: 穿透成功。参考packet::systemAddress和packet::guid。
// 如果你是发送者第一个字节为1,否则为0
// 你现在可以使用RakPeer::Connect() or其他调用与系统通信
ID_NAT_PUNCHTHROUGH_SUCCEEDED,
// ReadyEvent plugin – 为一个特殊的系统设置准备好状态。
// 消息之后的最前面的四个字节包含了id值
ID_READY_EVENT_SET,
// ReadyEvent plugin – 将一个系统的准备好状态重置掉,消息后的4个字节包含了id值
ID_READY_EVENT_UNSET,
// 所有的系统都处于ID_READY_EVENT_SET状态
// 消息后的4个字节包含了id值
ID_READY_EVENT_ALL_SET,
// \internal, 在游戏中不要使用
// ReadyEvent plugin – 准备好事件状态请求,新连接时用于拉取数据
ID_READY_EVENT_QUERY,
/// Lobby 数据包,第二个字节表明了数据类型
ID_LOBBY_GENERAL,
// RPC3, RPC4插件错误
ID_RPC_REMOTE_ERROR,
// 基于RPC系统的穿件的替换
ID_RPC_PLUGIN,
// FileListTransfer传递大文件,仅仅在需要的时候再读取,以节省内存
ID_FILE_LIST_REFERENCE_PUSH,
// 强制重置所有的准备好事件
ID_READY_EVENT_FORCE_ALL_SET,
// 房间函数
ID_ROOMS_EXECUTE_FUNC,
ID_ROOMS_LOGON_STATUS,
ID_ROOMS_HANDLE_CHANGE,
/// Lobby2消息
ID_LOBBY2_SEND_MESSAGE,
ID_LOBBY2_SERVER_ERROR,
// 通知用户新的主机的GUID. Packet::Guid包含了这个新的主机的RakNetGuid。
// 老主机可以使用BitStream->Read(RakNetGuid)读取这个值
ID_FCM2_NEW_HOST,
/// \internal For FullyConnectedMesh2 plugin
ID_FCM2_REQUEST_FCMGUID,
/// \internal For FullyConnectedMesh2 plugin
ID_FCM2_RESPOND_CONNECTION_COUNT,
// \internal For FullyConnectedMesh2 plugin
ID_FCM2_INFORM_FCMGUID,
// UDP 代理消息。第二个类型表明数据类型
ID_UDP_PROXY_GENERAL,
// SQLite3Plugin – 执行
ID_SQLite3_EXEC,
// SQLite3Plugin – 远端数据库位置
ID_SQLite3_UNKNOWN_DB,
// SQLiteClientLoggerPlugin事件发生
ID_SQLLITE_LOGGER,
// 向NatTypeDetectionServer发送数据
ID_NAT_TYPE_DETECTION_REQUEST,
// 向NatTypeDetectionClient发送。字节1包含了NAT检测类型
ID_NAT_TYPE_DETECTION_RESULT,
// 用于router2 插件
ID_ROUTER_2_INTERNAL,
// 没有可用路径,或没有到远端系统的连接
// Packet::guid 包含了我们要达到的端点的guid
ID_ROUTER_2_FORWARDING_NO_PATH,
// \brief 现在可以调用connect, ping, 其他操作
// 按照如下代码进行连接:
// RakNet::BitStream bs(packet->data, packet->length, false);
// bs.IgnoreBytes(sizeof(MessageID));
// RakNetGUID endpointGuid;
// bs.Read(endpointGuid);
// unsigned short sourceToDestPort;
// bs.Read(sourceToDestPort);
// char ipAddressString[32];
// packet->systemAddress.ToString(false, ipAddressString);
// rakPeerInterface->Connect(ipAddressString, sourceToDestPort, 0,0);
ID_ROUTER_2_FORWARDING_ESTABLISHED,
// 一个转发连接的IP已经改变
// 对于每一个 ID_ROUTER_2_FORWARDING_ESTABLISHED读取endpointGuid 和 port
ID_ROUTER_2_REROUTED,
// \internal 用于team balancer 插件
ID_TEAM_BALANCER_INTERNAL,
// 由于人数已满而无法转到满意的团队。然而,如果这个团队有人离开,你会获得
// 获取 ID_TEAM_BALANCER_SET_TEAM值,字节1包含了你想要加入团队的号码
ID_TEAM_BALANCER_REQUESTED_TEAM_CHANGE_PENDING,
// 由于团队已经上锁,无法转到想去的团队,你会获得
// 获取 ID_TEAM_BALANCER_SET_TEAM值,字节1包含了你想要加入团队的号码
ID_TEAM_BALANCER_TEAMS_LOCKED,
// Team balancer插件通知你你的团队。Byte 1 包含了你要加入的团队
ID_TEAM_BALANCER_TEAM_ASSIGNED,
// Gamebryo Lightspeed集成
ID_LIGHTSPEED_INTEGRATION,
// XBOX 集成
ID_XBOX_LOBBY,
// 密码用于挑战传递这个密码的系统,意味着其他的系统需要使用我们传递给
// TwoWayAuthentication::Challenge()的密码调用TwoWayAuthentication::AddPassword()
/// You can read the identifier used to challenge as follows:
/// RakNet::BitStream bs(packet->data, packet->length, false);
// bs.IgnoreBytes(sizeof(RakNet::MessageID)); RakNet::RakString password; bs.Read(password);
ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_SUCCESS,
ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_SUCCESS,
// 远端系统使用TwoWayAuthentication::Challenge()向我们发送一个挑战,挑战失败
// 如果其他的系统需要将挑战保持,你应该调用RakPeer::CloseConnection()
// 终止到其他系统的连接(此处不理解是什么意思,包括前面两条)
ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_FAILURE,
// 其他的系统没有加入我们在TwoWayAuthentication::AddPassword()使用的密码
// 可以使用如下读取挑战标识符:
// RakNet::BitStream bs(packet->data, packet->length, false); bs.IgnoreBytes(sizeof(MessageID));
// RakNet::RakString password; bs.Read(password);
ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_FAILURE,
// 其他的系统没有在事件门限内给出反应。这个系统或者是没有运行相应插件,
// 或者它在某个事件上长时间阻塞了。
// 可以按照如下方式读取用于challenge的标识符:
/// RakNet::BitStream bs(packet->data, packet->length, false);
// bs.IgnoreBytes(sizeof(MessageID));
// RakNet::RakString password; bs.Read(password);
ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_TIMEOUT,
// \内部
ID_TWO_WAY_AUTHENTICATION_NEGOTIATION,
// CloudClient / CloudServer
ID_CLOUD_POST_REQUEST,
ID_CLOUD_RELEASE_REQUEST,
ID_CLOUD_GET_REQUEST,
ID_CLOUD_GET_RESPONSE,
ID_CLOUD_UNSUBSCRIBE_REQUEST,
ID_CLOUD_SERVER_TO_SERVER_COMMAND,
ID_CLOUD_SUBSCRIPTION_NOTIFICATION,
// 可以在不修改用户的枚举类型前提下,增加一些协议
ID_RESERVED_1,
ID_RESERVED_2,
ID_RESERVED_3,
ID_RESERVED_4,
ID_RESERVED_5,
ID_RESERVED_6,
ID_RESERVED_7,
ID_RESERVED_8,
ID_RESERVED_9,
// 留给用户,从这个值开始你的消息类型定义
ID_USER_PACKET_ENUM,
假设在客户端即将发生一个事件,它的本地系统时间是2000,服务器上的系统时间为12000,另外一个远端系统的时间是8000.如果数据包没有打时间戳调整时间,服务器会获得时间2000,或者说是10000ms以前的事件。同样,另外的客户端会得到2000,这样比他自己的本地时间要提前6000ms。
幸运得是,RakNet为你处理了这种情况,补偿了系统时间和ping。使用相对时间,服务器会看到这个事件是大概在ping/2ms前发生(相对于每一个客户端)。简言之,你仅仅需要使用timestamps,你要做的就是正确地编码数据包,不需要额外考虑其他的任何事情。
参见Creating Packet中的例子,学习如何在你的数据包中打入时间戳。
注意:推荐使用GetTime.h中的时间函数来获取系统的时间。这是一个高精度的定时器。你可以使用Windows的函数timeGetTime(),但是这个函数的时间值不准确。时间戳也依赖于自动的pinging,因此你需要调用SetOccasionalPing()方法来保证这个时间的准确性。
NetworkIDObject 和NetworkIDManager类允许使用普通的ID查询指针。
NetworkIDOjbect类是一个可选类,可以将自己的类从这个类派生,那么你的类就自动赋值标识数字(NetworkID)。这种方法对于多玩家游戏特别有用,否则你必须有自己的方法动态的访问远端系统上分配的对象。
在RakNet 4中,NetworkID是8字节长的全局唯一数字,随机选择。旧版本的RakNet要求中心授权者(服务器)赋值NetworkIDs。这种方法已经废弃了,因为如果游戏是端到到(p2p)的形式,或者有多个分布式的服务器存在,那么编程人员创建这个ID号就非常困难。如果客户端要创建一个对象,也要增加额外的很多工作。因为客户端必须先赋值一个临时的ID,然后从服务器请求真实的NetworkID,然后才可以将它赋值给这个客户端创建的对象。
NetworkIDObject类提供了如下的函数:
SetNetworkIDManager( NetworkIDManager * manager)
NetworkIDManager保存了一个NetworkID的列表用于查询。因此,这就要求你在调用GetNetworkID()或SetNetworkID() 之前调用SetNetworkIDManager()。List不能简单设置为静态的,原因是你或许想要多个NetworkIDManager,例如如果你想要启动多个游戏,它们之间没有任何的交互,如果设置为静态,就会出现错误。
NetworkID GetNetworkID(void)
如果 SetNetworkID() 在前面调用了,这个函数就会返回NetworkID值。否则它为对象生成一个新的,推测是唯一的NetworkID。本质上说,对象只有在调用了GetNetworkID()时才会真正为这个对象赋值一个NetworkID。在客户端/服务器应用下,如果所有的对象依旧是由服务器创建的,那么就不需要客户端生成NetworkID。
SetNetworkID( NetworkID id)
给对象赋值一个NetworkID。用到这个方法的例子就是服务器创建新的游戏对象,广播对象的数据到客户端。客户端会创建一个相同时间的类,读取在用户消息中编码的NetworkID,在同一个对象上调用SetNetworkID()方法。
NetworkIDManager类仅仅有一个用户函数:
template < class returnType>
returnType GET_OBJECT_FROM_ID(NetworkID x);
这是一个模板函数,因此你可以如下一样写代码:
Solider * solider = networkIDManager.GET_OBJECT_FROM_ID(networkId);
如下是一个将指针存储到类的一个例子,重新检索出来,使用Assert确保有效工作(不出现错误):
class Solider : public NetworkIDObject{}
int main(void)
{
NetworkIDManager networkIDManager;
Solider * solider = new Solider;
solider->SetNetworkIDManager(&networkIDManager);
NetworkID soliderNetworkID = solider->GetNetworkID();
Assert(networkIDManager.GET_OBJECT_FROM_ID(soliderNetworkID)== solider);
}
如下是一个例子,使用系统创建一个远端系统上的对象,将同一个ID值赋给两者:
Server:
void CreateSoldier(void)
{
Soldier *soldier = new Soldier;
soldier->SetNetworkIDManager(&networkIDManager);
RakNet::BitStream bsOut;
bsOut.Write((MessageID)ID_CREATE_SOLDIER);
bsOut.Write(soldier->GetNetworkID());
rakPeerInterface->Send(&bsOut,HIGH_PRIORITY,RELIABLE_ORDERED,0,UNASSIGNED_SYSTEM_ADDRESS,true);
}
Client:
Packet *packet = rakPeerInterface->Receive();
if (packet->data[0]==ID_CREATE_SOLDIER)
{
RakNet::BitStream bsIn(packet->data, packet->length, false);
bsIn.IgnoreBytes(sizeof(MessageID));
NetworkID soldierNetworkID;
bsIn.Read(soldierNetworkID);
Soldier *soldier = new Soldier;
soldier->SetNetworkIDManager(&networkIDManager);
soldier->SetNetworkID(soldierNetworkID);
}
静态对象:
// 所有的NetworkID在这里增加
enum StaticNetworkIDs
{
CTF_FLAG_1,
CTF_FLAG_2,
CTF_FLAG_3,
};
class Flag : public NetworkIDObject
{
// 关卡设计者给标记命名flag1, flag2, 或flag3都可以,
// 地图在其他系统也是以相似方式加载
Flag(std::string flagName, NetworkIDManager *networkIDManager) {
SetNetworkIDManager(networkIDManager);
if (flagName=="flag1")
SetNetworkID(CTF_FLAG_1);
else if (flagName=="flag2")
SetNetworkID(CTF_FLAG_2);
else if (flagName=="flag3")
SetNetworkID(CTF_FLAG_3);
};
如何读取RakNet的统计数据,以及如何解析统计数据
统计数据对于在线的游戏非常重要,因为它可以让你看到你游戏的传输瓶颈在什么地方。关于统计功能,RakNet提供了结构体RakNetStatics,由RakPeerInterface中的GetStatics()函数返回。这个结构体在Source/RakNetStatics.h中定义。函数StaticsToString()用于将这些统计参数转化为格式化缓存形式。
enum RNSPerSecondMetrics
{
// 每一次调用RakPeerInterface::Send()所推送的字节数。
USER_MESSAGE_BYTES_PUSHED,
// 通过调用RakPeerInterface::Send()所发送的用户数据的字节数。
// 这个数值要小于或等于USER_MESSAGE_BYTES_PUSHED的值
// 由于拥塞,一条消息可能已经推送了,但是没有发送
USER_MESSAGE_BYTES_SENT,
// 重发了多少字节用户消息。如果消息标识为可靠类型但是消息没有到达
// 或消息确认没有到达,这个消息就会重发。
USER_MESSAGE_BYTES_RESENT,
// 接收并且成功了多少字节用户消息
USER_MESSAGE_BYTES_RECEIVED_PROCESSED,
// 接收了,但是由于格式错误而丢弃的消息字节数。这个值通常为0
USER_MESSAGE_BYTES_RECEIVED_IGNORED,
// 事实上发送的数据的字节数,包括每一条消息和每一个数据包的消耗,
// 可靠性消息确认
ACTUAL_BYTES_SENT,
// 事实上接收到的数据的字节数,包括开销和确认
ACTUAL_BYTES_RECEIVED,
// \internal
RNS_PER_SECOND_METRICS_COUNT
};
// 网络统计使用
// 存储与网络使用相关的统计信息
struct RAK_DLL_EXPORT RakNetStatistics
{
// 对于RNSPerSecondMetrics中的每一种类型, 超过最后一秒的值是什么?
uint64_t valueOverLastSecond[RNS_PER_SECOND_METRICS_COUNT];
// 对于RNSPerSecondMetrics中的每一种类型,在整个连接的生命周期的总值是什么?
uint64_t runningTotal[RNS_PER_SECOND_METRICS_COUNT];
// 连接是什么时候开始的?
/// \sa RakNet::GetTimeUS()
RakNet::TimeUS connectionStartTime;
// 我们当前的发送速率被拥塞控制遏制??
// 如果你每一秒钟发送数据量比你实际的带宽要大这个值为TRUE
bool isLimitedByCongestionControl;
// 如果isLimitedByCongestionControl是true, 限制是什么,每一秒钟的字节数是多少?
uint64_t BPSLimitByCongestionControl;
//发送速率是否受到RakPeer::SetPerConnectionOutgoingBandwidthLimit()函数的限制?
bool isLimitedByOutgoingBandwidthLimit;
// 如果isLimitedByOutgoingBandwidthLimit为true,每一秒钟字节数的限制是什么?
uint64_t BPSLimitByOutgoingBandwidthLimit;
// 每一个优先级,有多少消息在等待发送?
unsigned int messageInSendBuffer[NUMBER_OF_PRIORITIES];
// 每一个优先级,有多少字节数据等待发送?
double bytesInSendBuffer[NUMBER_OF_PRIORITIES];
// 有多少字节数据等待在重发缓存?这个数据包括等待确认的消息,
// 正常应该是较小的值
// 如果这个值随着时间增长,需要发送数据的速率正在超过了带宽能力
// 参考BPSLimitByCongestionControl值
unsigned int messagesInResendBuffer;
// 有多少字节等待在重发队列也可参考messagesInResendBuffer值
uint64_t bytesInResendBuffer;
// 在最后一秒,系统丢包率是多少?这个值范围是从0.0 (没有)到1.0 (100%丢包)
float packetlossLastSecond;
// 在连接期间,平均总的丢包率是多少?
float packetlossTotal;
};
RakNet提供了使用256位传输层安全的数据安全解决方案。每一个域服务器连接都拥有一个256位的椭圆曲线密钥协议实现前向安全保护。
Cookies:在握手中使用无状态的cookie,类似于SYN cookies,这使得远端IP地址欺骗这种作弊手段很难实现。
Efficient:最近两年发布,并且改进过的现代技术用于提供安全,没有任何的效率代价。
Forward secrecy:使用Tunnel Key Agreement“Tabby”协议。如果服务器在未来的某时间点被攻破,先前交换的数据不会被解密。
Protection:每一条消息都进行加密,使用消息授权码(MAC)和唯一标识符进行打戳处理,保护隐私数据,防止重放攻击。
如果服务器密钥是提前获知的,对于动态攻击(man-in-the-middle)就会免疫。
使用256位的椭圆曲线加密算法。
Elliptic Curve:在有限域Fp,p=2^n –c, c 小的曲线形状:
a’ * x ^2 + y ^ 2 = 1 + d’ * x ^ 2 *y ^ 2 , a’ = -1(在Fp平方)d’(在Fp不平方) —> 更早的曲线 = q*cofactor h, 生成点的更早值 = q
曲线满足MOV条件,不是一个类似点,使用Extended Twisted Edwards组规律实现操作。
安全连接会给每一个数据包增加11字节的数据,需要花费相应的时间计算,因此你可能希望可控地在发布的游戏中使用。
1、增加#define LIBCAT_SECURITY 1 到 “NativeFeatureIncludesOverrides.h”。重新编译所有的文件。
2、包含“Source/SecureHandshake.h”头文件,不要包含任何Source/cat中的文件。
3、提前生成公有密钥和私有密钥。
cat::EasyHandshake::Initialize()
cat::EasyHandshake handshake;
char public_key[cat::EasyHandshake::PUBLIC_KEY_BYTES];
char private_key[cat::EasyHandshake::PRIVATE_KEY_BYTES];
handshake.GenerateServerKey(public_key, private_key);
4、将公有密钥和私有密钥写入磁盘
fwrite(private_key, sizeof(private_key), 1, fp);
fwrite(public_key, sizeof(public_key), 1, fp);
5、服务器会加载公有密钥和私有密钥,将它传递给RakPeerInterface::InitializeSecurity()方法,不要发布私有密钥,这个必须要保持保密状态。
6、客户端应该加载公有密钥。公有密钥应该使用客户端应用程序分发,或者从安全位置下载。将公有密钥传递给RakPeerInterface::Connect()方法。
7、按照普通的方法将客户端连接到服务器即可。
可能的错误,在packet->data[0]中返回:
ID_REMOTE_SYSTEM_REQUIRES_PUBLIC_KEY: 你没有将公有密钥传递给RakPeerInterface::Connect()。
ID_OUR_SYSTEM_REQUIRES_SECURITY: 给RakPeerInterface::Connect()传递了公有密钥,但是在服务器上并没有调用InitializeSecurity()方法
ID_PUBLIC_KEY_MISMATCH: 客户端的公有密钥与服务器上的公有密钥不匹配。
公有密钥传输:
可选的解决方案:
1、你可以使用PHPDirectoryServer来传递公有密钥。尽管不太安全,但是它仅仅需要一个PHP的webhost(典型的站点一个月仅需要$5 -$10),并且它相对来说比选择2更加安全。
2、在RakPeer::Connect中,给acceptAnyPublicKey传递TRUE值以使用publicKey参数。这个与RakNet 第三版类似,在第三版中你仅仅需要给每一个InitialzeSecurity参数传递0。
注册:
创建服务器
第一步要创建一个服务器,使用Hosting/Cloud Server/Add Server。这里会给出一个菜单,询问你需要Linux或Windows服务器,以及多少RAM。最低配置的Linux服务器比Windows服务器要更加便宜。RakNet在两种服务器上都可以运行。云服务器和NAT穿透服务器都使用最少的RAM。Autopatcher需要较多的RAM,我推荐4G内存来服务器256个并发用户,或者8G服务器512个并发用户。
服务器一旦创建,你会收到一封邮件,通知你服务器的密码和登录IP。
对于Windows7,输入用户名,密码和使用远端桌面的登录IP,在Start/Accessories下。
设置服务器
登录到服务器后, 服务器的设置与任何的计算机都相同。
1. 默认的安装包含了IE,可以使用它下载Firefox或Chrome。
2. 使用浏览器下载RakNet。
3. 在Windows上,可以下载免费的Visual C++ 2010 Express。安装完成后,需要进行重新启动。
服务器设置完成后,打开RakNet的工程,进行编译。那么你就有了一个自己的工作服务器,IP地址就是你连接到服务器桌面使用的IP地址。
备份和扩展服务器
按照你自己的喜好设置完服务器后,可以创建一个服务器镜像,它可以作为一个硬盘备份。这对于扩展很重要,有了这个镜像,你完全可以按照你镜像的相同配置生成一个新的服务器实例。
不运行服务器时,保证删除实例,保留你“廉价”的镜像,以备扩展服务器时使用。Amazon AWS仅仅对使用收费,与Amazon不同,Rakspace中只要你的服务器存在,就会对你进行收费。再次启动你的服务器,或者开始使用多个相同的服务器实例,使用Cloud Servers菜单下的My Server Images。
用编程的方式管理应用服务器
Rakspace提供了基于API的HTTPS,具体的API信息可以在连接中找到(http://docs.rackspacecloud.com/servers/api/v1.0/cs-devguide-20110112.pdf)。使用TCPInterface时,RakNet支持HTTPS,编译时需要将RakNetDefines.h中的OPEN_SSL_CLIENT_SUPPORT定义为1。
#include "Rackspace.h"
#include "TCPInterface.h"
//创建了一个TCPInstance实例,Rackspace实例,以及Rackspace的事件处理器
RakNet::Rackspace rackspaceApi;
RakNet::TCPInterface tcpInterface;
RakNet::RackspaceEventCallback_Default callback;
tcpInterface.Start(0, 0, 1); //启动了TCPInterface类,那么可以初始化对外的连接。
rackspaceAPI.Authenticate(&tcpInterface, "myAuthURL", "myRackspaceUsername", "myRackspaceAPIKey"); //日志记录 ,存储一个密钥用于下面一系列的操作。
//假设验证成功,Authenticate会返回一些除了UNASSIGNED_SYSTEM_ADDRESS值以外的其他值,以表明连接完成。那么之后要获取回调函数OnAuthenticationResult,这个回调处理设置
给//RET_Success_204的事件类型。这完成之后,你可以调用Rackspace类给出的函数方法。
//如果连接立即关闭,没有来自服务器的任何响应,那你可能是在编译时忘记了将RakNetDefines.h或RakNetDefinesOverrides.h中的OPEN_SSL_CLIENT_SUPPORT设置为1。
while (1)
{
for(RakNet::Packet *packet=tcpInterface.Receive(); packet; tcpInterface.DeallocatePacket(packet), packet=tcpInterface.Receive())
rackspaceApi.OnReceive(packet);
rackspaceApi.OnClosedConnection(tcpInterface.HasLostConnection());
}
列出镜像的例子:
rackspaceApi.ListImages();
等待回调OnListImagesResult(RackspaceEventType eventType, const char * htmlAdditonalInfo)
正如在devguide中描述的一样,回调会返回HTML事件码,200, 203, 400, 500, 503, 401, 400,或413。200和203表示成功。事件码会在eventType参数中返回。真正的HTML
信息会在htmlAdditionalInfo变量中。例子输出:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
vary: Accept, Accept-Encoding, X-Auth-Token
Last-Modified: Tue, 29 Mar 2011 22:41:36 GMT
X-PURGE-KEY: /570016/images
Cache-Control: s-maxage=1800
Content-Type: application/xml
Content-Length: 1334
Date: Sat, 02 Apr 2011 15:15:19 GMT
X-Varnish: 601046147
Age: 0
Via: 1.1 varnish
Connection: keep-alive
RakNet不能帮你解析XML。如果你自己没有XML解析器,你可以在DependentExtenstions\XML下找到一个XML解析器。
执行用户命令:
RakNet::RakString xml(""
"" " " , name.C_String(), imageId.C_String(),
flavorId.C_String());
AddOperation(RO_CREATE_SERVER, "POST", "servers", xml);
使用RakNet进行服务器管理的例子:
if (packet->data[0]==ID_NAT_TYPE_DETECTION_RESULT)
{
RakNet::NATTypeDetectionResult detectionResult = (RakNet::NATTypeDetectionResult) packet->data[1];
}
如果detectionResult的值是NATTypeDetectionResult::NAT_TYPE_NONE,那么这个系统没有路由。你可以连接到任何系统,并且每一个系统也可以连接到你。
#include "miniupnpc.h"
#include "upnpcommands.h"
#include "upnperrors.h"
bool OpenUPNP(RakPeerInterface *rakPeer, SystemAddress serverAddress)
{
struct UPNPDev * devlist = 0;
devlist = upnpDiscover(2000, 0, 0, 0);
if (devlist)
{
char lanaddr[64]; /* my ip address on the LAN */
struct UPNPUrls urls;
struct IGDdatas data;
if (UPNP_GetValidIGD(devlist, &urls, &data, lanaddr, sizeof(lanaddr))==1)
{
DataStructures::List< RakNetSmartPtr< RakNetSocket> > sockets;
rakPeer->GetSockets(sockets);
char iport[32];
Itoa(sockets[0]->boundAddress.GetPort(),iport,10);
char eport[32];
Itoa(rakPeer->GetExternalID(serverAddress).GetPort(),eport,10);
int r = UPNP_AddPortMapping(urls.controlURL, data.first.servicetype, eport, iport, lanaddr, 0, "UDP", 0);
if(r!=UPNPCOMMAND_SUCCESS)
{
return false;
}
}
else
{
return false;
}
}
else
{
return false;
}
return true; //如果OpenUPNP返回true,那么说明成功了。你可以连接到其他的系统,并且其他的系统也可以链接到你。远端系统应该连接到你对外可以被服务器看到的端口。
}
你应该告诉服务器自己的系统直接可以连接,那么进入的系统不需要花费时间做NAT穿透。参考附录A,传度NAT_TYPE_SUPPORTS_UPNP。在游戏会话中连接到每一个存在的用户。
第四步:运行NATPunchthroughClient
对于每一个我们调用OpenNAT的用户,我们会获得如下的响应代码:
ID_NAT_TARGET_NOT_CONNECTION –在游戏会话中将这个用户从远端玩家列表中移除。
ID_NAT_TARGET_UNRESPONSIVE -在游戏会话中将这个用户从远端玩家列表中移除。
ID_NAT_CONNECTON_TO_TARGET_LOST -在游戏会话中将这个用户从远端玩家列表中移除。
ID_NAT_ALREADY_IN_PROGRESS – 忽略
ID_NAT_PUNCHTHROUGH_FAIED – 将玩家存储到内存,标记为穿透失败,我们会在第五步处理这些玩家。
ID_NAT_PUNCHTHROUGH_SUCCEEDED – 将这个玩家存储到内存,标记为穿透成功,我们在第六步中处理这类玩家。
void PostConnectivityState(RakNet::NATTypeDetectionResult result, RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid)
{
RakNet::CloudKey cloudKey("NATConnectivityState",0);
RakNet::BitStream bs;
bs.WriteCasted(result); // 这里可以是任何东西,例如玩家列表,游戏名字
cloudClient->Post(&cloudKey, bs.GetData(), bs.GetNumberOfBytesUsed(), serverGuid);
}
更加简单的解决方法
在RakNetDefinesOverrides.h文件中定义了下面这些值。在RakNetDefines.h中这些都是作为默认设置。在NativeFeatureIncludes.h文件中,不要编译那些你用不到的功能。
// 定义 __GET_TIME_64BIT 变量使得RakNet::TimeMS使用64位保存数据,而不是32位值。
// 32位值在使用了5周之后会发生溢出错误。但是,使用64位值会使得发送时间所使用的带宽加倍
// 因此不要轻易使用这个值,除非你有充分的原因。如果使用的是iPod Touch TG,注释掉这个定义。
// 更多内容参考http://www.jenkinssoftware.com/forum/index.php?topic=2717.0
// 这个值的定义在所有的系统上必须相同,否则无法实现连接通信。
#define __GET_TIME_64BIT 1
// 如果想要剥去文件和LINE信息对于EXE 的内存追踪,定义 _FILE_AND_LINE_ 为0
#define _FILE_AND_LINE_ __FILE__,__LINE__
// 在BitStream中不支持字节序交换定义 __BITSTREAM_NATIVE_END值,
// 如果要使系统加速,则定义这个值。
// 除非你想要不同的字节序系统进行相互连接时才将这个值设置为可用。默认设置不可用
// #define __BITSTREAM_NATIVE_END
// 使用new 和 delete之前用于_alloca的最大栈值。
#define MAX_ALLOCA_STACK_ALLOCATION 1048576
// 使用WaitForSingleObject函数代替sleep函数
// 定义这个值,可以使得系统表现更好,CPU使用率较小,但是RakNet 的性能较差
// 如果注释掉这个定义,CPU使用时间增加,但是RakNet响应更快,更加及时。
#define USE_WAIT_FOR_MULTIPLE_EVENTS
// 取消注释,使用RakMemoryOverride用于用户内存追踪,参考RakMemoryOverride.h了解更多信息。
#define _USE_RAK_MEMORY_OVERRIDE 0
// 如果定义了,OpenSSL可以用于TCPInterface类。要使用SendEmail类必须设置这个定义。注意OpenSSL带有自己的授权限制,这点需要注意。
// 如果你不同意使用OpenSSL的授权限制,则不要定义这个值。使用的话需要将头文件搜索路径设置加上DependentExtensions\openssl-0.9.8g。
#define OPEN_SSL_CLIENT_SUPPORT 0
// 执行malloc/free的门限值,而不是将数据压进bitstream类的固定栈中。
// 任意的大小,仅仅拿出一些比大多数包都大的
#define BITSTREAM_STACK_ALLOCATION_SIZE 256
// 如果你不想用,或修改调试用的输出方法,可以定义RAKNET_DEBUG_PRINTF
#define RAKNET_DEBUG_PRINTF printf
// 支持的最大本地IP地址数
#define MAXIMUM_NUMBER_OF_INTERNAL_IDS 10
// 这个定义控制用于每一个连接的最大内存数。如果比这个值更大,许多数据包将被发送,但是不会有确认,那么确认就没有了作用
#define DATAGRAM_MESSAGE_ID_ARRAY_LENGTH 512
// 这个值定义了可靠用户消息的最大数,这个最大数是某一时刻可以在线上传输的可靠用户消息的最大数
#define RESEND_BUFFER_ARRAY_LENGTH 512
#define RESEND_BUFFER_ARRAY_MASK 511
// 如果你想要使用RakMemoryOverride链接到DLMalloc 则需要定义下面的这个值
// #define _LINK_DL_MALLOC
// 其他的变通方案参考http://support.microsoft.com/kb/274323
// 如果在RakNet::GetTime()之间发生两次调用,这个delta会被返回
// 注意:如果你设置一个断点暂停RakPeer的UpdateNetworkLoop()线程
// 这会导致ID_TIMESTAMP 暂时变得不十分准确。
// 这个值定义在RakNetDefinesOverrides.h文件中,使得它可用定义为非零,这个功能不可用则定义为0。
#define GET_TIME_SPIKE_LIMIT 0
// 使用滑动窗口代替基于拥塞控制ping
#define USE_SLIDING_WINDOW_CONGESTION_CONTROL 1
// 当一个大的消息到达时,为整个块预分配内存。
// 对于大消息,这种机制不需要重组数据报,浪费时间,但是很容易受到攻击者攻击,造成主机耗尽内存。
#define PREALLOCATE_LARGE_MESSAGES 0
void* (*rakMalloc) (size_t size) = RakMalloc;
void* (*rakRealloc) (void *p, size_t size) = RakRealloc;
void (*rakFree) (void *p) = RakFree;
#include "RakMemoryOverride.h"
void *MyMalloc(size_t size)
{
return malloc(size);
}
int main()
{
rakMalloc=MyMalloc;
// ...
}
然后编辑RakNetDefinesOverrides.h文件,加入如下定义:
#define _USE_RAK_MEMORY_OVERRIDE 1
可选的一项就是编辑RakNetDefines.h中的 __USE_RAK_MEMORY_OVERRIDE
支持IPv6
这一段代码来自于ChatExampleServer工程。如果可能的话,它启动了两个socket,第二个socket支持IPv6。并不是所有的操作系统默认支持IPv6,或有些系统更本就不支持IPv6,因此退一步来说使用IPv4还是很有必要的。
RakNet::SocketDescriptor socketDescriptors[2];
socketDescriptors[0].port=atoi(portstring);
socketDescriptors[0].socketFamily=AF_INET; // Test out IPV4
socketDescriptors[1].port=atoi(portstring);
socketDescriptors[1].socketFamily=AF_INET6; // Test out IPV6
bool b = server->Startup(4, socketDescriptors, 2 )==RakNet::RAKNET_STARTED;
server->SetMaximumIncomingConnections(4);
if (!b)
{
printf("Failed to start dual IPV4 and IPV6 ports. Trying IPV4 only.\n");
// Try again, but leave out IPV6
b = server->Startup(4, socketDescriptors, 1 )==RakNet::RAKNET_STARTED;
if (!b)
{
puts("Server failed to start. Terminating.");
exit(1);
}
}
正如上述代码所示,要想使用IPv6就要设置SocketDescriptor的socketFamily成员的值为AF_INET6。默认值为AF_INET,这个值是使用IPv4所需要使用的值。
注:在写本篇文档时,RakNet的控制台还不支持IPv6。
PluginInterface2.h是与RakNet一起工作的一个类接口,提供了一些自动功能,也即在消息到达用户之前,拦截,修改,以及创建消息。插件可以附加到RakPeerInterface或者PacketizedTCP实例上。每一次Receive()被调用,插件都会进行更新。使用这个插件,仅仅需要从基类派生,并且实现想要处理的虚函数。然后通过调用RakPeerInterface::AttachPlugin()方法注册这个插件类即可。
如下列举了一些你在大多数情况下要实现的虚函数:
// 每次检验数据包都会调用Update()函数.
virtual void Update(void);
// 每一个数据包都会调用OnReceive()方法.
// \param[in] packet 返回给用户的数据包(packet)
// \return True 允许游戏或其他的插件得到这个消息,False则不允许。
virtual PluginReceiveResultOnReceive(Packet *packet);
// 因为用户为指定的系统调用了RakPeer::CloseConnection(),连接丢失时,调用这个方法
// \param[in] systemAddress 丢失连接的用户的systemAddress
// \param[in] rakNetGuid 指定系统的guid
// \param[in] lostConnectionReason 连接是如何关闭的,手动,连接丢失,或断开通知
virtual voidOnClosedConnection(SystemAddress systemAddress, RakNetGUID rakNetGUID,PI2_LostConnectionReason lostConnectionReason );
// 得到一个新连接时,调用这个函数。
// \param[in] systemAddress 新连接的地址。
// \param[in] rakNetGuid 指定系统的guid
// \param[in] isIncoming 如果为true, 连接是ID_NEW_INCOMING_CONNECTION,
// 或其他相同情况。
virtual void OnNewConnection(SystemAddresssystemAddress, RakNetGUID rakNetGUID, bool isIncoming);
客户端:
在客户端一边,大多数的工作只需要AutopatherClient的几个方法就可以完成。
void SetFileListTransferPlugin(FileListTransfer * flt);
这个插件依赖于FileListTransfer插件,FileListTransfer插件用于发送文件。因此需要将插件实例注册到RakPeerInterface的实例,指向这个接口的指针应该传递给这个函数。
bool PatchApplication(const char *_applicationName, const char *_applicationDirectory, const char *lastUpdateDate, SystemAddress host,
FileListTransferCBInterface *onFileCallback, const char *restartOutputFilename, const char *pathToRestartExe);
升级一个应用程序的指定目录,这个应用程序是在补丁服务器上有相同名字的应用程序。
_applicatonName – 应用程序的名字
_applicationDirectory – 要写输出文件的目录
_lastUpdateDate – 你最后从补丁服务器更新文件的时间字符串,因此仅仅需要检验最新的文件。如果是第一次,或者你先要进行全部扫描,只需要赋值0。成功调用
PatchApplication之后,返回GetServerDate()。
host – 消息发送到的远端系统的地址信息
onFileCallback – (可选)对每一个文件要调用的回调函数。当在回调中fileINdex+1 == setCount,然后下载就完成了。
_restartOutputFilename – 如果需要重启应用程序,应用程序是写入重启数据的地方。在这个文件名中,可以包含一个路径。
pathToRestartExe – 从AutopathcerClientRestarter加载exe。argv[0]会重新加载这个应用程序。
在客户端也有其他的函数和类,可以从\Samples\AutopatcherClient例子中进行学习。
服务器:
在服务器一端,可以使用AutopatcherServer。
void SetFileListTransferPlugin(FileListTransfer * fit);
与AutopathcerClient类似,这个插件也要依赖于FileListTransfer插件,FileListTransfer是真正用来发送文件的插件。因此需要将这个插件注册给RakPeerInterface的实例,指向这个实例的指针应该传递给函数。
void SetAutopathcerRepositoryInterface(AutopathcerRepositoryInterface* ari);
使用这个函数,告诉AutopacherServer如何看管好补丁的网络传输。这个类仅仅用于autopacher的网络传输。所有的数据都存储在数据库中。使用这个函数,将接口传递给数据仓库。RakNet带有AutopacherPostgreRepository,如果需要就可以使用。
查看\Samples\AutoPatcherServer下的例子。它将AutopacherRepositoryInterface的实例用于PostgreSQL(AutopacherRepository)来存储在PostgreSQL数据库中的应用程序的所有文件。
在本地和远端系统调用C函数
注册函数:
注册一个函数,使用RegisterSlot()或RegisterBlockingFunctioin()成员。
void RegisterSlot(const char *sharedIdentifier, void ( *functionPointer ) ( RakNet::BitStream *userData, Packet *packet ), int callPriority);
bool RegisterBlockingFunction(const char* uniqueID, void ( *functionPointer ) ( RakNet::BitStream *userData, RakNet::BitStream *returnData, Packet *packet ));
第一个参数是一个字符代表了要调用的函数。它可以和函数的名字一样。第二个参数是一个指针,指向要被调用的函数。如果它是一个块函数,参数列表也包含了返回数据给调用者的BitStream。
RPC4GlobalRegistration类可以用于在他们声明的地方注册函数。例如:
void CFunc1(RakNet::BitStream * bitStream, Packet * packet){}
RPC4GlobalRegistration cfunc1reg( "CFunc1", CFunc1);
使用Signal()函数调用一个非阻塞函数(到底是非块函数,还是非阻塞函数,我也没有明白。)。否则调用CallBlocking()函数。
void Signal(const char *sharedIdentifier, RakNet::BitStream * bitStream, PacketPriority priority, PacketReliability reliability, char orderingChannel, const AddressOrGUID systemIdentifier, bool broadcast, bool invokeLocal);
bool CallBlocking( const char* uniqueID, RakNet::BitStream * bitStream, PacketPriority priority, PacketReliability reliability, char orderingChannel, const AddressOrGUID systemIdentifier, RakNet::BitStream *returnData );
Signal会调用所有在RegisterSlot()函数中使用标识符注册的函数,包括有可能可以用于同一个系统中。CallBlocking()会在单个系统上回调用一个信号函数,使用RegisterBlockingFunction()函数注册了。CallBlocking()函数调用直到远端系统有回复,或连接断开才会返回,否则一直处于阻塞状态。
参考Samples/RPC4插件的演示例子。
通过服务器云实现客户端可访问的内存/事件
有时想要大量的没有相互连接的客户端,它们不需要相互知道对方的存在而共享内存或得到事件的通知。例如:
1. 高性能服务器浏览器
2. 游戏内百万用户的统计
3. 云计算
在云服务中的“云”意味着系统支持分布式服务器。任何的服务器可以Post()到服务器,或从服务器上Get()信息。不论客户端从什么服务器上订阅的服务,Post()操作可以定制给其他客户端或被其他的客户端Get()。服务器可以在运行时增加或移除,系统的运行也不会受到影响,继续按照期望的情况运行。这要求系统可以根据玩家的负载进行扩展,可以使用自己的服务器或者虚拟的服务器,例如Rackspace。
系统的设计假设所有的服务器完整连接。在使用CloudServer::AddServer()方法将服务器加入进来之前,应该验证这些服务器。TwoWayAuthentication和ConnectionGraph2插件可以帮助实现这个功能。可以通过运行/修改CloudServer例子来实现,这个例子已经实现这个功能。
通过静态IP地址,没有路由或有端口打开,可以通过Internet访问服务器。
注:系统的设计是当客户端连接到服务器时,客户端数据才会持续到达。服务器本身可以执行本地操作或使用本地的客户端进行持久化。
代码使用方法:
在服务器上,可以有选择地限制下载或上传,以减少内存或带宽使用。
CloudServer cloudServer;
rakPeer->AttachPlugin(&cloudServer);
// 限制客户端可以向服务器上传多少字节数据
cloudServer.SetMaxUploadBytesPerClient(MAX_UPLOAD_BYTES);
cloudServer.SetMaxBytesPerDownload(MAX_DOWNLOAD_BYTES);
在客户端:
CloudClient cloudClient;
rakPeer->AttachPlugin(&cloudClient);
// 可选:提供一个分配器存储或释放下载行
Cloud_Allocator cloudAllocator;
// 可选:你会想要真正提供一个实例处理下载。
Cloud_ClientCallback clientCallback;
// 设置分配内存的回调函数
cloudClient.SetCallbacks(&cloudAllocator, &clientCallback);
// 每一个上传关联一对密钥
Cloud_DataKey dataKey;
dataKey.primaryKey="ApplicationName";
dataKey.secondaryKey=ID_PLAYER_COUNT; // Enumeration
unsigned char playerCount=16;
// 将数据上传到云,我们已经连接的服务器
cloudClient.Post(&dataKey, &playerCount, sizeof(playercount), serverAddressOrGuid);
// 下载我们刚刚上传的数据,clientCallback会在查询完成时调用OnDownload成员
Cloud_KeyQuery query;
query.keys.Push(dataKey, __FILE__, __LINE__);
cloudClient.Get(&query, serverAddressOrGuid);
使用的情况:
为应用程序赋值一个秘密的唯一名字,将它作为Cloud_DataKey 私有密钥。使得每一个游戏服务器维护一个共同的枚举类型的列表,这些枚举类型代表了查询的域,例如PLAYER_COUNT, PLAYER_LIST,和PLAYER_NAME。其他服务器的IP地址和RAkNetGUID属性是自动维护地。当一个游戏服务器的可以进行列表,使用CloudClient::Post()操作。查询其他的服务器,使用CloudClient::Get()操作,将你关心的CloudMemory_DataKey密钥传到Cloud_KeyQuery。如果在前面的调用中没有查询所有的密钥,可以使用CloudClient::Get()操作使用大量的密钥列表获得更多的细节信息;或使用重载的CloudClient::Get(),带有specificSystem参数,来获取更多详细信息。如果愿意,可以在CloudClient::Get()调用中定制updates,但通常这个操作不是必须的。游戏服务器如果从CloudServer(比如说crashing)断开了连接,那么它将被从云服务器列表清除。游戏服务器也可以手动调用CloudClient::Release()调用从列表中清除。
In-game 统计:
DataStructure::WeightGraph * GetGraph(void);
参考Samples\ConnectionGraph中的例子。
在目录之间自动发送不同文件的目录信息
如果有允许用户修改的内容(user-moddable),DirectoryDeltaTransfer.h就显得非常有用了。例如,如果每一个服务器有一个/skins 目录,那么你就可以运行这个插件将目录下载到客户端上。每一个没有特殊skin的客户端都会接收到这个目录信息。在下载过程中,可以通过user-supplied回调获得下载过程中的提示信息。DirectoryDeltaTransfer实际传输文件依赖于FileListTransfer插件。
使用:
1、连接插件到系统中,并连接到远端系统。
2、服务器和客户端:调用directoryDeltaTransfer.SetFileListTransferPlugin(&fileListTransfer);
3、服务器:设置应用程序目录:directoryDeltaTransfer.SetApplicationDirectory(“c:\myGame”);
4、服务器:设置下载目录:directoryDeltaTransfer.AddUploadsFromSubdirectory(“skin”);
5、客户端:下载目录调用:directoryDeltaTransfer.DownloadFromSubdirectory(“skin”,“downloaded\skins”, true, serverAddress, &transferCallback, HIGN_PRIORITY,0);
6、客户端:等待毁掉成员函数OnFileProgress().如果onFileStruct->fileIndex等于了onFileStruct->setCount,那么下载就算是完成了。
如果想要详细查看所有参数和可用的函数方法的详细信息,参考DirectoryDeltaTransfer.h头文件,以及Samples/DirectoryDeltaTransfer目录下的例子。
参考FileList.h,查看所有的函数和参数的一个完整的描述信息。
// 在一个指定的目录增加所有的文件
void AddFilesFromDirectory(const char *applicationDirectory, const char *subDirectory, bool writeHash, bool writeData, bool recursive, unsigned char context);
// 释放所有的内存
void Clear(void);
// 将所有的编码数据写入到bitstream
void Serialize(RakNet::BitStream *outBitStream);
// 从bitstream中读取出所有的编码数据,在反序列化之前调用Clear()
bool Deserialize(RakNet::BitStream *inBitStream);
// 给定已存的一组文件,从applicationDirectory指定目录中搜索相同的文件
// 对于每一个丢失或者不同的文件,将这个文件添加到missingOrChangedFiles。
// 注意:此处没有写入文件内容,如果alwaysWriteHash参数为真,仅仅写入hash。
void ListMissingOrChangedFiles(const char *applicationDirectory, FileList *missingOrChangedFiles, bool alwaysWriteHash, bool neverWriteHash);
// 返回需要写入与当前的FileList对象对比的文件。
void GetDeltaToCurrent(FileList *input, FileList *output, const char *dirSubset, const char *remoteSubdir);
// 假设FileList包含了大概的文件名列表,其中并不包含数据,为这些文件读取数据
void PopulateDataFromDisk(const char *applicationDirectory, bool writeFileData, bool writeFileHash, bool removeUnknownFiles);
// 将所有的文件写到磁盘,使用applicationDirectory参数传递目录前缀。
void WriteDataToDisk(const char *applicationDirectory);
// 假设文件数据已经放到了内存,增加一个文件。
void AddFile(const char *filename, const char *data, const unsigned dataLength, const unsigned fileLength, unsigned char context);
// 增加一个文件,从磁盘读取文件。
void AddFile(const char *filepath, const char *filename, unsigned char context);
// 删除所有存储在文件列表中的文件
void DeleteFiles(const char *applicationDirectory);
4. 当收到ID_FCM2_NEW_HOST消息时,如果有必要,请做主机迁移。
SetConnectionOnNewRemoteConnection()方法,如果调用参数是true,当ID_REMOTE_NEW_INCOMMING_CONECTION消息到达ConnectionGraph2插件时在远端系统调用RakPeer::Connect()方法。然而,这种方法通常行不通,因为多数的系统都在路由后面。因此,通常需要手动连接到其他的系统。
SetAutoparticipateConnections()调用传参通常是false,因为通常在将一个玩家接受进入游戏之前需要做一些玩家有效性验证。
直到主机计算完成,否则GetHostSystem()会返回你自己的guid,GetConnectedHost()方法会返回UNSIGNED_RAKNET_GUID。一旦主机的设定完成,就会得到ID_FCM2_NEW_HOST标识消息。主机通常是使用AddParticipant()方法加入到系统的计算机中,并且它是运行时间最长的一个。如果在游戏开始时需要立即知道谁是主机,那么在获得ID_FCM2_NEW_HOST消息之前不要开始游戏。
case ID_NEW_INCOMING_CONNECTION:
case ID_CONNECTION_REQUEST_ACCEPTED:
{
fullyconnectedmesh->AddParticipant(packet->guid);
if (fullyconnectedmesh->GetConnectedHost()!=UNASSIGNED_RAKNET_GUID)
{
// 已经知道了主机是哪个计算机,因此立即连接到游戏中
AddGameParticipant(packet->guid);
}
// 否则如果不知道主机,在调用AddGameParticipant()方法之前
// 等待ID_FCM2_NEW_HOST消息到达
}
case ID_FCM2_NEW_HOST:
{
RakNet::BitStream bs(packet->data,packet->length,false);
bs.IgnoreBytes(1);
RakNetGUID oldhost;
bs.Read(oldhost);
// 如果老的主机和新的相同,那么这时第一次收到这种消息
if (oldhost==packet->guid)
{
// 因为第一次知道了主机是哪个,游戏准备开始
SignalGameStart();
// 作为游戏参与者加入系统,这个参与者前面已经加入到了FullyConnectedMesh2
// 从ID_NEW_INCOMING_CONNECTION 和 ID_CONNECTION_REQUEST_ACCEPTED消息
DataStructures::List participantList;
fullyconnectedmesh->GetParticipantList(participantList);
for (unsigned int i=0; i < participantList.Size(); i++)
AddGameParticipant(participantList[i]);
}
}
GetParticipantList()和GetHostOrder()可以用于查找那些系统已经使用Addparticipant()方法加入的计算机的列表或顺序。
如果游戏没有立即开始连网,如果网络游戏现在是相关的,应该调用ResetHostCaculation()来初始化Host定时器。否则,用户可以单机玩游戏,连接到网络会话中,然后才会被考虑是否可以成为主机,尽管它是最后一个加入到网络会话的主机。在试图加入到网络房间或是大厅时是调用ResetHostCaculation()方法最好的时机。
读取主机:
ID_FCM2_NEW_HOST消息中编码了网络stream中的老主机的RakNetGUID。新主机地址信息写入了systemAddress以及packet的guid成员中。如果老主机和新主机是相同的,那么就没有老主机了(这是第一次主机设定计算)。
case ID_FCM2_NEW_HOST:
{
if (packet->guid==rakPeer->GetMyGUID())
printf("Got new host (ourselves)");
else
printf("Got new host %s, GUID=%s", packet->systemAddress.ToString(true), packet->guid.ToString());
RakNet::BitStream bs(packet->data,packet->length,false);
bs.IgnoreBytes(1);
RakNetGUID oldHost;
bs.Read(oldHost);
// 如果老主机与新的不同,那么这条消息表明丢失了到主机的链接
if (oldHost!=packet->guid)
printf(". Oldhost Guid=%s\n", oldHost.ToString());
else
printf("\n");
}
break;
}
查看插件使用实例,参考Samples/FCMHost。
如何实现在下面的例子中做了说明:
// 这个functor向数据库中异步加入一个title,完整的例子位于LobbyDB_PostgreSQLTest中
AddTitle_PostgreSQLImpl *functor = AddTitle_PostgreSQLImpl::Alloc();
printf("Adds a title to the database\n");
printf("Enter title name: ");
gets(inputStr);
if (inputStr[0]==0)
strcpy(inputStr, "Hangman EXTREME!");
functor->titleName = inputStr;
printf("Enter title password (binary): ");
gets(inputStr);
if (inputStr[0]==0)
strcpy(inputStr, "SECRET_PER_GAME_LOGIN_PW_PREVIOUSLY_SETUP_ON_THE_DB");
functor->titlePassword = AddTitle_PostgreSQLImpl::AllocBytes((int) strlen(inputStr));
functor->titlePasswordLength = (int) strlen(inputStr);
memcpy(functor->titlePassword, inputStr, functor->titlePasswordLength);
functor->allowClientAccountCreation=true;
functor->lobbyIsGameIndependent=true;
functor->defaultAllowUpdateHandle=true;
functor->defaultAllowUpdateCCInfo=true;
functor->defaultAllowUpdateAccountNumber=true;
functor->defaultAllowUpdateAdminLevel=true;
functor->defaultAllowUpdateAccountBalance=true;
functor->defaultAllowClientsUploadActionHistory=true;
// 将这个functor放入处理队列,那么在后面线程会处理它
// 参考LobbyDB_PostgreSQLTest, TitleValidationDB_PostgreSQLTest, RankingServerDBTest
// 工程,查看functor 的完整例子。
lobbyServer.PushFunctor(functor);
这段代码将带有各种属性的title加入到了数据库中,这些属性表明了可以在这个数据库中进行哪些类型的操作。在DependentExtensions\Lobby\TitleValidationDBSpec.h文件中
查看AddTitle_Data类声明,详细了解每一个参数的解释。其他的一些functor用于执行各种数据库操作。DependentExtensions/*_PostgreRepository目录中包含了这些实现,但是数据成
员和函数的注释文档包含在DependentExtensions\Lobby\*DBSpec.h文件中。测试程序和各种操作的例子可以再TitleValidationDB_PostgreSQLTest,RankingServerDB_PostgreSQLTest和
LobbyDB_PostgreSQLTest工程中找到。
参考LobbyServerTest例子,查看运行LobbyServer的控制台应用程序。
需要的文件(使用PostgreSQL):
所有依赖的文件(使用PostgreSQL):
PostgreSQL 8.2或更新版本,安装到C:\Program Files\PostgreSQL\8.2。如果安装目录不是这个,要修改工程的属性路径。不要忘记检查PostgreSQL安装中是否安装了开发工具,否则头文件和libs不会背安装。
Lobby Client(PC)
数据库的用户接口
快速开始:
1、将插件附加到RakPeerInterface实例上,并且连接到服务器。
2、使用LobbyClientInterfaceCB的派生实例调用SetCallbackInterface()方法。系统的一般设计是所有的调用都是异步进行,那么每一次调用都会将结果(成功或失败)返回到相应的注册回调。
3、如果还没有账户,调用RegisterAccount方法在大厅创建一个用户账户。等待LobbyClientInterfaceCB::RegisterAccount_Result()方法返回,查看它的查询是否成功。一种失败原因是名字已经使用,或者是不允许使用的用户名。
4、使用标识了你正在使用的数据库的信息调用SetTitleLoginId()方法(如果每一个大厅允许多游戏,那么可以再后面调用该方法)。这个机制应该硬编码进游戏中,当数据库加入到服务器时被返回。
5、使用你刚刚创建的账户(或先前存储的账户)获得调用Login()方法。等待LobbyClientInterfaceCB::Login_Result()方法返回,看是否调用成功。如果有好友,应该得到
LobbyClientInterfaceCB::FriendStatus_Notify()函数的调用返回,通知他们你已经上线。
6、使用DownloadRooms()方法获得所有房间的列表,给予搜索过滤器,紧接着是JoinRoom(),SetReadyToPlayStatus()和StartGame()方法的调用。或者使用QuickMatch()方法自动启动一个有指定玩家数据的游戏。
7、 一旦游戏开始,会得到LobbyClientInterfaceCB::StartGame_Notify()方法调用。这个方法可以给你提供所有玩家的IP地址,参与者,以及谁是仲裁者,用户处理人以及与游戏相关的其他的信息。在这个时候,可以从大厅断开连接。如果不断开,会被自动发回主大厅(任何房间外面)。
要查看一个完整的函数列表,以及函数参数,可以参考DependentExtensions\Lobby\LobbyClientPC.h。 要求的文件:
DependentExtensions\Lobby目录下的所有文件,出列LobbyServer.h 和LobbyServer.cpp文件
如果你可以使用控制台应用程序,则还需要Samples\LobbyClientTest中的所有文件。
初始化RakNet:
RakNet::Lobby2Message* startupMsg = messageFactory->Alloc(RakNet::L2MID_Client_Login);
((RakNet::Client_Login_PS3*) startupMsg)->cellSysutilRegisterCallback_slot = 3;
((RakNet::Client_Login_PS3*) startupMsg)->npCommId = NpConf::npCommId();
((RakNet::Client_Login_PS3*) startupMsg)->npCommPassphrase = NpConf::npCommPassphrase();
lobby2Client->SendMessage(startupMsg);
if (startupMsg->resultCode != RakNet::L2RC_PROCESSING && startupMsg->resultCode != RakNet::L2RC_SUCCESS)
printf("PS3 Login failed.\n");
messageFactory->Dealloc(startupMsg);
附加插件,注册回调
struct PS3Results : public RakNet::Lobby2Callbacks
{
// ...
} ps3Results;
// 附加插件,注册回调
messageFactory = new RakNet::Lobby2MessageFactory_PS3;
lobby2Client = new RakNet::Lobby2Client_PS3();
lobby2Client->AddCallbackInterface(&ps3Results);
lobby2Client->SetMessageFactory(messageFactory);
rakPeer->AttachPlugin(lobby2Client);
登陆:
RakNet::Lobby2Message* startupMsg = messageFactory->Alloc(RakNet::L2MID_Client_Login);
((RakNet::Client_Login_PS3*) startupMsg)->cellSysutilRegisterCallback_slot = 3;
((RakNet::Client_Login_PS3*) startupMsg)->npCommId = NpConf::npCommId();
((RakNet::Client_Login_PS3*) startupMsg)->npCommPassphrase = NpConf::npCommPassphrase();
lobby2Client->SendMessage(startupMsg);
if (startupMsg->resultCode != RakNet::L2RC_PROCESSING && startupMsg->resultCode != RakNet::L2RC_SUCCESS)
{
printf("PS3 Login failed.\n");
}
messageFactory->Dealloc(startupMsg);
读取异步登陆结果:
// 使用这些代码更新PS3Results类
struct PS3Results : public RakNet::Lobby2Callbacks
{
virtual void MessageResult(RakNet::Client_Login *message)
{
if (message->resultCode == RakNet::L2RC_Client_Login_CANCELLED)
{
printf("L2RC_Client_Login_CANCELLED");
}
else if (message->resultCode == RakNet::L2RC_Client_Login_CABLE_NOT_CONNECTED)
{
printf("L2RC_Client_Login_CABLE_NOT_CONNECTED");
}
else if (message->resultCode == RakNet::L2RC_Client_Login_NET_NOT_CONNECTED)
{
printf("L2RC_Client_Login_NET_NOT_CONNECTED");
}
else if (message->resultCode != RakNet::L2RC_SUCCESS)
{
printf("An error has occurred while unloading NetStartDialog.");
}
else
{
//Success
printf("Login Success");
}
}
} ps3Results;
重要:
1、使用这个系统,在发送Client_Login消息之前,需要启动RakNet。此外,在传递给Startup()方法SocketDescriptor参数中,要指定remotePortRakNetWasStartedOn_PS3。如果
所有的系统从一个相同端口启动,那么SocketDescriptor::remotePortRakNetWasStartedOn_PS3应该等于SocketDescriptor::port。
2、当RakNet没有运行时,需要手动调用Lobby2Client_PS3::Update()方法,否则回调函数不会被调用。
SystemAddress roomToJoinAddress;
// 端口地址必须事先知道,它从XNKID返回回来。
roomToJoinAddress.SetFromXSessionInfo(&msg360->roomToJoin, RAKNET_PORT);
char ipAddress[32];
roomToJoinAddress.ToString(false, ipAddress);
rakPeer->Connect(ipAddress, roomToJoinAddress.port, ...);
注意此处仍然需要调用Console_SignIntoRoom_360来加入房间,就和你使用Console_CreateRoom_360一样简单。
主机迁移:
1、在Client_Login之前调用XLiveInitialize()
2、加入如下的代码:
// 使用PeekMessage()函数,这样我们可以使用空闲时间来绘制屏幕
if( PeekMessage( &msg, NULL, 0U, 0U, PM_REMOVE ) != 0 )
{
if( FALSE == XLivePreTranslateMessage( &msg ) )
{
// 将消息作为XLivePreTranslateMessage继续处理,不要销毁这个消息
TranslateMessage( &msg );
DispatchMessage( &msg );
}
}
3、在shutdown()方法之后调用XLiveUnInitialize()方法
4、在处理器设置中,定义GFWL。
例子:
messageFilter.SetAutoAddNewConnectionsToFilter(0);
messageFilter.SetAllowMessageID(true, ID_USER_PACKET_ENUM, ID_USER_PACKET_ENUM, 0);
messageFilter.SetAllowMessageID(true, ID_USER_PACKET_ENUM+1, ID_USER_PACKET_ENUM+1, 1);
这个设置会自动增加所有的新连接到过滤器,设置为0。仅仅允许ID_USER_PACKET_ENUM消息到达。它也会创建一个新的过滤器集,将filterSet id 设置为1,它允许
ID_USER_PACKET_ENUM+1消息进入。
总是允许进入的消息(过滤它们没有任何作用):
ID_CONNECTION_LOST
ID_DISCONNECTION_NOTIFICATION
ID_NEW_INCOMING_CONNECTION
ID_CONNECTION_REQUEST_ACCEPTED
ID_CONNECTION_ATTEMPT_FAILED
ID_NO_FREE_INCOMING_CONNECTIONS
ID_RSA_PUBLIC_KEY_MISMATCH
ID_CONNECTION_BANNED
ID_INVALID_PASSWORD
ID_MODIFIED_PACKET
ID_PONG
ID_ALREADY_CONNECTED
ID_ADVERTISE_SYSTEM
ID_REMOTE_DISCONNECTION_NOTIFICATION
ID_REMOTE_CONNECTION_LOST
ID_REMOTE_NEW_INCOMING_CONNECTION
ID_DOWNLOAD_PROGRESS
参考Samples/MessageFilter中的完整例子。参考MessageFilter.h文件,了解所有的函数和参数的完整列表以及注释信息。
要完成NAT穿透需要提前确定NAT类型
NAT穿透的成功几率依赖于NAT使用的算法类型。
Full cone NAT:可以从先前使用过的端口上接收到任何数据报。可以从远端的Peer接收到第一个数据报。
Address-Restricted cone NAT:只要数据报源IP地址是先前我们发送过数据的系统,那么可以从端口上收到数据。如果两个系统同时发送数据报,可以接收到第一个数据报。否则,在我们发送一个数据报以后才会收到第一个数据报。
Port-Restricted cone NAT:与Address-restricted cone NAT类似,但是我们需要发送到正确的远端IP和正确的远端端口。到不同目的地的相同的源地址和端口使用相同的映射。
Symmetric NAT: 为每一远端目的地选择同的端口。到不同目的地的相同的源地址和端口使用不同的映射。因为端口号不同,第一次的外部穿透尝试就会失败。如果要使得这种模式工作,它要求有端口预测(MAX_PREICTIVE_PORT_RANGE > 1),以及路由按序选择端口。
Success Graph
Router Type |
Full cone NAT |
Address-Restricted cone NAT |
Port-Restricted cone NAT |
Symmetric NAT |
Full cone NAT |
YES |
YES |
YES |
YES |
Address-Restricted cone NAT |
YES |
YES |
YES |
YES |
Port-Restricted cone NAT |
YES |
YES |
YES |
NO |
Symmetric NAT |
YES |
YES |
NO |
NO |
NatTypeDetection插件允许你检测你自己的NAT的类型,以及NAT穿透是否可以完成。这个要在加入游戏之前确定。
NAT 类型检测算法:客户端实现:
1、创建一个插件实例:NatTypeDetectionServer nayTypeDetectionClient。
2、将插件附加到RakPeerIntance实例上:rakPeer->AttachPlugin( &nayTypeDetectionClient);
3、连接服务器,等待ID_CONNECTION_REQUEST_ACCEPTED消息。使用如下的代码来使用RakNet提供的免费服务器:rakPeer->(“8.17.250.34”, 60481, 0, 0);
4、使用服务器的SystemAddress调用DetectNATType。
5、等待ID_NAT_TYPE_DETECTION_RESULT消息
6、第一个字节包含了你的NAT类型。参考NATTypeDetectionCommon.h的NATTypeDetectionResult枚举类型。
7、为这个枚举类型提供了各种功能函数:CanConnect(), NATTypeDetectionResultToString(), NATTypeDetectionResultToStringFriendly()。
服务器实现:
1、在某地设置一台主机,不要使用NAT/或者位于防火墙之后。(RakNet提供了一个免费的服务器,地址为8.17.250.34:60481,但是为了使用方便,你可能想要维护你自己的主机,以便长时间运行)。服务器必须有足够多的外部IP地址,正如在NAT Type Detection Algorithm中所描述的那样。
2、创建一个插件实例:NatTypeDetectionServer natTypeDetectionServer。
3、将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin( &natTypeDetectionServer);
4、获得系统上的IP地址列表:
char ipList[ MAXIMUM_NUMBER_OF_INTERNAL_IDS ][ 16 ];
unsigned int binaryAddresses[MAXIMUM_NUMBER_OF_INTERNAL_IDS];
SocketLayer::Instance()->GetMyIP( ipList, binaryAddresses );
5、调用natTypeDetectionServer.Startup( ip2, ip3, ip4);
参考\Samples\NATCompleteClient中的例子。
什么是NAT?
NAT是解决地址转换问题的一种技术。它使得路由将路由之后的地址使用不同的端口映射到同一个目的地址。例如,如果路由后有两个计算机,但是仅仅有一个ISP提供商提供的IP地址,那么两个计算机将使用同一个IP地址,但是使用的是与应用程序真正赋值的端口号不同的一个端口。路由提供了一个它所做的映射的查询表,因此当远端计算机回复时,这个消息将根据这个映射表路由给NAT之后的对应本地主机。
NAT存在的问题是远端计算机无法发起向本地计算机发送消息的动作,因为本地计算机在路由中没有到这个远端系统的相应应用的映射存在。因此,如果两个计算机都是位于NAT之后,并且想要相互连接,哪一个计算机也无法完成连接操作。这个对于语音通信,端到端游戏或用户自己的主机做游戏主机的游戏等应用是有问题的。在原来处理的时候,用户必须进入路由配置界面,自己做一个地址映射设置。然而,在现代的应用程序中,用户一般不需要手动做这些工作了,主要是由于NatPunchthrough插件的使用。
NAT穿透概览
NatPunchthroughClient.cpp插件要求用户有自己的主机服务器,并且不能位于NAT之后,要运行客户端都能连接的NatPunchthroughServer.cpp插件。服务器会为每一个客户端寻找IP地址,告诉两个客户端同时连接到那个地址。如果连接失败,每一个客户端都要检查端口是否被其他的应用使用。如果依旧失败,再重复上述过程,以防后面对端口的估计打开前面的端口。如果依旧失败,插件将会返回ID_NAT_PUNCHTHROUGH_FAILED消息。
注意1:如果你的游戏使用Steam,RakNet也提供了SteamLobby,它使用V主机hosted by Valve,这种情况下不需要NATPunchthrough。
注意2:如果使用的是IPv6,则不需要使用NAT Punchtrough插件。
NAT穿透算法
1、Peer P1想要连接到Peer P2,他们都连接到了一个非NAT的系统F。
2、Peer P1使用P2连接到F的RakNetGUID参数调用OpenNAT()函数。
3、如果P2没有连接到F,则F返回失败消息,或者已经尝试穿透到P1。
4、F记录P1到P2的忙碌状态。如果P1或P2忙碌,那么连接请求就被放到了队列中。否则F请求来自P1和P2的最近使用过的端口。P1和P2标记为忙。
5、如果P1或P2没有响应,返回ID_NAT_TARGET_UNRESPONSEIVE消息,忙标记被删除。否则F同时发送打了时间戳的连接消息到P1和P2。
6、P1和P2这时各自进行操作。首先他们想各自的内部LAN地址发送多个UDP数据报。然后他们各自尝试F看到的外部的IP/Port。端口按照顺序被实验,根据
MAX_PREDICTIVE_PORT_RANGE。
7、如果任何时间从远端来了一个数据报,我们进入了PUNCHING_FIXED_PORT状态。按照算法的提示,数据报仅仅可以发送到那个IP/port组合上。如果我们的回应到达了远端系统,NAT被认为是双向连通,将会给用户发送ID_NAT_PUNCHTHROUGH_SUCCEEDED消息。
8、当NAT打开后,或者如果尝试了所有的端口,P1和P2发送消息到F,准备开始新一轮的穿透尝试。
算法有效性依赖于NAT的类型。It work with whichever NAT is the most permissive.
Full cone NAT: 可以从前面使用过的端口上接收任何数据报。会从远端Peer接收到第一个数据报。。
Address-Restricted cone NAT:只要数据报的源IP地址是我们已经发送过数据的系统的,就可以得到数据。否则,第一个数据报需要在我们发送了一个数据报之后收到。
Port-Restricted cone NAT:与Address-restriced cone NAT类似,但是我们需要发送到对应的远端IP和对应的端口。到不同的目的端的相同的源地址和端口使用同一个映射。
Symmetric NAT:每一个远端目的地都使用不同的端口。到不同目的地相同的源地址和端口使用不同的映射。因为端口不同,第一次外部穿透尝试将失败。要是这种NAT实现穿透,
需要端口预测(MAX_PREDICTIVE_PORT_RANGE>1),路由顺序选择端口。
Success Graph
Router Type |
Full cone NAT |
Address-Restricted cone NAT |
Port-Restricted cone NAT |
Symmetric NAT |
Full cone NAT |
YES |
YES |
YES |
YES |
Address-Restricted cone NAT |
YES |
YES |
YES |
YES |
Port-Restricted cone NAT |
YES |
YES |
YES |
NO |
Symmetric NAT |
YES |
YES |
NO |
NO |
客户端实现:
1、创建一个插件实例:NatPunchthroughClient natPunchthroughClient;
2、将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin(&natPunchthroughClient);
3、连接服务器,等待ID_CONNECTION_REQUEST_ACCEPTED消息。使用如下的代码,来使用RakNet提供的免费服务器:rakPeer->Connect(“8.17.250.34”, 60481, 0, 0);
4、使用想要连接到的系统的RakNetGUID参数调用OpenNAT()函数。为了获得NakNetGUID,你需要使用代码将你的RakNetGUID传递给服务器,上传到PHPDirectoryServer,或者使用一个插件来存储它,例如LightweightDatabase;natPunchthroughClient.OpenNAT(remoteGuid, serverSystemAddress);,为了读取你自己的RakNetGUID,使用RakPeerInterface::GetGuidFromSystemAddress(UNSSIGNED_SYSTEM_ADDRESS);
5、稍等片刻。要尝试所有的端口,大概要花费10秒,但是一般几秒钟就可以完成。如果你想要得到一些当前正在进行的任务的文本提示,可以调用
NatPunchthroughClient::SetDebugInterface();
6、ID_NAT_PUNCHTHROUGH_SUCCEEDED意味着穿透成功,那么你就可以连接到或者发送其他的消息到远端系统了。Packet::SystemAddress是你现在可以连接到的系统的地址。任何其他的ID_NAT_*消息都意味着穿透失败。参考MessageIdentifiers.h文件,详细了解每一个消息代码以及注释。
服务器实现:
1、在某地建立你自己的服务器,不要位于NAT之后或者防火墙之后。(RakNet提供的免费的服务器位于8.17.250.34:60481,然而你可能想要维护你自己的主机,以实现不间断运行)。
2、创建插件实例:NatPunchthroughServer natPunchthroughServer。
3、附加插件:rakPeer->AttachPlugin(&natPunchthroughServer);
4、不要忘记调用RakPeerInterface::Startup()和RakPeerInterface::SetMaximumIncomingConnections(MAX_CONNECTIONS);
使用NatPunchthrough类:参考例子\Samples\NATCompleteClient and \Samples\NATCompleteServer
UDP代理
使用一些质量较差或者家庭制作路由器,可能NAT穿透无法实现。例如,如果路由器为每一个外出的连接选择一个新的随即端口,那么仅仅允许进来的连接连接到这个端口,那么这样的端口永远不可能实现网络穿透。大约5%的情况下会出现这种情况。要处理这种情况,RakNet提供了UDPProxy系统。要使用UDPProxy,则需要使用一个服务器来在源端和目的端之间透明地路由消息。这个服务器也用于为不适用RakNet的系统路由UDP数据报(但你仍然需要使用RakNet进行转发)。NATPunchthrough和UDPProxy的结合使用应该可以使得任何系统以100%的概率连接到服务器,前提是你愿意提供足够的代理服务器来转发数据流。
UDP代理系统使用3个主要的类:
UDPProxyClient: 满足UDPProxyCoordinator的要求设置转发设置。这个类是客户端运行的类。
UDPProxyCoordinator:在服务器端运行,可以得到来自UDPProxyClient的所有请求。也可以获得所有的来自UDPProxyServer的登录。
UDPProxyServer:事实上做UDP数据报转发的类,通过UDPForwarder.cpp组件实例。
SystemAddress coordinatorAddress;
coordinatorAddress.SetBinaryAddress(“8.17.250.34”);
coordinatorAddress.port = 60481;
udpProxyClientRequestForwarding(coordinatorAddress, UNASSIGNED_SYSTEM_ADDRESS, p->systemAddress, 7000);
7、假设你被连接到了协调者,协调者也正在运行插件,在第二步创建的事件处理类应该在一秒或两秒之内获得回调。如果一个UDPProxyServer已经赋值来从一个在第六步指定的原系统转发数据报到目的系统,那么UDPProxyClientRequestHandler::OnForwardingSucess会返回。例如,连接到远端系统使用rakPeer->Connect(proxyIPAddress, proxyPort, 0, 0);
udpProxyServer.LogintoCoordinator(COORDINATOR_PASSWORD, coordnatorSystemAddress);
如果协调者插件是作为服务器插件运行在同一个机器上,可以使用如下的代码:
udpProxyServer.LogintoCoordinator(COORDINATOR_PASSWORD, rakPeer->GetInternalID(UNASSIGNED_SYSTEM_ADDRESS));
5、如果在时间发生时获得一个回调(特别是登录失败时)那么从RakNet::UDPProxyServerREsultHandler类派生,使用UDPProxyServer::SetResultHandler()函数进行注册。
使用UDP代理的状态图:
建立自己的服务器
服务器要求:
1、没有网络地址转换
2、没有防火墙,或防火墙上打开适当的端口
3、静态IP地址,Dynamic DNS也是满足这个要求的一种方法。
4、如果你的服务器连续运行时间超过一个月,使用__GET_TIME_64BIT进行编译。
5、需要足够的带宽处理所有的连接。
商业建立服务器解决方案
1、Hypemia
定位于世界范围内提供服务器,服务器是单个机器。
记录进入和发出的消息,用于调试
PacketLogger是一个插件,它可以打印系统所有进入和发出的消息,以便用于调试。它在必要地方解析消息,以表示消息是RPC还是一个时间戳。它也可以将数字的MessageID转换为对应的字符串。默认输出是由逗号分割文本,也可以作为CSV文件读取,在控制台中使用printf函数打印。
要改变输出目的地,从PacketLogger派生,然后重写WriteLog()方法。
除了PacketLogger类本身以外,如下的实现也包括在内:
PacketConsoleLogger – 与ConsoleServer一起使用
PacketFIleLogger – 记录到一个文件。调用StartLog()打开文件。
ThreadsafePacketLogger – 与PacketLooger类似,但是延迟到WriteLog()函数知道出了RakNet线程之后才会记录。如果你要记录重要的日志那么可以使用这个类。
RakVoice是RakNet的一个特色,这个插件可以实现实时语音通信,在8000 16 bit per sec的采样标准下,通信代价仅仅是每秒2200字节数据。这个插件使用Speex来进行语音的编码。RakVoice是一个插件类,使得编码,发送,解码和播放音频数据更加简单。
得到RakVoice实例,仅仅需要使用new分配对象,或者声明全局对象,更加容易处理。
RakVoice rakVoice;
因为RakVoice是一个插件,你需要将它附加到RakPeer对象上。
rakPeer->AttachPlugin( &rakVoice);
使用采样频率和处理缓存的大小来初始化这个类。如果采样频率使用8000hz, 512字节的缓存比较合适。缓存大小是编码时使用的缓存字节数,以及解码器返回的字节数。这个值通常是你想要锁定声音缓存的值。编码将数据报的数据减少大约75%。
rakVoice.Init( 8000, 512);
当数据从麦克进入语音缓存时,你应该调用SendFrame函数,要传递接收者系统的SystemAddress值,以及要编码的缓存的指针。与普通的发送API调用不同,你不能对音频数据报进行广播,因为每一个编码器和解码器都是配对的。因此,你必须指定SystemAddress,这样发送方知道使用哪个编码器。要进行广播,需要将数据分别发送给不同的接收方。注意输入缓存的大小必须与前面我们设置的缓存大小匹配。例如:
rakVoice->SendFrame( recipientSystemAddress, (char *) inputBuffer);
在其他的系统,数据到达时,依据你使用的声音引擎,你可能使用的是循环音频缓存,需要使用从RakVoice接收到的数据填充这些缓存。每一次播放引擎需要数据播放,应该调用ReceiveFrame()方法获得音频数据。这个方法会在传递的指针处写声音缓存,或者如没有新数据可以使用,则不播放声音。再提醒一次,记住返回的数据要与你在Init中设置的大小相同。
rakVoice.ReceiveFrame((char *)outbuffer);
最后要注意的一点是RakVoice要求在聊天会话中的所有的客户端都能够感知到所有其他的客户端的连接状态。原因如下:
1. 你需要使用指定的接收方调用SendFrame函数来广播数据。
2. 你可能想要调用CloseVoiceChannel()方法来停止与指定系统的通信。
RakVoice仅仅提供了一个方法编码和解码原始音频数据,以及与网络通信的方法。它并没有包含播放或录制声音的机制。然而,两个例子介绍了如何集成声音引擎:
\Samples\RakVoiceFMOD – 提供了一个将RakVoice与流行的FMOD声音引擎结合的例子。
\Samples\RakVoice – 如何将RakVoice与免费开源的PortAudio结合使用的例子。
PortAudio和Speex源代码在RakNet中都有,在RakNet根目录下都可以找到,以备用户重新编译用于其他的平台。这些是独立于RakNet的开源APIs,这些源码即不属于我,我也对此不进行维护。请参考他们各自的网页获得更多参考消息,以及他们的使用license。
常用的情况:
在端到端大厅中,这个可以用于存储用户什么时候准备好以及没有准备好的状态。当用户准备好时,游戏可以开始。
在端到端的轮转游戏(也即游戏是一局一局进行的,如斗地主)中,这个插件可以用于通知每一个用户已经准备好开始下一轮游戏。
在任何端到端游戏中,这个插件可以用于处理每一个人都等待的事件,例如所有玩家完成加载游戏level。
与FullyConnectedMesh2结合使用:
如果你正在使用FullyConnectedMesh2用于主机选定决策,ReplicaManager3依赖于这个插件,那么你需要延迟加入参与者,直到游戏中决定了主机服务器。下面是如何实现:
1、replicaManager3->SetAutoManageConnections(false, [false or true, depends on your preference]); //第一个参数设置为false,目的是让ReplicaManager3不能自动调用
PushConnection()方法,为何如此做,原因就是在确定游戏主机之前,你要让系统延迟,从而不让它参与到ReplicaManager3中来。
2、fullyConnectedMesh2->SetAutoparticipateConnections(false);
3、开始连接到所有的其他的系统。
4、接收到ID_CONNECTION_REQUEST_ACCEPTED或者ID_NEW_INCOMING_CONNECTION消息, 执行:
fullyConnectedMesh2->AddParticipant(packet->guid);
//如果连接到你的每一个系统是另外一个游戏实例,可以保持SetAutoparticipateConnections()函数默认是true,不需要调用fullyConnectedMesh2->AddPaticipant(packet->guid);
后//面也同样如此处理。原因是你在一些情况下想要连接到profiling工具或者其他的非游戏程序。
if (fullyConnectedMesh2->GetConnectedHost()!=UNASSIGNED_RAKNET_GUID)
{
//FullyConnectedMesh2要求一个完整的连接网拓扑,因此你需要在游戏实例中连接到每一个人。简单地使用rakPeer->Connect()调用就可以实现,或许根据你的需要,在系//统中需要加
入其他的系统,例如NAT Punchthrough。这里的代码在每一个人同时从大厅开始的情况下起作用,和游戏中加入的情况。参考帮助手册的Connecting获得更多信息。
DataStructures::List participantList;
fullyConnectedMesh2->GetParticipantList(participantList);
RM3AllocConnections(participantList);
}
5、接收到ID_FCM2_NEW_HOST 消息,执行:
BitStream bsIn(packet->data, packet->length, false);
bsIn.IgnoreBytes(sizeof(MessageID));
RakNetGUID oldHost;
bsIn.Read(oldHost);
if (oldHost==UNASSIGNED_RAKNET_GUID)
{
DataStructures::List participantList;
fullyConnectedMesh2->GetParticipantList(participantList);
RM3AllocConnections(participantList);
}
6、void RM3AllocConnections(DataStructures::List &participantList)
{
for (unsigned int i=0; i < participantList.Size(); i++)
{
Connection_RM3 *connection = replicaManager3->AllocConnection(rakPeer->GetSystemAddressFromGuid(participantList[i]), participantList[i]);
if (replicaManager3->PushConnection(connection)==false)
replicaManager3->DeallocConnection(connection);
}
}
与基于系统的组件集成:
1、相同的actor实例既有有相同的类型,序列以及组件数,也有需要提供相同的方法序列化类似的组件。实现Serialize()函数,首先Serialize()自己的英雄。然后按序序列化(持久化)组件。
2、如下是一个在端到端游戏中QuerySerialization()的例子,在端到端游戏中,主机控制对象加载等级(静态对象)。否则,创建实例的对等端序列化这个实例。然而,组件可以重写这个对象,可以让主机不关注对象的序列化。例如,如果一个玩家在地面上放了一个武器,如果我们自己的系统是主机系统,那么武器会返回RM3QSR_CALL_SERIALIZE消息。否则它返回RM3QSR_DO_NOT_CALL_SERIALIZE消息。
if (IsAStaticObject())
{
// 在关卡中加载的对象被主机序列化
if (fullyConnectedMesh2->IsHostSystem())
return RM3QSR_CALL_SERIALIZE;
else
return RM3QSR_DO_NOT_CALL_SERIALIZE;
}
else
{
// 允许组件重写序列化方法
for (int i=0; i < components.Size(); i++)
{
RM3QuerySerializationResult res = components[i]->QuerySerialization(destinationconnection);
if(res != RM3QSR_MAX)
return res;
}
return QuerySerialization_PeerToPeer(destinationconnection);
}
3、这个QueryConstruction()函数的变量,使得组件返回Replica3P2PMode。例如枪的那个例子,当枪在地上放着时,枪是由一个主机控制,或者在被捡起来时,由玩家的actor控制。如果组件返回了R3P2PM_MULTI_OWNER_CURRENTLY_AUTHORITATIVE或者是R3P2PM_MULTI_OWNER_NOT_CURRENTLY_AUTHORITATIVE,然后QueryConstruction_PeerToPeer()方法会使用这个值返回一个适当的值用于PM3ConstructionState。如果我们控制对象,那么QueryConstruction_PeerToPeer()方法会返回RM3CS_SEND_CONSTRUCTION消息,如果我们没有控制对象或没有人可以控制该对象,组件会返回RM3CS_NEVER_CONSTRUCT消息,如果其他人控制了对象,但是拥有者可以修改,那么组件会返回RM3CS_ALREADY_EXISTS_REMOTELY消息。
if (destinationConnection->HasLoadedLevel() == false)
return RM3CS_NO_ACTION;
if (IsAStaticObject())
{
if(fullyConnectedMesh2->IsHostSystem())
return RM3CS_ALREADY_EXISTS_REMOTELY;
else
return RM3CS_ALREADY_EXISTS_REMOTELY_DO_NOT_CONSTRUCT;
}
else
{
Replica3P2PMode p2pMode = R3P2PM_SINGLE_OWNER;
for (int i=0; i < components.Size(); i++)
{
p2pMode = components[i]->QueryP2PMode();
if(p2pMode != R3P2PM_SINGLE_OWNER)
break;
}
return QueryConstruction_PeerToPeer(destinationconnection, p2pMode);
}
virtual Replica3P2PMode BaseClassComponent::QueryP2PMode() {return R3P2PM_SINGLE_OWNER;}
virtual Replica3P2PMode GunComponent::QueryP2PMode(){
if (IsOnTheGround())
if(fullyConnectedMesh2->IsHostSystem())
return R3P2PM_MULTI_OWNER_CURRENTLY_AUTHORITATIVE;
else
return R3P2PM_MULTI_OWNER_NOT_CURRENTLY_AUTHORITATIVE;
else
if (WeOwnTheGun())
return R3P2PM_MULTI_OWNER_CURRENTLY_AUTHORITATIVE;
else
return R3P2PM_MULTI_OWNER_NOT_CURRENTLY_AUTHORITATIVE;
}
4、如果需要使用合成对象(compostion)而不是(derivation),参考ReplicaManager3.h中的Replica3Composite。它是一个模板类,仅仅有一个成员r3CompositeOwner。所有的Replica3结构可以从r3CompositeOwner查询。
对象序列化的方法:
例如:
void SetHealth(float newHealth)
{
if (health==newHealth)
return;
health=newHealth;
serializeHealth=true;
}
void SetScore(float newScore)
{
if (score==newScore)
return;
score=newScore;
serializeScore=true;
}
virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)
{
bool anyVariablesNeedToBeSent=false;
if (serializeHealth==true)
{
serializeParameters->outputBitstream[0]->Write(true);
serializeParameters->outputBitstream[0]->Write(health);
anyVariablesNeedToBeSent=true;
}
else
{
serializeParameters->outputBitstream[0]->Write(false);
}
if (serializeScore==true)
{
serializeParameters->outputBitstream[0]->Write(true);
serializeParameters->outputBitstream[0]->Write(score);
anyVariablesNeedToBeSent=true;
}
else
{
serializeParameters->outputBitstream[0]->Write(false);
}
if (anyVariablesNeedToBeSent==false)
serializeParameters->outputBitstream[0]->Reset();
// Won't send anything if the bitStream is empty (was Reset()).
M3SR_SERIALIZED_ALWAYS skips default memory compare
return RM3SR_SERIALIZED_ALWAYS;
}
virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)
{
bool healthWasChanged, scoreWasChanged;
deserializeParameters->serializationBitstream[0]->Read(healthWasChanged);
if (healthWasChanged)
deserializeParameters->serializationBitstream[0]->Read(health);
deserializeParameters->serializationBitstream[0]->Read(scoreWasChanged);
if (scoreWasChanged)
deserializeParameters->serializationBitstream[0]->Read(score);
}
基于对象变化的序列化:
例如:
void SetHealth(float newHealth)
{
health=newHealth;
}
virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)
{
serializeParameters->outputBitstream[0]->Write(health);
serializeParameters->outputBitstream[0]->Write(score);
// Memory compares against last outputBitstream write. If changed, writes everything on the changed channel(s), which can be wasteful in this case if only health or
score changed, and not both
return RM3SR_BROADCAST_IDENTICALLY;
}
virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)
{
deserializeParameters->serializationBitstream[0]->Read(health);
deserializeParameters- >serializationBitstream[0]->Read(score);
}
序列化每一个变量:
例子(也可以参考RplicaManager3例子工程):
virtual RM3SerializationResult Serialize(SerializeParameters *serializeParameters)
{
VariableDeltaSerializer::SerializationContext serializationContext;
// All variables to be sent using a different mode go on different channels
serializeParameters->pro[0].reliability=RELIABLE_ORDERED;
variableDeltaSerializer.BeginIdenticalSerialize( &serializationContext,serializeParameters->whenLastSerialized==0,&serializeParameters->outputBitstream[0]);
variableDeltaSerializer.SerializeVariable(&serializationContext, var3Reliable);
variableDeltaSerializer.SerializeVariable(&serializationContext, var4Reliable);
variableDeltaSerializer.EndSerialize(&serializationContext);
return RM3SR_SERIALIZED_ALWAYS_IDENTICALLY;
}
virtual void Deserialize(RakNet::DeserializeParameters *deserializeParameters)
{
VariableDeltaSerializer::DeserializationContext deserializationContext;
variableDeltaSerializer.BeginDeserialize(&deserializationContext, &deserializeParameters->serializationBitstream[0]);
if (variableDeltaSerializer.DeserializeVariable(&deserializationContext, var3Reliable))
printf("var3Reliable changed to %i\n", var3Reliable);
if (variableDeltaSerializer.DeserializeVariable(&deserializationContext, var4Reliable))
printf("var4Reliable changed to %i\n", var4Reliable);
variableDeltaSerializer.EndDeserialize(&deserializationContext);
}
快速开始:
1、从Connection_RM3派生,实现Connection_RM3::AllocReplica()方法。这是一个工厂函数,参数传递了类的标示符(例如名字),返回一个该类的实例。应该可以返回你游戏中的任何的
网络对象。
2、从ReplicaManager3派生,实现AllocConnection()和DeallocConnection()函数,返回在第一步创建的类。
3、从Replica3派生你的网络游戏对象。所有的纯虚方法必须实现,然而默认根据你的网络结构提供了Replica3::QueryConstruction()和Replica3::QueryRemoteConstruction()方法。
4、在本地系统上创建了一个新的游戏对象,将它传递给ReplicaManager3::Reference()方法。
5、本地系统上一个游戏对象销毁时,想要其他的系统知道该对象被销毁,调用Replica3::BroadcastDestruction()方法。
6、将ReplicaManager3作为一个插件附加到RakPeer上。
所有函数列表,以及函数参数的详细文档,参考ReplicaManager.h文件。
主要样例位于Samples\ReplicaManager3中。
ReplicaManager3和ReplicaManager2的区别
ReplicaManager3应该更简单,更加透明化
1、Connection_RM2::Construct现在是两个函数:Connection_RM3::AllocReplica()和Connection_RM3::DeserializeConstruction()。先前,在Connection_RM2::Construct中给的是原始数据,需要你自己创建和销毁对象的构建。现在AllocRelica会创建对象,DeserializeConstruction会为对象填充数据。
2、由于上述变化,NetworkID,creatingSystemGUID,和replicaManager等变量在你得到DeserializeConstruction回调之前,已经被设置为成员变量了。这个简化使用主要是因为对象已经准备被使用了。
3、同一个tick创建的对象前面是使用单独的消息发送。这意味着对于连接的用户,他可能在不同的远端游戏ticks接收到两个对象。如果开始工作之前,两个对象相互依赖,那将出现问题。现在,在同一个tick创建的对象在同一个消息中发送(被RakPeerInterface::Receive()调用定义,这个函数会调用PluginInterface2::Update()方法)。
4、先前,你需要使用一个特殊的连接工厂类调用ReplicaManager2::SetConectionFactory()方法创建Connction_RM2实例。现在,ReplicaManager3自己有纯虚函数AllocaConnection()和DeallocConnection()。
5、先前,对对象的访问是隐式的。如果对象不存在,调用RelicaManager2::SendConstruction,ReplicaManager2::SendSerialize,或者RelicaManager2::SendVisiblity可以注册实例。现在访问对象是显示的,使用ReplicaManager3::Reference()替代了ReplicaManager2的这三个调用。这是先前的混乱的源头,这些Send函数(或者Broadcast替代函数)没有校验对应的Relica2::Query*函数。Construction和Serialization函数现在没有了,通过自动的更新tick实现。
6、ReplicaManager2没有支持每一个连接不同的Serialization。ReplicaManager3做到了,通过从ReplicaManager3::Serialize函数返回RM3SR_SERIALIZED_UNIQUELY消息实现。如果对所有的连接,Serializations是相同的,那么返回RM3SR_SERIALIZED_IDENDICALLY更加高效。
7、ReplicaManager3不支持可见命令,例如ReplicaManager2::SendVisibility,以保持系统更加简单,更加透明化。要支持这个功能,增加一个布尔类型可见标记。在Serialize中转换一次,使用RM3SR_SERIALIZED_UNIQUELY来转化。在远端系统上,如果可见标记是false,隐藏这个对象。在发送系统上,如果可见标记是false,从ReplicaManager3::Serialize函数返回RM3SR_DO_NOT_SERIALIZE。你可以验证这个replica/connection对的可见标记是不是已经被SerializeParameter::lastSerializationSent改变了,lastSerializationSent里面包含了SerializeParameters::outputBitstream函数中最后传递的值。
8、RelicaManager3不支持Connection_RM2::SerializeDownloadStarted函数,使得系统更加简单和透明。可以再ReplicaManager3::SerializeConstruction函数中使用
destinationConnection->IsInitialDownload()函数进行验证。更多的复杂操作,也可以在注册远端系统时发送数据。参数autoCreate = false调用
RelicaManager3::SetAutoManageConnections函数。发送你的数据,然后调用ReplicaManager3::PushConnection函数。
9、QueryDestruction 不在存在。QueryConstruction现在返回值表明析构。
10、QueryIs*Authority不在存在,从ReplicaManager3函数中返回值达到相同的结果。
RakNet::BitStream bs(packet->data, packet->length, false);
bs.IgnoreBytes(sizeof(MessageID));
RakNetGUID endpointGuid;
bs.Read(endpointGuid);
unsigned short sourceToDestPort;
bs.Read(sourceToDestPort);
char ipAddressString[32];
packet->systemAddress.ToString(false, ipAddressString);
rakPeerInterface->EstablishRouting(ipAddressString, sourceToDestPort, 0,0);
注意:重新路由是自动进行的。当一个连接被重路由,你会得到ID_ROUTER_2_REROUTED返回值。SystemAddress地址已经改变,RakNetGUID不会改变。因此,当使用这个插件时,只能用RakNetGUID对象来访问远端系统。
特别有价值的一点是它具有将多人游戏会话记录到一个单独日志文件的功能。这功能使得开发者可以通过LAN查看多人游戏会话,仅仅在一台机子上就可以实现。与自动数据报记录和网络统计功能结合,这个功能使得高精确度的多人会话分析变得可能。
SQLiteServerLoggerPlugin
SQLiteServerLoggerPlugin从SQLiteLogger派生而来,增加了将从SQLiteClientLogger发来的日志写到日志文件的功能。启动这个插件,可以运行工程DependentExtensions\SQLite3Plugin\ServerOnly\SQLiteServerLoggerSample.cpp。在VS2005解决方案中,可以在Samples/SQLite3Plugin/SQLiteServerLogger。
新的SQL日志文件可以手动指定,或者自动创建。手动指定log文件,需要创建和注册这些文件,方法与SQLiteLogger相同。例如,如下代码是创建一个内存数据库。
if (sqlite3_open_v2(":memory:", &database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0)!=SQLITE_OK)
{
return 1;
}
static const char* DATABASE_IDENTIFIER="ConnectionStateDBInMemory";
sqlite3ServerPlugin.AddDBHandle(DATABASE_IDENTIFIER, database);
基于新的客户端会话自动创建日志文件,使用CREATE_EACH_NAMED_DB_HANDLE 或者CREATE_SHARED_NAMED_DB_HANDLE 作为参数调用方法
SQLiteServerLoggerPlugin::SetSessionManagementMode()实现。CREATE_EACH_NAMED_DB_HANDLE会为每一个发送日志的新的连接创建一个日志文件。CREATE_SHARED_NAMED_DB_HANDLE参数指定每一次新的连接发送日志在工作目录下创建一个新的日志文件,如果所有的连接都断开了,关闭该文件。参考SQLiteServerLoggerPlugin.h获得更多设置的详细信息。
使用TCP最小化服务器设置
PacketizedTCP packetizedTCP;
RakNet::SQLiteServerLoggerPlugin loggerPlugin;
loggerPlugin.SetSessionManagementMode(RakNet::SQLiteServerLoggerPlugin::CREATE_SHARED_NAMED_DB_HANDLE, true, "");
packetizedTCP.AttachPlugin(&loggerPlugin);
packetizedTCP.Start(38123,8);
当原始图像数据发送到服务器时,如果客户端(更多关于客户端的信息参考下面的客户端部分)指定了要压缩,服务器会自动压缩该图片。如果图片的数量或大小太频繁保持CPU使用率很高,服务器可以开启基于DXT压缩的硬件支持。使用SQLiteServerLoggerPlugin::SetEnableDXTCompression(true)方法开启这个设置。这要求服务器有一个硬件的3D加速卡。如果设置失败,服务器自动使用JPEG压缩代替。JPEG压缩生成更小的文件,那么如果CPU持续利用率很高,这也是一个不错的选择。
SQLiteClientLoggerPlugin
客户端的源码与你的应用程序集成。你需要将目录DependentExtensions\SQLite3Plugin\ClientOnly下的所有的文件加入到应用程序中,除了文件
SQLiteClientLogger_PacketLogger.h/.cpp 和 SQLiteClientLogger_RNSLogger.h/cpp等是可选的。他们是用于记录所有数据报日志的的插件,和RakNet统计。此外你的程序中还必须要加入DependentExtensions\SQLite3Plugin\SQLiteLoggerCommon.h/.cpp文件。
使用TCP最小化客户端的设置
PacketizedTCP packetizedTCP;
RakNet::SQLiteClientLoggerPlugin loggerPlugin;
packetizedTCP.AttachPlugin(&loggerPlugin);
packetizedTCP.Start(0,0);
SystemAddress serverAddress = packetizedTCP.Connect("127.0.0.1", 38123, true);
// 假设连接完成了,参考TCPInterface::HasNewIncomingConnection()方法
loggerPlugin.SetServerParameters(serverAddress, "functionLog.sqlite");
在这种情况下,我们假设服务器位于本地(127.0.0.1)的38123端口上。这个端口可以任意选择,它是你的服务器所选择的端口号。
“functionLog.sqlite”是传递给SQLite3ServerPlugin::AddDBHandle()方法的第一个参数的名字。然而,如果在服务器使用的是CREATE_EACH_NAMED_DB_HANDLE 或者
CREATE_SHARED_NAMED_DB_HANDLE参数,这个会被创建。对于应用程序使用相同的名字没有问题,因为这些文件是在不同的目录下存储的。事实上,对于多人游戏,你希望所有的系统使用相同的名字,那么所有的日志存进了相同的文件,可以按照时间相关进行对比。
要限制系统使用的内存的数量,调用函数SQLiteClientLoggerPlugin::SetMemoryConstraint(unsigned int constraint)进行设置。如果发送的数据非常多,对内存的限制设置很
有必要。否则,如果服务器崩溃,应用程序会消耗很多内存,直到得到服务器崩溃的通知。
记录日志(Logging)
#define FUNCTION_CALL_TABLE 'functionCalls'
#define FUNCTION_CALL_PARAMETERS_TABLE 'functionCallParameters'
所有的参数按照字符串存储。
记录一个函数调用,可以使用定义rakFnLog("function name", (parameterList));。
例如:
RakNet::SQLLogResult res;
int x=1;
unsigned short y=2;
float c=3;
double d=4;
char *e="HI";
res = rakFnLog("My func", (x,y,c,d,e));
RakAssert(res==RakNet::SQLLR_OK);
注意参数列表旁边的括号。对于宏自动记录在C++中使用的__FILE__和__LINE__文件这是必须的。
返回值不是必须进行检验,但是你第一次使用该系统检验是非常好的习惯。如果没有适当地设置系统,该调用会失败。
所有其他类型的日志:
记录日志使用宏定义rakSqlLog("table name", "column1,column2,column3...", (parameterList));
例如:
rakSqlLog("sqlLog", "handle, mapName, positionX, positionY, positionZ, gameMode, connectedPlayers", ("handle1", "mapname1", 1,2,3,"",4));
rakSqlLog("sqlLog", "handle, mapName, positionX, positionY, positionZ, gameMode, connectedPlayers", ("handle2", "mapname2", 5,6,7,"gameMode2",8));
rakSqlLog("sqlLog", "x", (999));
第一行代码会在数据库sqlLog中创建一个新的数据表。数据表有7列:handle, mapName, positionX, positionY, positionZ, gameMode, connectedPlayers。列的类型是基于参数列表类
型的,在这种情况下,两个字符串,紧跟着3个数字,后面是另外一个字符串,以及另外一个数字。
第二行会增加一行到数据表中。注意—如果在参数列表中的类型没有符合这点,例如,如果第一个参数想要发送一个数字而不是一个字符串,那么在服务器端的调用会失败。
第三行会增加一列’x’到数据表,类型是整形,值是999。
通过宏,__FILE__和__LINE__的值也自动发送到服务器。
Tick计数
二进制数据:
使用BlobDescriptor结构体将两个参数封装整合到一个值中。
rakSqlLog("blobTable", "blobColumn", ( &RakNet::BlobDescriptor(bytes,byteLength) ));
图象数据:
使用RGBImageBlob结构体将相关的参数封装整合进参数列表的一个值中。
RGBImageBlob(void *_data, uint16_t _imageWidth, uint16_t _imageHeight, uint16_t _linePitch, unsigned char _input_components, ImageBlobCompressionMode mode=DXT);
第一个参数是要发送的数据。第二个和第三个参数是图象维数。_linePitch参数是_data每行的字节数。_input_components参数对于RGB文件应该设置为3,如果是RGBA文件则设置
为4。mode,ImageBlobCompressionMode的值,应该是首选的编码模式。如果制定了DXT,但是不支持或在服务器设置失败,那么使用JPEG代替。
如下是一个来自SQLiteClientLoggerSample.cpp的例子。
rakSqlLog("gradient", "gradientImage", ( &RakNet::RGBImageBlob(bytes,4096,4096,4096*4,4) ));
先前的几个RakNet版本使用LightweightDatabase提供这项服务。它是一个数据库的C++实现,使用了适当的接口。然而,这个工具有一些性能问题,并且难于使用,灵活性也比SQL差很多。作为一个替代,SQLitePlugin出现了。它允许客户端在一个运行了SQLite的远端系统上(一个主机服务器)执行SQL语句。
为什么不直接使用SQLite?默认情况下,SQLite仅仅对于文件操作有效。游戏需要通过一个真正的网络连接来执行语句。SQLite3Plugin解决了这个问题,它通过使用PacketizedTCP或者RakPeerInterface来传输语句,将语句解析成为结构体,然后将它发送回给用户。因为它是一个RakNet插件,在玩家连接或断开连接时,你也会访问到时间回调。以及这些玩家的信息。
SQLite是一个公共域软件,包含在了RakNet发布包中,位于DependentExtensions\SQLite3Plugin。
对于客户端和服务器,将插件附加到PacketizedTCP或者RakPeerInterface实例上。从SQLite3PluginInterface派生,实现函数来执行这些时间发生时想要实现的处理。使用插件注册你的派生类。
仅仅在服务器端,在执行任何语句之前,你需要设置SQLite连接。下面的例子在内存中创建了一个SQLite连接:
sqlite3_open_v2(":memory:", &database, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, 0);
sqlite3_open_v2方法的细节,或其他的命令,参考他们的帮助手册http://www.sqlite.org/c3ref/open.html。
将打开的数据库使用ADDDBHandle()方法传递到插件中。ADDDBHandle()方法的dbIdetifier参数仅仅是一个使用指针查找关联,可以是任何你想要的值。让这个参数与数据库文件的地址相同时才会有意义。
在客户端,可以使用SQLite3Plugin::_sqlite3_exec方法发送语句。服务器会相应,结果的处理回调会被调用,使用SQLite3PluginResultInterface::_sqlite3_exec处理成功调用SQLite3PluginResultInterface::OnUnknownDBIdentifier用于处理错误 (未知数据库标示符)。
不要忘记转换用户输入!RakString::SQLEscape()方法可以用于实现这个功能。它会在任何引号,双引号,或者反斜线之前加一个反斜线。
这个系统默认是不安全的,默认情况下任何人可以执行任何查询。如果你想要安全性,你可以从SQLite3Plugin派生类,从而控制谁可以发送查询等。或者在数据库本身添加各种限制。
注释或者不注释SQLitePlugin.h文件中的SQLITE3_STATEMENT_EXECUTE_THREADED来控制是否在线程中执行语句。通常默认这一项是不注释的,因此在语句执行中阻塞并不阻塞你的程序。
因为这个系统的目的在于替代LightweightDatabase插件,样例SQLite3Sample.cpp显示了如何自动执行最重要的函数,这些功能是增加或删除连接和断开的连接的IP地址。你可以修改或从样例实现中派生,来增加更多你需要的功能。
参考样例工程DependentExtensions\SQLite3Plugin中本系统的实现。
TeamBalancer插件用于在游戏会话中给每一个玩家赋予一个团队编号。玩家默认没有团队,通过调用RequestSpecificTeam()或RequestAnyTeam()方法来加入团队。
操作包括:
SetTeamSizeLimits() —能够加入到一个给定团队号的玩家的最大数。
SetDefaultAssignmentAlgorithm() —定义如何自动向团队加入新的玩家—按序填充团队,或加入一个最小的团队。这个函数有RequestAnyTeam()方法触发。
SetForceEvenTeams() —使得所有团队变得更加平衡。有太多玩家的团队会随机地让一些玩家转移到玩家较少的团队中。
RequestSpecificTeam() —变为一个需请求的团队。如果这个团队满了,你的加入将被挂起,直到团队不满,或在想要加入的团队中的一个玩家想要与你交换团队为止。
CancelRequestSpecificTeam() —如果RequestSpecificTeam()还没有完成,这个函数会移除掉函数的请求。
RequestAnyTeam() —随即加入到一个团队中,基于默认的团队分配算法。
GetMyTeam() —如果已经加入到了某个团队,返回自己所在的团队。
SetAllowHostMigration() —如果是端到端,那么调用该函数时使用true参数,否则使用false参数调用该函数。
参考TeamBalancer.h头文件获得更多详细信息,以及每一个函数和参数的完整文档,以及函数返回给用户的消息。
参考例子工程Samples\TeamBalancer中本系统的实现。
使用:
// 将插件附加到RakPeerInterface实例上
rakPeer->AttachPlugin(&twoWayAuthenticationPlugin);
// 增加一个密码,真正的密码(Password0)与快速hash查询的标识(PWD0)相关
twoWayAuthenticationPlugin.AddPassword("PWD0", "Password0");
// Challenge我们连接的另外的一个系统
twoWayAuthenticationPlugin.Challenge("PWD0", remoteSystemAddressOrGuid);
如果另外一个系统也运行了two way Authentication插件,并且设置了相同的密码,你会得到ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_SUCCESS消息,另外一个系统会得到ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_SUCCESS消息。如果远端系统运行了这个插件,但是有不同的密码,他们会得到ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_FAILURE消息,你会得到ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_FAILURE消息。如果另外一个系统没有运行这个插件你会得到ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_TIMEOUT消息。
在这些情况下:
ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_SUCCESS
ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_SUCCESS
ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_TIMEOUT
ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_FAILURE
你可以读取消息所相关的那个challenge密码,使用如下的代码:
RakNet::BitStream bs(packet->data, packet->length, false);
bs.IgnoreBytes(sizeof(RakNet::MessageID));
RakNet::RakString password;
bs.Read(password);
系统所做的一切工作是在两个系统之间验证密码。它不能够开启或关闭任何的RakNet功能,或阻止在challenge期间发送其他的消息。然而,你可以将这个插件和MessageFilter插件配对使用,这样一个新的连接在验证之前无法发送任何消息。要实现这个功能,在附加本插件之前,将MessageFilter插件附加到RakPeerInterface实例上(事实上应该MessageFilter先加入)。调用MessageFilter::SetAutoAddNewConnectionsToFilter()这样可以过滤新的连接。通过调用MessageFilter::SetAllowMessageID()方法来保证two way authentication消息在相同过滤器的同一个channel被允许。当已经验证了一个连接,使用MessageFilter::SetSystemFilterSet()修改系统的channel。
参考Samples/TwoWayAuthentication中的完整例子。参考TwoWayAuthentication.h头文件查看函数的完整文档和参数说明。
#include “DbgHelp.h”
连接DbgHelp.lib和ws2_32.lib库。
有时当你不在特殊计算机之前时,命令行控制台控制服务器是非常有用的。由于服务器确定时会有用,这个主机与该服务器控制地不同的情况下就会有用了。或者或许你有许多服务器需要控制,你想要通过一个脚本控制这些服务器。ConsoleServer,CommandParserInterface,和TransportInterface是三个一起工作类满足这些要求。
ConsoleServer
ConsoleServer类,在ConsoleServer.h中可以找到,包含了CommandParserInterface类的列表,使用ConsoleServer::AddCommandParser()函数加入。每一个游戏tick通过调用ConsoleServer::Update(),所有的CommandParserInterface类处理所有的进来的输入信息。
CommandParserInterface
命令解析器是一个类,它可以操作命名的注册命令集。CommandParserInterface是一个一个基类,从这个基类你应该为每一个功能解析器派生功能。例如,RakNetCommandParser.h暴露了在RakPeerInterface中可以调用的函数。RakNetTransportCommandParser暴露处理RakNetTransport类的函数,这个类事实上被ConsoleServer用于发送数据。
TransportInterface
TransportInterface类提供了函数给ConsoleServer发送字符串。当前有TransportInterface类的两个实现:TelnetTransport和RakNetTransport。TelnetTransport使用TCPInterface.h来回复给一个远端Telnet终端。RakNetTransport通过RakPeer实例发送字符串,需要使得secure connections可用。
来自CommandConsoleServer中的例子
ConsoleServer consoleServer;
TelnetTransport tt;
RakNetCommandParser rcp;
LogCommandParser lcp;
consoleServer.AddCommandParser(&rcp);
consoleServer.AddCommandParser(&lcp);
consoleServer.SetTransportProvider(ti, port);
lcp.AddChannel("TestChannel");
while (1)
{
lcp.WriteLog("TestChannel", "Test of logger");
consoleServer.Update();
// Game loop here
}
事实上这个是很简单的。你有一个控制台服务器实例,传输接口的实例(Either TelnetTransport 或 RakNetTransport),以及你的命令解析。调用ConsoleServer::AddCommandParser用于每一个parser,ConsoleServer::SetTranseportProvider()用于telnet或RakNet,每一个tick调用ConsoleServer::Update()一次。这里我还加了一个输出信道到LogCommandParser,每一个tick输出到log一次。
假设服务器已经启动,你可以按照如下步骤连接:
1、从start菜单启动telnet
2、使用telnet连接到服务器
3、系统应该处理每一件事情
RakNetTransport
安全控制台连接
Telnet很容易连接,但是并不安全。如果你想要发送密码或其他机密数据,你应该在服务器上使用RakNetTransport来代替Telnet。这个形成了一个另外的命令解析器,RakNetTransportCommandParser,这个命令解析器可以增加功能,在RakNetTransport内部改变RakPeer实例的密码。这种方法非常适合于远程用户在没有连接到游戏时连接到命令解析器,游戏和命令行解析器可以有不同的密码。
对于客户端,CommandConsoleClient例子是一个控制台应用程序的实现,这个应用程序使用RakNet连接到RakNetTransport。
创建你自己的命令解析器
预定义命令或将命令字符串直接传递到你的脚本系统中
要增加一个新的命令解析器,首先从CommandParserInterface派生一个类,就如RakNetCommandParser.h中的做法一样。你需要重写(覆盖)OnCommand,GetName,和SendHelp方法。此外,任何与你的游戏通信的函数,例如SetGamePointer()或SetLogger(),你应该你自己增加他们。
1、在新类的构造函数中,在添加每一个你想要添加的命令时,调用RegisterCommand方法,如下是RakNetCommandParser.cpp中的实例:
RegisterCommand(4, "Startup","( unsigned short maxConnections, int _threadSleepTimer, unsigned short localPort, const char *forceHostAddress );");
第一个参数,4,是传递给函数的参数数量。第二个参数,”Startup”是命令的名字,会在命令列表缩略显示中显示出来。第三个参数,”unsigned short maxConnections…”,
定义了helpString,这个字符串是在对一个特别的命名命令调用了help时显示。
给出语法格式,ConsoleServer会验证传递参数的正确数,当调用一个特殊命令时这并不是应该有的情况,那么给用户返回错误信息。
2、在OnCommand()方法中,将命令字符串与你注册的命令进行对比,采取合适的响应动作。
if (strcmp(command, "Startup")==0)
{
SocketDescriptor socketDescriptor((unsigned short)atoi(parameterList[1]), parameterList[3]);
ReturnResult(peer->Startup((unsigned short)atoi(parameterList[0]), atoi(parameterList[2]), &socketDescriptor, 1), command, transport, systemAddress);
}
OnCommand方法的返回结果值当前并没有使用,因此仅仅返回true即可。
ReturnResult是一个函数,可以有选择地调用该函数,它将给请求系统返回一个字符串。
3、实现GetName()方法,返回命令解析器的名字。这个名字会在命令解析器列表中显示。
4、实现SendHelp()方法,当你查询你自己的特殊解析器时,它可以返回一些额外的信息。如果你的解析器由于前置条件失败,不能够运行,那比较好的做法是返回一个通知信息,以便调用者可以了解情况。
未知或可变参数数
#define COMMAND_DELINATOR ' '
#define COMMAND_DELINATOR_TOGGLE '"'
通过C++发送邮件的简单类
EmailSender类,可以在EmailSender.h中找到该类,这个类是一个仅仅有一个函数Send(…)的简单类,这个函数用于使用一个mail服务器发送email。它被内在地用于CrashReporter类来为未被监控的服务器发送邮件。参考EmailSender.h文件,了解每一个参数的完整描述。
参考Samples/SendEmail工程中SendEmail的例子。
这个类已经验证了可以与Gmail 的POP服务器一起使用,如果你有一个Gmail账户,即使没有你自己的邮件服务器,也可以发送邮件。例子的设置你需要按照默认不变。同时需要取消RakNetDefines.h中的OPEN_SSL_CLIENT_SUPPORT的注释,按照Gmail的要求,TCP连接是由SSL来完成。
安全编码和解码字符串
StringCompressor类位于StringCompressor.h文件中,它可以以一种安全的方式编码和解码字符串,避免过度运算。
发送方:
const char *str = "My string";
stringCompressor->EncodeString(str,TRUNCATION_LENGTH,&bitStream,languageId);
接收方:
char buffer[TRUNCATION_LENGTH];
stringCompressor->DecodeString(buffer, TRUNCATION_LENGTH, &bitStream, languageId);
第一个参数是要编码或解码的字符串。第二个参数是写或读的最大字符数。如果字符串数大于这个参数,那么会按照本参数的大小发送字符串。第三个参数是要写入或读出的bitstream。最后一个参数表明使用什么样的字符频率表,两个系统上的两个表必须是相同的。
字符串会被该类根据字符频率表使用胡夫曼编码进行压缩,由languageId指明该算法。默认的频率表参数使用0,它是在StringCompressor.h中使用englishCharacterFrequencies变量静态定义。要想加入你自己的频率表,使用想要用的languageID参数,来调用GenerateTreeFromStrings()方法设置。
如果你的应用程序使用的是CString类,可以在StringCompressor.h中定义_CSTRING_COMPRESSOR来使该类支持CString字符串的压缩。
类似地,如果你的应用程序使用的是std::string,可以在StringCompressor.h中定义_STD_STRING_COMPRESSOR来使该类支持对std::string字符串的压缩。
StringTable概述
StringTable类是一个与StringCompressor类非常像的一个类,增加了一个AddString方法。
void AddString(const char *str, bool copyString);
str是要加入的字符串。
如果你的字符串不是常量,copyString应该设置为true,如果在内存中是静态的,则设置为false(这个时候仅仅存储一个指针)。
AddString会检查内部的数据数组,查看是否这个字符串已经被注册了。如果没有,它会内在地为该标示符存储两个字节的标示符,用该标示符来代表这个字符串。那么进一步的发送将仅仅发送两个字节的标示符,而不是发送整个字符串,这样如果字符串有三个字符或更多,那么字符串的发送速度更快,也更加节省带宽。如果发送一个没有使用AddString加入的字符串,那么函数的动作与你直接调用stringCompressor一样,但是会多花费额外的一位。
两个系统必须有相同的注册字符串集合,并且是按照相同的顺序注册,同时还要求系统在对应的发送和接收调用中使用StringTable和StringCompressor。
连接到Telnet、HTTP服务器、mail服务器或其他
TCPInterface类可以在TCPInterface.h文件中找到,它是一个功能类用于使用TCP协议在一些必要情况下进行连接。连接过程和RakPeerInterface.h类似,但是TCPInterface类中Receive()函数返回接收到的数据,第一个字节不是一些特定的标示符。
为了获得连接状态更新,使用HasNewConnection()方法和HasLostConnection()方法。
在RakNet中没有指定的TCPInterface类的例子,但是可以参考TelnetTransport.cpp中的做法。
函数:
// 在指定的端口上启动服务器
bool Start(unsigned short port, unsigned short maxIncomingConnections);
// 停止TCP服务器
void Stop(void);
// 使用指定的端口连接到指定的主机
SystemAddress Connect(const char* host, unsigned short remotePort);
// 发送字节流
void Send( const char *data, unsigned length, SystemAddress systemAddress );
// 返回接收到的数据
Packet* Receive( void );
// 断开一个玩家/主机地址的连接
void CloseConnection( SystemAddress systemAddress );
// 解包Receive返回来的数据包
void DeallocatePacket( Packet *packet );
// 新连接的排队事件
SystemAddress HasNewConnection(void);
// 丢失的连接的排队事件
SystemAddress HasLostConnection(void);
使用共享的Web主机给出游戏列表
Lightweight数据库插件功能强大,但是它要求一个一台专用的服务器运行RakNet实例。在有些情况下,这个要求无法满足,并且运行专用服务器额负担也是不可取的。对于这些情况,RakNet提供了一个DirectoryServer.php,它可以再Sample\PHPDirecotory目录下找到。
设置web服务器,仅仅需要将Samples\PHPDirectoryServer\Directory.php上传到你的PHP的web主机即可(webhost可以是任何标准的web服务器)。转到你新上传的webpage,点击显示按钮,输入密码和其他的想要的设置参数。
C++代码更加复杂。首先,你需要定义一个TCPInterface的实例,并且启动该实例。这是通常的TCP通信必备。
TCPInterface tcp;
tcp.Start(0, 64);
第二,还需要一个HTTPConnection实例,它用于通过TCPInterface实例与webpages进行通信。
HTTPConnection httpConnection(tcp, "jenkinssoftware.com");
第三,你需要定义一个PHPDirectoryServer实例。它用于解析和与指定的DirectoryServer.php通信。
PHPDirectoryServer phpDirectoryServer(httpConnection, "/raknet/DirectoryServer.php");
转到http://www.jenkinssoftware.com/raknet/DirectoryServer.php可以实际看一看页面效果。
然后你可以设置列,上传你的表或下载已经存在的表:
// 使用columnname / value设置域值
phpDirectoryServer.SetField("beehive","inthewater");
// 上传前面设置的域,要用到游戏名称,游戏端口,和密码
phpDirectoryServer.UploadTable(50, "Game name", 1234, "");
// 下载上传的服务器
phpDirectoryServer.DownloadTable("");
更新系统,应该从TCPInterface向两个接口传递数据包,并且要调用Update()。TCP数据报没有包含一个完整的webpage响应,webpage可能包含错误代码,来自webpage的信息不全是与我们的服务器相关的使得事情变得比较复杂。例子详细说明了如何处理,如下的代码是一个简要的摘录:
Packet *packet = tcp.Receive();
if(packet)
{
// 在这个例子中,这一行不是必须的,但是如果我们正在使用TCPInterface,我们想要确保我们仅仅给它一个消息表明我们我们的本条连接。
if (packet->systemAddress==httpConnection.GetServerAddress())
{
// 从一个web服务器一条请求可能返回多个数据报。当最后的数据报达到时, ProcessFinalTCPPacket会返回真
if (httpConnection.ProcessFinalTCPPacket(packet))
{
int code;
RakNet::RakString data;
/// 检查请求已经处理,没有错误代码
if (httpConnection.HasBadResponse(&code, &data)==false)
{
// 好的响应,让PHPDirectoryServer类处理该数据
// 如果resultCode不是空字符串,那么我们得到了一些东西而非表
// (例如删除行成功提示,或消息仅仅是HTTP的并不是本类的数据).
HTTPReadResult readResult = phpDirectoryServer.ProcessHTTPRead(httpResult);
if (readResult==HTTP_RESULT_GOT_TABLE)
{
/// 获得了一个内部存储的表,打印出来。
char out[10000];
const DataStructures::Table *games = phpDirectoryServer.GetLastDownloadedTable();
games->PrintColumnHeaders(out,sizeof(out),',');
printf("COLUMNS: %s\n", out);
// 打印表的每一行
for (unsigned i=0; i < games->GetRowCount(); i++)
{
games->PrintRow(out,sizeof(out),',',true, games->GetRowByIndex(i,NULL));
printf("ROW %i: %s\n", i+1, out);
}
}
}
}
}
// 释放数据报
tcp.DeallocatePacket(packet);
}
httpConnection.Update();
phpDirectoryServer.Update();
某些列被保存,给你返回一个查询。这些列名字不允许用于终端用户,如果参数使用会发生assert(断言)。
// 带有这个头的列包含了游戏的名字,传递给UploadTable()
static const char *GAME_NAME_COMMAND="__GAME_NAME";
// 带有这个头的列包含了游戏的端口,传递给UploadTable()
static const char *GAME_PORT_COMMAND="__GAME_PORT";
// 从PHP服务器返回,表明了这行最后更新的时间。
static const char *LAST_UPDATE_COMMAND="__SEC_AFTER_EPOCH_SINCE_LAST_UPDATE";
// 删除一行的命令
static const char *DELETEME_COMMAND="__DELETE_ROW";
// 传递给PHP服务器,它是作为密码
static const char *GAME_PASSWORD_COMMAND="__PHP_DIRECTORY_SERVER_PASSWORD";
// 这一列传递给PHP服务器,作为在自动推出之前多长时间列出这个服务器。
static const char *GAME_TIMEOUT_COMMAND="__GAME_LISTING_TIMEOUT";
__SYSTEM_ADDRESS参数被返回表明外部的一个IP用于上传webpage。
注意由于技术限制,在同一个时间仅仅允许一个上传。如果你的服务器仅仅管理一个游戏,那么这个限制不是问题。当一个上传正在进行,如果调用了PHPDirectoryServer::UploadTable()方法会覆盖掉这前一个上传。当HTTPConnectionBusy()返回false时可以执行另外一个上传过程。
3D Interpolation说明
Ogre 3D interpolation样例使用了图形引擎Ogre 3D来渲染爆米花爆的情景。
服务器有一个一束爆米花核心,它向外弹出爆米花,漫天乱飞。一会所有爆米花都删除了。
客户端是一个静默(dumb)客户端,因为客户端不做任何动作,也没有处理核心泼洒或弹出的细节。
Ogre的特殊点:
如何在显示和可视位置使用一个帮助类TransformationHistory插补。给定一个过去的时间,使用插补它会告诉你那时你的位置。如果你按下空格,你会看到客户端非插补地运行,这个时候画面其实是起伏不断的,因为它每秒仅仅发送4次。放开空格键,图形再次变得平滑了。
转化为RakNet的一部分:
ReplcaManager3类,可以自动处理爆米花核心的创建、删除,以序列化等。
要运行它,在同一个电脑上启动两个实例。在其中一个用作服务器的实力上输入’s’,用作客户端的实例上输入’c’。按下空格键,观察客户端没有插补地运行的效果。
如果你想要在因特网上运行。修改硬编码的SERVER_IP变量为你的服务器的地址。
这个代码可以在DependentExtensions\Ogre3DInterpDemo目录下找到。
依赖:
Ogre 3D必须安装。它保证了你有OGRE_SDK作为环境变量。如果没有,按照工程属性进行修改。
以第一人射击视角说明端到端连接性
FPS样例使用了Irrlicht游戏引擎来四处移动角色和子弹。
运行这个实例,下载免费的游戏引擎Irrilicht。默认情况下,它安装在了C:\irrlicht-1.6目录下。
在解决方案中,打开Samples/3D Demos/Irrlicht Demo工程,右击并编译。
多数的网络代码可以DependentExtensions\IrrlichtDemo目录下的RakNetStuff.cpp中找到。要查看代码是如何实现的,可以参考同一个目录下的readme.txt文件。由于例子是端到端的,它要求NAT穿透服务器运行着。Jenkins软件提供了一个免费服务器,被DEFAULT_NAT_PUNCHTHROUGH_FACILITATOR_IP指向。如果你不能连接,可能是用于测试的服务器已经关掉了。
你也可以运行你自己的服务器,它就是NAT穿透样例中的代码。
这个代码位于DependentExtensions\IrrlichtDemo中。
依赖:
Irrlicht必须安装。例子假设你安装在c:\irrlicht-1.6。它同样还需要irrKlang用于语音,它是默认提供的插件。
Raknet是一个网络引擎,为网络通讯传输提供了完美的解决方案。
本文介绍了源码功能以及使用方法,参考了官方文档。