快速网络库 2






4.79/5 (10投票s)
一款易于使用、快速的网络游戏库
引言
游戏开发是家庭开发者经常涉足的领域之一,这正是我正在做的事情。然而,尽管在 XNA 中设置一个简单的独立游戏相对容易,但要设置一个在线游戏却非常困难。原因是 XNA 仅支持 Windows Live 系统进行网络游戏。这是一个很好的系统,但前提是您正在为 XBox 开发并且自己也拥有一个 XBox。据我所知,在没有 XBox 的情况下很难获得 Windows Live 帐户,并且使用网络游戏类不如应该的那么容易。因此,出于这些原因,我决定创建自己的网络游戏库,以便能够制作出可靠、快速的多人游戏。
Using the Code
代码包含 4 个主要类。最简单的类是 GlobalMethods
,简而言之,它有两个 static
方法用于将对象转换为字节数组以及从字节数组转换回对象。请注意,如果对象是您定义的类,它必须具有 [Serializable]
属性。序列化器方法使用 BinaryFormatter
和 MemoryStream
将对象/字节数组进行转换。(BinaryFormatter
来自 System.Runtime.Serialization.Formatters.Binary
,MemoryStream
来自 System.IO
。)
其余 3 个类是比较有趣的。它们是
客户端
GameServer
ManagerServer
GameServer
是一个服务器,允许最大数量的连接到它,并允许客户端通过它将数据发送给其他玩家,或者仅发送给服务器本身(如果指定)。ManagerServer
也执行相同的功能,但它还内置了跟踪 GameServer
IP 地址的能力,然后将这些地址传递给请求它们的客户端。
Client
有两种模式:GameClient
和 ManagerClient
。当客户端连接到 GameServer
时,应使用 GameClient
;当客户端连接到 ManagerServer
时,应使用 ManagerClient
。这是因为用于在 Client
和两种类型的服务器之间快速高效地传输数据的基本消息对象类型是不同的。
我将通过逐行解释测试应用程序并说明不同方法调用的作用和它们运行的代码来解释这些类。
首先,控制台测试应用程序的开头定义了许多变量。它们是
static int GSMaxConnections = 2;
static int SMMaxConnections = 100;
static int GSPort = 56789;
static int MSPort = 56788;
static int LoopDelay = 500;
public static int BufferSize = 9999999;
static bool Terminate = false;
static List<IPAddress> ServerAddresses = null;
static ManualResetEvent AddressesReceivedEvent = new ManualResetEvent(false);
static Client MSClient2 = new Client(ClientModes.ManagerClient,
new Client.MessageReceivedDelegate(MSClient_OnMessageRecieved));
static ManagerServer TheMS = new ManagerServer(
new MaxConnectionsDelegate(TheMS_OnMaxConnections),
new ManagerServer.MessageReceivedDelegate(TheMS_OnMessageRecieved));
GSMaxConnections
定义了GameServer
(稍后创建)允许接受的最大连接数。SMMaxConnections
定义了ManagerServer
(稍后创建)允许接受的最大连接数。GSPort
是GameServer
将要监听的端口号。MSPort
是ManagerServer
将要监听的端口号。LoopDelay
稍后用于定义while
循环发送测试消息的速度。它以毫秒为单位设置。最小设置值应为10
(发送小消息时)或50
(发送大量数据时),以确保最大可靠性。如果您发现消息似乎丢失了,请降低发送消息的频率,您可能会发现消息能够成功发送。BufferSize
是底层套接字可用的发送和接收缓冲区允许的最大大小。此数字的最大值可以是 9999999,因为任何更大的值都会导致底层套接字抛出错误。Terminate
用于停止测试消息的while
循环(见下文)。ServerAddresses
用于存储从ManagerServer
返回的地址列表。AddressesRecievedEvent
用于等待地址被接收(见下文)。MSClient2
是一个ManagerServerClient
,用于获取服务器地址并将GameServer
从ManagerServer
地址列表中移除。TheMS
是ManagerServer
。它用于跟踪现有的GameServers
并显示它知道多少GameServers
。
所有主要代码都运行在 Main
中,它也是应用程序的入口点(符合 C# 标准的要求)。
Thread TerminateThread = new Thread(new ThreadStart(TerminateThreadRun));
TerminateThread.Start();
这最初的两行代码启动了一个后台线程,该线程等待用户按下 Escape 键,然后将 Terminate
设置为 true
。这允许用户在任何方便的时候退出。
Continue = TheMS.Start(MSPort, SMMaxConnections);
这调用了 ManagerServer
上的 Start
方法。此方法启动 ManagerServer
监听连接。它调用的代码是
if (!Initialised)
{
OK = Initialised = Initialise(Port);
}
if (OK)
{
MaxConnections = MaximumConnections;
TheListener.Start(MaxConnections);
TheListener.BeginAcceptSocket(AcceptSocketCallback, null);
}
这初始化了 ManagerServer
,它创建了一个新的 TcpListener
来监听指定端口上的 TCP 连接。它设置了 ManagerServer
允许接受的最大连接数,启动了 TcpListener
,最后调用 BeginAcceptSocket
,允许 ManagerServer
异步接受连接。
Main 中的下一行代码是
Client MSClient1 = new Client(ClientModes.ManagerClient,
new Client.MessageReceivedDelegate(MSClient_OnMessageRecieved));
MSClient1.OnErrorMessage += new ErrorMessageDelegate(MSClient_OnErrorMessage);
MSClient1.OnDisconnected += new Client.DisconnectDelegate(MSClient_OnDisconnected);
Continue = MSClient1.Connect(MSPort, Dns.GetHostAddresses(Dns.GetHostName()).First());
这创建了一个新的 Client
,并将其设置为 ManagerClient
模式,即它将连接到 ManagerServer
。如果模式与您尝试连接的服务器类型不匹配,连接仍会被接受,但发送到连接的大多数(如果不是全部)消息将在另一端未被处理。这里的关键代码行是 MsClient1
.Connect
,它尝试将客户端连接到运行 ManagerServer
的本地计算机上的 MSPort
(之前已定义)。它调用的代码如下所示:
TheSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
TheSocket.Connect(TheAddress, Port);
byte[] Buffer = new byte[BufferSize];
TheSocket.BeginReceive(Buffer, 0, Buffer.Length,
SocketFlags.None, ReceiveCallback, Buffer);
这创建了一个新的套接字,该套接字设置为使用 TCP 协议,并设置为流类型,这意味着数据将在连接的两端双向发送,方式与您使用流的方式相同。然后,它尝试将套接字连接到指定的 IP 地址和指定的套接字。代码之后不需要检查套接字是否已连接,因为如果套接字连接失败,会抛出一个错误,该错误会被 try
/catch
块捕获。最后,调用 BeginReceive
,允许客户端异步接收数据。
如果 MSClient
连接成功,它会发送一个 AddServer
请求,这是一个默认的 ManagerServerMessageType
,它会导致 ManagerServer
将收到此类型消息的客户端的 IP 地址添加到其服务器列表中。
接下来,代码设置了一个 GameServer
。理论上,上面的代码应该在游戏服务器设置完成后调用,但出于测试目的,这样设置更方便。游戏服务器设置代码如下所示:
GameServer.BufferSize = BufferSize;
GameServer TheGS = new GameServer(new MaxConnectionsDelegate(TheGS_OnMaxConnections),
new GameServer.MessageReceivedDelegate(TheGS_OnMessageRecieved));
TheGS.MessageHiding = false;
TheGS.OnError += new ErrorMessageDelegate(TheGS_OnError);
TheGS.OnClientDisconnect += new GameServer.ClientDisconnectDelegate(
TheGS_OnClientDisconnect);
请注意,BufferSize
是一个 static
变量,可以在代码的任何地方设置。但是,GameServer BufferSize
和 Client BufferSize
应该相同(只要发送到连接的数据不超过较小的缓冲区大小,就不会发生错误,因为缓冲区大小是最大允许大小),并且 ManagerServer BufferSize
必须与 Client BufferSize
相同。
if (TheGS.Start(GSPort, GSMaxConnections))
上面的代码行执行的操作与 TheMS
.Start
相同,只是它启动 GameServer
监听 GSPort
而不是 MSPort
(之前已定义)。
Client.BufferSize = BufferSize;
MSClient2.OnErrorMessage += new ErrorMessageDelegate(MSClient_OnErrorMessage);
MSClient2.OnDisconnected += new Client.DisconnectDelegate(MSClient_OnDisconnected);
if (MSClient2.Connect(MSPort, Dns.GetHostAddresses(Dns.GetHostName()).First()))
上面的代码与之前的 MSClient1
代码一样,只是请注意第一行现在定义了 Client BufferSize
。理想情况下,这应该更早定义,但在这种简单的测试应用程序中,它对程序的运行或结果没有影响。
MSClient2.MSSend(null, ManagerServerMessageType.GetServers, false);
AddressesReceivedEvent.WaitOne(1000);
上面的两行代码向 ManagerServer
发送一个请求以获取其服务器地址列表,然后等待它们被发送回来。它将最多等待 1000 毫秒(1 秒)的响应。此超时确保程序不会因任何问题而无限期等待。此时我们必须跳转到 MSClient_OnMessageReceived
。
ManagerServerMessageObject TheClass = (ManagerServerMessageObject)
(GlobalMethods.FromBytes(e.TheBytes));
if (TheClass != null)
{
if (TheClass.TheMessageType == ManagerServerMessageType.ServersResponse)
{
ServerAddresses = (List<IPAddress>)(GlobalMethods.FromBytes(TheClass.TheBytes));
Console.WriteLine("");
if (ServerAddresses != null)
{
Console.WriteLine("Server addresses received.
Addresses count : " + ServerAddresses.Count);
}
else
{
Console.WriteLine("Server addresses received. Addresses were null!");
}
AddressesReceivedEvent.Set();
}
}
上面的代码处理 MSClients
接收到的任何消息。它使用 GlobalFunctions
.FromBytes
获取 ManagerServerMessageObject
(这是底层消息对象),然后测试它是否为 null
。(**注意**:如果 GlobalFunctions
.FromBytes
遇到错误,它才会为 null
。)然后代码测试消息是否为 ServersResponse
,即数据是否为服务器 IP 地址列表。如果是,它会获取列表,设置 ServerAddresses
,然后释放正在等待 AddressesReceivedEvent
的线程,即主线程。
最后,代码设置了两个游戏 Clients
,并反复通过其中一个连接发送测试消息。
Client GSClient1 = new Client(ClientModes.GameClient,
new Client.MessageReceivedDelegate(GSClient_OnMessageRecieved));
GSClient1.OnErrorMessage += new ErrorMessageDelegate(GSClient_OnErrorMessage);
GSClient1.OnDisconnected += new Client.DisconnectDelegate(GSClient_OnDisconnected);
bool Connected = GSClient1.Connect(GSPort, ServerAddresses[0]);
Console.WriteLine("Attempted to connect game server client 1.
Connected : " + Connected.ToString());
Client GSClient2 = new Client(ClientModes.GameClient,
new Client.MessageReceivedDelegate(GSClient_OnMessageRecieved));
GSClient2.OnErrorMessage += new ErrorMessageDelegate(GSClient_OnErrorMessage);
GSClient2.OnDisconnected += new Client.DisconnectDelegate(GSClient_OnDisconnected);
Connected = GSClient2.Connect(GSPort, ServerAddresses[0]);
Console.WriteLine("Attempted to connect game server client 2.
Connected : " + Connected.ToString());
while (!Terminate)
{
DataClass NewDataClass = new DataClass
("Your random number is : ", new Random().Next());
GSClient1.GSSend(GlobalMethods.ToBytes(NewDataClass), false, false);
Thread.Sleep(LoopDelay);
}
代码最后执行的操作是调用两个 GameServer Clients
的断开连接,断开 MSClient2
(MSClient1
已断开连接),并停止 GameServer
和 ManagerServer
。这些是通过 Client
.Disconnect()
和 Server
.Stop()
完成的。
程序的输出如下所示(测试消息的数量取决于您何时决定退出!)

关注点
这是我第二次制作网络库,我从过去的错误中吸取了教训,也就是说,这个库更可靠、更快、更灵活!
历史
我添加了两个演示应用程序,它们一起运行以展示我希望类用于的网络结构。第一个应用程序是 Manager Server 控制台应用程序,它简单地显示了 ManagerServer
类的作用。第二个是客户端控制台应用程序,它演示了您将如何使用游戏。简而言之,程序加载,连接到管理器服务器,获取现有服务器列表,如果列表中没有服务器,它就成为服务器;如果列表中有一些 IP 地址,它会尝试依次连接到每个 IP 地址,直到建立连接。如果无法建立连接,应用程序将结束。如果游戏成为游戏服务器,它会通知管理器服务器,然后连接到它刚刚创建的游戏服务器,即它自己。如果程序现在连接到游戏服务器,它将允许用户将测试消息发送给游戏服务器的所有其他客户端。这两个程序都应该通过按 Escape 键来退出,因为这可以关闭所有连接并进行清理,而不会留下未关闭的连接的风险。下图显示了一个管理器服务器实例以及两个控制台游戏应用程序实例的输出。一个实例作为游戏服务器和游戏客户端运行,另一个仅作为游戏客户端运行。

新更新:我已更新代码,使其更可靠。我发现如果我连续发送消息,它们就会丢失。起初,我以为是数据包丢失了,但有几个来源向我保证这是不可能的。因此,我得出的结论是,由于数据包是连续发送的,TCP 可能会将它们分组到一个数据包中。因此,我已更新我的代码,使其能够在接收端分离这些消息。我通过在每条消息的前面加上一个六位数的数字来实现这一点,该数字表示消息的长度(字节)。这使得我的程序能够在另一端分离消息。代码更改与库的先前版本兼容,因此如果您已经开始使用它,请重新下载文件。
新更新:我已向 ManagerServer
、GameServer
和 Client
类添加了 OnConnected
事件。对于 ManagerServer
和 GameServer
,当客户端连接到它们时,会触发该事件。对于客户端,连接后会立即触发该事件。