Boost asio:包括 <arpa/inet.h> 导致套接字始终输出 0 字节

Boost asio: including <arpa/inet.h> causes socket to always output 0 bytes

我正在尝试将 包含在低级库中,以便我可以访问库中的 hton* 和 ntoh* 函数。低级库被高级代码调用 运行 一个 Boost asio 套接字。我知道 Boost asio 包含 hton* 和 ntoh* 函数,但我想避免将所有 Boost asio 链接到库,因为 hton*/ntoh* 是我所需要的。

但是,如果我简单地将 包含在低级库中,则始终会从 Boost asio 套接字发送 0 字节。通过 Wireshark 确认。

这是 class 我想包括 但不包括 Boost 的地方。如果包含 ,将发送 0 个字节。

#pragma pack(push, 1)

#include "PduHeader.h"
#include <arpa/inet.h>

class ClientInfoPdu
{
public:
    ClientInfoPdu(const uint16_t _client_receiver_port)
    {
        set_client_receiver_port(_client_receiver_port);
    }

    PduHeader pdu_header{CLIENT_INFO_PDU, sizeof(client_receiver_port)};


    inline void set_client_receiver_port(const uint16_t _client_receiver_port)
    {
        //client_receiver_port = htons(_client_receiver_port);
        client_receiver_port = _client_receiver_port;
    }

    inline uint16_t get_client_receiver_port()
    {
        return client_receiver_port;
    }

    inline size_t get_total_size()
    {
        return sizeof(PduHeader) + pdu_header.get_pdu_payload_size();
    }

private:
    uint16_t client_receiver_port;
};

#pragma pack(pop)

这是更高级别的代码,其中包含 Boost 并尝试通过套接字发送数据。打印输出表明发送了 5 个字节,但实际发送了 0 个字节。

#include "ServerConnectionThread.h"
#include "config/ClientConfig.h"
#include "protocol_common/ClientInfoPdu.h"
#include <boost/asio.hpp>
#include <unistd.h>

using boost::asio::ip::udp;

void ServerConnectionThread::execute()
{
    boost::asio::io_service io_service;
    udp::endpoint remote_endpoint = 
    udp::endpoint(boost::asio::ip::address::from_string(SERVER_IP), SERVER_PORT);
    udp::socket socket(io_service);
    socket.open(udp::v4());

    ClientInfoPdu client_info_pdu = ClientInfoPdu(RECEIVE_PORT);

    while (true)
    {
        uint16_t total_size = client_info_pdu.get_total_size();
        socket.send_to(boost::asio::buffer(&client_info_pdu, total_size), remote_endpoint);
        printf("sent %u bytes\n", total_size);
        usleep(1000000);
    }
}

同样,简单地删除“#include ”将使此代码按预期运行并每个数据包发送 5 个字节。

ClientInfoPdu是怎么定义的?这看起来很可能是 UB:

boost::asio::buffer(&client_info_pdu, total_size)

总大小是 sizeof(PduHeader) + pdu_header.get_pdu_payload_size()(所以 sizeof(PduHeader) + 2);

第一个问题是您在混合使用访问修饰符,从而破坏了类型的 POD/standard_layout 属性。

#include <type_traits>
static_assert(std::is_standard_layout_v<PduHeader> && std::is_trivial_v<PduHeader>);
static_assert(std::is_standard_layout_v<ClientInfoPdu> && std::is_trivial_v<ClientInfoPdu>);

这将无法编译。将类型视为 POD(如您所做的那样)调用 Undefined Behaviour.

This is likely the explanation for the fact that "it stops working" with some changes. It never worked: it might just accidentally have appeared to work, but it was undefined behaviour.

在获得 POD 的便利性的同时实现 POD 性并不容易 构造函数。事实上,我认为这是不可能的。简而言之,如果你想 将您的结构视为 C 风格的 POD 类型,使它们... C 风格的 POD 类型。

