“前言”
大家好,我叫 Glenn Fiedler,欢迎阅读我的“游戏程序员网络”系列文章的第二篇。
在上一篇文章中我们讨论了在不同计算机之间发送数据的方法,并决定使用用户数据报协议(UDP)代替传输控制协议(TCP)。我们使用用户数据报协议(UDP)是因为它能够使数据按时传送,而无需等待重传数据包并造成数据累积。
现在我将向您展示如何使用用户数据报协议(UDP)发送和接收数据包。
伯克利套接字(BSD 套接字)
对于大多数现代平台,您都可以找到基于 Berkeley 套接字构建的套接字。Berkeley 套接字通过几个简单的函数进行控制,这些函数称为“socket”、“bind”、“sendto”和“recvfrom”。您可以根据需要直接调用这些函数,但由于每个平台都有细微的差异,因此很难保持代码平台独立性。套接字示例代码用于说明其基本使用功能。我们不会大量直接使用 Berkeley 套接字。因此,当我们掌握了所有基本的套接字函数后网络游戏开发,我们将把所有内容总结成一系列课程,以便您轻松编写代码。
平台特异性
首先我们创建一个“定义”程序来测试我们拥有的平台,以便我们可以发现不同平台之间每个套接字的细微差别。
接下来我们为套接字编写适当的头文件。由于头文件是特定于平台的,我们将使用“#define”根据平台引用不同的文件。
如果socket是在unix平台上搭建的,我们不需要其他额外的连接,如果是在windows系统上搭建的,我们需要连接“winsock”库才能保证socket正常使用。
这是一个简单的技巧,可以在不改变现有项目或 makefile 的情况下完成上述操作。
因为我太懒了,所以我很喜欢这个小技巧~当然如果你每次都想链接项目或生成文件,那也是可以的。
套接字层的初始化
大多数“类 Unix”平台(包括 Mac OS X)不需要任何特殊步骤来初始化套接字层。但是,Windows 需要一些特殊设置来确保您的套接字代码正常工作。在使用任何其他套接字函数之前,您必须先调用“WSAStartup”来初始化它们,并且还必须使用“WSACleanup”在程序段结束时终止。
我们来添加上面两个新功能:
这样我们就有了一个初始化套接字层的方法。对于不需要套接字初始化的平台,可以忽略这些函数。
创建套接字
现在是时候创建一个基于用户数据报协议 (UDP) 的套接字了。操作方法如下:
接下来,我们将用户数据报协议 (UDP) 套接字映射到一个端口(例如,端口 30000)。每个套接字必须映射到一个唯一的端口。这样做的原因是端口号决定了每个数据报的端口号。数据包要发送到的位置。不要使用 1024 以下的端口,因为这些端口是为系统调用保留的。
有一种特殊情况,您可以输入“0”,系统会自动为您选择一个空闲的端口。
现在我们的套接字已准备好发送和接收数据包。
那么上面提到的“htons”的作用是什么呢?这就需要你随时在socket结构中直接设置整数数字了。
您将在本文中多次看到“htons”(主机到网络短字节)及其 32 位整数大小“参数”(主机到网络长字节)。下载时请注意它们,当您在文中再次遇到它们时,您就会明白。
将套接字设置为非阻塞模式
默认情况下,套接字设置为“阻塞模式”。这意味着如果您尝试使用“recvfrom”函数读取数据包,则在读取数据包之前不会返回函数值。返回的可供读取的数据包可能少于一个。这与我们的目标完全不一致。视频游戏是以每秒 30 或 60 帧的速度运行的实时程序,它们不能只是坐在那里等待数据包到达!
解决方案是在创建套接字之前将其切换为“非阻塞模式”。完成此操作后,“recvfrom”函数可以在没有数据包可读取时立即返回,并且返回值指示您应该稍后再次调用。尝试读取数据包。
以下是将套接字设置为非阻塞模式的方法:
从上面的程序我们可以发现Windows本身并没有提供“框架”功能,所以我们使用“ioctlsocket”函数来实现。
发送数据包
UDP 是一种无连接协议,因此每次发送数据包时都必须指定目标地址。您可以使用 UDP 数据包将数据包发送到任意数量的不同 IP 地址,并且没有任何计算机连接到您的用户数据报协议 (UDP) 套接字的另一端。
以下是如何将数据包发送到特定地址:
重要的一点!“sendto”的返回值仅表示数据包是否从本地计算机成功发送。它并不表示您是否从目标计算机收到了数据包!用户数据报协议 (UDP) 无法知道数据包是否可以到达目的地。
在上面的代码中,我们传递“sockaddr_in”结构作为目标地址。
那么我们怎样建立这些结构呢?
现在我们以发送到地址 207.45.186.98:30000 为例
我们从以下程序开始:
可以看到,我们首先将 [0255] 范围内的 A、B、C、D 值转换为单个整数,使得每个字节都有一个与输入值对应的整数值。然后我们使用整数地址和端口初始化一个“sockaddr_in”结构,从而确保使用“htonl”和“htons”将整数地址和端口值从主机字节序列转换为网络字节序列。
特殊情况:如果你要给自己发一个数据包,你不需要查询自己机器的 IP 地址,数据包会被发送到你本地的环回地址 127.0.0.1 中的机器。
接收数据包
将 UDP 套接字绑定到端口后,发送到套接字 IP 地址和端口的任何 UDP 数据包都将被放入队列中。循环中收到的数据包和不断调用“recvfrom”的指令意味着队列中没有其他数据包。由于用户数据报协议 (UDP) 是无连接的,因此数据包可以到达许多不同的计算机。每当您收到数据包时,“recvfrom”都会为您提供发送者的 IP 地址和端口,以便您知道数据包来自何处。
以下是循环并接收传入数据包的方法:
在队列中,大于您的接收缓冲区的数据包将被系统默默丢弃。因此,如果您有一个 256 字节的缓冲区用于接收数据包,而有人给您一个 300 字节的缓冲区来发送,您将不会收到 300 字节数据包的前 256 个字节。
由于您正在编写自己的游戏网络协议,所以这并不是什么大问题。
实际上,您需要确保接收缓冲区足够大,以接收代码可能发送的最大数据包。
清除套接字
在大多数 Unix 平台上,一旦您完成程序,只需使用标准文件“close”函数清理套接字文件即可。但是在 Windows 系统上,上述情况略有不同,我们必须使用“closesocket”函数来操作:
套接字类
现在我们已经完成了所有基本操作:创建套接字、将其绑定到端口并将其设置为非阻塞、发送和接收数据包以及清除套接字。
但你会发现这些操作或多或少都是平台相关的,每次要执行一个socket操作时,你都要记住“#ifdef”指令以及不同平台的各种细节,这些繁琐的操作让人抓狂。
为了解决这个问题,我们可以将所有套接字功能封装到一个“套接字类”中。在此过程中,我们将添加一个“地址类”,以便更轻松地指定 Internet 地址。这避免了每次发送或接收数据包时都需要我们手动编码或解码“sockaddr_in”结构。
下面是“socket类”的程序:
这是“地址类”的程序:
这些类接收和发送数据包的方式如下:
“综上所述”
我们现在有一种与平台无关的方式来发送和接收用户数据报协议(UDP)数据包。
用户数据报协议 (UDP) 是无连接的网络游戏开发,因此我编写了一个简单的示例程序,该程序从文本文件中读取 IP 地址并每秒向这些地址发送一个数据包。当它收到数据包时,它会告诉您这些数据包来自哪台机器以及收到的数据包的大小。
您可以非常轻松地进行设置,然后您将在本地计算机上拥有一组节点来相互发送数据包。这样,您可以使用以下程序通过不同的端口访问不同的应用程序:
> 节点30000
> 节点 30001
> 节点 30002
ETC...
然后每个节点将尝试向每个其他节点发送数据包,其工作原理就像一个小型的“对等”设置。
我是在 MacOSX 上开发这个程序的,但我认为你应该能够在任何 Unix 系统或 Windows 上轻松编译它。如果您有任何针对不同机器的兼容性补丁,欢迎您提供。请与我联系。