آشنایی با سوکت های شبکه در سی و لینوکس - بخش ششم

در دنباله نوشته های آشنایی با سوکت های شبکه در سی و لینوکس، در این نوشته می خواهیم چگونگی ساخت یک برنامه سوکت محور با سی و در سیستم عامل لینوکس را بر پایه پروتکل TCP نشان دهیم. سوکت های شبکه دارای آدرس فامیلی AF_INET برای IPv4 و AF_INET6 برای IPv6 هستند. همچنین اگر بخواهیم پروتکل TCP را به کار ببریم، باید SOCK_STREAM یا SOCK_DGRAM برای UDP را به دومین ورودی بفرستیم.

https://virgool.io/linux-internals/%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-%D8%B3%D9%88%DA%A9%D8%AA-%D9%87%D8%A7%DB%8C-%D8%B4%D8%A8%DA%A9%D9%87-%D8%AF%D8%B1-%D8%B3%DB%8C-%D9%88-%D9%84%DB%8C%D9%86%D9%88%DA%A9%D8%B3-%D8%A8%D8%AE%D8%B4-%DB%8C%DA%A9%D9%85-wbohvfgswvmm
https://virgool.io/linux-internals/%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-%D8%B3%D9%88%DA%A9%D8%AA-%D9%87%D8%A7%DB%8C-%D8%B4%D8%A8%DA%A9%D9%87-%D8%AF%D8%B1-%D8%B3%DB%8C-%D9%88-%D9%84%DB%8C%D9%86%D9%88%DA%A9%D8%B3-%D8%A8%D8%AE%D8%B4-%D8%AF%D9%88%D9%85-e4ku3bqi7tsj
https://virgool.io/linux-internals/%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-%D8%B3%D9%88%DA%A9%D8%AA-%D9%87%D8%A7%DB%8C-%D8%B4%D8%A8%DA%A9%D9%87-%D8%AF%D8%B1-%D8%B3%DB%8C-%D9%88-%D9%84%DB%8C%D9%86%D9%88%DA%A9%D8%B3-%D8%A8%D8%AE%D8%B4-%D8%B3%D9%88%D9%85-lb8d7ce5sqig
https://virgool.io/linux-internals/%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-%D8%B3%D9%88%DA%A9%D8%AA-%D9%87%D8%A7%DB%8C-%D8%B4%D8%A8%DA%A9%D9%87-%D8%AF%D8%B1-%D8%B3%DB%8C-%D9%88-%D9%84%DB%8C%D9%86%D9%88%DA%A9%D8%B3-%D8%A8%D8%AE%D8%B4-%DA%86%D9%87%D8%A7%D8%B1%D9%85-fi6jlgdjlwjt

فهرست فایل های سرآیند که نیاز هستند

  • فایل stdio.h برای تابع های ()printf و ()fprintf
  • فایل sys/socket.h برای تابع های ()socket و ()bind و ()connect
  • فایل arpa/inet.h برای ساختار struct sockaddr_in و تابع ()inet_ntoa
  • فایل stdlib.h برای تابع ()atoi
  • فایل string.h برای تابع ()memset
  • فایل unistd.h برای تابع ()close

در این نوشته از دو کد سرور و کلاینت این سایت کمک گرفته شده است، پس پیشنهاد می شود همزمان با خواندن این نوشته، این کدها را نیز داشته باشید.

کد سرور

وظیفه سرور، گوش دادن به درخواست های ورودی از سوی کلاینت ها است، بنابراین کد (یا برنامه سرور) باشد نخست آماده و اتصالی را بر روی سوکت با نشانی IP یا شماره پورت برقرار کند. در سوی سرور نخست باید یک سوکت با ()socket ساخته شود که برگشتی این تابع یک شماره صحیح است که در واقع شماره توصیفگر فایل به سوکت است. همچنین می توانیم یک سری از گزینه ها را برای سوکت به کمک تابع ()setsockopt مشخص کنیم.

