آموزش Node.js: درخواست های HTTP و وب سوکت – بخش هشتم
در این بخش از سری مقالات آموزش Node.js به بررسی شیوه ایجاد درخواستهای HTTP و وب سوکت میپردازیم. زمانی که یک URL را در مرورگر وارد میکنیم، از آغاز تا به پایان ارسال درخواست چه اتفاقهایی رخ میدهند؟ در ادامه به بررسی شیوه اجرای درخواستهای صفحه از سوی مرورگر و با استفاده از پروتکل HTTP/1.1 میپردازیم. برای مطالعه بخش قبلی این سری به لینک زیر رجوع کنید:
یکی از سؤالاتی که در زمان مصاحبه با متقاضیان استخدام در حوزه برنامهنویسی وب، مطرح میشود این است که: «وقتی چیزی را در کادر جستجوی گوگل وارد میکنیم و اینتر را میزنیم چه اتفاقی میافتد؟»
این سؤال استخدامی بسیار رایج است و در واقع مصاحبهکننده میخواهد بداند آیا شما ایدهای ابتدایی از طرز کار اینترنت دارید یا نه. در ادامه به تحلیل اتفاقاتی خواهیم پرداخت که در زمان وارد کردن یک آدرس اینترنتی در نوار آدرس مرورگر رخ میدهند.
دلیل این که این بخش در این سری مقالات آموزش Node.js قرار گرفته است، این است که موضوع جذابی محسوب میشود و فناوریهای زیادی را شامل میشود. این بخش از فناوری وب به ندرت تغییر مییابد و قویترین و پیچیدهترین بخش از اکوسیستمهای بسیار بزرگتری است که از سوی انسان برای خلق اینترنت ساخته شده است.
پروتکل HTTP
ما در این بخش صرفاً به تحلیل درخواستهای URL میپردازیم. مرورگرهای مدرن این قابلیت را دارند که تشخیص دهند چیزی که در نوار آدرس نوشته شده یک URL و یا یک عبارت جستجو است. در حالت دوم، کاربر میخواهد از موتور جستجوی پیشفرض مرورگر برای جستجو استفاده کند و یک URL معتبر وارد نکرده است.
تصور ما این است که کاربر در نوار آدرس یک URL وارد میکند. زمانی که دکمه اینتر زد شود، مرورگر ابتدا URL کامل را میسازد. اگر شما صرفاً نام یک دامنه مانند faradars.org وارد کرده باشید، مرورگر به طور پیشفرض در ابتدای آن //:http میگذارد، چون به صورت پیشفرض از پروتکل HTTP استفاده میکند.
موارد مرتبط با لینوکس و macOS
توجه داشته باشید که ویندوز ممکن است کارها را به نحو اندکی متفاوت انجام دهد.
مرحله بررسی DNS
مرورگر ابتدا شروع به گشتن به دنبال DNS میکند تا آدرس IP سرور را پیدا کند. نام دامنه میانبر کارآمدی برای انسانها محسوب میشود، اما اینترنت به طرزی سازمان یافته است که رایانهها مکان دقیق یک سرور را از طریق آدرس IP آن مییابند. این آدرس IP مجموعهای از اعداد مانند 222.324.3.1 (نسخه IPv4) است.
مرورگر ابتدا کش محلی DNS سیستم را بررسی میکنید تا ببیند آیا دامنه اخیراً resolve شده است یا نه. کروم یک ابزار بصریسازی کش DNS کارآمد دارد که میتوانید آن را در این آدرس ببینید:
chrome://net-internals/#dns
اگر با مراجعه به آدرس فوق چیزی مشاهده نکردید، به این معنی است که مرورگر از DNS resolver استفاده میکند. با استفاده از فراخوانی سیستمی POSIX به نام gethostbyname میتوانید اطلاعات میزبان را بازیابی کنید.
gethostbyname
gethostbyname ابتدا فایل hosts محلی را بررسی میکند که در macOS و لینوکس در مسیر /etc/hosts قرار دارد. بدین ترتیب مشخص میشود که سیستم اطلاعات را به صورت محلی دارد یا نه. اگر هیچ اطلاعاتی در مورد دامنه مورد نظر پیدا نشود، سیستم یک درخواست به سرور DNS ارسال میکند.
آدرس سرور DNS در بخش system preferences ذخیره شده است. 2 سرور DNS محبوب به شرح زیر وجود دارند:
- 8.8.8.8: the Google public DNS server
- 1.1.1.1: the CloudFlare DNS server
اغلب افراد از سرور DNS ارائه شده از سوی شرکت ارائهدهنده خدمات اینترنتیشان استفاده میکنند. مرورگر درخواست DNS را با استفاده از پروتکل UDP ارسال میکند.
TCP و UDP دو مورد از پروتکلهای بنیادی در شبکههای کامپیوتری محسوب میشوند. آنها در سطح مفهومی یکسانی قرار دارند؛ اما TCP بیشتر اتصال محور است، در حالی که UDP یک پروتکل بدون اتصال و سبکتر است که برای ارسال پیام با سربار پایین مورد استفاده قرار میگیرد. بررسی این که درخواست UDP چگونه ارسال میشود، خارج از دامنه این مقاله است. سرور DNS ممکن است IP دامنه را در کش خود داشته باشد. اگر چنین نباشد از سرور DNS ریشه آن را میپرسد. سرور DNS ریشه یک سیستم متشکل از 13 سرور واقعی است که در سراسر کره زمین توزیع یافتهاند و کل اینترنت را هدایت میکنند.
سرور DNS آدرس همه نامهای دامنه جهان را نمیداند. بلکه تنها چیزی که میداند این است که resolver-های DNS سطح بالا کجا قرار دارند. منظور از دامنه سطح بالای دامنهای است که پسوند com ،.it ،.pizza. و غیره دارد. زمانی که سرور DNS ریشه، درخواست را دریافت کند، آن را به سرور DNS دامنه سطح بالا (یا TLD) فوروارد میکند. فرض کنید به دنبال دامنهای با عنوان faradars.org میگردیم، سرور DNS دامنهی ریشه، IP مربوط به سرور TLD با عنوان org. را بازگشت میدهد. اکنون resolver دیاناس IP آن سرور TLD را کش میکند تا در موارد بعدی مجبور به پرسش مجدد از سرور DNS ریشه نباشد.
سرور TLD DNS آدرسهای IP سرور نام مجاز برای دامنهای که به دنبالش هستیم را دارد. شاید بپرسید این سرور از کجا آدرسها را میداند. زمانی که یک دامنه خریداری میکنید، ثبتکننده دامنه سرورهای نام را به TLD مربوطه ارسال میکند. زمانی که سرورهای نام را بهروزرسانی کنید، این اطلاعات به صورت خودکار از سوی ثبتکننده دامنه بهروزرسانی خواهند شد. اینها سرورهای DNS ارائهدهنده خدمات میزبانی هستند. به طور معمول بیش از یک سرور نام وجود دارد تا به عنوان پشتیبان عمل کند. برای نمونه سرورهای نام میتوانند به صورت زیر باشند:
- ns1.faradars.org
- ns2.faradars.org
- ns3.faradars.org
resolver دیاناس کار خود را با سرور نام نخست آغاز کرده و تلاش میکند که IP دامنهای که به دنبالش هستید را از آن بپرسد. این سرور منبع نهایی اعتماد در مورد آدرس IP محسوب میشود. زمانی که آدرس IP به دست آید، میتوانیم به وبسایت مربوطه مراجعه کنیم.
مرحله درخواست TCP
زمانی که آدرس IP سرور در اختیار ما قرار گیرد، مرورگر میتواند شروع به برقراری اتصال TCP با آن بکند. اتصال TCP نیازمند یک بیت handshaking است تا بتواند به طور کامل راهاندازی شود و بتوانید دادهها را ارسال کنید. زمانی که اتصال برقرار شد، میتوانیم درخواست خود را ارسال کنیم.
ارسال درخواست
درخواست یک سند متنی ساده است که به روشی دقیق که از سوی پروتکل ارتباطی تعیینشده سازماندهی یافته است. این درخواست از 3 بخش تشکیل مییابد:
- خط درخواست
- هدر درخواست
- بدنه درخواست
خط درخواست
خط درخواست در یک خط منفرد قرار میگیرد:
- متد HTTP
- مکان منبع
- نسخه پروتکل
مثالی از آن را در ادامه مشاهده میکنید:
1GET / HTTP/1.1
هدر درخواست
هدر درخواست مجموعهای از جفتهای field: value است که مقادیر خاصی را تعیین میکند. دو فیلد اجباری وجود دارند که یکی Host و دیگری Connection است، در حالی که فیلدهای دیگر اختیاری هستند:
1Host: flaviocopes.comConnection: close
Host نشاندهنده نام دامنهای است که میخواهیم به آن برسیم و Connection همواره Close تعیین میشود مگر این که بخواهیم اتصال به دلیل خاصی باز باقی بماند. برخی از پرکاربردترین فیلدهای هدر به شرح زیر هستند:
- Origin
- Accept
- Accept-Encoding
- Cookie
- Cache-Control
- Dnt
اما موارد بسیار زیادی دیگری نیز وجود دارند. بخش هدر با یک خط خالی به پایان میرسد.
بدنه درخواست
بدنه درخواست اختیاری است و در درخواستهای GET استفاده نمیشود، اما در درخواستهای POST استفاده زیادی دارد و برخی اوقات در درخواستهای دیگر نیز ظاهر میشود. این بدنه میتواند شامل دادههایی در قالببندی JSON باشد. از آنجا که ما مشغول درخواست بررسی درخواست GET هستیم، بدنه خالی خواهد بود و توضیح زیادی در مورد آن وجود ندارد.
پاسخ
زمانی که درخواست ارسال شد، سرور آن را پردازش میکند و یک «پاسخ» (Response) بازگشت میدهد. این پاسخ با یک کد وضعیت و پیام وضعیت آغاز میشود و اگر درخواست موفق باشد کد بازگشتی 200 خواهد بود و به صورت زیر آغاز میشود:
200 OK
درخواست ممکن است کد وضعیت و پیام وضعیت متفاوتی مانند زیر داشته باشد:
404 Not Found403 Forbidden301 Moved Permanently500 Internal Server Error304 Not Modified401 Unauthorized
در این حالت پاسخ شامل لیستی از هدرهای HTTP و بدنه پاسخ است که چون درخواست در مرورگر ارسال شده در قالب HTML است.
تجزیه HTML
اکنون مرورگر کد HTML را دریافت کرده است و شروع به تجزیه آن میکند. به این ترتیب دقیقاً همان فرایند را برای همه منابعی که در اختیار نداریم تکرار میکند:
- فایلهای CSS
- تصاویر
- Favicon
- فایلهای جاوا اسکریپت
شیوه رندر کردن صفحه از سوی مرورگرهای مختلف نیز خارج از حیطه این مقاله است، اما مهم است که بدانیم فرایندی که توصیفی کردیم نه فقط برای صفحههای HTML بلکه برای هر چیزی که روی پروتکل HTTP عرضه شده اجرا میشود.
ساخت یک سرور HTTP با Node.js
در ادامه یک وبسرور HTTP را میبینید که از اپلیکیشن hello World در Node.js برای معرفی استفاده میکند:
1const http = require('http')
2
3const port = 3000
4
5const server = http.createServer((req، res) => {
6 res.statusCode = 200 res.setHeader('Content-Type'،
7 'text/plain') res.end('Hello World\n')
8})
9
10server.listen(port،() => {
11 console.log(`Server running at http://${hostname}:${port}/`)
12})
در ادامه کد فوق را مورد تحلیل قرار میدهیم. ما ابتدا ماژول http را گنجاندهایم. از این ماژول برای ایجاد سرور HTTP استفاده میکنیم. این سرور طوری تنظیم شده است که به پورت خاصی یعنی 3000 گوش دهد. زمانی که سرور آماده شود، تابع Callback به نام Listen فراخوانی میشود.
تابع Callback که ارسال میکنیم تابعی است که قرار است برای هر درخواست دریافتی اجرا شود. هر زمان که یک درخواست جدید دریافت میشود، رویداد request فراخوانی میشود که دو شیء عرضه میکند. یکی درخواست به صورت شیء http.IncomingMessage است و دیگری یک پاسخ به صورت شیء http.ServerResponse است. request جزییات درخواست را ارائه میکند. از طریق آن میتوانیم به هدرها و دادههای درخواست دسترسی داشته باشیم.
response برای تجمیع دادههایی که قرار است در سمت کلاینت دریافت شوند مورد استفاده قرار میگیرد. در این مورد:
1res.statusCode = 200
مشخصه statusCode را برابر با مقدار 200 تعیین میکنیم تا مشخص شود که پاسخ موفق بوده است. همچنین هدر Content-Type را به صورت زیر تنظیم میکنیم:
1res.setHeader('Content-Type'، 'text/plain')
و پاسخ را میبندیم و محتوا را به صورت یک آرگومان به ()end اضافه میکنیم:
1res.end('Hello World\n')
ایجاد درخواست HTTP با Node.js
در این بخش به توضیح شیوه ایجاد درخواستهای HTTP با استفاده از GET ،POST ،PUT و DELETE در Node.js میپردازیم. گرچه ما از واژه HTTP استفاده میکنیم، اما در همه جا عملاً از HTTPS استفاده میکنیم، بنابراین توجه داشته باشید که در مثالهای زیر به جای HTTP از HTTPS استفاده شده است.
اجرای درخواست GET
1const https = require('https') const options = {
2 hostname: 'flaviocopes.com'،
3 port: 443، path: '/todos'،
4 method: 'GET'
5}
6
7const req = https.request(options،(res) => {
8 console.log(`statusCode: ${res.statusCode}`)
9
10 res.on('data'،(d) => {
11 process.stdout.write(d)
12 })
13})
14
15req.on('error'،(error) => {
16 console.error(error)
17})
18
19req.end()
اجرای درخواست POST
1const https = require('https')
2
3const data = JSON.stringify({
4 todo: 'Buy the milk'
5})
6
7const options = {
8 hostname: 'flaviocopes.com'،
9 port: 443، path: '/todos'،
10 method: 'POST'،
11 headers: {
12 'Content-Type': 'application/json'،
13 'Content-Length': data.length
14 }
15}
16
17const req = https.request(options،(res) => {
18 console.log(`statusCode: ${res.statusCode}`)
19
20 res.on('data'،(d) => {
21 process.stdout.write(d)
22 })
23})
24
25req.on('error'،(error) => {
26 console.error(error)
27})
28
29req.write(data) req.end()
PUT و DELETE
درخواستهای PUT و DELETE از همان قالب درخواست POST بهره میگیرند و تنها کافی است مقدار options.method را عوض کنید.
درخواست HTTP در Node.js با استفاده از Axios
Axios یک کتابخانه بسیار محبوب جاوا اسکریپت است که میتوان از آن برای اجرای درخواستهای HTTP استفاده کرد به طوری که هم در مرورگر و هم در Node.js اجرا شود. این کتابخانه از همه مرورگرهای مدرن شامل IE8 و بالاتر پشتیبانی میکند. کتابخانه Axios مبتنی بر Promise است و بدین ترتیب امکان استفاده از کدهای async/await برای اجرای هر چه آسانتر درخواست XHR را داریم.
استفاده از Axios چند مزیت نسبت به API بومی به نام Fetch دارد که به شرح زیر هستند:
- از مرورگرهای قدیمی پشتیبانی میکند (Fetch نیازمند پلیفیل است).
- امکان خروج از درخواست وجود دارد.
- روشی برای تعیین timeout برای پاسخ دارد.
- حفاظت CSRF داخلی دارد.
- از نمایش پیشرفت آپلود پشتیبانی میکند.
- تبدیل داده JSON به صورت خودکار اجرا میشود.
- با Node.js کار میکند.
نصب
Axios را میتوان با استفاده از npm نصب کرد:
npm install axios
همچنین به این منظور میتوان از yarn استفاده کرد:
yarn add axios
روش دیگر این است که به سادگی از unpkg.com در صفحه خود استفاده کنید:
1<script src="https://unpkg.com/axios/dist/axios.min.js"><;/script>
API مربوط به Axios
درخواست HTTP را میتوان از شیء axios آغاز کرد:
1axios({ url: 'https://dog.ceo/api/breeds/list/all'، method: 'get'، data: { foo: 'bar' }})
اما برای سهولت کار، عموماً از روشهای زیر استفاده میشود:
- axios.get()
- axios.post()
مانند jQuery میتوان از ()get.$ و ()post.$ به جای ()ajax.$ استفاده کرد.
Axios متدهایی برای همه افعال HTTP ارائه میکند که گرچه محبوبیت کمتری دارند، اما همچنان مورد استفاده قرار میگیرند:
- axios.delete()
- axios.put()
- axios.patch()
- axios.options()
یک متد نیز برای دریافت هدرهای HTTP و حذف بدنه درخواست دارد:
- axios.head()
درخواستهای GET
یک روش آسان برای استفاده از Axios بهرهگیری از ساختار مدرن async/await است که در نسخه ES2017 عرضه شده است. در مثال نمونه زیر، Node.js به API به نام Dog کوئری میزند تا لیستی از همه نژادهای سگ را با استفاده از ()axios.get بازیابی کرده و آنها را شمارش کند:
1const axios = require('axios')
2const getBreeds = async () => {
3 try {
4 return await axios.get('https://dog.ceo/api/breeds/list/all')
5 } catch (error) {
6 console.error(error)
7 }
8}
9const countBreeds = async () => {
10 const breeds = await getBreeds()
11 if (breeds.data.message) {
12 console.log(`Got ${Object.entries(breeds.data.message).length} breeds`)
13 }
14}
15countBreeds()
اگر نمیخواهید از async/await استفاده کنید میتوانید از Promises بهره بگیرید:
1const axios = require('axios')
2const getBreeds = () => {
3 try {
4 return axios.get('https://dog.ceo/api/breeds/list/all')
5 } catch (error) {
6 console.error(error)
7 }
8}
9const countBreeds = async () => {
10 const breeds = getBreeds().then(response => {
11 if (response.data.message) {
12 console.log(`Got ${Object.entries(response.data.message).length} breeds`)
13 }
14 }).catch(error => {
15 console.log(error)
16 })
17}
18countBreeds()
افزودن پارامتر به درخواستهای GET
یک پاسخ GET میتواند شامل پارامترهایی در URL به صورت زیر باشد:
1https://site.com/?foo=bar
زمانی که از Axios استفاده میکنیم، میتوانیم این وضعیت را به سادگی با استفاده از آن URL به دست آوریم:
1axios.get('https://site.com/?foo=bar')
همچنین میتوان از مشخصه params در گزینهها استفاده کرد:
1axios.get('https://site.com/'، { params: { foo: 'bar' }})
درخواستهای POST
اجرای یک درخواست POST همانند درخواست GET است، اما به جای axios.get از axios.post استفاده میکنیم:
1axios.post('https://site.com/')
شیء شامل پارامترهای POST در آرگومان دوم قرار میگیرد:
1axios.post('https://site.com/'، { foo: 'bar'})
استفاده از WebSockets در Node.js
WebSockets یک روش جایگزین برای ارتباط HTTP در وباپلیکیشنها محسوب میشود. این روش یک کانال ارتباطی طولانیمدت دوطرفه بین کلاینت و سرور برقرار میسازد. زمانی که WebSockets اتصال پیدا کند، کانال باز میماند و اتصال بسیار سریعی با تأخیر و سربار کم ارائه میکند. گفتنی است که WebSockets از سوی همه مرورگرهای مدرن پشتیبانی میشود.
تفاوت WebSockets با HTTP چیست؟
HTTP پروتکل بسیار متفاوتی است و روش متفاوتی برای برقراری ارتباط دارد. HTTP یک پروتکل درخواست/پاسخ است که در آن سرور بنا به درخواست کلاینت مقداری داده بازگشت میدهد. اما در WebSockets شرایط زیر وجود دارد:
- سرور میتواند پیامی را بدون درخواست صریح از سوی کلاینت به وی ارسال کند.
- کلاینت و سرور میتوانند به صورت همزمان با هم صحبت کنند.
- به سربار داده بسیار کمی برای مبادله و ارسال پیام نیاز است. این بدان معنی است که ارتباط با تأخیر کمی صورت میپذیرد.
WebSockets برای ارتباطهای همزمان و طولانیمدت بسیار عالی هستند. HTTP برای مبادله دادهها و تعاملهایی که از سوی کلاینت آغاز میشوند مناسب است. پیادهسازی HTTP بسیار آسانتر است در حالی که WebSockets نیازمند کمی سربار بیشتر است.
WebSockets امن
شما باید همواره از پروتکل امن و رمزنگاریشده WebSockets به صورت //:WSS استفاده کنید. //:WS به معنی نسخه غیر امن WebSockets است و بدیهی است که نباید از آن استفاده کرد.
ایجاد یک اتصال WebSockets
1const url = 'wss://myserver.com/something'const connection = new WebSocket(url)
connection یک شیء WebSocket است. زمانی که اتصال با موفقیت برقرار شود، رویداد open تحریک میشود. با انتساب یک تابع callback به مشخصه onopen در شیء connection میتوان به این رویداد گوش داد.
1connection.onopen = () => {//...}
اگر خطایی وجود داشته باشد، callback تابع onerror اجرا میشود:
1connection.onerror = error => { console.log(`WebSocket error: ${error}`)}
ارسال دادهها به سرور با WebSockets
زمانی که اتصال WebSockets برقرار شد، میتوان دادهها را به سرور ارسال کرد. این کار در تابع callback به نام onopen به سادگی امکانپذیر است:
1connection.onopen = () => { connection.send('hey')}
دریافت دادهها از سرور با WebSockets
با یک تابع callback روی onmessage میتوان به دریافت دادهها از سمت سرور گوش داد. این تابع زمانی فراخوانی میشود که رویداد message فراخوانی شود:
1connection.onmessage = e => { console.log(e.data)}
پیادهسازی یک سرور WebSockets در Node.js
Ws کتابخانه محبوب WebSockets برای Node.js محسوب میشود. ما از این کتابخانه برای ساخت سرور WebSockets استفاده میکنیم. از آن برای پیادهسازی یک کلاینت نیز میتوان بهره جست و بدین ترتیب با استفاده از WebSockets بین سرویسهای بکاند ارتباط برقرار کرد. با دستور زیر این کتابخانه را به آسانی نصب کنید:
yarn inityarn add ws
کدی که باید نوشت بسیار اندک است:
1const WebSocket = require('ws')
2const wss = new WebSocket.Server({
3 port: 8080
4})
5wss.on('connection', ws => {
6 ws.on('message', message => {
7 console.log(`Received message => ${message}`)
8 }) ws.send('ho!')
9})
این کد یک سرور جدید روی پورت 8080 ایجاد میکند که پورت پیشفرض WebSockets است. همچنین یک تابع callback برای زمانی که اتصال برقرار میشود اضافه میکند و عبارت ho! را به کلاینت ارسال میکند و پیامهای دریافت را لاگ میکند. در این صفحه (+) میتوانید نمونهای از یک سرور WebSockets را مشاهده کنید. در این آدرس (+) هم یک کلاینت WebSockets را میبینید که با سرور ارتباط دارد. بدین ترتیب به پایان بخش هشتم از سری مقالات آموزش Node.js میرسیم. در بخش بعدی در مورد توصیفگرهای فایل در Node.js با شما صحبت خواهیم کرد. برای مطالعه بخش بعدی به لینک زیر رجوع کنید:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای جاوا اسکرپیت
- مجموعه آموزشهای برنامهنویسی
- مجموعه آموزشهای طراحی سایت
- Node.js چیست؟ — به زبان ساده
- آموزش راه اندازی و اجرای Express ،Node.js و MongoDB — راهنمای گام به گام
- Node.js چیست و چه نقشی در توسعه وب دارد؟ — به زبان ساده
- آموزش وب سوکت | راهنمای رایگان و جامع — به زبان ساده
==