Computer >> कंप्यूटर >  >> प्रोग्रामिंग >> Ruby

रूबी में खरोंच से एक साधारण वेबसोकेट सर्वर का निर्माण

वेबसोकेट इन दिनों अधिक से अधिक प्रेस हो रहे हैं। हम सुनते हैं कि वे "भविष्य" हैं। हमने सुना है कि रेल 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 अनुरोधों के रूप में जीवन शुरू करता है। उनके पास एक अजीब जीवनचक्र है:

  1. ब्राउज़र एक सामान्य HTTP अनुरोध भेजता है, जिसमें कुछ विशेष हेडर होते हैं जो कहते हैं कि "कृपया मुझे एक वेबसोकेट बनाएं।"
  2. सर्वर एक निश्चित HTTP प्रतिसाद के साथ उत्तर देता है, लेकिन कनेक्शन बंद नहीं करता है।
  3. ब्राउज़र और सर्वर तब खुले कनेक्शन पर डेटा के फ़्रेम का आदान-प्रदान करने के लिए एक विशेष वेबसोकेट प्रोटोकॉल का उपयोग करते हैं।

तो हमारे लिए पहला कदम एक वेब सर्वर बनाना है। नीचे दिए गए कोड में, मैं सबसे सरल संभव वेब सर्वर बना रहा हूं। यह वास्तव में कुछ भी सेवा नहीं करता है। यह बस एक अनुरोध की प्रतीक्षा करता है और फिर इसे एसटीडीईआरआर को प्रिंट करता है।

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 के बारे में कुछ सीखा होगा। सर्वर में कई चीजें गायब हैं। यदि आप व्यायाम करना जारी रखना चाहते हैं, तो आप उनमें देख सकते हैं:

  • मल्टी-फ़्रेम पेलोड के लिए समर्थन
  • बाइनरी पेलोड समर्थन
  • पिंग / पोंग समर्थन
  • लंबे पेलोड समर्थन
  • हाथ मिलाना बंद करना

  1. अपना पहला वेब स्क्रैपर बनाना, भाग 3

    वेब स्क्रैपर बनाने की इस श्रृंखला में आपका फिर से स्वागत है। इस ट्यूटोरियल में, मैं अपनी पॉडकास्ट साइट से डेटा स्क्रैप करने के एक उदाहरण के माध्यम से जाऊँगा। मैं विस्तार से कवर करूंगा कि मैंने डेटा कैसे निकाला, कैसे सहायक और उपयोगिता विधियां अपना काम पूरा करती हैं, और सभी पहेली टुकड़े एक साथ कैसे आत

  1. अपना पहला वेब स्क्रैपर बनाना, भाग 3

    वेब स्क्रैपर बनाने की इस श्रृंखला में आपका फिर से स्वागत है। इस ट्यूटोरियल में, मैं अपनी पॉडकास्ट साइट से डेटा स्क्रैप करने के एक उदाहरण के माध्यम से जाऊँगा। मैं विस्तार से कवर करूंगा कि मैंने डेटा कैसे निकाला, कैसे सहायक और उपयोगिता विधियां अपना काम पूरा करती हैं, और सभी पहेली टुकड़े एक साथ कैसे आत

  1. फॉलआउट 76 को सर्वर से डिसकनेक्ट ठीक करें

    फॉलआउट 76 एक लोकप्रिय मल्टीप्लेयर रोल-प्लेइंग एक्शन गेम है जिसे बेथेस्डा स्टूडियो ने 2018 में रिलीज़ किया था। यह गेम विंडोज पीसी, एक्सबॉक्स वन और प्ले स्टेशन 4 पर उपलब्ध है और अगर आपको फॉलआउट सीरीज़ गेम पसंद हैं, तो आप इसे खेलने का आनंद लेंगे। हालांकि, कई खिलाड़ियों ने बताया है कि जब उन्होंने अपने क