Noções básicas sobre serialização

38

Sou engenheiro de software e, após uma discussão com alguns colegas, percebi que não tenho uma boa compreensão da serialização de conceitos. Pelo que entendi, serialização é o processo de converter alguma entidade, como um objeto no OOP, em uma sequência de bytes, para que a referida entidade possa ser armazenada ou transmitida para acesso subseqüente (o processo de "desserialização").

O problema que tenho é: nem todas as variáveis ​​(sejam primitivas intou objetos compostos) já são representadas por uma sequência de bytes? (É claro que são, porque são armazenados em registradores, memória, disco etc.)

Então, o que torna a serialização um tópico tão profundo? Para serializar uma variável, não podemos simplesmente pegar esses bytes na memória e gravá-los em um arquivo? Que complexidades eu perdi?

ddcz
fonte
21
A serialização pode ser trivial para objetos contíguos . Quando o valor do objeto é representado como um gráfico de ponteiro , as coisas se tornam muito mais complicadas, especialmente se o referido gráfico tiver loops.
chi
1
@chi: Sua primeira frase é um pouco enganadora, dado que a contiguidade é irrelevante. Você pode ter um gráfico que seja contínuo na memória e que ainda não o ajudaria a serializá-lo, pois ainda é necessário (a) detectar que ele é contíguo e (b) corrigir os ponteiros internos. Eu diria apenas a segunda parte do que você disse.
Mehrdad 27/03
@Mehrdad Concordo que meu comentário não é completamente preciso, pelos motivos mencionados. Talvez / free-ponteiro ponteiro-usando uma distinção melhor (mesmo que não seja completamente exata, qualquer um)
chi
7
Você também precisa se preocupar com a representação no hardware. Se eu serializar um int 4 bytesno meu PDP-11 e tentar ler esses mesmos quatro bytes na memória do meu macbook, eles não terão o mesmo número (por causa dos Endianes). Portanto, você precisa normalizar os dados para uma representação que pode decodificar (isso é serialização). A maneira como você serializa os dados também tem vantagens e desvantagens em velocidade / flexibilidade humana / legível por máquina.
Martin York
E se você estiver usando o Entity Framework com muitas propriedades de navegação profundamente conectadas? Em um caso, convém serializar uma propriedade de navegação, mas em outro deixe-a nula (porque você recarregará esse objeto real do banco de dados com base no ID que está no seu objeto pai serializado). isso é apenas um exemplo. Há muitos.
ErikE

Respostas:

40

Se você tiver uma estrutura de dados complicada, sua representação na memória poderá normalmente estar espalhada pela memória. (Pense em uma árvore binária, por exemplo.)

Por outro lado, quando você deseja gravá-lo em disco, provavelmente deseja ter uma representação como uma sequência (espero que curta) de bytes contíguos. É isso que a serialização faz por você.

DW
fonte
27

O problema que tenho é: todas as variáveis ​​(primitivas, como objetos int ou compostos) já não são representadas por uma sequência de bytes? (É claro que são, porque são armazenados em registradores, memória, disco etc.)

Então, o que torna a serialização um tópico tão profundo? Para serializar uma variável, não podemos simplesmente pegar esses bytes na memória e gravá-los em um arquivo? Que complexidades eu perdi?

Considere um gráfico de objeto em C com nós definidos como este:

struct Node {
    struct Node* parent;
    struct Node* someChild;
    struct Node* anotherLink;

    int value;
    char* label;
};

//

struct Node nodes[10] = {0};
nodes[5].parent = nodes[0];
nodes[0].someChild = calloc( 1, sizeof(struct Node) );
nodes[5].anotherLink = nodes[3];
for( size_t i = 3; i < 7; i++ ) {
    nodes[i].anotherLink = calloc( 1, sizeof(struct Node) );
}

No tempo de execução, o Nodegráfico inteiro do objeto seria espalhado pelo espaço da memória e o mesmo nó poderia ser apontado a partir de muitos nós diferentes.

