0

我们正在将 C 中的遗留代码重写为 C++。在我们系统的核心,我们有一个 TCP 客户端,它连接到 master。Master 将不断地流式传输消息。每个套接字读取都会产生 N 条格式为 - 的消息{type, size, data[0]}

现在我们不将这些消息复制到单独的缓冲区中——而只是将消息开头的指针、长度和 shared_ptr 传递给底层缓冲区给工作人员。

旧的 C 版本是单线程的,会进行如下的 NTOH 转换:

struct Message {
   uint32_t something1; 
   uint16_t something2; 
};

process (char *message)
Message *m = (message);
m->something1 = htonl(m->something1);
m->something2 = htons(m->something2);

然后使用消息。

跟踪新代码的登录存在几个问题。

  1. 由于我们将消息分派给不同的工作人员,每个进行 ntoh 转换的工作人员都会导致缓存未命中问题,因为消息没有缓存对齐 - 即消息没有填充 b/w。

  2. 相同的消息可以由不同的工作人员处理 - 这是消息需要在本地处理并中继到另一个进程的情况。在这里,中继工作者需要原始网络顺序的消息,本地工作需要转换为主机顺序。显然,由于消息不重复,两者都不能满足。

我想到的解决方案是 -

  1. 复制消息并为所有中继工作人员发送一份副本(如果有)。在分派之前,在分派器本身中对属于同一缓冲区的所有消息进行 ntoh 转换 - 比如说通过调用 ahandler->ntoh(message);以便解决缓存未命中问题。

  2. 向每个工人发送原件。每个worker将消息复制到本地缓冲区,然后进行ntoh转换并使用它。在这里,每个工作人员都可以使用特定于线程的(thread_local)静态缓冲区作为便签本来复制消息。

现在我的问题是

  1. 选项 1 是进行 ntoh 转换的方式 - C++sy 吗?我的意思是结构的对齐要求将与 char 缓冲区不同。(我们还没有遇到任何问题。)。在这种情况下使用方案 2 应该没问题,因为暂存缓冲区可以具有 max_align_t 对齐,因此应该可以类型转换为任何结构。但这会导致复制整个消息 - 这可能非常大(比如几个 K 大小)

  2. 有没有更好的方法来处理这种情况?

4

1 回答 1

1

您的主要问题似乎是如何处理未对齐的消息。也就是说,如果每个消息结构的末尾没有足够的填充以使以下消息正确对齐,则可以通过将指向消息开头的指针重新解释为对象来触发未对齐的读取。

我们可以通过多种方式解决这个问题,也许最简单的方法是ntoh基于单字节指针,它实际上总是对齐的。

我们可以将令人讨厌的细节隐藏在包装类后面,它会接受一个指向消息开头的指针,并具有访问ntoh适当字段的访问器。

正如评论中所指出的,偏移量必须由 C++ 结构确定,因为这是最初创建消息的方式,并且它可能不会被打包。

首先,我们的ntoh实现是模板化的,因此我们可以按类型选择一个:

template <typename R>
struct ntoh_impl;

template <>
struct ntoh_impl<uint16_t>
{
    static uint16_t ntoh(uint8_t const *d)
    {
        return (static_cast<uint16_t>(d[0]) << 8) |
                d[1];
    }
};

template <>
struct ntoh_impl<uint32_t>
{
    static uint32_t ntoh(uint8_t const *d)
    {
        return (static_cast<uint32_t>(d[0]) << 24) |
               (static_cast<uint32_t>(d[1]) << 16) |
               (static_cast<uint32_t>(d[2]) <<  8) |
               d[3];
    }
};

template<>
struct ntoh_impl<uint64_t>
{
    static uint64_t ntoh(uint8_t const *d)
    {
        return (static_cast<uint64_t>(d[0]) << 56) |
               (static_cast<uint64_t>(d[1]) << 48) |
               (static_cast<uint64_t>(d[2]) << 40) |
               (static_cast<uint64_t>(d[3]) << 32) |
               (static_cast<uint64_t>(d[4]) << 24) |
               (static_cast<uint64_t>(d[5]) << 16) |
               (static_cast<uint64_t>(d[6]) <<  8) |
               d[7];
    }
};

现在我们将定义一组讨厌的宏,它们将通过在结构中查找具有匹配名称的成员proto(每个类的私有结构)来自动实现给定名称的访问器:

#define MEMBER_TYPE(MEMBER) typename std::decay<decltype(std::declval<proto>().MEMBER)>::type

#define IMPL_GETTER(MEMBER) MEMBER_TYPE(MEMBER) MEMBER() const { return ntoh_impl<MEMBER_TYPE(MEMBER)>::ntoh(data + offsetof(proto, MEMBER)); }

最后,我们有一个您给出的消息结构的示例实现:

class Message
{
private:
    struct proto
    {
        uint32_t something1;
        uint16_t something2;
    };

public:
    explicit Message(uint8_t const *p) : data(p) {}
    explicit Message(char const *p) : data(reinterpret_cast<uint8_t const *>(p)) {}

    IMPL_GETTER(something1)
    IMPL_GETTER(something2)

private:
    uint8_t const *data;
};

现在Message::something1()Message::something2()已实现,并将从data指针中读取它们最终所在的相同偏移量Message::proto

ntoh在标头中提供实现(有效地内联)有可能在每个访问器的调用站点内联整个序列!

此类不拥有构造它的数据分配。如果这里有所有权维护的细节,大概你可以编写一个基类。

于 2017-04-11T22:55:28.753 回答