فهرست مقاله
در دریافت دورهای یک تصویر لحظهای از دادهٔ حسگر، ممکن است همان بایتها بارها تکرار شوند. پیش از ارسال دوباره، میتوان نسخهای را که دریافتکننده معرفی کرده با بازنمایی انتخابشده مقایسه کرد. این درس منطق محدود GET شرطی و ETag را روی دو رشتهٔ بایتی ساختگی پیاده میکند. هیچ سرور، مرورگر، شبکه یا حافظهٔ نهان واقعی اجرا نمیشود؛ کد فقط تصمیم وضعیت و بدنه را میسازد و ادعای اندازهگیری سرعت یا کاهش مصرف شبکه ندارد.
۱. ابتدا بازنمایی و محدودهٔ تصمیم را ثابت کنیم

نمونهٔ نخست، دادهٔ JSON فشرده با شمارهٔ ۷، زمان ۲۰۰۰ میکروثانیه از مبدأ ساختگی و ولتاژ ۱۲۰۰ میلیولت است. بایتهای آن در کد صریح نوشته شدهاند و طولشان ۴۲ است. نمونهٔ دوم فقط مقدار ولتاژ را به ۱۲۰۱ تغییر میدهد. این اعداد، قرائت دستگاه یا زمان دریافت واقعی نیستند. نمونه را به عنوان محتوای آماده در نظر میگیریم؛ تابع HTTP، ساختار JSON، واحد یا صحت فیزیکی مقدار را اعتبارسنجی نمیکند.
فرض دیگر این است که بازنمایی موجود انتخاب شده، مجاز بودن درخواست قبلاً تعیین شده و پاسخ عادی GET بدون شرط، ۲۰۰ بود. مسیرهای نبود منبع، هدایت، خطای دسترسی و انتخاب میان چند بازنمایی داخل این تابع نیستند. حتی b'' در قرارداد ما یک بازنمایی موجود با صفر بایت است؛ با «منبع وجود ندارد» یکسان نیست. این مرزبندی مانع آن میشود که تطابق نسخه، جای بررسیهای قبلی درخواست بنشیند.
برای بررسی داده پیش از ساخت بازنمایی، اعتبارسنجی دادهٔ حسگر با dataclass یک مرحلهٔ مستقل نشان میدهد. در اینجا فقط bytes دقیق پذیرفته میشود؛ رشتهٔ پایتون یا bytearray خودکار تبدیل نمیشود. انتخاب صریح بایتها کمک میکند موضوع مقایسه مبهم نباشد، هرچند کل سامانهٔ انتقال داده را پیاده نمیکند.
۲. شرط غایب، برچسب متفاوت و مقایسهٔ ضعیف

طبق RFC 9110، بخش If-None-Match، مقایسهٔ این شرط ضعیف است؛ بخش مبهم برچسبها مقایسه میشود و نشان ضعف، مانع تطابق نیست. در مسیر GET یا HEAD انتخابشده، تطابق شرط به پاسخ ۳۰۴ میرسد. این قاعده را به If-Match یا روشهای تغییردهندهٔ داده تعمیم نمیدهیم؛ نمونه فقط دو روش خواندن را میپذیرد.
مدل EntityTag دو مؤلفه دارد: رشتهٔ opaque و پرچم بولی weak. تابع مقایسه تنها opaque را نویسهبهنویسه میسنجد؛ بنابراین abc با ABC برابر نیست. برای شرط غایب، None میدهیم. شرط فهرستی، تاپل غیرتهی از همین اشیاست و تطابق یکی از اعضا کافی است. رشتهٔ مستقل ستاره، حالت ویژهٔ وجود بازنمایی است. در مقابل، شیء دارای opaque ستاره، یک برچسب عادی است و معنای همهچیز را ندارد.
این رابط، تجزیهگر سرآیند خام نیست. رشتهای مانند '"abc"'، فهرست معمولی پایتون، تاپل تهی یا ترکیب ستاره با اشیای برچسب رد میشود. با این قرارداد میتوان منطق تصمیم را از تجزیهٔ متن جدا آزمود. اگر بعداً سرور اضافه شود، باید تبدیل سرآیند واقعی به همین ساختار با کتابخانه و آزمون مستقل انجام شود؛ پذیرش این اشیا به معنی پشتیبانی کامل از تمام نحو HTTP نیست.
۳. کد کامل: برچسب از بایتها و ورودی ساختاریافته

