ข้าม​ไป​ยัง​เนื้อหา

ออ​เด​อร์​ใบ​หนึ่ง​เป็น byte แปด​ตัว และ​ไม่มี​ชื่อ field อยู่​ใน​นั้น​เลย

บริการ​ที่​เขียน​ด้วย Go ส่ง​ออ​เด​อร์​ไป​ให้​บริการ​ที่​เขียน​ด้วย Java สอง​ฝั่ง​นี้​คนละ​ทีม คนละ repo และ​ไม่​เคย​ขึ้น production พร้อม​กัน​สัก​ครั้ง

สิ่ง​เดียว​ที่​ผูก​สอง​ฝั่ง​ไว้​ด้วย​กัน​คือ byte ที่​วิ่ง​อยู่​ระหว่าง​มัน คอร์ส​นี้​เริ่ม​จาก​การ​อ่าน byte เหล่า​นั้น​ให้​ออก​ก่อน แล้ว​ค่อย​ถาม​ว่า​แก้ schema แบบ​ไหน​ที่​ทำให้​อีก​ฝั่ง​อ่าน​ผิด

📦 orderctl — ของ​ชิ้น​เดียวกัน​กับ​สาม​คอร์ส​ภาษา​ใน​เสา​นี้

orderctl คือ​เครื่อง​มือ​ประมวล​ผล​ออ​เด​อร์​ที่​สาม​คอร์ส​ภาษา​ใน​เสา​นี้​สร้าง​ซ้ำ​ที​ละ​ภาษา repo ตัวอย่าง กำลัง​จัด​ทำ ระหว่าง​นี้​ทุก​ตัวเลข​ใน​คอร์ส​นี้​รัน​เอง​ได้ สั่ง scripts/wire-contract/up.sh เพื่อ​ดึง​เครื่อง​มือ​ที่​ตรึง version ไว้ แล้ว​ตาม​ด้วย python3 scripts/wire-contract/probe.py

ออ​เด​อร์​ที่​มีชื่อ​ลูกค้า​และ​จำนวน​จาน ขึ้น​สาย​เป็น byte แปด​ตัว

Order ทรง​ตั้ง​ต้น​ของ​คอร์ส​นี้​มี​สอง​ช่อง ช่อง​หนึ่ง​เป็น​ข้อความ อีก​ช่อง​เป็น​จำนวนเต็ม

order.proto
syntax = "proto3";
package order.v1;
message Order {
string customer = 1;
int32 quantity = 2;
}

ฝั่ง Go ใส่​ค่า​ลง​ไป​สอง​ค่า คือ customer เป็น wira และ quantity เป็น 7 แล้ว​สั่ง proto.Marshal ผล​ที่​ได้​คือ 8 byte เขียน​เป็น hex ได้​แบบ​นี้

0a04776972611007

แปด byte นี้​คือ​ทั้งหมด​ที่​เดิน​ทาง​ไป ไม่มี​วงเล็บ ไม่มี​เครื่องหมาย​คั่น และ​ไม่มี​คำ​ว่า customer หรือ quantity อยู่​ใน​นั้น​เลย​สัก​ตัว​อักษร

byte แปด​ตัว​แบ่ง​เป็น​สอง​กลุ่ม กลุ่ม​ละ​หนึ่ง​ค่า และ​แต่ละ​กลุ่ม​ขึ้น​ต้น​ด้วย tag

รูป​แบบ​บน​สายwire formatรูป​แบบ​ของ byte ที่ protobuf ใช้​จริง​บน​สาย · แต่ละ field คือ tag แล้ว​ตาม​ด้วย​ค่า และ​ไม่มี​ชื่อ field เดิน​ทาง​ไป​ด้วย​สัก​ตัวของ protobuf วาง​ของ​เรียง​กัน​เป็นก​ลุ่ม กลุ่ม​หนึ่ง​คือ​หนึ่ง​ค่า​ของ​ช่อง​หนึ่ง​ช่อง และ​ไม่มี​อะไร​คั่น​ระหว่าง​กลุ่ม ตัว​ที่​บอก​ว่า​กลุ่ม​ใหม่​เริ่ม​แล้ว​คือ tag ที่​อยู่​หัว​กลุ่ม

