高级WinSock多人游戏编程技术

来源:互联网 发布:天下3魍魉捏脸数据男 编辑:程序博客网 时间:2024/05/21 10:59

高级WinSock多人游戏编程技术


转自:NPC6 作者:Southdy
  
  原 文:Advanced WinSock Multiplayer Game Programming: Multicasting
  译 者:Southdy(GWeekly)、Mays
  版 本:the first edition(Ver 1.0 )
  
  --------------------------------------------------------------------
  
  多人网络游戏中如何避免迟延是一个比较重要的话题。作为多人网络游戏开发者,我们总是努力使事情做得更快,减少迟延以获得更多的带宽。这也是我们为什么通常会抛弃TCP的稳定性而使用UDP提高速度的原因。减少延迟,接受更多应答多播也是其中一种方法。在未来的因特网,多播涉及高速高品质跨网络数字电视数据传输。在网络游戏中,多播会给我们带来什么好处?简而言之,它不仅能降低游戏服务器的工作量,并且解决了在网络上无游戏管理者裁判状况下玩家彼此通信的老问题,假如DirectPlay进一步使用多播的话,那么我们也将有更多的理由使用它。
  
  Combatting lag is a major problem in multiplayer network game development. As multiplayer game developers, we always strive to make things faster, leaner and meaner to reduce lag and free up bandwidth. This is why we often forsake the reliability of TCP for the speed that UDP provides. Multicasting is yet another step in the fight against latency, carrying many promises, including the transmission of very high quality streaming digital TV over Networks and in the future, the Internet. What is the magic behind multicasting and how can it be used in our games? In short, it can not only reduce server workload but is also a solution to the age old problem of players fining each other on networks without the game developer having to put up dedicated master servers (but more on that later).Oh, and if DirectPlay uses multicasting extensively, then it’s all more the reason for us to use it :).
  
  1、基于多播技术的想法(The Idea Behind Multicasting)
  
  在一些理论学说中。它们通常以服务器至客户机作为开发模式,客户机发送数据到服务器,这一输入更新游戏状态并且服务器而后把由上述的信息发送到所有客户机上:
  
  The theory goes something like this. In the most commonly used networking client-server model, when a client sends input to the server, this input updates the game state and then the server tells all the other clients about what has happened by sending the same information to all the clients:
  
  图1
  
  



  
  正如你所看到,在服务器的网络连接上存在一个流量问题。如果说,此刻服务器连接着32个玩家,然后同一样的信息将被发送32次(每个玩家)。如果被送到32个玩家的数据都为20字节,那么将有640字节将必须通过服务器的网络连接被发送出去。如果此时32个玩家又从事着各自不同的任务时,例如敲击键盘或移动鼠标,这些事件产生的数据量将非常庞大。当然,我们必须写出更好的代码处理欲发送的数据,多播可以很好的解决问题。
  
  As you can see there is a traffic problem on the server’s Network connection. If, say, there were 32 players connected to the server at the time, then the same information would be sent 32 times (once to each player). If there were 20 bytes of data to be sent to each of the 32 players then 640 bytes would have to be sent through the server’s Network connection. If that were to happen every time any of the 32 players pressed a key or moved the mouse, a huge amount of traffic is generated. Naturally, there is no replacement for good code practice and sending only the data that is needed, but multicasting can seriously help.
  
  如何获得多播的帮助呢?这是很好的问题,多播可以显著地减少服务器向网络基层组织发送数据包任务的负担。在多播中,信息包能被送到网络组里的各个地址,而不是单个的地址。
  
  So how can multicasting help? Well, Multicasting can dramatically reduce the amount of data that needs to be sent by taking the task of packet replication away from the game server to the actual Network infrastructure. In multicasting, packets can be sent to groups of Network addresses, instead of individual addresses.
  
  这是一方法类似我们发电子邮件的工作 — 当我们想要把同一内容的电子邮件发送到多个邮件地址时,我们不是在自己的电脑上给每个地址都写一封单独的信。相反,我们会告诉服务器重复的将这信息发送到其它的邮件地址上。
  
  This is a similar to the way email works - when we want to send the same email message to multiple email addresses, we don’t send the message to every address from our computer. Instead we send the message once, telling the server to replicate the message to all the other addresses.
  
  图2
  
  



  
  2、不足之处(The Darker Side)
  
  当然,有原因致使多播技术通常情况不被使用到:
  
  Of course, there are reasons why multicasting is not commonly used:
  
  一些因特网服务提供者及网络还不支持多播。倒霉。因此,如果你想要在一个游戏中实现多播,你可以增加多播的功能选项。网上很少看见支持多播的游戏,但是它在未来将希望。
  
  Some ISPs and networks don’t support multicasting yet. Bastards. So if you want to implement multicasting in a game, you’re better off adding it as an option. Internet multicasting is rarely supported, but hopefully it will be in the future.
  
  只有在多于三个玩家以上的游戏中使用多播技术才是有价值的。
  
  Multicasting only makes a worthwhile gain in performance when network data is replicated, r ealistically only worth bothering when there is support for more than about three players.
  
  多播需求更多甚至连程序员都懒得浏览检验的代码。就像我们所知道的,实际上多播几乎不需要额外的代码。有限公司采用多播技术的高质量数字电视的主意看起来好像延缓了游戏程序员共同开发程序。我怀疑他应学习黑客论理学,源码开放(非营利)组织万岁。
  
  Multicasting requires some more coding and programmers are lazy to even look into it. As you will see, in fact it requires very little additional code. The corporate "Quality Digital TV via Multicasting" idea seems to put game programmers off the subject altogether, I suspect it has something to do with hacker ethics, so long live the .org’s!
  
  3、多播工作原理(How Multicasting Works)
  
  你也许听说过广播。广播把数据发送给在网络中的每个地址。不同广播是,多播仅仅发送给那些明确地登记的而且对数据的兴趣的地址。
  
  You may have heard of broadcasting. Broadcasting forwards data to every address on the network. Unlike broadcasting, multicasting only forwards to those addresses who have explicitly registered interest in the data.
  
  在一个支持多播的IP网络上,这里存在一些象多播组的事物。如果你想要收到多播数据包,你就必须连接入一个多播组。虽然不考虑成员关系而发送数据包至一个多播组是可能的,但是在发送数据之前自身加入一个组是更好办法(由于某些我不敢冒险的原因)。如果你是你所发送多播数据包目的地的组中的成员,同样你也将收到一份你所发数据的拷贝。一个客户不会受到所有从多播组中发来的数据包,除非数据包被发至socket响应的端口。
  
  On an IP Network supporting multicasting there are such things as multicast groups. If you want to receive multicast data packets, you must join a multicast group. Although it is should be possible to send data packets to a multicast group regardless of membership, it is often better to join a group before sending to it for reasons I won’t venture into. If you are a member of a group to which you are sending multicast datapackets, you will receive a copy of the data packets. Also, a client will not receive all data packets from a multicast group, but only those which are sent to the port that the socket is bound to.
  
  因此明智的做法是将所有的游戏客户机连接到多播组,在同一端口上等待循环数据。然后服务器将单个数据包发送到多播组里,当信息包沿着路线被重复某地操作时,将发送到所有的客户机上。
  
  So a sensible idea would be for all the game clients to join a multicast group and wait for data on the same port. Then the server, by sending a single packet of data to that multicast group, would be sending to all the clients as the packets are replicated somewhere along the way.
  
  图3
  
  



  
  我们现在已经了解了这项技术的优点和缺点,现在让我们来看看代码该如何写。
    
  We’ve seen the light, we’ve seen the darkness, so let us onto the code...
  
  4、连接多播组与接收多播数据包(Joining a Multicast Group and Receiving Multicast Data Packets)
    
  为了收到发送至多播组的多播数据包,你的游戏需要加入或成为一个多播组的成员。请求成为一个多播组的成员比你想象的要简单的多。首先,你需要绑定你的UDP套接口至一个本地的端口。
    
  To receive multicast packets sent to a multicast group, your game will need to join or become a member of that multicast group. To request becoming a member of a multicast group is a lot simpler than you may at first imagine. You need to first bind() your UDP socket to a local port (elementary, my dear friend):
  
    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
  
    然后调用setsockopt(),该函数的原型如下:
    And then just make a call to setsockopt(), and here’s a prototype for your convenience grin:
  
    int WSAAPI setsockopt(SOCKET s, int level, int optname,
               const char FAR * optval, int optlen);
  
  这个函数调用的参数:s为socket句柄、level设置成IPPROTO_IP、optname设置成IP_ADD_MEMBERSHIP、optval是一个指向p_mreq结构体的指针、optlen为长度。其中p_mreq结构体看起来如下:
  
  If you thought you were getting away with just 1 new line of code to learn, you were wrong... you’re only getting away with 4 new lines =). There are special parameters to prepare for this call: s is your socket handle, level should be set to IPPROTO_IP, optname should be set to IP_ADD_MEMBERSHIP and a pointer to the p_mreq structure passed as optval, with its length in optlen. This is what the p_mreq structure looks like:
  
    struct ip_mreq {
      struct in_addr imr_multiaddr;  /* multicast group to join */
      struct in_addr imr_interface;  /* interface to join on */
    }
  
  它有两个域,两者都为in_add r结构体:imr_multiaddr 指定跟进多点传送组的地址。而imr_interface指定局部地址INADDR_ANY。
    
  It has 2 fields, both of them are in_add r structures: imr_multiaddr specifies the address of the multicast group to join and imr_interface specifies the local address INADDR_ANY.
  
  专门(Class‘D’)为多播组分配地址。范围是从224.0.1.0到239.255.255.255。你可以从目标多播组范围内选择一个作为跟进地址,并且将其放到imr_multiaddr中。整个setsockopt的()调用看起来象这样:
  
  There are special (Class ’D’) addresses allocated for multicast groups. These are in the range from 224.0.1.0 to 239.255.255.255. You can choose an address from the range as the target multicast group to join, and set the imr_multiaddr to this address. The full setsockopt() call would look something like this:
  
    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));
  
  为了的简洁,我去掉了不少的错误检测代码。这套接口现在将在调用recvfrom()的特殊端口收到派送至多播组的数据包。
  
  And that’s all there is to it, apart from a lot of error checking which I’ve decided to leave out for clarity (aka Laziness). The socket will now receive data packets sent to the multicast group on the specified port with calls to recvfrom():
  
    SOCKADDR_IN addrSrc;
    nRet = recvfrom(hUDPSocket, (char *)&Data, sizeof(Data), 0,
            (struct sockaddr*)&addrSrc, sizeof(addrSrc));
  
  当你和该组结束关系并想离开这个组,只需重复调用使用除IP_ADD_MEMBERSHIP(已被IP_DROP_MEMBERSHIP替换)以外的同类参数。
  
  When you’re finished with the group and want to leave, just repeat the call with identical parameters apart from IP_ADD_MEMBERSHIP which should be replaced with IP_DROP_MEMBERSHIP.
  
    nRet = setsockopt(hUDPSocket, IPPROTO_IP, IP_DROP_MEMBERSHIP,
             (char*)&mreq, sizeof(mreq));
  
  既然我们可以加入一个多播组并且收到发送给他的数据包,逻辑上做这件事就是学习如何发送包至单个多播组。
  
  Now that we can join a multicast group and receive packets sent to it, the logical thing to do is to learn how to send packets to a multicast group.
  
  5、发送多播数据包(Sending Multicast Data Packets)
    
  通过调用sendto()函数来实现发送多播数据包,需要指定一个多播组地址作为目标IP地址还有你指定的端口号。这里我们先来了解一下关于IP多播的Winsock选项。(译者:有三个IP多播独有的选项:IP_MULTICAST_TTL,IP_MULTICAST_IF,IP_MULTICAST_LOOP)这里要说的是IP_MULTICAST_TTL选项,它用于设置多播数据的生存时间,默认情况下,TTL的值为1。也就是说多播数据遇到第一个路由器便会被丢弃。假如增大TTL的值,多播数据就可以经历多个路由器传到其他网络。TTL的值是多少,最多就能经过多少个路由器,每过一个路由器,TTL的值就会减一。下面那张表说明了具体情况。
    
  Sending multicast data packets is accomplished with a call to sendto(), specifying a multicast group address as the destination IP address and the wanted port (on which your clients are tuned to listen for data). So there really a lot to learn apart from using the TTL (Time To Live) socket option.
    
  All IP packets carry a TTL value to make sure that they are discarded if they don’t reach a destination so they don’t clog up the Network. In a multicast data packet, TTL specifies how far a multicast data packet can travel:
  
  TTL阈值(Threshold) Description
  TTL equal to 0 Restricted to the same host
  TTL equal to 1 Restricted to the same subnet
  TTL equal to 32 Restricted to the same site
  TTL equal to 64 Restricted to the same region
  TTL equal to 128 Restricted to the same continent
  TTL equal to 255 Unrestricted in scope
  [ From MSDN ]
  
  Multicasting is nowhere as dangerous as broadcasting in terms of unwanted traffic that it can produce but caution is advised when using some of the higher TTL values.
    
  To set a socket’s multicast TTL value, setsockopt() can be used with IPPROTO_IP as the protocol level and IP_MULTICAST_TTL as the socket option.
  
    char TTL = 32 ; // Restrict to our school network, for example
    setsockopt(hUDPSocket, IPPROTO_IP, IP_MULTICAST_TTL,
          (char *)&TTL, sizeof(TTL));
  
    设置好选项了就可以调用sendto()函数发送数据包了。
    Once the TTL is set, just sendto() away:
  
    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));
  
  到此我们就可以加入多播组,从多播组发送和接收数据,但如何将它应用到我们的游戏中呢?接着看……
    
  We can now join multicast groups, send and receive data from them, but how do we implement multicasting as an option in our game and what would we use it for?

