वेबसोकेट इन दिनों अधिक से अधिक प्रेस हो रहे हैं। हम सुनते हैं कि वे "भविष्य" हैं। हमने सुना है कि रेल 5 में एक्शनकेबल के लिए उनका उपयोग करना पहले से कहीं अधिक आसान है। लेकिन वास्तव में वेबसोकेट क्या हैं? वे कैसे काम करते हैं?
इस पोस्ट में हम रूबी में स्क्रैच से एक साधारण वेबसॉकेट सर्वर बनाकर इन सवालों के जवाब देने जा रहे हैं। जब हम काम पूरा कर लेंगे तो हम एक ब्राउज़र और हमारे सर्वर के बीच द्वि-दिशात्मक संचार प्राप्त कर लेंगे।
<ब्लॉककोट>इस पोस्ट में कोड एक सीखने के अभ्यास के रूप में है। यदि आप वास्तविक उत्पादन ऐप में websockets को कार्यान्वित करना चाहते हैं, तो उत्कृष्ट websocket-ruby मणि देखें। आप WebSocket Spec पर भी एक नज़र डाल सकते हैं।
तो आपने कभी websockets के बारे में नहीं सुना होगा
सामान्य HTTP कनेक्शन में निहित कुछ समस्याओं को हल करने के लिए वेब सॉकेट का आविष्कार किया गया था। जब आप सामान्य HTTP कनेक्शन का उपयोग करके वेबपेज का अनुरोध करते हैं, तो सर्वर आपको सामग्री भेजता है और फिर कनेक्शन बंद कर देता है। यदि आप किसी अन्य पृष्ठ का अनुरोध करना चाहते हैं, तो आपको दूसरा कनेक्शन बनाना होगा। यह सामान्य रूप से ठीक काम करता है, लेकिन कुछ उपयोग के मामलों के लिए यह सबसे अच्छा तरीका नहीं है:
- चैट जैसे कुछ अनुप्रयोगों के लिए, जैसे ही एक नया संदेश आता है, फ्रंट एंड को अपडेट करने की आवश्यकता होती है। यदि आपके पास सामान्य HTTP अनुरोध हैं, तो इसका मतलब है कि आपको यह देखने के लिए सर्वर को लगातार मतदान करना होगा कि क्या है नई सामग्री।
- यदि आपके फ्रंट-एंड एप्लिकेशन को सर्वर से बहुत सारे छोटे अनुरोध करने की आवश्यकता है, तो प्रत्येक अनुरोध के लिए नए कनेक्शन बनाने का ओवरहेड एक प्रदर्शन समस्या बन सकता है। HTTP2 में यह कोई समस्या नहीं है।
वेब सॉकेट के साथ, आप सर्वर से एक कनेक्शन बनाते हैं जिसे तब खुला रखा जाता है और द्विदिश संचार के लिए उपयोग किया जाता है।
क्लाइंट साइड
वेब सॉकेट आमतौर पर ब्राउज़र और वेब सर्वर के बीच संचार के लिए उपयोग किए जाते हैं। ब्राउज़र पक्ष जावास्क्रिप्ट में लागू किया गया है। नीचे दिए गए उदाहरण में मैंने अपने स्थानीय सर्वर पर एक वेब सॉकेट खोलने और उसे एक संदेश भेजने के लिए जावास्क्रिप्ट का एक बहुत ही सरल टुकड़ा लिखा है।
<!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>
अगर मैं थोड़ा स्थिर सर्वर शुरू करता हूं और इस फाइल को अपने वेब ब्राउज़र में खोलता हूं, तो मुझे एक त्रुटि मिलती है। यह समझ में आता है, क्योंकि अभी तक कोई सर्वर नहीं है। हमें अभी भी एक बनाना है। :-)
सर्वर की शुरुआत
वेब सॉकेट सामान्य HTTP अनुरोधों के रूप में जीवन शुरू करता है। उनके पास एक अजीब जीवनचक्र है:
- ब्राउज़र एक सामान्य HTTP अनुरोध भेजता है, जिसमें कुछ विशेष हेडर होते हैं जो कहते हैं कि "कृपया मुझे एक वेबसोकेट बनाएं।"
- सर्वर एक निश्चित HTTP प्रतिसाद के साथ उत्तर देता है, लेकिन कनेक्शन बंद नहीं करता है।
- ब्राउज़र और सर्वर तब खुले कनेक्शन पर डेटा के फ़्रेम का आदान-प्रदान करने के लिए एक विशेष वेबसोकेट प्रोटोकॉल का उपयोग करते हैं।
तो हमारे लिए पहला कदम एक वेब सर्वर बनाना है। नीचे दिए गए कोड में, मैं सबसे सरल संभव वेब सर्वर बना रहा हूं। यह वास्तव में कुछ भी सेवा नहीं करता है। यह बस एक अनुरोध की प्रतीक्षा करता है और फिर इसे एसटीडीईआरआर को प्रिंट करता है।
require 'socket'
server = TCPServer.new('localhost', 2345)
loop do
# Wait for a connection
socket = server.accept
STDERR.puts "Incoming Request"
# Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
http_request = ""
while (line = socket.gets) && (line != "\r\n")
http_request += line
end
STDERR.puts http_request
socket.close
end
अगर मैं सर्वर चलाता हूं, और अपने वेबसोकेट परीक्षण पृष्ठ को रीफ्रेश करता हूं, तो मुझे यह मिलता है:
$ 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 अनुरोध में वेब सॉकेट से संबंधित शीर्षलेखों का एक समूह है। यह वास्तव में वेबसोकेट हैंडशेक का पहला चरण है
द हैंडशेक
सभी वेब सॉकेट अनुरोध हैंडशेक के साथ प्रारंभ होते हैं। यह सुनिश्चित करने के लिए है कि क्लाइंट और सर्वर दोनों समझते हैं कि वेब सॉकेट होने वाले हैं और वे दोनों प्रोटोकॉल संस्करण पर सहमत हैं। यह इस तरह काम करता है:
क्लाइंट इस तरह एक 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
को छोड़कर सर्वर प्रतिक्रिया बॉयलरप्लेट है शीर्षलेख। यह हेडर इस प्रकार उत्पन्न होता है:
# Take the value provided by the client, append a magic
# string to it. Generate the SHA1 hash, then base64 encode it.
Digest::SHA1.base64digest([sec_websocket_accept, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"].join)
आपकी आंखें आपसे झूठ नहीं बोल रही हैं। इसमें एक जादू निरंतर शामिल है।
हाथ मिलाना लागू करना
आइए हैंडशेक पूरा करने के लिए अपने सर्वर को अपडेट करें। सबसे पहले, हम अनुरोध हेडर से सुरक्षा टोकन निकालेंगे:
# Grab the security key from the headers.
# If one isn't present, close the connection.
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."
जब मैं वेबसोकेट परीक्षण पृष्ठ को रीफ्रेश करता हूं, तो अब मैं देखता हूं कि अब कोई कनेक्शन त्रुटि नहीं है। कनेक्शन स्थापित हो गया था!
यहाँ सर्वर से आउटपुट है, सुरक्षा कुंजी और प्रतिक्रिया कुंजी दिखा रहा है:
$ ruby server2.rb
Incoming Request
Websocket handshake detected with key: Fh06+WnoTQQiVnX5saeYMg==
Responding to handshake with key: nJg1c2upAHixOmXz7kV2bJ2g/YQ=
Handshake completed.
वेबसॉकेट फ़्रेम प्रोटोकॉल
एक बार WebSocket कनेक्शन स्थापित हो जाने के बाद, HTTP का उपयोग नहीं किया जाता है। इसके बजाय, वेबसॉकेट प्रोटोकॉल के माध्यम से डेटा का आदान-प्रदान किया जाता है।
फ़्रेम वेबसॉकेट प्रोटोकॉल की मूल इकाई हैं।
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:फिन और ओपकोड
ऊपर दी गई तालिका से, आप देख सकते हैं कि पहले बाइट (पहले आठ बिट्स) में कुछ डेटा होते हैं:
- फिन:1 बिट अगर यह गलत है, तो संदेश कई फ़्रेमों में विभाजित हो जाता है
- ओपकोड:4 बिट हमें बताता है कि क्या पेलोड टेक्स्ट है, बाइनरी है, या यदि यह कनेक्शन को जीवित रखने के लिए सिर्फ एक "पिंग" है।
- आरएसवी:3 बिट ये वर्तमान WebSockets spec में अप्रयुक्त हैं।
पहला बाइट प्राप्त करने के लिए, हम IO#getbyte
. का उपयोग करेंगे तरीका। और डेटा निकालने के लिए, हम कुछ सरल बिटमास्किंग का उपयोग करेंगे। यदि आप बिटवाइज़ ऑपरेटरों से परिचित नहीं हैं, तो मेरा अन्य लेख रूबी में बिटवाइज़ हैक्स देखें
first_byte = socket.getbyte
fin = first_byte & 0b10000000
opcode = first_byte & 0b00001111
# Our server will only support single-frame, text messages.
# Raise an exception if the client tries to send anything else.
raise "We don't support continuations" unless fin
raise "We only support opcode 1" unless opcode == 1
बाइट 2: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' # Provides TCPServer and TCPSocket classes
require 'digest/sha1'
server = TCPServer.new('localhost', 2345)
loop do
# Wait for a connection
socket = server.accept
STDERR.puts "Incoming Request"
# Read the HTTP request. We know it's finished when we see a line with nothing but \r\n
http_request = ""
while (line = socket.gets) && (line != "\r\n")
http_request += line
end
# Grab the security key from the headers. If one isn't present, close the connection.
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
जब मैं अपने वेबसॉकेट परीक्षक वेबपेज को रीफ्रेश करता हूं और यह मेरे सर्वर से अनुरोध करता है, तो मुझे यह आउटपुट दिखाई देता है:
$ 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?"
क्लाइंट को डेटा वापस भेजना
इसलिए हमने अपने क्लाइंट से अपने टॉय वेबसॉकेट सर्वर पर सफलतापूर्वक एक परीक्षण संदेश भेजा है। अब सर्वर से क्लाइंट को संदेश वापस भेजना अच्छा होगा।
यह थोड़ा कम शामिल है, क्योंकि हमें किसी भी मास्किंग सामान से निपटने की ज़रूरत नहीं है। सर्वर से क्लाइंट को भेजे गए फ़्रेम हमेशा नकाबपोश होते हैं।
जैसे हम एक बार में एक बाइट फ्रेम का उपभोग करते हैं, वैसे ही हम इसे एक बार में एक बाइट बनाने जा रहे हैं।
बाइट 1:FIN और opcode
हमारा पेलोड एक फ्रेम में फिट होने वाला है, और यह टेक्स्ट होने वाला है। इसका मतलब है कि फिन 1 के बराबर होगा, और ओपकोड भी एक के बराबर होगा। जब मैं उसी बिट प्रारूप का उपयोग करने वालों को मिलाता हूं जो हमने पहले इस्तेमाल किया था, तो मुझे एक नंबर मिलता है:
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 के बारे में कुछ सीखा होगा। सर्वर में कई चीजें गायब हैं। यदि आप व्यायाम करना जारी रखना चाहते हैं, तो आप उनमें देख सकते हैं:
- मल्टी-फ़्रेम पेलोड के लिए समर्थन
- बाइनरी पेलोड समर्थन
- पिंग / पोंग समर्थन
- लंबे पेलोड समर्थन
- हाथ मिलाना बंद करना