در نوشته پَسین درباره این گزنه ها و تابع های ()setsockopt و ()getsockopt را آموزش خواهم داد.

سپس باید با ساختار struct sockaddr_in یک دوتایی (IP, Port) را به همراه آدرس فامیلی مشخص می کنیم، زیرا هر سوکت یک دوتایی IP و Port است. سپس باید به کمک ()bind این دوتایی را به سوکت ساخته شده با ()socket انقیاد (Bind) کنیم.

اکنون نوبت به فراخوانی تابع ()listen است، زیرا باید سرور آماده گوش دادن به درخواست های ورودی از روی شبکه باشد. یک درخواست از سوی کلاینت با دو مولفه نشانی IP و شماره پورت مشخص می شود. برای نمونه زمانی اگر فرض کنیم در یک شبکه محلی LAN، ماشین سروری هست که روی آن تنها یک کارت شبکه هست که نشانی آن 192.168.1.100 است و شماره پورت 9090 را مشخص کرده ایم، پس دوتایی سوکت سرور بر روی آن گوش می دهد و کلاینت ها آن را برای یک درخواست به کار می برند، 192.168.1.100:9090 است.

آدرس 0.0.0.0 به همه آدرس های روی یک ماشین اشاره دارد که در نوشته بخش چهارم گفتیم با INADDR_ANY مشخص می شود.

تابع ()listen به درخواست های ورودی گوش می دهد و ر دنباله، تابع ()accept یک درخواست ورودی را می پذیرد. خروجی این تابع، یک سوکت تازه است که در واقع همانند ()socket، یک شماره شناسه برای توصفیگر فایل است.

همانگونه که گفته شد، سوکت به ریخت فایل ها در لینوکس و سیستم عامل های شبه یونیکس پیاده سازی می شوند. بنابراین خواندن و نوشتن بر روی آنها در هسته، چیزی همانند خواندن و نوشتن روی فایل ها است. در واقع تابعی به نام ()read است که از روی توصیفگر فایل برگشتی از تابع ()accept، بافر به بافر می خواند.

سرور در کنار اینکه می تواند از کلاینت ها پیغام ها و داده هایی را دریافت کند، می تواند به کمک تابعی به نام ()send به کلاینت پاسخی را نیز بفرستد که در ساده ترین گونه، یک پیغام ساده رشته ای است. بنابراین در سوی کلاینت به کمک ()recv، یک پیغام یا داده به سرور فرستاده شده است.

شکل زیر چند خط نخستین از درون تابع ()main را نشان می دهد. در خط ۱۰ سه متغیر int تعریف شده است که به ترتیب برای ذخیره سازی برگشتی تابع های ()socket و ()accept و ()read نگهداری می شوند. سپس یک متغیر از نوع struct sockaddr_in ساخته ایم. متغیر opt از نوع int برای به کارگیری درون ()setsockoptبه کار می رود.

تابع ()sizeof اندازه بایت های اشغال شده توسط یک متغیر را برگشت می دهد. در خط ۱۳ اندازه متغیر address که از نوع struct sockaddr_in است را در متغیر addrlen می رزیم. متغیر buffer آرایه ای کاراکترها یا رشته ای به طول 1024 است که سپس درون تابع ()read به کار می بریم تا بافرهای خوانده شده از روی سوکت در آن نگهداری شوند. در پایان یک اشاره گر از کاراکتر که در واقع رشته اَسکی است را نوشته ایم که پیغام پاسخ سرور به کلاینت را در خود نگهداری می کند.

سپس در یک شرط if در خط های ۱۷ تا ۲۱ بررسی می شود که آیا توسکت به کمک تابع ()socket به درستی ساخته شده است. نتیجه برگشتی ()socket در صورت اجرای درست، باید شک شماره صحیح مثبت و نامنفی باشد. بنابراین شرط خط ۱۷ همین را بررسی می کند. توجه کنید چون می خواهیم برای IPv4 سوکت TCP بسازیم پس ورودی های یکم و دوم به ترتیب AF_INET و SOCK_STREAM هستند.

