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 int
ou 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?
4 bytes
no 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.Respostas:
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ê.
fonte
Considere um gráfico de objeto em C com nós definidos como este:
No tempo de execução, o
Node
grá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.exe
despejar 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
Node
gráfico no dia 1 e, no dia 2, você adicionar outro campoNode
(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.
fonte
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:
Você pode representar isso como uma lista de caminhos lineares no YAML assim:
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.)
fonte
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:
Eu serializo a
long
. Algum tempo depois, eu desserializá-lo, mas ... em uma plataforma diferente, e agoralong
éint64_t
mais do queint32_t
eu 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.
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.char
UTF-8 e umawchar_t
e 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.
fonte
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:
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:
fonte
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).
fonte
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.
fonte
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?sin
em 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?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.
fonte
Sim, eles estão. O problema aqui é o layout desses bytes. Um simples
int
pode 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
int
biná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.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:integer
XML é 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 quepossui um layout de memória definido em um determinado computador + SO? Bem, isso não acontece. Dependendo da
#pragma pack
configuração atual , o compilador preencherá os campos. Nas configurações padrão da compilação de 32 bits, ambosshorts
serão preenchidos com 4 bytes, de modo que nastruct
verdade terão 3 campos de 4 bytes na memória. Portanto, agora, você não apenas precisa especificar queshort
tem 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
int
já é bastante complicado se você só quero ter certeza de que você será capaz de lê-lo de forma confiável.fonte