Você não pode simplesmente despejar memória em um arquivo / fluxo / disco e chamá-lo de serializado porque os valores do ponteiro (que são endereços de memória) não puderam ser desserializados (porque esses locais de memória já podem estar ocupados quando você carrega o despejo de volta na memória). Outro problema ao simplesmente descarregar a memória é que você acabará armazenando todos os tipos de dados irrelevantes e espaço não utilizado - no x86, um processo tem até 4GiB de espaço na memória, e um sistema operacional ou MMU tem apenas uma idéia geral do que é realmente a memória significativo ou não (com base nas páginas de memória atribuídas a um processo), portanto, Notepad.exedespejar 4 GB de bytes brutos no meu disco sempre que eu quiser salvar um arquivo de texto parece um pouco inútil.

Outro problema está no controle de versão: o que acontece se você serializar seu Nodegráfico no dia 1 e, no dia 2, você adicionar outro campo Node(como outro valor do ponteiro ou um valor primitivo) e, no dia 3, des serializará seu arquivo de dia 1?

Você também deve considerar outras coisas, como endianness. Uma das principais razões pelas quais os arquivos MacOS e IBM / Windows / PC eram incompatíveis entre si nas décadas de 1980 e 1990, apesar de ostensivamente terem sido criados pelos mesmos programas (Word, Photoshop, etc.), porque nos valores inteiros de vários bytes x86 / PC foram salvas em ordem little-endian, mas ordem big-endian no Mac - e o software não foi construído com a portabilidade entre plataformas em mente. Atualmente, as coisas estão melhores graças à melhoria da educação do desenvolvedor e ao nosso mundo da computação cada vez mais heterogêneo.

Dai
fonte
2
Despejar tudo no espaço da memória do processo também seria horrível por razões de segurança. Uma noite de programa tem na memória 1) alguns dados públicos e 2) senha, nonce secreto ou chave privada. Ao serializar o primeiro, não se deseja revelar nenhuma informação sobre o último.
chi
8
Uma observação muito interessante sobre este tópico: Por que os formatos de arquivo do Microsoft Office são tão complicados?
marcando
15

As é complicado, na verdade, já descrito na própria palavra: " série ização".

A questão é basicamente: como posso representar um gráfico dirigido cíclico interconectado arbitrariamente complexo de objetos arbitrariamente complexos como uma sequência linear de bytes?

Pense bem: uma sequência linear é como um gráfico direcionado e degenerado, onde todo vértice tem exatamente uma aresta de entrada e saída (exceto o "primeiro vértice" que não possui aresta de entrada e o "último vértice" que não possui aresta de saída) . E um byte é obviamente menos complexo que um objeto .

Portanto, parece razoável que, à medida que passamos de um gráfico arbitrariamente complexo para um "gráfico" muito mais restrito (na verdade apenas uma lista) e de objetos arbitrariamente complexos para bytes simples, as informações serão perdidas, se fizermos isso de forma ingênua e não ' t codificar as informações "estranhas" de alguma maneira. E é exatamente isso que a serialização faz: codifique as informações complexas em um formato linear simples.

Se você estiver familiarizado com o YAML , poderá examinar os recursos de âncora e alias que permitem representar a ideia de que "o mesmo objeto pode aparecer em locais diferentes" em uma serialização.

Por exemplo, se você tiver o seguinte gráfico:

A → B → D
↓       ↑
C ––––––+

Você pode representar isso como uma lista de caminhos lineares no YAML assim:

- [&A A, B, &D D]
- [*A, C, *D]

Você também pode representá-lo como uma lista de adjacência, ou uma matriz de adjacência, ou como um par cujo primeiro elemento é um conjunto de nós e cujo segundo elemento é um conjunto de pares de nós, mas em todas essas representações, é necessário ter uma maneira de recuar e encaminhar para nós existentes , ou seja, ponteiros , que você geralmente não possui em um arquivo ou fluxo de rede. Tudo o que você tem, no final, são bytes.

(O que significa que o arquivo de texto YAML acima também precisa ser "serializado", é para isso que servem as várias codificações de caracteres e formatos de transferência Unicode ... não é estritamente "serialização", apenas codificação, porque o arquivo de texto já é serial / lista linear de pontos de código, mas você pode ver algumas semelhanças.)

