Course-এর সূচি

2.9

Serialization: JSON, XML, Protobuf, Avro ও Thrift

Text বনাম binary format, এদের আকার আর গতি, আর API, event ও storage-এর জন্য কোনটা বেছে নেবে।

45 মিনিট পড়া + Animation

টিপস: ডটেড-আন্ডারলাইন করা যেকোনো শব্দে ট্যাপ বা Hover করলে তার অর্থ দেখবে।

এই Lesson শেষে তুমি পারবে

  • Serialization (Marshalling) আর Deserialization কী, আর প্রতিটা Network Call, Cache Entry, Queue Message আর File-এ কেন এগুলো লাগে, তা বোঝাতে।
  • যে কোনো Format-কে দুটো মাপকাঠিতে বসাতে, Text বনাম Binary আর Schema-ছাড়া বনাম Schema-ভিত্তিক, আর সেখান থেকেই তার আকার, গতি আর বদলানোর আচরণ আগে থেকে বলতে।
  • JSON নিরাপদে ব্যবহার করতে: এর Type-এর ফাঁক (৬৪ bit Integer নেই, Binary নেই, Date নেই, Comment নেই), বড় ID-র 2⁵³ সমস্যা, base64-এর খরচ, আর JSON Schema।
  • XML-এর জোর (Namespace, XSD, Text আর Markup মেশানো) আর এর প্রধান নিরাপত্তার ফাঁদ XXE চিনতে।
  • হাতে-কলমে Protocol Buffers Decode করতে: Tag, Field Number, Wire Type, Varint আর Zigzag, অচেনা Field, proto3-এ Presence, আর Code Generation।
  • Tag ছাড়া Avro কীভাবে চলে (Writer বনাম Reader Schema, নাম ধরে মেলানো, Schema Registry), আর Thrift কীভাবে Protocol আর Transport যোগ করে, তা বোঝাতে।
  • কখন MessagePack, CBOR, FlatBuffers বা Cap’n Proto মানায় তা জানতে, আর আকার ও গতির সৎ সংখ্যা বলতে।
  • অনিরাপদ Deserialization (pickle, Java Serialization, XXE) চিনতে, আর Public API, ভেতরের RPC, Event আর Storage-এর জন্য Format বাছতে।

#এই Lesson কোথায় বসে

এ পর্যন্ত প্রতিটা Lesson-এ আমরা কোথাও না কোথাও Data পাঠিয়েছি। Lesson ১.১-এ দেখেছ JSON Body আর Content-Type Header সহ একটা Request; Lesson ১.৫-এ Data Format-কে Presentation Layer-এ বসিয়েছি, Character Encoding (UTF-8) আর Compression-এর পাশে, আর সেখানে JSON আর XML-এর নামও ছিল। Lesson ১.৩-এ দেখেছ JWT: base64url করা JSON। আর Lesson ২.৩-এ দেখেছ HTTP কীভাবে gzip, Brotli বা zstd দিয়ে Body ছোট করে।

এই Lesson খুলবে Body-টাকেই: একটা Program-এর Memory-তে থাকা Object কীভাবে এমন Byte হয়ে যায়, যেটা একদম অন্য একটা Program আবার Object বানিয়ে নিতে পারে। এখানে যে Format-গুলো শিখবে: JSON, XML, Protocol Buffers (Protobuf), Apache Avro আর Apache Thrift, সাথে Production-এ দেখা হবে এমন আরও কয়েকটা।

দুটো প্রতিবেশী বিষয় এখানে ইচ্ছে করেই ছোট রাখা হয়েছে:

  • নিরাপদে Format বদলানো (Field যোগ, বাদ আর নাম বদল; Backward, Forward আর Full Compatibility) আছে Lesson ২.১০, Schema Evolution-এ। এখানে শুধু দেখব কোন কৌশলগুলোর জন্য সেটা সম্ভব হয়।
  • API Design (REST বনাম gRPC বনাম GraphQL) আছে Chapter ২৬-এ, Messaging আর Schema Registry গভীরভাবে Chapter ৩০–৩২-এ, আর Columnar Format (Parquet, ORC) Chapter ৪২-এ।

#Serialization কী, আর কেন লাগে

চলমান একটা Program Data রাখে Memory-তে Object হিসেবে: Field সহ Struct আর Class, Heap-এর অন্য জায়গায় রাখা String-এর Pointer, Compiler-এর বসানো Padding, CPU-র নিজের ক্রমে সাজানো Integer। এর কিছুই Process-এর বাইরে যেতে পারে না। অন্য Program তোমার Memory পড়তে পারে না, অন্য Language এই সাজানো বুঝবে না, আর অন্য মেশিনে একটা Pointer-এর কোনো মানেই নেই।

  • Serialization (একে Marshalling বা Encoding-ও বলে) Memory-র একটা Object বা Data Structure-কে মেনে নেওয়া একটা Format-এ পরপর সাজানো Byte-এ বদলে দেয়, যাতে সেটা File-এ Save করা যায় বা Network দিয়ে অন্য মেশিনে পাঠানো যায়।
  • Deserialization (Unmarshalling, Decoding, Parsing) উল্টো কাজটা করে: Byte পড়ে একই Value দিয়ে নতুন একটা Object বানায়।

তুমি যতটা টের পাও, তার চেয়ে অনেক বেশিবার Serialize করো:

কোথায়উদাহরণ
Network CallHTTP API-র Body, gRPC Message, WebSocket Frame
StorageDatabase-এর Row বা Document, Log-এর লাইন, Backup, Data Lake-এর File
CacheRedis বা Memcached-এ রাখা User Profile
Queue আর StreamKafka Message, SQS Job, Event Bus-এর Event
Process থেকে Processএক মেশিনের Worker Pool, Browser আর তার Web Worker

Serialization-এর পুরো পথ: Object → Byte → Object

Network পার হতে পারে বা File-এ বসে থাকতে পারে শুধু Byte। Serialization Memory-র Object-কে Byte বানায়; Deserialization সেই Byte থেকে নতুন একটা Object বানায়।

পরিস্থিতি

Order Service নতুন একজন User-এর খবর Email Service-কে দেয়, HTTP-র ওপর JSON হিসেবে।

Order ServiceGo-তে লেখা
Wire বা Disk
Email ServiceJava-তে লেখা
Memory-তে User{id: 101, name: "Rahim", …}
ধাপ 1 / 6
1

Memory-তে Object-টা একটা Struct: তাতে String-এর জন্য Heap-এর অন্য জায়গার Pointer, Field-এর মাঝে Padding, আর Go-র নিজের ঠিক করা সাজানো। অন্য কোনো Process এই Memory পড়তে পারে না, আর পারলেও Java Program এই সাজানো বুঝত না।

প্রতিটা Format-কে একই পাঁচটা প্রশ্নের উত্তর দিতে হয়, আর এই উত্তরগুলোই তার আকার, গতি আর নিরাপত্তার প্রায় সব কিছু ব্যাখ্যা করে:

  1. কোন কোন Type আছে? কত বড় Integer, Float, String, Bytes, Boolean, Null, Date, Map, List।
  2. একটা Field চেনা যায় কী দিয়ে? নাম দিয়ে (JSON, XML, MessagePack), নম্বর দিয়ে (Protobuf, Thrift), নাকি Schema-য় তার অবস্থান দিয়ে (Avro)।
  3. একটা Value কোথায় শেষ? সীমানা-চিহ্ন দিয়ে, যেগুলো Escape করতে হয় (Quote, Comma, বন্ধ করার Tag), নাকি একটা Length Prefix দিয়ে (Binary Format)।
  4. Schema কোথায় থাকে? প্রতিটা Message-এর ভেতরে (নিজের বর্ণনা নিজেই দেয়), Code-এ Compile করা আলাদা একটা File-এ, নাকি একটা Registry-তে।
  5. Text নাকি Binary? মানুষ Byte-গুলো পড়তে পারে, নাকি শুধু Program?

