首页 > 解决方案 > 我们如何将 Haskell 数据结构序列化和反序列化为字节串?

问题描述

我有一个用 C++ 编写的 TCP 服务器,它需要以下格式的请求:

struct Header {
   int headerField1;
   int headerField2;
}

struct Request {
   Header header;
   char[14] uniqueID;
   char[12] password;
}

我想实现一个客户端将此请求发送到我在 Haskell 中的服务器

我已经尝试过 Data.Binary.encode 没有成功。我更困惑如何在haskell中使用任意大小的类型。即字符[12];

哈斯克尔代码:

data Header = Header 
  {
     headerField1   :: Word32
  ,  headerField2   :: Word32
  } deriving (Generic)
instance Binary Header

data Request = Request 
  {
     header         :: Header
  ,  uniqueID       :: ByteString -- I am not sure which data type to use here.
  ,  password       :: ByteString -- Same as above, as length is defined 12 bytes which is arbitrary.
  } deriving (Generic)
instance Binary Request

我已经为数据解析器编写了一个自定义字节串,它非常适合标头,因为没有任意大小的类型

parseHeader = do
    Header <$> 
        getWord32le <*>
        getWord32le

我正在寻找一种 Haskell 方法来对定义为 ByteString 的数据包结构进行序列化和反序列化(反之亦然),以及一种创建任意大小数据类型的方法——char [12]

标签: haskellnetworkingtcp

解决方案


getByteString要首先解决主要问题,您可以使用(或getLazyByteString)解析已知长度的字节串。所以二进制解析器Request可能是:

parseRequest :: Get Request
parseRequest =
  Request
    <$> parseHeader
    <*> getByteString 14
    <*> getByteString 12

如果你也有一个序列化器,比如说putRequest,你可以把它放在一个Binary带有解析器的实例中,为了方便起见,你可以使用库的更多功能(但你不必这样做)。

instance Binary Request where
  get = parseRequest
  put = putRequest

为避免混淆密码和 ID,将它们包装在新类型中似乎是个好主意:

newtype UniqueID = MkUniqueID ByteString  -- length 14
newtype Password = MkPassword ByteString  -- length 12

在对它们执行操作时,请确保它们不会构造错误长度的值。然后您可以在导出类型时隐藏构造函数,以便用户无法破坏这些不变量。

这些类型的解析器是您指定所需长度的地方:

parseUniqueID :: Get UniqueID
parseUniqueID = MkUniqueID <$> getByteString 14

parsePassword :: Get Password
parsePassword = MkPassword <$> getByteString 12

现在这使得定义Request更具描述性,在 Haskell 代码中混合密码和 ID 的唯一方法是在序列化/反序列化中弄错顺序,因此这减少了其他地方出错的可能性。

data Request = Request
  { header   :: Header
  , uniqueID :: UniqueID
  , password :: Password
  }

parseRequest :: Get Request
parseRequest =
  Request
    <$> parseHeader
    <*> parseUniqueID
    <*> parsePassword

推荐阅读