فهرست مقاله
در یک برنامهٔ تلهمتری، پایان فرستادن درخواست لزوماً پایان دریافت پاسخ نیست. اگر این دو اتفاق را با یک پرچم مشترک نمایش دهیم، ممکن است پاسخِ هنوز خواندهنشده کنار گذاشته شود. برای روشن کردن مسئله، مدلی کوچک و کاملاً آفلاین میسازیم که جهت ارسال، پایان منبع ورودی و بستهشدن شیء را جدا نگه میدارد. دادهها بایتهای ساختگیاند؛ اتصال شبکه، حسگر یا سرویس واقعی در این آزمایش وجود ندارد.
۱. نیمبستن یعنی پایان یک جهت، نه پایان همهٔ کار

بخش نیمبستهٔ RFC 9293 استقلال بستهشدن دو جهت TCP را توضیح میدهد. از همین مفهوم برای طرح یک قرارداد کاربردی الهام میگیریم: برنامه میتواند اعلام کند که دیگر چیزی نمیفرستد، ولی همچنان منتظر پاسخ باشد. پرچم write_open این امکان ارسالِ محلی را نشان میدهد و دربارهٔ آمادهبودن دادهٔ دریافتی تصمیم نمیگیرد.
پرچم دوم، source_finished، میگوید در دنبالهٔ رویدادهای ساختگی ما دیگر دادهٔ ورودی تازهای نخواهد آمد. پرچم سوم، closed، عمر همین شیء آموزشی را پایان میدهد. این نامها، حالتهای کامل TCP مانند CLOSE-WAIT یا TIME-WAIT نیستند. مدل نه شمارهٔ توالی دارد و نه دستدهی، تأیید دریافت یا بازفرستادن؛ موفقیت آزمون آن، سازگاری پیادهسازی یک پشتهٔ TCP را اثبات نمیکند.
سناریوی اصلی روشن است: ابتدا سه بایت REQ در دفتر پذیرش ارسال ثبت میشود؛ سپس جهت ارسال محلی پایان مییابد. پاسخ پنجبایتی ABCDE بعداً وارد بافر میشود. درخواست و پاسخ فقط برای فهم ترتیب عملیات انتخاب شدهاند و پروتکل کاربردی یا محتوای اندازهگیری تعریف نمیکنند. تا زمانی که شیء کاملاً بسته نشده، نیمبستن ارسال مانع افزودن و خواندن پاسخ نیست.
۲. بافر خالی، انتظار و EOF سه معنای متفاوت دارند

read با maximum مثبت، حداکثر همان تعداد بایت از بافر خودمان برمیدارد. اگر بایتی موجود باشد، وضعیت data همراه داده برمیگردد؛ حتی اگر پایان منبع قبلاً ثبت شده باشد. فقط وقتی بافر خالی و source_finished درست باشد، وضعیت eof داریم. اگر بافر فعلاً خالی ولی منبع باز باشد، نتیجه would_wait است. این وضعیت، انتظار منطقی مدل است و عملیات مسدودشوندهٔ سیستمعامل اجرا نمیکند.
توضیح RECEIVE در RFC نیز دادهٔ دریافتشده اما هنوز تحویلنداده را پس از پایان سمت مقابل از بین نمیبرد. در مدل، source_end یک نشانگر انتهای منبعِ بالادستی است؛ ادعا نمیکنیم برنامه قبلاً از socket پاسخ خالی دیده، ولی هنوز بایت در بافر کرنل مانده است. بافر pending مشخصاً بافر شیء آموزشی خودمان است.
از رویداد incoming با بایت خالی برای نشاندادن پایان استفاده نمیکنیم؛ پایان متد جداگانه دارد. در نتیجه دادهٔ خالی، نبود دادهٔ فعلی و پایان قطعی منبع با هم مخلوط نمیشوند. واحد maximum و طول پاسخ، بایت است؛ تعداد نویسهٔ فارسی یا تعداد نمونهٔ ولتاژ نیست. رمزگشایی متن و بررسی کاملبودن رکورد، مسئولیت لایهٔ دیگری خواهد بود.
۳. کد کامل و سیاستهای صریحِ همین مدل

