多人网络游戏中如何避免迟延是一个比较重要的话题。作为多人网络游戏开发者,我们总是努力使事情做得更快,减少迟延以获得更多的带宽。这也是我们为什么通常会抛弃TCP的稳定性而使用UDP提高速度的原因。减少延迟,接受更多应答多播也是其中一种方法。在未来的因特网,多播涉及高速高品质跨网络数字电视数据传输。在网络游戏中,多播会给我们带来什么好处?简而言之,它不仅能降低游戏服务器的工作量,并且解决了在网络上无游戏管理者裁判状况下玩家彼此通信的老问题,假如DirectPlay进一步使用多播的话,那么我们也将有更多的理由使用它。
1、基于多播技术的想法(The Idea Behind Multicasting)
在一些理论学说中。它们通常以服务器至客户机作为开发模式,客户机发送数据到服务器,这一输入更新游戏状态并且服务器而后把由上述的信息发送到所有客户机上:
如果说,此刻服务器连接着32个玩家,然后同一样的信息将被发送32次(每个玩家)。如果被送到32个玩家的数据都为20字节,那么将有640字节将必须通过服务器的网络连接被发送出去。如果此时32个玩家又从事着各自不同的任务时,例如敲击键盘或移动鼠标,这些事件产生的数据量将非常庞大。当然,我们必须写出更好的代码处理欲发送的数据,多播可以很好的解决问题。
如何获得多播的帮助呢?这是很好的问题,多播可以显著地减少服务器向网络基层组织发送数据包任务的负担。在多播中,信息包能被送到网络组里的各个地址,而不是单个的地址。
这是一方法类似我们发电子邮件的工作 — 当我们想要把同一内容的电子邮件发送到多个邮件地址时,我们不是在自己的电脑上给每个地址都写一封单独的信。相反,我们会告诉服务器重复的将这信息发送到其它的邮件地址上。
2、不足之处(The Darker Side)
当然,有原因致使多播技术通常情况不被使用到:
一些因特网服务提供者及网络还不支持多播。倒霉。因此,如果你想要在一个游戏中实现多播,你可以增加多播的功能选项。网上很少看见支持多播的游戏,但是它在未来将希望。
只有在多于三个玩家以上的游戏中使用多播技术才是有价值的。
多播需求更多甚至连程序员都懒得浏览检验的代码。就像我们所知道的,实际上多播几乎不需要额外的代码。有限公司采用多播技术的高质量数字电视的主意看起来好像延缓了游戏程序员共同开发程序。我怀疑他应学习黑客论理学,源码开放(非营利)组织万岁。
3、多播工作原理(How Multicasting Works)
你也许听说过广播。广播把数据发送给在网络中的每个地址。不同广播是,多播仅仅发送给那些明确地登记的而且对数据的兴趣的地址。
在一个支持多播的IP网络上,这里存在一些象多播组的事物。如果你想要收到多播数据包,你就必须连接入一个多播组。虽然不考虑成员关系而发送数据包至一个多播组是可能的,但是在发送数据之前自身加入一个组是更好办法(由于某些我不敢冒险的原因)。如果你是你所发送多播数据包目的地的组中的成员,同样你也将收到一份你所发数据的拷贝。一个客户不会受到所有从多播组中发来的数据包,除非数据包被发至socket响应的端口。
因此明智的做法是将所有的游戏客户机连接到多播组,在同一端口上等待循环数据。然后服务器将单个数据包发送到多播组里,当信息包沿着路线被重复某地操作时,将发送到所有的客户机上。
4、连接多播组与接收多播数据包(Joining a Multicast Group and Receiving Multicast Data Packets)
为了收到发送至多播组的多播数据包,你的游戏需要加入或成为一个多播组的成员。请求成为一个多播组的成员比你想象的要简单的多。首先,你需要绑定你的UDP套接口至一个本地的端口。
SOCKADDR_IN addrLocal; // We want to use the Internet address family addrLocal.sin_family = AF_INET; // Use any local address addrLocal.sin_addr.s_addr = INADDR_ANY; // Use arbitrary port - but the same as on other clients/servers addrLocal.sin_port = htons(uiPort); // Bind socket to our address if(SOCKET_ERROR == bind(hUDPSocket, (LPSOCKADDR)&addrLocal, sizeof(struct sockaddr))) { cout << "Euston, we have a problem"; } // Ready to switch to multicasting mode |
int WSAAPI setsockopt(SOCKET s, int level, int optname, const char FAR * optval, int optlen); |
struct ip_mreq { struct in_addr imr_multiaddr; /* multicast group to join */ struct in_addr imr_interface; /* interface to join on */ } |
struct ip_mreq mreq; mreq.imr_multiaddr.s_addr = inet_addr("234.5.6.7"); mreq.imr_interface.s_addr = INADDR_ANY; nRet = setsockopt(hUDPSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP,(char*)&mreq, sizeof(mreq)); |
SOCKADDR_IN addrSrc; nRet = recvfrom(hUDPSocket, (char *)&Data, sizeof(Data), 0, (struct sockaddr*)&addrSrc, sizeof(addrSrc)); |
nRet = setsockopt(hUDPSocket, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char*)&mreq, sizeof(mreq)); |
char TTL = 32 ; // Restrict to our school network, for example setsockopt(hUDPSocket, IPPROTO_IP, IP_MULTICAST_TTL, (char *)&TTL, sizeof(TTL)); |
SOCKADDR_IN addrDest; szHi[50]; addrDest.sin_family = AF_INET; // Target multicast group address addrDest.sin_addr.s_addr = inet_addr("234.5.6.7"); // Port on which client is set to receive data packets addrDest.sin_port = htons(uiPort); // Something unoriginal to send strcpy(szHi,"Hello Multicast Group!"); nRet = sendto(hUDPSocket, (char *)szHi, sizeof(szHi), 0, (struct sockaddr*)&addrDest, sizeof(addrDest)); |
到此我们就可以加入多播组,从多播组发送和接收数据,但如何将它应用到我们的游戏中呢?接着看……
6、在游戏中使用多播(Uses of Multicasting in Games)
我能立刻想到两个用途。其一是减轻或避免服务器不得不重复发出的数据数量,另一个有趣的用途是提供一个用于查找网络中其他玩家的服务器无关通用接口。
比如:有两个玩家想在一个大型网络中一起玩游戏,但他们不知道各自的IP地址也不知道是否有其他玩家存在。通常连接两个玩家的方法是:
玩家发送一个广播消息给整个网络,然而这会造成巨大的网络流量同时有可能受到子网的限制。所以在互联网上进行广播是不允许的。
所有玩家连接到一个主服务器的IP地址,在这里他们就能知道其他玩家的存在。这样服务器的开销很大,同步也难以得到保证。
当玩家进入一个聊天房间希望找到其他玩家并一起游戏,这并不需要连接所有其他不在同一个房间的玩家,这个查找其他玩家的过程需要花费一些时间。
这里我们有一个涉及到一方和多播的另一方的老问题(A如何找到B)。多播组通常有一个确定的地址即服务器的地址,它必定100%在线,可以认为它在连接和发送方面不需要花费其他开销。游戏客户端简单的连接到多播组,多播一个要加入游戏的信息。服务端将它的可见性直接(不用多播以节省带宽)宣告给多播组中的其他客户端。
当然,这里有一个叫做itsy-bitsy的技术问题需要解决,但这个想法是够酷的。TTL控制允许我们查询路由范围,所以我们可以指定是发送给局域网或是校园网或是全国范围。是不是够酷了?
唯一问题(见“不足之处”那节)是ISP和网络对多播的支持。所以最好的方法是将多播功能作为游戏的一个选项(虽然我希望将来多播能得到100%的支持)。如何将多播作为一个选项与游戏结合起来呢?接着看。
7、让游戏与多播结合起来(Integrating Multicasting into Games)
那么,我们从何处开始呢?有很多不同类型的网络游戏,这里我不想过多解释如何将多播与各种类型的游戏如何结合,只是给出一些关于解决客户端和服务端关系的想法。
首先,最好保留你已经写的网络部分的代码,当你要加入多播支持时首先确认你确实需要否则不必改动你已写好的代码。
添加多播支持时,可以和你原先的代码写在一块或者做成和原先代码分开成两部分,然后加入一个开关选项供使用者选择,这样,当能支持多播时就可以开启多播功能,不支持多播时就可以关闭多播功能。
这就是平行结合方法,原有的普通网络代码可以照常工作,而多播代码只在多播得到支持时运行。那么我们如何检查多播是否得到支持呢?可以通过检查setsockopt()函数返回值来实现:
nRet = setsockopt(hUDPSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char*)&mreq, sizeof(mreq)); if(WSAESOCKTNOSUPPORT == nRet) { // Multicasting not supported. Damn. } |
struct Client { SOCKADDR_IN addrRemote; /* ... Game specific info here ...*/ BOOL bSupportMulticast; } |
int SendToAll(char *Data) { if(bServerSupportMulticast) { // First send multicast, then send individually // to those who don't support it SendMulticast(Data, addrMulticast); for(int index = 0; index < MAX_CLIENTS; index++) { if(Clients[index].Exist && !Clients[index].bSupportMulticast) { OldSendToClient(Data, Clients[index].addrRemote); } } } else { // Use the old method all the way regardless of support // as we ourselves don't support it for(int index = 0; index < MAX_CLIENTS; index++) { if(Clients[index].Exist) { OldSendToClient(Data, Clients[index].addrRemote); } } } } |