查看原文
其他

游戏网络开发(二):发送和接收数据包

2016-10-14 翻译:杨嘉鑫 Gad-腾讯游戏开发者平台


序言

大家好,我是Glenn Fiedler,欢迎阅读《》系列教程的第二篇文章。

在前面的文章中我们讨论了在不同计算机之间发送数据的方法,并决定使用用户数据报协议(UDP)而非传输控制协议(TCP)。我们之所以使用用户数据报协议(UDP),是因为它能够使数据在不等待重发包而造成数据聚集的情况下按时被送达。

现在我将要告诉各位如何使用用户数据报协议(UDP)发送和接收数据包。


伯克利套接字 (BSD socket)

对于大多数现代的平台来说你都可以找到建立在伯克利套接字上的sockets。伯克利套接字主要通过“socket”,“bind”, “sendto” and “recvfrom”几个简单函数进行控制。如果你愿意的话你当然可以直接对这几个函数进行调用,但是由于每个平台之间有细微差别,保持代码平台的独立性将会变得有些困难。因此,尽管我将先给各位介绍伯克利套接字的示例代码用以说明它的基本使用功能,我们也不会大量的直接使用伯克利套接字。所以当我们掌握了所有基础socket 功能后,我们将会把所有内容汇总到一个系列的课中,以便你可以轻松地编写代码。


平台的特殊性

首先 我们先建立一个“define”程序用来测试我们现有的平台是什么,这样我们就可以发现不同平台间间各个socket里的细微差别。


接下来我们为sockets写入适当的标头,由于头文件具有平台的特殊性所以我们将使用“#define”来根据不同的平台引用不同的文件。


如果sockets是建立在unix平台上,我们就不需要任何其他多余的连接,若它是建立在windows系统里,为了确保socket正常使用我们就需要连接到“winsock”库内。

以下是一个简单的技巧,它可以在不改变已有项目或生成文件的前提下完成上述工作。


我之所以非常喜欢这个小技巧是因为我太懒了~当然啦,如果你愿意每次都进行项目链接或生成文件也未尝不可。


socket层的初始化

大多数“unix-like”的平台 (包括macosx) 是不需要任何特殊的步骤去初始化socket层的。但是Windows需要进行一些特殊设置来确保你的sockets代码正常工作。在你使用其他任何sockets功能前你必须先调用 “WSAStartup” 来初始化它们,在你的程序段结束时你也必须使用 “WSACleanup”来结束。

下面让我们来添加以上两个新功能:


这样我们就得到了一个初始化socket层的方法。对于那些不需要socket初始化的平台来说这些功能可以忽略不计。


建立一个socket

现在是时候来建立一个基于用户数据报协议(UDP)的socket了,下面是实施的方法:


接下来我们把用户数据报协议(UDP)的socket对应到一个端口上(比如30000这个端口)。每一个socket都必须对应到一个独一无二的端口上。这么做的原因是端口号决定了每个数据包发送到的位置。不要使用1024以下的端口,因为这是为系统调用所预留的。

有一种特殊情况,如可以输入“0”,这样系统将会自动为你选择一个闲置的端口。

这样我们的socket已经准备就绪并可以发送和接收包了。

那么上面提到的“htons”是起什么作用这就要求你在任何时候都直接在socket结构里设置整数数字。

你会看到“htons”(主机到网络短字节)及其32位整数大小的 “参数”(主机到网络长字节)这在这篇文章中被多次使用,你留意了以后你在下文中再次遇到就会明白。


将socket设置为非阻塞形式

默认情况下,socket是被设置在 “阻塞模式”的状态下。这意味着,如果你想使用“recvfrom”功能读一个包,在一个数据包被读取前该函数值将不能被返回。你可能将不到一包可阅读回归。这与我们的目标完全不符。视频游戏是拟态在30或60帧每秒实时的程序,他们不能只是坐在那里等待数据包的到达!

解决方案是你将socket转换成以“非阻塞模式”后再创建他们。一旦做到这一点,当没有包可供阅读时,“recvfrom”函数就可以立即返回,返回值显示你应该稍后再尝试读取包。

下面是如何将socket设置为非阻塞模式的方法:

从上面的程序我们可以发现,Windows本身并不提供框架的功能,所以我们使用“ioctlsocket”功能来实现。


发送数据包

