आजकल WebSockets को काफी चर्चा मिल रही है। हम सुनते हैं कि वे "भविष्य" हैं। हम सुनते हैं कि Rails 5 में ActionCable की बदौलत उनका उपयोग पहले से आसान हो गया है। लेकिन वास्तव में WebSockets क्या हैं? वे कैसे काम करते हैं?
इस पोस्ट में हम एक साधारण WebSocket सर्वर को Ruby में स्क्रैच से बनाकर इन सवालों का जवाब देंगे। जब हम पूरा कर लेंगे, तो हमने ब्राउज़र और हमारे सर्वर के बीच द्वि-दिशात्मक संचार हासिल कर लिया होगा।
इस पोस्ट में कोड एक सीखने के अभ्यास के लिए है। यदि आप वास्तविक प्रोडक्शन ऐप में WebSockets लागू करना चाहते हैं, तो उत्कृष्ट websocket-ruby जेम देखें। आप WebSocket स्पेसिफिकेशन पर भी नज़र डाल सकते हैं।
यदि आपने कभी WebSockets के बारे में नहीं सुना
WebSockets का आविष्कार सामान्य HTTP कनेक्शन में अंतर्निहित कुछ समस्याओं को हल करने के लिए किया गया था। जब आप सामान्य HTTP कनेक्शन का उपयोग करके एक वेबपेज का अनुरोध करते हैं, तो सर्वर आपको सामग्री भेजता है और फिर कनेक्शन बंद कर देता है। यदि आप दूसरा पेज चाहते हैं, तो आपको दूसरा कनेक्शन बनाना होगा। यह सामान्य रूप से ठीक काम करता है, लेकिन कुछ उपयोग के मामलों के लिए यह सर्वोत्तम दृष्टिकोण नहीं है:
- चैट जैसे कुछ एप्लिकेशन के लिए, फ्रंट एंड को नया संदेश आते ही अपडेट करने की आवश्यकता होती है। यदि आपके पास केवल सामान्य HTTP अनुरोध हैं, तो इसका मतलब है कि आपको लगातार सर्वर को पोल करना होगा यह देखने के लिए कि क्या नई सामग्री है।
- यदि आपके फ्रंट-एंड एप्लिकेशन को सर्वर पर बहुत सारे छोटे अनुरोध करने की आवश्यकता है, तो प्रत्येक अनुरोध के लिए नए कनेक्शन बनाने का ओवरहेड प्रदर्शन की समस्या बन सकता है। यह HTTP2 में कम समस्या है।
WebSockets के साथ, आप सर्वर पर एक कनेक्शन बनाते हैं जिसे खुला रखा जाता है और द्वि-दिशात्मक संचार के लिए उपयोग किया जाता है।
क्लाइंट साइड
WebSockets का उपयोग आमतौर पर ब्राउज़र और वेब सर्वर के बीच संचार के लिए किया जाता है। ब्राउज़र साइड JavaScript में कार्यान्वित होता है। नीचे दिए गए उदाहरण में मैंने एक बहुत ही सरल JavaScript लिखा है जो मेरे स्थानीय सर्वर पर एक WebSocket खोलता है और उसे एक संदेश भेजता है।
<!doctype html> <html lang="en"> <head> <title>Websocket Client</title> </head> <body> <script> var exampleSocket = new WebSocket("ws://localhost:2345"); exampleSocket.onopen = function (event) { exampleSocket.send("Can you hear me?"); }; exampleSocket.onmessage = function (event) { console.log(event.data); } </script> </body> </html>
यदि मैं एक छोटा स्टैटिक सर्वर शुरू करता हूं और इस फाइल को अपने वेब ब्राउज़र में खोलता हूं, तो मुझे एक त्रुटि मिलती है। यह समझ में आता है, क्योंकि अभी तक कोई सर्वर नहीं है। हमें अभी भी एक बनाना है। :-)