#দুটো মাপকাঠি: Text বনাম Binary, Schema-ছাড়া বনাম Schema-ভিত্তিক

নোটে Format-গুলোকে দুই ভাগে ভাগ করা হয়েছে:

  1. মানুষের পড়ার মতো Text Format: JSON, XML, YAML। পড়া আর Debug করা সহজ, কিন্তু আকারে বড় আর Parse করতে সময় বেশি লাগে।
  2. Binary Format: Protocol Buffers, Apache Avro, Apache Thrift। মানুষ সরাসরি পড়তে পারে না, কিন্তু খুব ছোট, Memory-তে সাশ্রয়ী আর Process করতে দারুণ দ্রুত।

আরেকটা মাপকাঠিও সমান জরুরি: Data কি নিজের Field-এর নাম নিজেই বয়ে নেয়, নাকি Reader-এর একটা Schema লাগে?

Schema-ছাড়া (নিজের বর্ণনা নিজেই দেয়)Schema-ভিত্তিক
TextJSON, XML, YAMLJSON Schema বা XSD দিয়ে যাচাই করা JSON বা XML (Schema ঐচ্ছিক, Data তখনো নাম বয়ে নেয়)
BinaryMessagePack, CBOR, BSON (Field-এর নাম ভেতরে আছে, শুধু আরও ঠাসা)Protobuf, Thrift (Wire-এ Field Number), Avro (Wire-এ শুধু Value, আর কিছু না)

নিজের বর্ণনা নিজে দেয় এমন Data যে কেউ যে কোনো সময় পড়তে পারে, এজন্যই Public API এটা পছন্দ করে; কিন্তু প্রতিটা Message-এ প্রতিটা Field-এর নাম আবার লেখা হয়। Schema-ভিত্তিক Binary পাঠায় শুধু নম্বর, বা কিছুই না, তাই ছোট; কিন্তু Schema ছাড়া Reader দেখে শুধু আবর্জনা।

নোটের User Record-টা এখানে ছয়ভাবে Encode করা। খেয়াল করো, সবুজ Value-র Byte একই থাকে, আর হলুদ ও নীল কাঠামোর Byte কমতে থাকে:

একটা User Record, ছয় রকম Encoding

নোটের User {id: 101, name: "Rahim", email: "rahim@example.com", isActive: true}। Value সব জায়গায় একই ২৪–২৯ Byte; পার্থক্য হলো তাদের সাথে কতটা কাঠামো যায়।

পরিস্থিতি

Minify করা JSON, API যেভাবে পাঠায়। দুই Space-এর Indent দিয়ে সুন্দর করে লিখলে ৮৬ Byte।

{"id":

Brace + Key "id" · 6

101

৩টা অঙ্কে 101 · 3

এখন পর্যন্ত আকার: 9 Byte (Value 3 · কাঠামো 6)

Field-এর নাম / Key (Text)আসল DataFraming / Header
ধাপ 1 / 4
1

JSON নিজের বর্ণনা নিজেই দেয়: "id" Key-টা Message-এর ভেতরেই যায়, তাই Schema ছাড়াই যে কোনো Reader বুঝতে পারে। দামটা দিতে হয় প্রতিটা Message-এ।

#JSON

JSON (JavaScript Object Notation) আজকের Web-এর সবচেয়ে জনপ্রিয় Data আদান-প্রদানের Format। Browser আর REST API-র কার্যত Standard এটাই, আর প্রায় সব Language-এ এর Parser আগে থেকেই থাকে। নোটের উদাহরণ:

{
  "id": 101,
  "name": "Rahim",
  "email": "rahim@example.com",
  "isActive": true
}

এটা একটা Text, Key-Value Format, আর Schema-ছাড়া (নিজের বর্ণনা নিজেই দেয়): Field-এর নাম Data-র সাথেই যায়, তাই পড়তে আলাদা Schema লাগে না।

সুবিধা (নোট থেকে): খুবই সহজ, মানুষের পড়ার জন্য সেরা, Debug করা পানির মতো সহজ (curl দিয়েই Response পড়ে ফেলা যায়), প্রায় সব জায়গায় আগে থেকেই Support আছে, Browser আর REST-এর ডিফল্ট।

অসুবিধা (নোট থেকে): বাড়তি আকার, কারণ প্রতিটা Message-এ Key আবার পাঠাতে হয় (আমাদের উদাহরণে ২৯ Byte Data-র জন্য ৬৯ Byte); Parse করার খরচ, কারণ Text থেকে Object বানাতে CPU খাটে; আর Schema জোর করে মানানো যায় না, কারণ যেখানে Number আশা করা হচ্ছে সেখানে কেউ String পাঠালে কিছুই আটকায় না (JSON Schema দিয়ে যাচাই যোগ করা যায়, নিচে দেখো)।

#JSON কী বলতে পারে আর কী পারে না

JSON-এ Value মাত্র ছয় রকম: String, Number, Boolean (true/false), Null, Object, Array। বাকি সবকিছু পাঠানো আর পড়ার দুই পাশের মধ্যে একটা মুখের কথা। এখনকার Standard হলো RFC 8259 (২০১৭), যেটা সিস্টেমে সিস্টেমে আদান-প্রদান হওয়া JSON-এর জন্য UTF-8 বাধ্যতামূলক করেছে।

JSON-এ যা নেইমানুষ বদলে যা করেঝামেলাটা
৬৪ bit IntegerString হিসেবে পাঠায়কোন String আসলে সংখ্যা, তা প্রতিটা Client-কে জানতে হয়
Binary DataString-এর ভেতরে base64৩৩% বড়, বাড়তি CPU, পুরো Value Memory-তে
Date আর Time"2026-11-02T10:15:00Z"-এর মতো ISO 8601 StringTime Zone আর নিখুঁততা তোমার দায়
CommentData-য় নেই; Config File-এর জন্য Comment সহ JSON-এর কিছু রূপভেদ আছেStandard Parser এগুলো বাতিল করে
শেষে বাড়তি Commaনেইহাতে Edit করার সময় খুব চেনা ভুল
NaN, Infinitynull বা একটা Stringকিছু Encoder অবৈধ JSON বানিয়ে ফেলে
Key একবারই থাকবেStandard শুধু বলে Key-গুলো উচিত আলাদা হওয়াএকই Key দুবার এলে Parser-রা একমত না (প্রথমটা জেতে বনাম শেষটা জেতে); দুটো Service একই Body আলাদাভাবে পড়লে আক্রমণকারী এটার সুযোগ নেয়

#2⁵³ সমস্যা: JavaScript-এ বড় ID

JSON-এর নিয়মে যত খুশি অঙ্কের Number রাখা যায়, কিন্তু JavaScript প্রতিটা Number Parse করে IEEE 754 Double-এ। Double-এর Mantissa ৫৩ bit, তাই Integer নিখুঁত থাকে শুধু 2⁵³ − 1 = 9,007,199,254,740,991 পর্যন্ত (Number.MAX_SAFE_INTEGER)। Database-এর BIGINT Key, Snowflake ধরনের ID (Timestamp + মেশিন + Counter) আর অনেক Payment বা Order ID এর চেয়ে বড়।

JSON.parse('{"id":1234567890123456789}').id ফেরত দেয় 1234567890123456800। কোনো Error নেই, কোনো সতর্কতা নেই; শুধু একটা আলাদা সংখ্যা, যেটা অন্য কোনো Record-কেও দেখাতে পারে।

Browser-এর JSON.parse দিয়ে ৬৪ bit-এর একটা ID

JavaScript-এর Number হলো IEEE 754 Double: Integer নিখুঁত থাকে শুধু 2⁵³ − 1 = 9,007,199,254,740,991 পর্যন্ত। Database আর Snowflake ধরনের ID প্রায়ই এর চেয়ে বড় হয়।

পরিস্থিতি

BrowserJavaScript
Order APIJava, long ID
GET /orders/latest
ধাপ 1 / 3
1

Page সর্বশেষ Order চায়।

সমাধান, ভালোটা আগে:

  1. ID String হিসেবে পাঠাও ("id": "1234567890123456789")। ID হলো নাম-চিহ্ন, পরিমাণ না। ঠিক এই কারণেই Twitter-এর API id-এর পাশে id_str যোগ করেছিল, আর Protobuf-এর অফিসিয়াল JSON Mapping প্রতিটা int64-কে String হিসেবে লেখে।
  2. Client-কে যদি বড় Integer নিয়ে হিসাব করতেই হয়, এমন Parser দিয়ে BigInt-এ পড়ো যেটা তা সমর্থন করে।
  3. প্রথম দিনেই API Contract-এ এটা ঠিক করো: পরে Number-কে String বানানো একটা Breaking Change (Chapter ২৬)।

#Binary Data আর base64

JSON String মানেই Text, তাই File, ছবি, Hash আর Encrypted Blob-কে base64 করতে হয়: প্রতি ৩ Byte হয় ৪টা Character, মানে ৩৩% বড়, সাথে দুই পাশে CPU খরচ (Lesson ১.৩-এর JWT-ও একই জিনিস, শুধু URL-safe অক্ষর দিয়ে)। কয়েক KB-র বড় যে কোনো কিছুর জন্য Byte-কে Byte হিসেবেই পাঠাও (Raw বা Multipart Body, বা Pre-signed Upload URL), আর JSON-এ রাখো শুধু একটা ID। Binary Format-এ সত্যিকারের bytes Type আছে, শুধু একটা Length Prefix দিয়ে।

#JSON Schema

JSON Schema বলে দেয় একটা বৈধ Document দেখতে কেমন: Type, কোন Field থাকতেই হবে, মানের সীমা, Pattern, আর বাড়তি Key চলবে কি না। এখনকার Version 2020-12, আর OpenAPI 3.1 Request আর Response Body-র জন্য এটাই ব্যবহার করে।

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["id", "name", "email"],
  "properties": {
    "id": { "type": "string", "pattern": "^[0-9]{1,19}$" },
    "name": { "type": "string", "maxLength": 100 },
    "email": { "type": "string", "format": "email" },
    "isActive": { "type": "boolean", "default": true }
  },
  "additionalProperties": false
}

