TypedArray و TextEncoder؛ تفاوت متن، بایت و اندازه
در این درس ۵ بخش
وقتی پیام فارسی را به یک سرور میفرستیم، «تعداد حرف» و «اندازهٔ داده» دو پرسش متفاوتاند. ممکن است متن کوتاهی روی صفحه ببینیم، ولی همان متن پس از تبدیل به UTF-8 بایتهای بیشتری داشته باشد. این تفاوت در محدودیت پیام چت، بارگذاری فایل و نگهداری دادهٔ باینری اهمیت دارد. در این درس آرایههای معمولی را از ابتدا تکرار نمیکنیم؛ با حافظهٔ بایتی و نمایی که روی آن ساخته میشود کار میکنیم. نمونهها مستقلاند و در Console مرورگر جدید یا یک فایل mjs روی Node.js ۲۴ اجرا میشوند. هر نمونه را جدا اجرا کنید تا نام متغیرهای دو آزمایش با هم تداخل نکند.
سه اندازه را از هم جدا کنید
رشتهٔ جاوااسکریپت از واحدهای UTF-16 استفاده میکند؛ ویژگی length تعداد همین واحدها را میدهد. پیمایش رشته با Array.from، نقطههای کد را جدا میکند، ولی آن هم لزوماً تعداد شکلهای دیداری متن نیست. در مقابل، شبکه معمولاً متن را با یک کدگذاری مانند UTF-8 به بایت تبدیل میکند. بایت واحد هشتبیتی داده است. لازم نیست جدول کدگذاری را حفظ کنید؛ باید بدانید برای هر تصمیم کدام اندازه لازم است. محدودیت جای نمایش در صفحه، قانون نام کاربر و سقف پیام شبکه را با یک عدد length مشترک پیاده نکنید.
const text = 'سلام 👋';
const encoded = new TextEncoder().encode(text);
console.log(text.length); // 7 واحد UTF-16
console.log(Array.from(text).length); // 6 نقطهٔ کد
console.log(encoded.byteLength); // 13 بایت UTF-8
console.log(encoded instanceof Uint8Array); // trueTextEncoder متن را به UTF-8 تبدیل میکند و Uint8Array برمیگرداند. این خروجی یک #array معمولی جاوااسکریپت نیست؛ نمایی برای خواندن و نوشتن عددهای بدون علامت هشتبیتی است. هر خانهٔ آن یک بایت را نشان میدهد. تبدیل متن به بایت، رمزگذاری امنیتی یا فشردهسازی نیست؛ با TextDecoder میتوان متن سازگار را دوباره خواند. همچنین این ابزار، تفاوت حروف فارسی و عربی یا قواعد نامگذاری برنامه را خودکار اصلاح نمیکند. آن تصمیمها متعلق به اعتبارسنجی متناند و پیش از ارسال، در قرارداد پروژه مشخص میشوند.
ArrayBuffer مخزن است؛ TypedArray نمای آن است
ArrayBuffer در این مثال یک محدودهٔ خام از حافظه با اندازهٔ مشخص نگه میدارد. Uint8Array یا Uint16Array راه تفسیر آن حافظه را تعیین میکنند. اولی عنصرهای یکبایتی و دومی عنصرهای دوبایتی دارد؛ پس length تعداد عنصر و byteLength تعداد بایتِ نمای انتخابشده است. سازندهٔ مشترکی با نام TypedArray را مستقیماً new نمیکنیم؛ یکی از نوعهای مشخص را انتخاب میکنیم. برای متن شبکه معمولاً Uint8Array مناسب است، چون با بایتها کار میکنیم. برای خواندن عددهای چندبایتیِ یک قالب فایل، ترتیب بایتها نیز مهم میشود و ابزار مناسب میتواند DataView باشد.
const words = new Uint16Array(3);
console.log(words.length, words.byteLength); // 3 6
const buffer = new ArrayBuffer(6);
const bytes = new Uint8Array(buffer);
bytes.set([10, 20, 30, 40, 50, 60]);
const view = bytes.subarray(1, 4);
const copy = view.slice();
view[0] = 99;
console.log([...bytes]); // [10, 99, 30, 40, 50, 60]
console.log([...copy]); // [20, 30, 40]
console.log(view.byteOffset, view.byteLength, view.buffer.byteLength);
// 1 3 6subarray در اینجا نمای تازه روی همان مخزن میسازد؛ بنابراین تغییر view در bytes هم دیده میشود. slice محتوای بخش انتخابشده را در مخزن تازه کپی کرده است و copy تغییر نمیکند. byteLength پنجره سه است، ولی buffer.byteLength شش؛ اگر فقط پنجره را باید بفرستید، فرستادن کل buffer میتواند دادهٔ بیرون از محدودهٔ موردنظر را هم شامل کند. این تفاوت، نسخهٔ بایتی مسئلهٔ مرجع مشترک است. مدخل #primitive-reference برای مرور اصل مرجع مفید است، اما اینجا باید offset و اندازهٔ نما را هم همراه داده دنبال کنید.
TypedArray ابزار بررسی معنای عدد نیست. مثلاً نوشتن عددی خارج از محدوده در یک نوع عددی میتواند آن را طبق قواعد همان نوع تبدیل کند؛ نباید انتظار داشته باشید همیشه خطای اعتبارسنجی بدهد. اگر دادهٔ بیرونی باید فقط عدد صحیح در محدودهٔ معینی داشته باشد، پیش از ساخت نمای بایتی آن را بررسی کنید. برای پول، شناسه یا شمار پیام، تبدیل خاموش به یک بایت راهحل مناسبی نیست. در پروژهٔ چت از این ابزار برای اندازهٔ متن کدشده استفاده میکنیم، نه برای کوچککردن دلخواه شمارهٔ ترتیب سرور.
کل پیام را اندازه بگیرید، نه فقط متن داخل آن
فرض کنید فقط برای این تمرین، سقف یک بسته ۶۴ بایت است. متن شامل ۳۲ حرف «س» بهتنهایی ۶۴ بایت UTF-8 میشود، ولی بستهٔ JSON نام ویژگی، کوتیشن و نشانههای دیگری هم دارد. پس پذیرفتن متن صرفاً با اندازهٔ خودش ممکن است بستهٔ نهایی را از سقف عبور دهد. در پروژهٔ واقعی، سقف متن و سقف کل پیام دو کنترل جدا هستند. اندازهٔ نهایی را پس از ساختن شیء مجاز و JSON.stringify محاسبه کنید. #json قالب انتقال است؛ اندازهٔ رشتهٔ فارسیِ روی صفحه، اندازهٔ قالب انتقال را تعیین نمیکند.
function jsonBytes(value) {
const json = JSON.stringify(value);
if (typeof json !== 'string') throw new TypeError('JSON value required');
return new TextEncoder().encode(json).byteLength;
}
const packet = { text: 'س'.repeat(32) };
console.log(new TextEncoder().encode(packet.text).byteLength); // 64
console.log(jsonBytes(packet)); // 75
console.log(jsonBytes(packet) <= 64); // falseتابع بالا را فقط با دادهٔ سادهٔ سازگار با JSON به کار ببرید. برای نمونه، ساختار دوری یا BigInt میتواند هنگام stringify خطا بدهد و باید در مرز مناسب مدیریت شود. این تابع قرار نیست تمام دادههای دلخواه را به بستهٔ مجاز تبدیل کند. در کلاینت چت، شکل بسته از قرارداد میآید و سپس اندازه کنترل میشود. نکتهٔ مهم دیگری هم وجود دارد: WebSocket.send با رشته، پیام متنی میفرستد؛ ارسال Uint8Array یک پیام باینری است. برای اندازهگیری، متن را encode میکنیم، ولی در این پروژه همان رشتهٔ JSON را میفرستیم؛ تغییر نوع ارسال، قرارداد سرور را عوض میکند.
بازگشت از بایت به متن و خطای کدگذاری
TextDecoder بایتها را طبق کدگذاری مشخص به رشته تبدیل میکند. در حالت پیشفرض، بعضی دادههای خراب با نویسهٔ جایگزین نمایش داده میشوند. این رفتار برای پیشنمایش یک متن آسیبدیده میتواند قابل قبول باشد، ولی در مرزی که باید دادهٔ معتبر بپذیرد، ممکن است خرابی را پنهان کند. گزینهٔ fatal باعث میشود دادهٔ کدگذاریشدهٔ نامعتبر به خطا منجر شود. انتخاب آن باید با سیاست برنامه هماهنگ باشد. تبدیل موفق نیز معنای پیام را تأیید نمیکند؛ پس از decode هنوز باید ساختار و محدودیتهای مورد انتظار بررسی شوند.
const decoder = new TextDecoder('utf-8', { fatal: true });
console.log(decoder.decode(new TextEncoder().encode('پیام'))); // پیام
try {
new TextDecoder('utf-8', { fatal: true }).decode(Uint8Array.of(255));
} catch (error) {
console.log(error.name); // TypeError
}
const streaming = new TextDecoder('utf-8', { fatal: true });
const first = streaming.decode(Uint8Array.of(0xD8), { stream: true });
const second = streaming.decode(Uint8Array.of(0xB3), { stream: true });
console.log(JSON.stringify(first + second + streaming.decode())); // "س"در نمونهٔ آخر، دو بایت یک حرف در دو تکه رسیدهاند. نگهداشتن وضعیت decoder با stream اجازه میدهد توالی چندبایتی کامل شود؛ پایان کار نیز با decode بدون ورودی تخلیه میشود. اگر هر تکه را مستقل و بدون این قرارداد بخوانید، ممکن است یک متن سالم را خراب تشخیص دهید. این تمرین دربارهٔ جریان بایت است. در WebSocket مرورگر، رویداد message برای پیام متنی معمولاً رشتهٔ کامل به شما میدهد؛ لازم نیست فریمهای شبکه را خودتان دوباره کنار هم بگذارید. کتابخانهٔ سمت سرور نیز مسئول جزئیات پروتکل انتقال است.
تمرین با پاسخ و معیار تحویل
سه پیشبینی انجام دهید: اگر بعد از ساخت view مقدار bytes[2] را عوض کنید، view چه میبیند؟ اگر در copy بنویسید چه میشود؟ و چرا بستهٔ ۷۵بایتیِ مثال، با وجود متن ۶۴بایتی رد شد؟ پاسخ: نمای مشترک تغییر مخزن را میبیند، کپی مستقل اثر جانبی ندارد و سقف بسته شامل ساختار JSON هم هست. سپس تابع jsonBytes را با متن لاتین، فارسی و ایموجی امتحان کنید و اندازهها را کنار هم ثبت کنید. معیار تحویل، توضیح دلیل اختلافهاست؛ صرف رسیدن به یک عدد بدون دانستن واحد آن کافی نیست.
برای مرور تفاوت مرجع و کپی به این درس برگردید: https://www.darsavand.com/courses/javascript-project-lab?lesson=506b935f-8cb8-4681-a728-c6c1776307e0
برای ارتباط قالب JSON با دادهٔ ذخیرهشده، این آموزش پیشنیاز است: https://www.darsavand.com/courses/javascript-from-zero-to-project?lesson=9c21ba3b-8ee4-49be-bfcc-4db19f3a440a
در ادامه، اندازهٔ بسته تنها یکی از شرطها خواهد بود؛ باید چرخهٔ عمر اتصال و وضعیت پیام تأییدنشده را هم کنترل کنیم. این دو مسئله با کوچککردن متن یا افزایش یک سقف بایت حل نمیشوند.
منابع رسمی انگلیسی:
MDN TypedArray: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
MDN subarray: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/subarray
MDN slice: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice
MDN TextEncoder: https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder
MDN TextDecoder: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
تألیف، مثالها و تمرینها: تحریریه درسآوند. منابع رسمی انگلیسی برای راستیآزمایی رفتار ابزارها و مطالعهٔ بیشتر معرفی شدهاند.
تألیف و مثالهای اختصاصی: تحریریه درسآوند. فایلهای کامل پروژه و راهنمای اجرا و دیباگ:
https://www.darsavand.com/resources/simple-websocket-chat-project
متن درسهای عمومی را بدون ورود بخوانید.