65.9K
CodeProject 正在变化。 阅读更多。
Home

EpServerEngine.cs - 一个轻量级的异步 IOCP TCP 模板服务器-客户端框架,使用 C# 和 Socket

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (18投票s)

2014年10月26日

MIT

8分钟阅读

viewsIcon

46128

downloadIcon

1145

如何使用 IOCP TCP 模板服务器-客户端框架 EpServerEngine.cs (C#) 快速创建服务器-客户端网络。

目录

  1. 引言 
  2. 背景
    1. 可选
  3. 模板服务器-客户端框架的目的 
  4. 框架类
    1. 服务器端
    2. 客户端
    3. 左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。
  5. 结论
  6. 参考 

简介  

在用 C++ 编写了 EpServerEngine 框架后,我认为如果有一个 C# 的模板服务器-客户端框架,构建原型会更容易。我推荐使用它来快速构建原型或测试数据包通信。此外,可以将其作为构建自己的模板服务器-客户端框架的指南。

背景  

有关 "Socket" 的详细信息,请参考微软的 MSDN。微软提供了关于如何使用 System.Net.Sockets 创建客户端和服务器的简单指南。我强烈建议您使用 Microsoft Visual Studio 按照本教程进行操作。

可选  

  1. EpServerEngine.cs 中使用的实用工具库
  2. EpServerEngine.cs 中使用的原始创意

模板服务器-客户端框架的目的  

作为一名软件开发人员,许多人会同意一遍又一遍地编写相同的代码是一项非常枯燥的工作。当代码只是一堆棘手的初始化器时,情况会更糟。这个模板服务器-客户端框架将为您完成所有这些工作,您只需为服务器和客户端设计和实现您的数据包结构和数据包处理例程,其余工作(初始化/等待数据包/创建工作线程/网络互斥等)都交给框架完成。

框架类  

服务器端  

IocpTcpServer 类  

这是一个 IOCP TCP 服务器类,负责以下功能:

  • 启动服务器 
  • 停止服务器  
  • 设置使用的端口   
  • 将消息广播给所有已连接的客户端。  
  • 断开所有已连接的客户端(用于紧急情况)   

除了上述功能外,它内部还会创建一个监听线程,负责接受客户端连接,并为每个已连接的客户端创建一个套接字,以便与已连接的客户端进行通信。

我需要做什么才能创建自己的服务器? 

这非常简单。

using EpServerEngine.cs;

// MyServer.cs

public class MyServer
{
   ...
   INetworkServer m_server = new IocpTcpServer();
   ...
};
  1. 创建您的服务器类(例如:MyServer)  
  2. 创建一个新的 IocpTcpServer 对象的成员变量。
  3. 完成!   
如何启动/停止服务器 

您可以非常轻松地像下面一样启动/停止您的服务器。

using EpServerEngine.cs;

// MyServer.cs

public class MyServer
{
   ...
   INetworkServer m_server = new IocpTcpServeR();
   ... 
   void StartServer(string portString)
   {
      ServerOps ops = new ServerOps(callbackObj, portString);
      m_server.StartServer(ops);
   }
   ...  
   void StopServer()
   {
      if(m_server.IsServerStarted()) // check if server is started 
      {  
         m_server.StopServer();  // Disconnect all clients connected and stop listening 
      }
   }  
   ...
};       
什么是 ServerOps 类和 callbackObj?  

IocpTcpServer 需要一个 ServerOps 对象来启动服务器。

ServerOps 接受两个参数:服务器的回调对象(继承自 INetworkServerCallback)和服务器监听的端口号字符串。

IocpTcpServer 将在三种情况下调用回调方法并提供信息。

  • OnServerStarted [ void OnServerStarted(INetworkServer server, StartStatus status) ]
    • 当服务器成功启动或启动失败时,将调用 OnServerStarted
    •  您可以通过 StartStatus 检查服务器是否成功启动,如果失败,则尝试重新启动服务器。
  • OnAccept [ INetworkSocketCallback OnAccept(INetworkServer server, IPInfo ipInfo) ]
    • OnAccept 包含两个参数:server 指的是接受客户端的服务器实例,用于后续的回调;ipInfo 指的是尝试连接的客户端的 IP 信息。
    • 您可以检查 ipInfo 并通过返回 null 来**拒绝**连接,或者返回已连接到此客户端的套接字的回调对象来接受连接(稍后将详细解释)。
  • OnServerStopped [ void OnServerStopped(INetworkServer server) ]
    • 当给定服务器完全停止时,将调用 OnServerStopped

因此,您可以将服务器的回调对象设为自己的服务器类,例如 MyServer.cs

using EpServerEngine.cs;

// MyServer.cs

public class MyServer: INetworkServerCallback
{
   ...
   INetworkServer m_server = new IocpTcpServer();
   ...
   void StartServer(string portString)
   {
      ServerOps ops=new ServerOps(this, portString);
      m_server.StartServer(ops);
   }
   ...
   public void OnServerStarted(INetworkServer server, StartStatus status)
   {
      if(status==StartStatus.SUCCESS || status == StartStatus.FAIL_ALREADY_STARTED)
      {
         // Server is started
      }
      else
      {
         // Server failed to start
      }
   }

   public INetworkSocketCallback OnAccept(INetworkServer server, IPInfo ipInfo)
   {
      // reject client with ip 10.231.44.124, otherwise accept
      if (ipInfo.GetIPAddress().Equals("10.231.44.124"))
      {
         return null;
      }
      return new ServerSocketCallback(); // Socket callback object explained later
   }
       
   public void OnServerStopped(INetworkServer server)
   {
      // Server is stopped
   }
   ... 
}

或者,您也可以创建一个单独的类,并将该类的实例传递给 ServerOps,如下所示:

using EpServerEngine.cs;

// ServerCallbackObj.cs

public class ServerCallbackObj: INetworkServerCallback
{
   ...
   public void OnServerStarted(INetworkServer server, StartStatus status)
   {
      if(status==StartStatus.SUCCESS || status == StartStatus.FAIL_ALREADY_STARTED)
      {
         // Server is started
      }
      else
      {
         // Server failed to start
      }
   }

   public INetworkSocketCallback OnAccept(INetworkServer server, IPInfo ipInfo)
   {
      // reject client with ip 10.231.44.124, otherwise accept
      if (ipInfo.GetIPAddress().Equals("10.231.44.124"))
      {
         return null;
      }
      return new ServerSocketCallback(); // Socket callback object explained later
   }
       
   public void OnServerStopped(INetworkServer server)
   {
      // Server is stopped
   }
   ... 
};

// MyServer.cs

public class MyServer{

   ...
   void StartServer(string portString)
   {
      INetworkServerCallback callbackObj = new ServerCallbackObj();
      ServerOps ops = new ServerOps(callbackObj, portString);
      m_server.StartServer(ops);
   }
   ...
}  

有关更多详细信息,请参阅 EpServerEngine.cs 的 Wiki 页面:此处

IocpTcpSocket 类  

当客户端被接受并提供套接字回调对象时,IocpTcpServer 实例会自动创建套接字。此 IocpTcpSocket 类负责以下功能:

  • 向客户端发送/接收消息  
  • 断开客户端连接

您还可以通过套接字检查连接状态或客户端的 IP 信息。

如何创建这个套接字回调类? 

这和创建服务器一样简单。

using EpServerEngine.cs;

// MyServerSocketCallback.cs

class MyServerSocketCallback: INetworkSocketCallback
{
   void OnNewConnection(INetworkSocket socket)
   {
      // the client is connected on given socket
   }

   void OnReceived(INetworkSocket socket, Packet receivedPacket)
   {
      // the data is received from client with given socket within receivedPacket
      byte[] receivedPacketBytes=receivedPacket.GetPacket();
   }

   void OnSent(INetworkSocket socket, SendStatus status)
   {
      // the data is sent to client with given status
      if(status==SendStatus.SUCCESS)
      {
         // succesfully sent
      }
      else
      {
         // failed to send data to client
      }
   }
   
   void OnDisconnect(INetworkSocket socket)
   {
      // client with given socket is disconnected
   }

};

  1. 创建您的套接字回调类(例如:MyServerSocketCallback)  
  2. 使您的类继承 INetworkSocketCallback  
  3.  实现四个回调函数(OnNewConnection, OnReceived, OnSent, OnDisconnect)。
  4. 完成! 
这些四个回调是什么?  

IocpTcpSocket 将在四种情况下调用回调方法并提供信息。

  • OnNewConnection [ void OnNewConnection(INetworkSocket socket) ]
    • 当客户端被服务器接受时,将调用 OnNewConnection
  • OnReceived [ void OnReceived(INetworkSocket socket, Packet receivedPacket) ]
    • OnReceived 包含两个参数:socket 指的是用于后续回调的连接到客户端的套接字实例;receivedPacket 指的是包含客户端发送的数据的 Packet 类。
  • OnSent [ void OnSent(INetworkSocket socket, SendStatus status) ]
    • 当数据成功发送到客户端或发送失败时,将调用 OnSent
    • 您可以通过 SendStatus 检查数据是否成功发送,如果失败,则尝试重新发送数据。
  • OnDisconnect [ void OnDisconnect(INetworkSocket socket) ]
    • 当客户端与服务器断开连接时,将调用 OnDisconnect
如何使用这个服务器套接字回调类? 

在继承 INetworkServerCallback 的服务器回调类中,在 "OnAccept" 调用时返回此套接字回调类的实例。

那么我如何向客户端发送数据?  

使用 INetworkSocket 实例,您可以像下面一样简单地将数据发送到客户端:

...
// socket is an instance of INetworkSocket
String helloString="hello";
byte[] packet = new byte[helloString.Length*sizeof(char)];
System.Buffer.BlockCopy(helloString.ToCharArray(), 0, packet, 0, packet.Length);
socket.Send(new Packet(packet, packet.Count(), false));
...

或者,您可以使用 INetworkServer 实例将数据广播给所有已连接的客户端,如下所示:

...
// socket is an instance of INetworkSocket
String helloString="hello";
byte[] packet = new byte[helloString.Length*sizeof(char)];
System.Buffer.BlockCopy(helloString.ToCharArray(), 0, packet, 0, packet.Length);
server.Broadcast(new Packet(packet, packet.Count(), false));
...

您可以使用 Stream Serializer 将数据转换为字节数组 (byte[]),然后将其包含在 Packet 类中,再通过 Send/Broadcast 方法发送数据包。

有关更多详细信息,请参阅 EpServerEngine.cs 的 Wiki 页面:此处

客户端  

IocpTcpClient 类  

这是一个 IOCP TCP 客户端类,负责以下功能:

  • 连接到服务器
  • 从服务器断开连接 
  • 向服务器发送/接收消息。 

我需要做什么才能创建自己的客户端?   

using EpServerEngine.cs;

// MyClient.cs

public class MyClient
{
   ...
   INetworkClient m_client = new IocpTcpClient();
   ...
};
  1. 创建您的客户端类(例如:MyClient) 
  2. 创建一个新的 IocpTcpClient 对象的成员变量。
  3. 完成!   
如何连接/断开与服务器的连接  

您可以像下面一样非常轻松地连接/断开与服务器的连接:

using EpServerEngine.cs;

// MyClient.cs

public class MyClient
{
   ...
   INetworkClient m_client = new IocpTcpClient();
   ...
   void ConnectToServer(String hostname, String port)
   {
      ClientOps ops= new ClientOps(callbackObj, hostname,port);
      m_client.Connect(ops);
   }

   void DisconnectFromServer()
   {
      if(m_client.IsConnectionAlive()) // check if connected to server
      { 
         m_client.Disconnect();        // disconnect from the server
      }
   }
   ... 
};   
什么是 ClientOps 类和 callbackObj?  

IocpTcpClient 需要一个 ClientOps 对象来连接服务器,这与 ServerOps 类似。

ClientOps 接受三个参数:客户端的回调对象(继承自 INetworkClientCallback)、主机名字符串以及服务器连接的端口号字符串。

IocpTcpClient 将在四种情况下调用回调方法并提供信息。

  • OnConnected [ void OnConnected(INetworkClient client, ConnectStatus status) ]
    • OnConnected 当客户端成功连接到服务器或连接失败时,将调用此方法。
    •  您可以通过 ConnectStatus 检查客户端是否成功连接到服务器,如果失败,则尝试重新连接服务器。
  • OnReceived [ void OnReceived(INetworkClient client, Packet receivedPacket) ]
    • OnReceived 包含两个参数:client 指的是用于后续回调的连接到服务器的客户端实例;receivedPacket 指的是包含服务器发送的数据的 Packet 类。
  • OnSent [ void OnSent(INetworkClient client, SendStatus status) ]
    • 当数据成功发送到服务器或发送失败时,将调用 OnSent
    • 您可以通过 SendStatus 检查数据是否成功发送,如果失败,则尝试重新发送数据。
  • OnDisconnect [ void OnDisconnect(INetworkClient client) ]
    • 当客户端与服务器断开连接时,将调用 OnDisconnect

因此,您可以将客户端的回调对象设为自己的客户端类,例如 MyClient.cs

using EpServerEngine.cs;

// MyClient.cs

public class MyClient: INetworkClientCallback
{
   ...
   INetworkClient m_client = new IocpTcpClient();
   ...
   void ConnectToServer(string hostname, string portString)
   {
      ClientOps ops=new ClientOps(this, hostname, portString);
      m_server.Connect(ops);
   }
   ...
   public void OnConnected(INetworkClient client, ConnectStatus status)
   {
      if(status==ConnectStatus.SUCCESS || status == ConnectStatus.FAIL_ALREADY_CONNECTED)
      {
         // Client is connected to the server
      }
      else
      {
         // failed to connect
      }
   }
 
  void OnReceived(INetworkClient client, Packet receivedPacket)
   {
      // the data is received from server within receivedPacket
      byte[] receivedPacketBytes=receivedPacket.GetPacket();
   }

   void OnSent(INetworkClient client, SendStatus status)
   {
      // the data is sent to server with given status
      if(status==SendStatus.SUCCESS)
      {
         // succesfully sent
      }
      else
      {
         // failed to send data to the server
      }
   }
   
   void OnDisconnect(INetworkClient client)
   {
      // client is disconnected from the server
   }
   ... 
}
或者,您也可以创建单独的类,并将该类的实例传递给 ClientOps 类,如下所示:
using EpServerEngine.cs;

// ClientCallbackObj.cs

public class ClientCallbackObj: INetworkClientCallback
{
   ...
   public void OnConnected(INetworkClient client, ConnectStatus status)
   {
      if(status==ConnectStatus.SUCCESS || status == ConnectStatus.FAIL_ALREADY_CONNECTED)
      {
         // Client is connected to the server
      }
      else
      {
         // failed to connect
      }
   }
   
   void OnReceived(INetworkClient client, Packet receivedPacket)
   {
      // the data is received from server within receivedPacket
      byte[] receivedPacketBytes=receivedPacket.GetPacket();
   }

   void OnSent(INetworkClient client, SendStatus status)
   {
      // the data is sent to server with given status
      if(status==SendStatus.SUCCESS)
      {
         // succesfully sent
      }
      else
      {
         // failed to send data to the server
      }
   }
   
   void OnDisconnect(INetworkClient client)
   {
      // client is disconnected from the server
   }
   ... 
};

// MyClient.cs

public class MyClient{

   ...
   void ConnectToServer(string hostname, string portString)
   {
      INetworkClientCallback callbackObj = new ClientCallbackObj();
      ClientOps ops = new ClientOps(callbackObj, hostname, portString);
      m_client.Connect(ops);
   }
   ...
}  
那么我如何向服务器发送数据?  

使用 INetworkClient 实例,您可以像下面一样简单地将数据发送到服务器:

...
// Whether client is INetworkClient or IocpTcpClient
String helloString="hello";
byte[] packet = new byte[helloString.Length*sizeof(char)];
System.Buffer.BlockCopy(helloString.ToCharArray(), 0, packet, 0, packet.Length);
client.Send(new Packet(packet, packet.Count(), false));
...

您可以使用 Stream Serializer 将数据转换为字节数组 (byte[]),然后将其包含在 Packet 类中,再通过 Send 方法发送。

有关更多详细信息,请参阅 EpServerEngine.cs 的 Wiki 页面:此处

通用  

PacketSerializer 类  

这是一个数据包序列化类,负责以下功能:

  • 将可序列化类的实例序列化为字节数组。
  • 将字节数组反序列化为给定类。
// Serializable Class example

[Serializable]
public class SomeData: ISerializable
{
   public int packetCode;
   public long data;
   public SomeData()
   {}
   public SomeData(int iPacketcode, long iData)
   {
      packetCode= iPacketcode;
      data= iData;
   }
   public void GetObjectData(SerializationInfo info, StreamingContext context)
   {
      info.AddValue("packetCode", packetCode, typeof(int));
      info.AddValue("data", data,typeof(long));
   }

   public SomeData(SerializationInfo info, StreamingContext context)
   {
      packetCode = (int)info.GetValue("packetCode", typeof(int));
      data = (long)info.GetValue("data",typeof(long));
   }
};

...

// Class Serializing example
PacketSerializer<SomeData> serializer =new PacketSerializer<SomeData>(new SomeData(1,10));
byte[] byetarr=serializer.GetPacketRaw();
...

// Class Deserialize example
void OnReceived(INetworkSocket socekt, Packet receivedPacket)
{
   PacketSerializer<SomeData> serializer = new PacketSerializer<SomeData>(receivedPacket.GetPacket(), 0, receivedPacketByteSize());
   SomeData someData = serializer.GetPacket();
   Debug.Log(someData.packetCode+" " + someData.data);
}

有关更多详细信息,请参阅 EpServerEngine.cs 的 Wiki 页面:此处。 

Packet 类  

 这是一个数据包类,负责以下功能:

  • EpServerEngine.cs 用于服务器和客户端之间通信的基本数据单元。 
如何使用这个数据包类? 
  • PacketSerializer
  • MemoryStream
  • 等等。

例如,如果您定义了上述数据包结构,您可以像下面一样创建 Packet 对象将其发送到服务器/客户端:

// PacketSerializer
PacketSerializer<SomeData> serializer = new PacketSerializer<SomeData>(new SomeData(14));
byte[] packet = serializer.GetPacketRaw();
Packet sendPacket= new Packet(packet             // byte array of packet
                              , packet.Count()   // byte size of the data
                              , false);          // flag whether to allocate the memory
                                                 //   for the data within Packet Object or not.

////////////////////////////
// OR
////////////////////////////

// MemoryStream
byte[] packet = new byte[12];
MemoryStream stream = new MemoryStream(packet);
stream.Write(BitConverter.GetBytes((int)PacketCode.SOME_CODE), 0, 4);
stream.Write(BitConverter.GetBytes((long)14),0,8);

Packet sendPacket = new Packet(packet, packet.Count(), false);

  • 请注意,由于在服务器开发中,数据包内存复制对于性能至关重要,您可以根据情况选择是为 Packet 对象分配内存,还是仅在 Packet 对象中保留对数据字节数组的引用。  

有关更多详细信息,请参阅 EpServerEngine.cs 的 Wiki 页面:此处。 

结论  

正如我在引言中所说,这可能不是最好的框架。但是,由于使用此框架可以非常快速地构建服务器和客户端,我认为它对于开发原型服务器/客户端应用程序或测试数据包通信非常有帮助,因为它让您可以忽略所有棘手的初始化和 IOCP 套接字及线程的处理,从而专注于服务器/客户端通信设计。希望这对您的服务器/客户端开发有所帮助。 

参考  

历史  

  • 2015.12.06: - 文章小更新
  • 2014.11.11: - 标题更改。
  • 2014.10.11: - 修正了拼写错误。
  • 2014.10.27: - 更新了源代码。
  • 2014.10.25: - 提交了文章。  
© . All rights reserved.