Jörg W Mittag
fonte
13

As outras respostas já abordam gráficos de objetos complexos, mas vale ressaltar que as primitivas de serialização também não são triviais.

Usando nomes de tipo primitivo C para concretude, considere:

  1. Eu serializo a long. Algum tempo depois, eu desserializá-lo, mas ... em uma plataforma diferente, e agora longé int64_tmais do que int32_teu armazenei. Portanto, preciso ter muito cuidado com o tamanho exato de cada tipo que armazeno ou armazenar alguns metadados que descrevem o tipo e o tamanho de cada campo.

    Observe que essa plataforma diferente pode ser a mesma depois de uma recompilação futura.

  2. Eu serializo um int32_t. Algum tempo depois, desserializá-lo, mas ... em uma plataforma diferente, e agora o valor está corrompido. Infelizmente, salvei o valor em uma plataforma big endian e carreguei em uma plataforma little endian. Agora, preciso estabelecer uma convenção para o meu formato ou adicionar mais metadados que descrevam a capacidade de endereçamento de cada arquivo / fluxo / qualquer coisa. E, é claro, efetue as conversões apropriadas.

  3. Eu serializo uma string. Desta vez, uma plataforma usa charUTF-8 e uma wchar_te UTF-16.

Então, eu diria que a serialização de qualidade razoável não é trivial, mesmo para primitivas na memória contígua. É necessário documentar muitas decisões de codificação ou descrever com metadados embutidos.

Os gráficos de objetos adicionam outra camada de complexidade além disso.

Sem utilidade
fonte
6

Existem vários aspectos:

Legibilidade pelo mesmo programa

Seu programa armazenou seus dados de alguma forma como bytes na memória. Mas pode ser arbitrariamente espalhado por registros diferentes, com ponteiros indo e voltando entre partes menores [editar: Como comentado, fisicamente os dados são mais prováveis ​​na memória principal do que um registro de dados, mas isso não elimina o problema do ponteiro] . Pense em uma lista inteira vinculada. Cada elemento da lista pode ser armazenado em um local totalmente diferente e tudo o que mantém a lista unida são os ponteiros de um elemento para o outro. Se você pegar esses dados como estão e tentar copiá-los em outra máquina executando o mesmo programa, terá problemas:

  1. Em primeiro lugar, o registro aborda seus dados armazenados em uma máquina que já pode ser usada para algo completamente diferente em outra máquina (alguém está navegando na troca de pilhas e o navegador já consome toda a memória). Então, se você simplesmente substituir esses registros, adeus, navegador. Assim, você precisaria reorganizar os ponteiros na estrutura para caber nos endereços que você tem gratuitamente na segunda máquina. O mesmo problema surge quando você tenta recarregar os dados na mesma máquina posteriormente.
  2. E se algum componente externo apontar para sua estrutura ou sua estrutura tiver ponteiros para dados externos, você não transmitiu? Segfaults em todos os lugares! Isso se tornaria um pesadelo de depuração.

Legibilidade por outro programa

Digamos que você consiga alocar apenas os endereços certos em outra máquina, para que seus dados se encaixem. Se seus dados forem processados ​​por um programa separado nessa máquina (idioma diferente), esse programa poderá ter um entendimento básico totalmente diferente dos dados. Digamos que você tenha objetos C ++ com ponteiros, mas sua linguagem de destino nem mesmo suporta ponteiros nesse nível. Novamente, você acaba sem uma maneira limpa de endereçar esses dados no segundo programa. Você acaba com alguns dados binários na memória, mas precisa escrever um código extra que envolva os dados e, de alguma forma, traduza-o em algo com o qual seu idioma de destino possa trabalhar. Parece desserialização, apenas que seu ponto de partida agora é um objeto estranho espalhado pela memória principal, diferente para diferentes idiomas de origem, em vez de um arquivo com uma estrutura bem definida. A mesma coisa, é claro, se você tentar interpretar diretamente o arquivo binário que inclui ponteiros - precisará escrever analisadores para todas as formas possíveis em que outro idioma possa representar dados na memória.

Legibilidade por um ser humano

