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 Call | HTTP API-র Body, gRPC Message, WebSocket Frame |
| Storage | Database-এর Row বা Document, Log-এর লাইন, Backup, Data Lake-এর File |
| Cache | Redis বা Memcached-এ রাখা User Profile |
| Queue আর Stream | Kafka 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 হিসেবে।
Memory-তে Object-টা একটা Struct: তাতে String-এর জন্য Heap-এর অন্য জায়গার Pointer, Field-এর মাঝে Padding, আর Go-র নিজের ঠিক করা সাজানো। অন্য কোনো Process এই Memory পড়তে পারে না, আর পারলেও Java Program এই সাজানো বুঝত না।
প্রতিটা Format-কে একই পাঁচটা প্রশ্নের উত্তর দিতে হয়, আর এই উত্তরগুলোই তার আকার, গতি আর নিরাপত্তার প্রায় সব কিছু ব্যাখ্যা করে:
- কোন কোন Type আছে? কত বড় Integer, Float, String, Bytes, Boolean, Null, Date, Map, List।
- একটা Field চেনা যায় কী দিয়ে? নাম দিয়ে (JSON, XML, MessagePack), নম্বর দিয়ে (Protobuf, Thrift), নাকি Schema-য় তার অবস্থান দিয়ে (Avro)।
- একটা Value কোথায় শেষ? সীমানা-চিহ্ন দিয়ে, যেগুলো Escape করতে হয় (Quote, Comma, বন্ধ করার Tag), নাকি একটা Length Prefix দিয়ে (Binary Format)।
- Schema কোথায় থাকে? প্রতিটা Message-এর ভেতরে (নিজের বর্ণনা নিজেই দেয়), Code-এ Compile করা আলাদা একটা File-এ, নাকি একটা Registry-তে।
- Text নাকি Binary? মানুষ Byte-গুলো পড়তে পারে, নাকি শুধু Program?
#দুটো মাপকাঠি: Text বনাম Binary, Schema-ছাড়া বনাম Schema-ভিত্তিক
নোটে Format-গুলোকে দুই ভাগে ভাগ করা হয়েছে:
- মানুষের পড়ার মতো Text Format: JSON, XML, YAML। পড়া আর Debug করা সহজ, কিন্তু আকারে বড় আর Parse করতে সময় বেশি লাগে।
- Binary Format: Protocol Buffers, Apache Avro, Apache Thrift। মানুষ সরাসরি পড়তে পারে না, কিন্তু খুব ছোট, Memory-তে সাশ্রয়ী আর Process করতে দারুণ দ্রুত।
আরেকটা মাপকাঠিও সমান জরুরি: Data কি নিজের Field-এর নাম নিজেই বয়ে নেয়, নাকি Reader-এর একটা Schema লাগে?
| Schema-ছাড়া (নিজের বর্ণনা নিজেই দেয়) | Schema-ভিত্তিক | |
|---|---|---|
| Text | JSON, XML, YAML | JSON Schema বা XSD দিয়ে যাচাই করা JSON বা XML (Schema ঐচ্ছিক, Data তখনো নাম বয়ে নেয়) |
| Binary | MessagePack, 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)
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 Integer | String হিসেবে পাঠায় | কোন String আসলে সংখ্যা, তা প্রতিটা Client-কে জানতে হয় |
| Binary Data | String-এর ভেতরে base64 | ৩৩% বড়, বাড়তি CPU, পুরো Value Memory-তে |
| Date আর Time | "2026-11-02T10:15:00Z"-এর মতো ISO 8601 String | Time Zone আর নিখুঁততা তোমার দায় |
| Comment | Data-য় নেই; Config File-এর জন্য Comment সহ JSON-এর কিছু রূপভেদ আছে | Standard Parser এগুলো বাতিল করে |
| শেষে বাড়তি Comma | নেই | হাতে Edit করার সময় খুব চেনা ভুল |
NaN, Infinity | null বা একটা 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 প্রায়ই এর চেয়ে বড় হয়।
পরিস্থিতি
Page সর্বশেষ Order চায়।
সমাধান, ভালোটা আগে:
- ID String হিসেবে পাঠাও (
"id": "1234567890123456789")। ID হলো নাম-চিহ্ন, পরিমাণ না। ঠিক এই কারণেই Twitter-এর APIid-এর পাশেid_strযোগ করেছিল, আর Protobuf-এর অফিসিয়াল JSON Mapping প্রতিটাint64-কে String হিসেবে লেখে। - Client-কে যদি বড় Integer নিয়ে হিসাব করতেই হয়, এমন Parser দিয়ে BigInt-এ পড়ো যেটা তা সমর্থন করে।
- প্রথম দিনেই 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;
}
নোটে যেভাবে আছে, এটা কাজ করে এভাবে:
.protoFile-এ Schema: আগে Data-র আকার ঠিক করে নিতে হয়।- Field Number (Tag): প্রতিটা Field পায় আলাদা একটা নম্বর (
id = 1)। Encode করার সময় নাম কখনো যায় না, যায় শুধু নম্বর। - Code Generation: Compiler
protocPython, 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 কত লম্বা? |
|---|---|---|---|
| 0 | VARINT | int32, int64, uint32, uint64, sint32, sint64, bool, enum | যতক্ষণ না ওপরের bit 0 এমন একটা Byte আসে |
| 1 | I64 | fixed64, sfixed64, double | সবসময় ৮ Byte |
| 2 | LEN | string, bytes, ভেতরের Message, Packed Repeated Field | একটা Varint Length, তারপর ততগুলো Byte |
| 3, 4 | SGROUP, EGROUP | Group (বাতিল, শুধু proto2) | মিলে যাওয়া End Group পর্যন্ত |
| 5 | I32 | fixed32, 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)
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)
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-এ চলছে।
প্রথম ব্যবহারে 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 | কী কী আছে | কী ঠিক করে |
|---|---|---|
| Protocol | Binary, Compact, JSON | Value কীভাবে Byte হবে (আমাদের User: Binary-তে ৪৮ Byte, Compact-এ ৩১) |
| Transport | Buffered, Framed, Memory, HTTP, File | Byte কীভাবে Frame হবে আর যাবে (নোটের “Buffered, Framed, Memory”) |
| Server | Simple, Threaded, Thread Pool, Non-blocking | Request কীভাবে সামলানো হবে (Non-blocking Server-এর Framed Transport লাগে) |
Thrift: Protocol-এর ভেতরে তোমার Struct, তার বাইরে Transport
Thrift একটা পুরো RPC Stack। Value কীভাবে Byte হবে তা ঠিক করে Protocol; Byte কীভাবে Frame হবে আর কীভাবে যাবে তা ঠিক করে Transport। দুটোই Code-এ বেছে নিতে হয়, নিজেরা দরদাম করে ঠিক হয় না।
পরিস্থিতি
Framed Transport-এর ওপর Binary Protocol, Server-এর সাধারণ সেটআপ।
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 ধরে হিসাব করা:
| Format | Byte | মন্তব্য |
|---|---|---|
| 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):
| কাঁচা | gzip | Brotli | |
|---|---|---|---|
| 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.parseNative 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 নেয়, কারণ লিখতে সহজ ছিল।
Pickle শুধু Data না: এটা একটা ছোট্ট Program, যার নির্দেশ মেনে Object আবার বানানো হয়, আর একটা নির্দেশ বলতে পারে “এই Function-কে এই Argument দিয়ে Call করো”।
| বিপদ | উদাহরণ | প্রতিরক্ষা |
|---|---|---|
| যে কোনো Object-এ Deserialize | Python 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 Entity | XXE দিয়ে File পড়া, ভেতরের Request, Billion Laughs দিয়ে Memory ফাটানো | Parser-এ DTD আর External Entity বন্ধ করো |
| বহুরূপী JSON | এমনভাবে সেট করা JSON Library, যে "@type" Field-এ লেখা Class বানিয়ে ফেলে | Default Typing বন্ধ করো; Subtype-এর অনুমোদিত তালিকা দাও |
| সম্পদ ফুরিয়ে ফেলা | ১ GB-র Body, ১,০০,০০০ স্তর গভীর JSON (Stack Overflow), বিশাল String বা Array | Body-র আকারের সীমা, গভীরতার সীমা, Schema-য় Length-এর সীমা |
| Parser-এ Parser-এ গরমিল | একই Key দুবার বা অদ্ভুত Number, যা Gateway আর Service আলাদাভাবে পড়ে | সীমানায় একটাই Parser আর একটাই আদর্শ রূপ; Duplicate বাতিল করো |
অনিরাপদ Deserialization OWASP Top 10-এ বারবার জায়গা পায় (এখন এটা Software and Data Integrity Failures-এর ভেতরে), আর Chapter ৩৪-এ অন্য Web আক্রমণের সাথে আবার শেখানো হবে।
#Format বেছে নেওয়া
নোটের তুলনার ছক, এই Lesson-এর সংশোধন সহ:
| JSON | XML | Protobuf | Avro | Thrift | |
|---|---|---|---|---|---|
| ধরন | Text | Text | Binary | Binary | Binary (বা 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, Config | Microservice, gRPC, Mobile | Kafka, Hadoop, Data Pipeline | পুরোনো Stack-এ নানা Language-এর RPC |
কাজের একটা মোটা দাগের নিয়ম:
| সীমানা | ভালো ডিফল্ট | কেন |
|---|---|---|
| Public API, Partner, Browser | OpenAPI / JSON Schema সহ JSON, সাথে HTTP Compression | যে কোনো Client পড়তে পারে; Debug করা যায়; Compression আকারের ফারাকের বেশিটাই মুছে দেয় |
| ভেতরের Service থেকে Service RPC | gRPC-র ওপর Protobuf | ছোট, দ্রুত, Type-সহ, Generate করা Client; .proto-ই Contract |
| Event আর Stream (Kafka) | Schema Registry সহ Avro বা Protobuf (JSON Schema-ও চলে) | অনেক Reader, অনেক দিন ধরে; Data লেখার আগেই Compatibility জোর করে মানানো |
| Analytics Storage | Parquet (বা ORC); Row ধরে নামানো File-এর জন্য Avro | Column ধরে Scan আর Compression (Chapter ৪২) |
| Cache | Service যেটা আগে থেকে ব্যবহার করছে; Memory বাঁচাতে Protobuf বা MessagePack | Cache-এর Memory দামি; Key বা Value-তে একটা Version রাখো |
| Config File | YAML, JSON বা TOML | মানুষ লেখে, কালেভদ্রে পড়া হয় |
| Game, WebSocket-এ Real-time Binary | Protobuf, MessagePack বা FlatBuffers | প্রতি সেকেন্ডে অনেক ছোট Message |
| সীমিত ক্ষমতার Device | CBOR বা Protobuf | ছোট্ট Code আর ছোট্ট Message |
কোথায় কোন Format: একটা দোকান, চারটা Format
আসল সিস্টেমে Format মেশানো থাকে। প্রতিটা সীমানা সেই Format বাছে, যেটা মানায় কে পড়ে, কতবার পড়ে, আর কতদিন রাখে তার সাথে।
পরিস্থিতি
সীমানা
Format
JSON (+ gzip / br)
Reader
যে কোনো Browser, Partner, curl
Contract
OpenAPI + JSON Schema
যে বাইরের 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।
আগে নিজে উত্তর দাও, তারপর ব্যাখ্যাটা পড়ো। ঠিক হলেও পড়ো, কারণ ভুল Option-গুলো কেন ভুল, সেটাও ওখানে আছে।
নিজেকে যাচাই করোপ্রশ্ন
Deserialization (Unmarshalling) কী করে?
নিজেকে যাচাই করোপ্রশ্ন
একটা দল Memory থেকে সরাসরি একটা C Struct Socket দিয়ে একটা Java Service-এ পাঠায়। Byte অক্ষত পৌঁছায়, কিন্তু id Field পড়া হয় 101-এর বদলে 1694498816। সবচেয়ে সম্ভাব্য কারণ কী?
নিজেকে যাচাই করোপ্রশ্ন
Text আর Binary Format নিয়ে কোন দুটো কথা ঠিক?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
একটা Browser App একটা Java API থেকে পায় {orderId: 9007199254740993}, তারপর সেই Order চায়। API উত্তর দেয় 404। কেন?
নিজেকে যাচাই করোপ্রশ্ন
Browser যে Public JSON API ব্যবহার করে, তাতে ৬৪ bit ID-র জন্য দীর্ঘমেয়াদে সবচেয়ে ভালো সমাধান কোনটা?
নিজেকে যাচাই করোপ্রশ্ন
একটা Mobile App ৬ MB-র একটা ছবি JSON Body-র ভেতরে base64 করে Upload করে। Request Body মোটামুটি কত বড়?
নিজেকে যাচাই করোপ্রশ্ন
JSON Schema নিয়ে কোন কথাটা ঠিক?
নিজেকে যাচাই করোপ্রশ্ন
একটা Gateway একই JSON Key দুবার এলে প্রথম মানটা রাখে, আর তার পেছনের Service রাখে শেষটা। আক্রমণকারী কী করতে পারে?
নিজেকে যাচাই করোপ্রশ্ন
XML-এর এমন কোন সুবিধা আছে যা JSON-এ নেই?
নিজেকে যাচাই করোপ্রশ্ন
একটা Partner Integration পুরোনো একটা Parser-এ XML পাঠায়। আক্রমণকারীর Document file:///etc/passwd-এর দিকে দেখানো একটা Entity বানায়, আর Response-এ File-টা চলে আসে। এটা কোন আক্রমণ, আর সমাধান কী?
নিজেকে যাচাই করোপ্রশ্ন
Protobuf-এ Field Number ৩ আর Wire Type ২ (Length-delimited)-এর Key Byte কোনটা?
নিজেকে যাচাই করোপ্রশ্ন
300 সংখ্যাটা Protobuf Varint হিসেবে কীভাবে Encode হয়?
নিজেকে যাচাই করোপ্রশ্ন
একটা তাপমাত্রার Field int32 হিসেবে ঘোষণা করা, আর প্রায়ই ছোট ঋণাত্মক মান রাখে। Message আশার চেয়ে বড় হচ্ছে। কোন বদলে সবচেয়ে বেশি লাভ?
নিজেকে যাচাই করোপ্রশ্ন
কোন দুটো সিদ্ধান্ত Protobuf Message ছোট করে?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
একটা v1 Protobuf Reader এমন Message পেল যাতে Field Number ৭ আছে, Wire Type ২, যেটা তার Schema-য় নেই। কী হয়?
নিজেকে যাচাই করোপ্রশ্ন
Pipeline-এর মাঝখানের একটা v1 Service Protobuf Message পড়ে, একটা Field বদলায়, তারপর সামনে লিখে পাঠায়। পরের v2 Service-গুলো হঠাৎ নতুন যোগ করা একটা Field হারাচ্ছে, কিন্তু শুধু তখনই যখন মাঝখানের Service protobuf 3.2-এ চলে। কেন?
নিজেকে যাচাই করোপ্রশ্ন
একটা দল int32 age = 5 মুছে ফেলেছিল, কয়েক মাস পরে যোগ করেছে int32 zip_code = 5। কিছু Service এখনো পুরোনো Schema-য় চলে। কী হয়?
নিজেকে যাচাই করোপ্রশ্ন
optional Keyword ছাড়া proto3-এ একটা Reader দেখে discount = 0। সে কী বুঝতে পারে?
নিজেকে যাচাই করোপ্রশ্ন
Required Field নিয়ে কোন কথাটা ঠিক?
নিজেকে যাচাই করোপ্রশ্ন
Writer-এর Schema ছাড়া একটা Kafka Consumer Avro Message Decode করতে পারে না, অথচ অচেনা Protobuf Field পার হয়ে যেতে পারত। কেন?
নিজেকে যাচাই করোপ্রশ্ন
একটা Avro Consumer Reader Schema v1 ব্যবহার করে। Message আসছে v2 দিয়ে লেখা, যেটা Default null সহ referral_code যোগ করেছে। Schema Resolution কী করে?
নিজেকে যাচাই করোপ্রশ্ন
Confluent-এর Avro Serializer দিয়ে বানানো Kafka Message-এর ৫ Byte-এর Prefix-এ কোন দুটো জিনিস থাকে?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
একজন Developer Default ছাড়া string Type-এর একটা country Field Avro Schema-য় যোগ করে Producer Deploy করে। Registry Backward Compatibility-তে চলছে। কী হয়?
নিজেকে যাচাই করোপ্রশ্ন
Schema Registry বন্ধ। কোন Consumer-রা ক্ষতিগ্রস্ত?
নিজেকে যাচাই করোপ্রশ্ন
কোন Record-এ Protobuf সম্ভবত Avro-র চেয়ে ছোট হবে?
নিজেকে যাচাই করোপ্রশ্ন
একটা Thrift Client Buffered Transport ব্যবহার করে আর Server ব্যবহার করে Framed Transport। Call ব্যর্থ হয় এই Error দিয়ে: frame size (2147549185) larger than max length। কেন?
নিজেকে যাচাই করোপ্রশ্ন
Thrift নিয়ে কোন কথাটা ঠিক?
নিজেকে যাচাই করোপ্রশ্ন
একটা Game প্রতিটা Player-এর জন্য প্রতি সেকেন্ডে ৬০টা ছোট State Update পাঠায় WebSocket দিয়ে, আর Payload-কে Schema Build ধাপ ছাড়াই Map হিসেবে পড়ার মতো রাখতে হবে। কোন Format সবচেয়ে মানানসই?
নিজেকে যাচাই করোপ্রশ্ন
FlatBuffers আর Cap’n Proto-র মতো Zero-copy Format কী দেয়?
নিজেকে যাচাই করোপ্রশ্ন
একটা Web API gzip চালু রেখে ২ MB-র JSON Response দেয়। একজন সহকর্মী Bandwidth বাঁচাতে Protobuf-এ যাওয়ার প্রস্তাব দিল। সবচেয়ে সঠিক প্রত্যাশা কোনটা?
নিজেকে যাচাই করোপ্রশ্ন
কোন দুটো Service Deserialization দিয়ে দূর থেকে Code চালানোর ঝুঁকিতে আছে?
বেছে নাও ২
নিজেকে যাচাই করোপ্রশ্ন
JSON নেয় এমন একটা Public Endpoint-এর জন্য কোনটা বুদ্ধিমানের প্রতিরক্ষা না?
নিজেকে যাচাই করোপ্রশ্ন
একটা E-commerce কোম্পানি তাদের Data Format ঠিক করছে। কোন সমন্বয়টা এই Lesson-এর পরামর্শ মানে?