Protobuf-এর মতো না, এখানে Data পড়তে Schema লাগে না; এটা একটা যাচাইয়ের ধাপ, যেটা তুমি চালাবে কি না সেটা তোমার সিদ্ধান্ত, সাধারণত API-র দরজায়। এতে JSON নমনীয় থাকে, আবার এর মানে হলো কেউ Validator লাগিয়ে না দিলে কিছুই জোর করে মানানো হয় না।

#XML

XML (Extensible Markup Language) পুরোনো আর খুব সমৃদ্ধ একটা Markup Language, Enterprise সিস্টেমে ব্যাপকভাবে চলত। নোটের উদাহরণ:

<User>
  <Id>101</Id>
  <Name>Rahim</Name>
  <Email>rahim@example.com</Email>
  <IsActive>true</IsActive>
</User>

এটা Tag-ভিত্তিক একটা Tree: HTML-এর মতোই প্রতিটা Element-এর একটা খোলার আর একটা বন্ধের Tag থাকে। এতে Schema-র Support আছে: একটা XSD (XML Schema Definition) বা পুরোনো DTD (Document Type Definition) দিয়ে Document কড়াভাবে যাচাই করা যায়, Type, ক্রম আর কয়বার আসবে সহ।

সুবিধা (নোট থেকে): জটিল Tree আর Metadata-র জন্য দারুণ; Namespace থাকায় আলাদা সিস্টেমের Document নামের সংঘর্ষ ছাড়াই মেশানো যায়; Attribute দিয়ে Content-এর পাশেই Metadata রাখা যায়।

<order xmlns="urn:example:shop" xmlns:pay="urn:example:payments">
  <user id="101" active="true">Rahim</user>
  <pay:method>card</pay:method>
</order>

এখানে id আর active হলো Attribute, আর pay:method Payments Namespace-এর, তাই দোকানের কোনো method Element-এর সাথে গুলিয়ে যাবে না।

অসুবিধা (নোট থেকে): অতিরিক্ত ভারী, কারণ প্রতিটা নাম দুবার আসে (আমাদের User-এর জন্য ১০০ Byte, JSON-এর প্রায় ১.৪৫ গুণ); Parse করা ভারী, সেটা DOM Parser হোক (পুরো Tree Memory-তে বানায়) বা Streaming SAX Parser (পড়তে পড়তে Event দেয়, Memory কম লাগে কিন্তু Code কঠিন); আর আধুনিক Web API আর Microservice-এ এটা মোটামুটি JSON আর Protobuf-এর কাছে জায়গা ছেড়ে দিয়েছে।

তবে XML মোটেই মরে যায়নি। SOAP Web Service, SAML Single Sign-on, RSS আর Atom Feed, SVG ছবি, Office Document (একটা .docx আসলে XML File-এর একটা Zip), Android Layout, Maven-এর pom.xml, আর Banking ও E-invoicing-এর বড় অংশ (ISO 20022 Payment Message) এখনো XML-এই চলে।

#XXE: যে Parser তোমার File পড়ে ফেলে

XML-এর এমন একটা সুবিধা আছে যা JSON-এ নেই: একটা Document Entity ঘোষণা করতে পারে, মানে নাম দেওয়া Text-এর টুকরো, আর একটা External Entity কোনো URL বা File-এর দিকে দেখাতে পারে। পুরোনো বা ভুলভাবে সেট করা Parser Parse করার সময়েই সেটা এনে ফেলে। এটাই XXE (XML External Entity) আক্রমণ: আক্রমণকারীর Document পড়ে ফেলে /etc/passwd, ভেতরের URL-এ Call করে (Server-side Request Forgery), বা একটার ভেতরে আরেকটা Entity দিয়ে (“Billion Laughs”) কয়েক KB-কে কয়েক GB Memory বানিয়ে ফেলে। সমাধান একটাই Setting: Parser-এ DTD আর External Entity বন্ধ করো। নিচে অন্য Deserialization আক্রমণগুলোর সাথে এটাও Animation-এ আছে।

#Protocol Buffers (Protobuf)

Protocol Buffers বানিয়েছে Google (ভেতরে ভেতরে প্রায় ২০০১ থেকে চলছে, ২০০৮-এ Open Source)। Protobuf Language-নিরপেক্ষ আর Platform-নিরপেক্ষ, খুব দ্রুত একটা Binary Format, আর gRPC-র Message Format। নোটের Schema:

// user.proto
syntax = "proto3";

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  bool is_active = 4;
}

নোটে যেভাবে আছে, এটা কাজ করে এভাবে:

  1. .proto File-এ Schema: আগে Data-র আকার ঠিক করে নিতে হয়।
  2. Field Number (Tag): প্রতিটা Field পায় আলাদা একটা নম্বর (id = 1)। Encode করার সময় নাম কখনো যায় না, যায় শুধু নম্বর।
  3. Code Generation: Compiler protoc Python, Go, Java, C++, C#, Kotlin, Swift আর আরও অনেক Language-এর জন্য Class বানিয়ে দেয়; তোমার Code হাতে Byte না বানিয়ে সেই Class ব্যবহার করে।
