IRCv3: बैकवर्ड-कम्पेटिबल प्रोटोकॉल डिज़ाइन के सबक
IRCv3 बिना तोड़े 1988 के प्रोटोकॉल को आधुनिक बनाता है। कैपेबिलिटी नेगोशिएशन, मैसेज टैग और बैकवर्ड-कम्पेटिबल प्रोटोकॉल विकास के सबक।

1988 में, एक फ़िनिश यूनिवर्सिटी के छात्र ने एक चैट प्रोटोकॉल लिखा जो कुछ पन्नों में समा जाता था। लगभग चार दशक बाद, वही प्रोटोकॉल — IRC — आज भी रोज़ लाखों संदेश ढोता है, और IRCv3 वर्किंग ग्रुप चुपचाप हम सबको सिखा रहा है कि इंस्टॉल्ड बेस को जलाए बिना प्रोटोकॉल कैसे विकसित किया जाए। अगर आपने कभी ऐसे API को वर्ज़न करने की कोशिश की है जिस पर अनजान लोग निर्भर हैं, तो IRCv3 वह केस स्टडी है जिससे आपको सीख लेनी चाहिए।
मैं सालों से IRC के खिलाफ बॉट और छोटे-छोटे टेस्ट क्लाइंट बनाता रहा हूँ, और जो बात मुझे लगातार चौंकाती है वह यह है कि जब दोनों तरफ़ नए एक्सटेंशन हों, तो अनुभव कितना आधुनिक लग सकता है। सर्वर से रीप्ले होने वाली हिस्ट्री। असली ऑथेंटिकेशन। बाउंसर के बावजूद बचे रहने वाले मैसेज टाइमस्टैम्प। फिर भी 1995 में लिखा गया क्लाइंट आज उसी सर्वर से कनेक्ट होकर बातचीत कर सकता है। यह मेल संयोग से नहीं होता, यह कुछ सोच-समझकर लिए गए डिज़ाइन फ़ैसलों और कुछ दर्दनाक गलतियों से निकले सबक का नतीजा है जिन्हें कम्युनिटी ने रास्ते से हटाना सीखा।
मूल पाप: ऐसी क्षमताएँ जिनके बारे में पूछा ही नहीं जा सकता था
कोर IRC प्रोटोकॉल, जो RFC 1459 और बाद में RFC 2812 में दर्ज है, में क्लाइंट के लिए यह पूछने का कोई तरीका नहीं था कि सर्वर क्या सपोर्ट करता है। कोई तरीका नहीं। आप कनेक्ट करते, NICK और USER भेजते, और फिर अंदाज़ा लगाते। क्या सर्वर किसी खास मोड या एक्सटेंशन को सपोर्ट करता है? उसे भेजकर देखो और अगर error आए तो समझ लो नहीं करता। यह सुनने में पुराना लगता है, जब तक आप ऐसी क्लाइंट लाइब्रेरी न संभालें जिसे पाँच अलग-अलग सर्वर इम्प्लीमेंटेशन के लिए अलग व्यवहार करना पड़ता है, और हर एक स्पेसिफ़िकेशन के अस्पष्ट हिस्सों को थोड़ा अलग तरह से समझता है।
यहाँ की समस्या सिर्फ़ सैद्धांतिक नहीं है। डेवलपर्स कमांड भेजकर और error numerics पार्स करके फ़ीचर टटोलते थे, जो एक नाज़ुक गड़बड़ था और सर्वर का कोई error स्ट्रिंग बदलते ही टूट जाता था। बॉट गलत धारणाओं के साथ चैनल जॉइन करते और लगातार failures स्पैम करते। यह specification-implementation gap का सबसे साफ़ उदाहरण है: जब स्पेक आपको कोई discovery mechanism नहीं देता, तो हर इम्प्लीमेंटर अपना तरीका खुद ईजाद करता है, और अक्सर बुरी तरह।
IRCv3 का हल था कैपेबिलिटी नेगोशिएशन, और यह इतना सरल है कि थोड़ा शर्मिंदा कर देता है। कनेक्शन रजिस्ट्रेशन के दौरान क्लाइंट CAP LS भेजता है और सर्वर उन नामित कैपेबिलिटी की सूची लौटाता है जिन्हें वह सपोर्ट करता है, जैसे sasl, server-time या message-tags। फिर क्लाइंट ठीक उतने ही सबसेट के लिए CAP REQ भेजता है, सर्वर उसे acknowledge करता है, और दोनों पक्ष सेशन की सपोर्ट क्षमताओं की साफ़ और साझा समझ के साथ आगे बढ़ते हैं। जो लीगेसी क्लाइंट कभी CAP नहीं भेजता, उसे पुराना व्यवहार ही मिलता है। जो लीगेसी सर्वर CAP नहीं समझता, वह एक error लौटाता है जिसे क्लाइंट सुरक्षित रूप से नज़रअंदाज़ करके फ़ॉलबैक कर सकता है।
-> CAP LS 302
<- CAP * LS :sasl=PLAIN,EXTERNAL server-time message-tags echo-message
-> CAP REQ :sasl server-time
<- CAP * ACK :sasl server-time
-> AUTHENTICATE PLAIN
<- AUTHENTICATE +
-> AUTHENTICATE dXNlcgB1c2VyAHBhc3N3b3Jk
<- 900 jane jane!jane@host jane :You are now logged in
यह सबक IRC से कहीं आगे तक लागू होता है। अगर आप आज कोई भी प्रोटोकॉल या API डिज़ाइन कर रहे हैं, तो पहले वर्ज़न में ही नेगोशिएशन मैकेनिज़्म बना लें, भले ही अभी नेगोशिएट करने को कुछ न हो। लागत बस एक मैसेज एक्सचेंज है; फ़ायदा यह है कि भविष्य का हर फ़ीचर opt-in बन जाता है, न कि breaking change।
सबक: एक्सटेंसिबिलिटी नई कमांड से बेहतर है
IRCv3 का दूसरा बड़ा विचार है मैसेज टैग, और यह एक असली गलती से निकला। लोग नई कमांड परिभाषित करके या मौजूदा मैसेज पैरामीटर में ज़बरदस्ती अर्थ ठूँसकर फ़ीचर जोड़ने की कोशिश करते रहे। IRC का वायर फ़ॉर्मेट बेहद संक्षिप्त है: एक prefix, एक command, और trailing parameters की सूची। मेटाडेटा टाँगने की कोई जगह नहीं थी। इसलिए जब किसी को replay हुई हिस्ट्री पर टाइमस्टैम्प चाहिए होते, या यह दिखाना होता कि मैसेज किस अकाउंट ने भेजा, तो पुराना जवाब था नया numeric गढ़ो या trailing text को तोड़-मरोड़ो। दोनों तरीके पुराने क्लाइंट तोड़ते थे या हर पार्सर में नए special cases जुड़वा देते थे।
हल यह निकला कि मैसेज की शुरुआत में एक नया, वैकल्पिक सेक्शन जोड़ा गया: prefix से पहले @key=value;key2=value2। सबसे अहम बात यह है कि सर्वर सिर्फ़ उन्हीं क्लाइंट को टैग भेजते हैं जिन्होंने message-tags कैपेबिलिटी नेगोशिएट की हो, इसलिए पुराने क्लाइंट को ऐसा कोई बाइट नहीं मिलता जिसे वे पार्स न कर सकें। टाइमस्टैम्प वाला मैसेज वायर पर असल में ऐसा दिखता है:
@time=2024-03-14T09:26:00.000Z;account=jane :jane!jane@host PRIVMSG #rust :borrow checker finally clicked for me
यह वही आर्किटेक्चरल चाल है जो HTTP headers, gRPC metadata, या protobuf की unknown-field सहनशीलता में दिखती है: एक सामान्य extension point आरक्षित कर दो, और फ़ीचर प्रोटोकॉल की सर्जरी माँगना बंद कर देते हैं। server-time एक्सटेंशन, जो उस पुरानी बाउंसर समस्या को ठीक करता है जहाँ replay हुए सभी मैसेज ऐसे लगते हैं मानो अभी-अभी आए हों, असल में बस एक टैग है। +reply ड्राफ़्ट, जो क्लाइंट को किसी खास मैसेज के जवाब को thread में दिखाने देता है, एक और टैग है। कोई नई कमांड नहीं। कोई नया numeric नहीं। कोई version flag day नहीं।
मैंने खुद उल्टी गलती की है। कुछ साल पहले एक internal pub/sub सिस्टम में हमने एक JSON फ़ील्ड पर जादुई prefix लगाकर फ़ीचर जोड़ा, क्योंकि “एन्वेलप फ़ॉर्मेट बदलना बहुत मेहनत का काम था।” आठ महीने बाद हम तीन भाषाओं में उस जादू को खोलने के लिए regex संभाल रहे थे, और एक टीम का पार्सर जादुई स्ट्रिंग को सादा डेटा समझ बैठा, जिससे एक यादगार incident हुआ। एक सीधा, स्पष्ट मेटाडेटा फ़ील्ड एक दिन का खर्च होता। जादुई स्ट्रिंग की कीमत हमें महीनों में चुकानी पड़ी। IRCv3 के मैसेज टैग वही हैं जो उस सिस्टम में शुरू से होने चाहिए थे।

