如何设计灵活的Erlang协议栈创建API

发布于 2024-09-29 21:32:04 字数 1521 浏览 8 评论 0原文

我对当前的方法不满意,只是尝试重新设计在 Erlang 中构建协议栈的方式。按重要性排序的功能:

  1. 性能

  2. 添加新协议变体的灵活性和实现速度

  3. 探索协议变体将有助于开发从 shell

我当前的模型 (这个问题已经描述了)除了通过函数调用发送()和通过消息接收的丑陋的不对称性之外,还达到了其极限。

整个协议引擎的整体情况如下所示:

底部:

  • 每个堆栈的底部有几个端口,或者有时可能还有一个 gen_tcp(独立通道有多个相同的堆栈,所以我们不能太这里的静态只是注册进程,必须在各处传递 Pids。

  • 在端口顶部是由主管管理的一些模块(从系统启动,并且在整个生命周期中没有错误)。

顶部:

  • 由事件发生触发(一般意义上不是 event_handler 意义上的)。是面向连接的协议端(例如,具有 connect()close() 语义。

  • 协议栈的顶端可能只能动态启动,因为堆叠在一起形成堆栈的模块是动态可配置的,并且可能会随着连接的不同而改变。

  • 当前计划将传递一个模块名称列表 + 来自顶层的可选参数,当在堆栈中调用 connect() 时,这些参数将被消耗。

  • 顶级进程将被链接,因此当此处出现任何问题时,整个连接都会失败。

模块类型以及它们之间的通信类型

目前为止发现了几种模块:

  • 无状态过滤模块

  • 有状态的模块,有些适合 gen_server,有些适合 gen_fsm,但大多数可能是简单的服务器循环,因为选择性接收将很有用,并且经常简化代码。

层与层之间的通信类型:

  • 独立发送和接收数据包(从外部看来是独立的)

  • 发送某些内容的同步调用,阻塞直到有答案,然后将结果作为返回值返回。

  • 与多个模块对话的多路复用器(这是我在此处的定义,以方便讨论)

  • 具有不同连接点的解复用器(名为 的解复用器与向上的

模块通信。目前,我唯一的解复用器位于堆栈的静态底部,而不是动态创建的顶部。多路复用器目前仅位于顶部。

在我之前链接的问题处理的答案和评论中,我听说 API 通常应该只包含函数而不是消息,除非另有说服力,否则我同意这一点。

请原谅对问题的冗长解释,但我认为它对于各种协议实现仍然具有普遍用途。

我将在答案中写下到目前为止我计划的内容,并且还将解释最终的实现以及我稍后的经验,以便在这里实现一些普遍有用的东西。

Unsatisfied with my current approach I'm just trying to redesign the way I build protocol stacks in Erlang. The feature ordered by importance:

  1. Performance

  2. Flexibility and implementation speed adding new protocol variants

  3. It would help development to explore the protocol variants from the shell

My current model (alreday described in this question) is getting at its limits besides the ugly asymmetry of send() by function call and receive by message.

The overall picture of the whole protocol engine looks like this:

Bottom part:

  • There a several ports or maybe also sometimes a gen_tcp at the bottom of each stack (there are multiple identical stacks for independent channels so we can't be too static here just registering processes, have to pass Pids everywhere.

  • On top of the ports are a few modules that are managed by supervisor (started with the system and in the absence of errors staying the whole lifetime).

Top part:

  • Triggered by event occurrence (in the general sense not in the event_handler sense) are connection oriented protocol ends (e.g. with connect() and close() semantics.

  • The top end of the protocol stack can probably only be started dynamically because the modules stacked on top of each other to form the stack are dynamically configurable and might change from connection to connection.

  • Currently planned would be passing a List of module names + optional params from the toplevel that get consumed while connect() is called down the stack.

  • Toplevel processes will be linked so when anything goes wrong here the whole connection fails.

Types of modules and types of communication between them

There are several kinds of modules found so far:

  • Stateless filter modules

  • Modules with state, some fit gen_server, some gen_fsm but most will probably be simple server loops since selective receive will be useful and simplify code quite often.

Types of communication between layers:

  • Independent sending and receiving of packets (independent as seen from the outside)

  • Synchronous calls that send something, block until there is a answer and then return the result as return value.

  • Multiplexers that talk down to multiple modules (thats my definition here to ease discussion)

  • Demultiplexers that have different attachment points (named by atoms currently) to talk with upward facing modules.

Currently my only demultiplexers are in the statical bottom part of the stack and not in the dynamically created top part. Multiplexers are only in the top part currently.

In the answers and comments of my linked previous question handling I heard that generally the API should only consist of functions and not messages and I concur with this unless convinced otherwise.

Please excuse the lengthy explanation of the problem but I think it is still of general use for all kinds of protocol implementation.

I will write what I have planned so far in the answers and also will also explain the resulting implementation and my experience with it later in order to achieve something generally useful here.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

空名 2024-10-06 21:32:04

我将添加到目前为止我计划的内容作为答案的一部分:

  • connect 传递了一个要堆栈的模块列表,在参数的情况下看起来像一个 proplist,例如:

    connect([module1, module2, {module3, [params3]}], param0, future_params)
    

    每一层都剥离头部并调用下一层连接。

  • connect()“以某种方式”向上和/或向下层传递有趣的引用

    • 发送异步发送到堆栈将由较低级别的连接返回
    • 用于异步接收堆栈的recv将作为参数传递给较低级别​​的连接
    • 调用同步发送并等待返回的回复 - 不确定如何处理这些,可能也从较低级别的连接返回
  • 多路复用器路由列表可能看起来像这些

    connect([模块1, 多路复用器, [[m_a_1, m_a_2, {m_a_3, [param_a_3]}], 
                                    [m_b_1,m_b_2],
                                    [{m_c_1, [param_c_1]}, m_c_2]], param0, 
                                                                    进一步的参数])。
    

目前我决定不会有用于同步调用的额外函数,我只是使用 send 来实现它。

在这种情况下,对于无状态模块,有一个该想法的实现示例:encode/1decode/1 对数据包进行一些 for 和 back 转换,例如解析二进制表示进入记录并返回:

connect(Chan, [Down|Rest], [], Recv_fun) ->
    {Down_module, Param} = case Down of
                               {F, P} -> {F, P};
                               F when is_atom (F) -> {F, []}
                           end,
    Send_fun = Down_module:connect(Chan, Rest, Param,
                                   fun(Packet) -> recv(Packet, Recv_fun) end),
    {ok, fun(Packet) -> send(Packet, Send_fun) end}.

send(Packet, Send_fun) ->
    Send_fun(encode(Packet)).

recv(Packet, Recv_fun) ->
    Recv_fun(decode(Packet)).

一旦我有了一个有状态的示例,我也会将其发布。

I'll throw in what I have planned so far as part of the answers:

  • connect gets passed a List of modules to stack, looking like a proplist in case of params e.g:

    connect([module1, module2, {module3, [params3]}], param0, further_params)
    

    each layer strips off the head and calls the next layers connect.

  • connect() "somehow" passes fun references up and/or down the layers

    • send for async sending down the stack will be returned by the lower level connect
    • recv for async receiving up the stack will be passed as param to the lower level connect
    • call for sync sending and waiting for an reply returned -- not sure how to handle these, probably also returned from the lower level connect
  • Multiplexers routing lists might look like these

    connect([module1, multiplexer, [[m_a_1, m_a_2, {m_a_3, [param_a_3]}], 
                                    [m_b_1, m_b_2],
                                    [{m_c_1, [param_c_1]}, m_c_2]], param0, 
                                                                    further_params]).
    

Currently I decided there will not be a extra function for synchronous calls, I'm just using send for it.

There is implementation example of the idea in this case for a module which is stateless: encode/1 and decode/1 do some for and back transformation on packets e.g. parse binary representation into a record and back:

connect(Chan, [Down|Rest], [], Recv_fun) ->
    {Down_module, Param} = case Down of
                               {F, P} -> {F, P};
                               F when is_atom (F) -> {F, []}
                           end,
    Send_fun = Down_module:connect(Chan, Rest, Param,
                                   fun(Packet) -> recv(Packet, Recv_fun) end),
    {ok, fun(Packet) -> send(Packet, Send_fun) end}.

send(Packet, Send_fun) ->
    Send_fun(encode(Packet)).

recv(Packet, Recv_fun) ->
    Recv_fun(decode(Packet)).

As soon as I have a stateful example I'll post it too.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文