EpServerEngine.cs - 一个轻量级的异步 IOCP TCP 模板服务器-客户端框架,使用 C# 和 Socket
如何使用 IOCP TCP 模板服务器-客户端框架 EpServerEngine.cs (C#) 快速创建服务器-客户端网络。
目录
简介
在用 C++ 编写了 EpServerEngine 框架后,我认为如果有一个 C# 的模板服务器-客户端框架,构建原型会更容易。我推荐使用它来快速构建原型或测试数据包通信。此外,可以将其作为构建自己的模板服务器-客户端框架的指南。
背景
有关 "Socket" 的详细信息,请参考微软的 MSDN。微软提供了关于如何使用 System.Net.Sockets 创建客户端和服务器的简单指南。我强烈建议您使用 Microsoft Visual Studio 按照本教程进行操作。
可选
- EpServerEngine.cs 中使用的实用工具库
- EpServerEngine.cs 中使用的原始创意
- "EpServerEngine - 使用 C++ 和 Windows Winsock 构建的轻量级模板服务器-客户端框架" 作者:Woong Gyu La
模板服务器-客户端框架的目的
作为一名软件开发人员,许多人会同意一遍又一遍地编写相同的代码是一项非常枯燥的工作。当代码只是一堆棘手的初始化器时,情况会更糟。这个模板服务器-客户端框架将为您完成所有这些工作,您只需为服务器和客户端设计和实现您的数据包结构和数据包处理例程,其余工作(初始化/等待数据包/创建工作线程/网络互斥等)都交给框架完成。
框架类
服务器端
IocpTcpServer 类
这是一个 IOCP TCP 服务器类,负责以下功能:
- 启动服务器
- 停止服务器
- 设置使用的端口
- 将消息广播给所有已连接的客户端。
- 断开所有已连接的客户端(用于紧急情况)
除了上述功能外,它内部还会创建一个监听线程,负责接受客户端连接,并为每个已连接的客户端创建一个套接字,以便与已连接的客户端进行通信。
我需要做什么才能创建自己的服务器?
这非常简单。
using EpServerEngine.cs;
// MyServer.cs
public class MyServer
{
...
INetworkServer m_server = new IocpTcpServer();
...
};
- 创建您的服务器类(例如:
MyServer
) - 创建一个新的
IocpTcpServer
对象的成员变量。 - 完成!
如何启动/停止服务器
您可以非常轻松地像下面一样启动/停止您的服务器。
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
[ voidOnServerStarted
(INetworkServer
server,StartStatus
status) ]- 当服务器成功启动或启动失败时,将调用
OnServerStarted
。 - 您可以通过
StartStatus
检查服务器是否成功启动,如果失败,则尝试重新启动服务器。
- 当服务器成功启动或启动失败时,将调用
OnAccept
[INetworkSocketCallback OnAccept
(INetworkServer
server,IPInfo
ipInfo) ]OnAccept
包含两个参数:server 指的是接受客户端的服务器实例,用于后续的回调;ipInfo 指的是尝试连接的客户端的 IP 信息。- 您可以检查 ipInfo 并通过返回
null
来**拒绝**连接,或者返回已连接到此客户端的套接字的回调对象来接受连接(稍后将详细解释)。
OnServerStopped
[ voidOnServerStopped
(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
}
};
- 创建您的套接字回调类(例如:
MyServerSocketCallback
) - 使您的类继承
INetworkSocketCallback
- 实现四个回调函数(OnNewConnection, OnReceived, OnSent, OnDisconnect)。
- 完成!
这些四个回调是什么?
IocpTcpSocket
将在四种情况下调用回调方法并提供信息。
OnNewConnection
[ voidOnNewConnection
(INetworkSocket
socket) ]- 当客户端被服务器接受时,将调用
OnNewConnection
。
- 当客户端被服务器接受时,将调用
OnReceived
[ void OnReceived(INetworkSocket
socket,Packet
receivedPacket) ]OnReceived
包含两个参数:socket 指的是用于后续回调的连接到客户端的套接字实例;receivedPacket 指的是包含客户端发送的数据的 Packet 类。
OnSent
[ voidOnSent
(INetworkSocket
socket,SendStatus
status) ]- 当数据成功发送到客户端或发送失败时,将调用
OnSent
。 - 您可以通过
SendStatus
检查数据是否成功发送,如果失败,则尝试重新发送数据。
- 当数据成功发送到客户端或发送失败时,将调用
OnDisconnect
[ voidOnDisconnect
(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();
...
};
- 创建您的客户端类(例如:
MyClient
) - 创建一个新的
IocpTcpClient
对象的成员变量。 - 完成!
如何连接/断开与服务器的连接
您可以像下面一样非常轻松地连接/断开与服务器的连接:
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
[ voidOnReceived
(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 套接字及线程的处理,从而专注于服务器/客户端通信设计。希望这对您的服务器/客户端开发有所帮助。
参考
- EpLibrary.cs
- EpServerEngine.cs
- EpServerEngine
- MSDN 上的 Socket
- Woong Gyu La 的 "EpServerEngine - 使用 C++ 和 Windows Winsock 构建的轻量级模板服务器-客户端框架"
- EpServerEngine.cs 的 Wiki 页面
历史
- 2015.12.06: - 文章小更新
- 2014.11.11: - 标题更改。
- 2014.10.11: - 修正了拼写错误。
- 2014.10.27: - 更新了源代码。
- 2014.10.25: - 提交了文章。