تابع ()perror یک خطای سیستمی را بر روی Standard Error نشان می دهد. این تابع در stdio.h شناسانده شده است.تابع ()exit روند بیرون آمدن یا پایان یافتن برنامه را انجام می دهد. EXIT_SUCCESS و EXIT_FAILURE به ترتیب دو ثابت در سی استاندارد هستند که به همراه تابع ()exit در stdlib.h شناسانده شده اند.
برای بیرون آمدن عادی و بی خطا در زبان های برنامه نویسی مانند سی و سی پلاس پلاس و پایتون، معمولا شماره صفر را به تابع ()exit می فرستند و از این رو ثابت EXIT_SUCCESS برابر با شماره صفر است. شماره 1- را نیز برای بیرون آمدن با خطا به کار می برند. البته می تواند شماره های دیگر مانند 2- را نیز برای شرایط دیگر خطا به کار برد.

شرط if خط های ۲۳ تا ۲۷ نیز نخست تلاش می کند تا گزینه های سوکت را با ()setsockopt تنظیم کند. اگر این کار به خطا دچار شود، پس همانند پیش، تابع های ()perror و ()exit فراخوانی می شوند. در نوشته دیگری درباره گزینه های سوکت خواهم گفت.

اکنون باید یک نمونه از ساختار struct sockaddr_in را مقداردهی کنیم . پیش از این متغیری به نام address را ساخته بودیم که اکنون مقدار هر عضو آن را مشخص کرده ایم. همچنین می بینید که به کمک ()htons شماره پورت را به بایت شبکه تبدیل کرده ایم. توجه کنید در خط ۳۱ نام یک ماکرو به نام PORT را به تابع ()htons فرستاده ایم که در واقع این ماکرو به کمک دستور زیر، پیش از تعریف تابع ()main انجام شده است.

#define PORT 9090

سپس باید به کمک ()bind نشانی IP و شماره درگاه را به سوکت انقیاد کنیم. ورودی یکم این تابع همان متغیری است که برآیند تابع ()socket است. ورودی دوم در شکل زیر نشان می دهد که یک تبدیل (Cast) از ساختار struct sockaddr_in به ساختار struct sockaddr است.

در نوشته های پشین گفتیم که می توانیم ساختار struct sockaddr_in را به ساختار عمومی struct sockaddr تبدیل (Cast) کنیم.

سپس باید تابع ()listen را فراخوانی کنیم که دو وروی را دریافت می کند. ورودی یکم نم متغیری است که برآیند تابع ()socket را در خودش نگه می دارد. ورودی دوم یک شماره صحیح که نام آن blocking است. این شماره، بیشینه اندازه یک صف انتظار اتصال ها را نشان می دهد که تا این اندازه می تواند رشد کند. توجه کنید برآیند ()listen نیز یک شماره صحیح (و نه یک شماره توصیفگر فایل) است که اگر کمتر از صفر باشد، پس خطایی رخ داده است.

اکنون باید تابع ()accept فراخوانی شود که یک درخواست ورودی را می پذیرد. این تابع سه ورودی دارد که نخستین آنها نام متغیری است که برآیند ()socket در آن نگهداری می شود. دومین ورودی همانند ()bind یک تبدیل از struct sockaddr_in به struct sockaddr است.

سومین ورودی یک تبدیل از int به socklen_t است. socklen_t یک گونه شناسانده شده در فایل sys/socket.h است که به گونه صحیح بی علامت ۳۲ بیتی اشاره می کند. توجه کنید متغیر addrlen که برآیند تابع ()sizeof را در خودش نگه می دارد از گونه int است و در سومین ورودی به socklen_t تبدیل می شود. همچنین توجه کنید که برآیند ()accept یک سوکت تازه است که در واقع به سوکت سوی کلاینت (در کد سرور) اشاره می کند که در اینجا نام آن new_socket است.