Duas das linguagens de serialização modernas mais importantes para serialização baseada na Web (xml, json) são facilmente compreensíveis por um ser humano. Em vez de uma pilha de gosma binária, a estrutura e o conteúdo reais dos dados são claros, mesmo sem um programa para ler os dados. Isso tem várias vantagens:

  • depuração mais fácil -> se houver um problema no seu pipeline de serviço, basta olhar para os dados que saem de um serviço e verificar se faz sentido (como um primeiro passo); você também vê diretamente se os dados parecem como deveria, quando você escreve sua interface de exportação.
  • arquivabilidade: se você tem seus dados como uma pilha de objetos binários puros e perde o programa que pretende interpretá-los, perde os dados (ou terá que gastar algum tempo para realmente encontrar algo nele); se seus dados serializados forem legíveis por humanos, você poderá usá-los facilmente como um arquivo ou programar seu próprio importador para um novo programa
  • a natureza declarativa dos dados serializados dessa maneira também significa que é totalmente independente do sistema de computador e de seu hardware; você pode carregá-lo em um computador quântico totalmente diferente ou infectar uma IA alienígena com fatos alternativos, para que acidentalmente voe para o próximo sol (Emmerich, se você ler isso, uma referência seria legal, se você usar essa ideia no próximo dia 4 de julho filme)
Frank Hopkins
fonte
Meus dados provavelmente estão principalmente na memória principal, não nos registros. Se meus dados se encaixam nos registros, a serialização é apenas um problema. Acho que você não entendeu o que é um registro.
David Richerby
Na verdade, eu usei o termo registrar muito livremente aqui. Mas o ponto principal é que seus dados podem conter ponteiros para o espaço de endereço para identificar seus próprios componentes ou para se referir a outros dados. Não importa se é um registro físico ou um endereço virtual na memória principal.
27617 Frank Hopkins
Não, você usou o termo "registrar" completamente incorretamente. As coisas que você está chamando de registradores estão em uma parte completamente diferente da hierarquia de memória dos registros reais.
David Richerby
6

Além do que as outras respostas disseram:

Às vezes, você deseja serializar coisas que não são dados puros.

Por exemplo, pense em um identificador de arquivo ou uma conexão com um servidor. Embora o identificador ou o soquete do arquivo seja um int, esse número não faz sentido na próxima vez que o programa for executado. Para recriar adequadamente objetos que contêm identificadores para essas coisas, é necessário reabrir arquivos e recriar conexões e decidir o que fazer se isso falhar.

Atualmente, muitos idiomas suportam o armazenamento de funções anônimas em objetos, por exemplo, um onBlah()manipulador em Javascript. Isso é desafiador, porque esse código pode conter referências a dados adicionais que, por sua vez, precisam ser serializados. (E há o problema de serializar código de uma forma multiplataforma, o que é obviamente mais fácil para idiomas interpretados.) Ainda assim, mesmo que apenas um subconjunto do idioma possa ser suportado, ele ainda pode ser bastante útil. Não há muitos mecanismos de serialização que tentam serializar código, mas consulte serialize-javascript .

Nos casos em que você deseja serializar um objeto, mas ele contém algo que não é suportado pelo seu mecanismo de serialização, é necessário reescrever o código de maneira a solucionar esse problema. Por exemplo, você pode usar enumerações no lugar de funções anônimas quando houver um número finito de funções possíveis.

Geralmente, você deseja que os dados serializados sejam concisos.

Se você estiver enviando dados pela rede ou mesmo armazenando-os em disco, pode ser importante manter o tamanho pequeno. Uma das maneiras mais fáceis de conseguir isso é jogar fora as informações que podem ser reconstruídas (por exemplo, descartando caches, tabelas de hash e representações alternativas dos mesmos dados).

Obviamente, o programador precisa selecionar manualmente o que deve ser salvo e o que deve ser descartado e garantir que as coisas sejam reconstruídas quando o objeto for recriado.