byteค่าความ​หมาย
10atagtagค่าที่​อยู่​หัว​ของ​แต่ละ​กลุ่ม​บน​สาย ซึ่ง​รวม​หมายเลข​กับ wire type ไว้​ด้วย​กัน · สูตร​ของ​มัน​คือ (หมายเลข << 3) | wire type · ตัว​มัน​เอง​เป็น varint หมายเลข 1 ถึง 15 จึง​ยาว byte เดียว ส่วน​หมายเลข 16 ยาว​สอง byte · ยิ่ง​หมายเลข​ใหญ่ tag ยิ่ง​ยาว ของ field หมายเลข 1 ชนิด length-delimitedlength-delimitedwire type 2 · ส่ง​ความ​ยาว​ก่อน​หนึ่ง byte แล้ว​ตาม​ด้วย byte ตาม​จำนวน​นั้น · string, bytes และ message ที่​ซ้อน​กัน​ใช้​รูป​นี้
204ความ​ยาว​ของ​ค่า คือ 4 byte
3–677 69 72 61ตัว​อักษร w i r a
710tag ของ field หมายเลข 2 ชนิด​ตัวเลข​แบบ varintvarintการ​เข้า​รหัส​จำนวนเต็ม​ที่​ใช้ byte เท่า​ที่​ค่า​นั้น​ต้องการ · เลข 7 กิน byte เดียว ส่วน int32 ที่​เป็น −1 กิน​สิบ byte
807ค่า​คือ 7

ช่อง​ข้อความ​ต้อง​บอก​ความ​ยาว​มา​ด้วย เพราะ​ผู้​อ่าน​ไม่มี​ทาง​รู้​เอง​ว่า​ข้อความ​จบ​ตรง​ไหน ส่วน​ช่อง​ตัวเลข​ไม่​ต้อง​บอก เพราะ varint บอก​จุดจบ​ไว้​ใน​ตัว​มัน​เอง​แล้ว

tag คือหมายเลข​ของ fieldfield numberหมายเลข​ประจำ field ที่​ประกาศ​ไว้​ใน schema · เป็น​สิ่ง​เดียว​ที่​ทั้ง​สอง​ฝั่ง​ใช้​จับ​คู่​ค่า​เข้า​กับ​ช่อง เปลี่ยน​ชื่อ​จึง​ฟรี แต่​เปลี่ยน​หมายเลข​คือ​เปลี่ยน​สัญญากับชนิด​บน​สายwire typeสาม​บิต​ท้าย​ของ tag ที่​บอก​ว่า​ค่า​ถัด​ไป​มี​รูป​ร่าง​แบบ​ไหน · wire type 0 คือ varint และ wire type 2 คือ length-delimited อัด​รวม​กัน​ด้วย​สูตร​เดียว

สูตร​คือ​เลื่อน​หมายเลข​ไป​ทาง​ซ้าย​สาม bit แล้ว​เอา​ชนิด​มา​ต่อ​ท้าย

tag = (หมายเลข << 3) | ชนิด

ลอง​กับ byte จริง​ทั้ง​สอง​ตัว​ที่​แกะ​ได้​ข้าง​บน

  • 0x0a คือ 10 ใน​ระบบ​สิบ · 10 >> 3 ได้ 1 และ 10 & 7 ได้ 2 · จึง​เป็น field 1 ชนิด 2
  • 0x10 คือ 16 ใน​ระบบ​สิบ · 16 >> 3 ได้ 2 และ 16 & 7 ได้ 0 · จึง​เป็น field 2 ชนิด 0

ชนิด​มี​ที่​อยู่​แค่​สาม bit จึง​มี​ได้​ไม่​เกิน​แปด​ค่า ที่​นิยาม​ไว้​จริง​มี​หก​ค่า และ​ใน​นั้น​เลิก​ใช้​ไป​สอง​ค่า สอง​ค่าที่​คอร์ส​นี้​ใช้​คือ 0 สำหรับ varint และ 2 สำหรับ​ของ​ที่​บอก​ความ​ยาว​นำ​หน้า

ย้าย​หมายเลข​จาก 15 ไป 16 ทำให้​ทุก message ยาว​ขึ้น​หนึ่ง byte ตลอด​ไป