تابع tag_for با sha256 و hexdigest در hashlib از بایتهای دقیق یک شناسهٔ قطعی میسازد و آن را در قالب نقلقولدار برچسب قوی قرار میدهد. در این مثال، نوع محتوا و روش بازنمایی ثابتاند؛ فشردهسازی، مذاکرهٔ محتوا و تغییر فراداده را مدل نکردهایم. تعمیم همین برچسب به تمام نسخههای فشرده یا شخصیشده، تصمیمی نیست که این کد گرفته باشد.
رشتهٔ opaque در نمونه به زیرمجموعهٔ ASCII نمایان محدود است؛ نقلقول دوتایی، فاصله و کنترلهای خط رد میشوند. بخش مبهم خالی مجاز است و ویرگول داخل آن یک نویسهٔ عادی باقی میماند. آزمون EntityTag('a,b') نشان میدهد چرا شکستن سادهٔ متن خام با ویرگول روش مناسبی برای تجزیه نیست. بکاسلش نیز در این مدل به صورت نویسهٔ لفظی بررسی میشود؛ برچسب تولیدی هش اصلاً آن را ندارد.
این تابع برابری معنایی JSON را بررسی نمیکند. افزودن فاصله یا جابهجایی ترتیب کلیدها میتواند بایتها را عوض کند، حتی اگر دادهٔ تفسیرشده یکسان باشد. برای آزمون تکرارپذیر، بایت ورودی را همراه نتیجه نگه میداریم. اگر برنامهٔ بزرگتر بازنمایی را هر بار با روش متفاوت بسازد، باید ابتدا سیاست تولید محتوا را روشن کند؛ تغییر الگوریتم مقایسه به تنهایی این ابهام را حل نمیکند.
کد را با نام http_conditional_sensor.py ذخیره و اجرا کنید. اجرای ثبتشده Python 3.14.5 دارد؛ نسخهٔ مستندات خواندهشده 3.14.8 بوده است. هجده بررسی نامدار، شاخههای تصمیم و قرارداد ورودی را پوشش میدهند. خروجی تابع، پیام کامل روی سیم نیست؛ سرآیندهای دیگر، قالب انتقال و چارچوب سرور برای استفادهٔ واقعی باید جداگانه تکمیل و بررسی شوند.
"""Offline selected-representation decision only; no HTTP server, client or cache."""
from dataclasses import dataclass
import hashlib
import json
import platform
BODY_A = b'{"seq":7,"time_us":2000,"voltage_mv":1200}'
BODY_B = b'{"seq":7,"time_us":2000,"voltage_mv":1201}'
@dataclass(frozen=True)
class EntityTag:
opaque: str
weak: bool = False
def __post_init__(self):
if type(self.opaque) is not str or type(self.weak) is not bool:
raise TypeError('Exact str opaque tag and bool weakness required')
# Deliberate visible-ASCII subset of RFC9110 etagc; empty is valid.
# Comma and backslash are literal allowed characters; DQUOTE is excluded.
if any(not (ord(ch) == 0x21 or 0x23 <= ord(ch) <= 0x7E) for ch in self.opaque):
raise ValueError('Only visible ASCII etagc excluding DQUOTE accepted')
def wire(self):
return ('W/' if self.weak else '') + '"' + self.opaque + '"'
def weak_equal(left, right):
if type(left) is not EntityTag or type(right) is not EntityTag:
raise TypeError('Pre-parsed EntityTag objects required')
return left.opaque == right.opaque
def tag_for(body):
if type(body) is not bytes:
raise TypeError('Exact selected representation bytes required')
return EntityTag('sha256-' + hashlib.sha256(body).hexdigest())
def checked_condition(condition):
# None: header absent; '*': sole wildcard; tuple: already parsed tags.
if condition is None:
return None
if type(condition) is str and condition == '*':
return condition
if type(condition) is tuple and condition and all(type(t) is EntityTag for t in condition):
return condition
raise ValueError('Use None, sole wildcard *, or a nonempty tuple of EntityTag')
def decide(method, selected_body, condition=None):
if type(method) is not str or method not in ('GET', 'HEAD'):
raise ValueError('This lesson handles only exact GET and HEAD methods')
current = tag_for(selected_body)
condition = checked_condition(condition)
# A representation already exists in this scoped example, even if b''.
matched = condition == '*' or (type(condition) is tuple and any(weak_equal(t, current) for t in condition))
status = 304 if matched else 200
response_body = selected_body if method == 'GET' and status == 200 else b''
return {'status': status, 'etag': current.wire(), 'body': response_body}
def describe(method, body, condition):
decision = decide(method, body, condition)
cond = condition if condition is None or type(condition) is str else [t.wire() for t in condition]
return {'method': method, 'condition': cond, 'status': decision['status'],
'etag': decision['etag'], 'body_byte_count': len(decision['body'])}
def rejects(call, exception):
try:
call()
except exception:
return True
return False
def run_example():
current = tag_for(BODY_A)
different = EntityTag('different')
weak_current = EntityTag(current.opaque, True)
get_plain = decide('GET', BODY_A)
get_match = decide('GET', BODY_A, (current,))
head_plain = decide('HEAD', BODY_A)
checks = {
'absent_condition_get_returns200_and_exact_bytes': get_plain['status'] == 200 and get_plain['body'] == BODY_A,
'different_tag_get_returns200_and_body': decide('GET', BODY_A, (different,))['status'] == 200 and decide('GET', BODY_A, (different,))['body'] == BODY_A,
'matching_strong_tag_get_returns304_without_body': get_match['status'] == 304 and get_match['body'] == b'',
'weak_client_tag_matches_strong_current_tag': decide('GET', BODY_A, (weak_current,))['status'] == 304,
'weak_comparison_ignores_either_weak_flag': weak_equal(current, weak_current) and weak_equal(weak_current, current),
'any_tag_in_preparsed_tuple_can_match': decide('GET', BODY_A, (different, weak_current))['status'] == 304,
'sole_wildcard_matches_existing_representation': decide('GET', BODY_A, '*')['status'] == 304,
'empty_bytes_are_still_an_existing_representation': decide('GET', b'', '*')['status'] == 304,
'unconditional_head_returns200_without_body': head_plain['status'] == 200 and head_plain['body'] == b'' and head_plain['etag'] == get_plain['etag'],
'matching_head_returns304_without_body': decide('HEAD', BODY_A, (current,))['status'] == 304 and decide('HEAD', BODY_A, (current,))['body'] == b'',
'changed_bytes_with_old_tag_return200': decide('GET', BODY_B, (current,))['status'] == 200 and tag_for(BODY_A) != tag_for(BODY_B),
'tag_is_deterministic_and_quoted_strong_format': tag_for(bytes(bytearray(BODY_A))) == current and not current.weak and current.wire() == '"sha256-' + hashlib.sha256(BODY_A).hexdigest() + '"',
'literal_comma_and_backslash_tags_are_not_split_or_unescaped': weak_equal(EntityTag('a,b\\c'), EntityTag('a,b\\c', True)) and EntityTag('a,b').wire() == '"a,b"',
'opaque_comparison_is_case_sensitive_and_allows_empty': not weak_equal(EntityTag('abc'), EntityTag('ABC', True)) and EntityTag('').wire() == '""',
'quoted_star_tag_is_distinct_from_wildcard': decide('GET', BODY_A, (EntityTag('*'),))['status'] == 200,
'unsupported_methods_and_nonbytes_body_rejected': rejects(lambda: decide('POST', BODY_A), ValueError) and rejects(lambda: decide('GET', BODY_A.decode()), TypeError),
'raw_headers_empty_or_mixed_tag_tuples_rejected': all(rejects(lambda value=value: decide('GET', BODY_A, value), ValueError) for value in ('"abc"', (), [current], ('*', current), True)),
'unsafe_or_wrong_type_tag_components_rejected': all(rejects(lambda value=value: EntityTag(value), exception) for value, exception in
(('a"b', ValueError), ('a b', ValueError), ('a\r\nb', ValueError), ('فارسی', ValueError), (1, TypeError)))
and rejects(lambda: EntityTag('abc', 1), TypeError),
}
assert all(checks.values()), checks
scenarios = [('GET', BODY_A, None), ('GET', BODY_A, (different,)), ('GET', BODY_A, (current,)),
('GET', BODY_A, (weak_current,)), ('GET', BODY_A, '*'), ('GET', BODY_B, (current,)),
('HEAD', BODY_A, None), ('HEAD', BODY_A, (current,))]
return {'python': platform.python_version(), 'scope': 'offline_GET_HEAD_existing_selected_representation_decision',
'snapshot_ascii': BODY_A.decode('ascii'),
'authored_units': {'seq': 'record count', 'time_us': 'synthetic microseconds', 'voltage_mv': 'integer millivolts'},
'snapshot_byte_count': len(BODY_A), 'strong_etag': current.wire(),
'condition_contract': 'None | sole wildcard str * | nonempty pre-parsed tuple[EntityTag]',
'tag_character_contract': 'visible ASCII etagc subset; comma/backslash literal; no raw-header parser',
'scenario_decisions': [describe(*row) for row in scenarios],
'network_server_cache_auth_or_freshness_implemented': False, 'checks': checks}
if __name__ == '__main__':
print(json.dumps(run_example(), indent=2))
۴. خروجی ۲۰۰ و ۳۰۴ را با اندازهٔ بدنه بخوانیم

