در یک مسیرِ دادهٔ مهندسی، ممکن است بایت‌های حسگر داخلِ پیامِ دودوییِ WebSocket قرار گیرند؛ اما درست‌بودنِ محتوای حسگر و درست‌بودنِ قالبِ فریم، دو قراردادِ جدا هستند. این درس تنها جزءِ دوم را با دادهٔ ساختگی می‌سنجد: یک فریمِ کامل، ماسک‌شده و دودویی از سمتِ کارخواه. نه سرور می‌سازیم، نه اتصال برقرار می‌کنیم و نه تأخیر یا امنیتِ شبکه را اندازه می‌گیریم. مرجعِ دستیِ بایت‌ها اجازه می‌دهد خطای کوچکِ جایگاه یا طول پنهان نماند.

۱. نمایهٔ کوچکِ یک فریمِ کامل

سربرگ کوتاه و محمولهٔ جدا در قاب دودویی
تصویر مفهومی: سربرگ کوتاه و محمولهٔ جدا در قاب دودویی؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

RFC 6455، بخشِ قالبِ فریم، نوعِ داده و بیت‌های کنترلی را از محموله جدا می‌کند. قراردادِ ما تنها FIN برابر یک، بیت‌های رزروشدهٔ صفر و opcode برابر دو را می‌پذیرد؛ بایتِ نخست 0x82 است. این انتخاب یعنی پیامِ دودوییِ تک‌فریمی، بدونِ افزونه. متن، ادامهٔ قطعه، ping، close و دیگر حالت‌ها در این تابع پشتیبانی نمی‌شوند؛ ردشدنشان، نامعتبر بودنِ همهٔ آن‌ها در پروتکلِ کامل را نمی‌رساند.

محموله بین صفر و ۲۵۶ بایت است. این سقفِ آموزشیِ خودمان است، نه حدِ همهٔ پیاده‌سازی‌های WebSocket. ورودیِ تابعِ خواندن باید دقیقاً یک فریمِ کامل باشد؛ بایتِ اضافه یا بدنهٔ ناقص خطا می‌دهد. قطعه‌های ورودی را ذخیره و منتظرِ تکمیل نمی‌کنیم. پس این جزء، جایِ حلقهٔ دریافتِ جریان یا مدیریتِ چند فریمِ پشتِ هم نیست؛ آن لایه باید پیش از فراخوانیِ مناسب طراحی شود.

در جهتِ کارخواه به کارساز، بیتِ MASK یک است و کلیدِ چهاربایتی در فریم قرار می‌گیرد. قاعدهٔ جهتِ مقابل متفاوت است؛ این خواننده را برای فریمِ ارسالیِ کارساز به کار نمی‌بریم. اسمِ محموله هم معنای واحد نمی‌سازد: شش بایتِ مثال، نمونهٔ واقعیِ ولتاژ، زمان یا رکوردِ یک دستگاه نیستند. تنها طول و جایگاهِ بایتیِ آن‌ها را می‌شناسیم.

۲. چرخهٔ ماسک از آغازِ محموله شروع می‌شود

چرخهٔ چهارقطعه‌ای ماسک از آغاز محموله
تصویر مفهومی: چرخهٔ چهارقطعه‌ای ماسک از آغاز محموله؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

در بخشِ ۵٫۳ همان RFC، هر بایتِ محموله با بایتِ متناظرِ کلید، با اندیسِ باقی‌ماندهٔ تقسیم بر چهار، XOR می‌شود. اندیس از آغازِ محموله است؛ سربرگ را واردِ چرخه نمی‌کنیم. اجرای دوبارهٔ همان تبدیل با همان کلید، بایتِ اولیه را برمی‌گرداند. این خاصیت، روشِ آزمونِ بازگشت را می‌دهد، ولی به‌تنهایی درستیِ ترتیبِ فریم را ثابت نمی‌کند.

محمولهٔ دستیِ ما 00 10 ff 20 aa 55 و کلید 01 02 03 04 است. چهار بایتِ نخست به‌ترتیب 01 12 fc 24 می‌شوند؛ سپس چرخه برمی‌گردد و دو بایتِ بعدی ab 57 هستند. طولِ محموله هنوز شش است. کلیدِ چهار‌بایتی در طولِ محموله شمرده نمی‌شود؛ همراهِ دو بایتِ نخست، کلِ فریمِ کوتاه دوازده بایت خواهد شد. مرجعِ هگز پیش از اجرای کد مشخص شده است.

