RakNet

来源:互联网 发布:windows欢迎界面后黑屏 编辑:程序博客网 时间:2024/05/21 13:33
简介
RakNet是一个基于UDP网络传输协议的C++网络库,允许程序员在他们自己的程序中实现高效的网络传输服务。通常情况下用于游戏,但也可以用于其它项目。

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如图:



项目描述名称描述DLLWindows平台下编译Raknet为动态库LibStaticWindows平台下编译Raknet为静态库JanssonStaticJSON库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_PGSQLPostgreSQL备份游戏数据的数据库RoomsPlugin SteamLobby Lobby3 NATCompleteClient穿透完成客户端NATCompleteServer穿透完成服务端UDPForwarderTestUDP代理Matrices Demo_BspCollision SQLiteClientLogger数据库日志SQLiteServerLogger数据库日志SQLite3Plugin数据库插件, 使用SQLite穿件一个网络日志文件,基于SQLite3PluginRakVoice音频传输插件RakVoiceDSound采用DSound来录制和播放声音RakVoiceFMOD采用FMOD来录制和播放声音BigPacketTest 大数据包测试Chat Example Client聊天客户端/服务器Chat Example Server聊天客户端/服务器CloudClient云客户端CloudServer云服务端ComprehensivePCGame CrashReporter测试/演示事故报告系统DirectoryDeltaTransfer目录列表传递,在目录之间发送变化或丢失的文件。 必要地,简单的补丁系统可以用于传输等级,皮肤等等。Encryption加密FileListTransfer文件传输FullyConnectedMesh饱和连接, 使得所有的对等端自动连接到所有其他对等段的一个插件,选择最老的对等端作为主机。LANServerDiscovery局域网服务器探索MasterServer2 MessageFilter消息过滤OfflineMessagesTest离线消息测试PacketLoggerTest包日志PHPDirectoryServer2PHP目录服务器,从或者到C++在网页上列举游戏列表。Ping测试pingRackspaceConsole托管API控制台ReadyEvent准备活动,同步系统中一组玩家都已经准备好一个共同的标识,在端到端环境同时启动游戏很有用,或在基于轮的游戏中进行轮次很有用。RelayPluginTest中转插件测试ReplicaManager3复制管理,   对你自己的游戏对象和玩家提供管理以实现序列化,划定范围以及创建和销毁对象更加容易的插件Router2向我们没有直接连接的远程系统发送网络消息RPC3测试/演示如何使用rpc3插件, 使用本地参数列表调用C和C++函数,使用Boost获得更多的功能RPC4测试/演示如何使用rpc4插件,调用C函数,不依赖额外的系统或库SendEmail发送emailStatisticsHistoryTest统计数据TeamManager演示一个游戏大厅,用户可以在3支球队之间切换Timestamping时间戳TwoWayAuthentication双向认证,不用传输密码就可以验证一个先前设置的密码。
详情
1、RakNet使用哪些数据结构?结构文件描述DS_BinarySearchTree.h二叉搜索树,以及AVL平衡二叉搜索树DS_BPlusTree.hB+树,用于快速查询,删除,和插入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发送emailEmptyHeader.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.hTcp数据包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.hC函数调用插件SecureHandshake.h握手协议SendToThread.h向线程发送消息SignaledEvent.h线程事件信号SimpleMutex.h封装一个互斥类SimpleTCPServer.h封装一个TCP服务器类SingleProducerConsumer.h通过使用一个循环缓冲区队列中读写指针线程之间的数据SocketDefines.h空SocketIncludes.h定义socket需要的头文件SocketLayer.hSocket布局实现StatisticsHistory.h统计历史StringCompressor.h字符串压缩StringTable.h字符串表SuperFastHash.h哈希快速查找TableSerializer.h表实序列化TCPInterface.hTcp接口TeamBalancer.h网络组的选择TeamManager.h网络组管理TelnetTransport.h远程传输ThreadPool.h封装线程操作类ThreadsafePacketLogger.h用户线程数据包记录。TransportInterface.h传输接口TwoWayAuthentication.h双向认证UDPForwarder.h封装的UDP数据包UDPProxyClient.hUDP代理客户端UDPProxyCommon.hUDP代理通用类UDPProxyCoordinator.hUDP服务器状态管理UDPProxyServer.hUDP代理服务器VariableDeltaSerializer.h VariableListDeltaTracker.h VariadicSQLParser.h VitaIncludes.h空WindowsIncludes.hWin下需要的头文件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()) {} //其中 <span style="font-family: Arial, Helvetica, sans-serif;">DeallocatePacket  //</span><span style="font-family: Arial, Helvetica, sans-serif;">数据包释放掉</span>发送数据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、系统概览系统结构<span style="white-space: pre;"></span>RakNet大致上说定义了3个库:网络通信库、网络通信的插件模块、扩展支持功能。<span style="white-space: pre;"></span>网络通信是用两个类来提供的。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_ENDbitStream.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_MISMATCHID_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_PORTID_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和partLengthID_DOWNLOAD_PROGRESS,// ConnectionGraph2插件-在客户端/服务器环境中,一个客户端已经断开了连接,// 修改Packet::systemAddress以反映这个断开的客户端的systemAddressID_REMOTE_DISCONNECTION_NOTIFICATION,// ConnectionGraph2插件-在客户端/服务器环境,客户端被迫断开了连接// 修改Packet::systemAddress来反映这个已经断开连接的客户端的systemAddressID_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: internalID_NAT_CONNECT_AT_TIME,// NATPunchthrough plugin: internalID_NAT_GET_MOST_RECENT_PORT,// NATPunchthrough plugin: internalID_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,否则是0ID_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 pluginID_FCM2_REQUEST_FCMGUID,/// \internal For FullyConnectedMesh2 pluginID_FCM2_RESPOND_CONNECTION_COUNT,// \internal For FullyConnectedMesh2 pluginID_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 / CloudServerID_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);       };
0 0
原创粉丝点击