सबक: साफ़ पासवर्ड वाले workaround को ख़त्म करो
2000 के दशक में IRC क्लाइंट चलाने वाले किसी से भी पूछो कि लॉगिन कैसे होता था, तो जवाब एक ही मिलेगा: आप /msg NickServ IDENTIFY password टाइप करते थे। आपके क्रेडेंशियल एक सामान्य चैट मैसेज की तरह जाते थे, लॉग में दिखते थे, और कभी-कभी क्लाइंट कमांड गड़बड़ा देता तो सार्वजनिक चैनल में चिपक जाते थे। यह इतना आम था कि एक मीम बन गया। प्रोटोकॉल में ऑथेंटिकेशन की कोई अवधारणा नहीं थी, इसलिए इसे application layer पर जोड़ा गया।
IRCv3 ने इसे ठीक से हल किया, रजिस्ट्रेशन के दौरान SASL ऑथेंटिकेशन को मानकीकृत करके। कनेक्ट होने के बाद किसी बॉट से बात करने के बजाय क्लाइंट sasl कैपेबिलिटी नेगोशिएट करता है और हैंडशेक के हिस्से के रूप में ऑथेंटिकेट करता है, उस नickname से पहले जो किसी को दिखे। सर्वर पहले मैसेज से ही जानता है कि आप कौन हैं, और इसका असर बाकी सब पर पड़ता है: account-tag एक्सटेंशन हर मैसेज पर आपका अकाउंट नाम जोड़ सकता है, बॉट NickServ-पार्सिंग के अपने अनुमानों के बिना permission के फ़ैसले ले सकते हैं, और क्लाइंट ऐसे रजिस्ट्रेशन फ़्लो बना सकते हैं जिनमें NOTICE के टेक्स्ट की दीवार पढ़नी न पड़े।
बड़ा सबक यह है: जब कोई workaround सार्वभौमिक हो जाता है, तो वह workaround नहीं रहता, वह आपका de facto प्रोटोकॉल बन जाता है, और उसमें डिज़ाइन की कोई सावधानी नहीं होती। NickServ किसी RFC में नहीं था, फिर भी उसने दो दशकों तक क्लाइंट के व्यवहार को आकार दिया, उसकी failure modes समेत। जो काम लोग पहले से कर रहे थे (कनेक्ट पर ऑथेंटिकेट करना) उसे एक असली मैकेनिज़्म (SASL, जिसमें PLAIN, क्लाइंट सर्टिफ़िकेट के लिए EXTERNAL और आगे के लिए जगह है) से मानकीकृत करने से लोककथा इन्फ़्रास्ट्रक्चर बन गई। अगर आपके यूज़र्स सब एक ही अटपटे तरीके से कोई काम कर रहे हैं, तो वह आपका अगला स्पेक आइटम है, सपोर्ट FAQ नहीं।
सबक: हिस्ट्री एक प्रोटोकॉल समस्या है, क्लाइंट समस्या नहीं
IRC के ज़्यादातर जीवन में प्रोटोकॉल सख़्ती से लाइव था: अगर भेजे जाने के समय आपका क्लाइंट कनेक्ट नहीं था, तो वह मैसेज आपके लिए कभी था ही नहीं। कम्युनिटी का जवाब था बाउंसर, एक स्थायी रूप से कनेक्टेड प्रॉक्सी (ZNC इसका क्लासिक उदाहरण है) जो VPS पर चलता, आपका सेशन थामे रखता और जो छूट गया था उसे रीप्ले करता। बाउंसर काम करते थे, पर वे चलाने के लिए एक और इन्फ़्रास्ट्रक्चर थे, एक और क्रेडेंशियल संभालने को, और रीप्ले का अनुभव बुरा था: सब कुछ रीप्ले के समय के टाइमस्टैम्प के साथ आता था, और जो आपने लाइव देखा था उसके साथ क्रम से बाहर।
IRCv3 ने इस पर परतों में काम किया। पहले server-time ने असली प्राप्ति टाइमस्टैम्प साथ ले जाकर रीप्ले हुई हिस्ट्री को उपयोगी बनाया। फिर chathistory ड्राफ़्ट ने क्लाइंट को हिस्ट्री की रेंज माँगने का मानक तरीका दिया, और Ergo जैसे सर्वर चैनल की हिस्ट्री सीधे स्टोर करने लगे। नतीजा, जैसा कई लोगों ने दोस्तों के लिए छोटा Ergo सर्वर चलाकर देखा है, चौंकाने वाला है: एक हफ़्ते ऑफ़लाइन रहने के बाद फ़ोन से कनेक्ट करो, और सर्वर सब कुछ सही टाइमस्टैम्प के साथ रीप्ले कर देता है, न बाउंसर, न VPS की देखभाल। IRC के इतिहास में यह जीवन की गुणवत्ता में सबसे बड़ी छलांग है, और यह किसी चालाक क्लाइंट ट्रिक से नहीं, प्रोटोकॉल से आई।
एक गंभीरता से सुनने लायक तर्क भी है, और रोज़ IRC इस्तेमाल करने वाले इसे रखते हैं: IRC की ज़्यादातर कमियाँ क्लाइंट-साइड की हैं, और अच्छे क्लाइंट और बाउंसर से आज भी अच्छा अनुभव मिल सकता है। बात सही है, पर यह तर्क दूसरी दिशा से भी वही साबित करता है। हर क्लाइंट लेखक ने स्वतंत्र रूप से स्क्रॉलबैक जोड़ना, हाइलाइट ट्रैकिंग और रीप्ले डेडअप लिखा, और हर बार थोड़े अलग बग के साथ। कठिन हिस्से को प्रोटोकॉल और सर्वर में ले जाने का मतलब है कि वह एक बार, सही तरीके से लागू होता है, और हर क्लाइंट को फ़ायदा मिलता है। वेब डेवलपमेंट में हम यही फ़ैसला लगातार लेते हैं: इनवेरिएंट्स को प्लेटफ़ॉर्म में डालो (सोचो native fetch आने पर कितना ऐप कोड गायब हो गया, या TLS कितना बुनियादी बन गया) न कि हज़ार कोडबेस को आधा-अधूरा हल करने दो।
सबक: वैकल्पिक फ़ीचर अलग-अलग वास्तविकताएँ बनाते हैं
अब वह गलती जो IRCv3 ने पूरी तरह हल नहीं की है, और जो किसी भी एक्सटेंसिबल सिस्टम डिज़ाइन करने वाले के लिए सबसे ज़्यादा मायने रखती है। हर एक्सटेंशन वैकल्पिक है और स्वतंत्र रूप से नेगोशिएट होता है, इसलिए एक ही चैनल में दो यूज़र सचमुच अलग बातचीत देख सकते हैं। एक क्लाइंट के मैसेज में +reply टैग होते हैं और वे थ्रेडेड जवाब की तरह दिखते हैं; दूसरा क्लाइंट वही मैसेज सपाट, संदर्भ-रहित पंक्तियों की तरह दिखाता है। एक यूज़र इमोजी रिएक्शन को किसी मैसेज से जुड़े साफ़ चिह्न की तरह देखता है; दूसरे को कुछ नहीं दिखता, या कोई रहस्यमय fallback। IRCv3 का क्लाइंट सपोर्ट मैट्रिक्स हाँ और ना का एक ग्रिड है, और आपका अनुभव इस पर निर्भर करता है कि आप किस कॉलम में हैं।
यह graceful degradation का अंधेरा पहलू है। बैकवर्ड कम्पेटिबिलिटी ने यह तो पक्का किया कि कुछ टूटे नहीं, पर मॉडल में सुसंगतता की कोई गारंटी नहीं है। Slack और Discord, आप उनके बारे में जो भी सोचें, यह समस्या कभी नहीं झेलते: प्लेटफ़ॉर्म दोनों सिरों को इतनी सख़्ती से नियंत्रित करता है कि रिएक्शन सबके लिए रिएक्शन ही रहता है। IRCv3 ने नियंत्रण की जगह विकास को चुना, और उसकी कीमत है एक स्थायी असमान फ़ीचर लैंडस्केप, जहाँ ड्राफ़्ट स्पेक कभी-कभी सालों तक ड्राफ़्ट ही रहते हैं।
अगर आप कुछ एक्सटेंसिबल बना रहे हैं, तो इसके लिए पहले दिन से बजट रखें। कुछ उपाय हैं जिन पर IRCv3 और उससे मिलते-जुलते इकोसिस्टम पहुँचे हैं:
- Degradation जानबूझकर डिज़ाइन करो। reply टैग को गायब होने के बजाय पढ़ने लायक quoted लाइन में बदलना चाहिए। अगर fallback बेकार है, तो फ़ीचर को वैकल्पिक बनाकर शिप करने के लिए तैयार नहीं है।
- सपोर्ट मैट्रिक्स प्रकाशित करो और उसे ईमानदार रखो। IRCv3 की क्लाइंट और सर्वर तालिकाएँ यूज़र्स को सोच-समझकर चुनाव करने देती हैं; चुप्पी गलत सॉफ़्टवेयर पर दोष लगवाती है।
- एक बेसलाइन तय करो और उसे आगे बढ़ाओ। क्रमांकित रिलीज़ (3.1, 3.2) कैपेबिलिटी को एक साथ बाँधती हैं ताकि क्लाइंट ग्यारह अलग-अलग फ़्लैग के बजाय “3.2 सपोर्ट” को टारगेट कर सकें। अपने इकोसिस्टम को एक न्यूनतम आधार दो जिस पर वह एकत्र हो।
- ड्राफ़्ट की एक्सपायरी तय करो। वह ड्राफ़्ट जो सर्वर में शिप हो जाए पर कभी फ़ाइनल न हो (चैनल हिस्ट्री इसी नियति के करीब रही है) उस ड्राफ़्ट से भी बुरा है जो कभी आया ही न हो, क्योंकि वह बिना प्रतिबद्धता के व्यवहार को बाँट देता है।
इसका एक रूप हमने collaborative editing इतना कठिन क्यों है वाले लेख में देखा था: जैसे ही भागीदार साझा स्टेट के आकार पर असहमत हो सकते हैं, आपको स्पष्ट convergence नियम चाहिए, सिर्फ़ अच्छी नीयत नहीं। डिफ़ॉल्ट रूप से वैकल्पिक होना कैपेबिलिटी के लिए ठीक रणनीति है, पर यूज़र को दिखने वाले व्यवहार के लिए उन लोगों की योजना चाहिए जो मैट्रिक्स के दूसरी तरफ़ फँसे हैं।