تابع ()read برای خواندن داده هایی است که از سوکت فرستاد شده است و از این رو در سوی سرور، یکمین ورودی آن نام متغیری است که سوکت سوی کلاینت (در اینجا new_socket) را در خودش نگه می دارد. دومین ورودی نام متغیری است که قرار است بافر ورود ها باشد که در اینجا همان متغیر آرایه ای از کاراکترهای ۱۰۲۴ بایتی است. سومین وررودی، اندازه ای به بایت است که نشان می دهد تا چه اندازه باید بایت به بایت بخوانیم که این همانند اندازه بافر (۱۰۲۴) بایت است.

سپس با تابع ()printf رشته ورودی که در متغیر buffer هست را نشان می دهیم. سپس می توانیم با تابع ()send روی سوکت کلاینت بنویسیم. نخستین ورودی نام متغیری است که به سوکت سوی سرور اشاره می کند. دومین ورودی رشته یا متغیر رشته ای (در اینجا متغیر اشاره گر کاراکتری hello) است که می خوهیم به کلاینت فرستاده شود.

کد کلاینت

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

در آغاز دو متغیر به نام sock با مقدار پیش فرض صفر و valread از نوع int ساخته ایم. متغیر sock سپس برای برآیند تابع ()socket و valread برای نگهداری برآیند تابع ()read به کار می روند. متغیر دیگر ser_addr است که نمونه ای از ساختار struct sockaddr_in است که قرار است نشانی IP و شماره درگاه سرور را در کلاینت نشان دهد. در پایان دو متغیر hello و buffer که کاربردی همانند کد سرور دارند. در خط های ۲۱ و ۲۲ شکل زیر می توانید ببینید که عضوهای این سوکت مقداردهی شده اند.

در خط ۱۸ می بینید که برای بیرون آمدن از برنامه در صورت بروز خطا، از دستور return -1 کمک گرفته شده است. این همسان با (exit(EXIT_FAILURE است، زیرا EXIT_FAILURE نیز شماره ای صحیح و منفی است.

سپس تابع ()inet_pton برای تبدیل رشته نشانی IP (در اینجا 127.0.0.1) به بایت دودویی شبکه به کار گرفته می شود. سپس باید به کمک تابع ()connect به سرور (سوکت سرور) وصل شویم. ورودی یکم این تابع، نام متغیری است که سوکت کلاینت را نگه می دارد که در اینجا sock نام دارد. ورودی دوم یک تبدیل (Cast) از ساختار struct sockaddr_in به struct sockaddr است. در واقع ورودی دوم سوکت سرور را Cast می کند. اگر برآیند انجام ()connect کوچکتر از صفر باشد، پس خطایی رخ داده است.

https://virgool.io/linux-internals/%D8%A2%D8%B4%D9%86%D8%A7%DB%8C%DB%8C-%D8%A8%D8%A7-%D8%B3%D9%88%DA%A9%D8%AA-%D9%87%D8%A7%DB%8C-%D8%B4%D8%A8%DA%A9%D9%87-%D8%AF%D8%B1-%D8%B3%DB%8C-%D9%88-%D9%84%DB%8C%D9%86%D9%88%DA%A9%D8%B3-%D8%A8%D8%AE%D8%B4-%D9%BE%D9%86%D8%AC%D9%85-ephuzqarrm9c

سپس باید یک پیغام یا داده به سرور فرستاده شود که این کار بدست تابع ()send انجام می شود. پس از فرستادن پیغام به سرور، کلاینت منتظر پاسخ می ماند که این پاسخ به کمک تابع ()read از روی سوکت خوانده می شود.

بنابراین

  • کلاینت آغاز کننده ارتباط است و به سوکت شبکه وصل می شود.
  • کلاینت نخست داده می فرستد و سپس در انتظار پاسخ ی ماند.
  • سرور نخست پیغامی را می گیرد و سپس پاسخی را می فرستد.
  • در سوی کلاینت باید ویژگی های سوکت شبکه را برای اتصال به آن داشته باشیم.

شاد و پیروز و تندست باشید.