protoc --python_out=. --go_out=. user.proto

#Wire-এ: Tag, Wire Type আর Varint

একটা Protobuf Message আসলে Field-এর একটা তালিকা, প্রতিটা লেখা হয় একটা Key, তারপর একটা Value। Key হলো একটা Varint, যার ভেতরে Field Number আর ৩ bit-এর একটা Wire Type:

key = (field_number << 3) | wire_type

Wire Type Reader-কে বলে দেয় Schema না জেনেও Value কোথায় শেষ:

Wire Typeনামকীসের জন্যValue কত লম্বা?
0VARINTint32, int64, uint32, uint64, sint32, sint64, bool, enumযতক্ষণ না ওপরের bit 0 এমন একটা Byte আসে
1I64fixed64, sfixed64, doubleসবসময় ৮ Byte
2LENstring, bytes, ভেতরের Message, Packed Repeated Fieldএকটা Varint Length, তারপর ততগুলো Byte
3, 4SGROUP, EGROUPGroup (বাতিল, শুধু proto2)মিলে যাওয়া End Group পর্যন্ত
5I32fixed32, sfixed32, floatসবসময় ৪ Byte

আমাদের User, Byte ধরে ধরে (মোট ৩০ Byte):

08 65                                   field 1, varint: 101
12 05 52 61 68 69 6d                    field 2, LEN 5: "Rahim"
1a 11 72 61 68 69 6d 40 65 78 61 6d     field 3, LEN 17: "rahim@example.com"
      70 6c 65 2e 63 6f 6d
20 01                                   field 4, varint: 1 (true)

0x08 মানে Field ১, Wire Type ০ (১ × ৮ + ০); 0x12 মানে Field ২, Wire Type ২ (২ × ৮ + ২ = ১৮); 0x1a হলো Field ৩ (২৬); 0x20 হলো Field ৪ (৩২)।

Varint একটা Integer রাখে প্রতি Byte-এ ৭ bit করে, নিচের Group আগে; প্রতিটা Byte-এর ওপরের bit হলো Continuation bit (১ = আরেকটা Byte আসছে)। তাই ০–১২৭ নেয় এক Byte, ১২৮–১৬,৩৮৩ দুই, আর পুরো ৬৪ bit-এর মান দশ পর্যন্ত। চিরচেনা উদাহরণ ৩০০ = 1 0010 1100: নিচের ৭ bit 010 1100, সাথে Continuation bit দিলে হয় ac, বাকি 10 থেকে 02, তাই ৩০০ হলো ac 02।

ফাঁদটা হলো ঋণাত্মক সংখ্যায়। −১ মানের একটা int32 ৬৪ bit পর্যন্ত Sign-extend হয়ে নেয় ১০ Byte। sint32/sint64 Type আগে Zigzag Encoding করে, যেটা ০, −১, ১, −২, ২-কে বানায় ০, ১, ২, ৩, ৪, তাই −১ নেয় এক Byte। যে মান সাধারণত বড় আর ছড়ানো (Hash, এলোমেলো ID), তার জন্য fixed64/fixed32 Varint-এর চেয়ে ছোট।

Varint Encoding, ধাপে ধাপে

Protobuf, Avro আর Thrift Compact সবাই Integer রাখে Varint হিসেবে: প্রতি Byte-এ সংখ্যার ৭ bit, আর সবচেয়ে ওপরের bit বলে আরেকটা Byte আসছে কি না।

পরিস্থিতি

চিরচেনা উদাহরণ: `int32 id = 1` Field-এ রাখা 300।

1 0010 1100

Binary-তে 300 (৯ bit)

আসল DataBinary Tag: Field Number + Type
ধাপ 1 / 6
1

300-এর লাগে ৯ bit। স্থির int32 সবসময় ৪ Byte খরচ করত; Varint খরচ করে যতটা Byte সংখ্যাটার দরকার, ঠিক ততটা।

আরও দুটো জায়গা বাঁচানোর কৌশল: Field Number ১–১৫-এর Key এক Byte (১৬–২০৪৭ নেয় দুই), তাই সবচেয়ে বেশি আসা Field-গুলোকে এগুলো দাও; আর proto3-এ কোনো Field তার Default মানে থাকলে (০, false, খালি String) মোটেই লেখা হয় না। proto3-এ Repeated Number ডিফল্টভাবে Packed: একটা Key, একটা Length, তারপর সব Varint পরপর।

#Field Number-ই আসল Contract

যেহেতু শুধু নম্বর যায়, .proto File-এর Field Number-ই আসল Contract, নামগুলো শুধু Code-এর জন্য। email-এর নাম বদলে email_address করলে Wire-এ কিছুই বদলায় না; কিন্তু email = 3 বদলে email = 5 করলে প্রতিটা Reader ভেঙে যায়। Field Number সর্বোচ্চ ৫৩৬,৮৭০,৯১১ (2²⁹ − 1) পর্যন্ত হতে পারে, আর ১৯০০০–১৯৯৯৯ Protobuf নিজের জন্য রেখে দিয়েছে।

পুরোনো একটা Reader যখন এমন Field Number পায় যেটা সে চেনে না, তখনো Wire Type তাকে বলে দেয় Value কত লম্বা, তাই সে সেটা লাফিয়ে পার হয়, আর protobuf 3.5 (২০১৭) থেকে Message-এর Unknown Field-এ রেখে দেয়। Message আবার Serialize হলে সেই Byte-গুলো হুবহু বেরিয়ে যায়। এর জন্যই নতুন Producer পুরোনো Consumer-এর সাথে কথা বলতে পারে।

পুরোনো Protobuf Reader আর নতুন একটা Field

Producer-এর Version ২ যোগ করেছে `string referral_code = 5`। Version ১-এর Reader Field ৫-এর নামও শোনেনি।

পরিস্থিতি

v1 Code একটা v2 Message (id, name, is_active, referral_code) পড়ছে।

08

Field ১, Varint · 1

65

101 · 1

12

Field ২, Length-delimited · 1

05

Length ৫ · 1

526168696d

Rahim · 5

20

Field ৪, Varint · 1

01

true · 1

Reader যা বানিয়েছে: User{id: 101, name: "Rahim", is_active: true}

এখন পর্যন্ত আকার: 11 Byte (Value 7 · কাঠামো 4)

Binary Tag: Field Number + Typeআসল DataLength Prefixবাদ দেওয়া বা না পড়ে রেখে দেওয়া
ধাপ 1 / 3
1

Field ১, ২ আর ৪ v1 Schema-তে আছে, তাই স্বাভাবিকভাবে Decode হয়।

কী বদলানো যায় আর কী যায় না, আর reserved Keyword-এর বিস্তারিত আছে Lesson ২.১০-এ।

#Default, Presence আর optional

proto3 Default মান পাঠায় না, তাই স্বাভাবিকভাবে Reader বুঝতে পারে না “Field-টা ০ ছিল” নাকি “Field-টা সেটই করা হয়নি”। একটা Counter-এর জন্য এতে সমস্যা নেই, কিন্তু ধরো একটা Discount, যেটা সত্যিই ০ হতে পারে, তার জন্য ভুল। সমাধানগুলো:

  • optional int32 discount = 6; (protobuf 3.15, ২০২১ থেকে proto3-এ ফিরে এসেছে) Field-কে স্পষ্ট Presence দেয়: Generate করা Code-এ has_discount() থাকে, আর স্পষ্টভাবে দেওয়া ০-ও পাঠানো হয়।
  • এর আগে google.protobuf.Int32Value-এর মতো Wrapper Type একই কাজ করত।
  • proto3-এ required নেই। proto2-তে ছিল, আর সেটা Outage ঘটিয়েছে: একটা Required Field কাউকে না ভেঙে আর কখনো সরানো যায় না। দরকারি Business Field Code-এ বা কোনো Validation Library দিয়ে যাচাই করো।
  • নতুন Protobuf Release-গুলো syntax = "proto2" / "proto3"-এর জায়গায় আনছে Edition (edition = "2023";), যেটা এই আচরণগুলোকে File বা Field ধরে ধরে বেছে নেওয়ার Feature বানায়। Wire Format বদলায় না।

