【问题标题】:how to design a server for variable size messages如何为可变大小的消息设计服务器
【发布时间】:2015-06-23 20:21:59
【问题描述】:

我想要一些关于如何设计处理可变大小消息的服务器的反馈或建议。

为了简化答案,我们假设:

  • 基于单线程 epoll()
  • 协议为:data-size + data
  • 数据存储在环形缓冲区中
  • 读取的代码,为了清晰起见做了一些简化,如下所示:

    如果(客户端->可读){ if (client->remaining > 0) { /* 为清晰而简化 - 假设我们总是能够读取 1+ 个字节 */ rd = read(client->sock, client->buffer, client->remaining); 客户端->缓冲区+= rd; 客户->剩余-= rd; } 别的 { /* 为清晰而简化 - 假设我们总是能够读取 4 个字节 */ 读取(客户端->袜子,&(客户端->剩余),4); 客户端->缓冲区=acquire_ringbuf_slot(客户端->剩余); } }

    请不要专注于 4 字节。只是假设我们在开始时是否压缩了数据大小对于本次讨论没有影响。

    现在,问题是:执行上述操作的最佳方法是什么?

  • 假设小“数据”、小字节和大数据 M​​B
  • 如何减少 read() 调用的次数?例如如果我们在流上有 4 条 16 字节的消息,那么执行 8 次 read() 调用似乎是一种浪费。
  • 这种设计有更好的替代方案吗?
  • 【问题讨论】:

    • IMO,如果您正在处理缓冲 I/O。读取的调用次数并不重要。 read() 只会去缓冲区并获取(或等待)字节数并返回。
    • 我相信真实代码中存在错误检查。 rd 可以为零,表示一件事,或 -1,表示另一件事,或正数,表示第三个。
    • 让我编辑,并添加第 3 个 SIMPLIFIED FOR CLARITY,因为它似乎不清楚...

    标签: c network-programming


    【解决方案1】:

    部分解决方案取决于您使用的传输层协议。 我假设您正在使用 TCP,它提供面向连接的可靠通信。 从您的代码中,我假设您了解 TCP 是一种面向流的协议 (因此,当客户端发送一条数据时,该数据存储在套接字发送缓冲区中,TCP 可能会使用一个或多个 TCP 段将其传送到另一端(服务器)。

    所以代码,到目前为止看起来非常好(考虑到你在真实代码中有错误检查和其他东西)。

    现在对于您的问题,我会根据我的经验给出我认为最好的答案(但可能会有更好的解决方案):

    1-这是一个解决方案,其挑战类似于操作系统管理内存、处理碎片的方式。 为了处理不同的消息大小,您必须了解根据您的性能目标总是需要权衡取舍。 提高内存利用率和并行化的一种解决方案是拥有一定大小的空闲缓冲区块列表,例如 4KB。 您将检索尽可能多的存储接收到的消息。在最后一个中,您将拥有未使用的数据。你玩的是内部碎片。 缺点可能是当您需要对消息应用某种类型的处理(可能是访问者模式)时,例如解析/路由/转换/等。与巨大的连续内存缓冲区相比,它会更复杂且效率更低。另一方面,大缓冲区的缺点是内存利用率低得多、内存瓶颈和并行化少。 你可以在中间实现一些更智能的东西(想想只要可用的块也可以是连续的)。始终取决于您的目标。有用的是在碎片内存上实现抽象,以便应用的每个函数(或访问者)都像处理连续内存一样工作。 如果您使用这些块,当消息被处理和丢弃/转发/吃掉/无论什么时,您会将未使用的块返回到空闲块列表中。

    2-读取调用的数量将取决于 TCP 将数据从客户端传输到服务器的速度。请记住,这是面向流的,您无法对其进行太多控制。当然,我假设您尝试在每次读取中读取最大可能(剩余)数据。 如果您使用我上面提到的块,要读取的最大数据也将取决于块大小。 您可以在 TCP 层做的事情是增加服务器接收缓冲区。因此,即使服务器无法足够快地读取数据,它也可以接收更多数据。

    3-环形缓冲区是可以的,如果你使用分块,环形缓冲区应该提供抽象。但我不知道你为什么需要一个环形缓冲区。 我喜欢环形缓冲区,因为有一种方法可以在没有锁定的情况下实现生产者-消费者同步(Linux 内核使用这种方法将数据包从 L2 移动到 IP 层),但我不知道这是否是您的目标。

    要将消息传递给其他组件和/或上层,您还可以使用指向消息的指针的环形缓冲区。

    【讨论】:

      【解决方案2】:

      更好的设计可能如下:

      1. 将用户空间套接字读取缓冲区设置为与内核套接字缓冲区相同的大小。如果您的用户空间套接字读取缓冲区较小,那么您将需要多个 read 系统调用来读取内核缓冲区。如果您的用户空间缓冲区更大,那么多余的空间就会被浪费。
      2. 您的读取函数应该只在一个read 系统调用中读取尽可能多的数据。这个函数必须对协议一无所知。这样您就无需针对不同的线路格式重新实现此功能。
      3. 当您的读取函数读入用户空间缓冲区时,它应该调用一个回调函数,将迭代器传递给缓冲区中可用的数据。该回调是一个解析器函数,它应该提取所有可用的完整消息并将这些消息传递给另一个更高级别的回调。返回时,解析器函数应返回消耗的字节数,以便可以从用户空间套接字缓冲区中丢弃这些字节。

      【讨论】:

      • 这不就是将问题从“too many read()”转移到“too many memcpy()”吗?假设我仍然需要将数据复制到环形缓冲区
      • @user5042034 嗯,不知道你的意思。理想情况下,您应该只有一个副本 - 从内核到用户空间,并且您的用户空间缓冲区应该是一个环形缓冲区。
      • 假设环形缓冲区在所有客户端/消息之间共享。 client-1 可读但可用数据为 3 字节,不足以识别在 ringbuf 上保留所需的空间。那么client-2是可读的..如果你直接写入ringbuf,你最终会得到[client-1 3byte][client-2][client-1 other bytes]。因此,ringbuf 不包含一组消息,而只是一组您必须以某种方式重建的字节
      • @user5042034 什么客户?不确定我是否关注你。
      • 服务器应该能够处理多个客户端。每个客户端发送消息。消息被添加到 ringbuf。 ringbuf 应该看起来像 [msg1, msg2, msg3, ...] ringbuf 不仅仅包含来自一个客户端的消息,而是所有客户端都附加到同一个 ringbuf
      猜你喜欢
      • 2014-03-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-17
      • 2011-05-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多