در دو سناریوی نخست، GET بدون شرط و GET با برچسب متفاوت، وضعیت ۲۰۰ و بدنهٔ ۴۲ بایتی میدهند. معرفی برچسب فعلی یا صورت ضعیف همان برچسب، وضعیت ۳۰۴ و صفر بایت بدنه میدهد. با تغییر یک میلیولت در نمونهٔ دوم، برچسب قبلی تطابق ندارد؛ تصمیم دوباره ۲۰۰ است. این تغییرِ نمونه را مشاهده کردهایم؛ از آن ادعای اثبات ریاضی نبود برخورد در تمام خروجیهای هش نمیسازیم.
{
"python": "3.14.5",
"scope": "offline_GET_HEAD_existing_selected_representation_decision",
"snapshot_ascii": "{\"seq\":7,\"time_us\":2000,\"voltage_mv\":1200}",
"authored_units": {
"seq": "record count",
"time_us": "synthetic microseconds",
"voltage_mv": "integer millivolts"
},
"snapshot_byte_count": 42,
"strong_etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"condition_contract": "None | sole wildcard str * | nonempty pre-parsed tuple[EntityTag]",
"tag_character_contract": "visible ASCII etagc subset; comma/backslash literal; no raw-header parser",
"scenario_decisions": [
{
"method": "GET",
"condition": null,
"status": 200,
"etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"body_byte_count": 42
},
{
"method": "GET",
"condition": [
"\"different\""
],
"status": 200,
"etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"body_byte_count": 42
},
{
"method": "GET",
"condition": [
"\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\""
],
"status": 304,
"etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"body_byte_count": 0
},
{
"method": "GET",
"condition": [
"W/\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\""
],
"status": 304,
"etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"body_byte_count": 0
},
{
"method": "GET",
"condition": "*",
"status": 304,
"etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"body_byte_count": 0
},
{
"method": "GET",
"condition": [
"\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\""
],
"status": 200,
"etag": "\"sha256-a45ce8ef0c1d761ff6ea06694e1932dca637d39a4ae016818e1f52c11992b582\"",
"body_byte_count": 42
},
{
"method": "HEAD",
"condition": null,
"status": 200,
"etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"body_byte_count": 0
},
{
"method": "HEAD",
"condition": [
"\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\""
],
"status": 304,
"etag": "\"sha256-8c3ce3fb66e0d74ae2a36e92d848faa62efc027553362af45af0d148c731da58\"",
"body_byte_count": 0
}
],
"network_server_cache_auth_or_freshness_implemented": false,
"checks": {
"absent_condition_get_returns200_and_exact_bytes": true,
"different_tag_get_returns200_and_body": true,
"matching_strong_tag_get_returns304_without_body": true,
"weak_client_tag_matches_strong_current_tag": true,
"weak_comparison_ignores_either_weak_flag": true,
"any_tag_in_preparsed_tuple_can_match": true,
"sole_wildcard_matches_existing_representation": true,
"empty_bytes_are_still_an_existing_representation": true,
"unconditional_head_returns200_without_body": true,
"matching_head_returns304_without_body": true,
"changed_bytes_with_old_tag_return200": true,
"tag_is_deterministic_and_quoted_strong_format": true,
"literal_comma_and_backslash_tags_are_not_split_or_unescaped": true,
"opaque_comparison_is_case_sensitive_and_allows_empty": true,
"quoted_star_tag_is_distinct_from_wildcard": true,
"unsupported_methods_and_nonbytes_body_rejected": true,
"raw_headers_empty_or_mixed_tag_tuples_rejected": true,
"unsafe_or_wrong_type_tag_components_rejected": true
}
}
در HEAD بدون شرط نیز وضعیت ۲۰۰ است، اما بدنه فرستاده نمیشود؛ در HEAD مطابق، وضعیت ۳۰۴ است. ممنوعیت محتوا برای HEAD و ۳۰۴ در RFC 9110 مشخص شده است. مقدار body_byte_count در گزارش فقط طول بدنهٔ مدل است؛ Content-Length، اندازهٔ کل پیام، اندازهٔ بستههای شبکه یا شمار بایتهای واقعاً منتقلشده نیست.
RFC 9111 بازاعتبارسنجی را از تازگی پاسخ ذخیرهشده جدا میکند. ما سن، زمان انقضا، ذخیرهسازی و بازاستفادهٔ پاسخ را پیاده نکردهایم. همچنین ETag مجوز دسترسی، امضای فرستنده یا اثبات کالیبراسیون حسگر نیست. برای دادهٔ شخصی، انتخاب سیاست نگهداری و تفکیک کاربران مرحلهای مستقل است؛ پاسخ ۳۰۴ به تنهایی محرمانگی ایجاد نمیکند. هیچ دادهٔ شخصی در نمونه وجود ندارد.
۵. چهار تمرین پاسخدار برای مرزهای قرارداد

