نصب سرویس Node.js روی وب سرور | راهنمای جامع

۲۶۲ بازدید
آخرین به‌روزرسانی: ۲۷ شهریور ۱۴۰۲
زمان مطالعه: ۸ دقیقه
نصب سرویس Node.js روی وب سرور | راهنمای جامع

با این که بخش اعظم وب روی مجموعه سنتی PHP/MySQL کار می‌کند، اما بسیاری از وب‌سایت‌ها و وب‌اپلیکیشن‌های جدید نیز از فناوری‌های جدیدتر مانند Node.js بهره می‌گیرند. در این مقاله به توضیح برخی از چالش‌های فنی که در زمان پیاده‌سازی و دیپلوی کردن یک چنین راهکارها پیش می‌آیند، خواهیم پرداخت. اما قبل از آن که به توضیح شیوه نصب سرویس Node.js روی وب سرور بپردازیم، به توضیح اجمالی مزیت‌های استفاده از چنین رویکرد ترکیبی خواهیم پرداخت.

دلایل زیادی برای پیاده‌سازی دست‌کم بخشی از یک اپلیکیشن با استفاده از PHP وجود دارد. برای نمونه این کار می‌تواند به دلیل وجود برخی توسعه‌دهندگان ماهر PHP در تیم، یا وجود برخی فناوری‌های موجود که می‌خواهید مورد استفاده مجدد قرار دهید باشد. مثلاً ممکن است یک کامپوننت شخصی داشته باشید و یا حتی ممکن است وجود پلتفرمی مانند وردپرس از جمله این دلایل باشد.

با این حال، استفاده از Node.js دست‌کم در بخش‌های خاصی از اپلیکیشن می‌تواند مزیت‌های زیادی داشته باشد. برای نمونه ارتباط هم‌زمان با یک API بیرونی یا دیتابیس به طور معمول کارایی بالاتری در Node.js دارد که برخلاف PHP ذاتاً ناهمگام است.

یک نمونه از این رویکرد ترکیبی می‌تواند اپلیکیشنی باشد که از PHP برای رندر فرانت‌اند و از سرور Node.js برای اجرای عملیاتی که نیازمند ارتباط با API-های مختلف بیرونی و منابع داده، برای مثال گردآوری نتایج جستجو از چند ارائه‌دهنده استفاده می‌کند. در این رویکرد سرور Node.js یک API بیرونی برای اپلیکیشن عرضه می‌کند که نیازی نیست از بیرون قابل دسترسی باشد.

نمونه دیگر از کاربرد این رویکرد ترکیبی اپلیکیشنی است که فرانت‌اند به طور جزئی با استفاده از کد PHP رندر شود و بخشی از آن نیز به صورت یک اپلیکیشن سمت کلاینت پیاده‌سازی شود. کد سمت کلاینت می‌تواند مستقیماً با یک API پیاده‌سازی شده با استفاده از Node.js ارتباط برقرار کند.

همچنین می‌توانید اپلیکیشنی داشته باشید که به طور کامل با Node.js پیاده‌سازی شده باشد و فرانت‌اند یک اپلیکیشن سمت کلاینت باشد که API با Node.js پیاده‌سازی شده و از رندرینگ سمت سرور نیز برای بهینه‌سازی بارگذاری و بهبود تجربه کاربری استفاده شده است. با این حال این اپلیکیشن همچنان باید روی یک وب‌سرور Apache یا nginx دیپلوی شود، از این رو اپلیکیشن Node.js باید به ترتیبی در این محیط حضور داشته باشد.

روش‌های زیادی برای دیپلوی کردن یک اپلیکیشن Node.js در یک محیط کانتینری کلود وجود دارد. با این حال، فرض می‌کنیم که می‌خواهید اپلیکیشن خودتان را روی همان سرور دیپلوی و اجرا کنید که مجموعه نرم‌افزارهای آپاچی یا nginx ،PHP و MySQL حضور دارند.

ایجاد سرویس Node.js

امکان اجرای مستقیم اپلیکیشن Node.js از shell نیز وجود دارد، ‌اما برای این که مطمئن شویم در زمان ری استارت شدن سرور و یا کرش‌های احتمالی زنده می‌ماند، باید آن را به صورت یک سرویس روی سرور نصب کنیم. روش‌های مختلفی برای این کار وجود دارد، اما یکی از بهترین راه‌حل‌ها ابزاری به نام forever-service (+) است. این ابزار باید به صورت یک پکیج npm سراسری نصب شود تا بتوانیم به صورت یک دستور shell از آن استفاده کنیم و هر تعداد سرویس Node.js که قصد داریم نصب کنیم.

