Buffer
ทำไม Buffer ถึงมีอยู่
หัวข้อที่มีชื่อว่า “ทำไม Buffer ถึงมีอยู่”JavaScript string ถูกเก็บภายในเป็น UTF-16 — สองไบต์ต่อหนึ่ง code point ใน Basic Multilingual Plane นั่นก็โอเคสำหรับข้อความ แต่งาน networking และ filesystem ต้องจัดการกับ byte ดิบ: TCP packet, ภาพ JPEG หรือ binary protocol frame ไม่มี encoding โดยธรรมชาติ
Buffer คือคำตอบของ Node: ลำดับ byte ที่มีความยาวคงที่ ถูก expose เป็น subclass ของ Uint8Array มันมีมาก่อน typed array และยังเป็น type มาตรฐานที่คุณจะเห็นตลอดใน stream, HTTP, crypto และ fs API ของ Node
การสร้าง Buffer
หัวข้อที่มีชื่อว่า “การสร้าง Buffer”วิธีที่แนะนำคือ Buffer.from() constructor new Buffer() ถูก deprecate แล้ว (DEP0005) — มันยังทำงานได้และพิมพ์ DeprecationWarning แต่อย่าใช้ใน code ใหม่เด็ดขาด
Output (verified on Node 24)
buf1: <Buffer 48 65 6c 6c 6f>
buf1 length: 5
buf2: <Buffer de ad be ef>
buf2 length: 4
buf3 (zeroed): <Buffer 00 00 00 00>Buffer.alloc vs Buffer.allocUnsafe
หัวข้อที่มีชื่อว่า “Buffer.alloc vs Buffer.allocUnsafe”Buffer.alloc(size) zero-fill หน่วยความจำก่อนส่งมอบให้คุณ — ปลอดภัย แต่เสียต้นทุนเขียนทับทุกไบต์ Buffer.allocUnsafe(size) ข้าม zero-fill นั้นไปและให้ byte ที่นั่งอยู่ในพื้นที่ memory pool นั้นอยู่แล้วมาให้คุณเลย — เร็วกว่า แต่ byte เหล่านั้นอาจเป็น อะไรก็ได้ รวมถึงเศษข้อมูลที่หลงเหลือจาก buffer ที่ถูกปล่อยไปก่อนหน้านี้ (request body เก่า, เนื้อหาไฟล์เก่า, ความลับเก่า)
Output (verified on Node 24)
alloc(8): <Buffer 00 00 00 00 00 00 00 00>
allocUnsafe(8) length: 8
allocUnsafe(8) is a Buffer: true
after fill(0): <Buffer 00 00 00 00 00 00 00 00>
now equal to alloc(8): trueตัวอย่างประกอบเท่านั้น — byte จริงด้านล่างจะแตกต่างกันทุกครั้งที่รัน ซึ่งนั่นคือประเด็นทั้งหมด:
// @skip-verify nondeterministic memory contents — illustrates allocUnsafe's danger, not something with a fixed expected outputvar mystery = Buffer.allocUnsafe(8);console.log(mystery);// <Buffer 00 00 00 00 8f 12 40 55> <- or anything else. Never rely on this.Encodings: utf8, hex, base64
หัวข้อที่มีชื่อว่า “Encodings: utf8, hex, base64”toString(encoding) แปลง Buffer กลับเป็น string ใน encoding ที่ต้องการ
Output (verified on Node 24)
utf8 : Node.js
hex : 4e6f64652e6a73
base64: Tm9kZS5qcw==
decoded: Node.jsbase64url — encoding ที่ปลอดภัยสำหรับ URL/cookie/header
หัวข้อที่มีชื่อว่า “base64url — encoding ที่ปลอดภัยสำหรับ URL/cookie/header”base64 มาตรฐานอาจมี +, / และ padding = ต่อท้าย — ทั้งสามตัวต้อง escape ใน URL, cookie หรือ HTTP header base64url (Node 15.7+) คือ encoding เดียวกันแต่ใช้ alphabet ที่ปลอดภัยสำหรับ URL (-/_ แทน +//) และไม่มี padding ซึ่งเป็น format เดียวกับที่ JWT ใช้สำหรับส่วน header และ payload
Output (verified on Node 24)
header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSJ9
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFkYSJ9.SIGNATURE
standard base64: //++
base64url : __--
decoded payload: { sub: '1234567890', name: 'Ada' }Byte length เทียบกับ string length
หัวข้อที่มีชื่อว่า “Byte length เทียบกับ string length”Output (verified on Node 24)
ascii chars: 5
ascii bytes: 5
thai chars: 6
thai bytes (utf8): 18
emoji .length: 7
emoji bytes (utf8): 9การ Slice และ copy
หัวข้อที่มีชื่อว่า “การ Slice และ copy”buf.slice(start, end) (หรือ buf.subarray) คืนค่า view ของ memory เดียวกัน — แก้ไข slice ก็แก้ไขต้นฉบับด้วย ใช้ Buffer.from(slice) เพื่อได้ copy อิสระ
Output (verified on Node 24)
slice: Hello
buf after mutation: Jello, World!
copy: world
buf unchanged: Jello, World!StringDecoder: ถอดรหัสตัวอักษร multi-byte ที่ถูกตัดข้าม chunk
หัวข้อที่มีชื่อว่า “StringDecoder: ถอดรหัสตัวอักษร multi-byte ที่ถูกตัดข้าม chunk”network stream ส่งมอบ byte เป็น chunk ตามที่ OS หยิบยื่นมาให้ — มันไม่รู้เลยว่าขอบเขตของตัวอักษร UTF-8 อยู่ตรงไหน ถ้าตัวอักษร multi-byte (เช่นตัวอักษรไทยส่วนใหญ่ที่ใช้ 3 ไบต์ต่อตัวใน UTF-8) ถูกตัดข้ามสอง chunk การเรียก .toString('utf8') บนแต่ละ chunk แยกกัน จะทำให้มันเสียหาย: แต่ละครึ่งเป็น byte sequence ที่ไม่สมบูรณ์ ทั้งสองครึ่งจึงถอดรหัสออกมาเป็นตัวอักษรทดแทนของ Unicode (�)
StringDecoder จาก node:string_decoder แก้ปัญหานี้: มัน buffer ไบต์ที่ตกค้างไม่สมบูรณ์และต่อเข้ากับ chunk ถัดไป ทำให้ตัวอักษร multi-byte จะถูกถอดรหัสก็ต่อเมื่อมันสมบูรณ์แล้วเท่านั้น
Output (verified on Node 24)
full string: สวัสดี
total bytes: 18
--- naive: toString() per chunk ---
chunk1.toString(): "ส�"
chunk2.toString(): "��ัสดี"
naive concat: ส���ัสดี
--- correct: StringDecoder buffers partial characters ---
decoded: สวัสดี
matches original: trueMental Model
หัวข้อที่มีชื่อว่า “Mental Model”flowchart LR A["Buffer.alloc(n)"] -->|"zero-fills first"| Z["all bytes 0x00 — safe to read immediately"] B["Buffer.allocUnsafe(n)"] -->|"skips zero-fill"| G["whatever was in the memory pool — must overwrite before reading"] G -->|".fill() or write every byte"| Z S["multi-byte char split across two chunks"] -->|"toString() per chunk"| Corrupt["each half is incomplete -> replacement character"] S -->|"StringDecoder.write() per chunk"| Correct["partial bytes carried to next chunk -> correct string"]
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ทางเลือก | ข้อดี | ต้นทุน |
|---|---|---|
Buffer.alloc(n) | ปลอดภัยเสมอ — memory ที่ zero แบบ deterministic | เสียต้นทุน zero-fill แม้ว่าคุณกำลังจะเขียนทับทุกอย่างอยู่แล้วก็ตาม |
Buffer.allocUnsafe(n) | ข้าม zero-fill — เร็วกว่าใน hot path | เนื้อหายังไม่ถูกกำหนดจนกว่าคุณจะเขียนทับ; ลืมทำแล้วรั่วไหลข้อมูล heap เก่า |
view จาก .slice()/.subarray() | zero-copy — ไม่มีการจอง memory ใหม่ | แก้ไข view ก็แก้ไข buffer ต้นฉบับด้วย ซึ่งพลาดได้ง่ายโดยไม่ตั้งใจ |
.toString() ต่อ chunk | code ง่ายที่สุดเท่าที่จะทำได้ | ทำให้ตัวอักษร multi-byte ที่ถูกตัดข้าม chunk เสียหายแบบเงียบ ๆ |
StringDecoder | ถูกต้องข้ามขอบเขต chunk | มี object เพิ่มอีกหนึ่งตัวที่ต้องพกไปกับ code จัดการ stream ของคุณ |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “
Buffer.allocUnsafeก็แค่Buffer.allocที่เร็วกว่า” — ความจริง: มันเร็วกว่าเพราะข้ามการ zero memory; byte ที่ได้กลับมาคือข้อมูลที่หลงเหลืออยู่ ไม่ใช่ zero จนกว่าคุณจะเขียนทับมันเอง - “base64 กับ base64url คือสิ่งเดียวกันแค่ชื่อต่างกัน” — ความจริง: base64url แทน
-/_ด้วย+//และตัด padding ออก โดยเฉพาะเพื่อให้ผลลัพธ์ปลอดภัยสำหรับฝังใน URL, cookie หรือ header โดยไม่ต้อง escape - “
chunk.toString('utf8')ปลอดภัยเสมอสำหรับ text stream” — ความจริง: มันปลอดภัยก็ต่อเมื่อขอบเขต chunk ไม่เคยตัดตัวอักษร multi-byte — ซึ่ง network stream ไม่สามารถรับประกันได้ - “
buf.slice()คืนค่า copy เหมือนArray.prototype.slice()” — ความจริง:Buffer#slice/#subarrayคืนค่า view เหนือ memory เดียวกัน; แก้ไข slice ก็แก้ไขต้นฉบับด้วย
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”Buffer.allocUnsafe()ถูกใช้ในที่ที่ buffer ถูกอ่าน (log, ส่งออก, hash) ก่อนที่ทุกไบต์จะถูกเขียนหรือ.fill()อย่างชัดเจนหรือเปล่า?- stream handler เรียก
chunk.toString('utf8')บนแต่ละ chunk แยกกัน แทนที่จะสะสมด้วยBuffer.concat()หรือถอดรหัสด้วยStringDecoderหรือเปล่า? แบบนี้จะพังทันทีที่ตัวอักษร multi-byte (ไทย, emoji, CJK) คร่อมขอบเขต chunk - code คำนวณขนาด string ด้วย
str.lengthแทนที่จะเป็นBuffer.byteLength(str, 'utf8')ก่อนจอง buffer ขนาดคงที่หรือตั้งค่า headerContent-Lengthหรือเปล่า? input ที่ไม่ใช่ ASCII จะล้นหรือถูกตัดทอน - JWT หรือ URL token ถูกสร้างด้วย
base64มาตรฐานแทนที่จะเป็นbase64urlซึ่งต้อง escape+///=ด้วยมือและพลาดง่ายหรือเปล่า?
🐛 หา bug ในโค้ดนี้
const { Transform } = require('node:stream');
class Uppercase extends Transform {_transform(chunk, encoding, callback) { // chunk is a raw Buffer — this decodes each piece independently var text = chunk.toString('utf8'); callback(null, text.toUpperCase());}}เฉลย
_transform รับ Buffer chunk ดิบโดยไม่มีการรับประกันว่าขอบเขต chunk จะเคารพขอบเขตตัวอักษร UTF-8 การเรียก chunk.toString('utf8') บนแต่ละ chunk แยกกันจะทำให้ตัวอักษร multi-byte (ไทย, emoji, CJK) ที่บังเอิญถูกตัดข้ามสอง chunk เสียหาย — ความล้มเหลวแบบเดียวกับที่แสดงใน demo StringDecoder ด้านบน ที่แต่ละครึ่งถอดรหัสออกมาเป็น � แทนที่จะเป็นตัวอักษรจริง วิธีแก้คือเก็บ instance ของ StringDecoder ไว้ใน class (this._decoder = new StringDecoder('utf8')) แล้วเรียก this._decoder.write(chunk) ใน _transform บวกกับ this._decoder.end() ใน _flush เพื่อ flush ไบต์ที่ตกค้างท้ายสุด