ตัวอย่าง​ข้าง​บน​ใช้​หมายเลข 1 กับ 2 ซึ่ง tag ลง​ได้​ใน byte เดียว​ทั้ง​คู่ แต่​นั่น​เป็น​เรื่อง​ของ หมายเลข​ที่​เลือก ไม่ใช่​กฎ​ของ tag

วัด​ได้​ด้วย​การ​เลื่อน​หมายเลข​ของ quantity ที​ละ​ขั้น โดย​ไม่​แตะ​อย่าง​อื่น​เลย

หมายเลข​ของ quantitytag บน​สายทั้ง​ใบ
2108 byte
15788 byte
1680019 byte

หมายเลข 15 คือ​ตัว​สุดท้าย​ที่​ยัง​ลง​ได้​ใน byte เดียว เพราะ (15 << 3) | 7 ได้ 127 ซึ่ง​เป็น​ค่า​มาก​ที่สุด​ที่ varint หนึ่ง byte ถือ​ได้ ตัว​ถัด​ไป​จึง​ต้อง​ใช้​สอง byte

ช่อง​ความ​ยาว​ก็​เป็น varint ด้วย​เหตุผล​เดียวกัน ชื่อ​ลูกค้า​ที่​ยาว 130 ตัว​อักษร​ทำให้​ช่อง​ความ​ยาว กลาย​เป็น 8201 สอง byte และ​ทั้ง​ใบ​เป็น 135 byte

นี่​คือ​ที่มา​ของ​คำ​แนะนำ​ว่า​ให้​เก็บ​หมายเลข 1 ถึง 15 ไว้​ให้​ช่อง​ที่​ส่ง​บ่อย​ที่สุด ราคา​ไม่​ได้​จ่าย​ครั้ง​เดียว มัน​จ่าย​ทุก message

flowchart LR
  B["byte 0a"] --> S["เลื่อนขวา 3 bit"]
  B --> M["และกับ 7"]
  S --> N["หมายเลข field = 1"]
  M --> T["ชนิดบนสาย = 2"]
  N --> R["ช่อง customer"]
  T --> R

คำ​บรรยาย​ภาพ: byte เดียว​ถูก​อ่าน​สอง​ครั้ง ครั้ง​แรก​ได้​หมายเลข​ของ field ครั้ง​ที่​สอง​ได้​ชนิด​ของ​มัน แล้ว​สอง​ค่า​นี้​รวม​กัน​จึง​ชี้​ไป​ที่​ช่อง​เดียว​ใน schema

เปลี่ยน customer เป็น customer_name แล้ว byte ทั้ง​แปด​ตัว​เหมือน​เดิม​ทุก​ตัว

schema ทรง​ที่​สอง​ต่าง​จาก​ทรง​แรก​ที่​ชื่อ​ของ field หมายเลข 1 อย่าง​เดียว หมายเลข​เท่า​เดิม ชนิด​เท่า​เดิม และ​ค่าที่​ใส่​ก็​เท่า​เดิม

เปลี่ยน​แค่​ชื่อ ไม่​แตะ​หมายเลข — เฉพาะ​ส่วน message
message Order {
string customer_name = 1;
int32 quantity = 2;
}

Go เขียน​ออก​มา​ได้ 0a04776972611007 ซึ่ง​เท่ากับ​ของ​ทรง​แรก​ทุก​ตัว​อักษร และ​เมื่อ​ให้ Java ที่​ถือ schema ชื่อ​ใหม่​อ่าน byte ที่​ทรง​ชื่อ​เก่า​เขียน​ไว้ มัน​ได้ customer_name เป็น wira กับ quantity เป็น 7

ผู้​อ่าน​ไม่​ได้​ผิด​หวัง​กับ​ชื่อ​ที่​ไม่​ตรง เพราะ​มัน​ไม่​เคย​เห็น​ชื่อ​ของ​ผู้​เขียน​ตั้งแต่​แรก มัน​เห็น​แค่​ว่า​มี​ของ​มา​ที่​ช่อง​หมายเลข 1 แล้ว​มัน​เปิด schema ของ​ตัว​เอง​ดู​ว่า​ช่อง​นั้น​ชื่อ​อะไร

ผู้​เขียน​คอร์ส​เปลี่ยน​ชื่อ field ทุก​ตัว​ใน​ทุก​ทรง​พร้อม​กัน แล้ว hex 11 จาก 12 ค่า​ไม่​ขยับ​เลย

