شروع کار با شبکه و سوکت

در حوزه توسعه نرم افزار و مهندسی شبکه، بدون توجه به تمرکز خاص شما در صنعت، درک اصول سوکت و شبکه بسیار ارزشمند است. هدف این مقاله ارائه یک نمای کلی جامع از این مفاهیم ضروری است که درک واضحتری از اهمیت و عملکرد آنها برای مهندسان تسهیل میکند. بیایید به اصول اولیه بپردازیم و اصول اساسی را که شبکه های مدرن را هدایت می کنند، کشف کنیم.
برای ساده نگه داشتن موارد، با مثال های اولیه شروع می کنیم و به تدریج پیچیدگی بیشتری را معرفی می کنیم. این رویکرد به ما این امکان را میدهد که یک پایه قوی بدون غرق شدن ایجاد کنیم. در مقالات آینده، موضوعات پیشرفته تری را بررسی خواهیم کرد.
درک مدل های OSI و TCP/IP: مبانی شبکه
مدل OSI
ما نمی توانیم بدون ذکر دوست قدیمی خود، مدل OSI، بحث درباره مفاهیم اولیه شبکه را شروع کنیم. برای اینکه دو کامپیوتر بتوانند به طور موثر ارتباط برقرار کنند، باید به یک زبان صحبت کنند که ساختار آن بر اساس مدل OSI است. این چارچوب اساسی سنگ بنای درک و پیاده سازی ارتباطات شبکه بوده است. با تجزیه فرآیند پیچیده انتقال داده ها به هفت لایه قابل مدیریت، مدل OSI یک رویکرد روشن و سازمان یافته برای شبکه ارائه می دهد که آزمون زمان را پس داده است.
مدل OSI با ارائه یک زبان جهانی برای شبکه، فرآیند پیچیده انتقال داده را ساده می کند. هر لایه از مدل OSI دارای مسئولیتها و قابلیتهای خاصی است که تضمین میکند فناوریها میتوانند به طور یکپارچه تعامل داشته باشند. لایههای بالاتر از این انتزاع بهره میبرند و از توابع لایه پایینتر بدون نیاز به درک عملکرد درونی آنها استفاده میکنند.
لایه های مدل OSI
- لایه فیزیکی: این پایین ترین لایه است که به ارتباط فیزیکی بین دستگاه ها می پردازد. مسئول انتقال جریان بیت خام از طریق یک رسانه فیزیکی مانند کابل ها یا سیگنال های بی سیم است.
- لایه پیوند داده: این لایه انتقال داده ها را بین دو گره مستقیماً متصل مدیریت می کند. تشخیص و تصحیح خطا و همچنین کنترل جریان را انجام می دهد و از ارتباطات قابل اعتماد در محیط فیزیکی اطمینان می دهد.
- لایه شبکه: این لایه مسئول مسیریابی داده ها از مبدا به مقصد در چندین شبکه است. از آدرس دهی منطقی (مانند آدرس های IP) برای تعیین بهترین مسیر برای انتقال داده ها استفاده می کند.
- لایه حمل و نقل: این لایه انتقال داده های قابل اعتماد بین سیستم ها را تضمین می کند. تشخیص و بازیابی خطا، کنترل جریان، و انتقال کامل داده ها از طریق پروتکل هایی مانند TCP (پروتکل کنترل انتقال) را تضمین می کند.
- لایه جلسه: این لایه جلسات بین برنامه ها را مدیریت می کند. این اتصالات را برقرار، حفظ و خاتمه میدهد و اطمینان حاصل میکند که دادهها همگامسازی و ترتیببندی مناسبی دارند.
- لایه نمایشی: این لایه داده ها را بین لایه برنامه و شبکه ترجمه می کند. رمزگذاری، فشرده سازی و ترجمه داده ها را مدیریت می کند و اطمینان می دهد که داده ها در قالب قابل خواندن ارائه می شوند.
- سطح کاربردی: این بالاترین لایه است که مستقیماً با برنامه های کاربر در تعامل است. خدمات شبکه را به برنامه های کاربر نهایی مانند مرورگرهای وب و کلاینت های ایمیل ارائه می دهد.
آیا تا به حال جوک در مورد لایه 8 را شنیده اید؟ اگر نه، برای کسانی که از طنز شبکه ای لذت می برند، اغلب به “لایه 8” – لایه کاربر – اشاره می شود. این یک یادآوری بازیگوش است که مهم نیست چقدر ساختار فنی کامل است، عنصر انسانی می تواند چالش های منحصر به فرد خود را معرفی کند!
هنگامی که داده ها از طریق لایه های پروتکل ارتباط برقرار می کنند، در بخش های کوچکی به نام بسته ارسال می شوند. هر بسته شامل پیاده سازی هایی از این لایه های پروتکل است. با شروع از لایه برنامه، داده ها توسط لایه ارائه کپسوله می شوند، که سپس توسط لایه جلسه و به دنبال آن لایه انتقال و غیره کپسوله می شوند. این فرآیند به عنوان کپسولاسیون شناخته می شود.
هر لایه یک هدر و یک بدنه به بسته اضافه می کند. هدر حاوی اطلاعات پروتکل خاص لازم برای آن لایه است، در حالی که بدنه حاوی داده های کپسوله شده، از جمله هدرهای لایه های قبلی است. این تکنیک کپسوله سازی را می توان مانند لایه های یک پیاز تجسم کرد.
تصویر زیر فرآیند کپسولهسازی را نشان میدهد که دادهها از لایه کاربردی یک رایانه، در سراسر اینترنت، به لایه کاربردی رایانه دیگر حرکت میکنند.
Computer A Application Computer B Application
| ▲
▼ |
+---------------------+ +---------------------+
| 7. Application | | 1. Physical |
+---------------------+ +---------------------+
| |
+---------------------+ +---------------------+
| 6. Presentation | | 2. Data-Link |
+---------------------+ +---------------------+
| |
+---------------------+ +---------------------+
| 5. Session | | 3. Network |
+---------------------+ +---------------------+
| |
+---------------------+ +---------------------+
| 4. Transport | | 4. Transport |
+---------------------+ +---------------------+
| |
+---------------------+ +---------------------+
| 3. Network | | 5. Session |
+---------------------+ +---------------------+
| |
+---------------------+ +---------------------+
| 2. Data-Link | | 6. Presentation |
+---------------------+ +---------------------+
| |
+---------------------+ +---------------------+
| 1. Physical | | 7. Application |
+---------------------+ +---------------------+
| |
+--------------------------------------+
| Internet |
+--------------------------------------+
TCP/IP به عنوان جایگزینی برای مدل OSI
در حالی که مدل OSI یک چارچوب دقیق برای درک ارتباطات شبکه ارائه می دهد، TCP/IP مدل کاربردی تر و پرکاربردتر در شبکه های دنیای واقعی است. TCP/IP ساختار را به چهار لایه ساده میکند و به عنوان پایهای برای اینترنت عمل میکند و بر ارتباطات قوی و مقیاسپذیر در میان شبکههای مختلف تاکید دارد.
تصور کنید می خواهید با وارد کردن یک URL در مرورگر خود از یک وب سایت بازدید کنید. در اینجا نحوه رسیدگی به این درخواست توسط TCP/IP آمده است:
- سطح کاربردی: مرورگر وب شما (برنامه) یک درخواست HTTP برای صفحه وب ایجاد می کند. در مدل TCP/IP، لایه Application توابع لایه های Application، Presentation و Session مدل OSI را ترکیب می کند. این بدان معناست که نه تنها رابط کاربری و پروتکل های ارتباطی (لایه برنامه در OSI)، بلکه ترجمه، رمزگذاری و فشرده سازی داده ها (لایه ارائه در OSI)، و همچنین مدیریت جلسات و گفتگوهای بین دستگاه ها (لایه جلسه در OSI) را نیز مدیریت می کند. ). این ادغام مدل را ساده می کند و آن را با پروتکل ها و برنامه های کاربردی دنیای واقعی تراز می کند.
- لایه حمل و نقل: درخواست HTTP به پروتکل TCP داده می شود، که آن را به بسته های کوچکتر تقسیم می کند، یک هدر با اطلاعات پورت اضافه می کند و انتقال قابل اعتماد را تضمین می کند. این لایه با لایه Transport در مدل OSI مطابقت دارد.
- لایه اینترنت: سپس هر بسته به پروتکل IP ارسال می شود، که سربرگ دیگری حاوی آدرس های IP منبع (رایانه شما) و مقصد (سرور وب) اضافه می کند. این لایه با لایه Network در مدل OSI مطابقت دارد.
- لایه دسترسی به شبکه: بسته ها سپس از طریق شبکه فیزیکی (مانند اترنت یا Wi-Fi) ارسال می شوند، جایی که آنها از طریق روترها و سوئیچ های مختلف برای رسیدن به وب سرور حرکت می کنند. این لایه لایه های Data Link و Physical مدل OSI را ترکیب می کند.
هر دو مدل OSI و TCP/IP استانداردهای باز هستند. آنها به گونه ای طراحی شده اند که هر کسی بتواند از آنها استفاده کند، یا آنها را بیشتر بسازد تا نیازهای خاص را برآورده کند.
برنامه هایی که از شبکه استفاده می کنند، مانند مرورگرهای وب و کلاینت های ایمیل، برای مدیریت ارتباطات شبکه به سیستم عامل متکی هستند. سیستم عامل جزئیات پیچیده کپسوله سازی شبکه را مدیریت می کند و نوشتن برنامه های شبکه را برای توسعه دهندگان آسان تر می کند. آنها به سادگی نیاز به استفاده از رابط شبکه ارائه شده توسط سیستم عامل دارند، بدون اینکه نگران پیچیدگی های اساسی باشند.
استفاده از سوکت برای ارتباط
برای ارتباط با شبکه یا enable inter-process communication
(IPC)، توسعه دهندگان اغلب از سوکت ها استفاده می کنند. سوکت ها به عنوان نقاط پایانی استاندارد شده برای ارسال و دریافت داده ها، چه در یک شبکه و چه بین فرآیندهای روی یک ماشین، عمل می کنند. آنها راهی برای ارتباط برنامه ها فراهم می کنند و پیچیدگی های لایه های پروتکل زیرین را انتزاع می کنند. با استفاده از سوکت API های ارائه شده توسط سیستم عامل، توسعه دهندگان می توانند بر روی ایجاد منطق برنامه خود تمرکز کنند در حالی که سیستم عامل عملیات شبکه و IPC را مدیریت می کند. این باعث می شود کارهایی مانند ایجاد یک وب سرور، یک برنامه چت یا تسهیل ارتباط بین فرآیندها ساده تر و در دسترس تر باشد. رایجترین انواع سوکتها سوکتهای جریانی هستند که یک سرویس اتصال محور قابل اعتماد ارائه میدهند و سوکتهای دیتاگرام که خدمات بدون اتصال را ارائه میدهند.
برای ارتباطات IPC ارائه شده توسط سوکت دامنه یونیکس با استفاده از
AF_UNIX
خانواده، ما قبلاً این را در مقاله زیر بررسی کرده ایم. ممکن است در بخشهای بعدی این مقاله دوباره آن را از منظر دیگری بررسی کنیم.
https://www.kungfudev.com/blog/2022/12/05/understanding-unix-domain-sockets-in-golang
Stream sockets
و datagram sockets
دو نوع رایج ترین سوکت ها هستند. سوکت های جریان، که از پروتکل TCP استفاده می کنند، یک سرویس قابل اعتماد و اتصال گرا را ارائه می دهند. این بدان معنی است که داده ها در یک جریان پیوسته منتقل می شوند و اطمینان حاصل می شود که به ترتیب صحیح و بدون خطا می رسند. نمونههایی از سوکتهای جریان فعال عبارتند از سرورهای وب، که در آن یکپارچگی و ترتیب دادهها (مانند صفحات HTML) برای رندر مناسب بسیار مهم است، و کلاینتهای ایمیل، که برای اطمینان از دریافت دست نخورده پیامها، به انتقال داده قابل اعتماد نیاز دارند. در مقابل، سوکت های دیتاگرام از پروتکل UDP استفاده می کنند و یک سرویس بدون اتصال را ارائه می دهند. این امکان انتقال سریعتر داده ها را فراهم می کند اما بدون ضمانت نظم و اطمینان. نمونهای از سوکتهای دیتاگرام در بازیهای آنلاین یا پخش ویدیوی زنده است که سرعت آن مهمتر از دقت کامل داده است و از دست دادن گاه به گاه داده قابل قبول است.
شرط می بندم که حداقل یک بار تصویر زیر را هنگام یادگیری در مورد شبکه، سوکت ها یا خواندن برخی از مقالات شبکه مشاهده کرده باشید. این نمودار جریان اصلی ارتباط سوکت بین سرور و کلاینت را نشان میدهد و فراخوانیهای عملکرد کلیدی و تعاملات مربوط به برقراری ارتباط و تبادل داده را برجسته میکند.
سوکت ها به دلیل فلسفه یونیکس که همه چیز را به عنوان یک فایل در نظر می گیرد، مانند فایل ها رفتار می کنند. این انتخاب طراحی یک رابط ثابت برای انجام عملیات ورودی و خروجی در انواع مختلف جریان دادهها فراهم میکند و مدل برنامهنویسی را ساده میکند.
سوکت ها و فایل ها هر دو جریانی از بایت ها را ارائه می دهند که می توان از آنها خواند یا نوشت. این انتزاع جریان به خوبی با بسیاری از انواع عملیات ورودی/خروجی، خواه شامل فایلهای محلی، ارتباطات شبکه از راه دور، یا ارتباطات بین فرآیندی باشد، تناسب دارد.
هنگامی که یک سوکت با socket()
تابع، به پارامترهایی مانند دامنه نیاز دارد (به عنوان مثال، AF_INET
برای IPv4)، نوع (به عنوان مثال، SOCK_STREAM
برای TCP)، و پروتکل (معمولا 0 برای انتخاب پروتکل پیش فرض برای نوع داده شده). سپس به سوکت یک توصیفگر فایل اختصاص داده می شود، یک عدد صحیح که به طور منحصر به فرد سوکت را در سیستم عامل شناسایی می کند.
توصیفگر فایل یک عدد صحیح منحصر به فرد است که توسط سیستم عامل به یک فایل باز یا سوکت اختصاص داده می شود و به عنوان یک شاخص انتزاعی برای دسترسی به منابع ورودی/خروجی مختلف مانند فایل ها، سوکت ها و لوله ها عمل می کند. به زبان ساده، هنگامی که یک فایل را باز می کنید، سیستم عامل یک ورودی برای نمایش آن فایل ایجاد می کند و اطلاعات مربوط به آن را ذخیره می کند. اگر N فایل باز باشد، N ورودی متناظر در سیستم عامل وجود خواهد داشت که هر کدام با یک عدد صحیح مانند 20، 21 و غیره نشان داده می شوند. این عدد صحیح توصیفگر فایل است. این به طور منحصربهفرد یک فایل باز را در یک فرآیند شناسایی میکند و به فرآیند اجازه میدهد تا عملیاتی مانند خواندن، نوشتن و بستن فایلها را با استفاده از توابع استاندارد شده انجام دهد. با مدیریت منابع از این طریق، سیستم عامل ارتباط موثر بین فرآیندها و منابع I/O را تضمین می کند.
اطلاعات بیشتر
را bind()
تابع سوکت را با محلی خاص مرتبط می کند address
و port
، که به عنوان استدلال ارائه شده است. را listen()
تابع سوکت را بهعنوان یک سوکت غیرفعال علامتگذاری میکند که برای پذیرش درخواستهای اتصال ورودی استفاده میشود، با گرفتن آرگومان که حداکثر تعداد اتصالات معلق را مشخص میکند. را accept()
تابع اولین درخواست اتصال را استخراج می کند queue of pending connections
، ایجاد یک سوکت جدید file descriptor
برای اتصال
در سمت مشتری، connect()
تابع برای ایجاد یک اتصال به سرور استفاده می شود و آدرس و پورت سرور را به عنوان آرگومان نیاز دارد. سپس مشتری و سرور هر دو می توانند استفاده کنند send()
و recv()
، یا مشابه آن write()
و read()
، برای انتقال و دریافت داده ها. را close()
از تابع برای بستن سوکت استفاده می شود و توصیفگر فایل را آزاد می کند.
این طراحی API را سادهتر میکند و مدیریت ارتباطات شبکه با استفاده از عملیات فایل آشنا را برای توسعهدهندگان آسانتر میکند، فرآیند توسعه را سادهتر میکند و کد را بصریتر و قابل نگهداریتر میکند. با تلقی سوکتها بهعنوان فایل، سیستم عامل میتواند انواع مختلفی از عملیات ورودی/خروجی را با استفاده از یک رابط یکپارچه مدیریت کند، و از مکانیسمهای موجود برای بافر، مسدود کردن و مدیریت خطا که برای سیستمهای فایل به خوبی تثبیت شدهاند، استفاده کند.
همانطور که اشاره کردیم، چند نوع سوکت و خانواده وجود دارد، اما در حال حاضر، برای خانواده های سوکت تمرکز می کنیم AF_INET
برای IPv4، AF_INET6
برای IPv6، و همانطور که اشاره کردیم AF_UNIX
برای ارتباط محلی در همان میزبان. و در مورد نوع، ما در مورد دو نوع اصلی سوکت بحث خواهیم کرد: سوکت جریان و سوکت دیتاگرام.
سوکت در اکشن
برای نشان دادن آنچه تاکنون آموخته ایم، چند برنامه را در Rust می نویسیم. ما استفاده خواهیم کرد nix
crate، که API های پلتفرم یونیکس دوستانه (لینوکس، داروین) را برای کار با سوکت API فراهم می کند. در حالی که کتابخانه استاندارد Rust عملکرد استاندارد سوکت را در std
، nix
جعبه دسترسی جامعتر و اصطلاحیتری به تماسهای سیستم یونیکس سطح پایینتر فراهم میکند و کار با ویژگیهای سوکت پیشرفته را آسانتر میکند و برای توضیح آنچه تاکنون دیدهایم عالی است.
سرور
بنابراین همانطور که قبلا دیدیم، برای ایجاد یک سرور، دنباله ای از توابع عبارتند از: socket()
، bind()
، listen()
، و accept()
. اولین قدم ایجاد سوکت است.
let socket_fd = socket(
nix::sys::socket::AddressFamily::Inet, // Socket family
nix::sys::socket::SockType::Stream, // Socket type
nix::sys::socket::SockFlag::empty(),
None,
)
.expect("Failed to create socket");
این قطعه کد یک سوکت جدید با استفاده از nix
جعبه در زنگ. را socket()
فراخوانی تابع شامل چندین پارامتر است:
-
nix::sys::socket::AddressFamily::Inet
: خانواده سوکت را مشخص می کند، در این مورد،AF_INET
، که برای آدرس های IPv4 استفاده می شود. -
nix::sys::socket::SockType::Stream
: نوع سوکت را مشخص می کند،SOCK_STREAM
، که یک سوکت جریان را با استفاده از پروتکل TCP نشان می دهد. -
nix::sys::socket::SockFlag::empty()
: نشان می دهد که هیچ پرچم خاصی برای سوکت تنظیم نشده است. -
None
: نشان می دهد که باید از پروتکل پیش فرض استفاده شود.
پس از ایجاد سوکت، مرحله بعدی اتصال سوکت به یک آدرس و پورت خاص است. این سوکت را با یک نقطه پایانی محلی خاص مرتبط می کند.
// Create a socket address
let sock_addr =
SockaddrIn::from_str("127.0.0.1:6797").expect("Failed to create socket address");
// Bind the socket to the address
bind(socket_fd.as_raw_fd(), &sock_addr).expect("Failed to bind socket");
این قطعه کد سوکت ایجاد شده قبلی را به آدرس محلی متصل می کند 127.0.0.1
(localhost) و پورت 6797
:
-
SockaddrIn::from_str("127.0.0.1:6797")
: یک آدرس سوکت جدید ایجاد می کند (SockaddrIn
) از نمایش رشته ای آدرس IPv4 و پورت. -
bind(socket_fd.as_raw_fd(), &sock_addr)
: توصیفگر فایل سوکت را به آدرس مشخص شده متصل می کند. این باعث می شود سوکت به اتصالات ورودی گوش دهد127.0.0.1:6797
.
چیزی که در اینجا قابل توجه است این است که ما از آدرس های IP و پورت ها در قالب خاصی استفاده می کنیم زیرا در حال استفاده هستیم
AddressFamily::Inet
. خانواده های پروتکل های مختلف روش های خاص خود را برای تعریف آدرس های نقطه پایانی دارند. این بدان معناست که قالب آدرس میتواند بسته به خانواده آدرس متفاوت باشد، و به سوکتها اجازه میدهد پروتکلهای مختلف شبکه و قالبهای آدرس را به درستی مدیریت کنند. برای این مقاله، ما بر روی خانواده INET تمرکز می کنیم، اما در مقالات آینده، سایر خانواده های آدرس را با جزئیات بیشتری بررسی خواهیم کرد.
پس از اتصال سوکت به یک آدرس و پورت خاص، مرحله بعدی گوش دادن به اتصالات ورودی است. این سوکت را برای پذیرش درخواست های اتصال از مشتریان آماده می کند.
// Listen for incoming connections
// The backlog parameter specifies the maximum length of the queue of pending connections
let backlog = Backlog::new(1).expect("Failed to create backlog");
listen(&socket_fd, backlog).expect("Failed to listen for connections");
این قطعه کد سوکت را برای گوش دادن به اتصالات ورودی تنظیم می کند:
-
let backlog = Backlog::new(1).expect("Failed to create backlog");
: این خط یک آبجکت بک لاگ ایجاد می کند که حداکثر طول صف اتصالات معلق را مشخص می کند. در این حالت، بک لاگ روی 1 تنظیم می شود، به این معنی که سوکت می تواند تا یک اتصال معلق در صف قرار دهد. -
listen(&socket_fd, backlog).expect("Failed to listen for connections");
: این خط بهlisten()
عملکرد، که سوکت را در حالت گوش دادن قرار می دهد.
ما از بک لاگ 1 استفاده می کنیم زیرا در حال حاضر مثال را ساده و همزمان نگه می داریم. در آینده، ما یک زمان اجرا ناهمزمان را برای مدیریت کارآمدتر چندین اتصال معرفی خواهیم کرد.
را
listen
عملیات شامل ایجاد دو صف برای این سوکت توسط هسته است: the syn صف و تایید کنید صف برای مختصر نگه داشتن این مقاله، در مقاله بعدی به بررسی دقیق این صف ها خواهیم پرداخت.
در این مرحله، سوکت سرور آماده است و منتظر درخواستهای اتصال ورودی از سوی مشتریان است. را listen()
تابع به سرور اجازه می دهد تا اتصالات ورودی را در صف قرار دهد، که می تواند یک به یک پذیرفته و پردازش شود.
هنگامی که سوکت سرور برای گوش دادن به اتصالات ورودی تنظیم شد، گام بعدی پذیرش این اتصالات و مدیریت ارتباط داده با مشتری است.
// Accept incoming connections
let conn_fd = accept(socket_fd.as_raw_fd()).expect("Failed to accept connection");
// Read data
let mut buf = [0u8; 1024];
let bytes_read = read(conn_fd, &mut buf).expect("Failed to read from connection");
let received_data =
std::str::from_utf8(&buf[..bytes_read]).expect("Failed to convert received data to string");
println!(
"Received {} bytes: {:?} repr: {}",
bytes_read,
&buf[..bytes_read],
received_data
);
این قطعه کد نحوه پذیرش اتصال ورودی و خواندن داده ها از آن را نشان می دهد:
-
let conn_fd = accept(socket_fd.as_raw_fd()).expect("Failed to accept connection");
: این خط بهaccept()
تابع پذیرش اتصال ورودی یک توصیفگر فایل سوکت جدید برای اتصال ایجاد می کند،conn_fd
. -
let mut buf = [0u8; 1024];
: این خط یک بافر را برای ذخیره داده های ورودی مقداردهی اولیه می کند. -
let bytes_read = recv(conn_fd, &mut buf, MsgFlags::empty()).expect("Failed to read from connection");
: این خط داده ها را از اتصال پذیرفته شده در بافر می خواند. راrecv()
تابع برای این منظور استفاده می شود، باMsgFlags::empty()
نشان می دهد که هیچ پرچم خاصی وجود ندارد. تعداد بایت های خوانده شده در آن ذخیره می شودbytes_read
. -
let received_data = std::str::from_utf8(&buf[..bytes_read]).expect("Failed to convert received data to string");
: این خط داده های بایت دریافتی را به رشته UTF-8 تبدیل می کند. بافر را به تعداد بایت های خوانده شده برش می دهد و آن را تبدیل می کند.
در این مرحله، سرور اتصال ورودی را پذیرفته و داده های ارسال شده توسط مشتری را می خواند. سپس سرور میتواند این دادهها را پردازش کند، آنها را بازتاب دهد، یا اقدامات دیگری را بر اساس منطق برنامه انجام دهد.
let bytes_written = send(conn_fd, &buf[..bytes_read], MsgFlags::empty())
.expect("Failed to write to connection");
این قطعه کد نحوه ارسال داده ها را به مشتری نشان می دهد:
-
let bytes_written = send(conn_fd, &buf[..bytes_read], MsgFlags::empty()).expect("Failed to write to connection");
: این خط داده ها را از بافر با استفاده از به مشتری برمی گرداندsend()
تابع. بافر به تعداد بایت های خوانده شده از کلاینت تقسیم می شود و اطمینان حاصل می شود که فقط داده های دریافتی برگردانده می شوند.MsgFlags::empty()
نشان می دهد که هیچ پرچم خاصی استفاده نشده است.
برای سادگی، ما استفاده می کنیم
MsgFlags::empty()
، که نشان می دهد هیچ گزینه خاصی تنظیم نشده است و اجازه می دهدsend()
وrecv()
عملکردها در حالت پیش فرض خود کار کنند. برای مطالعه بیشتر در مورد این پرچم ها.
با این مرحله، سرور داده های دریافتی را به مشتری بازتاب می دهد و یک ارتباط رفت و برگشت ساده را تکمیل می کند. این جریان اصلی داده ها را از مشتری به سرور و بازگشت به مشتری نشان می دهد و عملیات اصلی ارتباطات سوکت را نشان می دهد.
بنابراین با کنار هم قرار دادن همه اینها، مثال کاملی برای این اکو TCP ساده داریم. همانطور که قبلاً توضیح داده شد، هر مرحله را توضیح دادیم:
fn main() {
let socket_fd = socket(
nix::sys::socket::AddressFamily::Inet, // Socket family
nix::sys::socket::SockType::Stream, // Socket type
nix::sys::socket::SockFlag::empty(),
None,
)
.expect("Failed to create socket");
// Create a socket address
let sock_addr =
SockaddrIn::from_str("127.0.0.1:6797").expect("Failed to create socket address");
// Bind the socket to the address
bind(socket_fd.as_raw_fd(), &sock_addr).expect("Failed to bind socket");
// Listen for incoming connections
let backlog = Backlog::new(1).expect("Failed to create backlog");
listen(&socket_fd, backlog).expect("Failed to listen for connections");
// Accept incoming connections
let conn_fd = accept(socket_fd.as_raw_fd()).expect("Failed to accept connection");
// echo back the received data
let mut buf = [0u8; 1024];
let bytes_read =
recv(conn_fd, &mut buf, MsgFlags::empty()).expect("Failed ... connection");
let received_data =
std::str::from_utf8(&buf[..bytes_read]).expect("Failed to ...");
// Echo back the received data
let bytes_written = send(conn_fd, &buf[..bytes_read], MsgFlags::empty())
.expect("Failed to write to connection");
}
در Rust، ما به صراحت از آن استفاده نمی کنیم
close
روش بستن سوکت ها زیرا سیستم مالکیت و قرض Rust به طور خودکار مدیریت منابع را مدیریت می کند. هنگامی که یک سوکت از محدوده خارج می شود، ایمنی حافظه Rust تضمین می کند که سوکت به درستی بسته شده و منابع آزاد می شوند. این امر نیاز به صریح را از بین می بردclose
تماسها، کاهش خطر نشت منابع و پاکتر و ایمنتر کردن کدها.
اکثر زبان های برنامه نویسی انتزاعات سطح بالاتری را برای ساده کردن برنامه نویسی سوکت ارائه می کنند و در دسترس تر و استفاده آسان تر از آن هستند. این انتزاعات اغلب فراخوانی های سیستمی، جزئیات رسیدگی و مدیریت منابع را برای شما در بر می گیرد. به عنوان مثال، در Rust، the std::net
ماژول یک API مناسب برای شبکه TCP ارائه می دهد:
use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("Connection established!");
}
}
در این مثال، TcpListener::bind
پیچیدگی ایجاد و اتصال یک سوکت را انتزاعی می کند و اطمینان می دهد که آدرس به درستی قالب بندی شده است “” و بایت مرتب شده است.” incoming
متد یک تکرار کننده را روی اتصالات ورودی برمی گرداند و حلقه پذیرش را مدیریت می کند. این انتزاع کد را خواناتر و نگهداری آسان تر می کند و به ما امکان می دهد به جای پیچیدگی های ارتباط سوکت، روی منطق برنامه تمرکز کنیم.
در حال اجرا سرور
حال اگر سرور را اجرا کنیم و استفاده کنیم telnet
از یک ترمینال دیگر، این را در عمل خواهیم دید:
$ cargo run part-one-server
Socket file descriptor: 3
Socket bound to address: 127.0.0.1:6797
Listening for incoming connections...
received 22 bytes
bytes: [72, 101, 108, 108, 111, 32, 102, 114, 111, 109, 32, 75, 117, 110, 103, 102, 117, 68, 101, 118, 13, 10]
hex repr: ["0x48", "0x65", "0x6c", "0x6c", "0x6f", "0x20", "0x66", "0x72", "0x6f", "0x6d", "0x20", "0x4b", "0x75", "0x6e", "0x67", "0x66", "0x75", "0x44", "0x65", "0x76", "0x0d", "0x0a"]
str repr: "Hello from KungfuDev\r\n"
Sent 22 bytes back to client
این خروجی نشان می دهد که سرور در حال اجرا است و به آدرس متصل است 127.0.0.1:6797
، و با موفقیت داده ها را از یک مشتری دریافت و بازتاب داد.
و در ما telnet(client)
ما می توانیم ببینیم که داده ها پاسخ داده شده اند.
telnet localhost 6797
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello from KungfuDev
Hello from KungfuDev
Connection closed by foreign host.
کد را می توانید در این مخزن پیدا کنید.
مشتری چطور؟
همانطور که در نمودار مشاهده کردید که جریان ارتباط سوکت را نشان می دهد، مشتری به جای تماس گرفتن listen
، استفاده می کند connect
برای شروع اتصال به سوکت سرور.
send(socket_fd.as_raw_fd(), data.as_bytes(), MsgFlags::empty())
.expect("Failed to send data to server");
مشتری از connect
عملکرد برقراری ارتباط با سرور. پس از اتصال موفقیت آمیز، مشتری می تواند داده ها را با استفاده از سرور به سرور ارسال کند send
عملکرد و دریافت داده ها از سرور با استفاده از recv
تابع.
شما می توانید کد این نمونه و نمونه های آینده را در این مخزن بیابید.
نتیجه گیری
در این مقاله، اصول برنامه نویسی سوکت با استفاده از Rust و the را بررسی کردیم nix
جعبه ما با درک مدل OSI و کاربرد عملی آن از طریق مدل TCP/IP شروع کردیم و زمینه را برای ارتباطات شبکه فراهم کردیم. سپس به دنباله ای از توابع لازم برای ایجاد یک سرور پرداختیم: socket()
، bind()
، listen()
، و accept()
. با مرور یک مثال کامل، نحوه راهاندازی یک سرور ساده را نشان دادیم که به اتصالات ورودی گوش میدهد، دادهها را دریافت میکند و آن را به مشتری بازتاب میدهد.
اجرای برنامه Rust ارائه شده و تست آن با telnet
این مفاهیم را در عمل نشان می دهد و نشان می دهد که چگونه داده ها از طریق یک شبکه منتقل و دریافت می شوند. اگرچه ما مثال را همزمان نگه داشتیم و از یک بک لاگ کوچک برای سادگی استفاده کردیم، این پایه راه را برای موضوعات پیشرفته تری مانند برنامه نویسی ناهمزمان و مدیریت کارآمد چندین اتصال هموار می کند.
منتظر مقالات بعدی باشید که در آن زمانهای اجرا ناهمزمان را معرفی میکنیم و تکنیکهای برنامهنویسی سوکت پیشرفته و اضافی را برای ارتقای مهارتهای شبکه خود بررسی میکنیم.