सर्वर शुरू करना
WebSocket सामान्य HTTP अनुरोधों के रूप में जीवन शुरू करते हैं। उनका एक अजीब जीवनचक्र होता है:
- ब्राउज़र एक सामान्य HTTP अनुरोध भेजता है, जिसमें कुछ विशेष हेडर्स होते हैं जो कहते हैं "कृपया मुझे एक websocket बनाएं।"
- सर्वर एक निश्चित HTTP प्रतिक्रिया के साथ जवाब देता है, लेकिन कनेक्शन बंद नहीं करता।
- ब्राउज़र और सर्वर तब खुले कनेक्शन पर डेटा के फ्रेम्स का आदान-प्रदान करने के लिए एक विशेष websocket प्रोटोकॉल का उपयोग करते हैं।
तो हमारे लिए पहला कदम एक वेब सर्वर बनाना है। नीचे दिए गए कोड में, मैं सबसे सरल संभव वेब सर्वर बना रहा हूं। यह वास्तव में कुछ भी सर्व नहीं करता। यह बस एक अनुरोध की प्रतीक्षा करता है फिर उसे STDERR पर प्रिंट करता है।
require 'socket'
server = TCPServer.new('localhost', 2345)
loop do
# कनेक्शन की प्रतीक्षा करें
socket = server.accept
STDERR.puts "Incoming Request"
# HTTP अनुरोध पढ़ें। हम जानते हैं कि यह तब समाप्त होता है जब हम \r\n के अलावा कुछ भी नहीं वाली लाइन देखते हैं
http_request = ""
while (line = socket.gets) && (line != "\r\n")
http_request += line
end
STDERR.puts http_request
socket.close
end
यदि मैं सर्वर चलाता हूं, और अपने websocket टेस्ट पेज को रीफ्रेश करता हूं, तो मुझे यह मिलता है:
$ ruby server1.rb
Incoming Request
GET / HTTP/1.1
Host: localhost:2345
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: cG8zEwcrcLnEftn2qohdKQ==
यदि आप ध्यान देंगे, तो इस HTTP अनुरोध में WebSockets से संबंधित कई हेडर्स हैं। यह वास्तव में websocket हैंडशेक में पहला कदम है।
हैंडशेक
सभी WebSocket अनुरोध एक हैंडशेक से शुरू होते हैं। यह सुनिश्चित करने के लिए कि क्लाइंट और सर्वर दोनों समझते हैं कि WebSockets होने वाले हैं और वे दोनों प्रोटोकॉल संस्करण पर सहमत हैं। यह इस तरह काम करता है:
क्लाइंट इस तरह एक HTTP अनुरोध भेजता है
GET / HTTP/1.1
Host: localhost:2345
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: E4i4gDQc1XTIQcQxvf+ODA==
Sec-WebSocket-Version: 13
इस अनुरोध का सबसे महत्वपूर्ण हिस्सा Sec-WebSocket-Key है। क्लाइंट को उम्मीद है कि सर्वर XSS हमलों और कैशिंग प्रॉक्सी के खिलाफ सबूत के रूप में इस मान का एक संशोधित संस्करण वापस करेगा।
सर्वर प्रतिक्रिया देता है
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: d9WHst60HtB4IvjOVevrexl0oLA=
सर्वर प्रतिक्रिया बॉयलरप्लेट है सिवाय Sec-WebSocket-Accept हेडर के। यह हेडर इस तरह उत्पन्न होता है:
# क्लाइंट द्वारा प्रदान किया गया मान लें, इसमें एक जादुई
# स्ट्रिंग जोड़ें। SHA1 हैश उत्पन्न करें, फिर बेस64 एन्कोड करें।
Digest::SHA1.base64digest([sec_websocket_accept, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
आपकी आंखें आपको धोखा नहीं दे रही हैं। इसमें एक जादुई स्थिरांक शामिल है।
हैंडशेक लागू करना
आइए अपने सर्वर को हैंडशेक पूरा करने के लिए अपडेट करें। पहले, हम रिक्वेस्ट हेडर्स से सिक्योरिटी टोकन निकालेंगे:
# हेडर्स से सिक्योरिटी की निकालें।
# यदि मौजूद नहीं है, तो कनेक्शन बंद करें।
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
websocket_key = matches[1]
STDERR.puts "Websocket handshake detected with key: #{ websocket_key }"
else
STDERR.puts "Aborting non-websocket connection"
socket.close
next
end
अब, हम सिक्योरिटी की का उपयोग करके एक वैध प्रतिक्रिया उत्पन्न करते हैं:
response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
STDERR.puts "Responding to handshake with key: #{ response_key }"
socket.write <<-eos
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{ response_key }
eos
STDERR.puts "Handshake completed."
जब मैं websocket टेस्ट पेज को रीफ्रेश करता हूं, तो मुझे अब दिखाई देता है कि कनेक्शन त्रुटि नहीं है। कनेक्शन स्थापित हो गया!

यहां सर्वर का आउटपुट है, जिसमें सिक्योरिटी कीज और रिस्पांस की दिखाई दे रही है:
$ ruby server2.rb
Incoming Request
Websocket handshake detected with key: Fh06+WnoTQQiVnX5saeYMg==
Responding to handshake with key: nJg1c2upAHixOmXz7kV2bJ2g/YQ=
Handshake completed.
WebSocket फ्रेम प्रोटोकॉल
एक बार WebSocket कनेक्शन स्थापित हो जाने के बाद, HTTP का उपयोग नहीं किया जाता। इसके बजाय, डेटा का आदान-प्रदान WebSocket प्रोटोकॉल के माध्यम से होता है।
फ्रेम्स WebSocket प्रोटोकॉल की बुनियादी इकाई हैं।
WebSocket प्रोटोकॉल फ्रेम-आधारित है। लेकिन इसका क्या मतलब है?
जब भी आप अपने वेब ब्राउज़र को WebSocket पर डेटा भेजने के लिए कहते हैं, या अपने सर्वर को प्रतिक्रिया देने के लिए कहते हैं, तो डेटा चंक्स की एक श्रृंखला में विभाजित हो जाता है और उनमें से प्रत्येक चंक को कुछ मेटाडेटा में लपेटकर एक फ्रेम बनाया जाता है।
यहां फ्रेम संरचना कैसी दिखती है। शीर्ष पर संख्याएं बिट्स हैं। और कुछ फील्ड्स, जैसे विस्तारित पेलोड लंबाई, हमेशा मौजूद नहीं हो सकते:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
पहली चीज जो ध्यान खींच सकती है वह यह है कि यह एक बाइनरी प्रोटोकॉल है। हमें कुछ बिट मैनिपुलेशन करना होगा, लेकिन चिंता न करें — यह इतना कठिन नहीं होगा। आंकड़े के शीर्ष पर संख्याएं बिट्स हैं। और कुछ फील्ड्स हमेशा मौजूद नहीं हो सकते। उदाहरण के लिए, विस्तारित पेलोड लंबाई तब मौजूद होगी जब पेलोड 127 बाइट्स से कम हो।
डेटा प्राप्त करना
अब उनका हैंडशेक पूरा हो गया है, हम बाइनरी फ्रेम को पार्स करना शुरू कर सकते हैं। चीजों को सरल रखने के लिए, हम इनकमिंग फ्रेम को एक समय में एक बाइट देखेंगे। उसके बाद, हम इसे एक साथ रखेंगे ताकि आप इसे क्रिया में देख सकें।
बाइट 1: FIN और Opcode
उपरोक्त तालिका से, आप देख सकते हैं कि पहला बाइट (पहले आठ बिट्स) में कुछ डेटा होता है:
- FIN: 1 बिट यदि यह गलत है, तो संदेश कई फ्रेम्स में विभाजित है
- opcode: 4 बिट्स बताता है कि पेलोड टेक्स्ट है, बाइनरी है, या यह केवल कनेक्शन को जीवित रखने के लिए "पिंग" है।
- RSV: 3 बिट्स ये वर्तमान WebSockets स्पेक में अप्रयुक्त हैं।
पहला बाइट प्राप्त करने के लिए, हम IO#getbyte मेथड का उपयोग करेंगे। और डेटा निकालने के लिए, हम कुछ सरल बिटमास्किंग का उपयोग करेंगे। यदि आप बिटवाइज़ ऑपरेटर्स से परिचित नहीं हैं, तो मेरा अन्य लेख "Bitwise hacks in Ruby" देखें।
first_byte = socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111
# हमारा सर्वर केवल सिंगल-फ्रेम, टेक्स्ट संदेशों का समर्थन करेगा।
# यदि क्लाइंट कुछ और भेजने का प्रयास करता है तो अपवाद उठाएं।
raise "We don't support continuations" unless fin
raise "We only support opcode 1" unless opcode == 1
बाइट 2: MASK और पेलोड लंबाई
फ्रेम का दूसरा बाइट पेलोड के बारे में अधिक जानकारी रखता है।
- MASK: 1 बिट बूलियन फ्लैग जो इंगित करता है कि पेलोड मास्क्ड है या नहीं। यदि यह सच है, तो उपयोग से पहले पेलोड को "अनमास्क" करना होगा। यह हमारे क्लाइंट से आने वाले फ्रेम्स के लिए हमेशा सच होना चाहिए। स्पेक ऐसा कहता है।
- पेलोड लंबाई: 7 बिट्स यदि हमारा पेलोड 126 बाइट्स से कम है, तो लंबाई यहां संग्रहीत है। यदि यह मान 126 से अधिक है, तो इसका मतलब है कि लंबाई देने के लिए और बाइट्स आएंगे।
यहां हम दूसरे बाइट को कैसे संभालते हैं:
second_byte = socket.getbyte
is_masked = second_byte & 0b10000000
payload_size = second_byte & 0b01111111
raise "All frames sent to a server should be masked according to the websocket spec" unless is_masked
raise "We only support payloads < 126 bytes in length" unless payload_size < 126
STDERR.puts "Payload size: #{ payload_size } bytes"
बाइट्स 3-7: मास्किंग की
हम उम्मीद करते हैं कि सभी इनकमिंग फ्रेम्स के पेलोड मास्क्ड होंगे। सामग्री को अनमास्क करने के लिए, हमें इसे मास्किंग की के विरुद्ध XOR करना होगा।
यह मास्किंग की अगले चार बाइट्स बनाती है। हमें इसे प्रोसेस करने की आवश्यकता नहीं है, हम बस बाइट्स को एक सरणी में पढ़ते हैं।
mask = 4.times.map { socket.getbyte }
STDERR.puts "Got mask: #{ mask.inspect }"
कृपया मुझे बताएं यदि आप 4 बाइट्स को सरणी में पढ़ने का कोई बेहतर तरीका जानते हैं।
times.mapथोड़ा अजीब है, लेकिन यह सबसे संक्षिप्त दृष्टिकोण था जो मैं सोच सका। मैं ट्विटर पर @StarrHorne हूँ।
बाइट्स 8 और आगे: पेलोड
ठीक है, हम मेटाडेटा के साथ कर चुके हैं। अब हम वास्तविक पेलोड प्राप्त कर सकते हैं।
data = payload_size.times.map { socket.getbyte }
STDERR.puts "Got masked data: #{ data.inspect }"
याद रखें कि यह पेलोड मास्क्ड है। इसलिए यदि आप इसे प्रिंट करते हैं, तो यह कचरे जैसा दिखेगा। इसे अनमास्क करने के लिए, हम बस प्रत्येक बाइट को मास्क के संबंधित बाइट के साथ XOR करते हैं। चूंकि मास्क केवल चार बाइट्स लंबा है, हम इसे पेलोड की लंबाई से मेल खाने के लिए दोहराते हैं:
unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] }
STDERR.puts "Unmasked the data: #{ unmasked_data.inspect }"
अब हमारे पास बाइट्स की एक सरणी है। हमें इसे यूनिकोड स्ट्रिंग में बदलने की आवश्यकता है। WebSockets में सभी टेक्स्ट यूनिकोड है।
STDERR.puts "Converted to a string: #{ unmasked_data.pack('C*').force_encoding('utf-8').inspect }"
सब कुछ एक साथ रखना
जब आप इस सभी कोड को एक साथ रखते हैं, तो आपको एक स्क्रिप्ट मिलती है जो इस तरह दिखती है:
require 'socket'
require 'digest/sha1'
server = TCPServer.new('localhost', 2345)
loop do
socket = server.accept
STDERR.puts "Incoming Request"
http_request = ""
while (line = socket.gets) && (line != "\r\n")
http_request += line
end
if matches = http_request.match(/^Sec-WebSocket-Key: (\S+)/)
websocket_key = matches[1]
STDERR.puts "Websocket handshake detected with key: #{ websocket_key }"
else
STDERR.puts "Aborting non-websocket connection"
socket.close
next
end
response_key = Digest::SHA1.base64digest([websocket_key, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
STDERR.puts "Responding to handshake with key: #{ response_key }"
socket.write <<-eos
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: #{ response_key }
eos
STDERR.puts "Handshake completed. Starting to parse the websocket frame."
first_byte = socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111
raise "We don't support continuations" unless fin
raise "We only support opcode 1" unless opcode == 1
second_byte = socket.getbyte
is_masked = second_byte & 0b10000000
payload_size = second_byte & 0b01111111
raise "All incoming frames should be masked according to the websocket spec" unless is_masked
raise "We only support payloads < 126 bytes in length" unless payload_size < 126
STDERR.puts "Payload size: #{ payload_size } bytes"
mask = 4.times.map { socket.getbyte }
STDERR.puts "Got mask: #{ mask.inspect }"
data = payload_size.times.map { socket.getbyte }
STDERR.puts "Got masked data: #{ data.inspect }"
unmasked_data = data.each_with_index.map { |byte, i| byte ^ mask[i % 4] }
STDERR.puts "Unmasked the data: #{ unmasked_data.inspect }"
STDERR.puts "Converted to a string: #{ unmasked_data.pack('C*').force_encoding('utf-8').inspect }"
socket.close
end
जब मैं अपने WebSocket टेस्टर वेबपेज को रीफ्रेश करता हूं और यह मेरे सर्वर पर अनुरोध करता है, तो मुझे यह आउटपुट दिखाई देता है:
$ ruby websocket_server.rb
Incoming Request
Websocket handshake detected with key: E4i4gDQc1XTIQcQxvf+ODA==
Responding to handshake with key: d9WHst60HtB4IvjOVevrexl0oLA=
Handshake completed. Starting to parse the websocket frame.
Payload size: 16 bytes
Got mask: [80, 191, 161, 254]
Got masked data: [19, 222, 207, 222, 41, 208, 212, 222, 56, 218, 192, 140, 112, 210, 196, 193]
Unmasked the data: [67, 97, 110, 32, 121, 111, 117, 32, 104, 101, 97, 114, 32, 109, 101, 63]
Converted to a string: "Can you hear me?"
क्लाइंट को डेटा वापस भेजना
तो हमने अपने क्लाइंट से अपने खिलौना WebSocket सर्वर पर एक टेस्ट संदेश सफलतापूर्वक भेज दिया है। अब सर्वर से क्लाइंट को वापस संदेश भेजना अच्छा होगा।
यह थोड़ा कम शामिल है, क्योंकि हमें किसी मास्किंग सामग्री से निपटने की आवश्यकता नहीं है। सर्वर से क्लाइंट को भेजे गए फ्रेम्स हमेशा अनमास्क्ड होते हैं।
जैसे हमने फ्रेम को एक बार में एक बाइट उपभोग किया, हम इसे एक बार में एक बाइट बनाएंगे।
बाइट 1: FIN और opcode
हमारा पेलोड एक फ्रेम में फिट होने वाला है, और यह टेक्स्ट होने वाला है। इसका मतलब है कि FIN 1 के बराबर होगा, और opcode भी एक के बराबर होगा। जब मैं उन्हीं बिट फॉर्मेट का उपयोग करके उन्हें जोड़ता हूं जिसका हमने पहले उपयोग किया था, तो मुझे एक संख्या मिलती है:
output = [0b10000001]
बाइट 2: MASKED और पेलोड लंबाई
क्योंकि यह फ्रेम सर्वर से क्लाइंट की ओर जा रहा है, MASKED शून्य के बराबर होगा। इसका मतलब है कि हम इसे अनदेखा कर सकते हैं। पेलोड लंबाई केवल स्ट्रिंग की लंबाई है।
output = [0b10000001, response.size]
बाइट्स 3 और आगे: पेलोड
पेलोड मास्क्ड नहीं है, यह सिर्फ एक स्ट्रिंग है।
response = "Loud and clear!"
STDERR.puts "Sending response: #{ response.inspect }"
output = [0b10000001, response.size, response]
चलो भेजते हैं!
इस बिंदु पर, हमारे पास एक सरणी है जिसमें वह डेटा है जिसे हम भेजना चाहते हैं। हमें इसे बाइट्स की एक स्ट्रिंग में बदलने की आवश्यकता है जिसे हम तार पर भेज सकें। इसके लिए हम सुपर-वर्सेटाइल Array#pack मेथड का उपयोग करेंगे।
socket.write output.pack("CCA#{ response.size }")
वह अजीब स्ट्रिंग "CCA#{ response.size }" Array#pack को बताती है कि सरणी में दो 8-बिट अनसाइन्ड इंट्स हैं, उसके बाद निर्दिष्ट आकार का एक कैरेक्टर स्ट्रिंग है।
यदि मैं क्रोम में नेटवर्क इंस्पेक्टर खोलता हूं, तो मैं देख सकता हूं कि संदेश जोर से और स्पष्ट रूप से आया।

अतिरिक्त क्रेडिट
बस इतना ही! मुझे आशा है कि आपने WebSockets के बारे में कुछ सीखा। सर्वर में बहुत सी चीजें गायब हैं। यदि आप अभ्यास जारी रखना चाहते हैं, तो आप इन पर गौर कर सकते हैं:
- मल्टी-फ्रेम पेलोड के लिए समर्थन
- बाइनरी पेलोड समर्थन
- पिंग/पोंग समर्थन
- लंबे पेलोड समर्थन
- क्लोजिंग हैंडशेक