فرض می‌کنیم که سرویس Node.js شما در زیردایرکتوری server دایرکتوری ریشه وب‌اپلیکیشن نصب شده است. توجه کنید که به دلایل امنیتی باید مطمئن شوید که فایل‌های سورس سرور از روی وب قابل دسترسی نیستند. ایجاد یک سرویس با استفاده از forever-service کاری بسیار ساده است:

forever-service install -s server/index.js my-service

در این مثال، server/index.js مسیر اسکریپت ورودی سرویس است و my-service نیز نام سرویس در سیستم عامل محسوب می‌شود. زمانی که سرویس نصب شد، می‌توانید آن را درست مانند هر سرویس دیگری با استفاده از دستورهایی مانند زیر آغاز، متوقف و یا ری‌استارت کنید:

service start my-service
/etc/init.d/my-service start

شیوه پیاده‌سازی شدن سرویس شما به مقصود شما از نصب آن بستگی دارد، اما در هر حال به یک روش برای برقراری ارتباط با دنیای خارج نیاز دارد و آسان‌ترین راه‌حل برای این کار استفاده از Express یا یک فریمورک مشابه دیگر است. با این حال، صرف‌نظر از پیاده‌سازی عملی، برخی مشکلات کلی وجود دارند که در زمان ایجاد سرویس با استفاده از Node.js با آن‌ها مواجه می‌شویم.

فایل پیکربندی

نخستین نکته در مورد سرویس این است که باید یک فایل پیکربندی داشته باشد. این فایل برای ذخیره پیکربندی پیش‌فرض استفاده می‌شود که در فاز توسعه به کار می‌آید. فایل دیگری نیز برای ذخیره‌سازی پیکربندی واقعی روی محیط پروداکشن یا تست مورد نیاز است که به ریپازیتوری سورس کد اضافه نمی‌شود.

در این مورد فایل پیکربندی مناسب به سادگی کد زیر است:

1const config = require(
2  fs.existsSync( path.join( __dirname, './config-local.json' ) )
3    ? './config-local.json'
4    : './config.json'
5);

با این حال، بسته به نیازها، از یک را‌ه‌حل پایدارتر مانند dotenv نیز می‌توانید استفاده کنید.

کاهش دسترسی‌ها

همه سرویس‌های به طور پیش‌فرض با استفاده از حساب root اجرا می‌شوند. با این حال، این راه‌حل، چندان امن نیست. اگر فردی روشی برای دور زدن امنیت سرویس پیدا کند، می‌تواند دسترسی کامل به سرور به دست آورد. رویکرد بهتر این است که مطمئن شویم سرویس Node.js با استفاده از همان حساب کاربری وب‌اپلیکیشن PHP که با آن کار می‌کنیم اجرا می‌شود.

به این منظور می‌توانیم gid و uid حساب کاربر را در فایل پیکربندی ذخیره کنیم. این موارد می‌توانند مقادیر عددی داشته باشند یا صرفاً نام کاربر و گروه باشند. این حساب‌های کاربری تابع زیر را در جایی در ابتدای اسکریپت اصلی سرویس فرا می‌خوانند:

1function lowerPermissions( config ) {
2  const { gid, uid } = config.permissions;
3  if ( process.setgid != null && gid != null )
4    process.setgid( gid );
5  if ( process.setuid != null && uid != null )
6    process.setuid( uid );
7}

توجه کنید که باید ()setgid را پیش از ()setuid فراخوانی کنید، زیرا اگر به طور عکس عمل کنید، کاربر با دسترسی کاهش‌یافته نمی‌تواند دیگر ()setgid را فراخوانی کند.

آغاز سرور Express

گام بعدی برای ایجاد عملی اپلیکیشن اکسپرس این است که درخواست‌های HTTP ورودی را مدیریت کنیم. هرجایی به جز محیط توسعه باید قطعاً از یک اتصال امن HTTPS استفاده کنیم که نیازمند یک گواهینامه امنیتی است.

اگر هم اپلیکیشن PHP و هم سرویس Node.js از نام دامنه یکسانی استفاده می‌کنند، می‌توانند از گواهینامه مشترکی نیز بهره بگیرند. در این مورد گواهینامه مورد نظر به طور خودکار از سوی سرویس رایگان Let’s Encrypt ایجاد می‌شود و از سوی certbot مدیریت خواهد شد. زمانی که یک اپلیکیشن و سرویس Node.js را دیپلوی می‌کنیم، لینک‌های نرمی به فایل‌های گواهینامه ایجاد می‌کنیم که در دایرکتوری /etc/letsencrypt/live درون جایی است که سرویس نصب شده است، قرار می‌گیرند.