تمرین اول: اگر دریافتکننده نسخهٔ ضعیف برچسب فعلی را معرفی کند، آیا چون برچسب سرور قوی است باید ۲۰۰ بگیرد؟ پاسخ: در همین If-None-Match و مسیر GET، خیر؛ opaque برابر است و مقایسهٔ ضعیف برقرار میشود. نتیجهٔ مدل ۳۰۴ با بدنهٔ تهی است.
تمرین دوم: ستاره برای بازنمایی خالی چه میکند؟ پاسخ: چون بازنمایی موجود است، ۳۰۴ میگیریم. تابع اصلاً وضعیت منبع ناموجود را نمیپذیرد؛ این تمرین را به پاسخ مناسب برای منبع حذفشده تعمیم ندهید.
تمرین سوم: چرا برچسب a,b به دو عضو تقسیم نمیشود؟ پاسخ: آن را به صورت یک EntityTag از پیش تفکیکشده دادهایم. ویرگول داخل opaque بخشی از مقدار است؛ تجزیهٔ متن نقلقولدار مسئولیت دیگری است و در این مثال وجود ندارد.
تمرین چهارم: آیا صفر بودن بدنهٔ ۳۰۴ ثابت میکند درخواست رایگان یا داده محرمانه بوده است؟ پاسخ: خیر؛ پردازش، فراداده و انتقال واقعی را نسنجیدهایم و مجوزدهی هم نداریم. مرزبندی پیام حسگر روی جریان TCP نیز مرحلهای مستقل از معنای HTTP است؛ عبور آزمون هر کدام، جای آزمون اتصال کامل را نمیگیرد.
منابع و مطالعهٔ بیشتر
- RFC9110؛ If-None-Match، ETag، مقایسهٔ ضعیف و پاسخ۳۰۴
- RFC9111؛ بازاعتبارسنجی، تازگی و محدودهٔ ذخیرهسازی پاسخ
- Python3.14 hashlib؛ sha256، بایت و hexdigest
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