کد با Python 3.14.5 اجرا شده و حتی ماژول socket را وارد نمیکند. payload باید bytes دقیق و غیرخالی باشد؛ رشته، bytearray و memoryview پذیرفته نمیشوند. maximum نیز int دقیق و مثبت است؛ bool، عدد اعشاری، NaN و صفر رد میشوند. این سختگیری انتخاب API آموزشی ماست، نه فهرست انواع پذیرفتهشدهٔ رابط واقعی شبکه.
"""Original offline two-direction application model, NOT a TCP/socket stack."""
import itertools
import json
import platform
class ModelStateError(ValueError):
pass
def _nonempty_bytes(payload):
if type(payload) is not bytes:
raise TypeError('payload must be exact bytes')
if not payload:
raise ValueError('this model requires nonempty data events')
class HalfCloseModel:
def __init__(self):
self.write_open = True
self.source_finished = False
self.closed = False
self.pending = b''
self.accepted_send = b''
def _ensure_open(self):
if self.closed:
raise ModelStateError('model object is closed')
def send(self, payload):
self._ensure_open()
_nonempty_bytes(payload)
if not self.write_open:
raise ModelStateError('local sending direction ended')
self.accepted_send += payload # logical acceptance only, not delivery/ACK
return len(payload)
def shutdown_write(self):
self._ensure_open()
self.write_open = False # repeated call is our idempotent policy
def incoming(self, payload):
self._ensure_open()
_nonempty_bytes(payload)
if self.source_finished:
raise ModelStateError('data after incoming source end')
self.pending += payload
def source_end(self):
self._ensure_open()
self.source_finished = True # end marker, not an emitted TCP FIN packet
def read(self, maximum):
self._ensure_open()
if type(maximum) is not int:
raise TypeError('maximum must be exact int bytes')
if maximum < 1:
raise ValueError('maximum must be positive')
if self.pending:
data, self.pending = self.pending[:maximum], self.pending[maximum:]
return 'data', data
return ('eof' if self.source_finished else 'would_wait'), b''
def close(self):
self.closed = True
self.write_open = False
self.pending = b'' # explicit local discard policy, not a flush/FIN claim
def snapshot(self):
return {'closed': self.closed, 'write_open': self.write_open,
'source_finished': self.source_finished, 'pending_hex': self.pending.hex(),
'accepted_send_hex': self.accepted_send.hex()}
def _raises(expected, function, *args):
try:
function(*args)
except expected:
return True
return False
def _partitions(payload):
for boundaries in itertools.product((False, True), repeat=len(payload) - 1):
cuts = [0] + [i + 1 for i, cut in enumerate(boundaries) if cut] + [len(payload)]
yield [payload[a:b] for a, b in zip(cuts, cuts[1:])]
def _reference_drain(parts, widths):
"""Independent whole-concatenated reference, no model state or methods."""
full = b''.join(parts)
offset, answer = 0, []
for width in itertools.cycle(widths):
piece = full[offset:offset + width]
answer.append(('data', piece) if piece else ('eof', b''))
offset += len(piece)
if not piece:
return answer
def run_example():
model = HalfCloseModel()
accepted = model.send(b'REQ')
model.shutdown_write()
waiting = model.read(2)
model.incoming(b'AB')
model.incoming(b'CDE')
model.source_end()
main_reads = [model.read(2) for _ in range(4)]
before_close = model.snapshot()
model.close()
final = model.snapshot()
peer_first = HalfCloseModel()
peer_first.source_end()
peer_send_count = peer_first.send(b'ACK')
local_first = HalfCloseModel()
local_first.shutdown_write()
local_first.incoming(b'REPLY')
local_read = local_first.read(10)
source_closed = HalfCloseModel()
source_closed.source_end()
source_before = source_closed.snapshot()
source_rejected = _raises(ModelStateError, source_closed.incoming, b'late')
write_closed = HalfCloseModel()
write_closed.shutdown_write()
write_before = write_closed.snapshot()
send_rejected = _raises(ModelStateError, write_closed.send, b'late')
buffered = HalfCloseModel()
buffered.incoming(b'XYZ')
buffered.source_end()
buffered_read = buffered.read(9)
empty = HalfCloseModel()
wait_result = empty.read(1)
empty.source_end()
eof_result = empty.read(1)
duplicate = HalfCloseModel()
duplicate.shutdown_write()
first_shutdown = duplicate.snapshot()
duplicate.shutdown_write()
same_shutdown = duplicate.snapshot() == first_shutdown
duplicate.close()
first_close = duplicate.snapshot()
duplicate.close()
same_close = duplicate.snapshot() == first_close
eof_before = empty.snapshot()
eof_repeat = [empty.read(4), empty.read(4)]
discard = HalfCloseModel()
discard.incoming(b'unread')
discard.close()
after_closed_rejections = [
_raises(ModelStateError, discard.send, b'A'),
_raises(ModelStateError, discard.incoming, b'B'),
_raises(ModelStateError, discard.read, 1),
_raises(ModelStateError, discard.source_end),
_raises(ModelStateError, discard.shutdown_write),
]
reference_checks = []
for parts in _partitions(b'ABCDE'):
for widths in ((1,), (2,), (3,), (2, 1, 3)):
sample = HalfCloseModel()
for part in parts:
sample.incoming(part)
sample.source_end()
expected = _reference_drain(parts, widths)
actual = [sample.read(width) for width in itertools.islice(itertools.cycle(widths), len(expected))]
reference_checks.append(actual == expected and sample.pending == b'')
bad = HalfCloseModel()
initial = bad.snapshot()
invalid_payloads = ('A', bytearray(b'A'), memoryview(b'A'), None, b'')
invalids = [
_raises((TypeError, ValueError), action, payload)
for action in (bad.send, bad.incoming) for payload in invalid_payloads]
invalids += [_raises((TypeError, ValueError), bad.read, width)
for width in (True, 0, -1, 1.0, float('nan'), None)]
direction_a, direction_b = HalfCloseModel(), HalfCloseModel()
direction_a.source_end()
direction_a.shutdown_write()
direction_b.shutdown_write()
direction_b.source_end()
byte_log = HalfCloseModel()
counts = [byte_log.send(b'A'), byte_log.send(b'BC')]
checks = {
'hand_request_log_three_bytes': accepted == 3 and before_close['accepted_send_hex'] == '524551',
'write_shutdown_still_permits_waiting_read': waiting == ('would_wait', b'')
and before_close['write_open'] is False,
'hand_buffer_drained_before_eof': main_reads == [('data', b'AB'), ('data', b'CD'), ('data', b'E'), ('eof', b'')],
'full_close_terminal_local_flags': final == {'closed': True, 'write_open': False,
'source_finished': True, 'pending_hex': '', 'accepted_send_hex': '524551'},
'incoming_source_end_does_not_close_local_send': peer_send_count == 3
and peer_first.accepted_send == b'ACK' and peer_first.write_open is True,
'local_write_end_does_not_close_incoming_data': local_read == ('data', b'REPLY')
and local_first.write_open is False and local_first.source_finished is False,
'incoming_after_source_end_rejected_without_change': source_rejected and source_closed.snapshot() == source_before,
'send_after_write_end_rejected_without_change': send_rejected and write_closed.snapshot() == write_before,
'source_end_preserves_existing_unread_data': buffered_read == ('data', b'XYZ')
and buffered.read(1) == ('eof', b''),
'no_current_data_is_distinct_from_eof': wait_result == ('would_wait', b'') and eof_result == ('eof', b''),
'repeated_shutdown_and_close_are_model_idempotent': same_shutdown and same_close,
'repeated_eof_reads_leave_state_unchanged': eof_repeat == [('eof', b''), ('eof', b'')]
and empty.snapshot() == eof_before,
'full_close_discards_pending_by_explicit_policy': discard.pending == b'' and discard.source_finished is False,
'operations_after_full_close_rejected': all(after_closed_rejections),
'independent_whole_stream_reference': len(reference_checks) == 64 and all(reference_checks),
'invalid_payload_and_read_limits_no_state_change': all(invalids) and bad.snapshot() == initial,
'independent_direction_end_order_same_toy_flags': direction_a.snapshot() == direction_b.snapshot()
and direction_a.read(1) == ('eof', b'') and not direction_a.closed,
'accepted_send_log_preserves_bytes_without_message_frames': counts == [1, 2]
and byte_log.accepted_send == b'ABC' and len(byte_log.accepted_send) == sum(counts),
}
assert len(checks) == 18 and all(type(value) is bool and value for value in checks.values())
return {'runtime': {'python': platform.python_version(), 'libraries': ['stdlib only, no socket import']},
'contract': {'layer': 'authored offline application model; no TCP state machine or syscalls',
'data_unit': 'bytes', 'would_wait': 'no pending bytes and source still open',
'eof': 'source ended and all pending bytes consumed',
'close': 'local terminal object policy discards unread bytes; does not imply observed peer end',
'idempotency': 'shutdown_write/source_end/close repeated according to own model rules'},
'metrics': {'accepted_request_bytes': accepted, 'first_empty_read_status': waiting[0],
'response_reads': [{'status': status, 'data_hex': payload.hex()} for status, payload in main_reads],
'before_full_close': before_close, 'after_full_close': final,
'peer_end_then_local_send_bytes': peer_send_count,
'concatenated_reference_cases': len(reference_checks)},
'checks': checks,
'limitations': ['No network, socket, ACK/FIN/RST, retransmission, timing, kernel state, server or TCP compliance test.',
'Logical send acceptance is not physical transmission, peer receipt or application processing.',
'Own idempotency/errors/close discard policy must not be projected onto platform socket behavior.',
'Finite buffers/logs retained in memory; no general memory bound, framing decoder, CRC or payload-field validation.']}
if __name__ == '__main__':
print(json.dumps(run_example(), ensure_ascii=False, indent=2))
مرجع shutdown پایتون تفاوت SHUT_WR و دو جهت اتصال را توضیح میدهد. shutdown_write ما تنها write_open را خاموش میکند. در مقابل، close این شیء را پایانی کرده و بایتهای خواندهنشدهٔ خودش را دور میریزد؛ این سیاست محلیِ صریح است و زمان رسیدن FIN، رفتار RST یا تخلیهٔ واقعی سوکت را نتیجه نمیدهد.
تکرار shutdown_write و close طبق قرارداد خودمان بیاثر است. source_end نیز پیش از close قابل تکرار است، ولی دادهٔ تازه پس از پایان منبع خطا دارد. عملیات دیگر روی شیء کاملاً بسته رد میشوند. close پرچم source_finished را بهدروغ درست نمیکند: دورریختن محلیِ بافر، مشاهدهٔ پایان سمت مقابل نیست. پیامهای خطای مدل و ترتیبشان را نباید به تمام سیستمعاملها تعمیم داد.
متد send تمام payload معتبر را به دفتر محلی اضافه و طولش را برمیگرداند. این عدد فقط پذیرش در مدل است؛ تأیید دریافتِ همتا یا پردازش درخواست نیست. send واقعی پایتون میتواند تنها بخشی از داده را بپذیرد. هیچیک از منطق مدیریت ارسال جزئی، محدودیت حافظه یا تضمین تحویل در این کلاس اجرا نشده است.
۴. پاسخ دستی و مرجع مستقل برای هجده گروه بررسی

