SFML2.6 网络模块--使用和扩展数据包

需要解决的问题

在网络上进行数据交换比看起来更加棘手。原因是不同的计算机,使用不同的操作系统和处理器,可能涉及其中。如果您想在这些不同的计算机之间可靠地交换数据,会有几个问题出现。

第一个问题是字节序。字节序是指占用一个或多个字节的基本类型的字节在特定处理器中的解释顺序(例如整数和浮点数)。有两个主要的家族:“大端序”处理器,它们首先存储最重要的字节,和“小端序”处理器,它们首先存储最不重要的字节。还有其他更奇特的字节序,但您可能永远不必处理它们。
问题很明显:如果您在两台字节序不匹配的计算机之间发送一个变量,它们不会看到相同的值。例如,大端序符号的16位整数“42”为00000000 00101010,但如果您将其发送到小端序计算机,则将被解释为“10752”。

第二个问题涉及基本类型的大小。C++标准没有设置基本类型(char,short,int,long,float,double)的大小,因此,再次,处理器之间可能存在差异,而确实存在。例如,长整数类型在某些平台上可能是32位类型,在其他平台上可能是64位类型。

第三个问题是特定于TCP协议的。因为它不保留消息边界,并且可以分割或组合数据块,接收器必须在解释它们之前正确重构传入的消息。否则会发生糟糕的事情,例如读取不完整的变量或忽略有用的字节。

当然,您可能会面临网络编程的其他问题,但这些都是最低层的问题,几乎每个人都需要解决。这就是SFML提供一些简单工具以避免它们的原因。

固定大小的基本类型

由于基本类型不能在网络上可靠地交换,解决方案很简单:不要使用它们。SFML 提供固定大小的类型进行数据交换:sf::Int8、sf::Uint16、sf::Int32 等等。这些类型只是基本类型的 typedef,但它们映射到按照平台预期大小的类型。因此,当您想在两台计算机之间交换数据时,可以(并且必须)安全地使用它们。

SFML 只提供了固定大小的整数类型。浮点类型通常也应该有它们的固定大小的等价类型,但实际上这并不需要(至少在SFML运行的平台上),float 和 double 类型始终具有相同的大小,分别是32位和64位。

数据包

另外两个问题(字节顺序和消息边界)可以通过使用特定的类来打包数据来解决:sf::Packet。另外,它提供了比普通的字节数组更好的接口。

数据包有一个类似于标准流的编程接口:您可以使用 << 运算符插入数据,并使用 >> 运算符提取数据。

// on sending side
sf::Uint16 x = 10;
std::string s = "hello";
double d = 0.6;

sf::Packet packet;
packet << x << s << d;
// on receiving side
sf::Uint16 x;
std::string s;
double d;

packet >> x >> s >> d;

与写入不同,如果尝试从数据包中提取比数据包包含的字节数更多的字节,则读取将失败。如果读取操作失败,数据包的错误标志将被设置。要检查数据包的错误标志,您可以像布尔值一样进行测试(与您在标准流中执行的方式相同):

if (packet >> x)
{
    // ok
}
else
{
    // error, failed to read 'x' from the packet
}

发送和接收数据包与发送/接收字节数组一样简单:套接字具有一个重载的发送和接收函数,可以直接接受sf::Packet。

// with a TCP socket
tcpSocket.send(packet);
tcpSocket.receive(packet);
// with a UDP socket
udpSocket.send(packet, recipientAddress, recipientPort);
udpSocket.receive(packet, senderAddress, senderPort);

数据包解决了“消息边界”问题,这意味着当你在TCP套接字上发送一个数据包时,你在另一端接收到的是完全相同的数据包,它不能包含少量的字节或来自你发送的下一个数据包的字节。然而,它有一个小小的缺点:为了保持消息边界,sf::Packet必须发送一些额外的字节以及你的数据,这意味着如果你想要它们被正确解码,你只能使用sf::Packet来接收它们。简而言之,你不能将SFML数据包发送到非SFML数据包的接收者,它必须使用SFML数据包来接收。请注意,这仅适用于TCP,UDP则没有问题,因为协议本身保留了消息边界。

扩展数据包以处理用户类型

数据包已经为所有基本类型和大多数标准类型提供了操作符的重载,但是对于自定义类呢?和标准流一样,你可以通过提供 << 和 >> 操作符的重载来让一个类型“兼容”sf::Packet。

struct Character
{
    sf::Uint8 age;
    std::string name;
    float weight;
};

sf::Packet& operator <<(sf::Packet& packet, const Character& character)
{
    return packet << character.age << character.name << character.weight;
}

sf::Packet& operator >>(sf::Packet& packet, Character& character)
{
    return packet >> character.age >> character.name >> character.weight;
}

这两个操作符都返回数据包的引用:这允许在数据插入和提取时进行链式操作。

现在这些操作符已经定义好了,就可以像其他基本类型一样,向数据包中插入/提取一个Character实例:

Character bob;

packet << bob;
packet >> bob;

自定义数据包

数据包提供了方便的功能,但如果想添加自己的功能,例如自动压缩或加密数据,怎么办?可以通过继承 sf::Packet 并覆盖以下函数来轻松实现:

  • onSend:在数据被套接字发送之前调用
  • onReceive:在数据被套接字接收之后调用

这些函数提供了对数据的直接访问,因此可以根据需要对其进行转换。

这是一个执行自动压缩/解压缩的数据包的示例:

class ZipPacket : public sf::Packet
{
    virtual const void* onSend(std::size_t& size)
    {
        const void* srcData = getData();
        std::size_t srcSize = getDataSize();
        return compressTheData(srcData, srcSize, &size); // this is a fake function, of course :)
    }
    virtual void onReceive(const void* data, std::size_t size)
    {
        std::size_t dstSize;
        const void* dstData = uncompressTheData(data, size, &dstSize); // this is a fake function, of course :)
        append(dstData, dstSize);
    }
};

这样的数据包类可以像 sf::Packet 一样使用。所有运算符重载也同样适用于它们。

ZipPacket packet;
packet << x << bob;
socket.send(packet);

你可能感兴趣的:(SFML2.6教程,网络,服务器,linux)