#gRPC, JSON Mapping আর Tooling

Protobuf হলো gRPC-র ডিফল্ট Message Format; gRPC এর ওপর যোগ করে Service (rpc GetUser(GetUserRequest) returns (User);), HTTP/2 Transport, Streaming আর Generate করা Client; gRPC নিজে শেখানো হবে Chapter ২৬-এ। Protobuf-এর একটা অফিসিয়াল JSON Mapping-ও আছে, তাই একই Message Browser-কে JSON হিসেবে দেওয়া যায়: Field-এর নাম হয়ে যায় lowerCamelCase (is_active → isActive), int64 হয় String, আর bytes হয় base64।

সুবিধা (নোট থেকে, সংশোধিত): খুব ছোট, কারণ নামের বদলে Tag (এখানে ৬৯-এর বদলে ৩০ Byte); দ্রুত, কারণ খুঁজে খুঁজে পড়ার মতো কোনো Text নেই (সাধারণ JSON Library-র চেয়ে প্রায়ই কয়েক গুণ দ্রুত, তবে কতটা, তা Language, Library আর Data-র ওপর অনেকটাই নির্ভর করে, নিচে দেখো); Type-সহ: Generate করা Code তোমাকে int32 Field-এ String বসাতে দেবে না।

অসুবিধা (নোট থেকে): Binary, তাই Text Editor-এ পড়া যায় না (protoc --decode_raw সাহায্য করে, “নিজে করে দেখো” অংশ দেখো); আর প্রতিটা Build-এ বাড়তি একটা Code Generation ধাপ।

#Apache Avro

Apache Avro Hadoop Project থেকে আসা একটা Binary Serialization Framework, এখন Big Data Pipeline-এ Apache Kafka-র সাথে খুব বেশি ব্যবহার হয়। এর Schema লেখা হয় JSON-এ। নোটের Schema:

{
  "type": "record",
  "name": "User",
  "fields": [
    {"name": "id", "type": "int"},
    {"name": "name", "type": "string"},
    {"name": "email", "type": "string"},
    {"name": "is_active", "type": "boolean"}
  ]
}

Protobuf-এর সাথে আসল পার্থক্য: Avro Data-র সাথে Field-এর নাম, Tag বা Type কিছুই পাঠায় না, শুধু কাঁচা Value পাঠায় একটার পর একটা, Schema-র ক্রমে (নোটের “Protobuf-এর মতো” কথাটা এখানে বিভ্রান্তিকর: Protobuf প্রতিটা Field-এর জন্য একটা Tag পাঠায়)। int আর long হলো Zigzag Varint, String হলো একটা Length আর UTF-8 Byte, Boolean এক Byte। আমাদের User ২৭ Byte: ca 01 (১০১), 0a + “Rahim”, 22 + Email, 01।

এর ফল: Avro পড়তে হলে যে Schema দিয়ে লেখা হয়েছে, ঠিক সেটাই লাগবে (Writer-এর Schema)। সেটা ছাড়া Byte-গুলো অর্থহীন; একটা Field কোথায় শেষ, তাও এতে চিহ্নিত নেই।

#Writer-এর Schema আর Reader-এর Schema

বদলের প্রশ্নে Avro-র উত্তর হলো Schema Resolution। Reader Writer-এর Schema দিয়ে Decode করে, তারপর ফলাফলটা নিজের Reader-এর Schema-তে বসিয়ে নেয়:

  • Field মেলানো হয় নাম ধরে (বা একটা Alias ধরে, নাম বদলানো এভাবেই হয়)।
  • Writer-এর আছে কিন্তু Reader-এর নেই, এমন Field উপেক্ষা হয়।
  • Reader-এর আছে কিন্তু Writer-এর নেই, এমন Field পায় Reader-এর Schema থেকে Default; Default না থাকলে Resolution ব্যর্থ হয়।
  • কিছু Type উন্নীত করা যায়: int → long → float → double, string ↔ bytes।

ঐচ্ছিক Field লেখা হয় null-এর সাথে একটা Union হিসেবে: {"name": "referral_code", "type": ["null", "string"], "default": null}।

#Writer-এর Schema থাকে কোথায়

  • Avro Object Container File (Hadoop, Data Lake): পুরো Schema File-এর Header-এ একবার রাখা থাকে, তারপর Sync Marker দিয়ে আলাদা করা Record-এর Block। Block-গুলো Compress করা যায়, আর File-টা কয়েকটা Worker-এর মধ্যে ভাগ করা যায়; এজন্যই Avro হয়ে উঠেছিল Hadoop দুনিয়ার Row Format।
  • Schema Registry সহ Kafka: প্রতিটা Message-এর সাথে Schema পাঠালে Data-র চেয়ে বেশি খরচ হতো, তাই প্রতিটা Message শুরু হয় ৫ Byte দিয়ে: একটা Magic Byte 0 আর ৪ Byte-এর Schema ID। Consumer সেই ID-র Schema Registry থেকে একবার এনে Cache করে রাখে। নতুন Schema Version Register করার সময় Registry Compatibility যাচাই করে আর ভাঙার মতো বদল ফিরিয়ে দেয়।
  • Avro RPC Handshake-এ Schema আদান-প্রদান করে (আজকাল খুব কম চলে)।

Kafka-য় Avro: Writer Schema, Reader Schema আর Registry

Avro Byte-এ কোনো Field-এর নাম বা Tag নেই। Reader Writer-এর Schema দিয়ে Decode করে, তারপর নিজের Schema-র সাথে নাম ধরে ধরে Field মিলিয়ে নেয়।

পরিস্থিতি

v2 যোগ করেছে `referral_code` (Type ["null","string"], Default null)। Consumer এখনো v1-এ চলছে।

ProducerWriter Schema v2
Schema Registry
Kafka Topicusers
ConsumerReader Schema v1
Schema v2 Register
ধাপ 1 / 6
1

প্রথম ব্যবহারে Producer-এর Serializer তার Schema-কে users-value Subject-এ Register করে।

Avro-তে Code Generation বাধ্যতামূলক না: Generic Record দিয়ে ব্যবহার করা যায় (Schema থেকে Run Time-এ বানানো Map-এর মতো একটা Object), যেটা এমন Pipeline-এ মানায় যেখানে নানা রকম Schema আসে, যেমন Kafka Connect, Spark আর Data Lake। Generate করা Class (“Specific Record”)-ও আছে। (নোটে একে বলা হয়েছে “Dynamic Typing”; Data কিন্তু তখনো Schema দিয়ে কড়াভাবে Type-বাঁধা।)

সুবিধা (নোট থেকে, সংশোধিত): খুব ঠাসা, কারণ Data-য় শুধু Value; আর Schema Registry থাকলে Kafka Message Schema-র বদলে বয়ে নেয় ৫ Byte-এর একটা ID। নোট বলছে Avro “বর্তমানে সবচেয়ে ছোট Binary Format”: আমাদের মতো ঘন Record-এ প্রায়ই সবচেয়ে ছোট (২৭ Byte, Protobuf-এর ৩০), কিন্তু সবসময় না। Avro প্রতিটা int Zigzag করে (১০১-এ লাগে ২ Byte, ১ না), আর প্রতিটা ঐচ্ছিক Field null হলেও অন্তত ১ Byte-এর একটা Union Index নেয়, যেখানে Protobuf সেট না করা Field-এর জন্য কিছুই লেখে না। অনেক খালি ঐচ্ছিক Field-ওয়ালা পাতলা Record-এ Protobuf প্রায়ই ছোট।