بعد از پایان ارسال، نخستین read با اندازهٔ دو، would_wait است. سپس AB و CDE وارد میشوند و source_end ثبت میشود. چهار خواندن دوبایتی بهترتیب AB، CD، E و eof میدهند. طول خواندن سوم یک بایت است؛ درخواست حداکثر دو، تعهدِ دقیقاً دو نیست. در خروجی ثبتشده، بایتها به شکل هگزادسیمال آمدهاند تا نوع داده با رشتهٔ نمایشی اشتباه نشود.
{
"runtime": {
"python": "3.14.5",
"libraries": [
"stdlib only, no socket import"
]
},
"contract": {
"layer": "authored offline application model; no TCP state machine or syscalls",
"data_unit": "bytes",
"would_wait": "no pending bytes and source still open",
"eof": "source ended and all pending bytes consumed",
"close": "local terminal object policy discards unread bytes; does not imply observed peer end",
"idempotency": "shutdown_write/source_end/close repeated according to own model rules"
},
"metrics": {
"accepted_request_bytes": 3,
"first_empty_read_status": "would_wait",
"response_reads": [
{
"status": "data",
"data_hex": "4142"
},
{
"status": "data",
"data_hex": "4344"
},
{
"status": "data",
"data_hex": "45"
},
{
"status": "eof",
"data_hex": ""
}
],
"before_full_close": {
"closed": false,
"write_open": false,
"source_finished": true,
"pending_hex": "",
"accepted_send_hex": "524551"
},
"after_full_close": {
"closed": true,
"write_open": false,
"source_finished": true,
"pending_hex": "",
"accepted_send_hex": "524551"
},
"peer_end_then_local_send_bytes": 3,
"concatenated_reference_cases": 64
},
"checks": {
"hand_request_log_three_bytes": true,
"write_shutdown_still_permits_waiting_read": true,
"hand_buffer_drained_before_eof": true,
"full_close_terminal_local_flags": true,
"incoming_source_end_does_not_close_local_send": true,
"local_write_end_does_not_close_incoming_data": true,
"incoming_after_source_end_rejected_without_change": true,
"send_after_write_end_rejected_without_change": true,
"source_end_preserves_existing_unread_data": true,
"no_current_data_is_distinct_from_eof": true,
"repeated_shutdown_and_close_are_model_idempotent": true,
"repeated_eof_reads_leave_state_unchanged": true,
"full_close_discards_pending_by_explicit_policy": true,
"operations_after_full_close_rejected": true,
"independent_whole_stream_reference": true,
"invalid_payload_and_read_limits_no_state_change": true,
"independent_direction_end_order_same_toy_flags": true,
"accepted_send_log_preserves_bytes_without_message_frames": true
},
"limitations": [
"No network, socket, ACK/FIN/RST, retransmission, timing, kernel state, server or TCP compliance test.",
"Logical send acceptance is not physical transmission, peer receipt or application processing.",
"Own idempotency/errors/close discard policy must not be projected onto platform socket behavior.",
"Finite buffers/logs retained in memory; no general memory bound, framing decoder, CRC or payload-field validation."
]
}
مرجع مستقل ابتدا همهٔ قطعههای ورودی را یکبار به هم میچسباند و با اندیس جدا، جواب خواندنها را میسازد. پنج بایت، شانزده روش تقسیم به قطعهٔ غیرخالی دارند؛ با چهار الگوی اندازهٔ خواندن، شصتوچهار مقایسه انجام میشود. همهٔ این مقایسهها یک گروه بررسیاند. آنها هجده گروه مستقل یا شصتوچهار اتصال شبکه نیستند؛ تعداد واقعی گروههای نامدار هجده است.
گروههای دیگر استقلال دو ترتیب پایان، رد ارسالِ دیرهنگام، حفظ بافر تا تخلیه، تکرار EOF و تغییرنکردن وضعیت پس از ورودی نامعتبر را میسنجند. مرجع تجمیعی فقط خواندن دنبالهٔ نهایی را کنترل میکند؛ برای دورهٔ بدون داده، جواب دستی would_wait جدا داریم. پوشش ترتیبهای ممکن همهٔ رویدادها، ظرفیت نامحدود بافر یا ایمنی همزمانی از این بررسیها به دست نمیآید.
مرزبندی پیام حسگر روی جریان TCP مسئلهٔ پیدا کردن مرز رکورد را توضیح میدهد. آزمون CRC بستهٔ ساختگی نیز به مقدار بررسی بایتها مربوط است. در این درس هیچکدام دوباره پیادهسازی نشدهاند؛ موضوع، عمر دو جهت و پایان خواندن است. منابع در ۲۴ مهر ۱۴۰۵ خوانده شدند؛ RFC مربوط به اوت ۲۰۲۲ و مستندات مرجع پایتون نسخهٔ 3.14.8 هستند.
۵. چهار پرسش با پاسخ کامل