ตอน​แรก​คอร์ส​นี้​ใช้​โดเมน​คลินิก ช่อง​ทั้ง​สาม​ชื่อ patient room และ clinic ต่อ​มา​ย้าย​มา​ใช้ โด​เม​นอ​อเด​อร์ ชื่อ​ทั้ง​สาม​จึง​กลาย​เป็น customer quantity และ branch

ตอน​นั้น​ตาราง WRITES ใน file ตัวเลข​ที่ probe เขียน​ไว้ ถือ hex อยู่ 12 ค่า วัด​ทั้ง​ก่อน​และ​หลัง​การ​ย้าย ผล​คือ 11 ค่า​ไม่​ขยับ​สัก​ตัว ผู้​อ่าน​ตรวจ​เอง​ได้​ด้วย​การ​เทียบ git show bd44452 กับ git show ac61437

ค่าที่​ขยับ​มี​ค่า​เดียว และ​มัน​เป็น​ค่า​เดียว​ที่​เปลี่ยนค่า​ข้าง​ใน ไม่ใช่​เปลี่ยน​ชื่อ คือ​ข้อความ clinic-b ที่​กลาย​เป็น branch-b

การ​แก้​ครั้ง​นั้น​ไม่​ได้​ตั้งใจ​จะ​เป็นการ​ทดลอง แต่​มัน​วัด​กฎ​ของ​บท​นี้​ได้​ตรง​กว่า​การ​ทดลอง​ที่​ตั้งใจ

ชื่อ​เปลี่ยน​ได้​ฟรี เพราะ​มัน​ไม่​เคย​อยู่​บน​สาย และ​หมายเลข​เปลี่ยน​ไม่​ได้ ด้วย​เหตุผล​เดียวกัน

ด้าน​ที่​ได้​เปล่า​คือ​ชื่อ ทีม​หนึ่ง​เปลี่ยน customer เป็น customer_name วัน​นี้​ได้​เลย โดย​ไม่​ต้อง​รอ​อีก​ทีม เพราะ​อีก​ทีม​ไม่​เคย​อ่าน​ชื่อ​นั้น

ด้าน​ที่​ต้อง​จ่าย​คือ​หมายเลข หมายเลข 1 ใน​ทีม​ของ​คุณ​กับ​หมายเลข 1 ใน​ทีม​อีก​ฝั่ง ต้อง​หมาย​ถึง ของ​อย่าง​เดียวกัน​ตลอด​ไป และ​ไม่มี​ใคร​ใน​สอง​ทีม​เห็น​ข้อ​ตกลง​นี้​จาก code ที่​ตัว​เอง​เขียน

บท​ที่ 2 ถาม​คำถาม​ที่มา​ก่อน​เรื่อง​หมายเลข คือ field ที่​มี​ค่า​เป็น​ค่า​เริ่ม​ต้น มัน​เดิน​ทาง​ไป​ด้วย​หรือ​เปล่า ส่วน​คำถาม​เรื่อง​หมายเลข​ที่​ถูก​ใช้​ซ้ำ รอ​อยู่​ที่ บท​ที่ 4


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้
  • Protocol Buffers — Encoding (เข้า​ถึง 2026-09-04) — ที่มา​ของ​สูตร tag และ​ของ​รายการ wire type

  • Protocol Buffers — Language Guide (proto 3) (เข้า​ถึง 2026-09-04) — ที่มา​ของ​กฎ​ว่า​หมายเลข field คือ​สิ่ง​ที่​ผูก​สัญญา​ไว้ ไม่ใช่​ชื่อ

  • ตัวเลข​ทุก​ตัว​ใน​บท​นี้​มา​จาก scripts/wire-contract/probe.py ซึ่ง​ให้ Go เขียน byte แล้ว​ให้ Java อ่าน​กลับ ต้อง​สั่ง scripts/wire-contract/up.sh ก่อน​หนึ่ง​ครั้ง ซึ่ง​ดึง​เครื่อง​มือ​สาม​ตัว​ที่​ตรึง version ไว้

    • protoc 36.1
    • protoc-gen-go v1.36.12
    • protobuf-java 4.36.1

เช็กความเข้าใจ — บทที่ 1

ข้อ 1 / 3

byte ตัวแรกของออเดอร์คือ `0a` มันบอกอะไรกับผู้อ่าน