另一件事:`PduHeader I 的可能实现 可以看到为你工作看起来有点像这样:

enum MsgId{CLIENT_INFO_PDU=0x123};
struct PduHeader {
    MsgId id;
    size_t payload_size;
    size_t get_pdu_payload_size() const { return payload_size; }
};

在这里,您可能会再次 have/need 字节顺序转换。

建议

简而言之,如果你想让它起作用,我会说保持简单。

与其通过为每个值添加 getters/setters 来在所有负责字节顺序转换的地方创建非 POD 类型,为什么不创建一个简单的用户定义类型来始终执行此操作,并使用他们呢?

struct PduHeader {
    Short id; // or e.g. uint8_t
    Long payload_size;
};

struct ClientInfoPdu {
    PduHeader pdu_header; // or inheritance, same effect
    Short client_receiver_port;
};

然后将其用作 POD 结构:

while (true) {
    ClientInfoPdu client_info_pdu;
    init_pdu(client_info_pdu);

    auto n = socket.send_to(boost::asio::buffer(&client_info_pdu, sizeof(client_info_pdu)), remote_endpoint);
    printf("sent %lu bytes\n", n);
    std::this_thread::sleep_for(1s);
}

函数 init_pdu 可以通过重载每个子消息来实现:

void init_pdu(ClientInfoPdu& msg) {
    msg.pdu_header.id = CLIENT_INFO_PDU;
    msg.pdu_header.payload_size = sizeof(msg);
}

There are variations on this where it can become a template or take a PduHeder& (if your message inherits instead of aggregates). But the basic principle is the same.

字节顺序转换

现在您会注意到我避免直接​​使用 uint32_t/uint16_t(尽管 uint8_t 很好,因为它不需要字节顺序)。相反,您可以将 LongShort 定义为围绕它们的简单 POD 包装器:

struct Short {
    operator uint16_t() const { return ntohs(value); }
    Short& operator=(uint16_t v) { value = htons(v); return *this; }
  private:
    uint16_t value;
};

struct Long {
    operator uint32_t() const { return ntohl(value); }
    Long& operator=(uint32_t v) { value = htonl(v); return *this; }
  private:
    uint32_t value;
};

赋值和转换意味着您可以将其用作另一个 int32_t/int16_t 除了总是完成必要的转换。

If you want to satnd on the shoulder of giants instead, you can use the better types from Boost Endian, which also has lots more advanced facilities

演示

Live On Coliru

#include <type_traits>
#include <cstdint>
#include <thread>
#include <arpa/inet.h>
using namespace std::chrono_literals;

#pragma pack(push, 1)

enum MsgId{CLIENT_INFO_PDU=0x123};

struct Short {
    operator uint16_t() const { return ntohs(value); }
    Short& operator=(uint16_t v) { value = htons(v); return *this; }
  private:
    uint16_t value;
};

struct Long {
    operator uint32_t() const { return ntohl(value); }
    Long& operator=(uint32_t v) { value = htonl(v); return *this; }
  private:
    uint32_t value;
};

static_assert(std::is_standard_layout_v<Short>);
static_assert(std::is_trivial_v<Short>);

static_assert(std::is_standard_layout_v<Long>);
static_assert(std::is_trivial_v<Long>);

struct PduHeader {
    Short id; // or e.g. uint8_t
    Long payload_size;
};

struct ClientInfoPdu {
    PduHeader pdu_header; // or inheritance, same effect
    Short client_receiver_port;
};

void init_pdu(ClientInfoPdu& msg) {
    msg.pdu_header.id = CLIENT_INFO_PDU;
    msg.pdu_header.payload_size = sizeof(msg);
}

static_assert(std::is_standard_layout_v<PduHeader> && std::is_trivial_v<PduHeader>);
static_assert(std::is_standard_layout_v<ClientInfoPdu> && std::is_trivial_v<ClientInfoPdu>);

#pragma pack(pop)

#include <boost/asio.hpp>
//#include <unistd.h>

using boost::asio::ip::udp;

#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 6767
#define RECEIVE_PORT 6868

struct ServerConnectionThread {
    void execute() {
        boost::asio::io_service io_service;
        udp::endpoint const remote_endpoint = 
            udp::endpoint(boost::asio::ip::address::from_string(SERVER_IP), SERVER_PORT);
        udp::socket socket(io_service);
        socket.open(udp::v4());

        while (true) {
            ClientInfoPdu client_info_pdu;
            init_pdu(client_info_pdu);

            auto n = socket.send_to(boost::asio::buffer(&client_info_pdu, sizeof(client_info_pdu)), remote_endpoint);
            printf("sent %lu bytes\n", n);
            std::this_thread::sleep_for(1s);
        }
    }
};

int main(){ }