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

در نوشته بخش ششم درباره ساخت سوکت TCP گفته شد. در این نوشته می خواهم درباره چگونگی ساخت پیغام (Construct Message) برای پروتکل TCP در ارتباط شبکه به کمک سوکت آموزشی از کتاب TCP IP Sockets in C Practical Guide for Programmers و برخی دیگر از منابع بدهم.

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
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
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%B4%D8%B4%D9%85-siqfddwwsbd4

پروتکل TCP اتصال گرا (Connection Oriented) است و ارتباطی پایدار را میان دو سوی شبکه فراهم می کند ولی این پروتکل هیچ دیدی نسبت به پیغام های فرستاده شده ندارد. سوکت ابزاری برای ارتباط و تبادل پیغام و اطلاعات میان دو سوی شبکه است، از این رو، فرستنده و گیرنده باید بر سر اینکه چگونه این اطلاعات کد گذاری (Encode) شوند به توافق رسیده باشند.

در واقع به دنبال یک پروتکل میان فرستنده و گیرنده هستیم که هر دو آن را پذیرفته اند. پروتکل به زبان ساده، یک دسته از قوانین و رویه ها است که میان دو نفر مشترک است. برای نمونه، زبان انگلیسی، امروزه پروتکلی است که دو نفر می توانند با آن و بر اساس یک سری از واژه و دستور زبان و جمله سازی، با یکدیگر گفتگو کنند.

در رایانه ها نیز همین گونه است و پروتکل تنها در پیوند با لایه برنامه (Application Layer) نیست، بلکه در لایه های دیگر TCP/IP نیز پروتکل هایی فراهم شده اند که دو لایه همسان در دو سوی شبکه با یکدیگر گفتگو کنند. در همین راستا، در سوی فرستنده، در هر لایه از بالا به پایین، یک سری از اطلاعات به بسته شبکه (Network Packet) افزوده می شود که در سوی دیگر، در گیرنده، از پایین به بالا، این اطلاعات در همان لایه برداشته و خوانده می شوند.

پروتکل به زبان ساده

  • اطلاعات چگونه کد گذاری شوند.
  • فرمت پیغام چیست.
  • اندازه (Length) پیغام چه اندازه است.
  • چگونه یک ارتباط با پایان یابد.

پروتکل TCP بر پایه فریم ها (Frame) است و انعطاف بالایی را برای آماده سازی پیام ها می دهد. شکل زیر ساختار ساده ای از یک پیغام را نشان می دهد که در Cassandra به کار گرفته شده و CQL نام دارد. همانگونه که می بنید پیام از چندین فیلد (Field) جدا از هم ساخته شده است که هر فیلد یک داده ویژه را نگهداری می کند.

CQL binary protocol
CQL binary protocol

آنچه که ما به دنبال آن هستیم، پروتکلی در سطح برنامه (Application Protocol) است که نیاز دارد تا آشکار بگوید که داده ها (پیغام ها) چگونه باید به دست فرستنده پیام فرمت شوند تا در سوی دیگر، گیرنده بتواند آن را پردازش و درک کند و داده هایی معنا دار را از آن بدست آورد.