6、在游戏中使用多播(Uses of Multicasting in Games)
  
  我能立刻想到两个用途。其一是减轻或避免服务器不得不重复发出的数据数量,另一个有趣的用途是提供一个用于查找网络中其他玩家的服务器无关通用接口。
    
  I can think of two ways straight away - one is to use it for reducing (maybe even eliminating) the amount of repeated data that a server has to send out, but another interesting use is a global server-less interface for finding other players on the Network.
  
  比如:有两个玩家想在一个大型网络中一起玩游戏,但他们不知道各自的IP地址也不知道是否有其他玩家存在。通常连接两个玩家的方法是:
    
  The scenario: there are 2 people on a large Network running the same game that want to play together, but they don’t know each other’s IP addresses let alone the fact that the potential opponent exists. The common ways for connecting the 2 players:
  
  玩家发送一个广播消息给整个网络,然而这会造成巨大的网络流量同时有可能受到子网的限制。所以在互联网上进行广播是不允许的。
  
  The players send out a broadcast message to the entire Network, however this would create huge traffic and will probably be restricted to sub Networks. Broadcasting on the Internet would create an enormous amount of traffic, so it is not allowed.
  
  所有玩家连接到一个主服务器的IP地址,在这里他们就能知道其他玩家的存在。这样服务器的开销很大,同步也难以得到保证。
  
  The players connect to an intermediate, "known" master server IP, which tells them of each other’s existence. These servers are costly to run and their uptime is often undependable.
  
  当玩家进入一个聊天房间希望找到其他玩家并一起游戏,这并不需要连接所有其他不在同一个房间的玩家,这个查找其他玩家的过程需要花费一些时间。
  
  The players go to a chatroom hoping to find other players and play together. This will not connect all the players as some may be in different chatrooms. And the process of finding someone may take a while.
    
  这里我们有一个涉及到一方和多播的另一方的老问题(A如何找到B)。多播组通常有一个确定的地址即服务器的地址,它必定100%在线,可以认为它在连接和发送方面不需要花费其他开销。游戏客户端简单的连接到多播组,多播一个要加入游戏的信息。服务端将它的可见性直接(不用多播以节省带宽)宣告给多播组中的其他客户端。
    
  So here we are with an age old problem (how A finds B) on one hand and multicasting on the other. Multicasting groups always have the same address - a "known" address as in the case of a dedicated server, they are online 100% of the time - unlimited uptime, they don’t cost anything to connect to or send information across. All game clients simply connects to a multicast group, multicast an "I want to play" message and the servers can then advertise their availability directly (instead of multicasting, to save bandwidth) to the clients who are members of the multicast group.
  
  当然,这里有一个叫做itsy-bitsy的技术问题需要解决,但这个想法是够酷的。TTL控制允许我们查询路由范围,所以我们可以指定是发送给局域网或是校园网或是全国范围。是不是够酷了?
    
  Sure, there are itsy-bitsy technical problems to sort out, but the idea is cool enough. And the TTL control allows us to query within a certain range of routers (see TTL table) so we can specifically only ask to send to our LAN, or a university network, or all servers within our country to respond. Don’t you think that is COOL? I sure do.
  
  唯一问题(见“不足之处”那节)是ISP和网络对多播的支持。所以最好的方法是将多播功能作为游戏的一个选项(虽然我希望将来多播能得到100%的支持)。如何将多播作为一个选项与游戏结合起来呢?接着看。
    
  The only problem (see "The Dark Side") remains multicast support by ISP’s and Networks. So the best way to add multicasting to a game is still as an option (although I hope and pray that multicasting will be 100% supported in the future). But how do we integrate multicasting into our game as an option?
  
  7、让游戏与多播结合起来(Integrating Multicasting into Games)
    
  那么,我们从何处开始呢?有很多不同类型的网络游戏,这里我不想过多解释如何将多播与各种类型的游戏如何结合,只是给出一些关于解决客户端和服务端关系的想法。
    
  Ok then, where do we start? There are so many different types of multiplayer games that I won’t even try to explain how to integrate multicasting into different types of games. Instead I’ll just give a few possible ideas of solutions in a client-server relationship.
  
  首先,最好保留你已经写的网络部分的代码,当你要加入多播支持时首先确认你确实需要否则不必改动你已写好的代码。
    
  First of all, all the current network code should be kept as it is, when you add multicast support make sure you do not remove any existing code unless you really think it is necessary.
  
  添加多播支持时,可以和你原先的代码写在一块或者做成和原先代码分开成两部分,然后加入一个开关选项供使用者选择,这样,当能支持多播时就可以开启多播功能,不支持多播时就可以关闭多播功能。
    
  When adding multicast support, you can either do a parallel integration where multicasting runs along with existing code, or you could write two separate sets of network code and add a ’multicast on/off switch’ for the user. The on/off switch would isolate servers using the other network code and add one more daunting and mysterious switch for the average newbie to get wrong. Parallel integration (bah, the things I learned in school last year) is my favorite as it will use multicasting only if it is supported and should be transparent to the user.
  
  这就是平行结合方法,原有的普通网络代码可以照常工作,而多播代码只在多播得到支持时运行。那么我们如何检查多播是否得到支持呢?可以通过检查setsockopt()函数返回值来实现:
    
  So let’s stick with parallel integration - in this case the normal network code runs always, but the multicasting code only runs if multicasting is supported. How do we determine if multicasting is supported? Just read the error setsockopt() gives us when trying to join a group:
  
    nRet = setsockopt(hUDPSocket, IPPROTO_IP, IP_ADD_MEMBERSHIP,
             (char*)&mreq, sizeof(mreq));
    if(WSAESOCKTNOSUPPORT == nRet)
    {
      // Multicasting not supported. Damn.
    }
  
  客户端和服务端的关系是一个游戏的两个部分。那么当服务端支持多播而客户端不支持多播该怎么办呢?服务端如何知道哪个客户端支持多播而哪个客户端不支持多播呢?客户端首先检查自己是否支持多播然后连接到服务端并告知服务端,服务端通常有一个客户端的列表,我们可以在列表中加入一个布尔变量来表示客户端是否支持多播:
    
  The client-server relationship is a game of two halves. So what if the server supports multicasting while the client does not? How does the server know which clients are covered with a single send to the multicast group and which are not? The client first determines that it does not support multicasting, then connects to the server and tells the server whether it supports multicasting. The server usually keeps a list or array of clients, to which it is easy to add an extra boolean flag:
  
    struct Client
    {
      SOCKADDR_IN addrRemote;
      /* ... Game specific info here ...*/
      BOOL bSupportMulticast;
    }
  
  于是服务端可以发送多播数据给支持多播的客户端,发送普通的UDP数据报给那些不支持多播的客户端。然而,当服务端也不支持多播时我们只能使用以前的老方法了。这里有一个将多播作为服务端选项的代码片段:
    
  The server’s function to send data sends a multicast to clients who support multicasting and normal UDP datagrams to those that do not. If, however, the server itself does not support multicasting then we must use the old method. Here’s a useful code snippet for a server with multicasting as an option:
  
    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);
          }
        }
      }
    }
  
  我希望我能为多播技术在游戏中的应用贡献出一份力量。如果你对这篇文章感到有一点兴趣或发现文章中的问题,请发信给我,说“我希望能很快写出下一篇文章”。现在,愿你愉快地使用多播。

原创粉丝点击