首页 > 代码库 > Android RakNet 系列之二 功能介绍
Android RakNet 系列之二 功能介绍
简介
RakNet 已经成功地在Android平台上测试成功。RakNet的文档很多,实现起来很简单,下面对Raknet功能细节进行详细了解。
详情
1、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的算法进行路由 |
2、Raknet源文件介绍
项目源码总共有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 | 空 |
2、RakNet 如何使用?
头文件
#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);
3、系统概览
系统结构
RakNet大致上说定义了3个库:网络通信库、网络通信的插件模块、扩展支持功能。
网络通信是用两个类来提供的。RakPeer和TCPInterface。RakPeer是游戏使用的主要的类,它基于UDP。它提供了连接,连接管理,拥塞控制,远程服务器检测,带外数据,连接统计,延迟,丢包仿真,阻止列表和安全连接功能。
TCPInterface是一个TCP的包装类,用于和基于TCP的外部系统通信。例如,EmailSender类,用于报告远程系统的本亏消息。有一些插件也支持它,建议用于文件传输,例如自动补丁升级系统。
RakNet中的插件模块是附加到RakPeer或PakcktizedTCP实例的类。基类更新或过滤或注入消息到网络流的时候,附加的插件自动更新。插件提供了自动功能,例如在端到端环境下的主机确定,文件传输,NAT跨越,语音通信,远程调用,游戏对象复制。
扩展支持的功能包括崩溃报告,通过Gmail的pop服务器发送邮件,SQL日志服务器,和基于服务器列表的PHP。
RakPeer内部结构
RakPeer.h 提供了UDP通信的基本功能,期望大多数的应用程序使用RakPeer而不是TCP。开始时,RakPeer启动两个线程——一个用于等待到来的数据包,另外一个用于执行周期的更新,例如检测连接丢失,或pings。用户制定了最多连接数,以及远程系统结构体的数组内在地分配成了这么个大小。每一个连接或连接尝试都赋值了一个远程系统结构体,这个远程系统结构体包含了一个类来管理两个连接系统之间的连接控制。连接是由SystemAddress或RakNetGuid来标识,后者是随即生成的,每一个RakPeer的实例对应于一个唯一的GUID。连接是通过包涵了连接请求数据和一个“离线消息”标识符的UDP消息来建立。“离线消息”标识符用于区分真正的离线消息和与离线消息相似的连接消息。连接请求在一个短的时间短内重复发送,以免数据包丢失,并且如果支持,可以使用一个递减的MTU用于MTU路径检测。
连接请求到来时,RakPeer传输内部状态数据,例如RakNetGUID,在禁止列表中检验这些连接,重连列表。和其他的安全措施。如果连接是安全的,安全连接协议启动,发送额外的数据。一旦成功,通知用户连接成功,使用ID_CONNECTION_REQUEST_ACCEPTED或ID_NEW_INCOMING_CONNECTION。失败条件也是以一个类似的方式返回。
从用户发出的到连接系统的消息在底层复制,并且进行内部缓存。如果外出信息加上头比MTU大,这个消息就需要在内部进行分片。在一段内部的发送数据整合为一个单包,按照拥塞控制和MTU大小的出散户限制发送。对没有收到ACK的数据包需要进行重发。出现丢失数据包序列号的情况下,要发送NAKs。消息是不可靠发送的,在一个用户定义的门限检测到时不能发送。消息按照优先级发送,Acks没有进行拥塞控制。然而重新发送的数据和新的数据发送中,重发要比新的发送有更高的优先级。
当开始时,发来的数据到达阻塞的接收线程。数据包到来时,时间戳立即记录,然后将数据推进一个处理线程管理的线程安全的队列。给处理线程发出信号,这样如果线程休眠了,他可以立即处理消息,或者在下一个可用的时间处理消息。
接收的数据包要经过字节序列检测,以表明是否发送者认为它是正确的。此外,源IP地址也会检测。如果消息标明是非连接的数据包,发送者没有连接到我们系统,这样消息会针对一个非常小的接收类型范围进行检测,例如连接请求,带外消息。如果一个消息表明了是已经连接的连接的消息,发送系统与我们的系统是连接的,那么它是通过ReliabilityLayer类进行处理的,用于拥塞控制和其他的通信相关的信息(ACKs,NAKs,重发,大数据包的组装)。
连接消息首先由RakPeer来处理。当前仅仅用于周期pings,检测丢失连接用户不应该在给定的门限内发送数据。所有的其他消息有插件处理或返回给用户。调用RakPeer::Receive()一次运行所有的插件的更新函数,并且返回一个消息。返回给用户的消息是从RakPeer::Receive返回的,每一次调用一条消息。需要在一个循环中调用一个循环获取所有的消息,知道没有消息为止。
其他的系统
NetworkIDObject类为系统提供了访问共同对象的功能,用于对象成员远程函数调用。每一个对象有一个64位的随即数赋值,可以用于通过一个哈希查找指针。SystemAddress结构体是RakNet用于代表远程系统的结构体。它是IP地址和使用的系统的端口的二进制编码,支持IPv4和IPv6。
BitStream类位于BitStream.h中,是RakNet直接支持的。即是一个用户类,也是一个内部类。它主要用于写一位数据到流中,以及自动的Endian交换,可以通过注释在RakNetDefines.h中的__BITSTREAM_NATIVE_END实现。
4、带宽消耗
1、Post3.6201
每个数据报: 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字节用于分片好的索引,为了提高性能不进行压缩
2、更早的3.x系列
每一个数据报1位用于位标记
8字节用于时间戳,用于拥塞控制中使用的RTT值计算
每一条消息
4字节用于数据长度
4字节用于序列号,用于防止返回给用户重复消息
4位用于位标记
If(UNRELIABLE_SEQUENCED,RELIABLE_SEQUENCED,RELIABLE_ORDERED)
A 4字节用于序列号,以有序识别消息
B 4位用于信道排序
if (message over MTU)
A 4字节用于分片数,但是压缩,平均使用1-2字节
B 4字节用于标识这个分片属于哪一个数据包
C 4字节用于分片内数字的索引,但是经过压缩,因此平均使用1-2字节
消息是从游戏上发送的数据。所有在RakNet之间发送的消息组成了一个数据报。因此如果你发送仅仅发送一条消息,那么消耗就是1数据报加一条消息。如果发送5条消息,那么就是1个数据报加上5条消息。如果你发送一条消息,但是是MTU的十倍大,那么需要发送10个数据报,每一个包涵一条消息(消息被分片)。
5、功能(函数)详解
1、Startup 函数
在调用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 参数
RakNet预先分配了用于连接其他系统的内存。指定maxConnections作为RakPeerInterface实例和其他实例之间的支持的最大连接数(进来和出去的连接)。注意如果你想要其他的系统连接到你,你需要使用一个等于或小于maxConnections的参数调用SetMaximumIncomingConnections()设置最大的进入连接数。
socketDescriptors 参数
在95%情况下,可以如下一样传递参数:SocketDescriptor(MY_LOCAL_PORT, 0);对于MY_LOCAL_PORT这个参数,如果运行的是服务器或对等端,你必须设置为想要服务器或对等端运行的端口。这个是将要传递给Connect()的remotePort参数。如果运行的是客户端,你愿意可以设置一个端口,或者设置为0让系统自动选择一个端口。注意在Linux上,想要使用1000以下的值,需要使用管理员的权限。一些端口是保留端口,尽管无法阻止你使用,但是最好不要使用。
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接口的函数。
IPv6是新的因特网协议。代替了传统ip地址例如94.198.81.195,可能使用这样一个IP地址,fe80::7C:31f7:fec4:27de:14。编码使用16字节,而不是4字节,那么IPv6用于游戏效率欠佳。也有积极的一方面,NAT穿透就不再需要了,因为IPv6有足够多的IP地址,不需要创建地址映射,也就不再需要NAT穿透了。
IPv6默认是不可用的。为了支持IPv6,将socket设置为AF_INET6。例如socketDescriptor.socketFamily=AF_INET6。
IPv6的sockets仅仅能够连接到其他的IPv6的sockets。相似地,IPv4(默认)仅能够连接到其他的IPv4的sockets。
2、Connecting 连接过程(五种连接方式、连接异常处理)
连接到其他的系统的方法,其实有五种方式来发现要连接到的系统,如下:
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。
注意:在RakPeer.cpp中有一个硬编码的MAX_OFFLINE_DATA_LENGTH限制了用户的数据长度。如果数据比这个值大,修改这个值,重新进行编译。
优点:
1. 在系统启动后,可以自动加入游戏,不需要GUI或用户交互。
2. 在LAN上最好的寻找游戏的方法。
不足:
1. 在一般的因特网上不可用
2. 不如lightweight database 插件灵活
方式三:使用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是一条概述性消息,意味着与远端系统没有建立连接。可能的原因包括如下几方面:
1. IP地址错误
2. 远端系统没有运行RAkNet,或RakPeerInterface::Startup()在这个系统上没有调用。
3. 远端系统启动了RakNet,但是RakPeerInterface::SetMaximumIncomingConnection没有调用。
4. 防火墙阻止了你选择的端口上的UDP数据包。
5. 远端系统的一个路由器阻塞了进入你选择的端口上的UDP数据包。参考NAT Punchthrough插件解决这个问题。
6. 在Windows Vista上,网络驱动安全服务器包有事破坏了UDP,不仅仅是RakNet,甚至是DirectPlay。这个服务包应该关闭或者不要安装。
7. Secure Connections启用了,但是安全检查不正确。
8. 你的iP地址被RakPeerInterface::AddToBanList()函数禁止了。注意有些插件例如Connection filter,有可选的自动禁止IP地址的功能。
3、Creating Packets 创建数据包
如何将游戏数据编码到数据包中?
运行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这一步就不需要了。
使用BitsStreams创建数据包
使用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。
4、 如果使用string非常频繁,可以使用StringTable类来代替,它和StringCompressor类类似,但是可以使用两个字节来代替一个一直字符串。
4、Send Packets 发送包、有序流
第一步:确定数据
正如在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,不会有序列。这些类型的数据包会忽略这个参数。
5、Recieving Packets 接受包
当一个数据包到来时,例如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 }使用注释:
1. 将数据包的数据转换为适合类型结构体的指针,这样可以避免复制数据造成的开销。然后在这种情况下,如果你修改了结构体中的任何数据,数据包中的数据也会被修改掉。当然了,这种情况不是我们想要看到的。作为一个服务器,在中继数据的时候要多加注意,因为中继数据会引起未知的Bugs。
2. 尽管assert不是特别必要,但是如果我们对标识符赋值错误了,assert对于发现bug非常有用。
3.在有人要发送一个大小或类型无效的数据包,使得服务器或客户端崩溃情况时,if语句就显得非常有用。在实践中,没有发生过这样的事情,虽然没有出现过也不能说明是安全的。
接收一个位流数据(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)释放数据包。
6、SystemAddress 系统地址
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很有必要。
7、BitStream 比特流
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对象后在动态重新分配内存。读取数据
读取数据也是一样的简单。创建一个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);序列化数据
需要同时使用相同的函数Read和Write,可以使用BitStream::Serialize()代替Read()和Write()。
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<char>(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的数据时使用。
8、Reliability Types 可靠性类型
控制何时如何使用数据包优先级和可靠性类型// 发送数据的时候,使用这些枚举类型设置数据类型 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)
不可靠得数据包直接使用UDP发送。他们到达目的地会出现乱序,或更不到达不了。这中方式对不太重要的数据包比较好,或者是那些发送频率非常高的数据,即使一些数据丢失了,更新到的数据包会弥补丢失的数据包。
优点:这些数据包不需要进行确认,在确认数据包中节省UDP数据包头大小的数据(大约50字节)。这些数据加起来也可以节省很大的带宽。
缺点:数据包不是按序到达,或者数据包根本到达不了,如果发送缓存满了,这些数据包是最先丢弃的那些数据包。
不可靠序列化(UNRELIABLE_SEQUENCED)
不可靠序列化数据包与不可靠数据包相同,但是这个条件下仅仅接收最新的数据包。更早的数据包会被忽略。
优点:与不可靠类型一样,开销比较低。不用担心较早的数据包会将游戏数据修改为老数据。
缺点:由于使用UDP进行发送,许多数据包可能会被丢弃,即使他们达到了接收端可能被丢掉。如果发送缓存满,这些数据是首先丢弃的对象。最后的发送的数据包可能不会到达,如果你在一些特定点停止了数据发送,这种情况下本类型数据有可能存在问题。
注意:三个可靠数据包类型中的一个的传输需要进行连接丢失检测。如果你不需要发送可靠数据包,那么需要实现手工的连接丢失检测。
可靠类型(RELIABLE)
可靠类型数据包由可靠层监督的UDP数据包,保证这些数据包可以到达目的地。
优点:可以知道数据最终到达了接收端。最终……
缺点:重传和确认会增加额外的带宽消耗。如果网络非常忙,数据包可能很晚到达。并且没有数据包排序。
可靠有序类型(RELIABLE_ORDERED)
可靠有序类型是由可靠层监督的UDP数据包,确保了按序到达目的端。
优点:数据包会按照发送的顺序到达接收端。这种方式是最简单的编程方式,编程人员不需要担心由于失序或丢包而出现的一些奇怪的行为。
缺点:重传和确认会明显增加带宽需求。如果网络过于忙碌,数据包可能到达非常晚。一个迟到的数据包可能会延迟其他更早到达的数据包,造成显著的延迟。然而,这些缺点完全可以通过有序流的合理利用来缓解。
可靠序列化类型(RELIABLE_SEQUENCED)
可靠序列化数据包是由可靠性层监控的UDP数据包,确保按照序列化的方式到达目的地。
优点:实现可靠的UDP数据包,并且数据包是按照序列化进行排序的,不需要等待较老的数据包。此外这种方式下,数据包到达的数量明显多于不可靠序列化类型方法。他们甚至更加分散。最重要的优势在于无论如何最后发送的数据包会到达,但是在不可靠序列化类型下,最后的数据包有可能丢失。(注:此处为何不可靠序列化类型的最后的数据包会丢失,不太明白。)
确认(ACK)
*_WITH_ACK_RECEIPT
通过制定包含了_WITH_ACK_RECEIPT的数据包可靠性类型,可以要求RakPeerInterface通知你当一个消息已经被远端系统确认了,或者传送超时了。
调用RakPeerInterface::Send() 或者RakPeerInterface::SendLists()返回一个四字节的无符号整数代表了一个发送的消息的ID。当使用_WITH_ACK_RECEIPT时,调用
RakPeerInterface::Receive()函数也会返回这个相同的ID。在数据包中字节0会是ID_SND_RECEIPT_ACKED 或 ID_SND_RECEIPT_LOSS。字节1-4会包含Send()或SendLists()返回的相同数字。
ID_SND_RECEIPT_ACKED 意味着消息到达接受方。对于可靠发送,你就会得到这个值。
对于UNRELIABLE_WITH_ACK_RECEIPT和 UNRELIABLE_SEQUENCED_ WITH_ACK_RECEIPT两个类型的数据,如果发送失败,会返回ID_SND_RECEIPT_LOSS值。返回这个值意味着对消息的确认没有在消息重发指定的时间门限内到达(大约为ping的几倍)。它所暗含的情况包括如下几种:
1、在传输中消息丢失
2、消息达到接受方,但是确认在传输中丢失。
3、消息到达,确认也到达,但是是在重发时间门限值之后到来。
情况最多是前两种。
一个读取返回值得例子:
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,那么就需要重新发送 本条返回消息值所对应的数据。高级发送类型
在Resends上发送最新的值。
当RakNet重新发送消息时,它仅仅能够发送你最初给它传递的值。对于不断变化的数据(例如位置),你可能就想发送最新的一些值。这样的话,使用
UNRELIABLE_WITH_ACK_RECEIPT类型发送数据。调用RakPeer::GetNextSendReceipt(),将值传递到RakPeer::Send()。在内存中存储一个消息类型和发送回复之间的一个关联。如果接收到的值是ID_SND_RECEIPT_ACKED,则将这个关联删除掉(也就是将相应的消息删除掉)。如果获取的返回值是ID_SND_RECEIPT_LOSS值,使用最新的值重新发送这条消息。
如果想让数据序列化,那么将自己的序列值与消息写到一起。远端应该存储接收到的最大的序列值。如果到达的消息的序列号比最高的序列值更低,那么这条消息都是更早的消息,可以忽略。
下面是一个使用unsigned char序列号的一个代码例子。只要你发送的数据不多于127,那么就不会出现失序:
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; }序列化的数据,而不是使用序列化的消息
RakNet的序列化只对整个消息的序列化有用。然而,有时想要在更高一层的粒度实现序列化。例如,需要位置和生命值的序列化。
消息A包含生命值
消息B包含生命值和位置信息
消息C包含位置值。
按照正常的序列化规则,如果消息按照A,C,B到达,消息B会被丢弃。然而,这样的话你会丢失掉很有用的信息,因为消息B包含了最新的生命值,且这个值可以使用。
可以通过写入自己的序列化编号给你要序列化的变量来实现子序列化。(像上面描述的一样)。然后根据自己的需要使用非序列化的发送的类型发送数据,UNRELIABLE,
UNRELIABLE_WITH_ACK_RECEIPT,RELIABLE,等等。尽管这样需要更多带宽和处理开销,它具有的优势是每一次更新可以尽快处理掉。
9、Network Messages 网络消息
从网络引擎发来的消息你接收到的一些数据包并不是使用你定义的类型,从你的代码中发送过来,而是从网络引擎中发来的消息。然而,你需要知道他们代表了什么含义,如何处理。每一个数据包的第一个字节,来自于API,会映射到如下列举的一些枚举类型。可能的接受方列举在了括号中,使用PakcetLogger::BaseIDTOString()将这些枚举类型转换为字符串。
// 保留类型—不要修改这些类型定义 // 所有的类型来自于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,
10、Timestamping your packets 时间戳
如何在不同的计算机上相同的时间帧内相应同一个事件?时间戳与本地系统时间并无关系。很不幸,每个系统都有不同的本地系统时间。如果仅仅通过网络发送获得的本地系统时间,你得到的时间是其他机器上得时间,这条消息除了告诉你发生了什么之外,没有其他有价值的信息了,因为你仅仅知道你自己的系统时间,其他人的系统时间都是不知道的,因此你不知道这个事件在你本机要什么时候触发。RakNet的时间戳功能可以让你读取其他人系统相对于你系统的时间值,使得你将精力投入到游戏设计中,而不是考虑其他系统的系统时间。插件自动透明实现了这个功能,即使是存在ping波动,也可以获得十分精确的准确度。
假设在客户端即将发生一个事件,它的本地系统时间是2000,服务器上的系统时间为12000,另外一个远端系统的时间是8000.如果数据包没有打时间戳调整时间,服务器会获得时间2000,或者说是10000ms以前的事件。同样,另外的客户端会得到2000,这样比他自己的本地时间要提前6000ms。
幸运得是,RakNet为你处理了这种情况,补偿了系统时间和ping。使用相对时间,服务器会看到这个事件是大概在ping/2ms前发生(相对于每一个客户端)。简言之,你仅仅需要使用timestamps,你要做的就是正确地编码数据包,不需要额外考虑其他的任何事情。
参见Creating Packet中的例子,学习如何在你的数据包中打入时间戳。
注意:推荐使用GetTime.h中的时间函数来获取系统的时间。这是一个高精度的定时器。你可以使用Windows的函数timeGetTime(),但是这个函数的时间值不准确。时间戳也依赖于自动的pinging,因此你需要调用SetOccasionalPing()方法来保证这个时间的准确性。
11、NetworkIDObject 网络ID对象
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<Solider *>(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<Solider ->(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); }静态对象:
有时候对象并不是动态创建的,而是在所有的系统上都已经存在了,所有的系统提前都知道了。例如,如果你在标记地图上有一个获取物,带有三个标记,这三个标记或许是硬编码进关卡设计中的,因此当游戏加载关卡信息的时候,这些数据就直接加载进了游戏。这些是静态对象,NetworkIDManager也是可以访问的。使得这些对象从NetworkIDObject对象派生出来,如同创建动态对象一样,调用SetNetworkIDManager。然后只需要给你的对象赋值一个唯一的ID即可。ID是什么并不重要,只要它是唯一的就好,那么你可以给flag1赋值ID 0,flag2 赋值 ID 1 以及给flag 3 赋值ID 2。这都没有问题。
// 所有的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); };
12、Statistics 统计
如何读取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; };
13、Secure Connections 安全连接
确保网络传输安全一旦在线游戏达到了一定的流行程度,人们就开始尝试作弊。那么你就需要在游戏层和网络层考虑这个这个作弊或欺骗问题。RakNet通过提供安全连接来处理这些问题,当然你也可以不使用这个机制。
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: 客户端的公有密钥与服务器上的公有密钥不匹配。公有密钥传输:
在试着连接到远端系统时,提前生成一个公有密钥。原因是公有密钥用于表示远端系统,这样你知道回复你的系统是你真正想要连接的系统。攻击者或许知道公有密钥,但是他们不知道私有密钥(这个你是要保密的),这样他们就不能与你建立连接。
对于端到端系统,你应该运行一个专用的服务器,这个服务器必须安全。仅仅给那些服务器预先知道的客户端分发公有密钥。连接到服务器,上传对等段的公有密钥到数据库中。然后等待到来的连接。相应地,当对等端想要连接时,连接到服务器,下载这个对等端的公有密钥,使用这个公有密钥进行连接。
你可以从dx.net(http://www.dx.net/raknet_dx.php)上租用一个打折的专用服务器。
可选的解决方案:
1、你可以使用PHPDirectoryServer来传递公有密钥。尽管不太安全,但是它仅仅需要一个PHP的webhost(典型的站点一个月仅需要$5 -$10),并且它相对来说比选择2更加安全。
2、在RakPeer::Connect中,给acceptAnyPublicKey传递TRUE值以使用publicKey参数。这个与RakNet 第三版类似,在第三版中你仅仅需要给每一个InitialzeSecurity参数传递0。
14、Cloud hosting 云端主机服务
通过Rackspace实现云端主机服务一些服务,例如Autopacher,Cloud Server,以及NAT穿透服务器,要求一个运行了RakNet的服务器。当可以使用额外的主机例如Hypernia或dx.net运行这些服务时,这些服务器一个月大概要$150。扩展服务也要求浪费时间安装代码库,不可能使用编程地实现。
RakNet使用两个云主机服务提供商进行了测试:Amanzon AWS和Rackspace Cloud。基于测试,Amazon AWS不支持进入服务的UDP连接。由于UDP连接在NAT穿透服务器中要使用,我并不推荐使用Amazon 的服务器。然而Raksapce Cloud可以支持UPD连接,并且他们最低配的Linux的花费很低。我同时也提供了一个到Rakspace的C++ 接口,这使得你通过编程可以控制你的服务器。
注册:
创建服务器
第一步要创建一个服务器,使用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。
15、Rackspace Interface 云服务器API的C++接口
用编程的方式管理应用服务器
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 <?xml version="1.0" encoding="UTF-8" standalone="yes"?><images xmlns="http://doc s.rackspacecloud.com/servers/api/v1.0"><image name="Windows Server 2008 R2 x64 - MSSQL2K8R2" id="58"/><image name="Fedora 14" id="71"/><image name="Red Hat Enterprise Linux 5.5" id="62"/><image name="Windows Server 2003 R2 SP2 x86" id="29"/ ><image name="Oracle EL Server Release 5 Update 4" id="40"/><image name="Windows Server 2003 R2 SP2 x64" id="23"/><image name="Gentoo 10.1" id="19"/><image name ="Windows Server 2008 SP2 x86" id="31"/><image name="Windows Server 2008 SP2 x64 - MSSQL2K8R2" id="57"/><image name="Ubuntu 9.10 (karmic)" id="14362"/><image na me="Ubuntu 10.04 LTS (lucid)" id="49"/><image name="Arch 2010.05" id="55"/><imag e name="Oracle EL JeOS Release 5 Update 3" id="41"/><image name="Ubuntu 8.04.2 L TS (hardy)" id="10"/><image name="CentOS 5.4" id="187811"/><image name="Fedora 1 3" id="53"/><image name="Windows Server 2008 SP2 x64" id="24"/><image name="Cent OS 5.5" id="51"/><image name="Ubuntu 10.10 (maverick)" id="69"/><image name="Win dows Server 2008 R2 x64" id="28"/><image name="Windows Server 2008 SP2 x86 - MSS QL2K8R2" id="56"/><image name="Red Hat Enterprise Linux 5.4" id="14"/><image nam e="Debian 5.0 (lenny)" id="4"/><image name="Ubuntu256MBBigPacketTest" id="943818 4"/><image name="RakNet-setup" id="9019637"/></images> RakNet不能帮你解析XML。如果你自己没有XML解析器,你可以在DependentExtenstions\XML下找到一个XML解析器。执行用户命令:
Rackspace提供了一个XML模式,这个模式带有API的操作和参数的列表。有一些命令比Rackspace类提供的命令要复杂。对于这些命令,可以直接调用Rackspace::AddOperaton()。
例如,如下是一个创建服务器的命令:
RakNet::RakString xml("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" "<server xmlns=\"http://docs.rackspacecloud.com/servers/api/v1.0\" name=\"%s\" imageId=\"%s\" flavorId=\"%s\">" "</server>" , name.C_String(), imageId.C_String(), flavorId.C_String()); AddOperation(RO_CREATE_SERVER, "POST", "servers", xml);使用RakNet进行服务器管理的例子:
重新启动崩溃的服务器 — 运行一个主服务器来监控其他的服务器。主服务器可以使用一个固定IP地址,或者可以使用DynDNS(这个也是一个C++ DynDNS类,位于/Source目录下)。当你启动一个Rackspace服务器时,让Rackspace服务器连接到主服务器。如果主服务器在某一点得到了ID_CONNECTON_LOST,让主服务器调用Rackspace::RebootServer,重新启动这个崩溃的服务器。
匹配能力 – 再次运行一个主服务器,使用CloudServer和CloudClient插件,通知主服务器每一个Rackspace服务器当前连接的负载。如果所有的服务器都满了或者接近要满了。使用Rackspace::CreateServer基于你应用程序的系统镜像创建新的服务器实例。相似地,如果服务器完全是空的,没有玩家在该服务器上登录,可以使用Rackspace::DeleteServer()方法删除空服务器,以节省服务器花费开销。
16、NAT traversal architecture NAT穿透的结构
结合如何使用UPNP,NAT类型检测,NAT穿透,以及Router2,使得P2P连接迅速而有效完成。RakNet 使用了4个独立的系统,每一个系统都解决了无法连接到其他系统问题的一部分问题。这些系统是:
1、NAT类型检测 – 发现是否我们有路由器,以及路由限制类型是怎样。
2、UPNP – 告诉路由打开指定的端口号
3、NAT穿透 – 通过在两个系统之间同时进行连接发送使得连接穿过路由。
4、Router(可选) – 使用其他玩家的带宽由于不能连接的路由。
5、UDPProxyClient(可选) – 路由无法连接到我们的服务器。
如下列举了一些结合这些系统快速实现P2P网络中连接玩家的最好方法。
要求:
1、有一个运行了NATCompleteServer组件 的远端服务器(或者使用了\Samples\NATCompletePeer中的例子的默认发现)
2、在游戏客户端,要加入NatTypeDetectionClient插件,NATPunchthroughClient插件,以及可选的Router2或UDPProxyClient插件。
3、在游戏客户端,已经连接,并建立了miniupnp,它位于DependentExtensions\miniupnpc-1.5下。
建立MiniUPNP
1、在包含路径中包含DependentExtensions\miniupnpc-1.5的路径
2、如果必要的话,要在preprocessor参数列表中定义STATICLIB参数(参考DependentExtensions\miniupnpc-1.5\declspec.h注释)
3、连接ws2_32.lib和IPHlpApi.lib。
步骤1:连接到服务器
使用普通的连接方法:RakPeerInterface::Connect(),连接到运行了NATCompleteServer的服务器。
步骤2:检测路由类型
调用NatTypeDetectionClient::DetectNATType()。你可以得到一个数据包ID_NAT_TYPE_DETECTION_RESULT指定NAT类型。例如:
if (packet->data[0]==ID_NAT_TYPE_DETECTION_RESULT) { RakNet::NATTypeDetectionResult detectionResult = (RakNet::NATTypeDetectionResult) packet->data[1]; }如果detectionResult的值是NATTypeDetectionResult::NAT_TYPE_NONE,那么这个系统没有路由。你可以连接到任何系统,并且每一个系统也可以连接到你。
你应该告诉服务器这个系统可以直接连接,这样进入系统不需要花费时间进行NAT穿透了。参考附录A,传递NAT_TYPE_NONE值。连接到每一个在游戏会话中已存的用户。
步骤3:使用UPNP打开路由
假设在第二步的路由不是NATTypeDetectionResult::NAT_TYPE_NONE,如下代码时使用UPNP打开路由器。
#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
1、从服务器的游戏会话中下载远端玩家的列表,包括他们的连接状态。
2、如果远端玩家的连接状态是NAT_TYPE_SUPPORTS_UPNP或者NAT_TYPE_NONE,那么你可以直接连接到这些玩家。将这个玩家作为穿透成功存储在内存中,这里会在第六步处理这个玩家。
3、如果远端玩家的连接状态是NAT_TYPE_SYMMETRIC,你自己在第二步获取的自己的NAT类型也是NAT_TYPE_SYMMETRIC,NATPunchthroughClient对这个玩家将失效,无法连接到。在内存中以穿透失败的标记存储这个玩家,我们会在第五步处理这些玩家。
4、否则,对这个远端的玩家调用NatPunchthroughClient::OpenNAT(),将这个玩家标记为正处理。
对于每一个我们调用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 – 将这个玩家存储到内存,标记为穿透成功,我们在第六步中处理这类玩家。
第五步:使用Router2或UDPProxyClient(可选)
对于NAT穿透失败的玩家,你可以将他们的连接通过连接成功的玩家进行路由,要实现路由使用Router2插件。如果你运行了UDPProxyServer,也可以使用UDPProxyClient通过服务器来转发这些连接。
如果转发无法实现,Router2 会返回ID_ROUTER_2_FORWARDING_NO_PAHT,如果转发成功,返回ID_ROUTER_2_FORWARDING_ESTABLISHED。
UDPProxyClient会返回ID_UDP_PROXY_GENERAL。字节1表示返回值。成功则返回ID_UDP_PROXY_FORWARDING_SUCCEEDED,远端系统会得到ID_UDP_PROXY_FORWARDING_NOTIFICATION消息。其他的消息都说明出现错误。
如果这些解决方案失败,或你没有使用他们,那么就不可能完成端到端游戏回话。将游戏会话留在服务器,你应该给用户提示,在他们开始游戏之前需要自己手动打开路由上的端口。你仅能够尝试一种不同的会话。
第六步:连接到所有我们没有连接到的玩家
第六步假设所有连接失败的用户已经在第五步成功连接。如果没有,就要离开服务器上的游戏会话。游戏应该给用户一个提示让他们手动打开路由器上的端口。
对于先前标识了NAT_TYPE_NONE,NAT_TYPE_SUPPORTS_UPNP,或者通过NAT穿透的用户,现在应该让这些用户连接。你可以假设连接完成了。
附录A: 通知服务器对等端连接状态
服务器应该追踪那些对等端是直接可连接的,如果不能够直接连接,他们路由的类型是什么。有了这些信息,进入的对等端就不需要花费时间执行NAT穿透了。可以手动编程实现这些内容,然而CloudServer插件也可以处理这些。如下是一个如何将我们连接状态上传到服务器的例子:
void PostConnectivityState(RakNet::NATTypeDetectionResult result, RakNet::CloudClient *cloudClient, RakNet::RakNetGUID serverGuid) { RakNet::CloudKey cloudKey("NATConnectivityState",0); RakNet::BitStream bs; bs.WriteCasted<unsigned char>(result); // 这里可以是任何东西,例如玩家列表,游戏名字 cloudClient->Post(&cloudKey, bs.GetData(), bs.GetNumberOfBytesUsed(), serverGuid); }更加简单的解决方法
仅仅需要UPNP和NATPunchthroughClient
这个简单的解决方案几乎在任何情况下都可以使用,并且很容易编码。缺点是连接到游戏会话花费的时间比较长,如果玩家连接失败,没有反馈信息。
1、按照上面第一步运行
2、调用第三步中的OpenUPNP()函数。不需要向服务器上传任何的状态。如果函数调用失败,忽略就可以了。
3、在会话/房间主机调用NatPunchthroughClient::OpenNATGroup()。如果成功就会返回ID_NAT_GROUP_PUNCH_SUCCEEDE或者一个失败码。如果获得的是失败码,那么你不能连接到房间,需要提醒用户打开他们路由上游戏使用的端口。
参考\Samples\NATSimplePeer例子。
17、Preprocessor Directives 预处理指令
在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
18、Custom Memory Management 内存管理
覆盖new,delete,malloc,free和realloc函数用户系统提供定制的内存管理函数,在RakMemoryOverride.cpp中的函数可以实现这些功能。
在这个文件中定义了三个全局指针,预定义默认如下:
void* (*rakMalloc) (size_t size) = RakMalloc; void* (*rakRealloc) (void *p, size_t size) = RakRealloc; void (*rakFree) (void *p) = RakFree;
进行覆盖,仅仅将这些变量值设置为其他的一些变量即可。
例如,覆盖malloc,可以按照如下形式写:
#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
19、IPV6 support IPv6支持以及与IPv4的不同
IPv6是下一代使用的IP地址。由于可用的IPv4地址已经用尽,需要开发具有更大地址空间的IPv6。整个网络工业会逐渐转向使用IPv6,因此有必要使RakNet支持IPv6以满足将来的应用。IPv4使用4字节代表IP地址,使用符号表示就是127.0.0.1。IPv6使用16字节,使用如下的标记形式:fe80::7c:31f7:fec4:27de。将RakNetDefines.h中的RAKNET_SUPPORT_IPV6设置为1,这个设置是默认的,对IPv6的支持将编译进RakNet。
IPv4的环回地址是127.0.0.1。IPv6的环回地址是::1。IPv4的广播地址是255.255.255.255。IPv6不支持广播。如果将路由设置为不支持多播,那么IPv6也不支持多播。IPv6中没有默认的多播地址,如果终端用户不进行手动设置,LANServerDiscovery插件将不能使用。
使用了IPv6,NAT穿透也没有必要使用了。假设你知道系统的IP地址,由于不存在NAT,每一个主机都有自己唯一的IP地址,即使这个系统处于路由之后,你也可以直接连接到该系统上。
IPv4 sockets/ IP 地址不能连接到IPv6 sockets/IP地址,反过来也一样,IPv6也无法连接IPv4。
如果一个socket使用IPv4,一个socket使用IPv6,那么在同一个端口上启动两个socket也是可以的。下面代码片段说明如何实现。
支持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。
20、Marmalade integration Marmalade支持
如何集成MarmaladeSDKMarmalade是一个SDK,它使得你可以使用Native C++为IOS和Android写游戏。它并不是一个游戏引擎,尽管它包含了图形库和其他的工具。因为Marmalade可以编译Native C++,它就能编译RakNet。因此你可以以一种一致的方式在这些平台上使用RakNet。
步骤1 – 下载Marmalade:
下载,并安装MarmaladeBeta 5.1或老一点的版本。获得Marmalade要求注册一下,以及一些其他必要的步骤。
步骤2 – 创建RakNet解决方案:
假设你已经下载了RakNet,进入DependentExtension\Marmalade目录,双击RakNet.mkb。如果正确安装了Marmalade,运行RakNet.mkb就会创建一个目录build_raknet_vc9或其他类似的目录。如果有必要加入或远程使用RakNet源文件,在文本编辑器中编辑.mkb和.mkf文件,里面列举了RakNet的源文件,再次双击.mkb。
步骤3 – 构建RakNet库:
在步骤2创建的目录中查找.sln解决方案文件。打开解决方案文件,为你所需要的平台编译库文件。Build/batch build/select all/build也可以进行编译。假设你完成了这一步,那你现在会得到目标文件,这些文件会在例如DependentExtensions\Marmalade\build_raknet_vc9\Debug_RakNet_vc9_x86。
步骤4 – 将RakNet连接到应用程序中:
将如下的两行加入到你的应用程序的.mkb文件。
optionmodule_path="../../DependentExtensions/Marmalade"
subprojectRakNet
在option module_path下的路径应该修改为指向你安装RakNet的地方。在Samples\Marmalade下有一个例子。设置完毕后,需要双击.mkb文件重新生成你的工程解决方案。
在编译RakNet时,如果你碰到了编译错误"unresolvedexternal symbol _strtoull ...",然后你需要更新Marmalade为更新的版本,或者将文件RakNetTypes.h中的RakNetGUID::FromString最后的strtoull注释掉,然后再进行第三步的编译。
21、Interface 2插件
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);
22、Autopatcher 自动补丁系统
Autopatcher是一个用于管理两个或多个系统之间丢失的或者改变的文件,在他们之间对不同的文件进行复制。这个插件可以处理传输文件,可以压缩传输的文件,文件安全和文件操作。但是它不处理最基本的连接,或提供用户连接接口。对于基础连接,则使用RakPeerInterface或者是PacketizedTCP进行系统之间的连接。至于UI的形式,则完全依赖于你自己的要求设计。Autopatcher用在所有的在线游戏中,大多数的AAA商业游戏中。在客户端,需要定义一个AutopatcherClient的实例。服务器需要定义一个AutopacherServer的实例。这些类的源码不是RakNet核心的一部分,他们位于 .\DependentExtensions\目录下,要使用Autopatcher插件需要将这些代码加入到工程中。
客户端: 在客户端一边,大多数的工作只需要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数据库中的应用程序的所有文件。
目录结构体
可以使用目录结构体。例如,假设你的应用程序有如下的目录结构体:
Readme.txt
Music/Song1.wav
Music/Song2.wav
Autopathcer会保持目录结构完整。因此如果发送系统有了这个目录结构体,下载系统会做一个这个目录结构体的镜像。
服务器请求文件(使用PostgreSQL):
1、在DependentExtensions\bzip2-1.0.3中的所有的源文件
2、DependentExtensions\CreatePatch.h and .cpp
3、DependentExtensions\MemoryCompressor.h and .cpp
4、DependentExtensions\AutopatcherServer.h 和 .cpp
5、在DependentExtensions\AutopatcherPostgreRepository中的所有源文件
6、在DependentExtensions\PostgreSQLInterface中的所有的源文件
7、所有的源文件在\Samples\AutopatcherServer,应该想要使用默认的控制台应用程序运行在服务器。
服务器依赖项(使用PostgreSQL):
PostgreSQL 8.2 或更新版本,安装在C:\Program Files\PostgreSQL\8.2。安装目录不同,需要修改工程属性路径。不要忘记检查在PostgreSQL安装包的开发工具,否则头文件和lib不会安装。
客户端需要的文件
1、在DependentExtensions\bzip2-1.0.3中的所有源文件
2、DependentExtensions\MemoryCompressor.h and .cpp
3、DependentExtensions\ApplyPatch.h and .cpp
4、DependentExtensions\AutopatcherClient.h 和 .cpp
5、在Samples\AutopatcherClient中的所有源文件,你应该会想将默认控制台应用程序当做是设计模板。
6、在Samples\AutopatcherClientRestarter中的所有源文件,你会想要使用默认控制台程序在补丁安装完成后,重新启动应用程序。
使用TCP代替UDP(推荐)
RakPeerInterfaces使用的是UDP,如果RakNet的协议修改了,这里将出现问题 – 自动升级系统不能够连接到新的协议。推荐你使用TCP来代替UDP。在例子
AutopatcherClientTest.cpp 和AutopatcherServerText_MySQL.cpp或者是AutopatcherServerTest.cpp中,要使用TCP代替UDP可以在这些文件的顶部定义USE_TCP。
变化:
1、使用PacketizedTCP类代替RakPeerInterface
2、PacketizedTCP会使用HasCompletedConnectionAttempt(),HasNewIncomingConnection( )和HasLostConnection()方法返回连接状态变化,而不仅仅是字节标识符。
3、将插件附加到PacketizedTCP实例,而不是RakPeerInterface实例。
对于大型游戏的优化:
AutopatcherClient::PatchApplication有一个参数lastUpdateDate。如果给这个参数传递了0,服务器不知道客户端使用的版本。那么会进行全面的扫描,服务器需要访问数据库来获取应用程序的每一个文件的哈希值,这样非常慢,如果可能的话,要尽量避免这种情况。
发布应用程序时,在补丁服务器应该使用time()函数获得时间戳,将这个时间与发布文件一起发布。然后可以将这个值传递给AutopatcherClient::PatchApplication。只要这个值大于或等于AutopatcherRepositoryInterface::UpdateApplicationFiles调用时的时间值,服务器可以使用它对从客户端使用的版本以后所做修改的文件来做优化查询。
AutopatcherServer通过CacheMostRecentPatch()函数来做更多的优化。如果使用了AutopatcherPostgreRepository插件,该方法会将最近的补丁存储到内存中。如果传递给PatchApplication()的时间比最近的补丁的时间还早,补丁就会完全跳过。如果这个值比最近的补丁的时间值小,但是比它前面的补丁的时间值大,补丁就直接从内存中获得,而不是访问硬盘来获取。要使得这个功能可用,每一次收到了ID_AUTOPATCHER_FINISHED则调用AutopatcherClient::GetServerDate()方法,保存到磁盘,将这个日期用于下一个PatchApplication()方法的调用。
性能tips总结:
1、自动升级系统并不是用于服务整个应用程序,仅仅用于补丁。在打补丁之前,用户应该从FTP或WebServer获得了大部分的游戏文件。
2、如果使用了AutopatcherPostgreRepository,使用AutopatcherServer::CacheMostRecentPatch()。
3、即使没有CacheMostRecentPatch()方法,AutopatcherPostgreRepository也比AutopathcerMySQL快大约十倍。
4、如果给AutopacherClient::PatchApplication()方法的lastUpdateDate参数传递了0,这会造成对文件的完整扫描,每一个文件都需要访问数据库。除非用户要求修复崩溃的游戏安装,否则不要这样设置,因为这样非常浪费时间。
5、分发应用程序,在调用了UpdateApplicationFiles()方法之后要在服务器调用time()。将这个值与客户端的发布程序存储一起,用于第一次调用AutopatcherClient::PatchApplication方法的lastUpdateDate的默认值。
6、一旦你收到了ID_AUTOPATHCER_FINISHED数据包,使用AutopatcherClient::GetServerDate()方法将这个返回值存储起来。用于下一次调用
AutopatcherClient::PatchApplication()方法lastUpdateDate参数的默认值。
7、使用CatheMostRecentPatch()方法,仅仅将最新的补丁放置到内存,因此不要动不动就调用UpdateApplicationFiles()方法。
内存使用上的一些注意:
补丁使用Colin Percival http://www.daemonology.net/bsdiff/上的代码来创建。补丁算法使用Larsson 和Sadakane的qsufsort,这些算法非常耗费内存,因此不推荐用于几百兆的文件。如果你使用了CacheMostRecentPath()方法,最近更新的补丁会保存在内存。此外,每一个用户的文件传输会花费8兆内存。
23、RPC4插件
在本地和远端系统调用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);
如果更广泛地使用RPC4GlobalRegistration,需要将RakNetDefines.h中的定义RPC4_GLOBAL_REGISTRATION_MAX_FUNCTIONS修改为更高的值。
调用函数:
使用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插件的演示例子。
24、Cloud Computing 云计算
通过服务器云实现客户端可访问的内存/事件
有时想要大量的没有相互连接的客户端,它们不需要相互知道对方的存在而共享内存或得到事件的通知。例如:
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 统计:
假设你想要从所有运行的服务器上追踪kills的数量(没有理解什么意思),游戏拥有百万个玩家,那么让每一个客户端单独访问一个数据库服务器根本不可行。同时呢,游戏运营商希望将数据库服务器进行隐藏。Cloud系统可以帮忙,在内存中镜像复制常用的统计信息。为了实现这个功能,每一个客户端需要将Kills值Post()到服务器。每一个在云上的服务器有一个本地的客户端用于更新数据库。本地的客户端使用Get()方法查询或签署客户端的kill上传。本地客户端也会周期写到数据库服务器(或如果想要实时统计,它可以在每一次kill消息时写到数据库)。相似地,本地的客户端会从中心数据库读取更新的kill数量,将这个数据Post()到云上。最后,游戏客户端使用带有specificSystem参数的Get()函数来获取更新的kill数。
云计算:
有时你想要通过许多客户端分隔一个问题。例如,在游戏等级中渲染灯光。云服务器可以协助充当发布事件协调者。每一个想处理这部分内容的客户端调用CloudClient::Post(),不用携带任何数据,仅仅通知准备好了执行这些处理。CloudMemory_DataKey::primaryKey或许是“LevelRender”,但是CloudMemory_DataKey::secondaryKey或许是ID_SIGNAL_READY。客户端也将调用Get()订制新的任务,例如ID_TASK_SUBSCRIPTION。要Post()结果,例如ID_TASK_COMPLETION。服务器会有一个本地客户端在127.0.0.1,包含了真正要解决的问题。这个本地的客户端会订阅ID_SIGNAL_READY。当通知到来时,问题被分开,用ID_SUBSCRIPTION调用Post()方法。数据域可以指定GUID,客户端应该处理这项任务。如果服务器上的本地客户端得到了Cloud_ClientCallback::OnDeletion调用,然后其中一个处理客户端丢失连接或崩溃了,处理的数据可以重新分配给其他的客户端。
分布式CloudServer实现:
基于迁移的带有DNS的CloudServer的分布式认证实现参考Samples/CloudServer。参考Samples/CloudServer/readme.txt,其中包含了CloudServer的实现细节,如果在多个客户端之间实现负载平衡,以及支持插件的列表。
服务器目录实现:
使用CloudServer实例作为备份的高性能服务器目录的实现参考Samples/CloudClient。分布式拓扑使得新的服务器可以在不进行重启,配置或修改代码的前提下加入到服务器群。
参考 CloudServer.h和CloudClient.h文件,查看所有的函数和参数的一个完整列表。
25、Connection Graph 插件接口实现
ConnectionGraph插件维护了整个网络的链接图,这样每一个对等端可以相互知道对方在线。连接图在新的系统连接或连接断开时进行更新。你可以有选择使用密码,用于允许确定的系统参加进地图中,保证只有知道密码的玩家才能进入地图游戏。
ConnectionGraph不能连接到其他相关的系统。它仅仅维护了整个网络的连接地图。如果你想要所有系统实现相互连接,参考Fully Connected Mesh。
如下的方法,返回网络连接图,使用邻接表存储地图。
DataStructure::WeightGraph<ConnectionGraph::SystemAddressAndGroupId,unsigned short, false> * GetGraph(void);参考Samples\ConnectionGraph中的例子。
26、Directory Delta Transfer 目录传输器
在目录之间自动发送不同文件的目录信息
如果有允许用户修改的内容(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目录下的例子。
27、FileListTransfer 文件传输
接收发送文件更加容易FileListTransfer插件用于发送可以读取进FileList类的文件列表。它与DirectoryDeltaTransfer插件类似,它并不发送预先存在的文件的目录信息,也不是完成将文件写到disk的任务。它仅仅处理要发送文件的网络传输部分。
使用:
1、服务器:使用一个允许发送文件的系统的地址作为参数,调用SetupReceive(…),当数据到达时,系统会调用FileListTransferCBInterface派生的回调处理器。
2、客户端:将要发送数据编码到FileList类。
3、客户端:使用FileList类实例调用Send(…),一组用户定义的ID用来标识这一组文件,参数传递给RakPeerInterface::Send()或TCPInterface::Send(),需要一个布尔类型的参数,它标识文件是否需要压缩。压缩速度并不快,因此除非带宽非常有限,否则保持原始参数不要修改。
FileList概览
FileList类存储了一个文件和数据列表,并且包含了一组处理硬件驱动的功能函数。它最初是为Autopatcher写的,但是也可以用于你自己专用目的。
参考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);
28、Fully Connected 2 完整的连接网络
在自动选择一个长期运行系统时,生成一个完整的连接网络在端到端的游戏中需要其中一个Peer充当主机,为系统创建唯一的游戏事件。例如,这个主机发送游戏结束通知或控制AI。FullyConnectedMesh2插件可以用于在连接之后自动决定主机。
概览:
1. 调用SetConnectOnNewRemoteConnection(false, “”);方法,手动连接到其他的系统来生成一个完全连接网络拓扑。
2. 如果每一个远端连接时,一个玩家可以被接受进入游戏,调用SetAutoparticipateConnections(true);函数,否则对每一个远端系统调用AddParticipant()。
3. 使用GetHostSystem()方法或者IsHostSystem()方法查询那个Peer端是主机
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<RakNetGUID> 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。
29、Lobby2Client PC 大厅服务器
数据库支持好友,房间,邮件,排名和更多功能大厅服务器是一个提供了PostgreSQL数据库驱动功能的插件,使用数据库可以持久化游戏数据和比赛信息(也即将游戏数据存入数据库长期存放)。大厅服务器本身不要求特别多的用户交互,涉及交互的地方的交互命令是通过LobbyClient_PC类执行。
大厅服务器分为两个类,LobbyServer本身单独提供了网络功能,调用接口就可以生成一个数据库。LobbyServerPostgreSQL,可以在文件DependentExtensions\Lobby
\LobbyServer_PostgreSQL\LobbyServer_PostgreSQL.h中找到,它是LobbyServer的一个实现,使用PostgreSQL来驱动数据库。
基本类有连接到数据库,创建和删除基本表的的函数方法。例子可以在LobbyServer工程中的Samples/LobbyServerTest/LobbyServerTest.cpp文件中找到,其中做了详细说明。
一个额外的要求是Functor类的使用。Functor是RakNet中的类,它实现了可以在FunctionThread中异步运行的特定功能单元,FunctionThread是RakNet中另外一个功能类。大部分情况下,你不需要要担心这个Functor——然而,有一个Functor是需要关注的,它就是向数据库中加入新的titles(游戏/应用程序)的Functor。
如何实现在下面的例子中做了说明: // 这个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):
目录DependentExtensions\Lobby中的所有文件,除了Client使用的文件
目录DependentExtensions\Lobby\LobbyDB_PostgreRepository中的所有文件
目录DependentExtensions\Lobby\LobbyServer_PostgreSQL中的所有文件
目录DependentExtensions\Lobby\RankingServerDB_PostgreRepository 中的所有文件
目录DependentExtensions\Lobby\TitleValidationDB_PostgreRepository中的所有文件
目录DependentExtensions\PostgreSQLInterface中的所有文件
如果想要使用控制台程序,可以参考目录Samples\LobbyServerTest中的文件。
所有依赖的文件(使用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中的所有文件。
30、Lobby2Client-Steam 使用Steamworks服务加入大厅,房间以及做NAT穿透
Lobby2Client_Steam插件提供了一个Steamworks服务的插件,这个服务可以与RakNet协作实现一些功能。这个服务不再需要提供你自己的服务器,自己提供服务器需要RakNet的NATPunchthrough和Lobby系统。依赖:
1、假设Steam SKD位于C:\Steamworks。如果SDK没有在这个目录下,按照安装目录相应地修改预编译路径和连接器路径。
2、可以从https://partner.steamgames.com/获得Steamworks API,要求注册,并且同意相应的使用协议。
3、你需要建立自己的steam_appid.txt文件。这个例子仅仅复制了steam的SDK包自带的这个文件。
包括的操作:
获得房间列表 – L2MID_Console_SearchRooms
离开房间 – L2MID_Console_LeaveRoom
创建房间 - L2MID_Console_CreateRoom
加入房间 - L2MID_Console_JoinRoom
获得指定房间的细节信息 - L2MID_Console_GetRoomDetails
向房间发送聊天消息 - L2MID_Console_SendRoomChatMessage
在房间中获得一个成员 – Lobby2Client_Steam::GetRoomMembers
获取NAT穿透成功提示 – SteamResults::Notification_Console_RoomMemberConnectivityUpdate。
参考Samples\SteamLobby例子中本系统的详细实现。
31、Lobby2Client-PS3 NP系统的接口,包括信号
PS3 NP系统提供了房间和大厅的概念。Sony也使用NAT穿透架起了自己的主机服务器。通过使用Lobby2Client_PS3插件,这些服务以RakNet接口的方式提供给用户使用。例子位于np_matching2_lobby工程下的RakNet_PS3_VS2005解决方案下。这个例子是使用了PS3 SDK的np_matching2例子,但是修改成了RakNet接口调用形式。如果你仔细看Lobby2Client_PS3.cpp和Lobby2Message_PS3.cpp的源代码,你会看到例子中使用的代码中其大部分的代码是相同的。这保证了与PS3 TCRs的一致性。
使用:
将Lobby2Client_PC3实例附加到RakPeer实例。大部分的操作是通过SendMessage()方法和SendMsgAndDealloc()方法来执行,类也包含了一些功能函数。例如,IsInRoom()返回你当前是否位于房间内。GetNumFriends()方法返回在线好友数。参考类的方法的完整列表。
可用的操作方法可以在Lobby2Message_PS3.h文件中查看。这些操作的文档是在文件中做了详细陈述。参考文件的底部的操作完整列表。
需要的文件:
DependentExtensions\Lobby2目录下的所有文件,除了Lobby2Server.h和Lobby2Server.cpp。这些是所有平台都需要的共有文件。
DependentExtension\Lobby2\ps3目录下的所有文件。这些文件是针对PS3的。Smaples\Lobby2Client_PS3\np_matching2\np_maching2_lobby\np_conf.*。
例子代码:
初始化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()方法,否则回调函数不会被调用。
32、Lobby2Client_360 带有Live的RakNet接口,包括音频聊天
XBOX没有提供房间和大厅的概念,事实上还是要求用户实现网络通讯来配置游戏比赛。RakNet使用Lobby2Client_360插件来处理这个问题。XBOX也不处理主机迁移,然而,如果如果加入了FullyConnectedMesh2插件,Lobby2Client_360插件可以自动检测主机迁移,自动执行适当的调用。参考例子代码,打开RakNet_360_VS2008.sln工程,然后运行LiveTest工程。使用:
在LiveTest.cpp中的main()方法中,实例化Lobby2Client_360类,将它附加到RakPeer实例上。如果游戏比赛是端到端形式,想要实现主机迁移,则还需要附加上FullyConnectedMesh2插件。如果想要实现语音聊天,那么需要附带上RakVoiceXBOX360插件,同时要在SocketLayer.cpp中定义要使用的处理器标记RAKNET_USE_VDP。
创建房间:
发送Console_CreateRoom_360消息来创建房间。可以同时创建多个房间。房间是与XBOX控制台相关联的-但是,当创建房间时,房间里面没有任何用户。要向房间内加入用户,需要发送Console_SignIntoRoom_360消息。用户注册并进入房间有一些限制 – 例如,如果房间使用分类会话,那么来宾无法进入房间。
重要:由于在Live API中的设计缺陷,不可能结束并且重新开始分类会话。分类会话必须被销毁,并且重新创建。然而,这样会造成所有在会话中的用户掉线。因此,没有很好的方法保证用户在会话中永远没有重大错误地存在。
RakNet通过创建两个会话来解决这个问题(即使不使用分类会话也创建两个)。第一个在XBOX中的术语中通常被称为当前会话,这种叫法仅仅用于连接。只要在房间内,当前连接就一直持续存在,并不分类或公然断开(XSESSION_CREATE_USES_ARBITRATION)。即使合适的游戏模式是分类的,当前会话也不是分类的。
当前会话可以通过搜索返回,这样你可以连接到其他的控制台程序。然而,这样做会有两个问题:
1. 你不知道对应的游戏会话是不是分类的。
2. 当创建房间时,你必须从定义STANDARD的.xlast文件中指定标记。
解决第一个问题,你需要在.xlast文件中加入一个额外的标记,通常是当前会话的搜索属性,表明游戏房间是不是分类的。
.xlast文件是预定义了你想要运行的那种搜索,游戏的模式,语言和其他一些信息的文件。PS3都是通过编程实现的。例如,生成一个新的比赛生成查询,打开.xlast文件,打开”Xbox 360 and Live authoring submission tool”,打开.xlast文件,右击,然后点击新的matchmaking查询。
加入房间
首先,使用Console_SearchRooms_360消息搜索房间。XSENSSION_INFO成员是一个包含了要加入房间的信息的结构体,在房间列表的Console_SearchRooms_360::rooms[index].sr->info中有此成员。接下来,执行Console_JoinRoom_360,复制XSESSION_INFO到结构体的roomToJoin成员。如果Console_JoinRoom成功完成,房间的一个本地复制就创建了,NAT穿透也完成了。现在就可以从会话ID中获得远端系统的IP地址,会话ID是XSESSION_INFO的XNKID成员,可以在此成员使用SystemAddress::SetFromXSessionInfo。
如下是抽取IP地址,调用Connect()的代码:
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一样简单。主机迁移:
当创建房间的时候,你可以选择当房间主人离开时销毁房间,或迁移到其他主人(主机上),就是Console_CreateRoom_360中的supportHostMIgration成员。如果选择迁移主机,则要求加入FullyConnectedMesh2插件实现自动迁移主机。当然了XBOX提供了确定新的主机会话的方法,我们建议使用FullyConnectedMesh2,因为它与Lobby2Client_360集成,并且它跨平台。
当FullyConnectedMesh2检测到连接丢失,它会返回ID_FCM2_NEW_HOSt消息。这个消息被插件读取,所有的系统使用相同的host调用XSessionMigrateHost,透明地将房间host转移到一个新的系统上。当这些发生时,可以得到Notification_Console_RoomOwnerChanged回调函数。
语音:
RakVoiceXBOX360插件与RakNet集成,支持XBOX TCRs要求的IPPROTO_VDP,这个插件位于Samples\XBOX360\RakVoiceXBOX360。根绝TCRs,最开始的两个字节必须包含游戏数据的长度,剩下的数据包含的是语音数据。RakNet的结构不支持同时发送语音和游戏数据,例如RakPeer::Send()没有选项可以包含语音数据。
为了解决这个问题,对于普通的数据传输要求定义RAKNET_USE_VDP,这会将SocketLayer.cpp包含载荷长度作为每一个数据包的最开始两个字节。每一个数据报起始的两个字节被丢弃,因为载荷总是数据报的长度。
对于语音传输,插件RakVoiceXBOX360使用未编码的基带数据,直接调用SocketLayer::SnetTo_360函数。带外消息不会被RakPeer捕获,但是按照现有状况返回。插件会检查是否拦截这个带外数据。因为数据格式是提前知道的,插件不需要显示地告知载荷或语音数据的长度。任何没有读取的数据都当做语音数据对待,然后将数据传输给IXHV2Engine::SubmitIncommingChatData。
万一不清晰,不能简单调用SocketLayer::SendTo_360来传输语音和游戏数据。RakNet会丢弃没有有效格式的游戏数据,RakNet的游戏数据编码进了ReliabliityLayer.cpp。语音数据必须发送带外数据,就像插件实现的那样。
33、Lobby2Client-Games for Windows Live 支持使用普通代码给360安排比赛
Windows Live的游戏提供了一组Windows函数,与360的大厅函数类似。事实上,按照GFWL定义修改一点,RakNet在两个平台上就可以使用相同的代码。所有的资料都类似于Xbox360lobby.html,下面是一些不同的地方:
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。
34、Message Filter 消息过滤
通过类别限制进入系统的消息对于Client/Server拓扑,你可能不想任何系统发送一些消息。例如,或许只有服务器可以发送kill消息。或者可能想要将玩家分为各个阶段,已经登录的用户,但是没有提供他们的密码,那么就不能发送游戏消息。消息过滤器的设计主要是用于自动处理这些情况。
MessageFilter插件通过“filterSet”定义了用户类别,filterSet是一个用户提供的数字标识。例如,你在系统中需要给新连接的系统提供一个过滤器集,对于已经认证的系统
则需要赋值另外一种过滤器集。对于每一个过滤器:
1、自动增加新的连接
2、允许RPC调用
3、限制什么消息,或者什么范围的消息可以接收,如果条件被违反,做出何种动作,丢弃还是缓存。
4、删除规则集。
例子: 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文件,了解所有的函数和参数的完整列表以及注释信息。
35、NAT Type detection NAT类型检测
要完成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、客户端在相同的IP地址打开两个端口。在NatTypeDetectionClient中,RakNet的socket是第一个端口,c2是第二个端口的socket,这两个socket在
NatTypeDetection::DetectNATType()中创建。
2、服务器在同一个IP地址上打开两个端口,以及在三个其他的IP地址上打开一个端口。这个在NatTypeDetectionServer::Startup()函数中完成。第一个IP地址上的第一个端口是正常的RakNet端口。第一个IP地址上的第二个端口是s1p2。其他的三个地址绑定到s2p3, s3p4和s4p5。
3、客户端连接到服务器通常是在第一个Ip地址上实现。
4、客户端请求NAT类型检测开始。
5、服务器尝试向客户端的第二个端口发送数据。这个端口是前面没有打开过的端口,因此如果接收到了,那么客户端就没有位于NAT之后。这个动作可以再
NatTypeDetectionServer::Update()方法中实现,通过STATE_TESTING_NONE_1和STATE_TESTING_NONE_2来定义。两次尝试的原因是每一次尝试出现两次。每一次尝试的时间是ping * 3 + 50毫秒。S4P5用在这里。
6、服务器从不同的IP地址向客户端的不同端口发送数据,这个端口RakNet已经连接。如果接收到了数据,那么客户端可以从已经使用的端口上接收到任何源IP地址的数据报。这个情况通常是full-cone NAT。s2p3用于这个目的。
7、在已经连接的Ip地址上从第二个端口发送数据,s102。如果接收到了数据,那么客户端的NAT类型是address-restriced cone NAT。
8、客户端向服务器的另外一个IP地址发送数据,从第一个端口(已经连接)。如果IP地址和端口是相同的,那么客户端使用external IP地址和端口来处理来自相同源地址的所有连接。这个是port-restriced NAT类型。
9、其他的都是symmetric 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);
ip2, ip3, ip4必须是没有使用的IP地址。如果你调用RakPeer::Startup()方法中将RakNet绑定到ip1,然后使用2nd到4th到ipList中。
参考\Samples\NATCompleteClient中的例子。
36、NAT punchthrough Nat穿透
什么是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组件实例。
客户端实现:
1、创建一个插件实例:UDPProxyudpProxyClient;
2、从RakNet::UDPProxyClientResultHandler类派生一个类,用于获得事件提示。
3、将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin(&udpProxyClient);
4、在第二步创建的类上调用UDPProxyClient::SetResultHandler()。
5、首先尝试NATPunchthrough。如果获得了ID_NAT_PUNCHTHROUGH_FAILED消息,转向第六步。两个系统都会返回ID_NAT_PUNCHTHROUGH_FAILED,然而,仅仅有一个系统需要启动代理系统。
6、使用协调者的地址作为参数调用UDPProxyClient::RequestForwarding,这个地址是你想要转发自的地址(UNASSIGNED_SYSTEM_ADDRESS用于你自己的),你想要转发到的地址,以
及没有数据时保持转发活动要多长时间。例如:
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);
如果可用的服务器多于一个,远端和目标中继系统都运行了RakNet,那么源端和目标端自动ping可用的服务器。服务器会尝试按照最低ping和到最大ping和来连接。这个是基于最低的ping值,那么两个系统之间有最短的路径,可以获得最小延迟。
协调者实现:
1、创建一个插件的实例:UDPProxyCoordinator udpProxyCoordinators;
2、将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin( &udpProxyCoordinator);
3、在服务器上为协调者设置密码使用udpProxyCoordinator.setRemoteLoginPassword(COORDINATOR_PASSWORD);
4、不要忘记调用RakPeerInterface::Startup()和RakPeerInterface::SetMaximumIncomingConnection(MAX_CONNECTION);
服务器实现:
1、创建一个插件实例:UDPProxyServer udpProxyServer;
2、将插件附加到RakPeerInterface实例上:rakPeer->AttachPlugin( &udpProxyCoordinator);
3、连接到协调者
4、登录到协调者。这个可以再运行时实现,那么如果你的游戏非常流行了,你可以动态添加更多转发服务器。
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
定位于世界范围内提供服务器,服务器是单个机器。
37、Packet Logger 包日志
记录进入和发出的消息,用于调试
PacketLogger是一个插件,它可以打印系统所有进入和发出的消息,以便用于调试。它在必要地方解析消息,以表示消息是RPC还是一个时间戳。它也可以将数字的MessageID转换为对应的字符串。默认输出是由逗号分割文本,也可以作为CSV文件读取,在控制台中使用printf函数打印。
要改变输出目的地,从PacketLogger派生,然后重写WriteLog()方法。
除了PacketLogger类本身以外,如下的实现也包括在内:
PacketConsoleLogger – 与ConsoleServer一起使用 PacketFIleLogger – 记录到一个文件。调用StartLog()打开文件。 ThreadsafePacketLogger – 与PacketLooger类似,但是延迟到WriteLog()函数知道出了RakNet线程之后才会记录。如果你要记录重要的日志那么可以使用这个类。
38、RakVoice 语音通话
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。
39、ReadyEvent 一个事件准备好可以作处理的信号
在端到端环境下,确定什么时候所有的Peer准备好了,可以发生一些共享的事件是非常繁琐的一件事,例如从大厅开始游戏,或者进行下一轮游戏等。最大的问题是,尽管你的系统已经准备好开始,所有的连接的系统或许在你知道时已经准备好,但是这些系统可能不知道他们相对于其他的用户是否已经准备好。这主要是由于延迟或者数据报丢失可能会破坏掉两个系统之间的连接从而导致玩家是否准备好未知,那么这两个用户担心并不是所有的用户都准备好开始游戏了。ReadyEvent插件可以自动处理这种情况。给定一个用户定义的数字事件标示符,每一个系统可以调用ReadyEvent::SetEvent(eventide, isReady);方法。当系统相对于你和其他的系统都准备好了,ReadyEvent::IsEventCompleted(eventide);方法会返回true,你可以处理这些共享事件。这个值一直存储到事件被DeleteEvent(eventID);方法删除。
常用的情况:
在端到端大厅中,这个可以用于存储用户什么时候准备好以及没有准备好的状态。当用户准备好时,游戏可以开始。
在端到端的轮转游戏(也即游戏是一局一局进行的,如斗地主)中,这个插件可以用于通知每一个用户已经准备好开始下一轮游戏。
在任何端到端游戏中,这个插件可以用于处理每一个人都等待的事件,例如所有玩家完成加载游戏level。
40、Replica Manager 3 插件接口实现(复制管理器)
任何在游戏进行期间有对象进行创建和销毁的游戏,也就是几乎所有的大型游戏,最少面临如下的三个问题:1、如何将已存的游戏对象广播给新的玩家
2、如何将新游戏对象广播给已存的玩家
3、如何将删除的游戏对象广播给已存在的玩家
根据游戏的复杂性和优化,还可能遇到如下的几个问题:
1、在玩家在游戏世界进行移动时,如何动态创建和销毁对象。
2、由于编程或图形绘制的原因(例如射击子弹),如何允许客户端在必要的时候立即在本地创建对象。
3、当游戏对象随着时间变化时,如何更新这些对象。
对于这些问题的解决方法通常很直接,即使这样仍然需要大量的编程工作和调试,每一个对象大概要二十几行的代码。
ReplicaManager3就是为了给用户提供一个一般的,可覆盖的插件,尽可能多地自动护理这些细节。ReplicaManager3自动创建和销毁对象,给新的玩家下载地图,管理玩家以及进行一些数据的自动序列化操作。这个插件同时还包含了自动中继消息的高级功能,以及当序列化的成员数据改变时,自动序列化你的对象。
操作顺序:
对象是按序远程创建的,使用ReplicaManager3::Reference()方法进行注册。在一个tick创建或销毁的对象是在同一个packet创建或销毁的,意味着对于构建或销毁对象的调用都是由相同的RakPeerInterface::Receive()调用触发。
Serialize()在构造之后发生。因此,所有对象都会被创建,会调用DeserializeConstruction()函数,这些操作是在任何Serialize()方法调用发生之前。不像构造函数,
Serialize()调用会被扩展到多个调用,一直到RakPeerInterface::Receive()调用,依赖于带宽能力。因此,对于这些在初始化的SerializeConstructioni()调用中设置的对象,确保发送了所有必需的数据,因为插件并不保证你在同一个tick上会接收到Deserialize()方法。
对象第一次被发送到远端系统时,一旦所有对象被构造且Desialized(),你会得到Connection_RM3::DeserialzeOnDownloadComplete()方法调用。
依赖解决方案:
如果一个对象参考到其他的对象(例如,一个枪要有一个指针指向他自己),那个依赖对象需要首先创建。在这个枪有他自己的拥有者的情况下,拥有者需要首先被创建。枪应该序列化拥有者的NetworkID,在对象拥有者的DeserialzedConstruction调用中可查找拥有者。这个可以通过使用RepelicaManager3::Reference()方法按序注册对象。
有时你有依赖链,这个链不能通过重新调序来解决。例如,玩家有一个物品清单,在清单中每一个项目都有一个指针指向它的拥有者。否则你或许会有一个环形链,也就是A依赖B,B依赖于C,并且C依赖于A。否则重新排序这些对象不再可行。对于这些情况,可以在Replica3::PostDeserializeConstruction()回调中分解这些依赖关系。在一个给定的更新tick中,如果一个DeseializeConstruction()方法对于所有的对象都完成了然后调用PostDeserializeConstruction()方法,因此所有将被创建的对象都将会被创建。
静态对象:
有时,在系统中有一个已存的对象,这个对象可以被所有系统感知到。例如,在等级加载中的门。在这些情况中,你不希望这些服务器传递对象创建消息,因为它会创建两次。然而你依旧想要访问和序列化这个对象。例如门打开或关闭,或者门的剩余血量。
1、从Replica3派生对象
2、对一个对象调用replicaManager3->Reference()之前,调用replica3Object->SetNetworkIDManager(replicaManager3->GetNetworkIDManager());
3、对一个对象调用replicaManager3->Reference()之前,调用replica3Object->SetNetworkID(unique64BitDLoadedWithLevel);
4、从主机/服务器的QueryConstruction()返回REM3CS_ALREADY_EXISTS_REMOTELY值,否则返回RM3CS_ALREDY_EXISTS_REMOTELY_DO_NOT_CONSTRUCT。
5、WriteAllocationID(),SerializeConstruction(), DeserializeConstruction()并不是用于静态对象,他们都是空实现。
6、如果对象不需要通过网络销毁,实现QueryActionOnPopConnection()方法,返回REM3AOPC_DO_NOTHING值。SerializeDestruction(),DeserializeDestruction(),
DeallocReplica()方法可以保持空实现。
7、时间QuerySerialization()方法,从序列化对象的系统返回RM3QSR_CALL_SERIALIZE值,典型的端到端主机或服务器。如果系统当前并不是主机,稍后可能成为主机,例如端到端的主机迁移,返回RM3QSR_DO_NOT_CALLSERIALIZE值,返回RM3QSR_DO_NOT_CALL_SERIALIZE。否则返回RM3QSR_NEVER_CALL_SERIALIZE。
8、在SerializeConsturctionExisting()方法中将你自己的初始化数据写到BitStream中,在DeseriallizeConstructuionExisting()方法中读取这个数据。当且仅当
QueryConstruction()方法返回RM3CS_ALREADY_EXISTS_REMOTELY值,然后才会调用这个函数。
9、在Serialize()方法中写per-tick的序列化数据,在Deserialize()方法中读取这些数据。
RM3CS_ALREADY_EXISTS_REMOTELY使得ReplicaManager3认为这个门已经在其他的系统上存在,那么当Serialize()函数被调用时,更新仍然会被发送到这个系统。但是调用SerializeConnection()调用,对象创建会被跳过。
与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<RakNetGUID> 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<RakNetGUID> participantList; fullyConnectedMesh2->GetParticipantList(participantList); RM3AllocConnections(participantList); } 6、void RM3AllocConnections(DataStructures::List<RakNetGUID> &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); } }与基于系统的组件集成:
基于组件的系统,我的意思是一个游戏的actor带有一系列的附加类,每一个都包含了actor自己的一个属性。例如,玩家有位置,生命值,动画,以及物理组件。
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查询。对象序列化的方法:
参考手册发送dirty标记
描述:当一个变量变化时,变量已经变化的一些标记设置依赖于你。下一个Serialize() tick,要发送所有的dirty 标记集。
优点: 快速,内存有效利用
缺点: 所有复制的变量必须通过访问者修改,那样标记集可以被设置。这样编程人员的劳动量非常大,因为需要编程人员编程设置dirty标记,在设置过程中有可能产生bug。
例如: 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); }基于对象变化的序列化:
描述:这个是ReplicaManager3所带有的功能。如果一个bitStream信道的对象的状态完全变化了,那么整个信道将被重置。
优点:方便编程人员
缺点:发送一些不必要的变量,浪费带宽。CPU和内存的使用也比较多。
例如: 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); }序列化每一个变量:
描述:正在讨论的是一个可选模块。每一个变量在内部拷贝,与最后一个状态对比。
优点:最大化带宽利用率
缺点:CPU和内存的使用非常严重
例子(也可以参考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函数中返回值达到相同的结果。
41、Router2 通过中间系统发送消息
Router2可以再没有直接相连的系统之间路由数据报,它需要使用第三个系统的带宽,要求两个系统都要与第三个系统相连。当希望使用完全连接网拓扑时,但是由于路由和/或防火墙的原因不能建立完全连接,这个插件非常有用。由于远端系统的系统地址是中间系统的地址,那么需要使用RakNetGUID对象来访问系统,包括其他的插件。使用:
1、将Router2插件附加到每一个系统上。
2、使用目的系统的RakNetGUID对象调用EstablishRouting函数。
3、消息将被广播到所有连接的RakPeer实例上。每一个运行了Router2插件的RakPeer,如果任何人连接到了目标系统,需要查询连接列表,那个用户被连接到了目标系统。
4、在查询了所有的系统后,ping值最小的系统,也即转发最少连接的系统,会启动UDPForwarder而系统,返回ID_ROUTER_2_FORWARDING_ESTABLISHED。如果没有可用的路径,返回ID_ROUTER_2_FORWARDING_NO_PATH。
一旦建立起了一个路径,应该尝试连接到目标系统,做法与普通连接方法相同。样例代码如下:
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对象来访问远端系统。
42、SQLite3LoggerPlugin 设置
RakNet的SQLLite日志系统允许任何支持TCP的系统向远端服务器发送日志。日志自动包含了源文件和行,以及日志发送的时间,发送者的IP地址。日志系统支持实时时间DXT1压缩,允许游戏会话的视频重放。记录日志并不需要SQL知识,事实上通过RakNet记录日志就和通过printf发送一样简单。在编译时间,各种类型是系统自动检测的。特别有价值的一点是它具有将多人游戏会话记录到一个单独日志文件的功能。这功能使得开发者可以通过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)
函数调用进行日志:
所有的函数调用记录日志,记录到一个单独表中,然而函数调用的参数列表记录进入另外一个数据表。表的名称在SQLiteServerLoggerPlugin.cpp的顶部进行了定义。
#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计数
多数的游戏通过离散的ticks更新仿真。在客户端的Tick是随着日志数据自动发送到服务器。通过调用SQLiteClientLoggerPlugin::IncrementAutoTickCount()更新tick计数。在用于简单步进动画Echo Chamber中支持Ticks。
二进制数据: 使用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) ));
43、SQLite3Plugin 使用SQLite通过网络存储游戏或会话数据
游戏通常需要一个服务器存储会话信息,例如所有正在运行的游戏,或者在游戏中的所有玩家,或者两者都有。这个服务器称为主服务器,由商业服务提供,通常有很高的租用费用。然而,这些服务器的核心是仅仅提供了一个类似于如下的画的一个数据库表格。
先前的几个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中本系统的实现。
44、TeamBalancer 请求和平衡团队客户端/服务器或端到端游戏
TeamBalancer插件用于在游戏会话中给每一个玩家赋予一个团队编号。玩家默认没有团队,通过调用RequestSpecificTeam()或RequestAnyTeam()方法来加入团队。
操作包括: SetTeamSizeLimits() —能够加入到一个给定团队号的玩家的最大数。 SetDefaultAssignmentAlgorithm() —定义如何自动向团队加入新的玩家—按序填充团队,或加入一个最小的团队。这个函数有RequestAnyTeam()方法触发。 SetForceEvenTeams() —使得所有团队变得更加平衡。有太多玩家的团队会随机地让一些玩家转移到玩家较少的团队中。 RequestSpecificTeam() —变为一个需请求的团队。如果这个团队满了,你的加入将被挂起,直到团队不满,或在想要加入的团队中的一个玩家想要与你交换团队为止。 CancelRequestSpecificTeam() —如果RequestSpecificTeam()还没有完成,这个函数会移除掉函数的请求。 RequestAnyTeam() —随即加入到一个团队中,基于默认的团队分配算法。 GetMyTeam() —如果已经加入到了某个团队,返回自己所在的团队。 SetAllowHostMigration() —如果是端到端,那么调用该函数时使用true参数,否则使用false参数调用该函数。 参考TeamBalancer.h头文件获得更多详细信息,以及每一个函数和参数的完整文档,以及函数返回给用户的消息。 参考例子工程Samples\TeamBalancer中本系统的实现。
45、TwoWayAuthentication 双向认证
由一对系统安全地验证已知的密码通常使用RakNet你可以使用Secure connections安全传输数据。然而,有时一对系统或许没有活动的安全连接。例如,在移动电话上,安全代码需要使用太多内存,变得很慢,或者不能编译。在这种情况下,你依然可以提前向两个系统使用密码验证一个远端系统。RakNet使用Two Way authentication实现了这个功能。并不是它自己发送密码,而是发送密码的one way hash。这个hash被验证是正确的,还是错误的,验证结果返回给用户。
使用: // 将插件附加到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头文件查看函数的完整文档和参数说明。
46、Crash Reporter 崩溃报告器
Minidumps使得崩溃报告功能容易实现CrashReporter可以在RakNet/Samples/CrashReporter中找到,仅仅可以用于Windows平台,它的作用是调试无监控的服务器或游戏客户端。当崩溃发生时,CrashReporter会捕捉异常,写一个minidump,然后将信息写到磁盘或发送email。Email的操作可以是交互式,打开用户的email客户端。或者是非交互式的,使用EmailSender类连接到mail服务器,自动发送崩溃报告。
从CrashReporter.h中复制
Minidump可以在visual studio中打开,可以查看在何处发生了崩溃,给出你本地的变化值。
如何使用minidump:
在硬件驱动器上放置minidump,双击它可以打开,使用Visual Studio打开。它会自动在引起崩溃的exe文件所在的目录寻找该exe文件。如果无法找到该exe文件,或者exe文件是不同的另外的文件,vs会在当前目录查找这个exe文件。如果仍然找不到,或者找到的文件是不同的一个文件,它会加载vs,指明vs无法找到可执行模块。这时会给出没有源码的提示。然而,在工程属性窗口中的命令行参数中指定modpath=<pathToExeDirectory>。最好的解决方法是将.dmp文件拷贝到一个包含了崩溃exe文件复件目录下。
一旦加载了,VS会寻找.pdb文件,这个文件用于查找源码文件和其他的信息。只要在你的硬盘上的源码文件与创建exe的相符就可以。如果不相符,你需要查看源代码,但是它可能是错误的源码。有三种方法处理这种情况。
第一种方法是修改指定源码位置的路径,那么就不会自动查找到错误的代码。这会导致debugger不会发现在.pdb中指向的源码。Vs会提醒你正确源码的位置。
第二种方法是在不同的路径路径构建一个exe文件。例如,当你在release 发布时使用的是c:/Working/Mygame路径,可以在c:/Version2.2/Mygame目录下重新构建exe文件。构建完成之后,保持本目录下的源码文件,exe,和pdb不变。当收到了崩溃的.dmp时,将它拷贝到exe相同的目录,也就是c:/Version2.2/Mygame/bin目录。这样.pdb文件会指向正确的崩溃源码上。
第三种方法是保存构建的标记或源码控制中的分支,在调试之前得到这个版本 (你仅仅需要源码,.exe和.pdb)。调试之后,保存你先前的工作。
使用:
#include “DbgHelp.h” 连接DbgHelp.lib和ws2_32.lib库。
47、Command Console Server 命令行控制台服务器
有时当你不在特殊计算机之前时,命令行控制台控制服务器是非常有用的。由于服务器确定时会有用,这个主机与该服务器控制地不同的情况下就会有用了。或者或许你有许多服务器需要控制,你想要通过一个脚本控制这些服务器。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()方法,当你查询你自己的特殊解析器时,它可以返回一些额外的信息。如果你的解析器由于前置条件失败,不能够运行,那比较好的做法是返回一个通知信息,以便调用者可以了解情况。未知或可变参数数
将CommandParserInterface::VARIABLE_NUMBER_OF_PARAMETERS作为第一个参数传递给RegisterCommand()方法。在这种情况下,RegisterCommand()会验证参数的有效数。是否在OnComand()方法中处理在这种情况下出现的错误,依据你的需求。
直接字符串解析
如果你不想ConsoleServer为你解析传递进来的字符串,使用参数originalString传递给OnCommand方法。例如,如果你控制脚本系统,你可能希望直接将字符串传递进去。
修改描绘器
如果你想要使用其他的分隔符分割命令而不是使用空格,或者使用引号以外的标记分割字符串,定义可以在CommandServer.cpp中找到。如下
#define COMMAND_DELINATOR ‘ ‘ #define COMMAND_DELINATOR_TOGGLE ‘"‘
48、EmailSender 邮件发送器
通过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来完成。
49、StringCompressor 字符串压缩器
安全编码和解码字符串
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。
50、TCP Interface TCP接口
连接到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);
51、PHP Directory Server 目录服务器
使用共享的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时可以执行另外一个上传过程。
52、Ogre 3D Interpolation 样例
3D Interpolation说明
Ogre 3D interpolation样例使用了图形引擎Ogre 3D来渲染爆米花爆的情景。
服务器有一个一束爆米花核心,它向外弹出爆米花,漫天乱飞。一会所有爆米花都删除了。
客户端是一个静默(dumb)客户端,因为客户端不做任何动作,也没有处理核心泼洒或弹出的细节。
Ogre的特殊点:
如何在显示和可视位置使用一个帮助类TransformationHistory插补。给定一个过去的时间,使用插补它会告诉你那时你的位置。如果你按下空格,你会看到客户端非插补地运行,这个时候画面其实是起伏不断的,因为它每秒仅仅发送4次。放开空格键,图形再次变得平滑了。
转化为RakNet的一部分:
ReplcaManager3类,可以自动处理爆米花核心的创建、删除,以序列化等。
要运行它,在同一个电脑上启动两个实例。在其中一个用作服务器的实力上输入’s’,用作客户端的实例上输入’c’。按下空格键,观察客户端没有插补地运行的效果。
如果你想要在因特网上运行。修改硬编码的SERVER_IP变量为你的服务器的地址。
这个代码可以在DependentExtensions\Ogre3DInterpDemo目录下找到。
依赖:
Ogre 3D必须安装。它保证了你有OGRE_SDK作为环境变量。如果没有,按照工程属性进行修改。
53、Irrlicht FPS 实例
以第一人射击视角说明端到端连接性
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是一个网络引擎,为网络通讯传输提供了完美的解决方案。
本文介绍了源码功能以及使用方法,参考了官方文档。
Android RakNet 系列之二 功能介绍