用户数据报协议(UDP是一种无连接协议,所以每次你发送一个数据包前都要指定一个目的地址。你可以使用一个用户数据报协议(UDP发送数据包到任意数量的不同的IP地址,而在你用户数据报协议(UDP socket的另一端并没有连接某一台计算机。

下面是如何发送一个数据包到一个特定的地址方法:

很重要的一点!“sendto”的返回值只是表明数据包是否被成功地从本地计算机发送,它并不能表明你是否接收到目标计算机的数据包!用户数据报协议(UDP)没有办法知道数据包是否能到达目的地。            

上面的代码中,我们通过“sockaddr_in”结构为目的地址。

那么我们如何设置这些结构呢?

现在让我们以发送到207.45.186.98:30000 这个地址为例

我们从以下这个程序开始:

正如您所看到的,我们首先将ABCD值在范围内的值[ 0255 ]转化为一个单一的整数,从而使每个字节的整数值对应输入值。然后以整数地址和端口来初始化一个“sockaddr_in”结构,这样就确保使用“htonl” “htons”来将整型地址和端口值从主机字节序列转换为为网络字节序列。

一种特殊情况:如果你想给自己发送一个数据包,不需要查询自己机器的IP地址,在回送地址127.0.0.1中数据包就将被发送到你的本地机器。


接收数据包

一旦你将一个用户数据报协议(UDP)套接字绑定到一个端口,任何发送到您scoket IP地址和端口的用户数据报协议(UDP)数据包都将放在一个队列里。在程序仅仅接收循环指令的数据包和一直调用“recvfrom”的指令时,就意味着队列里有没有留下其他的数据包了。由于用户数据报协议(UDP)是无连接性的,数据包可以到达许多不同的计算机。每当你收到一个数据包,“recvfrom”都会给你发送者的IP地址和端口以便你知道这是来自哪里的数据包。

下面是如何进行循环指令和接收传入的数据包的方法:


在队列中,数据包一旦大于你接收缓冲区的范围,他们都会被系统悄悄舍弃。因此,如果你有一个256字节的缓冲区用来接收数据包,有人给你一个发送300字节的数据包,300字节的数据包都将被删除。您将不会接收到300字节数据包的前256个字节。

因为您正在编写自己的游戏网络协议,以上这些操作这是没有什么影响的。

在实践中您就要确保您的接收缓冲区足够大,以接收最大的数据包,这样您的代码才可能会发送。


清除一个socket

在大多数Unix平台,一旦你完成了自己所需的程序后,在socket文件中只要使用标准的文件“close”函数来清理即可。然而,在Windows系统中以上情形会有点不同,我们要用“closesocket”函数来操作:


Socket class

现在,我们已经完成了所有的基本操作:创建一个socket,将他绑定到一个端口并设置为非阻塞,发送和接收数据包,清除socket。

但是你会发现以上这些操作中多多少少都是依赖于平台的,在每一次你想执行socket操作时,你不得不记住“# ifdef”指令和针对不同平台的各种细节,这些繁琐的操作是很令人抓狂的。

为了解决这个问题,我们可以将所有的socket功能封装成一个“socket class‘’。当我们在使用它的时候,我们将添加一个“Address class‘’,这样使它更容易指定互联网地址。这避免了我们每次发送或接收数据包时进行手动编码或解码“sockaddr_in”结构。

下面是“socket class‘’的程序:


下面是“address class”的程序:


下面是这些class如何接收和发送数据包的程序:



结论


我们现在有了一种不限平台的方法来发送和接收用户数据报协议(UDP)的数据包。

用户数据报协议(UDP)是无连接性的,因此我编写了一个简单的示例程序,它可以从文本文件中读取IP地址,并能够每秒向这些地址发送一个数据包。每当这个程序接收到一个数据包时,它就会告诉你它们来自哪个机器,以及接收到的数据包的大小。

您可以很容易地设置它,然后你就拥有了一系列在本地机器上互相发送数据包的节点。这样你就可以利用以下程序通过不同的端口,进入不同的应用程序:

   > Node30000

   > Node 30001

   > Node 30002

   etc...

然后每个节点都将尝试发送数据包到每个其他节点,它的工作原理就像一个小型的“peer-to-peer”设置。

我是在MacOSX系统中开发的这个程序,但我想你应该能够轻松地在任何Unix系统或Windows上对他进行编译。如果你有任何应用在其他不同机器上的兼容性补丁,也非常欢迎您与我取得联系。

【版权声明】

原文作者未做权利声明,视为共享知识产权进入公共领域,自动获得授权。


点击一下立即阅读相关好文章


《贪婪》谈贪婪设计腾讯GAD游戏创新大赛


游戏美术3D设计干货回顾为VR优化UE4渲染器


这么做设计才好玩Unity教程


MOBA类游戏核心设计分析

......




近期热文

游戏网络开发(一):UDP vs. TCP

Unity3D Native 插件开发: IL2CPP 机制篇


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存