RakNet 致力于网络和网络相关服务的游戏引擎。不仅包含了网络通信,也包括游戏级别复制,补丁升级,NAT穿透,和语音聊天。RakNet可以用于任何的应用,且 可以与其他任何使用了RakNet的系统通信,不论它们位于同一个计算机,跨LAN,或跨Internet。
特点
高性能 (每秒传输25,000条信息)
容易使用(在线用户手册,视频教程( 在线技术支持))
跨平台
安全的传输(代码中自动使用SHA1, AES128, SYN,用RSA避免传输受到攻击)
音频传输(用Speex编码解码,8位的音频只需要每秒500字节传输)
远程终端(远程功能调用,远程管理你的程序,包括程序的设置,密码的管理和日志的管理)
目录服务器(目录服务器允许服务器列举他们自己需要的客户端,并与他们连接。)
Autopatcher (补丁系统,它将限制客户端传输到服务端的文件,这样是为了避免一些不合法的用户将一些不合法的文件传输到服务端。)
对象重载系统
网络数据压缩( BitStream类允许压缩矢量,矩阵,四元数和在-1到1之间的实数。)
强健的通信层(可以保障信息按照不同的信道传输)
网络连接类别
1、端到端模式;2、服务器/服务器模式。
项目
Raknet最新版RakNet 4.081。
VS如图:
Eclipse如图:
名称 | 描述 |
DLL | Windows平台下编译Raknet为动态库 |
LibStatic | Windows平台下编译Raknet为静态库 |
JanssonStatic | JSON库 |
MiniupnpcStatic | 支持UPnP网络网关设备库 |
BurstTest | 测试发送突发消息发送到远程系统 |
CloudTest | 云端测试 |
ComprehensiveTest | 综合内部测试,记录崩溃或泄漏。 |
CrossConnectionTest | 交叉连接测试,如果两个实例同时互相连接的问题。 |
DroppedConnectionTest | 掉线测试 |
FCM2Host | 测试服务器最大连接后的转移 |
FCM2HostSimultaneous | 测试多个服务器同时最大连接后的转移 |
FCM2VerifiedJoinSimultaneous | 测试多个服务器同时认证 |
FlowControlTest | 测试流量自动控制 |
LoopbackPerformanceTest | 性能测试,多个实例的吞吐量性能和开销。 |
MessageSizeTest | 消息大小测试 |
ReliableOrderedTest | 测试发送大量消息,顺序的可靠性。 |
ReplicatedLogin | 重复登录的问题 |
ServerClientTest2 | 测试连接采用客户/服务器的拓扑结构 |
TestDLL | 动态调用测试 |
ThreadTest | 测试多线程下的异常 |
IrrlichtDemo | 游戏演示测试 |
Ogre3DInterpDemo | 三维演示测试,使用Ogre 3D通过客户端/服务器网络显示一个爆米花的实例,使用ReplicaManager3。 |
AutopatcherClientGFx3 | 自动补丁测试 |
AutopatcherClient | 补丁客户端 |
AutopatcherClient_SelfScaling | 补丁完,安全退出测试 |
AutopatcherClientRestarter | 补丁完,重启测试 |
AutopatcherMySQLRepository | 补丁服务器,采用MySQL数据库实现 |
AutopatcherServer_MySQL | 补丁服务器测试,测试它的完整性 |
AutopatcherPostgreSQLRepository | 补丁信息和异步数据库查询 |
AutopatcherServer_PostgreSQL | 补丁服务器测试,测试它的完整性 |
AutopatcherServer_SelfScaling | 负载测试 |
CommandConsoleClient | 命令控制台客户端 |
CommandConsoleServer | 命令控制台服务器 |
PacketConsoleLogger | 控制台日志 |
Lobby2ClientGFx3 | |
RoomsBrowserGFx3_RakNet | |
Lobby2Client | |
Lobby2Server_PGSQL | PostgreSQL备份游戏数据的数据库 |
RoomsPlugin | |
SteamLobby | |
Lobby3 | |
NATCompleteClient | 穿透完成客户端 |
NATCompleteServer | 穿透完成服务端 |
UDPForwarderTest | UDP代理 |
Matrices | |
Demo_BspCollision | |
SQLiteClientLogger | 数据库日志 |
SQLiteServerLogger | 数据库日志 |
SQLite3Plugin | 数据库插件, 使用SQLite穿件一个网络日志文件,基于SQLite3Plugin |
RakVoice | 音频传输插件 |
RakVoiceDSound | 采用DSound来录制和播放声音 |
RakVoiceFMOD | 采用FMOD来录制和播放声音 |
BigPacketTest | 大数据包测试 |
Chat Example Client | 聊天客户端/服务器 |
Chat Example Server | 聊天客户端/服务器 |
CloudClient | 云客户端 |
CloudServer | 云服务端 |
ComprehensivePCGame | |
CrashReporter | 测试/演示事故报告系统 |
DirectoryDeltaTransfer | 目录列表传递,在目录之间发送变化或丢失的文件。 必要地,简单的补丁系统可以用于传输等级,皮肤等等。 |
Encryption | 加密 |
FileListTransfer | 文件传输 |
FullyConnectedMesh | 饱和连接, 使得所有的对等端自动连接到所有其他对等段的一个插件,选择最老的对等端作为主机。 |
LANServerDiscovery | 局域网服务器探索 |
MasterServer2 | |
MessageFilter | 消息过滤 |
OfflineMessagesTest | 离线消息测试 |
PacketLoggerTest | 包日志 |
PHPDirectoryServer2 | PHP目录服务器,从或者到C++在网页上列举游戏列表。 |
Ping | 测试ping |
RackspaceConsole | 托管API控制台 |
ReadyEvent | 准备活动,同步系统中一组玩家都已经准备好一个共同的标识,在端到端环境同时启动游戏很有用,或在基于轮的游戏中进行轮次很有用。 |
RelayPluginTest | 中转插件测试 |
ReplicaManager3 | 复制管理, 对你自己的游戏对象和玩家提供管理以实现序列化,划定范围以及创建和销毁对象更加容易的插件 |
Router2 | 向我们没有直接连接的远程系统发送网络消息 |
RPC3 | 测试/演示如何使用rpc3插件, 使用本地参数列表调用C和C++函数,使用Boost获得更多的功能 |
RPC4 | 测试/演示如何使用rpc4插件,调用C函数,不依赖额外的系统或库 |
SendEmail | 发送email |
StatisticsHistoryTest | 统计数据 |
TeamManager | 演示一个游戏大厅,用户可以在3支球队之间切换 |
Timestamping | 时间戳 |
TwoWayAuthentication | 双向认证,不用传输密码就可以验证一个先前设置的密码。 |
结构文件 | 描述 |
DS_BinarySearchTree.h | 二叉搜索树,以及AVL平衡二叉搜索树 |
DS_BPlusTree.h | B+树,用于快速查询,删除,和插入 |
DS_BytePool.h | 返回某个大小门限的数据块,减少内存碎片 |
DS_ByteQueue.h | 用于读写字节的队列 |
DS_Heap.h | 堆数据结构体,包括最小堆和最大堆 |
DS_HuffmanEncodingTree.h | 胡夫曼编码树,以给定的频率表用于查找最小按位显示 |
DS_HuffmanEncodingTreeFactory.h | 创建胡夫曼编码树实例 |
DS_HuffmanEncodingTreeNode.h | 胡夫曼编码树中的节点 |
DS_LinkedList.h | 标准链接链表 |
DS_List.h | 动态数组(有时不适宜地成为向量)。双向时作为一个栈 |
DS_Map.h | 关联数组,每一个元素带有分类键值的有序列表 |
DS_MemoryPool.h | 分配和释放固定大小的重用的实例,用于减少内存碎片 |
DS_Multilist.h | 将列表,栈和游戏列表整合成为一个带有通用接口的类 |
DS_OrderedChannelHeap.h | 最大堆返回一个基于关系权重的节点的相关信道,用于带有属性的任务调度 |
DS_OrderedList.h | 通过快排以一个任意键值排序的列表 |
DS_Queue.h | 用数组实现的标准队列 |
DS_QueueLinkedList.h | 用一个链表实现的标准队列 |
DS_RangeList.h | 存储一个列表的数字值,数字是顺序的,以一个序列代表他们。当存储许多序列值时比较有用。 |
DS_Table.h | 带有行列,以及表上的操作 |
DS_Tree.h | 非循环图 |
DS_WeightedGraph.h | 带有权重边得图,用于使用Dijkstra的算法进行路由 |
项目源码总共有268个文件,其中头文件有157个,C++中头文件一般都是描述类的,而实现都放在.cpp文件中,笔者将根据头文件列出相应的类名以及作用。
头文件 | 描述 |
_FindFirst.h | 查找数据结构,函数有:_findfirst、_findnext、_findclose。 |
AutopatcherPatchContext.h | 补丁枚举结构,成员有补丁哈希值、文件、失败原因、通知类。 |
AutopatcherRepositoryInterface.h | 补丁服务器接口,可以获取补丁文件、日期、错误等消息。 |
Base64Encoder.h | 实现一个编码函数Base64Map。 |
BitStream.h | 定义了一个可写入、读取比特流的类。 |
CCRakNetSlidingWindow.h | |
CCRakNetUDT.h | 封装了UDT阻塞控制。 |
CheckSum.h | 生成验证信息。 |
CloudClient.h | 顾名思义,云端客户端,实现拓扑结构网络结构。 |
CloudCommon.h | 云端辅助类,包含了云端信息生成、检索等功能。 |
CloudServer.h | 云服务端,存储了客户端的信息并提供跨服务检索信息。 |
CommandParserInterface.h | 命令解析接口。 |
ConnectionGraph2.h | 连接信息图,提供检索结构。 |
ConsoleServer.h | 服务器远程控制台实现。 |
DataCompressor.h | 数据压缩,就两个方法。 |
DirectoryDeltaTransfer.h | 目录文件传输,一般用于补丁、皮肤等。 |
DR_SHA1.h | 安全散列算法,用于记录文件是否修改。 |
DS_Hash.h | 哈希数据结构 |
DS_ThreadsafeAllocatingQueue.h | 线程安全队列,维护多线程。 |
DynDNS.h | 动态域名 |
EmailSender.h | 发送email |
EmptyHeader.h | 头信息,空文件 |
EpochTimeToString.h | 时间转为字符串 |
Export.h | 导出 |
FileList.h | 文件列表 |
FileListNodeContext.h | 文件节点句柄 |
FileListTransfer.h | 文件传输 |
FileListTransferCBInterface.h | 文件传输接口 |
FileOperations.h | 文件操作类 |
FormatString.h | 格式输出 |
FullyConnectedMesh2.h | 连接网络插件,负责连接所有的节点。 |
Getche.h | 获取字符 |
Gets.h | 获取字符 |
GetTime.h | 获取时间 |
gettimeofday.h | 获取日期 |
GridSectorizer.h | 网格 |
HTTPConnection.h | 封装了Http连接 |
HTTPConnection2.h | 同上 |
IncrementalReadInterface.h | 文件增加部分读取 |
InternalPacket.h | 定义了内部包结构 |
Itoa.h | 整形转换 |
Kbhit.h | 敲击键盘,获取响应的数据。 |
LinuxStrings.h | 字符串操作 |
LocklessTypes.h | 数据锁,增加和减少操作。 |
LogCommandParser.h | 日志命令解析 |
MessageFilter.h | 消息过滤 |
MessageIdentifiers.h | 包含了一个巨大的枚举数据,表示了RakNet用于发送消息的标识符,例如断开连接通知。 |
MTUSize.h | 定义MTU消息大小,最大值、最小值。 |
NativeFeatureIncludes.h | 定义本地功能,宏定义需要哪些功能。 |
NativeFeatureIncludesOverrides.h | 空文件 |
NativeTypes.h | 本地基本类型定义 |
NatPunchthroughClient.h | 穿透客户端配置 |
NatPunchthroughServer.h | 穿透服务端配置 |
NatTypeDetectionClient.h | 客户端匹配穿透方式 |
NatTypeDetectionCommon.h | 穿透方式 |
NatTypeDetectionServer.h | 服务端匹配穿透方式 |
NetworkIDManager.h | 网络标识管理 |
NetworkIDObject.h | 网络标识对象 |
PacketConsoleLogger.h | 网络日志控制,传入和传出过程日志解析。 |
PacketFileLogger.h | 文件数据包日志 |
PacketizedTCP.h | Tcp数据包 |
PacketLogger.h | 数据包日志 |
PacketOutputWindowLogger.h | 数据包输出日志 |
PacketPool.h | 空 |
PacketPriority.h | 枚举,包含优先级和可靠性。 |
PluginInterface2.h | 扩展插件接口,例如:声音插件、补丁更新插件。 |
PS3Includes.h | 空 |
PS4Includes.h | 空 |
Rackspace.h | 辅助管理服务器 |
RakAlloca.h | 定义申请内存函数 |
RakAssert.h | 空 |
RakMemoryOverride.h | 定义申请内存函数 |
RakNetCommandParser.h | 网络命令解析 |
RakNetDefines.h | 预定义 |
RakNetDefinesOverrides.h | 空 |
RakNetSmartPtr.h | 引用计数 |
RakNetSocket.h | 内部套接字 |
RakNetSocket2.h | 内部套接字 |
RakNetStatistics.h | 相关网络信息的统计数据 |
RakNetTime.h | 定义时间类型 |
RakNetTransport2.h | 安全的控制台连接 |
RakNetTypes.h | 定义了在RakNet中使用的结构体,包括SystemAddress结构体——系统的唯一标识符,以及当你需要接收数据或需要发送数据时,API返回给你的数据包。 |
RakNetVersion.h | 网络版本 |
RakPeer.h | 连接管理,例如流量控制。 |
RakPeerInterface.h | 连接管理接口。 |
RakSleep.h | 睡眠函数 |
RakString.h | 字符串的实现,比std::string速度高4.5倍 |
RakThread.h | 封装线程 |
RakWString.h | 封装宽字符操作 |
Rand.h | 随机数 |
RandSync.h | 随机数 |
ReadyEvent.h | 定义事件,端对端的事件。 |
RefCountedObj.h | 引用计数 |
RelayPlugin.h | 消息传递标识 |
ReliabilityLayer.h | 数据包控制,可靠的、有序的、无序的、流量控制等。 |
ReplicaEnums.h | 复制枚举类型 |
ReplicaManager3.h | 复制管理 |
Router2.h | 路由器插件,负责穿透时连接目标路由器。 |
RPC4Plugin.h | C函数调用插件 |
SecureHandshake.h | 握手协议 |
SendToThread.h | 向线程发送消息 |
SignaledEvent.h | 线程事件信号 |
SimpleMutex.h | 封装一个互斥类 |
SimpleTCPServer.h | 封装一个TCP服务器类 |
SingleProducerConsumer.h | 通过使用一个循环缓冲区队列中读写指针线程之间的数据 |
SocketDefines.h | 空 |
SocketIncludes.h | 定义socket需要的头文件 |
SocketLayer.h | Socket布局实现 |
StatisticsHistory.h | 统计历史 |
StringCompressor.h | 字符串压缩 |
StringTable.h | 字符串表 |
SuperFastHash.h | 哈希快速查找 |
TableSerializer.h | 表实序列化 |
TCPInterface.h | Tcp接口 |
TeamBalancer.h | 网络组的选择 |
TeamManager.h | 网络组管理 |
TelnetTransport.h | 远程传输 |
ThreadPool.h | 封装线程操作类 |
ThreadsafePacketLogger.h | 用户线程数据包记录。 |
TransportInterface.h | 传输接口 |
TwoWayAuthentication.h | 双向认证 |
UDPForwarder.h | 封装的UDP数据包 |
UDPProxyClient.h | UDP代理客户端 |
UDPProxyCommon.h | UDP代理通用类 |
UDPProxyCoordinator.h | UDP服务器状态管理 |
UDPProxyServer.h | UDP代理服务器 |
VariableDeltaSerializer.h | |
VariableListDeltaTracker.h | |
VariadicSQLParser.h | |
VitaIncludes.h | 空 |
WindowsIncludes.h | Win下需要的头文件 |
WSAStartupSingleton.h | 启动计数 |
XBox360Includes.h | 空 |
获取实例 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。每个数据报: 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系列每一个数据报
threadPriority 参数
对 于窗口程序,这个是RakPeer更新线程的优先级,传递给_beginthreadex()。对于Linux,这个参数传递给 pthread_attr_setschedparam()用于pthread_create()方法。默认的参数是-99999,在Windows上使 用0(NORMAL_PRIORITY),在Linux意味着使用优先权1000。Windows下,默认的参数就不错。而Linux下,可以将这个值设 置为正常优先权线程应该设置的值。
其实可以创建一组Socket的描述符,代码如下:
SocketDescriptor sdArray[2]; sdArray[0].port=SERVER_PORT_1; strcpy(sdArray[0].hostAddress, "192.168.0.1"); sdArray[1].port=SERVER_PORT_2; strcpy(sdArray[1].hostAddress, "192.168.0.2"); if (rakPeer->Startup( 32, 30, &sdArray, 2 ) OnRakNetStarted();这个是高级用户想要绑定多个网卡时使用。例如一个网卡连接到LAN后的安全服务器,另外一个网卡连接到因特网。访问不同的绑定组,可以将binding的索引传递给有参数connectionSocketIndex的RakPeerInterface接口的函数。
连接到其他的系统的方法,其实有五种方式来发现要连接到的系统,如下:
1、直接输入IP地址(这个广为人知)
2、LAN广播
3、使用ClientServer/CloudClient插件
4、使用游戏大厅服务器或房间插件
5、使用目录服务器DirectoryServer
方法一:直接输入IP地址
从编码的角度看,最简单,最容易的方式就是将IP地址或域名硬编码,或使用GUI询问用户,让他们来输入他们想要连接的系统的IP地址。很多例子使用这种方法。游戏刚刚出来的时候支持这种方式,这种方式是唯一可用的方式。
优势:
1. 对于编程人员和美工的要求较少,GUI可以很简单。
2. 如果IP地址或域名是固定的,例如运行的是一个专用服务器,这个就是最好的解决方案。
不足:
1. 缺乏灵活性
2. 用户仅仅可以与他们知道的人们玩游戏。
注意:要连接到本机上的RakPeer实例或其他相同的应用程序,IP地址要使用127.0.0.1或localhost。
方式二:LAN广播
RakNet支持在局域网中广播一个数据报发现其他的系统的功能,使用可选的数据来发送和检索相似的应用程序。例子LANServerDiscovery说明了这项技术。
在RakPeerInterface中,Ping函数可以做到这些,如下所述: rakPeer->Ping("255.255.255.255", REMOTE_GAME_PORT, onlyReplyOnAcceptingConnections); REMOTE_GAME_PORT应该是其他系统上你关心的应用程序运行的端口。 onlyReplyOnAcceptConnections是一个布尔值,来标识其他系统是否需要回复,即使你没有可用连接连接到该系统。 开放系统会回复ID_UNCONNECTED_PONG,例如下面的例子:
if (p->data[0]==ID_UNCONNECTED_PONG) { RakNet::TimeMS time; RakNet::BitStream bsIn(packet->data,packet->length,false); bsIn.IgnoreBytes(1); bsIn.Read(time); printf("Got pong from %s with time %i\n", p->systemAddress.ToString(), RakNet::GetTime() - time); }为了发送用户数据,调用RakPeer::SetOfflinePingResponse(customUserData, lengthInBytes);,RakNet会拷贝传递给它的数据,然后将数据返回回来追加到ID_UNCONNECTED_PONG。
方式三:使用CloudServer/CloudClient插件
不用修改,CloudServer/CloudClient插件直接就可以作为目录服务器。
方式四:使用游戏大厅服务器或房间插件
游戏大厅服务器提供了一个数据库驱动服务器,用于交互和开始游戏。它提供了一些功能,例如好友,配对,邮件,排名,即时通信,快速配对,房间,或房间协调。
参考Lobby2Server_PGSQL和Lobby2Client中对这项功能的使用方法。
优势:
1. 玩家加入游戏最灵活的处理方式
2. 允许用户在开始游戏之前进行交互
3. 建立社区
4. 支持多个标题
不足:
1. 需要一个分离的专用服务器来承载这个插件,服务器需要有数据库支持。
2. 功能相对于简单的游戏列表较大,且复杂,需要时间和编程方面投入更多。
方式五:DirectoryServer.php
DirectoryServer.php和相关的代码可以在Samples\PHPDirectoryServer2中找到。这种方式是给出游戏列表比 较廉价的方式,游戏上线后使用web服务器来存储,游戏信息是使用字符串来给出。获得更多信息,参考这个功能的参考手册。
优点:
1. 不需要专用的服务器,仅需要一个web页
缺点:
1. 不灵活
2. 有时不可用(需要多次访问)
发起连接尝试代码如下:
一旦知道了想要连接的远端系统的IP地址,使用RakPeerInterface::Connect()方法初始化一个异步的连接尝试,连接参数如下: ConnectionAttemptResult Connect( const char* host, unsigned short remotePort, const char *passwordData, int passwordDataLength, PublicKey *publicKey=0, unsigned connectionSocketIndex=0, unsigned sendConnectionAttemptCount=6, unsigned timeBetweenSendConnectionAttemptsMS=1000, RakNet::TimeMS timeoutTime=0 ) 1. host是一个IP地址,或域名 2. remotePort是远端系统监听的端口,传递给Startup()函数的端口参数。 3. passwordData是随着连接请求发送的二进制数据。如果这个参数与传递给RakPeerInterface::SetPassword()的参数不同,远端系统会回复ID_INVALID_PASSWORD。 4. passwordDataLength是passwordData的长度,单位是字节。 5. publicKey 是远端系统上传递给InitializeSecurity()函数的公用密钥参数。如果你不适用,传递0。 6. connectionSocketINdex是你要发送的客户端的Socket在传递给RakPeer::Startup()函数的socket描述符的数组中的索引。 7. sendConnectionAttemptCount是在确定无法连接前要做出的发送尝试次数。这个也用于MTU检测,使用3个不同的MTU大小。默认的值12意味着发送每个MTU四次,这对于容忍任何原因的包丢失也是足够的了。更低的值意味着ID_CONNECTION_ATTEMPT_FAILED会更快返回。 8. timeBetweenSendConnectionAttemptsMS是进行另外一次连接尝试要等待的毫秒数。比较好的值是4倍的ping值。 9. 如果消息不能发送,在丢掉远端系统之前,为这次连接,timeoutTime指出了要等待多少毫秒。默认值是0,意味着使用SetTimeoutTime()方法中的全局值。 连接尝试成功Connect()会返回CONNECTION_ATTEMPT_STARTED值,如果失败会返回其他的值。 注意:Connect()返回TRUE并不意味着已经连接成功。如果连接成功,应该会返回ID_CONNECTION_REQUEST_ACCEPTED。否则,会收到一条错误消息。
其中连接消息作为Packet::data结构的第一个字节返回,如下: 连接关闭: ID_DISCONNECTION_NOTIFICATION 丢失通知 ID_CONNECTION_LOST 连接关闭 新的连接: ID_NEW_INCOMING_CONNECTION 新的连接 ID_CONNECTION_REQUEST_ACCEPTED 请求接受 连接尝试失败: ID_CONNECTION_ATTEMPT_FAILED 连接失败 ID_REMOTE_SYSTEM_REQUIRES_PUBLIC_KEY 公钥 ID_OUR_SYSTEM_REQUIRES_SECURITY 安全请求 ID_PUBLIC_KEY_MISMATCH ID_ALREADY_CONNECTED 已经存在 ID_NO_FREE_INCOMING_CONNECTIONS 未释放连接 ID_CONNECTION_BANNED ID_INVALID_PASSWORD 无效密码 ID_INCOMPATIBLE_PROTOCOL_VERSION 无效协议 ID_IP_RECENTLY_CONNECTED 已连接ID_CONNECTION_ATTEMPT_FAILED是一条概述性消息,意味着与远端系统没有建立连接。可能的原因包括如下几方面:
如何将游戏数据编码到数据包中?
运 行RakNet的系统,事实上所有在因特网上的系统,都是通过人们所熟知的数据包进行通信。或更加准确点在UDP下,它用的是数据报。每一个数据报由 RakNet创建,并且包含了一条或多条消息。消息可以由你创建,例如位置或健康(health这个词确实不知道如何翻译好),或者有时由RakNet内 部创建的数据,例如pings。按照惯例,消息的第一个字节包含了一个从0到255的数字标识符,它用于表明消息的类型。RakNet有一大组内部使用的 消息,或者插件使用的标识符。这些可以在文件MessageIdentifiers.h查看到详细信息。
使用结构体或位流?
任何时候发送数据都是发送一个字符流。有两种很容易的方法将数据编码成为这种格式:一种是创建、一种结构体,然后将它转化为(char *),另外一种就是使用内置的BitStream类。
创建结构体进行转化的优点是很容易修改结构,并且可以看到你事实上正在发送的数据。由于发送者和接收者能够共享定义了结构体的文件,避免了转化的错误。也 没有让数据乱序,或者使用错误类型的危险。创建结构体的不足就是常常不得不改变和重新编译文件。并且丧失了使用Bitstream类进行自动压缩的便利。 并且RakNet不能自动转换结构体成员的字节序。
使用Bitstream的优点是不需要改变任何外部文件。仅仅需要一个bitstream,在其中写入你想要写入的数据,然后发送即可。可以使用 “Compressed”版本的read和write方法写入相对较少的数据,例如使用它写入bool类型,仅仅需要一位。可以动态写入数据,在某些确定 情况下写的值是true或者false。使用Serialize(),Write(), Read()等方法写的数据,Bitstream会自动进行网络字节序的转换。Bitstream的不足就是很容易出现数据处理错误。读取数据的方式与写 入的方式不完全相同-错误的序列,或者一个字节的错误数据,或者其他的错误。
下面将介绍两种方法创建数据包:
使用结构体创建数据包
没有时间戳的情况 #pragma pack(push, 1)//强制编译器(在VC++下)按照字节对齐的方式填充数据结构体。 struct structName { unsigned char typeId; // 数据类型(一个单字节的枚举类型数据) //+ 放置数据 //+时间戳的数据包 //+数据包数据类型的标识 //+传输的实际数据 }; #pragma pack(pop) 带有时间戳 #pragma pack(push, 1) struct structName { unsigned char useTimeStamp; // 赋值 ID_TIMESTAMP值 RakNet::Time timeStamp; // 将由RakNet::GetTime()返回的系统时间值或其他方式返回的类似值 unsigned char typeId; // 你的类型放到这里 // 这里放数据 }; #pragma pack(pop) 注意:发送数据的时候,RakNet假设timeStamp是网络字节序。必须使用timeStamp域的函数BitStream::EndianSwapBytes()实现字节序的变换。在接收系统上读取时间戳,使用if (bitStream->DoEndianSwap()) bitStream->ReverseBytes(timeStamp, sizeof(timeStamp)获得时间戳。如果使用的是BitStream这一步就不需要了。使用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。
第一步:确定数据
正如在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 }使用注释:
接 收一个位流数据(BitStream),如果你最初发送的是一个Bitstream,那就需要创建一个BitStream,按照我们的写入顺序来解析数 据。使用数据和数据包的长度来创建一个BitStream。我们写入数据的时候,使用的是Write函数,那么就使用Read函数读取数据。如果前面使用 的WriteCompressed函数,那读取数据就要使用ReadCompressed函数。如果我们条件性的写入任何数据,依据这个逻辑分支。在接下 来的例子中给出了处理在Creating packets中的地雷的数据:
void DoMyPacketHandler(Packet *packet) { Bitstream myBitStream(packet->data, packet->length, false); // false指定不拷贝数据,提高效率 myBitStream.Read(useTimeStamp); myBitStream.Read(timeStamp); myBitStream.Read(typeId); bool isAtZero; myBitStream.Read(isAtZero); if (isAtZero==false) { x=0.0f; y=0.0f; z=0.0f; } else { myBitStream.Read(x); myBitStream.Read(y); myBitStream.Read(z); } myBitStream.Read(networkID); // 在结构体中这里是 NetworkID networkId myBitStream.Read(systemAddress); // 在结构体中这里是SystemAddress systemAddress }3、通过将数据包传递给RakPeerInterface实例的DeallocatePakcet(Packet *packet)释放数据包。
SystemAddress是包含了网络上系统的二进制的IP地址和端口的结构体。结构体在RakNetTypes.h中定义。
在一些情况下需要使用SystemAddress,例如:
1. 服务器从一个特殊的客户端获取一个消息,想要中继(转发)给所有的其他客户端。你需要在Send函数中指定发送者的SystemAddress(在 RakNet::systemAddress域中给出),并且将广播设置为true(就是在Send函数中将广播标志设置为true,将不转发的客户端指 定为发送者的SystemAddress)。在游戏世界的一些项目,例如地雷,属于一个特定玩家,这个地雷杀死人之后也会给放置者分数。
2. 在一个端到端的网络上要发送一个消息到任何的端。
功能函数:
ToString() – 指定一个系统地址结构体,返回一个点分的IP地址
FromString – 指定一个点分的IP地址,填充结构体的binaryAddress部分
重要的注意点:
1. 数据包的接受方会自己知道发送数据包的系统的SystemAddress,因为它可以从发送者的IP/Port结合体中来获得这个值。如果仅仅需要服务器 知道SystemAddress是什么,那么发送者不需要将它自己的SystemAddress编码到数据包中。原始发送者的SystemAddress 在数据包结构体中自动传递给编程人员,Receive会返回它!
2. 当使用客户端/服务器模型时,客户端不知道发送数据包的原始发送者的SystemAddress。只要客户端连接了服务器,所有的数据包都来自服务器。因 此如果客户端需要知道另外一个客户端的SystemAddress,则要将数据包中加入一个SystemAddress数据结构。可以让发送客户端填充这 个数据域,或者也可以让服务器从原始发送者那里接收到数据包时来填充这个数据结构体。
3. 在连接期间一个特定的RakPeer实例的系统地址不会发生变化,Router2插件除外。然而并不是所有的系统都是这样的(例如在对称的NAT后面的系 统就不是这样的情况)。需要一个唯一的标识符,因为在数据包结构体重有一个唯一的标识符,例如rakNetGUID。RakPeerInterface 有一些函数来操作RakNetGUID。
4. 通过RakNetGUID来指向远端系统是非常好的方法,而SystemAddress则不是特别好的选择。RakNetGUID对于一个RakPeer 实例来说是唯一的,然后SystemAddress却不一定是唯一的。如果在系统中要使用Router2插件,唯一地使用RakNetGUID很有必要。
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对象后在动态重新分配内存。读取数据
// 假设我们接收到一个数据包Packet * BitStream myBitStream(packet->data, packet->length, false); struct MyVector { float x,y,z; } myVector; // 没有字节序转换 bitStream.Read(myVector); // 要转换字节序(__BITSTREAM_NATIVE_END在RakNetDefines.h中要注释掉) #undef __BITSTREAM_NATIVE_END #include "BitStream.h" bitStream.Read(myVector.x); bitStream.Read(myVector.y); bitStream.Read(myVector.z);序列化数据
struct MyVector { float x,y,z; // 如果ToBitstream==true,则是写入数据, 如果ToBitstream==false,则是读取数据 void Serialize(bool writeToBitstream, BitStream *bs) { bs->Serialize(writeToBitstream, x); bs->Serialize(writeToBitstream, y); bs->Serialize(writeToBitstream, z); } } myVector;有用函数,参考BitStream.h查看完整的函数列表,如下:
Rese t函数 重置bitstream,清除所有的数据。 Write 函数 Write函数在bitstream的最后写入数据。应该使用类似的Read函数从bitstream中将数据读取出来。 Read函数 Read函数用来读取已经存在在bitstream中的数据,从头到尾按照顺序读取。如果读到了bitstream的结尾处了,Read函数会返回false值。 WriteCasted,ReadCasted 写一种类型的数据就像是它被转化为了其他类型的数据。例如WriteCasted控制何时如何使用数据包优先级和可靠性类型(5),等价于写入char c=5; Write(c); WriteNormVector, ReadNormVector 写入一个通常的向量,其中每一个元素的范围都是-1 — 1。每一个元素有16位。 WriteFloat16,ReadFloat16 给出一个floating指针数字的最大值和最小值,除以范围65535,将结果以16个字节写入。 WriteNormQuat,ReadNormQuat 在16*3 + 4位中,写入一个四元组。 WriteOrthMatrix,ReadOrthMatrix 将一个正交矩阵转换为四元组,然后调用WriteNormQuat,ReadNormQuat写入和读取数据 GetNumberOfBitsUsed,GetNumberOfBytesUsed 返回写入的字节数或位数。 GetData 返回一个指向Bitstream内部数据的指针。这个数据是用(char *)类型使用malloc分配的,在你需要直接访问bitstream的数据时使用。 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)
packet = rakPeer->Receive(); while (packet) { switch(packet->data[0]) { case ID_SND_RECEIPT_ACKED: memcpy(&msgNumber, packet->data+1, 4); printf("Msg #%i was delivered.\n", msgNumber); break; case ID_SND_RECEIPT_LOSS: memcpy(&msgNumber, packet->data+1, 4); printf("Msg #%i was probably not delivered.\n", msgNumber); break; } sender->DeallocatePacket(packet); packet = sender->Receive(); } 使用这个值得原因就是了解不可靠类型消息是否到达接受方。有时候如果不可靠消息丢失,要重发不可靠消息,由于这些数据都是时新(实时的)数据,不能使用可靠类型。要实现 这样的功能,在发送不可靠数据的时候,需要创建一个Send()或SendList()返回的值到接受方返回值之间的一个映射。如果接受方的返回值为ID_SND_RECEIPT_LOSS,那么就需要重新发送 本条返回消息值所对应的数据。高级发送类型
typedef unsigned char SequenceNumberType; bool GreaterThan(SequenceNumberType a, SequenceNumberType b) { // a > b? const SequenceNumberType halfSpan =(SequenceNumberType) (((SequenceNumberType)(const SequenceNumberType)-1)/(SequenceNumberType)2); return b!=a && b-a>halfSpan; }序列化的数据,而不是使用序列化的消息
// 保留类型—不要修改这些类型定义 // 所有的类型来自于RakPeer // 这些类型不会返回给用户 // 来自于一个连接的系统的Ping。更新时间戳(仅仅内部使用) ID_CONNECTED_PING, // 来自于一个未连接系统的Ping。回复,但是不要更新时间戳(仅仅内部使用) ID_UNCONNECTED_PING,// 来自于未连接系统的Ping,如果已经打开了连接,则回复,不要更新时间戳(仅用于内部) ID_UNCONNECTED_PING_OPEN_CONNECTIONS,// 来自连接系统的Pong,更新时间戳(仅内部内部) ID_CONNECTED_PONG,// 一个可靠数据包,用于检测连接丢失(仅仅用于内部) ID_DETECT_LOST_CONNECTIONS,// C2S: 初始化查询: Header(1), OfflineMesageID(16), Protocol number(1), Pad(toMTU), 发送 // 不用分片,如果在服务器上协议失败,返回ID_INCOMPATIBLE_PROTOCOL_VERSION // 到客户端 ID_OPEN_CONNECTION_REQUEST_1, // S2C: Header(1), OfflineMesageID(16), server GUID(8), HasSecurity(1), // Cookie(4, 如果设置了HasSecurity), public key (如果doSecurity设置为true), // MTU(2). 如果公钥在客户端使用失败,返回ID_PUBLIC_KEY_MISMATCH ID_OPEN_CONNECTION_REPLY_1, // C2S: Header(1), OfflineMesageID(16), Cookie(4, 如果在服务器HasSecurity为true), // clientSupportsSecurity(1 bit), handshakeChallenge (如果在服务器和客户端设置了security), // remoteBindingAddress(6), MTU(2), client GUID(8) // 如果cookie有效则分配连接间隙,服务器没有满,GUID和IP没有使用 ID_OPEN_CONNECTION_REQUEST_2, // S2C: Header(1), OfflineMesageID(16), 服务器GUID(8), MTU(2), // doSecurity(1位), handshakeAnswer (如果doSecurity值为true) ID_OPEN_CONNECTION_REPLY_2, /// C2S: Header(1), GUID(8), Timestamp, HasSecurity(1), Proof(32) ID_CONNECTION_REQUEST, // RakPeer –远端系统要求安全连接,给RakPeerInterface::Connect()公有密钥 ID_REMOTE_SYSTEM_REQUIRES_PUBLIC_KEY, // RakPeer–给RakPeerInterface::Connect()传递了公共密钥,但是系统没有开启安全检测 ID_OUR_SYSTEM_REQUIRES_SECURITY, // RakPeer- 传递给RakPeerInterface::Connect()的公钥密钥是错误的 ID_PUBLIC_KEY_MISMATCH, // RakPeer-与ID_ADVERTISE_SYSTEM相同,但是仅仅是系统内部使用,不会返回给用户 // 第二个字节指明类型,当前用于NAT 穿透的端口广播。 // 参考ID_NAT_ADVERTISE_RECIPIENT_PORT ID_OUT_OF_BAND_INTERNAL, // 如果调用了RakPeerInterface::Send(),其中PacketReliability中有 _WITH_ACK_RECEIPT, // 然后稍迟一点调用RakPeerInterface::Receive(),可以得到ID_SND_RECEIPT_ACKED或 // ID_SND_RECEIPT_LOSS。消息有5字节长,并且1-4字节包含了一个本地有序的号码, //它标识了这条消息。这个数字会由RakPeerInterface::Send()或RakPeerInterface::SendList() //返回. ID_SND_RECEIPT_ACKED意味着这条消息到达了接受方 ID_SND_RECEIPT_ACKED, // 如果用PacketReliability包含_WITH_ACK_RECEIPT 调用的RakPeerInterface::Send() // 然后调用RakPeerInterface::Receive(),会得到一个ID_SND_RECEIPT_ACKED或 // ID_SND_RECEIPT_LOSS。这条消息会有5字节长,并且1-4字节会包含一个标识这条消 // 息的数字。这个数字由RakPeerInterface::Send()或RakPeerInterface::SendList()返回 // ID_SND_RECEIPT_LOSS意味着消息没有达到的确认(这条消息发送了,或许没有发送, // 可能没有).在连接断开或关闭时,对于没有发送的消息会得到ID_SND_RECEIPT_LOSS // 标识,应该将这些消息当做是完全丢失了 ID_SND_RECEIPT_LOSS, // 用户类型-不要修改这些定义 // RakPeer-在客户端/服务器环境下,我们的连接要求服务器已经接受 ID_CONNECTION_REQUEST_ACCEPTED, // RakPeer-如果连接请求无法完成时,给玩家回复这样一个消息 ID_CONNECTION_ATTEMPT_FAILED, // RakPeer-向你当前要连接到的系统发送连接请求。 ID_ALREADY_CONNECTED, // RakPeer-远端系统已经成功连接。 ID_NEW_INCOMING_CONNECTION, // RakPeer-试图连接的系统不接受新的连接。 ID_NO_FREE_INCOMING_CONNECTIONS, // RakPeer-系统在Packet::systemAddress中指定的已经从服务器断开的。对于客户端,这个 // 标识意味着服务器已经关闭 ID_DISCONNECTION_NOTIFICATION, // RakPeer-可靠数据包不能传递到Packet::systemAddress指定系统。到该系统连接已经断开 ID_CONNECTION_LOST, // RakPeer-被要连接到的系统禁止掉了 ID_CONNECTION_BANNED, // RakPeer-远端系统使用了密码,因为设置密码不正确拒绝了连接请求 ID_INVALID_PASSWORD, // 在 RakNetVersion.h中的RAKNET_PROTOCOL_VERSION与远端系统上的本值不匹配 // 这意味这两个系统无法通信 // 消息的第二个字节包含了远端系统的RAKNET_PROTOCOL_VERSION值 ID_INCOMPATIBLE_PROTOCOL_VERSION, // 意味着这个IP最近连接到了系统,作为安全连接,已经无法再建立连接 // 参考RakPeer::SetLimitIPConnectionFrequency() ID_IP_RECENTLY_CONNECTED, // RakPeer- sizeof(RakNetTime)个字节大小的值,紧跟着它的一个字节代表了自动修改的 // 发送方和接收方系统的差值,需要调用用SetOccasionalPing方法获取这个值。 ID_TIMESTAMP, // RakPeer-来自未连接的系统的Pong。第一个字节是ID_UNCONNECTED_PONG, // 第二个 sizeof(RakNet::TimeMS)大小字节的值是ping。紧跟着这个字节的是系统指定的 // 枚举数据,使用bitstreams读取。 ID_UNCONNECTED_PONG, // RakPeer- 通知远端系统我们的IP/Port。 // 在接受方,所有的传递的ID_ADVERTISE_SYSTEM数据是传递的数据参数。 ID_ADVERTISE_SYSTEM, // RakPeer-下载一个大的消息,格式是ID_DOWNLOAD_PROGRESS (MessageID), // partCount (unsigned int),partTotal (unsigned int),partLength (unsigned int), // 第一个部分数据 (length <= MAX_MTU_SIZE)。参见文件FileListTransferCBInterface.h中 // 的OnFileProgress的三个参数 partCount, partTotal和partLength ID_DOWNLOAD_PROGRESS, // ConnectionGraph2插件-在客户端/服务器环境中,一个客户端已经断开了连接, // 修改Packet::systemAddress以反映这个断开的客户端的systemAddress ID_REMOTE_DISCONNECTION_NOTIFICATION, // ConnectionGraph2插件-在客户端/服务器环境,客户端被迫断开了连接 // 修改Packet::systemAddress来反映这个已经断开连接的客户端的systemAddress ID_REMOTE_CONNECTION_LOST, // ConnectionGraph2 产检: 1-4字节 = count。 // 对于 (count items)包含了{SystemAddress, RakNetGUID} ID_REMOTE_NEW_INCOMING_CONNECTION, /// FileListTransfer插件 – 设置数据 ID_FILE_LIST_TRANSFER_HEADER, // FileListTransfer plugin – 一个文件 ID_FILE_LIST_TRANSFER_FILE, // 请求加入文件,发送多个文件。 ID_FILE_LIST_REFERENCE_PUSH_ACK, // DirectoryDeltaTransfer 插件-从远端系统请求要下载的目录 ID_DDT_DOWNLOAD_REQUEST, // RakNetTransport plugin – 用于远端控制台的提供者消息 ID_TRANSPORT_STRING, // ReplicaManager plugin – 创建一个对象 ID_REPLICA_MANAGER_CONSTRUCTION, // ReplicaManager plugin – 改变对象的范围 ID_REPLICA_MANAGER_SCOPE_CHANGE, // ReplicaManager plugin – 序列化对象的数据 ID_REPLICA_MANAGER_SERIALIZE, // ReplicaManager plugin – 新的连接,要发送所有的对象 ID_REPLICA_MANAGER_DOWNLOAD_STARTED, // ReplicaManager plugin –完成了所有序列化对象的下载 ID_REPLICA_MANAGER_DOWNLOAD_COMPLETE, // 已经存在于远端系统对象的序列化构造 ID_REPLICA_MANAGER_3_SERIALIZE_CONSTRUCTION_EXISTING, ID_REPLICA_MANAGER_3_LOCAL_CONSTRUCTION_REJECTED, ID_REPLICA_MANAGER_3_LOCAL_CONSTRUCTION_ACCEPTED, // RakVoice plugin – 打开通信信道 ID_RAKVOICE_OPEN_CHANNEL_REQUEST, // RakVoice plugin – 接收通信信道 ID_RAKVOICE_OPEN_CHANNEL_REPLY, // RakVoice plugin – 关闭通信信道 ID_RAKVOICE_CLOSE_CHANNEL, // RakVoice plugin – 语音数据 ID_RAKVOICE_DATA, // Autopatcher plugin – 获取一个从某个时间开始的修改过的文件 ID_AUTOPATCHER_GET_CHANGELIST_SINCE_DATE, // Autopatcher plugin – 要创建的文件的列表 ID_AUTOPATCHER_CREATION_LIST, // Autopatcher plugin – 要删除的文件的列表 ID_AUTOPATCHER_DELETION_LIST, // Autopatcher plugin – 要升级的文件的列表 ID_AUTOPATCHER_GET_PATCH, // Autopatcher plugin – 用于一个文件列表的补丁列表 ID_AUTOPATCHER_PATCH_LIST, // Autopatcher plugin –返回到用户:一个补丁系统数据库的错误 ID_AUTOPATCHER_REPOSITORY_FATAL_ERROR, // Autopatcher plugin –从补丁系统下载的所有文件已经完成下载 ID_AUTOPATCHER_FINISHED_INTERNAL, ID_AUTOPATCHER_FINISHED, // Autopatcher plugin – 返回到用户: 需要重启完成补丁过程。 ID_AUTOPATCHER_RESTART_APPLICATION, // NATPunchthrough plugin: 内部使用 ID_NAT_PUNCHTHROUGH_REQUEST, // NATPunchthrough plugin: internal ID_NAT_CONNECT_AT_TIME, // NATPunchthrough plugin: internal ID_NAT_GET_MOST_RECENT_PORT, // NATPunchthrough plugin: internal ID_NAT_CLIENT_READY, // NATPunchthrough plugin:目的系统没有连接到服务器,偏移量为1的字节包含了 // RakNetGUID, NatPunchthroughClient::OpenNAT()的目的域。 ID_NAT_TARGET_NOT_CONNECTED, // NATPunchthrough plugin:目的系统没有对ID_NAT_GET_MOST_RECENT_PORT做出 //反应,或许插件没有安装,从偏移量为1的字节开始 // 包含了NatPunchthroughClient::OpenNAT()目的域的RakNetGUID。 ID_NAT_TARGET_UNRESPONSIVE, // NATPunchthrough plugin: 在建立设置穿透时,服务器丢失了到目的系统的连接 // 可能消息没有安装。从偏移量为1的字节开始 // 包含了NatPunchthroughClient::OpenNAT()目的域的RakNetGUID。 ID_NAT_CONNECTION_TO_TARGET_LOST, // NATPunchthrough plugin: 穿透工作正在进行,可能该插件没有安装。从偏移量为1的字节 //开始包含了NatPunchthroughClient::OpenNAT()目的域的RakNetGUID。 ID_NAT_ALREADY_IN_PROGRESS, // NATPunchthrough plugin: 这条消息是本地系统生成的,并不是来自网络, // packet::guid 包含了NatPunchthroughClient::OpenNAT()的目的域。如果自己是发送方, // 第一个字节为1,否则是0 ID_NAT_PUNCHTHROUGH_FAILED, // NATPunchthrough plugin: 穿透成功。参考packet::systemAddress和packet::guid。 // 如果你是发送者第一个字节为1,否则为0 // 你现在可以使用RakPeer::Connect() or其他调用与系统通信 ID_NAT_PUNCHTHROUGH_SUCCEEDED, // ReadyEvent plugin – 为一个特殊的系统设置准备好状态。 // 消息之后的最前面的四个字节包含了id值 ID_READY_EVENT_SET, // ReadyEvent plugin – 将一个系统的准备好状态重置掉,消息后的4个字节包含了id值 ID_READY_EVENT_UNSET, // 所有的系统都处于ID_READY_EVENT_SET状态 // 消息后的4个字节包含了id值 ID_READY_EVENT_ALL_SET, // \internal, 在游戏中不要使用 // ReadyEvent plugin – 准备好事件状态请求,新连接时用于拉取数据 ID_READY_EVENT_QUERY, /// Lobby 数据包,第二个字节表明了数据类型 ID_LOBBY_GENERAL, // RPC3, RPC4插件错误 ID_RPC_REMOTE_ERROR, // 基于RPC系统的穿件的替换 ID_RPC_PLUGIN, // FileListTransfer传递大文件,仅仅在需要的时候再读取,以节省内存 ID_FILE_LIST_REFERENCE_PUSH, // 强制重置所有的准备好事件 ID_READY_EVENT_FORCE_ALL_SET, // 房间函数 ID_ROOMS_EXECUTE_FUNC, ID_ROOMS_LOGON_STATUS, ID_ROOMS_HANDLE_CHANGE, /// Lobby2消息 ID_LOBBY2_SEND_MESSAGE, ID_LOBBY2_SERVER_ERROR, // 通知用户新的主机的GUID. Packet::Guid包含了这个新的主机的RakNetGuid。 // 老主机可以使用BitStream->Read(RakNetGuid)读取这个值 ID_FCM2_NEW_HOST, /// \internal For FullyConnectedMesh2 plugin ID_FCM2_REQUEST_FCMGUID, /// \internal For FullyConnectedMesh2 plugin ID_FCM2_RESPOND_CONNECTION_COUNT, // \internal For FullyConnectedMesh2 plugin ID_FCM2_INFORM_FCMGUID, // UDP 代理消息。第二个类型表明数据类型 ID_UDP_PROXY_GENERAL, // SQLite3Plugin – 执行 ID_SQLite3_EXEC, // SQLite3Plugin – 远端数据库位置 ID_SQLite3_UNKNOWN_DB, // SQLiteClientLoggerPlugin事件发生 ID_SQLLITE_LOGGER, // 向NatTypeDetectionServer发送数据 ID_NAT_TYPE_DETECTION_REQUEST, // 向NatTypeDetectionClient发送。字节1包含了NAT检测类型 ID_NAT_TYPE_DETECTION_RESULT, // 用于router2 插件 ID_ROUTER_2_INTERNAL, // 没有可用路径,或没有到远端系统的连接 // Packet::guid 包含了我们要达到的端点的guid ID_ROUTER_2_FORWARDING_NO_PATH, // \brief 现在可以调用connect, ping, 其他操作 // 按照如下代码进行连接: // RakNet::BitStream bs(packet->data, packet->length, false); // bs.IgnoreBytes(sizeof(MessageID)); // RakNetGUID endpointGuid; // bs.Read(endpointGuid); // unsigned short sourceToDestPort; // bs.Read(sourceToDestPort); // char ipAddressString[32]; // packet->systemAddress.ToString(false, ipAddressString); // rakPeerInterface->Connect(ipAddressString, sourceToDestPort, 0,0); ID_ROUTER_2_FORWARDING_ESTABLISHED, // 一个转发连接的IP已经改变 // 对于每一个 ID_ROUTER_2_FORWARDING_ESTABLISHED读取endpointGuid 和 port ID_ROUTER_2_REROUTED, // \internal 用于team balancer 插件 ID_TEAM_BALANCER_INTERNAL, // 由于人数已满而无法转到满意的团队。然而,如果这个团队有人离开,你会获得 // 获取 ID_TEAM_BALANCER_SET_TEAM值,字节1包含了你想要加入团队的号码 ID_TEAM_BALANCER_REQUESTED_TEAM_CHANGE_PENDING, // 由于团队已经上锁,无法转到想去的团队,你会获得 // 获取 ID_TEAM_BALANCER_SET_TEAM值,字节1包含了你想要加入团队的号码 ID_TEAM_BALANCER_TEAMS_LOCKED, // Team balancer插件通知你你的团队。Byte 1 包含了你要加入的团队 ID_TEAM_BALANCER_TEAM_ASSIGNED, // Gamebryo Lightspeed集成 ID_LIGHTSPEED_INTEGRATION, // XBOX 集成 ID_XBOX_LOBBY, // 密码用于挑战传递这个密码的系统,意味着其他的系统需要使用我们传递给 // TwoWayAuthentication::Challenge()的密码调用TwoWayAuthentication::AddPassword() /// You can read the identifier used to challenge as follows: /// RakNet::BitStream bs(packet->data, packet->length, false); // bs.IgnoreBytes(sizeof(RakNet::MessageID)); RakNet::RakString password; bs.Read(password); ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_SUCCESS, ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_SUCCESS, // 远端系统使用TwoWayAuthentication::Challenge()向我们发送一个挑战,挑战失败 // 如果其他的系统需要将挑战保持,你应该调用RakPeer::CloseConnection() // 终止到其他系统的连接(此处不理解是什么意思,包括前面两条) ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_FAILURE, // 其他的系统没有加入我们在TwoWayAuthentication::AddPassword()使用的密码 // 可以使用如下读取挑战标识符: // RakNet::BitStream bs(packet->data, packet->length, false); bs.IgnoreBytes(sizeof(MessageID)); // RakNet::RakString password; bs.Read(password); ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_FAILURE, // 其他的系统没有在事件门限内给出反应。这个系统或者是没有运行相应插件, // 或者它在某个事件上长时间阻塞了。 // 可以按照如下方式读取用于challenge的标识符: /// RakNet::BitStream bs(packet->data, packet->length, false); // bs.IgnoreBytes(sizeof(MessageID)); // RakNet::RakString password; bs.Read(password); ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_TIMEOUT, // \内部 ID_TWO_WAY_AUTHENTICATION_NEGOTIATION, // CloudClient / CloudServer ID_CLOUD_POST_REQUEST, ID_CLOUD_RELEASE_REQUEST, ID_CLOUD_GET_REQUEST, ID_CLOUD_GET_RESPONSE, ID_CLOUD_UNSUBSCRIBE_REQUEST, ID_CLOUD_SERVER_TO_SERVER_COMMAND, ID_CLOUD_SUBSCRIPTION_NOTIFICATION, // 可以在不修改用户的枚举类型前提下,增加一些协议 ID_RESERVED_1, ID_RESERVED_2, ID_RESERVED_3, ID_RESERVED_4, ID_RESERVED_5, ID_RESERVED_6, ID_RESERVED_7, ID_RESERVED_8, ID_RESERVED_9, // 留给用户,从这个值开始你的消息类型定义 ID_USER_PACKET_ENUM, 10、Timestamping your packets 时间戳如何在不同的计算机上相同的时间帧内相应同一个事件?
假设在客户端即将发生一个事件,它的本地系统时间是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静态对象:(networkId); 如下是一个将指针存储到类的一个例子,重新检索出来,使用Assert确保有效工作(不出现错误): class Solider : public NetworkIDObject{} int main(void) { NetworkIDManager networkIDManager; Solider * solider = new Solider; solider->SetNetworkIDManager(&networkIDManager); NetworkID soliderNetworkID = solider->GetNetworkID(); Assert(networkIDManager.GET_OBJECT_FROM_ID (soliderNetworkID)== solider); } 如下是一个例子,使用系统创建一个远端系统上的对象,将同一个ID值赋给两者: Server: void CreateSoldier(void) { Soldier *soldier = new Soldier; soldier->SetNetworkIDManager(&networkIDManager); RakNet::BitStream bsOut; bsOut.Write((MessageID)ID_CREATE_SOLDIER); bsOut.Write(soldier->GetNetworkID()); rakPeerInterface->Send(&bsOut,HIGH_PRIORITY,RELIABLE_ORDERED,0,UNASSIGNED_SYSTEM_ADDRESS,true); } Client: Packet *packet = rakPeerInterface->Receive(); if (packet->data[0]==ID_CREATE_SOLDIER) { RakNet::BitStream bsIn(packet->data, packet->length, false); bsIn.IgnoreBytes(sizeof(MessageID)); NetworkID soldierNetworkID; bsIn.Read(soldierNetworkID); Soldier *soldier = new Soldier; soldier->SetNetworkIDManager(&networkIDManager); soldier->SetNetworkID(soldierNetworkID); }
// 所有的NetworkID在这里增加 enum StaticNetworkIDs { CTF_FLAG_1, CTF_FLAG_2, CTF_FLAG_3, }; class Flag : public NetworkIDObject { // 关卡设计者给标记命名flag1, flag2, 或flag3都可以, // 地图在其他系统也是以相似方式加载 Flag(std::string flagName, NetworkIDManager *networkIDManager) { SetNetworkIDManager(networkIDManager); if (flagName=="flag1") SetNetworkID(CTF_FLAG_1); else if (flagName=="flag2") SetNetworkID(CTF_FLAG_2); else if (flagName=="flag3") SetNetworkID(CTF_FLAG_3); };