اگر تبدیل را به دو برشِ محموله تقسیم کنیم، برشِ دوم باید آفستِ ادامه را بداند. برای برش در جایگاهِ سه، نخستین بایتِ بخشِ دوم با بایتِ چهارمِ کلید جفت می‌شود. شروعِ دوباره از بایتِ اولِ کلید، جواب را عوض می‌کند. در آزمون، هر هفت محلِ برشِ این محموله سنجیده می‌شوند؛ این برش‌ها فقط تقسیمِ کارِ تبدیل‌اند، نه fragmentation واقعیِ WebSocket.

۳. طولِ کوتاه، طولِ امتدادی و کدِ کامل

امتداد طول شبکه میان سربرگ و کلید ماسک
تصویر مفهومی: امتداد طول شبکه میان سربرگ و کلید ماسک؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

برای طولِ صفر تا ۱۲۵، مقدار در همان بخشِ کوتاه می‌آید. برای ۱۲۶ تا سقفِ ۲۵۶ِ این برنامه، نشانِ ۱۲۶ و سپس عددِ شانزده‌بیتی داریم. طبقِ مرجع struct پایتون، قالبِ !H عددِ صحیحِ بدونِ علامتِ دوبایتی با ترتیبِ شبکه است. برای ۲۵۶، دو بایتِ طول 01 00 هستند؛ استفاده از ترتیبِ بومیِ دستگاه، قراردادِ شبکه را تغییر می‌دهد.

"""Offline bounded masked binary-frame fixture, not a WebSocket endpoint."""
import json
import platform
import struct

MAX_PAYLOAD_BYTES = 256


def xor_mask(payload, key, offset=0):
    if type(payload) is not bytes or len(payload) > MAX_PAYLOAD_BYTES:
        raise ValueError("payload_profile")
    if type(key) is not bytes or len(key) != 4:
        raise ValueError("mask_key")
    if type(offset) is not int or not 0 <= offset <= MAX_PAYLOAD_BYTES:
        raise ValueError("payload_offset")
    if offset + len(payload) > MAX_PAYLOAD_BYTES:
        raise ValueError("payload_extent")
    return bytes(value ^ key[(offset + i) % 4] for i, value in enumerate(payload))


def encode_fixture(payload, key):
    """Explicit deterministic test key; deliberately not a production key generator."""
    masked = xor_mask(payload, key)
    n = len(payload)
    length = bytes((0x80 | n,)) if n <= 125 else b"\xfe" + struct.pack("!H", n)
    return b"\x82" + length + key + masked


def decode_fixture(frame):
    if type(frame) is not bytes or not 2 <= len(frame) <= MAX_PAYLOAD_BYTES + 8:
        raise ValueError("frame_profile")
    if frame[0] != 0x82:  # FIN=1, RSV=0, opcode=2: our one binary-frame profile.
        raise ValueError("binary_fin_rsv_profile")
    if not frame[1] & 0x80:
        raise ValueError("client_mask_required")
    indicator = frame[1] & 0x7f
    if indicator == 127:
        raise ValueError("length64_outside_profile")
    position = 2
    if indicator == 126:
        if len(frame) < 4:
            raise ValueError("truncated_length16")
        length = struct.unpack("!H", frame[2:4])[0]
        if length < 126:
            raise ValueError("nonminimal_length")
        position = 4
    else:
        length = indicator
    if length > MAX_PAYLOAD_BYTES:
        raise ValueError("payload_too_large")
    if len(frame) < position + 4:
        raise ValueError("truncated_mask_key")
    if len(frame) != position + 4 + length:
        raise ValueError("exact_complete_frame_required")
    key = frame[position:position + 4]
    return xor_mask(frame[position + 4:], key)


def error(call):
    try:
        call()
    except ValueError as exc:
        return str(exc)
    return None


def run_example():
    checks = {}
    def check(name, value):
        checks[name] = bool(value)
        if not checks[name]:
            raise AssertionError(name)
    payload = bytes.fromhex("00 10 ff 20 aa 55")
    key = bytes.fromhex("01 02 03 04")
    literal = bytes.fromhex("82 86 01 02 03 04 01 12 fc 24 ab 57")
    check("hand_literal_frame_not_only_roundtrip", encode_fixture(payload, key) == literal)
    check("hand_literal_decodes_exact_original_bytes", decode_fixture(literal) == payload)
    check("four_byte_cycle_does_not_mask_header_or_include_key_in_length", xor_mask(key * 2, key) == b"\0" * 8 and literal[1] == 0x86)
    check("empty_binary_payload_still_contains_mask_key", encode_fixture(b"", key) == bytes.fromhex("82 80 01 02 03 04") and decode_fixture(encode_fixture(b"", key)) == b"")
    check("125_uses_short_length", encode_fixture(b"a" * 125, key)[:6] == bytes.fromhex("82 fd 01 02 03 04"))
    check("126_uses_minimal_network_16bit_length", encode_fixture(b"a" * 126, key)[:8] == bytes.fromhex("82 fe 00 7e 01 02 03 04"))
    check("256_uses_big_endian_not_native_little_endian", encode_fixture(b"a" * 256, key)[:4] == bytes.fromhex("82 fe 01 00"))
    transformed = xor_mask(payload, key)
    check("all_seven_payload_splits_preserve_offset_one_group", all(
        xor_mask(payload[:split], key) + xor_mask(payload[split:], key, split) == transformed
        for split in range(len(payload) + 1)))
    check("restarting_key_on_second_chunk_is_detectably_wrong", xor_mask(payload[:3], key) + xor_mask(payload[3:], key) != transformed)
    # Independent Boolean truth-table oracle, not ^ or the encoder/decoder.
    cases = 0
    for value in range(256):
        for key_octet in (0, 85, 170, 255):
            expected = int("".join("1" if a != b else "0" for a, b in zip(f"{value:08b}", f"{key_octet:08b}")), 2)
            if xor_mask(bytes((value,)), bytes((key_octet,)) * 4) != bytes((expected,)):
                raise AssertionError("independent_bit_truth_table")
            cases += 1
    check("1024_bit_truth_table_cases_one_group", cases == 1024)
    roundtrips = 0
    for n in (0, 1, 3, 4, 5, 125, 126, 127, 256):
        data = bytes(i % 256 for i in range(n))
        for mask in (key, b"\0" * 4, b"\xff" * 4):
            if decode_fixture(encode_fixture(data, mask)) != data:
                raise AssertionError("bounded_roundtrip")
            roundtrips += 1
    check("27_bounded_lengths_and_masks_roundtrips_one_group", roundtrips == 27)
    check("fragments_text_control_and_rsv_not_in_binary_profile", all(
        error(lambda first=first: decode_fixture(bytes((first,)) + literal[1:])) == "binary_fin_rsv_profile"
        for first in (0x02, 0x80, 0x81, 0x89, 0xc2)))
    check("unmasked_and_64bit_form_rejected_explicitly", error(lambda: decode_fixture(b"\x82\x06" + literal[2:])) == "client_mask_required"
          and error(lambda: decode_fixture(bytes.fromhex("82 ff 00 00 00 00 00 00 00 00"))) == "length64_outside_profile")
    check("truncated_extended_length_key_and_payload_rejected", error(lambda: decode_fixture(b"\x82\xfe\0")) == "truncated_length16"
          and error(lambda: decode_fixture(b"\x82\x80\x01")) == "truncated_mask_key"
          and error(lambda: decode_fixture(literal[:-1])) == "exact_complete_frame_required")
    check("nonminimal_extra_data_and_declared_oversize_rejected", error(lambda: decode_fixture(bytes.fromhex("82 fe 00 01") + key + b"a")) == "nonminimal_length"
          and error(lambda: decode_fixture(literal + b"\0")) == "exact_complete_frame_required"
          and error(lambda: decode_fixture(bytes.fromhex("82 fe 01 01") + key)) == "payload_too_large")
    check("exact_bytes_keys_offsets_and_size_profile", all(error(lambda v=v: encode_fixture(v, key)) == "payload_profile" for v in ("x", bytearray(b"x"), b"a" * 257))
          and all(error(lambda k=k: encode_fixture(payload, k)) == "mask_key" for k in (None, b"abc", bytearray(key)))
          and all(error(lambda off=off: xor_mask(b"a", key, off)) == "payload_offset" for off in (True, -1, 257, 0.0, float("nan")))
          and error(lambda: xor_mask(b"a", key, 256)) == "payload_extent"
          and error(lambda: decode_fixture(bytearray(literal))) == "frame_profile")
    return {"python_version": platform.python_version(), "own_profile": "one complete FIN1 RSV0 opcode2 client-masked binary frame; payload0..256bytes, no extension, fragment, control, text or64bit form; explicit fixed keys only test vectors, not production generation",
            "hand_payload_hex": payload.hex(), "hand_key_hex": key.hex(), "hand_frame_hex": literal.hex(),
            "hand_masked_payload_hex": transformed.hex(), "hand_frame_bytes": len(literal),
            "length126_header_hex": encode_fixture(b"a" * 126, key)[:8].hex(),
            "length256_header_hex": encode_fixture(b"a" * 256, key)[:8].hex(),
            "independent_bit_truth_table_cases_one_group": cases, "roundtrip_cases_one_group": roundtrips,
            "limitations": "No server, handshake, network, TLS, RNG, UTF8/text validation or full RFC conformance. RFC production masks must be fresh unpredictable keys; repeated/zero keys here are deterministic fixtures only. Masking is not encryption/authentication.",
            "checks": checks}


if __name__ == "__main__":
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

طول باید با کوتاه‌ترینِ شکلِ مجاز نوشته شود. بنابراین نمایشِ یک بایت با نشانِ ۱۲۶ و عددِ یک پذیرفته نیست. شکلِ ۶۴بیتیِ پروتکل وجود دارد، اما این نمایه با سقفِ کوچک، آن را صریح رد می‌کند. پس از خواندنِ طول، چهار بایتِ کلید و تعدادِ دقیقِ محموله لازم‌اند. تابع از بایتِ بعدیِ اضافه عبور نمی‌کند و آن را فریمِ دیگری فرض نمی‌کند.

payload و key باید bytes دقیق باشند؛ key دقیقاً چهار بایت دارد. offset نیز int دقیق و در محدودهٔ قرارداد است و مجموعِ آن با طولِ برش از ۲۵۶ نمی‌گذرد. bool، رشته، bytearray و NaN در جایگاه‌های مربوط رد می‌شوند. این سخت‌گیری انتخابِ API آموزشیِ ماست؛ کتابخانه‌های دیگر ممکن است بافرهای متفاوتی بپذیرند. تابع، ورودیِ اشتباه را بی‌صدا تبدیل یا کوتاه نمی‌کند.

۴. مرجعِ مستقل و شانزده گروهِ بررسی

مرجع بایتی مستقل در کنار خروجی بازسازی‌شده
تصویر مفهومی: مرجع بایتی مستقل در کنار خروجی بازسازی‌شده؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

مرجعِ کاملِ فریمِ دستی، 82 86 01 02 03 04 01 12 fc 24 ab 57 است. هم خروجیِ سازنده با آن برابر می‌شود و هم خواندنِ همان رشتهٔ ثابت، محمولهٔ اولیه را می‌دهد. اگر تنها سازنده و خواننده را به هم وصل کنیم، ممکن است هر دو یک خطای مشترک داشته باشند؛ ثابتِ دستی، این وابستگی را کم می‌کند. طول‌های ۱۲۵، ۱۲۶ و ۲۵۶ نیز سربرگِ موردِ انتظارِ جدا دارند.

{
  "python_version": "3.14.5",
  "own_profile": "one complete FIN1 RSV0 opcode2 client-masked binary frame; payload0..256bytes, no extension, fragment, control, text or64bit form; explicit fixed keys only test vectors, not production generation",
  "hand_payload_hex": "0010ff20aa55",
  "hand_key_hex": "01020304",
  "hand_frame_hex": "8286010203040112fc24ab57",
  "hand_masked_payload_hex": "0112fc24ab57",
  "hand_frame_bytes": 12,
  "length126_header_hex": "82fe007e01020304",
  "length256_header_hex": "82fe010001020304",
  "independent_bit_truth_table_cases_one_group": 1024,
  "roundtrip_cases_one_group": 27,
  "limitations": "No server, handshake, network, TLS, RNG, UTF8/text validation or full RFC conformance. RFC production masks must be fresh unpredictable keys; repeated/zero keys here are deterministic fixtures only. Masking is not encryption/authentication.",
  "checks": {
    "hand_literal_frame_not_only_roundtrip": true,
    "hand_literal_decodes_exact_original_bytes": true,
    "four_byte_cycle_does_not_mask_header_or_include_key_in_length": true,
    "empty_binary_payload_still_contains_mask_key": true,
    "125_uses_short_length": true,
    "126_uses_minimal_network_16bit_length": true,
    "256_uses_big_endian_not_native_little_endian": true,
    "all_seven_payload_splits_preserve_offset_one_group": true,
    "restarting_key_on_second_chunk_is_detectably_wrong": true,
    "1024_bit_truth_table_cases_one_group": true,
    "27_bounded_lengths_and_masks_roundtrips_one_group": true,
    "fragments_text_control_and_rsv_not_in_binary_profile": true,
    "unmasked_and_64bit_form_rejected_explicitly": true,
    "truncated_extended_length_key_and_payload_rejected": true,
    "nonminimal_extra_data_and_declared_oversize_rejected": true,
    "exact_bytes_keys_offsets_and_size_profile": true
  }
}

مرجعِ دیگری، XOR هر بایت را از جدولِ حقیقتِ بیت‌های متفاوت می‌سازد و از عملگرِ ^ یا تابعِ تبدیل استفاده نمی‌کند. ۱۰۲۴ ترکیبِ بایت و کلید، یک گروهِ بررسی هستند. بیست‌وهفت آزمونِ بازگشتِ طول و کلید نیز گروهی جدا هستند، نه بیست‌وهفت مقاله یا بررسیِ تازه. حالتِ تهی، کلیدِ ناقص، طولِ نامتعارف، دادهٔ اضافه، نوعِ نامناسب و فریم‌های خارجِ نمایه هم کنترل شده‌اند.

کلیدهای ثابت، صفر و تکراریِ این کد فقط fixture هستند. پروتکل برای کارخواهِ واقعی، کلیدِ تازه و پیش‌بینی‌ناپذیر می‌خواهد؛ این برنامه مولدِ کلیدِ مناسب را پیاده نمی‌کند. همچنین ماسک، رمزگذاری یا احرازِ اصالت نیست؛ کلید در خودِ فریم قرار دارد. خواندنِ موفقِ این بایت‌ها، TLS، دست‌دهی، مجوزِ کاربر یا اعتبارِ محتوای پیام را تأیید نمی‌کند.

اجرای ثبت‌شده با Python 3.14.5 و کتابخانهٔ استاندارد است؛ مستنداتِ خوانده‌شده 3.14.8 و تاریخِ دسترسی ۲۵ مهر ۱۴۰۵ هستند. مرزبندیِ پیام روی جریانِ TCP مسئلهٔ برشِ دریافت را جدا توضیح می‌دهد. آزمونِ اشاره‌گرِ نامِ DNS نیز نمونه‌ای از مرجعِ بایتیِ مستقل است؛ هیچ‌یک، نتیجهٔ این تمرین را به آزمونِ اتصالِ واقعی تبدیل نمی‌کنند.

۵. چهار پرسش با پاسخِ کامل

قاب کامل محدود در برابر قطعات ناتمام و دادهٔ اضافه
تصویر مفهومی: قاب کامل محدود در برابر قطعات ناتمام و دادهٔ اضافه؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

پرسش اول: طولِ محمولهٔ شش‌بایتی با کلید چند است؟ پاسخ: هنوز شش؛ چهار بایتِ کلید در آن شمرده نمی‌شوند. فریمِ کوتاهِ نمونه با دو بایتِ آغازین، کلید و محموله، دوازده بایت دارد.

پرسش دوم: برشِ دوم در جایگاهِ سه از کدام کلید شروع می‌شود؟ پاسخ: بایتِ چهارمِ کلید، چون اندیسِ سراسریِ محموله سه است. شروعِ دوباره از کلیدِ اول، نتیجهٔ متفاوت و در این قرارداد نادرست می‌دهد.

پرسش سوم: دو بایتِ طول برای ۲۵۶ چیست؟ پاسخ: 01 00 با ترتیبِ شبکه؛ همراهِ نشانِ امتدادِ طول می‌آیند. نمایشِ یک با همین شکلِ امتدادی، به‌علت کوتاه‌نبودنِ نمایش رد می‌شود.

پرسش چهارم: می‌توان کلیدِ ثابتِ آزمون را در کارخواهِ واقعی تکرار کرد؟ پاسخ: خیر؛ این‌ها دادهٔ بازتولیدپذیرِ آزمایش‌اند. نیازِ کلیدِ تازه و پیش‌بینی‌ناپذیرِ پروتکل و دیگر اجزای اتصالِ واقعی، در این کد تأمین نشده‌اند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.