پرسش اول: shutdown_write بهتنهایی EOF دریافت میسازد؟ پاسخ: خیر؛ فقط ارسال محلی پایان مییابد. اگر بافر خالی و منبع ورودی هنوز باز باشد، read مقدار would_wait میدهد و پاسخ بعدی همچنان قابل دریافت است.
پرسش دوم: پس از ورود ABCDE و نشانگر پایان، خواندن دوبایتی چه میدهد؟ پاسخ: AB، سپس CD، سپس E و در نوبت چهارم eof. پایان منبع بایتهای قبلیِ pending را حذف نمیکند؛ ابتدا باید مصرف شوند.
پرسش سوم: چرا close روی بافر خواندهنشده خطری آموزشی دارد؟ پاسخ: سیاست این مدل آن را دور میریزد؛ بنابراین پایان محلی را نباید جای تخلیهٔ پاسخ گذاشت. این انتخاب هیچ ادعایی دربارهٔ خطای واقعی سوکت یا زمان ارسال FIN ندارد.
پرسش چهارم: برگشت عدد سه از send ثابت میکند درخواست پردازش شده؟ پاسخ: خیر؛ فقط سه بایت در دفتر پذیرش محلی ثبت شده است. حتی پایان منبعِ ورودی نیز ارسال محلی را خودکار نمیبندد؛ هر جهت قرارداد جدا دارد و تأیید کاربردی باید مستقل طراحی شود.
منابع و مطالعهٔ بیشتر
- RFC 9293؛ نیمبسته بودن جهتها و تحویل دادهٔ بافرشده
- Python 3.14؛ shutdown، recv و send در رابط socket
بررسی منابع: ۲۴ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