Pense no ato de salvar um jogo. Os objetos podem conter muitos ponteiros para dados gráficos, dados de som e outros objetos. Mas a maioria dessas coisas pode ser carregada dos arquivos de dados do jogo e não precisa ser armazenada em um arquivo salvo. Descartar isso pode ser trabalhoso, então poucas coisas são deixadas. Eu editei hexadecimalmente alguns arquivos salvos no meu tempo e descobri dados claramente redundantes, como descrições de itens de texto.

Às vezes, o espaço não é importante, mas a legibilidade é - nesse caso, você pode usar um formato ASCII (possivelmente JSON ou XML).

Artelius
fonte
3

Vamos definir o que realmente é uma sequência de bytes. Uma sequência de bytes consiste em um número inteiro não negativo chamado comprimento e alguma função / correspondência arbitrária que mapeia qualquer número inteiro i que seja pelo menos zero e menor que o comprimento para um valor de byte (um número inteiro de 0 a 255).

Muitos dos objetos com os quais você lida em um programa típico não estão nessa forma, porque os objetos são realmente compostos de muitas alocações de memória diferentes que estão em locais diferentes da RAM e podem ser separados um do outro por milhões de bytes de material que você não me importo. Pense em uma lista básica vinculada: cada nó da lista é uma sequência de bytes, sim, mas os nós estão em vários locais diferentes na memória do computador e estão conectados a ponteiros. Ou apenas pense em uma estrutura simples que tenha um ponteiro para uma cadeia de comprimento variável.

A razão pela qual queremos serializar estruturas de dados em uma sequência de bytes é geralmente porque queremos armazená-las em disco ou enviá-las para um sistema diferente (por exemplo, pela rede). Se você tentar armazenar um ponteiro no disco ou enviá-lo para um sistema diferente, será bastante inútil porque o programa que estiver lendo esse ponteiro terá um conjunto diferente de áreas de memória disponíveis.

David Grayson
fonte
1
Não tenho certeza de que seja uma ótima definição de sequência. A maioria das pessoas definiria uma sequência para ser, bem, uma sequência: uma linha de coisas uma após a outra. Por sua definição, int seq(int i) { if (0 <= i < length) return i+1; else return -1;}é uma sequência. Então, como vou armazenar isso em disco?
David Richerby
1
Se o comprimento é de 4, eu armazenar um arquivo de quatro bytes com conteúdo: 1, 2, 3, 4.
David Grayson
1
@DavidRicherby Sua definição é equivalente a "uma linha de coisas uma após a outra", é apenas uma definição mais matemática e precisa do que a sua definição intuitiva. Observe que sua função não é uma sequência, porque para ter uma sequência, você precisa dessa função e de outro número inteiro chamado comprimento.
precisa saber é o seguinte
1
@FreshAir Meu argumento é que a sequência é 1, 2, 3, 4, 5. A coisa que escrevi é uma função . Uma função não é uma sequência.
David Richerby
1
Uma maneira simples de escrever uma função no disco é a que eu já propus: para cada entrada possível, armazene a saída. Acho que talvez você ainda não entenda, mas não tenho certeza do que dizer. Você sabia que em sistemas embarcados é comum converter funções caras, como sinem uma tabela de pesquisa, que é uma sequência de números? Você sabia que sua função é a mesma para as entradas de que gostamos? int seq(n) { int a[] = [1, 2, 3, 4]; return a[n]; } Por que exatamente você diz que meu arquivo de quatro bytes é uma representação inadequada?
David Grayson
2

Os meandros refletem os meandros dos dados e objetos em si. Esses objetos podem ser objetos do mundo real ou apenas objetos de computador. A resposta está no nome. Serialização é a representação linear de objetos multidimensionais. Existem muitos outros problemas além da RAM fragmentada.

Se você pode achatar 12 matrizes tridimensionais e algum código de programa, a serialização também permite transferir um programa de computador inteiro (e dados) entre máquinas. Protocolos de computação distribuídos, como RMI / CORBA, usam serialização extensivamente para transferir dados e programas.

Considere sua conta de telefone. Pode ser um único objeto, composto por todas as suas chamadas (lista de cadeias), valor a pagar (número inteiro) e país. Ou sua conta telefônica pode estar de dentro para fora a partir do exposto acima e consistir em chamadas telefônicas detalhadas e detalhadas, vinculadas ao seu nome. Cada achatado terá uma aparência diferente, refletirá como a companhia telefônica escreveu a versão do software e a razão pela qual os bancos de dados orientados a objetos nunca decolaram.