گمان کنید می خواهیم دو شماره int x, y را از یک برنامه به برنامه دیگر بفرستیم. برای یکپارچه بودن داده ها میان فرستنده و گیرنده، باید این دو داده به گونه ای توافق شده کدگذاری شوند وگرنه ایرادهایی پدید خواهد آمد. برای نمونه، داده های بانکی که باید فرستاده شوند که یکی از شماره ها نشان دهنده سپرده (Deposit) و دیگری نشان دهنده برداشت (Withdrawal) است. در این نوشته و بر پایه کتاب گفته شده چندین متغیر هست که در آن کتاب به کار رفته است.

  • متغیر deposit برای اشاره به سپرده.
  • متغیر withdrawals برای اشاره به برداشت ها.
  • متغیر s که به توصیفگر فایل سوکت TCP اشاره دارد.
  • برای ماشین و کامپایلر، اندازه (sizeof(int برابر با ۴ بایت و (sizeof(short برابر با ۲ بایت است.

کدگذاری داده ها

بنابراین در اینجا به دنبال کد کردن این دو مقدار (متغیر) هستیم. یک راه این است که از کد گذاری نویسه ای (Character Encoding) کمک بگیریم. در واقع این دو مقدار شماره int را به رشته هایی از شماره های دهدهی چاپ شدنی کد می کنیم.

یک کد گذاری ساده، کد گذاری اَسکی (ASCII) است که توالی از بایت ها است که هر مقدار آن متناظربا یک کد اسکی است. برای نمونه دو شماره 17998720 و 47034615 را می توانیم همانند شکل زیر در یک توالی از کدهای اسکی کد گذاری کنیم. در میان کدهای اسکی، کد شماره 48 برای شماره 0 و کد شماره 57 برای شماره 9 است. همچنین کد شماره 32 برای جدا کننده به کار رفته است، زیرا 32 کد اسکی تک فضای خالی (Space) است.

ASCII Table
ASCII Table

بنابراین در اینجا یک کد گذاری ساده برای دو شماره انجام دادیم که جدا کننده این دو شماره درون بایت کد شده فضای خالی است. در کنار به کارگیری از تک فضای خالی می توانستیم از کد اَسکی کاراکتر Null Terminator، یعنی کاراکتر 0\ یا از یک Tab کمک بگیریم. در شکل زیر که رشته کد شده را نشان می دهد، نخستین شماره برای سپرده و دومین برای برداشت ها است.

Encoded Data
Encoded Data

بنابراین اگر پیام آماده شده تنها در برگیرنده همین دو شماره باشد، پس اندازه آن ۱۸ بایت می شود. اکنون گمان کنید یک متغیر (کاراکتری از رشته ها) به نام msgBuffer داشته باشیم، می توانیم دو شماره (متغیر) را با تابع ()sprintf فرمت و آماده کنیم.

sprintf(msgBuffer, &quot%d%d &quot,  deposit,  withdrawals);
send(s, msgBuffer, strlen(msgBuffer),  0);

ایرادها

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

دومین ایراد این است که متغیری که بافر (در اینجا msgBuffer) را نگه می دارد، باید به اندازه ای کافی بزرگ باشد تا بیشترین اندازه بایت کد شده رادر خودش نگه دارد. به زبان ساده، باید همه رقم های شماره ها و همچنین در صورت بودن، علامت منفی، دو فضای خالی جدا کننده و کاراکتر Null Terminate را نیز نگه دارد.

اگر هر شماره دارای ۹ رقم باشد، پس رشته کد شده نهایی ۲۳ بایت می شود، زیرا برای هر رقم ۲ بایت، برای هر فضای خالی نیز ۲ بایت و برای کاراکتر Null Terminate نیز ۱ بایت را در بر می گیرند. آیا این ۲۳ بایت اندازه ای برای رشته کد شده در برنامه است؟ اگر شماره ای چند رقم بیشتر داشته باشد، برنامه ار نخواهد کرد.

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

در کد زیر یک ماکرو تعریف شده است که اندازه بافر، یعنی ۱۳۲ بایت را مشخص می کند. سپس یک آرایه به نام msgBuffer ساخته شده که اندازه کلی آن برابر با ماکرو BUFSIZE است. سپس همه این آرایه ۱۳۲ بایتی بی توجه به اندازه پیام فرمت شده به گیرنده فرستاده می شود، پس در گیرنده، پیامی با اندازه ای بزرگ دریافت شده که تنها بخشی از آن داده های کد شده هستند.

#define BUFSIZE 132
// Another Codes
char msgBuffer[BUFSIZE]
// Another Codes
sprintf(msgBuffer, &quot%d%d &quot,  deposit,  withdrawals);
send(s, msgBuffer,  BUFSIZE,  0);

بنابراین کد گذاری شماره ها به رشته های (اَسکی)، شیوه کارآمدی نیست. هر بایت در رشته کد شده یکی از شماره های صفر تا نه است که هر شماره تنها ۴ بیت (و نه یک بایت) را نیاز دارد و این برای تغییر داده های شماره ای کد شده پسندیده نیست.

برای نمونه، اگر نیاز باشد تا پردارشی (مانند جمع - تفریق) روی داده ها در سوی دیگر انجام دهیم، پس نیاز است تا یک تبدیل از String به Integer انجام شود. برای پوشش اندازه پیغام فرستاده شده، در زبان سی ساختار ها (struct) برای ساخت پیغام به کار می رد.

یک struct در زبان سی و سی پلاس پلاس، ساختاری است که از یک تا چندین عضو ساخته شده است که هر نوع می تواند از یک گونه بومی باشد. مانند int, float, double, char, bool یا آرایه ای از داده ها مانند [] int یا [] char) یا اشاره گر ها مانند * char که به یک رشته از توالی کاراکترها اشاره دارد. همچنین یک struct می تواند عضوی از گونه struct دیگری داشته باشد.

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

در زیر نمونه کدی نشان داده شده است که یک ساختار ساخته شده است. این ساختار یک الگو یا فریم پیام ساده TCP را نشان می دهد که دارای دو عضو به نام dep (اشاره به deposit) و wd (اشاره به withdrawals) از گونه int است.

struct{
        int dep;
        int wd;
} msgStruct;
// Another Codes
msgStruct.dep = deposit;
msgStruct.wd   = withdrawals;
send(s, &msgStruct, sizeof(msgStruct), 0);

اندازه یک شی مانند int یا short در سی به کمک تابع ()sizeof بدست می آید. اندازه struct ها برابر با مجموع اندازه بایت هایی است که هر عضو آن اشغال می کند. برای نمونه int اندازه ۴ بایت را اشغال می کند، پس ساختار بالا در کل ۸ بایت را نیاز دارد، بی توجه به اینکه آن را به رشته اَسکی تبدیل کنیم که دارای یک کاراکتر NULL است.

پروتکل TCP بسیار انعطاف پذیر است و می توانید دوبار به کمک ()send مقدار های متغیرهای deposit و withdrawals را بفرستیم. در واقع شیوه دیگر این است که در TCP خود متغیر را بفرستیم ولی این در UDP شدنی نیست، زیرا دو دیتاگرام (Datagram) جدا از هم برای هر متغیر فرستاده می شود.

بنابراین در هر یک از دو شیوه (فرستادن تک تک متغیرها یا کمک از struct)، یک توالی از بایت ها همزمان فرستاده می شوند. این توالی از بایت ها از دو تا ۴ بایت برای هر int ساخته شده است. البته باز هم یک ایراد هست و این است که در ماشین (سیستم عامل) های گوناگون شاید نمایشی ناهمسان از int باشد که در این باره در نوشته های پیشین گفتیم روش استاندارد در شبکه، به کارگیری Big Endian یا Network Byte Order است.

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