आपके अगले API या प्रोटोकॉल के लिए इसका मतलब
चैट की पुरानी यादों को हटा दें तो IRCv3 किसी भी wire format को विकसित करने की एक संक्षिप्त playbook है। यह वह चेकलिस्ट है जो मैं किसी भी टीम को थमाऊँगा जो कोई प्रोटोकॉल, इवेंट schema, या यहाँ तक कि सार्वजनिक REST API शुरू कर रही है:
- पहले नेगोशिएशन भेजो। पहले वर्ज़न में ही कैपेबिलिटी एक्सचेंज रखो, भले ही सूची खाली हो। डिस्कवरी बाद में जोड़ना दस गुना कठिन है।
- एक्सटेंशन पॉइंट आरक्षित करो। टैग, headers, मेटाडेटा मैप: एक सामान्य स्लॉट जिस पर भविष्य के फ़ीचर मुख्य व्याकरण को छुए बिना टिक सकें।
- हर कनेक्शन पर नए व्यवहार को opt-in बनाओ। पुराने क्लाइंट को बिल्कुल वैसा ही पुराना व्यवहार मिलना चाहिए। IRCv3 सर्वर किसी ऐसे क्लाइंट को टैग नहीं भेजते जिसने उन्हें माँगा न हो।
- लोककथा को मानकीकृत करो। आपके यूज़र्स ने जो भी जुगाड़ अपनाया है (NickServ-शैली ऑथ, बाउंसर-शैली रीप्ले) वह एक ऐसा स्पेक है जो ठीक से लिखा जाना बाकी है।
- सपोर्ट मैट्रिक्स की योजना बनाओ। वैकल्पिक फ़ीचर अलग होते हैं; तय करो कि degraded अनुभव कैसा दिखेगा और कौन क्या सपोर्ट करता है, यह प्रकाशित करो।
- प्रगति को नामित मील के पत्थरों में बाँधो। अलग-अलग फ़्लैग implementers के लिए हैं; क्रमांकित रिलीज़ इंसानों के लिए।
मुझे जो सचमुच प्रभावित करता है वह है संयम। IRCv3 ने end-to-end encryption, federation का पुनर्डिज़ाइन, या कोई बड़ा फिर से लिखना नहीं चुना। उसने छोटी, तीखी समस्याएँ चुनीं (डिस्कवरी, मेटाडेटा, ऑथ, समय) और उन्हें ऐसे तरीकों से हल किया जिन्हें 1995 का क्लाइंट नज़रअंदाज़ कर सके। इसीलिए IRC अपनी पीढ़ी के इंटरनेट प्रोटोकॉल में लगभग अकेला बचा है जो ज़्यादातर अपने मूल डिज़ाइन के अनुसार चल रहा है, और इसीलिए आज एक वीकेंड प्रोजेक्ट $5 के VPS पर दोस्तों के समूह को हिस्ट्री सिंक, TLS और असली अकाउंट दे सकता है।
बैकवर्ड कम्पेटिबिलिटी बदलाव की अनुपस्थिति नहीं है। यह अनुशासन है कि हर बदलाव ऐसा हो जिसे अतीत सुरक्षित रूप से नज़रअंदाज़ कर सके।
अगली बार जब आप API को /v1 से /v2 पर ले जाकर हर उपभोक्ता को माइग्रेट करने के लिए मजबूर करने का मन बनाएँ, तो उस प्रोटोकॉल को याद कीजिए जिसने डायल-अप ज़माने के क्लाइंट और आधुनिक मोबाइल ऐप को एक ही चैनल साझा करने दिया। कैपेबिलिटी नेगोशिएट करें। नए कमांड गढ़ने के बजाय मेटाडेटा जोड़ें। फ़्लोर को मज़बूत रखें और छत को अपनी रफ़्तार से ऊपर उठने दें। IRCv3 साबित करता है कि ऐसा प्रोटोकॉल बनाया जा सकता है जो अपने बुज़ुर्गों का सम्मान करे और फिर भी नई चालें सीखे, और अगर 1988 की एक चैट प्रणाली यह कर सकती है, तो आपकी तीन साल पुरानी माइक्रोसर्विस के पास कोई बहाना नहीं है।