অসুবিধা (নোট থেকে): Reader-কে Writer-এর Schema (বা সমতুল্য একটা) জানতেই হবে, নইলে Data অর্থহীন। এতে Registry (বা File Header) হয়ে যায় একটা জরুরি নির্ভরতা।

#Apache Thrift

Apache Thrift বানিয়েছিল Facebook (২০০৭-এ Open Source), পরে দিয়ে দেয় Apache Software Foundation-কে। এটা একটা নানা Language-এর মধ্যে চলা RPC আর Serialization Framework। Protobuf-এর মতোই এটা নম্বর দেওয়া Field সহ একটা IDL (Interface Definition Language) File ব্যবহার করে। নোটের উদাহরণ, সাথে যে Service সাধারণত থাকে:

// user.thrift
struct User {
  1: i32 id,
  2: string name,
  3: string email,
  4: bool isActive
}

service UserService {
  User getUser(1: i32 id)
}

এটা শুধু একটা Format না। thrift Compiler Data Class আর পুরো RPC Client ও Server বানিয়ে দেয়, খুলে বদলানো যায় এমন কয়েকটা Layer দিয়ে:

Layerকী কী আছেকী ঠিক করে
ProtocolBinary, Compact, JSONValue কীভাবে Byte হবে (আমাদের User: Binary-তে ৪৮ Byte, Compact-এ ৩১)
TransportBuffered, Framed, Memory, HTTP, FileByte কীভাবে Frame হবে আর যাবে (নোটের “Buffered, Framed, Memory”)
ServerSimple, Threaded, Thread Pool, Non-blockingRequest কীভাবে সামলানো হবে (Non-blocking Server-এর Framed Transport লাগে)

Thrift: Protocol-এর ভেতরে তোমার Struct, তার বাইরে Transport

Thrift একটা পুরো RPC Stack। Value কীভাবে Byte হবে তা ঠিক করে Protocol; Byte কীভাবে Frame হবে আর কীভাবে যাবে তা ঠিক করে Transport। দুটোই Code-এ বেছে নিতে হয়, নিজেরা দরদাম করে ঠিক হয় না।

পরিস্থিতি

Framed Transport-এর ওপর Binary Protocol, Server-এর সাধারণ সেটআপ।

Struct User1: i32 id, 2: string name, 3: string email, 4: bool isActive
ধাপ 1 / 5
1

Generate করা Client getUser(42)-কে একটা Call বানায়। Data Type আসে .thrift IDL File থেকে, যেটা thrift Compiler দিয়ে প্রতিটা Language-এর জন্য Compile করা।

সুবিধা (নোট থেকে): একটা IDL থেকেই Serialization আর পুরো Client ও Server তৈরি হয়; কয়েক রকম Transport-এর Support আছে।

অসুবিধা (নোট থেকে, হালনাগাদ): Protobuf-এর চেয়ে ছোট Community আর কঠিন Documentation। আজ Thrift বেশি দেখা যায় ২০১০-এর দশকে বানানো সিস্টেমে (Facebook-এর নিজের Fork, fbthrift; Twitter-এর Finagle Stack; Cassandra আর HBase-এর পুরোনো Interface; Hadoop যুগের Tool)। নতুন Project প্রায় সবসময় Protobuf আর gRPC বেছে নেয়।

#আরও যেসব Format-এর সাথে দেখা হবে

  • MessagePack: “Binary JSON”। একই Map আর List, কোনো Schema নেই, কিন্তু Quote আর যতিচিহ্নের বদলে Type Byte আর Length: আমাদের User ৫০ Byte। যেখানে JSON চলছে সেখানে সহজে বসানো যায় (Redis-এর Lua Script, Fluentd Log, কিছু Real-time App)।
  • CBOR (Concise Binary Object Representation): MessagePack-এর মতো ধারণার একটা IETF Standard (RFC 8949), ছোট Device-এর কথা ভেবে বানানো; IoT Protocol-এ আর Passkey-তে ব্যবহার হয়, যার Authenticator Data CBOR।
  • BSON: MongoDB-র Storage আর Wire Format। JSON-এ নেই এমন Type যোগ করে (Date, ৬৪ bit Integer, Binary, Object ID), আর Length রাখে যাতে Document দ্রুত ঘুরে দেখা যায়; সাধারণত JSON-এর চেয়ে ছোট না।
  • Zero-copy Format, FlatBuffers আর Cap’n Proto: Byte-গুলো এমনভাবে সাজানো যে জায়গাতেই পড়া যায়, Object বানানোর মতো কোনো Parse ধাপ ছাড়া। বড় একটা Message-এর একটা Field পড়তে প্রায় কোনো খরচই নেই, যেটা Game, বড় Read-only Data লোড করা Mobile App আর Machine Learning Model File-এর জন্য মানায় (FlatBuffers এসেছে Google থেকে, Device-এর ভেতরে চলা Machine Learning Model-এ ব্যবহার হয়; Cap’n Proto বানিয়েছেন Protobuf-এর আগের প্রধান, চলে Cloudflare Workers-এর ভেতরে)। খরচ: Message বড় (Alignment, Offset), আর API একটু অসুবিধাজনক।
  • Columnar Format, Parquet আর ORC: প্রতিটা Row-এর বদলে প্রতিটা Column একসাথে রাখে, এমন Analytics-এর জন্য যেটা কোটি কোটি Row-এর মাত্র কয়েকটা Column পড়ে। এটা Storage Design-এর বিষয়: Chapter ৪২।
  • YAML: নোটের তৃতীয় Text Format। হাতে লেখা Configuration-এর জন্য দারুণ (Kubernetes, CI Pipeline), Wire-এ খারাপ: Parse করতে ধীর, Indent-এর ওপর নির্ভরশীল, আর চমকে ভরা (পুরোনো YAML 1.1 Parser-এ দেশের Code NO হয়ে যায় false)।
  • Language-এর নিজস্ব Format: Java Serialization, Python-এর pickle, PHP-র serialize, .NET-এর BinaryFormatter (এখন .NET থেকে বাদ)। এক Program-এর ভেতরে সুবিধাজনক, কিন্তু বিশ্বাসের যে কোনো সীমানা পেরোলে বিপজ্জনক (পরের অংশ), আর অন্য Language থেকে পড়া যায় না।

#আকার আর গতি: সৎ সংখ্যা

নোটের User Record-এর আকার, প্রতিটা Byte ধরে হিসাব করা:

FormatByteমন্তব্য
XML১০০<?xml …?> Declaration সহ ১৩৮; Indent দিলে আরও
JSON৬৯সুন্দর করে সাজালে ৮৬
MessagePack৫০Key তখনো আছে
Thrift Binary Protocol৪৮স্থির মাপের int আর Length
Thrift Compact Protocol৩১Varint, Field ID-র দূরত্ব
Protobuf৩০Tag + Varint
Avro২৭Kafka Registry-র Prefix সহ ৩২

আর ১,০০০ জন একই রকম User-এর জন্য (বানানো Test Data):

কাঁচাgzipBrotli
JSON৭৯.৯ KB৯.৪ KB৫.০ KB
Protobuf৩৯.৯ KB৯.৫ KBপ্রযোজ্য না