با این حال، از آنجا که دایرکتوری /etc/letsencrypt/live تنها از سوی root قابل دسترسی است، این گواهینامه باید پیش از کاهش دسترسی‌ها خوانده شود. از این رو مثال واقعی عملیات در زمان آغاز شدن سرویس به صورت زیر است:

const options = getServerOptions(config);

lowerPermissions(config);

const app = express();

app.set('env', config.development? 'development': 'production');

// ... call app.use() to install request handlers ...

const server = createServer(config, options, app);

server.listen(config.server.port, () => {
// ... additional initialization after the server starts ...
} );

تابعی که گواهینامه را در زمان استفاده از HTTPS بارگذاری می‌کند به صورت زیر است:

1function getServerOptions( config ) {
2  if ( config.server.secure ) {
3    const keyPath = path.join( __dirname, './ssl/server.key' );
4    const certPath = path.join( __dirname, './ssl/server.crt' );
5    const key = fs.readFileSync( keyPath );
6    const cert = fs.readFileSync( certPath );
7    return { key, cert };
8  } else {
9    return null;
10  }
11}

تابعی که سرور را ایجاد می‌کنید نیز به صورت زیر است:

1function createServer( config, options, app ) {
2  if ( config.server.secure )
3    return https.createServer( options, app );
4  else
5    return http.createServer( app );
6}

درخواست‌های Cross-origin

در محیطی که آپاچی یا انجین‌ایکس روی پورت 80 اجرا می‌شوند، سرویس Node.js شما باید از پورت متفاوتی استفاده کند. زمانی که سرویس تنها به صورت داخلی از وب‌اپلیکیشن PHP استفاده می‌کند، این موضوع مشکل عمده‌ای محسوب نمی‌شود. کد جاوا اسکریپت که در مرورگر اجرا می‌شود یک فراخوانی نیز به API مربوط به Node.js می‌زند و از یک پورت غیراستاندارد با استفاده از یک URL مانند /http://example.com:3000 استفاده می‌کند. فقط به خاطر داشته باشید که مرورگر با این درخواست از یک پورت متفاوت مانند یک درخواست Cross-origin رفتار می‌کند و از این رو باید سیاست CORS صحیحی روی سرور خود اتخاذ کنید.

این کار با افزودن دستگیره زیر به اپلیکیشن Expresss انجام می‌یابد:

1function cors( request, response, next ) {
2  const origin = config.allowOrigin == '*'
3    ? request.header( 'origin' ) : config.allowOrigin;
4  if ( request.method == 'OPTIONS' ) {
5    response.header( 'Access-Control-Allow-Origin', origin );
6    response.header( 'Access-Control-Allow-Methods',
7      'POST, GET, OPTIONS' );
8    response.header( 'Access-Control-Allow-Headers',
9      'Origin, Content-Type, Authorization' );
10    response.header( 'Access-Control-Max-Age', '7200' )
11    response.send();
12  } else {
13    response.header( 'Access-Control-Allow-Origin', origin );
14    next();
15  }
16}

به این ترتیب مبدأ مجاز می‌تواند با استفاده از فایل پیکربندی، تنظیم شود. به منظور اجرای کارهای مربوط به توسعه، از یک وایلدکارد استفاده می‌کنیم، اما از آنجا که از آن نمی‌توان به همراه هدرهای احراز هویت استفاده کرد، آن را با مبدأ اصلی از هدرهای درخواست جایگزین می‌کنیم.

البته متدها و هدرهای پذیرفته شده به اپلیکیشن شما بستگی دارند. نکته‌ای که باید به خاطر داشته باشید این است که مرورگر هر بار پیش از ارسال یک درخواست POST، یک درخواست OPTIONS متفاوت ارسال می‌کند. در این وضعیت روی شبکه‌های کندتر، ممکن است یک تأخیر چشمگیر پدید آید. یک راه‌حل ساده این است که هدر Access-Control-Max-Age را اضافه کنیم که به مرورگر امکان می‌دهد تا نتیجه درخواست OPTIONS را به جای ارسال هرباره، کَش کند.

خاموشی صحیح

این سرویس باید روشی برای خاموشی صحیح نیز داشته باشد. زمانی که یک سرویس را متوقف می‌کنید، پردازش Node.js یک سیگنال «خاتمه» (termination) دریافت می‌کند که به طور معمول موجب خروج بی‌درنگ می‌شود. این بدان معنی است که هر درخواست معلق، تراکنش پایگاه داده و غیره متوقف می‌شوند.

بسته به پیاده‌سازی‌تان ممکن است اجرای یک توقف برنامه‌ریزی‌شده کار دشواری باشد، اما آسان‌ترین راه‌حل این است که تا وقتی همه درخواست‌های کنونی مدیریت نشده‌اند، صبر کنید و دیگر درخواست‌های جدید را نپذیرید. رویه خاموشی زمانی آغاز می‌شود که سیگنال SIGTERM دریافت شود:

if (!config.development && process.platform!= 'win32')
process.on('SIGTERM', shutDown);

توجه کنید که خاموشی برنامه‌ریزی‌شده در محیط توسعه که پردازش Node.js مستقیماً از سوی IDE آغاز می‌شود، حذف شده است، بنابراین به صورت یک سرویس سیستمی اجرا نمی‌شود. تابع ()shutDown باید همه منابعی که پردازش Node.js در حال اجرا اشغال کرده است، آزاد کند. برای نمونه باید همه تایمرها تخریب شوند و سپس سرور HTTP بسته شود.

1function shutDown() {
2  // ... destroy active timers ...
3  const timeout = setTimeout( () => {
4    process.exit();
5  }, 15 * 1000 );
6  timeout.unref();
7  server.close( () => {
8    // ... final cleanup when all requests are handled ...
9  } );
10}

توجه کنید که تابع یک تایمر با تأخیر 15 ثانیه‌ای آغاز می‌کند. اگر برخی درخواست‌ها همچنان پس از این زمان پردازش شوند، یا چیز دیگری از خروج نرمال از پردازش Node.js جلوگیری کند، آخرین چاره این است که پردازش را با فراخوانی ()exit متوقف سازیم. موارد مختلفی وجود دارند که می‌توانند موجب تداوم اجرای یک پردازش Node.js شوند. این موارد شامل تایمرها، عملیات معلق‌مانده I/O و غیره می‌شوند و در یک اپلیکیشن پیچیده امکان پاک‌سازی همه چیز وجود ندارد.

اگر سرور پیش از timeout شدن با موفقیت بسته شود، کد پاک‌سازی اضافی اجرا می‌شود و پردازش Node.js باید خارج شود. برای این که مطمئن شویم تأثیر ایجاد شده در این تابع موجب تداوم اجرای پردازش نمی‌شود، متد ()under فراخوانی می‌شود.

عملیات دوره‌ای

از لحاظ نظری یک چنین سروری که در بخش‌های قبلی توصیف کردیم، می‌تواند برای دوره‌های زمانی کاملاً طولانی اجرا شود. در محیط پروداکشن به دلایل مختلف ناچار هستیم که سرور را متوقف کنیم، مثلاً با تازه‌سازی گواهینامه Let’s Encrypt، سرور همچنان با گواهینامه قدیمی کار می‌کند که در نهایت منقضی خواهد شد.

آسان‌ترین راه‌حل این است که در برخی بازه‌های زمانی مانند روزی یک بار سرور را ری استارت کنیم. این کار به دو روش میسر است. یک روش این است که سرویس را با استفاده از cron ری‌استارت کنیم. روش دیگر آن است که یک تایمر داخلی در سرویس پیاده‌سازی کنیم که می‌تواند برای اجرای عملیات پاک‌سازی دوره‌ای و دیگر وظایف دوره‌ای نیز مورد استفاده قرار گیرد. این یک مثال از کدی که است استفاده کرده‌ایم:

1const startDate = new Date();
2const cronInterval = setInterval( runCron,
3  config.settings.cronInterval * 1000 );
4function runCron() {
5  const now = new Date();
6  // ... perform periodic tasks ...
7  if ( now.getHours() == 3 && now.getDate() != startDate.getDate()
8       && !config.development )
9    shutDown();
10}

چنان که می‌بینید در انتهای تابعی که با بازه پیکربندی‌شده اجرا شده است، تابع ()shutDown که قبلاً دیدیم، هر روز از زمان آغاز به کار سرور، در ساعت 03:00 صبح فراخوانی می‌شود.

نکته خوب در مورد forever-service این است که وقتی تشخیص می‌دهیم که پردازش سرویس متوقف شده است، چه ناشی از کرش و چه خاتمه برنامه‌ریزی‌شده باشد، دوباره به صورت خودکار آغاز خواهد شد. بنابراین عملاً این راه‌حل ما را مطمئن می‌سازد که سرویس هر 24 ساعت یک بار در زمان شب ری‌استارت می‌شود.

سخن پایانی

ساخت یک سرویس Node.js کار آسانی نیست و در اپلیکیشن‌های بزرگ، بهتر است از یک فریمورک اختصاصی که می‌تواند همه موارد مطرح شده در این مقاله را مدیریت کند بهره بگیریم. با این حال، در صورتی که قصد دارید یک وب‌اپلیکیشن را که از Node.js استفاده می‌کند، پیاده‌سازی و دیپلوی کنید، نکاتی که در این راهنما مطرح شدند به آغاز بهتر و آسان‌تر این کار کمک می‌کنند.

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
codeburst
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *