面向对象网络编程

16
我写了许多网络系统,并且对网络通信有很好的理解。但是,我总是会编写一个巨大的 switch 语句作为数据包接收函数。这让我感到烦恼。我更愿意采用优雅的面向对象的方式来处理数据包接收,但每次我尝试想出一个好的解决方案时,都无法满足要求。
例如,假设你有一个网络服务器。它只是等待响应。数据包到达并需要验证以及决定如何处理。目前我是通过在消息头中切换包ID,然后调用一系列函数来处理每种数据包类型。对于复杂的网络系统,这导致一个庞大的 switch 语句,我真的不喜欢这样处理。一种考虑过的方法是使用处理程序类的映射。然后,我可以将数据包传递给相关的类来处理传入的数据。我的问题是需要一种方法来“注册”每个数据包处理程序与映射关联。通常,这意味着我需要创建一个类的静态副本,然后在构造函数中将其注册到中央数据包处理程序。虽然这有效,但它似乎是一种不太优雅和琐碎的处理方式。
编辑:同样,最好有一个很好的系统既可以处理发送数据包类型,也可以处理接收数据包类型(通过不同的函数显然)。
有人能指点我更好的处理接收数据包的方法吗?链接和有用信息将不胜感激!
如果我没有很好地描述我的问题,也请谅解,因为我无法描述它也是我从未能够想出解决方案的原因。

你不需要处理程序类的静态副本。你可以在构造函数中传递实例。虽然没有太大的改进。:( - R. Martinho Fernandes
@belisarius:我也找不到。有文章的直接链接吗?我在citeseer或google学术上都没找到 :( - Goz
当你说“huge”时,我们在谈论什么?是几十还是几百?具体来说,它是否少于30? - Nim
@Nim:这取决于情况,但我最后写的一个大约有80个... - Goz
@Goz,我本来想建议使用基于Boost MPL的方法,但如果是80,那么这种方法就不适用了...表格或映射是你最好的选择。 - Nim
显示剩余4条评论
8个回答

5
关于处理数据包类型的方法:对我来说,使用map是最好的选择。然而,我会使用一个简单的数组(或向量)代替map。如果您顺序枚举数据包类型,则可以使访问时间恒定。
至于类结构,已经有一些库做了这项工作:可用的游戏网络协议定义语言和代码生成。例如,Google的Protocol Buffer似乎很有前途。它为协议描述中的每个消息生成了一个带有getter、setter、序列化和反序列化例程的存储类。协议描述语言看起来更加丰富。

1

在进行面向对象编程时,您会尝试将每个事物表示为一个对象,对吧?因此,您的协议消息也会变成对象;您可能会有一个基类YourProtocolMessageBase,它将封装任何消息的行为,并从中继承您的多态专用消息。然后,您只需要一种方法将每条消息(即每个YourProtocolMessageBase实例)转换为一串字节,以及一种反向操作的方法。这些方法称为序列化技术;还存在一些基于元编程的实现方法

以下是Python的快速示例:

from socket import *
sock = socket(AF_INET6, SOCK_STREAM)
sock.bind(("localhost", 1234))
rsock, addr = sock.accept()

服务器块,为客户端启动另一个实例:

from socket import *
clientsock = socket(AF_INET6, SOCK_STREAM)
clientsock.connect(("localhost", 1234))

现在使用Python内置的序列化模块pickle;客户端:
import pickle
obj = {1: "test", 2: 138, 3: ("foo", "bar")}
clientsock.send(pickle.dumps(obj))

服务器:

>>> import pickle
>>> r = pickle.loads(rsock.recv(1000))
>>> r
{1: 'test', 2: 138, 3: ('foo', 'bar')}

所以,正如你所看到的,我刚刚通过本地链接发送了一个 Python 对象。这不是面向对象编程吗?

我认为除了序列化之外,唯一可能的替代方案就是维护双向映射的 IDs ⇔ 类。这看起来真的是不可避免的。


1
作为我的网络安全和网络编程课程项目的一部分,我解决了这个问题并可以保证它不是一个巨大的数据包开关语句。该库名为跨平台网络,并且我将其建模于OSI模型的结构和如何将其输出为一个简单的对象序列化之上。该存储库在此处:https://bitbucket.org/ptroen/crossplatformnetwork/src/master/ 有无数的协议,例如NACK,HTTP,TCP,UDP,RTP,Multicast等,它们都通过C++元模板被调用。好的,那就是概括性的答案,现在让我深入一些,解释你如何解决这个问题以及为什么这个库可以帮助你,无论你是自己设计还是使用库。
首先,让我们谈谈一般的设计模式。要使它有良好的组织,你需要首先围绕它创建一些设计模式,作为解决问题的框架。对于我的C++模板,我最初围绕OSI模型(https://en.wikipedia.org/wiki/OSI_model#Layer_7:_Application_layer)来进行设计,到传输层(此时变成了套接字)。回顾一下OSI:应用层:对最终用户的意义。即序列化或反序列化信号,从网络堆栈向上或向下传递;表示:来自应用程序和网络堆栈的数据独立性;会话:会话之间的对话;传输:运输数据包。
但是当你仔细看这些时,它们并不像设计模式,而更像围绕从A到B的传输创建的命名空间。因此,为了最终用户,我使用以下标准化的C++元模板设计了跨平台网络:
template <class TFlyWeightServerIncoming,//表示服务端传入负载的类。注意,FlyWeight是一个设计模式,是类型的联合,即将事物组合在一起。这是你打包传入对象的地方 class TFlyWeightServerOutgoing,//表示服务器不同类型的传出负载的类 class TServerSession,//代表如何将负载转换为会话层翻译的钩子类。关键是保持关注点分离(https://en.wikipedia.org/wiki/Separation_of_concerns) class TInitializationParameters>//表示服务器初始化的类(例如端口等)
两个示例:https://bitbucket.org/ptroen/crossplatformnetwork/src/master/OSI/Transport/TCP/TCPTransport.h https://bitbucket.org/ptroen/crossplatformnetwork/src/master/OSI/Transport/HTTP/HTTPTransport.h 每个协议可以像这样被调用:
OSI::Transport::Interface::ITransportInitializationParameters init_parameters;

const size_t defaultTCPPort = 80;
init_parameters.ParseServerArgs(&(*argv), argc, defaultTCPPort, defaultTCPPort);
OSI::Transport::TCP::TCP_ServerTransport<SampleProtocol::IncomingPayload<OSI::Transport::Interface::ITransportInitializationParameters>, SampleProtocol::OutgoingPayload<OSI::Transport::Interface::ITransportInitializationParameters>, SampleProtocol::SampleProtocolServerSession<OSI::Transport::Interface::ITransportInitializationParameters>,  OSI::Transport::Interface::ITransportInitializationParameters> tcpTransport(init_parameters);
tcpTransport.RunServer();

引用: https://bitbucket.org/ptroen/crossplatformnetwork/src/master/OSI/Application/Stub/TCPServer/main.cc 我在MVC的代码库中还有一个完整的MVC实现,但让我们回到你的问题。你提到:
“目前,我一直通过在头文件中开启数据包ID,然后有一大堆函数调用来处理每个数据包类型。”
“对于复杂的网络系统,这会导致一个庞大的switch语句,我真的不喜欢以这种方式处理它。我考虑的一种方法是使用处理程序类的映射。然后我可以将数据包传递给相关的类并处理传入的数据。我对此的问题是我需要某种方式将每个数据包处理程序注册到该映射中。通常,这意味着我需要创建类的静态副本,然后在构造函数中将其注册到中央数据包处理程序。虽然这样可以工作,但它似乎真的是一种不优雅和麻烦的处理方式。”
在跨平台网络中,添加新类型的方法如下:
1.定义服务器类型后,您只需创建传入和传出类型即可。处理它们的实际机制嵌入在传入对象类型中。其中的方法是ToString(),FromString(),size()和max_size()。这些方法处理保持应用程序层以下层级安全的安全问题。但是,现在您正在定义对象处理程序,因此需要将其转换为不同的对象类型的翻译代码。您至少需要在此对象中: 1.应用程序层的枚举对象类型列表。这可以简单地对它们进行编号。但是对于会话层之类的内容,请查看会话层问题(例如RTP具有诸如抖动和如何处理不完美连接之类的内容。即会话问题)。现在,您还可以从枚举切换到哈希/映射,但这只是解决了查找变量的问题的另一种方式。 2.定义序列化和反序列化对象(针对传入和传出类型)。 3.在序列化或反序列化后,将逻辑放置到适当的内部设计模式中以处理应用程序层。这可能是一个生成器、命令或策略,这取决于它的用例。在跨平台上,一些关注点由TServerSession层委托,另一些关注点由传入和传出类处理。这取决于关注点的分离。 4.解决性能问题。即它不是阻塞的(当您扩展并发用户时,这变得更加重要)。 5.解决安全问题(渗透测试)。
如果您感到好奇,可以查看我的API实现。它是一个单线程异步boost反应堆实现,当与像mimalloc(用于覆盖new delete)这样的东西结合使用时,可以获得非常好的性能。我轻松地在单个线程上测量了50k的连接。

是的,这一切都与良好的设计模式、分离关注点以及选择一个良好的模型来表示服务器设计有关。我认为OSI模型是适合这个的,这就是为什么我加入了跨平台网络以提供卓越的面向对象的网络。


1

1
处理程序实例的映射是处理它的最佳方式。没有任何不优雅的地方。

1
但肯定非常麻烦 ;) - Goz
1
有一定量的琐碎工作,这种情况下只是问题在于你如何和在哪里分配它。 - Blrfl

1

根据我的经验,表驱动解析是最有效的方法。

虽然std::map很好用,但我最终使用静态表。 std::map无法作为常量表进行静态初始化。它必须在运行时加载。表(结构体数组)可以声明为数据并在编译时初始化。我还没有遇到过足够大以至于线性搜索成为瓶颈的表。通常表的大小足够小,二分查找的开销比线性搜索慢。

为了获得高性能,我会将消息数据用作表中的索引。


1

你想继续使用相同的数据包网络协议,但将其转换为编程中的对象,对吗?

有几种协议可以让您将数据视为编程对象,但似乎您不想改变协议,只是想改变在应用程序中处理它的方式。

数据包是否带有类似于“标签”或元数据或任何“ID”或“数据类型”的东西,允许您映射到特定的对象类?如果是这样,您可以创建一个存储ID和匹配类的数组,并生成一个对象。


是的,它确实有数据包ID。我无法想象在没有某种唯一标识符的情况下如何识别数据包类型... - Goz
如果您可以使用数据包ID来“分类数据包”,那么您可以执行以下操作:对象函数PacketToClass(字符串packetTypeTag){...},它是一种基于ID生成给定类的对象的函数,也称为“工厂”模式或软件。 - umlcat

1

更加面向对象的处理方式是使用状态模式构建状态机。

处理传入的原始数据是解析,其中状态机提供了一种优雅的解决方案(你将不得不在优雅和性能之间做出选择)

您有一个数据缓冲区需要处理,每个状态都有一个处理缓冲区方法,该方法解析和处理缓冲区的一部分(如果已经可能),并根据内容设置下一个状态。

如果您想追求性能,仍然可以使用状态机,但省略面向对象的部分。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接