Interview-তে গতি নিয়ে সৎভাবে যা বলবে:

  • Format-এর চেয়ে অনেক বেশি নির্ভর করে Language আর Library-র ওপর। Java, Go বা C++-এর প্রচলিত Library-তে Protobuf Decode সাধারণত JSON-এর চেয়ে ২–১০ গুণ দ্রুত (নোটের “৫–১০ গুণ” এই সীমার একটা বিন্দু, কোনো নিয়ম না)।
  • simdjson-এর মতো দ্রুত JSON Parser প্রতি সেকেন্ডে কয়েক GB Parse করে, অনেক Protobuf Decoder-এর চেয়েও দ্রুত।
  • Browser-এ JSON.parse Native C++-এ লেখা আর খুব দ্রুত; JavaScript-এর Protobuf Library প্রায়ই এর চেয়ে দ্রুত না, লাভটা আসে ছোট Payload থেকে।
  • Zero-copy Format Parse করাই বাদ দেয়, তাই Field ছোঁয়ার আগ পর্যন্ত “Decode”-এর সময় প্রায় শূন্য।
  • নিজের Data দিয়ে মেপে দেখো: যে Record-এ String বেশি, সেখানে Binary-তে লাভ কম (String সব জায়গায় সমান খরচের); যেখানে Number বেশি আর অনেক ছোট ছোট Message, সেখানে লাভ অনেক।

#নিরাপত্তা: অবিশ্বস্ত Data কখনো যে কোনো Type-এ Deserialize করবে না

JSON আর Protobuf-এর মতো Data Format শুধু Value বানাতে পারে: Number, String, List, আর তোমার Code যে একটা Type চেয়েছে সেটা। কিছু Format আবার যে কোনো Class-এর যে কোনো Object বর্ণনা করতে পারে, আর সেই Object আবার বানাতে গিয়ে Code চলে। আক্রমণকারীর হাতে পড়লে চলে আক্রমণকারীর Code।

অনিরাপদ Deserialization: যখন Data-ই ঠিক করে কোন Code চলবে

কিছু Format যে কোনো Class-এর যে কোনো Object বর্ণনা করতে পারে, আর সেটা আবার বানাতে গিয়ে Code চলে। আক্রমণকারীর Byte পেলে চলে আক্রমণকারীর Code।

পরিস্থিতি

একটা Import Endpoint Pickle করা Object নেয়, কারণ লিখতে সহজ ছিল।

আক্রমণকারী
API Server
Server OS
POST /import (বানানো Pickle)
ধাপ 1 / 3
1

Pickle শুধু Data না: এটা একটা ছোট্ট Program, যার নির্দেশ মেনে Object আবার বানানো হয়, আর একটা নির্দেশ বলতে পারে “এই Function-কে এই Argument দিয়ে Call করো”।

বিপদউদাহরণপ্রতিরক্ষা
যে কোনো Object-এ DeserializePython pickle.loads, Java ObjectInputStream.readObject (Classpath-এর Library দিয়ে Gadget Chain), .NET BinaryFormatter, PHP unserialize, অনিরাপদ YAML Loaderঅবিশ্বস্ত Input-এ কখনো না। একটা নির্দিষ্ট Type-এ JSON বা Protobuf ব্যবহার করো; একান্তই দরকার হলে Class-এর অনুমোদিত তালিকা দাও (Java-র Deserialization Filter), আর yaml.safe_load ব্যবহার করো
XML External EntityXXE দিয়ে File পড়া, ভেতরের Request, Billion Laughs দিয়ে Memory ফাটানোParser-এ DTD আর External Entity বন্ধ করো
বহুরূপী JSONএমনভাবে সেট করা JSON Library, যে "@type" Field-এ লেখা Class বানিয়ে ফেলেDefault Typing বন্ধ করো; Subtype-এর অনুমোদিত তালিকা দাও
সম্পদ ফুরিয়ে ফেলা১ GB-র Body, ১,০০,০০০ স্তর গভীর JSON (Stack Overflow), বিশাল String বা ArrayBody-র আকারের সীমা, গভীরতার সীমা, Schema-য় Length-এর সীমা
Parser-এ Parser-এ গরমিলএকই Key দুবার বা অদ্ভুত Number, যা Gateway আর Service আলাদাভাবে পড়েসীমানায় একটাই Parser আর একটাই আদর্শ রূপ; Duplicate বাতিল করো

অনিরাপদ Deserialization OWASP Top 10-এ বারবার জায়গা পায় (এখন এটা Software and Data Integrity Failures-এর ভেতরে), আর Chapter ৩৪-এ অন্য Web আক্রমণের সাথে আবার শেখানো হবে।

#Format বেছে নেওয়া

নোটের তুলনার ছক, এই Lesson-এর সংশোধন সহ:

JSONXMLProtobufAvroThrift
ধরনTextTextBinaryBinaryBinary (বা JSON Protocol)
পড়তে Schema লাগে?না (JSON Schema ঐচ্ছিক)না (XSD ঐচ্ছিক)হ্যাঁ (নইলে শুধু নম্বর আর কাঁচা Value)হ্যাঁ, ঠিক Writer-এর Schemaহ্যাঁ
Wire-এ Field চেনার উপায়নামনাম (দুবার)নম্বরকিছুই না (অবস্থান)নম্বর
আকার (আমাদের User)মাঝারি, ৬৯ Bখুব বড়, ১০০ Bছোট, ৩০ Bএখানে সবচেয়ে ছোট, ২৭ Bছোট, ৩১ B (Compact)
Parse-এর গতিমাঝারি (ভালো Library-তে দ্রুত)ধীরদ্রুতদ্রুতদ্রুত
Schema Evolutionহাতে, নিয়ম মেনেহাতে, XSD Versionসহজ: Field Number, অচেনা Field রেখে দেয়চমৎকার: Reader/Writer মেলানো, Registry-র যাচাইসহজ: Field ID
সাধারণ ব্যবহারPublic Web API, REST, Browser, Configপুরোনো আর Enterprise Integration, SOAP, SAML, Document, ConfigMicroservice, gRPC, MobileKafka, Hadoop, Data Pipelineপুরোনো Stack-এ নানা Language-এর RPC

কাজের একটা মোটা দাগের নিয়ম:

সীমানাভালো ডিফল্টকেন
Public API, Partner, BrowserOpenAPI / JSON Schema সহ JSON, সাথে HTTP Compressionযে কোনো Client পড়তে পারে; Debug করা যায়; Compression আকারের ফারাকের বেশিটাই মুছে দেয়
ভেতরের Service থেকে Service RPCgRPC-র ওপর Protobufছোট, দ্রুত, Type-সহ, Generate করা Client; .proto-ই Contract
Event আর Stream (Kafka)Schema Registry সহ Avro বা Protobuf (JSON Schema-ও চলে)অনেক Reader, অনেক দিন ধরে; Data লেখার আগেই Compatibility জোর করে মানানো
Analytics StorageParquet (বা ORC); Row ধরে নামানো File-এর জন্য AvroColumn ধরে Scan আর Compression (Chapter ৪২)
CacheService যেটা আগে থেকে ব্যবহার করছে; Memory বাঁচাতে Protobuf বা MessagePackCache-এর Memory দামি; Key বা Value-তে একটা Version রাখো
Config FileYAML, JSON বা TOMLমানুষ লেখে, কালেভদ্রে পড়া হয়
Game, WebSocket-এ Real-time BinaryProtobuf, MessagePack বা FlatBuffersপ্রতি সেকেন্ডে অনেক ছোট Message
সীমিত ক্ষমতার DeviceCBOR বা Protobufছোট্ট Code আর ছোট্ট Message

কোথায় কোন Format: একটা দোকান, চারটা Format

আসল সিস্টেমে Format মেশানো থাকে। প্রতিটা সীমানা সেই Format বাছে, যেটা মানায় কে পড়ে, কতবার পড়ে, আর কতদিন রাখে তার সাথে।

পরিস্থিতি

Browser
API Gateway
Order Service
Payment Service
Mobile App
Kafka
Data Lake
JSON

সীমানা

Format

JSON (+ gzip / br)

Reader

যে কোনো Browser, Partner, curl

Contract

OpenAPI + JSON Schema

ধাপ 1 / 2
1

যে বাইরের Client তোমার নিয়ন্ত্রণে নেই, তারা পায় JSON: সব Language আর Tool এটা পড়ে, চোখে দেখে Debug করা যায়, আর HTTP Compression ভালোভাবে ছোট করে। এখানে আকারের চেয়ে সবার কাছে পৌঁছানো বেশি জরুরি।

#Interview-এর প্রশ্ন

  • “ভেতরে gRPC/Protobuf আর বাইরে JSON কেন?” ভেতরে দুই প্রান্তই তোমার হাতে, আর বেশি Call-এর হারে প্রতিটা Byte আর CPU-র খরচ দিতে হয়; বাইরে সবার কাছে পৌঁছানো আর Debug করতে পারাটা বেশি জরুরি, আর Compression আকারের ফারাক কমিয়ে দেয়।
  • “তোমার JavaScript Client ভুল Order ID দেখাচ্ছে।” 2⁵³-এর বড় ৬৪ bit ID JSON.parse-এ নিখুঁততা হারায়: String হিসেবে পাঠাও।
  • “Consumer না ভেঙে একটা Field যোগ করবে কীভাবে?” Protobuf: নতুন Field Number, পুরোনো Reader লাফিয়ে পার হয় আর রেখে দেয়। Avro: Default সহ যোগ করো; Registry Compatibility যাচাই করে। বিস্তারিত Lesson ২.১০-এ।
  • “Registry ছাড়া Consumer Avro পড়তে পারে না কেন?” Data-য় কোনো Tag নেই; শুধু Writer-এর Schema-ই Byte-গুলোকে মানে দেয়।
  • “Redis-এ Pickle করা Object রাখা কি নিরাপদ?” শুধু তখনই, যদি অবিশ্বস্ত কেউ কোনোদিন সেই Redis-এ লিখতে না পারে। একটা Data Format-ই ভালো।

#নিজে করে দেখো

# ১. 2^53 সমস্যা (Node.js)
node -e 'console.log(JSON.parse("{\"id\":1234567890123456789}").id)'
#   1234567890123456800
node -e 'console.log(Number.MAX_SAFE_INTEGER)'
#   9007199254740991

# ২. JSON-এর আকার, আর একটা ছোট Message-এ gzip কী করে
printf '%s' '{"id":101,"name":"Rahim","email":"rahim@example.com","isActive":true}' | wc -c          # 69
printf '%s' '{"id":101,"name":"Rahim","email":"rahim@example.com","isActive":true}' | gzip -c | wc -c   # প্রায় 89

# ৩. protoc দিয়ে User Encode করে ৩০ Byte দেখো (আগে নোটের user.proto Save করে নাও)
printf 'id: 101 name: "Rahim" email: "rahim@example.com" is_active: true' \
  | protoc --encode=User user.proto | xxd
#   00000000: 0865 1205 5261 6869 6d1a 1172 6168 696d  .e..Rahim..rahim
#   00000010: 4065 7861 6d70 6c65 2e63 6f6d 2001       @example.com .

# ৪. Schema ছাড়া Decode: নাম না, পাবে শুধু নম্বর
printf 'id: 101 name: "Rahim" email: "rahim@example.com" is_active: true' \
  | protoc --encode=User user.proto | protoc --decode_raw
#   1: 101
#   2: "Rahim"
#   3: "rahim@example.com"
#   4: 1

# ৫. Varint হিসেবে 300
printf 'id: 300' | protoc --encode=User user.proto | xxd     # 08ac 02

# ৬. Pickle একটা Program, Data না (নিরাপদ: এটা শুধু ভেঙে দেখায়)
python3 -c "import pickle, pickletools; pickletools.dis(pickle.dumps({'id': 101}))"

Avro আর Registry দিয়ে Encode করা Kafka Message দেখতে Confluent-এর kafka-avro-console-consumer (বা Registry চেনে এমন যে কোনো Tool) সেগুলো Decode করে দেয়; সাধারণ একটা Consumer দেখায় ৫ Byte-এর Prefix আর তারপর পড়া যায় না এমন Byte।

#সারসংক্ষেপ

মূল শিক্ষা

  • Serialization (Marshalling) Memory-র Object-কে Network, Disk, Cache আর Queue-এর জন্য Byte বানায়; Deserialization সেগুলো থেকে নতুন Object বানায়। দুই পাশ একমত হয় শুধু Format-এ, Language বা Memory-র সাজানোয় কখনো না।
  • Format আলাদা হয় Text বনাম Binary আর Schema-ছাড়া বনাম Schema-ভিত্তিক হিসেবে। Field চেনা যায় নাম দিয়ে (JSON, XML, MessagePack), নম্বর দিয়ে (Protobuf, Thrift) বা অবস্থান দিয়ে (Avro)।
  • JSON: সবখানে চলে আর পড়া যায়, কিন্তু ৬৪ bit Integer নেই (নিখুঁত শুধু 2⁵³ − 1 পর্যন্ত: বড় ID String হিসেবে পাঠাও), Binary নেই (base64 +৩৩%), Date নেই, Comment নেই। JSON Schema যাচাই করে; পড়তে সেটা লাগে না।
  • XML: ভারী (নাম দুবার) আর Parse করতে খাটুনি, কিন্তু সমৃদ্ধ (Namespace, Attribute, XSD); XXE ঠেকাতে External Entity বন্ধ রাখো।
  • Protobuf: key = (field number << 3) | wire type; Varint (৩০০ = ac 02), sint-এর জন্য Zigzag, Default পাঠানো হয় না, Presence-এর জন্য optional। Field Number-ই Contract; পুরোনো Reader অচেনা Field লাফিয়ে পার হয় আর রেখে দেয়। কোনো নম্বর আবার ব্যবহার করবে না।
  • Avro: কোনো Tag নেই, Schema-র ক্রমে শুধু Value; পড়ো Writer-এর Schema দিয়ে, মেলাও Reader-এর Schema-র সাথে নাম ধরে, ফাঁক ভরায় Default; Schema থাকে File Header-এ বা একটা Registry-তে (Kafka-য় ৫ Byte-এর Prefix)।
  • Thrift: IDL + পুরো RPC Stack, খুলে বদলানো যায় এমন Protocol (Binary, Compact, JSON) আর Transport (Buffered, Framed…) সহ, যা দুই প্রান্তে হুবহু একই সেট করতে হয়।
  • সৎ সংখ্যা: আমাদের User ১০০ / ৬৯ / ৫০ / ৩১ / ৩০ / ২৭ Byte (XML, JSON, MessagePack, Thrift Compact, Protobuf, Avro); বড় Payload-এ Compression ফারাকের বেশিটাই মুছে দেয়; গতি Library-র ওপর নির্ভর করে, তাই মেপে দেখো।
  • অবিশ্বস্ত Data কখনো যে কোনো Type-এ Deserialize করবে না (pickle, Java Serialization, BinaryFormatter, অনিরাপদ YAML): নির্দিষ্ট Type-এ Decode করো আর যাচাই করো।
  • সীমানা দেখে বাছো: Public API-তে JSON, ভেতরে Protobuf/gRPC, Event-এ Registry সহ Avro বা Protobuf, Analytics-এ Columnar File।