Algumas partes de uma estrutura podem nem estar na memória. Se você tiver um cache lento, algumas partes de um objeto poderão ser referenciadas apenas a um arquivo de disco e serão carregadas apenas quando a parte desse objeto em particular for acessada. Isso é comum em estruturas de persistência sérias. BLOBs são um bom exemplo. A Getty Images pode armazenar uma enorme imagem de Fidel Castro, com vários megabytes, e alguns metadados, como o nome da imagem, o custo do aluguel e a própria imagem. Você pode não querer carregar a imagem de 200 MB na memória todas as vezes, a menos que você realmente olhe para ele. Serializado, o arquivo inteiro exigiria mais de 200 MB de armazenamento.

Alguns objetos nem podem ser serializados. No campo da programação Java, você pode ter um objeto de programação representando a tela gráfica ou uma porta serial física. Não existe um conceito real de serializar nenhum deles. Como você envia sua porta para outra pessoa através de uma rede?

Algumas coisas, como senhas / chaves de criptografia, não devem ser armazenadas ou transmitidas. Eles podem ser marcados como tal (voláteis / transitórios, etc.) e o processo de serialização os ignorará, mas eles poderão viver na RAM. Omitir essas tags é como as chaves de criptografia são enviadas / armazenadas inadvertidamente em ASCII simples.

Esta e as outras respostas é por que é complicado.

Paul Uszak
fonte
2

O problema que tenho é: todas as variáveis ​​(primitivas, como objetos int ou compostos) já não são representadas por uma sequência de bytes?

Sim, eles estão. O problema aqui é o layout desses bytes. Um simples intpode ter 2, 4 ou 8 bits de comprimento. Pode ser em endian grande ou pequeno. Pode ser sem assinatura, assinado com o complemento 1 ou até mesmo em alguns códigos de bits super exóticos, como o negabinário.

Se você apenas despejar o intbinário da memória e chamá-lo de "serializado", precisará conectar praticamente todo o computador, sistema operacional e seu programa para que seja desserializável. Ou pelo menos, uma descrição precisa deles.

Então, o que torna a serialização um tópico tão profundo? Para serializar uma variável, não podemos simplesmente pegar esses bytes na memória e gravá-los em um arquivo? Que complexidades eu perdi?

A serialização de um objeto simples é praticamente anotada de acordo com algumas regras. Essas regras são muitas e nem sempre óbvias. Por exemplo, um xs:integerXML é escrito na base 10. Não é base 16, não é base 9, mas 10. Não é uma suposição oculta, é uma regra real. E essas regras tornam a serialização uma serialização. Porque, basicamente, não existem regras sobre o layout de bits do seu programa na memória .

Isso foi apenas uma ponta de um iceberg. Vamos dar um exemplo de uma sequência desses primitivos mais simples: um C struct. Você poderia pensar que

struct {
short width;
short height;
long count;
}

possui um layout de memória definido em um determinado computador + SO? Bem, isso não acontece. Dependendo da #pragma packconfiguração atual , o compilador preencherá os campos. Nas configurações padrão da compilação de 32 bits, ambos shortsserão preenchidos com 4 bytes, de modo que na structverdade terão 3 campos de 4 bytes na memória. Portanto, agora, você não apenas precisa especificar que shorttem 16 bits, mas é um número inteiro, escrito no complemento de 1 negativo, grande ou pequeno endian. Você também precisa anotar a configuração de compactação da estrutura com a qual seu programa foi compilado.

É disso que trata a serialização: criar um conjunto de regras e seguir essas regras.

Essas regras podem ser expandidas para aceitar estruturas ainda mais sofisticadas (como listas de comprimento variável ou dados não lineares), recursos adicionais como legibilidade humana, controle de versão, compatibilidade com versões anteriores e correção de erros, etc. Mas até escrever uma única intjá é bastante complicado se você só quero ter certeza de que você será capaz de lê-lo de forma confiável.

Agent_L
fonte