11. برنامهنویسی اینترنتی
11.1. Généralités
11.1.1. پروتکلهای اینترنت
در اینجا مقدمهای بر پروتکلهای ارتباطی اینترنت ارائه میدهیم که به عنوان مجموعه پروتکل TCP/IP (پروتکل کنترل انتقال / پروتکل اینترنت) نیز شناخته میشود و نام خود را از دو پروتکل اصلی گرفته است. ممکن است برای خواننده مفید باشد که پیش از پرداختن به توسعه برنامههای توزیعشده، درک کلی از نحوه کار شبکهها، و بهویژه پروتکلهای TCP/IP داشته باشد. متن زیر ترجمهای جزئی از گزیدهای است که در سند «Lan Workplace for Dos – راهنمای مدیر» اثر NOVELL، سندی از اوایل دهه ۱۹۹۰، یافت میشود.
مفهوم کلی ایجاد شبکهای از کامپیوترهای ناهمگون از تحقیقاتی نشأت میگیرد که توسط DARPA (آژانس پروژههای تحقیقاتی پیشرفته دفاعی) در ایالات متحده انجام شده است. DARPA مجموعه پروتکلهای شناختهشده به نام TCP/IP را توسعه داد که امکان ارتباط بین ماشینهای ناهمگون را فراهم میکند. این پروتکلها روی شبکهای به نام ARPAnet آزمایش شدند که بعدها به شبکه INTERNET تبدیل شد. پروتکلهای TCP/IP فرمتها و قوانینی را برای انتقال و دریافت تعریف میکنند که از معماری شبکه و سختافزار مورد استفاده مستقل هستند.
شبکهای که توسط DARPA طراحی شده و توسط پروتکلهای TCP/IP مدیریت میشود، یک شبکه سوئیچینگ بسته است. چنین شبکهای اطلاعات را در قالب بستههای کوچک به نام پکت منتقل میکند. بنابراین، اگر یک رایانه فایلی بزرگ را ارسال کند، آن را به بستههای کوچک تقسیم میکند که از طریق شبکه ارسال شده و در مقصد دوباره مونتاژ میشوند. TCP/IP فرمت این پکتها را تعریف میکند، یعنی:
- منبع بسته
- مقصد
- طول
- نوع
11.1.2. مدل OSI
پروتکلهای TCP/IP به طور کلی از مدل شبکه باز معروف به OSI (مدل مرجع اتصال سیستمهای باز) تعریفشده توسط ISO (سازمان بینالمللی استانداردها) پیروی میکنند. این مدل یک شبکه ایدهآل را توصیف میکند که در آن ارتباط بین ماشینها را میتوان با یک مدل هفتلایه نشان داد:
![]() |
هر لایه خدماتی را از لایه زیرین دریافت میکند و خدمات خود را به لایه بالادست ارائه میدهد. فرض کنید دو برنامه کاربردی مستقر بر روی ماشینهای مختلف، A و B، مایل به برقراری ارتباط هستند: آنها این کار را در لایه Application انجام میدهند. آنها نیازی به دانستن تمام جزئیات نحوه عملکرد شبکه ندارند: هر برنامه اطلاعات مورد نظر خود را برای ارسال به لایه زیرین - لایه Présentation - منتقل میکند. بنابراین، برنامه تنها نیاز به دانستن قوانین رابط با لایه Présentation دارد.
هنگامی که اطلاعات به لایه Présentation رسید، طبق قواعد دیگر به لایه Session منتقل میشود و این روند ادامه مییابد تا زمانی که اطلاعات به رسانه فیزیکی برسد و به صورت فیزیکی به ماشین مقصد ارسال شود. در آنجا، این دادهها فرآیند معکوس آنچه را که در ماشین فرستنده طی کردهاند، پشت سر خواهند گذاشت.
در هر لایه، فرآیند فرستنده مسئول ارسال اطلاعات، آن را به فرآیند گیرنده در ماشین دیگر که به همان لایه تعلق دارد، ارسال میکند. این کار بر اساس قواعدی مشخص که به پروتکل لایه معروف است، انجام میشود. بنابراین، نمودار نهایی ارتباطات به شرح زیر است:
![]() |
نقش لایههای مختلف به شرح زیر است:
انتقال بیتها را از طریق یک رسانه فیزیکی تضمین میکند. این لایه شامل تجهیزات پایانی پردازش داده (E.T.T.D.) مانند ترمینالها یا کامپیوترها، و همچنین تجهیزات پایاندهنده مدار داده (E.T.C.D.) مانند مودولاتورها/دمودولاتورها، چندرابطهها و متمرکزکنندهها است. نکات کلیدی در این سطح عبارتند از:
| |
ویژگیهای فیزیکی لایه فیزیکی را انتزاع میکند. خطاهای انتقالی را تشخیص داده و اصلاح میکند. | |
مسیر را که اطلاعات ارسالی از طریق شبکه باید طی کند، مدیریت میکند. این به عنوان routage شناخته میشود: تعیین مسیری که اطلاعات باید برای رسیدن به گیرنده خود طی کند. | |
ارتباط بین دو برنامه را ممکن میسازد، در حالی که لایههای قبلی تنها ارتباط بین ماشینها را مجاز میدانستند. یکی از خدمات ارائه شده توسط این لایه، چندراهیسازی (multiplexing) است: لایه حملونقل میتواند از یک اتصال شبکه واحد (از ماشینی به ماشین دیگر) برای انتقال دادههای متعلق به چندین برنامه استفاده کند. | |
این لایه خدماتی را فراهم میکند که به یک برنامه امکان میدهد یک جلسه کاری را روی یک ماشین راه دور باز و حفظ کند. | |
هدف آن استانداردسازی نمایش دادهها در سراسر ماشینهای مختلف است. بنابراین، دادههای منشاءگرفته از ماشین A توسط لایه Présentation آن ماشین بر اساس یک قالب استاندارد «فرمت» میشوند، پیش از آنکه از طریق شبکه ارسال شوند. پس از رسیدن به لایه Présentation در ماشین گیرنده B، که به لطف فرمت استاندارد آنها را شناسایی میکند، دادهها به شکلی متفاوت قالببندی میشوند تا برنامه در ماشین B بتواند آنها را تشخیص دهد. | |
در این سطح، برنامههایی را مییابیم که عموماً به کاربر نزدیک هستند، مانند ایمیل یا انتقال فایل. |
11.1.3. مدل TCP/IP
مدل OSI یک مدل ایدهآل است که تاکنون محقق نشده است. مجموعه پروتکلهای TCP/IP در شکل زیر به آن نزدیک میشود:
![]() |
لایه فیزیکی
در شبکههای محلی، معمولاً از فناوری اترنت یا توکن رینگ استفاده میشود. ما در اینجا فقط فناوری اترنت را بررسی خواهیم کرد.
اترنت
این نامی است که به فناوری شبکه محلی سوئیچینگ بسته داده شده است که در اوایل دهه ۱۹۷۰ در شرکت PARC زیرکس اختراع شد و در سال ۱۹۷۸ توسط زیرکس، اینتل و دیجیتال اکویپمنت استانداردسازی گردید. شبکه از نظر فیزیکی شامل یک کابل کواکسیال به قطر تقریبی ۱.۲۷ سانتیمتر و طول تا ۵۰۰ متر است. این کابل را میتوان با استفاده از répéteurs گسترش داد، به شرطی که بین هر دو دستگاه، حداکثر دو تکرارکننده قرار داشته باشد. کابل غیرفعال است: تمام اجزای فعال روی دستگاههای متصل به کابل قرار دارند. هر دستگاه از طریق یک کارت دسترسی شبکه به کابل متصل میشود که شامل:
- یک فرستنده (transceiver) که وجود سیگنالها را روی کابل تشخیص میدهد و سیگنالهای آنالوگ را به سیگنالهای دیجیتال و بالعکس تبدیل میکند.
- یک کوپلر که سیگنالهای دیجیتال را از فرستنده دریافت کرده و برای پردازش به رایانه منتقل میکند، یا بالعکس.
ویژگیهای اصلی فناوری اترنت به شرح زیر است:
- ظرفیت ۱۰ مگابیت بر ثانیه.
- توپولوژی باس: تمام دستگاهها به یک کابل مشترک متصل هستند
![]() |
- شبکه پخش – یک دستگاه فرستنده اطلاعات را با آدرس دستگاه گیرنده از طریق کابل ارسال میکند. سپس تمام دستگاههای متصل این اطلاعات را دریافت میکنند و تنها گیرنده مورد نظر آن را نگه میدارد.
- روش دسترسی به این صورت است: فرستندهای که قصد ارسال دارد به کابل گوش میدهد – سپس تشخیص میدهد که آیا موج حامل وجود دارد یا خیر؛ وجود موج حامل نشاندهنده در حال انجام بودن یک ارسال است. این تکنیک CSMA (دسترسی چندگانه با تشخیص حامل) است. اگر هیچ حامل وجود نداشته باشد، فرستنده میتواند تصمیم بگیرد که نوبت خود را برای ارسال انتخاب کند. چندین فرستنده ممکن است همزمان این تصمیم را بگیرند. سیگنالهای ارسالشده همپوشانی پیدا میکنند؛ این وضعیت به «تداخل» (collision) معروف است. فرستنده این وضعیت را تشخیص میدهد: در حین ارسال بر روی کابل، به آنچه واقعاً از آن عبور میکند نیز گوش میدهد. اگر تشخیص دهد که اطلاعاتی که در امتداد کابل در حال حرکت است با آنچه خود ارسال کرده یکسان نیست، نتیجه میگیرد که یک برخورد رخ داده است و ارسال را متوقف میکند. فرستندههای دیگر که در حال ارسال بودند نیز همین کار را انجام میدهند. هر کدام پس از یک تأخیر تصادفی که بسته به فرستندهٔ مربوطه متفاوت است، ارسال را از سر میگیرند. این تکنیک به نام CD (تشخیص برخورد) شناخته میشود. بنابراین، روش دسترسی به نام CSMA/CD شناخته میشود.
- آدرسدهی ۴۸ بیتی. هر دستگاه دارای یک آدرس است که در اینجا به آن آدرس فیزیکی گفته میشود و روی کارتی که آن را به کابل متصل میکند، نوشته شده است. این آدرس با نام آدرس Ethernet دستگاه شناخته میشود.
لایه شبکه
در این لایه، پروتکلهای IP، ICMP، ARP و RARP را مییابیم.
ارسال بستهها بین دو گره شبکه | |
ICMP ارتباط بین برنامه پروتکل IP روی یک ماشین و روی ماشین دیگر را تسهیل میکند. بنابراین این یک پروتکل تبادل پیام در خود پروتکل IP است. | |
آدرس اینترنتی ماشین را به آدرس فیزیکی آن نگاشت میکند | |
آدرس فیزیکی ماشین را به آدرس اینترنتی آن نگاشت میکند |
لایههای انتقال/جلسه
این لایه شامل پروتکلهای زیر است:
اطمینان از تحویل قابلاعتماد اطلاعات بین دو کلاینت | |
تضمین تحویل غیرقابلاطمینان اطلاعات بین دو کلاینت |
لایههای کاربرد/ارائه/جلسه
پروتکلهای مختلفی در اینجا یافت میشوند:
شبیهساز ترمینال که ماشین A را قادر میسازد به ماشین B بهعنوان یک ترمینال متصل شود | |
امکان انتقال فایل را فراهم میکند | |
امکان انتقال فایل را فراهم میکند | |
تبادل پیامها بین کاربران شبکه را امکانپذیر میسازد | |
یک نام ماشین را به آدرس اینترنتی آن تبدیل میکند | |
ایجاد شده توسط Sun MicroSystems، یک نمایش استاندارد و مستقل از ماشین از دادهها را مشخص میکند | |
که توسط Sun نیز تعریف شده است، یک پروتکل ارتباطی مستقل از لایه انتقال برای برنامههای کاربردی راه دور است. این پروتکل مهم است: برنامهنویس را از نیاز به دانستن جزئیات لایه انتقال رها میکند و برنامههای کاربردی را قابل حمل میسازد. این پروتکل بر اساس پروتکل XDR است. | |
که همچنین توسط Sun تعریف شده است؛ این پروتکل به یک ماشین امکان میدهد تا سیستم فایل ماشین دیگر را «ببیند». این پروتکل بر اساس پروتکل قبلی RPC است. |
11.1.4. نحوه کار پروتکلهای اینترنت
برنامههای کاربردی توسعهیافته در محیط TCP/IP معمولاً از چندین پروتکل در این محیط استفاده میکنند. یک برنامه کاربردی با بالاترین لایه پروتکلها ارتباط برقرار میکند. این لایه اطلاعات را به لایه زیرین خود منتقل میکند، و این روند به همین ترتیب ادامه مییابد تا زمانی که به رسانه فیزیکی برسد. در آنجا، اطلاعات به صورت فیزیکی به ماشین گیرنده منتقل میشود، جایی که دوباره از همان لایهها عبور میکند – این بار در جهت مخالف – تا به برنامهای که قرار است اطلاعات ارسالشده را دریافت کند، برسد. نمودار زیر مسیری را که اطلاعات طی میکند نشان میدهد:
![]() |
بیایید یک مثال بزنیم: برنامه کاربردی FTP، که در لایه Application تعریف شده و امکان انتقال فایل بین دستگاهها را فراهم میکند.
- این برنامه یک دنباله بایت را برای ارسال به لایه transport. تحویل میدهد
- لایه transport این دنباله بایتها را به segments و TCP تقسیم میکند و شماره سگمنت را به ابتدای هر سگمنت اضافه میکند. بخشها به لایه شبکه ارسال میشوند که توسط پروتکل IP اداره میشود.
- لایه IP یک بسته ایجاد میکند که شامل بخش دریافتشده TCP است. در ابتدای این بسته، آدرسهای اینترنتی ماشینهای مبدأ و مقصد قرار میگیرد. همچنین آدرس فیزیکی ماشین مقصد تعیین میشود. کل این مجموعه به لایه پیوند داده و فیزیکی، یعنی به کارت شبکه که ماشین را به شبکه فیزیکی متصل میکند، ارسال میشود.
- در آنجا، بسته IP به نوبه خود در یک فریم فیزیکی قرار داده شده و از طریق کابل برای گیرنده آن ارسال میشود.
- در ماشین مقصد، لایه پیوند داده و فیزیکی عملیات معکوس را انجام میدهد: این لایه بسته IP را از فریم فیزیکی خارج کرده و آن را به لایه IP میسپارد.
- لایه IP بررسی میکند که بسته معتبر است: آن یک چکسام بر اساس بیتهای دریافتی (checksum) محاسبه میکند که سپس باید آن را در هدر بسته بیابد. اگر اینطور نباشد، بسته دور ریخته میشود.
- اگر بسته معتبر تشخیص داده شود، لایه IP بخش TCP را از آن جدا کرده و به لایه transport ارسال میکند.
- لایه transport – لایه TCP در مثال ما – شماره سگمنت را بررسی میکند تا اطمینان حاصل شود که سگمنتها به ترتیب صحیح قرار دارند.
- همچنین یک چکسام برای سگمنت TCP محاسبه میکند. اگر این چکسام صحیح باشد، لایه TCP یک تأیید دریافت (acknowledgement) به ماشین مبدأ ارسال میکند؛ در غیر این صورت، سگمنت TCP رد میشود.
- تنها کاری که برای لایه TCP باقی میماند این است که بخش دادهای سگمنت را به برنامهای که در لایه بالاتر قرار است آن را دریافت کند، منتقل نماید.
11.1.5. برخورد با مشکلات در اینترنت
یک noeud در یک شبکه میتواند یک رایانه، یک چاپگر هوشمند، یک سرور فایل – در واقع، هر چیزی باشد که قادر به برقراری ارتباط با استفاده از پروتکلهای TCP/IP باشد. هر گره دارای یک آدرس فیزیکی است که قالب آن به نوع شبکه بستگی دارد. در یک شبکه اترنت، آدرس فیزیکی در ۶ بایت رمزگذاری میشود. یک آدرس در یک شبکه X25 یک عدد ۱۴ رقمی است.
آدرس اینترنتی یک گره یک آدرس منطقی است: این آدرس از سختافزار و شبکه مورد استفاده مستقل است. این یک آدرس ۴ بایتی است که هم یک شبکه محلی و هم یک گره در آن شبکه را شناسایی میکند. آدرس اینترنتی معمولاً به صورت چهار عدد—مقادیر چهار بایت—که با نقطه از هم جدا شدهاند، نمایش داده میشود. بنابراین، آدرس ماشین «Lagaffe» در دانشکده علوم آنژ به صورت 193.49.144.1 و آدرس ماشین «Liny» به صورت 193.49.144.9 نوشته میشود. از این طریق، میتوان نتیجه گرفت که آدرس اینترنتی شبکه محلی 193.49.144.0 است. حداکثر 254 گره در این شبکه وجود دارد.
از آنجایی که آدرسهای اینترنتی یا آدرسهای IP از شبکه مستقل هستند، یک رایانه در شبکه A میتواند بدون نیاز به دانستن نوع شبکهای که در آن قرار دارد، با رایانهای در شبکه B ارتباط برقرار کند: این رایانه تنها نیاز دارد که آدرس IP خود را بداند. پروتکل IP در هر شبکه، تبدیل بین آدرس IP و آدرس فیزیکی را در هر دو جهت انجام میدهد.
تمام آدرسهای IP باید با هم متفاوت باشند. در فرانسه، این INRIA است که مسئول تخصیص آدرسهای IP است. در واقع، این سازمان یک آدرس برای شبکه محلی شما صادر میکند، برای مثال 193.49.144.0 برای شبکه دانشکده علوم در آنژ. مدیر این شبکه سپس میتواند آدرسهای IP را از 193.49.144.1 تا 193.49.144.254 به دلخواه تخصیص دهد. این آدرس معمولاً در یک فایل مشخص روی هر دستگاه متصل به شبکه ثبت میشود.
11.1.5.1. کلاسهای آدرس IP
یک آدرس IP توالیای از ۴ بایت است که اغلب به صورت I1.I2.I3.I4 نوشته میشود و در واقع شامل دو آدرس است:
- آدرس شبکه
- آدرس یک گره در آن شبکه
بسته به اندازه این دو بخش، آدرسهای IP به سه کلاس تقسیم میشوند: کلاسهای A، B و C.
کلاس A
آدرس IP: I1.I2.I3.I4 به شکل R1.N1.N2.N3 است، که در آن
R1 آدرس شبکه است
N1.N2.N3 آدرس یک میزبان در آن شبکه است
به طور دقیقتر، یک آدرس کلاس A به شکل IP به شرح زیر است:
آدرس شبکه ۷ بیت طول دارد و آدرس گره ۲۴ بیت طول دارد. بنابراین میتوان ۱۲۷ شبکه کلاس A داشت که هر کدام شامل حداکثر ۲^۲۴ گره میشوند.
کلاس B
در اینجا، آدرس IP:I1.I2.I3.I4 به شکل R1.R2.N1.N2 درمیآید، که در آن
R1.R2 آدرس شبکه است
N1.N2 آدرس یک میزبان در آن شبکه است
به طور دقیقتر، یک آدرس کلاس B به شکل IP به شرح زیر است:
هم آدرس شبکه و هم آدرس گره ۲ بایت (دقیقتر بگوییم ۱۴ بیت) هستند. بنابراین میتوان ۲^۱⁴ شبکه کلاس B داشت که هر کدام شامل حداکثر ۲^۱⁶ گره میشوند.
کلاس C
در این کلاس، آدرس IP:I1.I2.I3.I4 به شکل R1.R2.R3.N1 در میآید، که در آن
R1.R2.R3 نشانگر آدرس شبکه است
N1 آدرس یک میزبان در آن شبکه است
به طور دقیقتر، یک آدرس کلاس C به شکل IP به شرح زیر است:
![]() |
آدرس شبکه ۳ بایت (منهای ۳ بیت) و آدرس میزبان ۱ بایت است. بنابراین میتوان ۲²¹ شبکه کلاس C داشت که هر کدام تا ۲۵۶ میزبان را در خود جای میدهند.
از آنجایی که آدرس ماشین Lagaffe در دانشکده علوم آنژ، 193.49.144.1 است، میتوانیم ببینیم که بایت مهمتر 193 است، یعنی 11000001 در مبنای دو. بنابراین میتوانیم نتیجه بگیریم که این شبکه یک شبکه کلاس C است.
آدرسهای رزرو شده
- برخی از آدرسهای IP، آدرسهای شبکهای هستند، نه آدرس گرهها در شبکه. اینها مواردی هستند که در آنها آدرس گره روی 0 تنظیم شده است. بنابراین، آدرس 193.49.144.0 آدرس شبکه دانشکده علوم در آنژ است. در نتیجه، هیچ گرهای در یک شبکه نمیتواند آدرس صفر را داشته باشد.
- وقتی در آدرسی مانند IP، آدرس گره صرفاً از بیتهای یک تشکیل شده باشد، این آدرس به عنوان آدرس پخش شناخته میشود: این آدرس به تمام گرههای شبکه اشاره دارد.
- در یک شبکه کلاس C که از نظر تئوری امکان وجود ۲^۸ = ۲۵۶ گره را فراهم میکند، اگر دو آدرس رزرو شده را حذف کنیم، تنها ۲۵۴ آدرس معتبر باقی میماند.
11.1.5.2. پروتکلهای تبدیل آدرس اینترنتی <--> آدرس فیزیکی
دیدیم که وقتی دادهها از یک دستگاه به دستگاه دیگر منتقل میشوند، هنگام عبور از لایه IP، در بستهها جاسازی میشوند. این بستهها شکل زیر را دارند:
![]() |
بنابراین بسته IP شامل آدرسهای اینترنتی ماشینهای مبدأ و مقصد است. هنگامی که این بسته به لایه مسئول ارسال آن بر روی شبکه فیزیکی ارسال میشود، اطلاعات بیشتری به آن افزوده میشود تا فریم فیزیکی را که در نهایت از طریق شبکه ارسال خواهد شد، تشکیل دهد. برای مثال، فرمت یک فریم در یک شبکه اترنت به شرح زیر است:
![]() |
فریم نهایی حاوی آدرسهای فیزیکی دستگاههای مبدأ و مقصد است. این آدرسها چگونه به دست میآیند؟
دستگاه ارسالکننده، با دانستن آدرس IP دستگاهی که میخواهد با آن ارتباط برقرار کند، آدرس فیزیکی آن را با استفاده از یک پروتکل خاص به نام ARP (پروتکل حل نشانی) به دست میآورد.
- این ماشین یک نوع بسته ویژه به نام ARP را ارسال میکند که حاوی آدرس IP ماشینی است که آدرس فیزیکی آن مورد جستجو قرار دارد. همچنین با دقت آدرس خود، IP، و همچنین آدرس فیزیکی خود را نیز در آن گنجانده است.
- این بسته به همه گرههای شبکه ارسال میشود.
- این گرهها ماهیت ویژه این بسته را تشخیص میدهند. گرهای که آدرس خود، IP، را در بسته تشخیص میدهد، با ارسال آدرس فیزیکی خود به فرستنده بسته پاسخ میدهد. این کار را چگونه انجام میدهد؟ این گره آدرس و آدرس فیزیکی فرستنده، IP، را در داخل بسته پیدا کرده است.
- بنابراین فرستنده، آدرس فیزیکی مورد نظر خود را دریافت میکند. این آدرس را در حافظه ذخیره میکند تا در صورتی که نیاز به ارسال بستههای بیشتر به همان گیرنده باشد، از آن استفاده کند.
آدرس IP یک دستگاه معمولاً در یکی از فایلهای آن ذخیره میشود، و بنابراین میتواند برای بازیابی آن به این فایل مراجعه کند. این آدرس قابل تغییر است: تنها کاری که لازم است انجام شود ویرایش فایل است. اما آدرس فیزیکی در حافظه کارت شبکه ذخیره میشود و قابل تغییر نیست.
هنگامی که یک مدیر قصد دارد شبکه خود را سازماندهی مجدد کند، ممکن است نیاز باشد آدرسهای IP تمام گرهها را تغییر دهد و در نتیجه فایلهای پیکربندی مختلف گرهها را ویرایش کند. این کار در صورت وجود تعداد زیاد ماشینها میتواند خستهکننده و مستعد خطا باشد. یک روش این است که به ماشینها آدرس IP اختصاص داده نشود: در عوض، یک کد ویژه در فایلی که ماشین قرار است آدرس IP خود را از آن بازیابی کند، وارد میشود. پس از تشخیص اینکه آدرس IP را ندارد، ماشین با استفاده از پروتکلی به نام RARP (پروتکل حل نشانی معکوس) آن را درخواست میکند. سپس یک بسته ویژه به نام بسته RARP را از طریق شبکه ارسال میکند که مشابه بسته قبلی ARP است و در آن آدرس فیزیکی خود را درج میکند. این بسته به همه گرهها ارسال میشود و آنها آن را به عنوان بسته RARP تشخیص میدهند. یکی از اینها که به نام سرور RARP شناخته میشود، فایلی دارد که نگاشت بین آدرسهای فیزیکی و آدرسهای IP را برای همه گرهها در خود جای داده است. سپس به فرستنده بسته RARP پاسخ میدهد و آدرس IP خود را ارسال میکند. بنابراین، یک مدیر که مایل به پیکربندی مجدد شبکه خود است، به سادگی کافی است فایل نگاشت را روی سرور RARP ویرایش کند. این سرور معمولاً باید دارای یک آدرس ثابت، IP، باشد که باید بتواند آن را بدون نیاز به استفاده از پروتکل RARP شناسایی کند.
11.1.6. لایه شبکه، که به نام لایه IP اینترنت شناخته میشود
پروتکل IP (پروتکل اینترنت) قالب بستهها و نحوهٔ پردازش آنها هنگام ارسال یا دریافت را تعریف میکند. این نوع خاص بسته، دیتاگرام IP نامیده میشود. ما قبلاً در این مورد بحث کردهایم:
![]() |
نکته مهم این است که، علاوه بر دادههای ارسالی، دیتاگرام IP حاوی آدرسهای اینترنتی ماشینهای مبدأ و مقصد است. این امر به ماشین مقصد امکان میدهد تا بداند چه کسی در حال ارسال پیام برای آن است.
برخلاف یک فریم شبکه که طول آن توسط ویژگیهای فیزیکی شبکهای که در آن حرکت میکند تعیین میشود، طول دیتاگرام IP توسط نرمافزار ثابت شده و بنابراین در شبکههای فیزیکی مختلف یکسان خواهد بود. دیدیم که هنگام حرکت از لایه شبکه به سمت لایه فیزیکی، دیتاگرام IP در یک فریم فیزیکی جاسازی شد. ما مثال فریم فیزیکی برای یک شبکه اترنت را ارائه دادیم:
فریمهای فیزیکی از گره به گره به سمت مقصد خود حرکت میکنند، که ممکن است در همان شبکه فیزیکی که ماشین فرستنده در آن قرار دارد نباشد. بنابراین، بسته IP ممکن است به صورت متوالی در فریمهای فیزیکی مختلف در گرههایی که محل اتصال دو شبکه با انواع مختلف هستند، جاسازی شود. همچنین ممکن است بسته IP برای جاسازی شدن در یک فریم فیزیکی واحد، بیش از حد بزرگ باشد. سپس نرمافزار IP در گرهای که این مشکل در آن رخ میدهد، بسته IP را طبق قوانین خاصی به fragments تقسیم میکند و هر بخش به نوبه خود از طریق شبکه فیزیکی ارسال میشود. این بخشها تنها در مقصد نهایی خود دوباره مونتاژ خواهند شد.
11.1.6.1. مسیریابی
روتینگ روشی است برای هدایت بستههای IP به مقصدشان. دو روش وجود دارد: مسیریابی مستقیم و مسیریابی غیرمستقیم.
مسیریابی مستقیم
مسیر یابی مستقیم به انتقال یک بسته IP مستقیماً از فرستنده به گیرنده در همان شبکه اشاره دارد:
- ماشینی که یک دیتاگرام IP را ارسال میکند، آدرس گیرنده، IP را دارد.
- این پروتکل آدرس فیزیکی گیرنده را از طریق ARP یا از جدولهای خود، در صورتی که قبلاً این آدرس بهدست آمده باشد، بهدست میآورد.
- این بسته را از طریق شبکه به آن آدرس فیزیکی ارسال میکند.
مسیریابی غیرمستقیم
مسیر یابی غیرمستقیم به فوروارد کردن یک بسته IP به مقصدی در شبکهای غیر از شبکهای که فرستنده به آن تعلق دارد، اشاره دارد. در این حالت، بخشهای آدرس شبکه در آدرسهای IP ماشینهای مبدا و مقصد متفاوت هستند. ماشین مبدا این موضوع را تشخیص میدهد. سپس بسته را به یک گره ویژه به نام روتر (router) ارسال میکند؛ گرهای که یک شبکه محلی را به شبکههای دیگر متصل میکند و آدرس آن (IP) را در جدولهای خود پیدا میکند؛ این آدرس در ابتدا یا از یک فایل، از حافظه دائمی، یا از طریق اطلاعاتی که در شبکه در گردش است، به دست آمده است.
یک روتر به دو شبکه متصل است و در هر دوی این شبکهها آدرسی به نام IP دارد.
![]() |
در مثال بالا:
. شبکه شماره ۱ آدرس اینترنتی 193.49.144.0 و شبکه شماره ۲ آدرس 193.49.145.0 را دارد.
. در شبکه شماره ۱، روتر آدرس ۱۹۳.۴۹.۱۴۴.۶ را دارد و در شبکه شماره ۲، آدرس ۱۹۳.۴۹.۱۴۵.۳ را دارد.
نقش روتر تبدیل بسته IP است که دریافت میکند—که در یک فریم فیزیکی متداول در شبکه شماره ۱ قرار دارد—به یک فریم فیزیکی که بتواند از طریق شبکه شماره ۲ ارسال شود. اگر آدرس مقصد بسته، IP، در شبکه شماره ۲ باشد، روتر بسته را مستقیماً به آن ارسال میکند؛ در غیر این صورت، آن را به روتر دیگری که شبکه شماره ۲ را به شبکه شماره ۳ متصل میکند، و به همین ترتیب، ارسال خواهد کرد.
11.1.6.2. پیامهای خطا و کنترل
همچنین در لایه شبکه – و بنابراین در همان سطح پروتکل IP – پروتکل ICMP (پروتکل پیام کنترلی اینترنت) قرار دارد. از این پروتکل برای ارسال پیامهایی در مورد عملکرد داخلی شبکه استفاده میشود: گرههای از کار افتاده، تراکم در یک روتر و غیره. پیامهای ICMP در بستههای IP جاسازی شده و از طریق شبکه ارسال میشوند. لایههای IP در گرههای مختلف بر اساس پیامهای ICMP که دریافت میکنند، اقدامات مناسب را انجام میدهند. در نتیجه، خودِ یک برنامه هرگز این مسائل خاص شبکه را مشاهده نمیکند.
یک گره از اطلاعات ICMP برای بهروزرسانی جدولهای مسیریابی خود استفاده میکند.
11.1.7. لایه انتقال: پروتکلهای UDP و TCP
11.1.7.1. پروتکل UDP: پروتکل دادگرام کاربر
پروتکل UDP امکان تبادل غیرقابل اعتمادی داده را بین دو نقطه فراهم میکند؛ به عبارت دیگر، تحویل صحیح یک بسته به مقصدش تضمین نشده است. برنامه کاربردی، در صورت تمایل، میتواند این کار را خود مدیریت کند، برای مثال با منتظر ماندن برای تأیید دریافت پس از ارسال یک پیام، قبل از ارسال پیام بعدی.
تا اینجای کار، در سطح شبکه، در مورد آدرسهای IP برای ماشینها بحث کردهایم. با این حال، روی یک ماشین واحد، فرآیندهای مختلفی میتوانند همزمان وجود داشته باشند که همگی میتوانند با یکدیگر ارتباط برقرار کنند. بنابراین، هنگام ارسال یک پیام، لازم است نه تنها آدرس IP ماشین گیرنده، بلکه «نام» فرآیند گیرنده را نیز مشخص کرد. این نام در واقع یک عدد است که به آن شماره پورت گفته میشود. برخی اعداد برای برنامههای استاندارد رزرو شدهاند: برای مثال، پورت ۶۹ برای برنامه TFTP (پروتکل انتقال فایل ساده) در نظر گرفته شده است.
پکتهایی که توسط پروتکل UDP پردازش میشوند، همچنین به عنوان دیتاگرام شناخته میشوند. آنها شکل زیر را دارند:
این دیتاگرامها در بستههای IP و سپس در فریمهای فیزیکی قرار میگیرند.
11.1.7.2. پروتکل TCP: پروتکل کنترل انتقال
برای ارتباطات امن، پروتکل UDP ناکافی است: توسعهدهنده برنامه باید پروتکل خود را برای اطمینان از مسیریابی صحیح بستهها طراحی کند. پروتکل TCP (پروتکل کنترل انتقال) از این مشکلات جلوگیری میکند. ویژگیهای آن به شرح زیر است:
- فرآیندی که قصد ارسال داده را دارد، ابتدا یک اتصال با فرایندی که قرار است اطلاعات ارسالی را دریافت کند، برقرار مینماید. این اتصال بین یک پورت در ماشین ارسالکننده و یک پورت در ماشین دریافتکننده برقرار میشود. بدین ترتیب یک مسیر مجازی بین دو پورت ایجاد میشود که منحصراً برای دو فرایندی که اتصال را برقرار کردهاند، رزرو شده است.
- تمام بستههای ارسالی توسط فرآیند مبدأ از این مسیر مجازی پیروی میکنند و به ترتیبی که ارسال شدهاند، میرسند؛ امری که در پروتکل UDP تضمین نشده بود، زیرا بستهها میتوانستند مسیرهای متفاوتی را دنبال کنند.
- دادههای ارسالشده پیوسته به نظر میرسند. فرآیند ارسال، دادهها را با نرخ خود ارسال میکند. این دادهها لزوماً بلافاصله ارسال نمیشوند: پروتکل TCP منتظر میماند تا دادهی کافی برای ارسال داشته باشد. این دادهها در ساختاری به نام سگمنت TCP ذخیره میشوند. به محض پر شدن این بخش، به لایه IP منتقل میشود، جایی که در یک بسته IP جاسازی خواهد شد.
- هر بخش ارسالشده توسط پروتکل TCP شمارهگذاری میشود. پروتکل دریافتکننده TCP بررسی میکند که بخشها را به ترتیب دریافت میکند. برای هر بخش که به درستی دریافت میشود، یک تأیید به فرستنده ارسال میکند.
- هنگامی که فرستنده این تأیید را دریافت میکند، فرآیند ارسال را مطلع میسازد. بنابراین، فرآیند ارسال میتواند تأیید کند که یک بخش به سلامت رسیده است، امری که با پروتکل UDP امکانپذیر نبود.
- اگر پروتکل TCP که یک سگمنت را ارسال کرده است، پس از گذشت مدت زمان معینی، تأیید دریافتی دریافت نکند، آن سگمنت را مجدداً ارسال میکند و بدین ترتیب کیفیت سرویس تحویل اطلاعات را تضمین مینماید.
- مدار مجازی ایجاد شده بین دو فرآیند ارتباطی full-duplex است: این بدان معناست که اطلاعات میتواند در هر دو جهت جریان یابد. بنابراین، فرآیند مقصد میتواند حتی زمانی که فرآیند منبع به ارسال اطلاعات ادامه میدهد، تأییدها را ارسال کند. این امر به پروتکل منبع TCP اجازه میدهد تا چندین سگمنت را بدون انتظار برای دریافت تأیید ارسال کند. اگر پس از گذشت مدت زمان مشخصی متوجه شود که برای یک سگمنت خاص با شماره n تأییدی دریافت نکرده است، ارسال سگمنتها را از همان نقطه از سر خواهد گرفت.
11.1.8. لایه کاربردی
در بالای پروتکلهای UDP و TCP، پروتکلهای استاندارد مختلفی وجود دارند:
TELNET
این پروتکل به یک کاربر روی ماشین A در شبکه اجازه میدهد تا به ماشین B (که اغلب به آن ماشین میزبان گفته میشود) متصل شود. TELNET یک ترمینال به اصطلاح جهانی را روی ماشین A شبیهسازی میکند. بنابراین کاربر طوری رفتار میکند که گویی یک ترمینال به ماشین B متصل است. تلنت بر پایه پروتکل TCP است.
FTP: (پروتکل انتقال فایل)
این پروتکل امکان تبادل فایل بین دو ماشین راه دور و همچنین عملیاتهای فایل مانند ایجاد دایرکتوری را فراهم میکند. این پروتکل بر اساس پروتکل TCP است.
TFTP: (کنترل انتقال فایل پیشپاافتاده)
این پروتکل گونهای از FTP است. این پروتکل بر اساس پروتکل UDP ساخته شده و نسبت به FTP از پیچیدگی کمتری برخوردار است.
DNS: (سیستم نام دامنه)
هنگامی که یک کاربر بخواهد با استفاده از FTP برای مثال، فایلها را با یک ماشین راه دور مبادله کند، باید آدرس اینترنتی آن ماشین را بداند. برای مثال، برای انجام FTP روی ماشین «Lagaffe» در دانشگاه آنژ، لازم است FTP را به صورت زیر اجرا کرد: FTP 193.49.144.1
این کار نیازمند یک نگاشت دایرکتوری برای تبدیل نام ماشینها به آدرسهای IP است. احتمالاً در این دایرکتوری، ماشینها با نامهای نمادین مانند زیر مشخص خواهند شد:
ماشین DPX2/320 در دانشگاه آنژه
ماشین Sun در دانشگاه آنژ، ISERPA
واضح است که ارجاع به یک ماشین با نام راحتتر است تا با آدرس آن IP. این مسئله مسأله یکتایی نام را مطرح میکند: میلیونها ماشین به هم متصل وجود دارد. ممکن است تصور شود یک نهاد متمرکز نامها را اختصاص میدهد. این بدون شک نسبتاً دستوپاگیر خواهد بود. در واقع کنترل نامها در میان دامنهها توزیع شده است. هر دامنه توسط سازمانی مدیریت میشود که معمولاً بسیار چابک است و در انتخاب نام ماشینها آزادی کامل دارد. بنابراین، ماشینها در فرانسه به دامنه «fr» تعلق دارند که توسط Inria در پاریس مدیریت میشود. برای سادهتر کردن موضوع، کنترل حتی بیشتر توزیع شده است: دامنههایی در داخل دامنه «fr» ایجاد میشوند. بنابراین، دانشگاه آنژه به دامنه «univ-Angers» تعلق دارد. بخش مدیریتکننده این دامنه آزادی کامل برای نامگذاری رایانههای موجود در شبکه دانشگاه آنژه دارد. در حال حاضر، این دامنه به زیرمجموعههایی تقسیم نشده است. با این حال، در یک دانشگاه بزرگ با رایانههای شبکهای فراوان، میتوان آن را به زیرمجموعههایی تقسیم کرد.
رایانه DPX2/رایانه 320 در دانشگاه آنژه با نام Lagaffe نامگذاری شده است، در حالی که یک رایانه با نام PC و 486DX50 با نام liny نامگذاری شده است. این ماشینها چگونه باید بهطور خارجی ارجاع داده شوند؟ با مشخص کردن سلسلهمراتب دامنههایی که به آنها تعلق دارند. بنابراین، نام کامل ماشین «Lagaffe» به این صورت خواهد بود:
Lagaffe.univ-Angers.fr
در داخل دامنهها میتوان از نامهای نسبی استفاده کرد. بنابراین، در داخل دامنهٔ «fr» و خارج از دامنهٔ «univ-Angers»، میتوان به ماشین «Lagaffe» به صورت اشاره کرد
Lagaffe.univ-Angers
در نهایت، در داخل دامنه univ-Angers، میتوان به آن به سادگی به صورت
لاگاف
بنابراین یک برنامه میتواند با استفاده از نام به یک ماشین ارجاع دهد. با این حال، در نهایت شما همچنان باید آدرس اینترنتی آن ماشین را به دست آورید. این کار چگونه انجام میشود؟ فرض کنید که از ماشین A میخواهیم با ماشین B ارتباط برقرار کنیم.
- اگر ماشین B به همان دامنهای که ماشین A در آن قرار دارد تعلق داشته باشد، آدرس آن IP احتمالاً در فایلی روی ماشین A یافت میشود.
- در غیر این صورت، ماشین A در فایلی دیگر یا همان فایل قبلی، فهرستی از چندین سرور نام به همراه آدرسهایشان IP را پیدا خواهد کرد. یک سرور نام مسئول نگاشت نام یک ماشین به آدرس آن IP است. ماشین A یک پرسش ویژه به اولین سرور نام در فهرست خود ارسال میکند که به آن پرسش DNS گفته میشود و بنابراین شامل نام ماشینی است که جستجو میشود. اگر سرور مورد پرسش این نام را در سوابق خود داشته باشد، آدرس متناظر، IP، را برای ماشین A ارسال خواهد کرد. در غیر این صورت، سرور در فایلهای خود فهرستی از سرورهای نام را که میتواند از آنها پرسوجو کند، پیدا خواهد کرد. سپس این کار را انجام میدهد. بدین ترتیب، از تعدادی سرور نام پرسوجو میشود، نه به صورت تصادفی، بلکه به گونهای که تعداد پرسوجوها را به حداقل میرساند. اگر ماشین در نهایت پیدا شود، پاسخ به ماشین A بازگردانده خواهد شد.
XDR: (نمایش داده eXternal)
این پروتکل که توسط Sun MicroSystems ایجاد شده است، یک نمایش استاندارد و مستقل از ماشین برای دادهها را مشخص میکند.
RPC: (فراخوانی رویهای از راه دور)
این پروتکل که توسط شرکت Sun نیز تعریف شده است، یک پروتکل ارتباطی مستقل از لایه انتقال برای برنامههای کاربردی راه دور است. این پروتکل مهم است: زیرا برنامهنویس را از نیاز به دانستن جزئیات لایه انتقال رها میکند و برنامههای کاربردی را قابل حمل میسازد. این پروتکل بر اساس پروتکل XDR ساخته شده است.
NFS: سیستم فایل شبکه
این پروتکل که همچنین توسط Sun تعریف شده است، به یک دستگاه امکان میدهد تا سیستم فایل دستگاه دیگر را «ببیند». این پروتکل بر اساس پروتکل قبلی RPC است.
11.1.9. نتیجهگیری
در این مقدمه، برخی از جنبههای کلیدی پروتکلهای اینترنت را تشریح کردیم. برای بررسی عمیقتر این موضوع، خوانندگان میتوانند به کتاب عالی داگلاس کامر مراجعه کنند:
عنوان TCP/IP: معماری، پروتکلها، کاربردها.
نویسنده داگلاس COMER
ناشر InterEditions
11.2. کلاسهای .NET برای مدیریت آدرس IP
یک دستگاه در اینترنت بهطور منحصربهفرد با یک آدرس پروتکل اینترنت (IP) شناسایی میشود که میتواند دو شکل داشته باشد:
- IPv4: رمزگذاریشده به صورت ۳۲ بیت و نمایش داده شده توسط یک رشته به شکل «I1.I2.I3.I4»، که در آن In عددی بین ۱ و ۲۵۴ است. اینها در حال حاضر رایجترین آدرسهای IP هستند.
- IPv6: رمزگذاریشده به صورت ۱۲۸ بیتی و نمایش داده شده توسط یک رشته از نوع «[I1.I2.I3.I4.I5.I6.I7.I8]»، که در آن In یک رشته ۴ رقمی هگزادسیمال است. در این سند، ما از آدرسهای IPv6 استفاده نخواهیم کرد.
یک ماشین همچنین میتواند با یک نام منحصربهفرد شناسایی شود. این نام اجباری نیست، زیرا برنامهها در نهایت همیشه از آدرسهای IP ماشینها استفاده میکنند. این نامها برای آسانتر کردن کار کاربران وجود دارند. بنابراین، درخواست http://www.ibm.com (URL) با استفاده از یک مرورگر وب آسانتر است تا URL http://129.42.17.99، هرچند هر دو روش امکانپذیر هستند.
یک دستگاه ممکن است چندین آدرس IP داشته باشد اگر به صورت فیزیکی به چندین شبکه در یک زمان متصل باشد. در این صورت، آن دستگاه روی هر شبکه یک آدرس IP خواهد داشت.
یک آدرس IP میتواند به دو روش در .NET نمایش داده شود:
- بهصورت یک رشتهٔ کاراکتری «I1.I2.I3.I4» یا «[I1.I2.I3.I4.I5.I6.I7.I8]»
- به صورت یک شیء از نوع IPAddress
کلاس IPAddress
متدهای M، ویژگیهای P و ثابتهای C کلاس IPAddress عبارتند از:
P | خانواده آدرس IP. نوع AddressFamily یک شمارششده است. دو مقدار رایج عبارتند از: AddressFamily.InterNetwork: برای نشانی IPv4 AddressFamily.InterNetworkV6: برای نشانی IPv6 | |
C | آدرس IP «0.0.0.0». وقتی یک سرویس با این آدرس مرتبط باشد، به این معنی است که آن سرویس، مشتریها را روی تمام آدرسهای IP ماشین میزبان خود میپذیرد. | |
C | آدرس IP "127.0.0.1". به عنوان «آدرس حلقه بازگشت» (loopback address) شناخته میشود. هنگامی که یک سرویس با این آدرس مرتبط میشود، به این معنی است که فقط مشتریانی را میپذیرد که روی همان دستگاهی که خود سرویس روی آن اجرا شده، قرار دارند. | |
C | آدرس IP "255.255.255.255". هنگامی که یک سرویس با این آدرس مرتبط میشود، به این معنی است که هیچ کلاینتی را نمیپذیرد. | |
M | تلاش میکند آدرس IP ipString، که به شکل "I1.I2.I3.I4" است، را به یک شیء آدرس IPAddress تبدیل کند. در صورت موفقیت عملیات، مقدار true را برمیگرداند. | |
M | در صورتی که آدرس IP برابر با "127.0.0.1" باشد، مقدار true را برمیگرداند. | |
Mمقدار را به true تنظیم میکند اگر آدرس QZXW2HTMLP005147Z | آدرس IP را به شکل "I1.I2.I3.I4" یا "[I1.I2.I3.I4.I5.I6.I7.I8]" تبدیل میکند |
نقشهبرداری بین آدرسهای IP و nomMachine توسط یک سرویس توزیعشده اینترنتی به نام DNS (سیستم نام دامنه) فراهم میشود. روشهای ایستا کلاس Dns میتوانند برای برقراری نگاشت آدرس IP <--> nomMachine استفاده شوند:
آدرس IPHostEntry را از آدرس IP بازمیگرداند، یا به صورت یک رشته یا از نام ماشین. در صورتی که ماشین پیدا نشود، یک استثنا پرتاب میکند. | |
یک آدرس IPHostEntry را از یک آدرس IP از نوع IPAddress بازمیگرداند. در صورتی که ماشین پیدا نشود، یک استثنا ایجاد میکند. | |
نام ماشینی را که برنامه در حال اجرای این دستور روی آن در حال اجرا است، بازمیگرداند | |
آدرسهای IP ماشینی را که با نام آن یا یکی از آدرسهای IP آن شناسایی شده است، بازمیگرداند. |
یک نمونه از نوع IPHostEntry نشانیها IP، نامهای مستعار و نام یک ماشین را در بر میگیرد. نوع IPHostEntry به شرح زیر است:
P | جدول آدرسهای IP ماشین | |
P | نامهای مستعار ماشین DNS. اینها نامهایی هستند که به آدرسهای مختلف ماشین IP مربوط میشوند. | |
P | نام میزبان اصلی ماشین |
برنامه زیر را در نظر بگیرید که نام ماشینی را که روی آن اجرا میشود نمایش میدهد و سپس، به صورت تعاملی، نگاشتهای بین آدرس IP و نام ماشین را نشان میدهد:
using System;
using System.Net;
namespace Chap9 {
class Program {
static void Main(string[] args) {
// نام ماشین محلی را نمایش میدهد
//سپس بهصورت تعاملی اطلاعاتی دربارهٔ ماشینهای شبکه ارائه میدهد
// شناساییشده توسط یک نام یا یک آدرس IP
// ماشین محلی
Console.WriteLine("Machine Locale= {0}" ,Dns.GetHostName());
//جلسه تعاملی پرسش و پاسخ
string machine;
IPHostEntry ipHostEntry;
while (true) {
//وارد کردن نام یا آدرس IP ماشین مورد جستجو
Console.Write("Machine recherchée (rien pour arrêter) : ");
machine = Console.ReadLine().Trim().ToLower();
// تمام شد؟
if (machine == "") return;
// پردازش استثنا
try {
//جستجوی ماشین
ipHostEntry = Dns.GetHostEntry(machine);
//نام ماشین
Console.WriteLine("Machine : " + ipHostEntry.HostName);
// آدرسهای ماشین IP
Console.Write("Adresses IP : {0}" , ipHostEntry.AddressList[0]);
for (int i = 1; i < ipHostEntry.AddressList.Length; i++) {
Console.Write(", {0}" , ipHostEntry.AddressList[i]);
}
Console.WriteLine();
// نامهای مستعار ماشین
if (ipHostEntry.Aliases.Length != 0) {
Console.Write("Alias : {0}" , ipHostEntry.Aliases[0]);
for (int i = 1; i < ipHostEntry.Aliases.Length; i++) {
Console.Write(", {0}" , ipHostEntry.Aliases[i]);
}
Console.WriteLine();
}
} catch {
// ماشین وجود ندارد
Console.WriteLine("Impossible de trouver la machine [{0}]",machine);
}
}
}
}
}
اجرا نتایج زیر را تولید میکند:
11.3. مبانی برنامهنویسی وب
11.3.1. عمومی
بیایید ارتباط بین دو ماشین دور، A و B را در نظر بگیریم:
![]() |
وقتی یک برنامه کاربردی AppA روی ماشین A بخواهد با یک برنامه کاربردی AppB روی ماشین B در اینترنت ارتباط برقرار کند، باید چندین نکته را بداند:
- آدرس IP یا نام ماشین B
- شماره پورت مورد استفاده توسط برنامه AppB. این به این دلیل است که ماشین B میتواند از برنامههای متعددی که از طریق اینترنت کار میکنند، پشتیبانی کند. هنگامی که اطلاعاتی را از شبکه دریافت میکند، باید بداند آن اطلاعات برای کدام برنامه در نظر گرفته شده است. برنامههای روی ماشین B از طریق رابطهایی که به نام پورتهای ارتباطی نیز شناخته میشوند، به شبکه دسترسی پیدا میکنند. این اطلاعات در بسته دریافتی توسط ماشین B گنجانده شده است تا بتواند به برنامه صحیح تحویل داده شود.
- پروتکلهای ارتباطی که ماشین B از آنها پشتیبانی میکند. در مطالعه ما، تنها از پروتکلهای TCP-IP استفاده خواهیم کرد.
- پروتکل گفتگوی پذیرفتهشده توسط برنامه AppB. در واقع، ماشینهای A و B با یکدیگر «صحبت» خواهند کرد. آنچه آنها میگویند در پروتکلهای TCP-IP قرار داده خواهد شد. با این حال، زمانی که در سوی دیگر این زنجیره، برنامه AppB اطلاعاتی را که توسط برنامه AppA ارسال شده است دریافت میکند، باید بتواند آن را تفسیر کند. این مشابه وضعیتی است که دو نفر، A و B، از طریق تلفن با یکدیگر ارتباط برقرار میکنند: مکالمه آنها توسط تلفن منتقل میشود. گفتار توسط تلفن A به صورت سیگنال رمزگذاری شده و از طریق خطوط تلفن منتقل میشود و به تلفن B میرسد که در آنجا رمزگشایی میشود. سپس شخص B گفتار را میشنود. اینجاست که مفهوم پروتکل ارتباطی مطرح میشود: اگر A به زبان فرانسوی صحبت کند و B آن زبان را نفهمد، A و B قادر به برقراری ارتباط مؤثر نخواهند بود.
بنابراین، دو برنامهای که در حال ارتباط هستند باید بر سر نوع دیالوگی که استفاده خواهند کرد، توافق کنند. برای مثال، دیالوگ با سرویس با نام ftp با دیالوگ با سرویس با نام pop یکسان نیست: این دو سرویس دستورات یکسانی را قبول نمیکنند. آنها پروتکل دیالوگ متفاوتی دارند.
11.3.2. ویژگیهای پروتکل TCP
در اینجا، ما فقط ارتباطات شبکهای را که از پروتکل حملونقل TCP استفاده میکنند، بررسی خواهیم کرد. بیایید ویژگیهای آن را به یاد بیاوریم:
- فرآیندی که قصد ارسال داده را دارد، ابتدا یک اتصال با فرایندی که قرار است اطلاعات را دریافت کند، برقرار مینماید. این اتصال بین یک پورت در ماشین ارسالکننده و یک پورت در ماشین دریافتکننده برقرار میشود. بدین ترتیب یک مسیر مجازی بین دو پورت ایجاد میشود که منحصراً برای دو فرایندی که اتصال را برقرار کردهاند، رزرو خواهد شد.
- تمام بستههای ارسالی توسط فرآیند مبدأ از این مسیر مجازی پیروی میکنند و به ترتیبی که ارسال شدهاند، میرسند
- دادههای منتقلشده به صورت یک جریان پیوسته ظاهر میشوند. فرآیند ارسالکننده دادهها را با سرعت خود ارسال میکند. این دادهها لزوماً بلافاصله ارسال نمیشوند: پروتکل TCP منتظر میماند تا دادهی کافی برای ارسال داشته باشد. این دادهها در ساختاری به نام «بخش TCP» ذخیره میشوند. به محض اینکه این بخش پر شود، به لایه IP منتقل میشود، جایی که در یک بسته IP جاسازی خواهد شد.
- هر بخش ارسالشده توسط پروتکل TCP شمارهگذاری میشود. پروتکل گیرنده TCP بررسی میکند که بخشها را به ترتیب دریافت میکند. برای هر بخش که به درستی دریافت میشود، یک تأیید دریافت به فرستنده ارسال میکند.
- هنگامی که فرستنده این تأیید را دریافت میکند، فرآیند ارسال را مطلع میسازد. بنابراین، فرآیند ارسال میتواند تأیید کند که یک بخش با موفقیت تحویل داده شده است.
- اگر پروتکل TCP که یک سگمنت را ارسال کرده است، پس از گذشت مدت زمان معینی، تأیید دریافت را دریافت نکند، آن سگمنت را مجدداً ارسال میکند و بدین ترتیب کیفیت سرویس تحویل اطلاعات را تضمین مینماید.
- مدار مجازی ایجاد شده بین دو فرآیند ارتباطی، full-duplex است: این بدان معناست که اطلاعات میتواند در هر دو جهت جریان یابد. بنابراین، فرآیند مقصد میتواند حتی زمانی که فرآیند منبع به ارسال اطلاعات ادامه میدهد، تأییدها را ارسال کند. این امر به فرآیند منشأ TCP اجازه میدهد تا چندین بخش را بدون انتظار برای تأیید ارسال کند. اگر پس از مدتی متوجه شود که برای بخش خاصی با شماره n تأییدی دریافت نکرده است، ارسال بخشها را از آن نقطه از سر خواهد گرفت.
11.3.3. رابطهٔ کلاینت-سرور
ارتباطات در اینترنت اغلب ناهمزمان (asymmetric) است: ماشین A یک اتصال را برای درخواست یک سرویس از ماشین B آغاز میکند و مشخص میکند که میخواهد یک اتصال با سرویس SB1 روی ماشین B برقرار کند. ماشین B یا آن را میپذیرد یا رد میکند. اگر درخواست پذیرفته شود، ماشین A میتواند درخواستهای خود را به سرویس SB1 ارسال کند. این درخواستها باید مطابق با پروتکل گفتگویی باشد که توسط سرویس SB1 درک میشود. بدین ترتیب یک دیالوگ درخواست-پاسخ بین ماشین A، که به عنوان ماشین کلاینت شناخته میشود، و ماشین B، که به عنوان ماشین سرور شناخته میشود، برقرار میگردد. یکی از این دو طرف اتصال را قطع خواهد کرد.
11.3.4. معماری کلاینت
معماری یک برنامه شبکهای که خدمات یک برنامه سرور را درخواست میکند به شرح زیر خواهد بود:
ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
tant que ce n'est pas fini
préparer une demande
l'émettre vers la machine B
attendre et récupérer la réponse
la traiter
fin tant que
finsi
fermer la connexion
11.3.5. معماری سرور
معماری برنامهای که خدمات ارائه میدهد به شرح زیر خواهد بود:
ouvrir le service sur la machine locale
tant que le service est ouvert
se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que
برنامه سرور، درخواست اتصال اولیه یک کلاینت را به طور متفاوتی از درخواستهای بعدی او برای یک سرویس مدیریت میکند. این برنامه خود سرویس را ارائه نمیدهد. اگر این کار را انجام میداد، در حین ارائه سرویس دیگر منتظر درخواستهای اتصال نمیماند و در نتیجه به کلاینتها خدماتی ارائه نمیشد. بنابراین، روند کار به این صورت است: به محض دریافت و پذیرش یک درخواست اتصال در پورت گوشدادن، سرور وظیفهای را ایجاد میکند که مسئول ارائه خدمتی است که توسط کلاینت درخواست شده است. این خدمت در پورت دیگری روی ماشین سرور ارائه میشود که به آن پورت سرویس گفته میشود. این امر امکان سرویسدهی به چندین کلاینت را به طور همزمان فراهم میکند.
یک وظیفه سرویس ساختار زیر را خواهد داشت:
tant que le service n'a pas été rendu totalement
attendre une demande sur le port de service
lorsqu'il y en a une, élaborer la réponse
transmettre la réponse via le port de service
fin tant que
libérer le port de service
11.4. پروتکلهای ارتباطی اینترنت را کشف کنید:
11.4.1. مقدمه
پس از آنکه یک کلاینت به یک سرور متصل شد، یک دیالوگ بین آنها برقرار میشود. ماهیت این دیالوگ همان چیزی است که به آن پروتکل ارتباطی سرور گفته میشود. از جمله رایجترین پروتکلهای اینترنت میتوان به موارد زیر اشاره کرد:
- HTTP: پروتکل انتقال HyperText – پروتکل ارتباط با یک سرور وب (سرور HTTP)
- SMTP: پروتکل انتقال ساده ایمیل – پروتکل ارتباط با یک سرور ارسال ایمیل (سرور SMTP)
- POP: پروتکل دفتر پستی – پروتکل ارتباط با یک سرور ذخیرهسازی ایمیل (سرور POP). این پروتکل برای بازیابی ایمیلهای دریافتی استفاده میشود، نه برای ارسال آنها.
- FTP: پروتکل انتقال فایل – پروتکل ارتباط با یک سرور ذخیرهسازی فایل (سرور FTP).
تمام این پروتکلها مبتنی بر متن هستند: کلاینت و سرور خطوط متنی را مبادله میکنند. اگر کلاینتی داشته باشیم که قادر به انجام موارد زیر باشد:
- برقراری ارتباط با یک سرور TCP
- نمایش خطوط متنی که سرور برای آن ارسال میکند در کنسول
- ارسال خطوط متنی که کاربر تایپ میکند به سرور
در این صورت، اگر قواعد آن پروتکل را بدانیم، میتوانیم با استفاده از یک پروتکل مبتنی بر متن با یک سرور TCP ارتباط برقرار کنیم.
برنامه تلنت موجود در سیستمعاملهای یونیکس یا ویندوز یکی از این کلاینتها است. در سیستمعامل ویندوز، ابزاری به نام PuTTY نیز وجود دارد و این همان ابزاری است که ما در اینجا از آن استفاده خواهیم کرد. PuTTY را میتوان از آدرس [http://www.putty.org/] دانلود کرد. این یک فایل اجرایی (.exe) است که میتوان آن را مستقیماً اجرا کرد. ما آن را به شرح زیر پیکربندی خواهیم کرد:
![]() |
- [1]: آدرس IP سرور TCP که میخواهیم به آن متصل شویم، یا نام آن
- [2]: پورت گوشدادن سرور TCP
- [3]: از حالت Raw استفاده کنید که نشاندهنده یک اتصال TCP خام است.
- [4]: را روی حالت Never تنظیم کنید تا اگر سرور اتصال را قطع کند، پنجرهٔ کلاینت putty بسته نشود.
- [6,7]: تعداد ستونها/ردهها در کنسول
- [5]: حداکثر تعداد خطوطی که در حافظه نگهداری میشوند. یک سرور HTTP ممکن است تعداد زیادی خط ارسال کند. باید بتوان آنها را پیمایش کرد.
![]() |
- [8,9]: برای حفظ تنظیمات قبلی، به پیکربندی یک نام بدهید ([8]) و آن را ذخیره کنید ([9]).
- [11,12]: برای بازیابی یک پیکربندی ذخیره شده، [11] را انتخاب کرده و آن را به عنوان [12] بارگذاری کنید.
با پیکربندی این ابزار به این صورت، بیایید برخی از پروتکلهای TCP را بررسی کنیم.
11.4.2. پروتکل HTTP (پروتکل انتقال HyperText)
بیایید کلاینت TCP خود را به سرور وب روی ماشین istia.univ-angers.fr [2]، پورت 80 [3] متصل کنیم:
![]() |
در کنسول putty، دیالوگ زیر را میسازیم: HTTP:
- خطوط ۱–۴ درخواست کلاینت هستند که روی صفحهکلید تایپ شدهاند
- خطوط ۵–۱۹ پاسخ سرور هستند
- خط ۱: سینتکس GET UrlDocument HTTP/1.1 – ما در حال درخواست URL / هستیم، c.a.d. ریشه وبسایت [istia.univ-angers.fr].
- خط ۲: دستورات میزبان: ماشین:پورت
- خط ۳: دستورات Connection: [mode de la connexion]. حالت [close] به سرور دستور میدهد که پس از ارسال پاسخ خود، اتصال را ببندد. حالت [Keep-Alive] درخواست میکند که اتصال باز باقی بماند.
- خط ۴: خط خالی. خطوط ۱–۳ به عنوان سربرگهای HTTP شناخته میشوند. ممکن است علاوه بر موارد نمایش داده شده، سربرگهای دیگری نیز وجود داشته باشد. پایان سربرگهای HTTP با یک خط خالی مشخص میشود.
- خطوط ۵–۱۳: سربرگهای HTTP از پاسخ سرور – اینها نیز با یک خط خالی پایان مییابند.
- خطوط 14–19: سند ارسالشده توسط سرور، در این مورد یک سند HTML
- خط ۵: سینتکس کد پیام HTTP/1.1 – کد ۲۰۰ نشان میدهد که سند درخواستشده یافت شده است.
- خط ۶: تاریخ و زمان سرور
- خط ۷: شناسایی نرمافزاری که سرویس وب را ارائه میدهد – در این مورد، یک سرور آپاچی در حال اجرا بر روی لینوکس/دبیان
- خط ۸: سند به صورت پویا توسط PHP تولید شده است
- خط ۹: کوکی شناسایی مشتری – اگر مشتری بخواهد در اتصال بعدی خود شناسایی شود، باید این کوکی را در هدرهای خود مجدداً ارسال کند: HTTP.
- خط ۱۰: نشان میدهد که پس از ارائه سند درخواستشده، سرور اتصال را قطع خواهد کرد
- خط ۱۱: سند به جای یک بلوک واحد، به صورت تکهها (chunks) منتقل خواهد شد.
- خط ۱۲: نوع سند: در این مورد، یک سند HTML
- خط ۱۳: خط خالی که پایان سربرگهای HTTP سرور را نشان میدهد
- خط 14: یک عدد هگزادسیمال که تعداد کاراکترهای بلوک اول سند را نشان میدهد. وقتی این عدد برابر 0 باشد (خط 19)، کلاینت متوجه میشود که کل سند را دریافت کرده است.
- خطوط ۱۵–۱۸: بخشی از سند دریافتشده.
اتصال بسته شده و کلاینت putty غیرفعال است. بیایید دوباره به [1] متصل شویم و صفحه را از نمایشهای قبلی [2,3] پاک کنیم:
![]() |
گفتوگو این بار به شرح زیر است:
- خط ۱: یک سند موجود نیست درخواست شده است
- خط ۵: سرور HTTP با کد ۴۰۴ پاسخ داد، به این معنی که سند درخواستشده یافت نشد.
اگر این سند را با مرورگر فایرفاکس درخواست کنید:

اگر کد منبع [Affichage/Code source] را مشاهده کنیم:
ما خطوط ۱۳–۲۲ را همانطور که توسط کلاینت ما putty دریافت شده است، مشاهده میکنیم. مزیت این کار این است که سربرگهای پاسخ HTTP را نیز به ما نشان میدهد. همچنین میتوان این موارد را با استفاده از فایرفاکس مشاهده کرد.
11.4.3. پروتکل SMTP (پروتکل ساده انتقال ایمیل)
![]() |
سرورهای SMTP معمولاً روی پورت 25 [2] کار میکنند. شما به سرور [1] متصل میشوید. در اینجا، شما معمولاً باید یک سرور را انتخاب کنید
متعلق به همان دامنهٔ ماشین، زیرا سرورهای IP معمولاً طوری پیکربندی شدهاند که فقط درخواستها را از ماشینهایی که به همان دامنهٔ خودشان تعلق دارند، بپذیرند. علاوه بر این، کاملاً رایج است که فایروالها یا نرمافزارهای آنتیویروس روی رایانههای شخصی برای مسدود کردن اتصالات به پورت ۲۵ از ماشینهای خارجی پیکربندی شوند. بنابراین ممکن است لازم باشد این فایروال یا نرمافزار آنتیویروس را مجدداً پیکربندی کنید.
کادر محاورهای SMTP در پنجرهٔ کلاینت putty به شرح زیر است:
در زیر، (D) نمایانگر درخواست کلاینت و (R) نمایانگر پاسخ سرور است.
- خط ۱: (R) پیام خوشآمدگویی از سمت سرور SMTP
- خط ۲: (D) فرمان HELO برای گفتن «سلام»
- خط ۳: (R) پاسخ سرور
- خط ۴: (D) آدرس فرستنده، برای مثال «ایمیل از: someone@gmail.com»
- خط ۵: (R) پاسخ سرور
- خط ۶: (D) آدرس گیرنده، برای مثال rcpt to: someoneelse@gmail.com
- خط ۷: (R) پاسخ سرور
- خط ۸: (D) نشاندهنده شروع پیام است
- خط ۹: (R) پاسخ سرور
- خطوط ۱۰–۱۲: (D) پیامی که باید ارسال شود، که با یک خط حاوی فقط یک نقطه پایان مییابد.
- خط ۱۳: (R) پاسخ سرور
- خط ۱۴: (D) کلاینت سیگنال میدهد که کارش تمام شده است
- خط ۱۵: (R) پاسخ سرور، که پس از آن سرور اتصال را قطع میکند
11.4.4. پروتکل POP (پروتکل اداره پست)
![]() |
سرورهای POP معمولاً روی پورت 110 [2] کار میکنند. یک اتصال به سرور [1] برقرار میشود. دیالوگ POP در پنجرهٔ کلاینت putty به شرح زیر است:
- خط ۱: (R) پیام خوشآمدگویی از سرور POP
- خط ۲: (D) کلاینت شناسهٔ خود را POP، c.a.d ارائه میدهد. ورود مورد استفاده برای خواندن ایمیل آن است
- خط ۳: (R) پاسخ سرور
- خط ۴: (D) رمز عبور کلاینت
- خط ۵: (R) پاسخ سرور
- خط ۶: (D) کلاینت فهرستی از ایمیلهای خود را درخواست میکند
- خطوط ۷–۱۲: (R) فهرست پیامها در صندوق پستی مشتری، در قالب [N° du message taille en octets du message]
- خط ۱۳: (D) پیام شماره ۶۴ درخواست شده است
- خطوط 14–25: (R) پیام شماره 64، که خطوط 15–22 حاوی سرآیند پیام و خطوط 23–24 حاوی بدنه پیام هستند.
- خط ۲۶: (D) کلاینت اعلام میکند که کارش تمام شده است
- خط ۲۷: (R) پاسخ سرور، که پس از آن اتصال را قطع میکند.
11.4.5. پروتکل FTP (پروتکل انتقال فایل)
پروتکل FTP پیچیدهتر از موارد ارائهشده در بالا است. برای مشاهده خطوط متنی که بین کلاینت و سرور رد و بدل میشود، میتوانید از ابزاری مانند FileZilla [http://www.filezilla.fr/] استفاده کنید.
![]() |
FileZilla یک کلاینت FTP است که رابط کاربری ویندوز را برای انتقال فایلها ارائه میدهد. اقدامات کاربر در رابط کاربری ویندوز به دستورات FTP تبدیل میشوند که در [1] ثبت میگردند. این روش خوبی برای کشف دستورات پروتکل FTP است.
11.5. کلاسهای .NET برای برنامهنویسی وب
11.5.1. انتخاب کلاس مناسب
چارچوب .NET کلاسهای مختلفی را برای کار با شبکه ارائه میدهد:
![]() |
- کلاس Socket در سطحی عمل میکند که به شبکه نزدیکتر است. این کلاس امکان مدیریت دقیق اتصال شبکه را فراهم میکند. اصطلاح socket به یک پریز برق اشاره دارد. این اصطلاح برای اشاره به یک پریز شبکه نرمافزاری نیز توسعه یافته است. در ارتباط TCP-IP بین دو ماشین A و B، دو sockets با یکدیگر ارتباط برقرار میکنند. یک برنامه کاربردی میتواند مستقیماً با اشیاء sockets کار کند. این مورد در برنامه کاربردی A فوق صادق است. یک سوکت میتواند یک سوکت client یا یک سوکت serveur باشد.
- اگر بخواهید در سطحی پایینتر از کلاس Socket کار کنید، میتوانید از کلاسهای
- TcpClient برای ایجاد یک کلاینت TCP
- TcpListener برای ایجاد یک سرور TCP
این دو کلاس با مدیریت جزئیات فنی مدیریت سوکت به نمایندگی از برنامه، دید سادهتری از ارتباطات شبکه را در اختیار آن قرار میدهند.
- کلاس NET کلاسهای خاصی را برای پروتکلهای معین فراهم میکند:
- کلاس SmtpClient برای مدیریت پروتکل SMTP جهت ارتباط با سرور SMTP برای ارسال ایمیلها
- کلاس WebClient برای مدیریت پروتکلهای HTTP یا FTP جهت ارتباط با یک سرور وب.
شایان ذکر است که کلاس Socket به تنهایی برای مدیریت تمام ارتباطات TCP/IP کافی است، اما هدف عمدتاً استفاده از کلاسهای سطح بالاتر برای سادهسازی توسعه برنامه کاربردی TCP/IP است.
11.5.2. کلاس TcpClient
کلاس TcpClient در اکثر موارد کلاس مناسبی برای ایجاد کلاینت برای سرویس TCP است. در میان سازندهای C، متدهای M و ویژگیهای P آن، موارد زیر را شامل میشود:
C | یک اتصال TCP با سرویس در حال اجرا روی پورت مشخص (port) از ماشین مشخص (hostname) ایجاد میکند. برای مثال، `new TcpClient("istia.univ-angers.fr",80)` برای اتصال به پورت 80 روی ماشین istia.univ-angers.fr | |
P | ساکتی که توسط کلاینت برای ارتباط با سرور استفاده میشود. | |
M | یک جریان خواندن-نوشتن به سرور را به دست میآورد. این جریان است که ارتباط کلاینت-سرور را ممکن میسازد. | |
M | اتصال را میبندد. ساکت و استریم NetworkStream نیز بسته میشوند | |
P | در صورتی که اتصال برقرار شده باشد، true |
کلاس NetworkStream نمایانگر جریان شبکه بین کلاینت و سرور است. این کلاس از کلاس Stream مشتق شده است. بسیاری از برنامههای مشتری-سرور خطوط متنی را که با کاراکترهای پایان خط «\r\n» خاتمه یافتهاند، مبادله میکنند. بنابراین، استفاده از اشیاء StreamReader و StreamWriter برای خواندن و نوشتن این خطوط در جریان شبکه مفید است. بنابراین، اگر یک ماشین M1 با استفاده از یک شیء TcpClient client1 با یک ماشین M2 ارتباط برقرار کرده و در حال تبادل خطوط متن باشند، میتواند استریمهای خواندن و نوشتن خود را به صورت زیر ایجاد کند:
StreamReader in1=new StreamReader(client1.GetStream());
StreamWriter out1=new StreamWriter(client1.GetStream());
out1.AutoFlush=true;
دستور
به این معنی است که جریان نوشتن از client1 از یک بافر واسطه عبور نخواهد کرد، بلکه مستقیماً وارد شبکه میشود. این یک نکته مهم است. بهطور کلی، وقتی client1 یک خط متن را برای شریک خود ارسال میکند، انتظار پاسخ دارد. این پاسخ هرگز نخواهد رسید اگر آن خط در واقع در ماشین M1 بافر شده و هرگز به ماشین M2 ارسال نشده باشد.
برای ارسال یک خط متن به ماشین M2، باید نوشت:
برای خواندن پاسخ از M2، شما مینوشتید:
اکنون ما عناصر لازم برای طراحی معماری پایه یک کلاینت وب با پروتکل ارتباطی پایهای زیر با سرور را داریم:
- کلاینت درخواستی را ارسال میکند که در یک خط واحد جای دارد
- سرور پاسخی را ارسال میکند که در یک خط واحد جای دارد
using System;
using System.IO;
using System.Net.Sockets;
namespace ... {
class ... {
static void Main(string[] args) {
...
try {
// اتصال به سرویس
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//حلقه درخواست-پاسخ
while (true) {
// درخواست از صفحهکلید میآید
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// تمام شد؟
if (demande.Trim().ToLower() == "bye")
break;
//ارسال درخواست به سرور
writer.WriteLine(demande);
// خواندن پاسخ سرور
réponse = reader.ReadLine();
// پردازش پاسخ
...
}
}
}
}
}
} catch (Exception e) {
// خطا
...
}
}
}
}
- خط ۱۱: برقراری اتصال کلاینت – بند using تضمین میکند که منابع مرتبط با این اتصال پس از خروج از using آزاد شوند.
- خط ۱۲: باز کردن جریان شبکه در یک بند using
- خط ۱۳: ایجاد و عملیات جریان خواندن در یک بند using
- خط ۱۴: ایجاد و اجرای جریان نوشتن در یک بند using
- خط ۱۶: از بافرگذاری جریان خروجی خودداری کنید
- خطوط ۱۸–۳۱: چرخه درخواست مشتری/پاسخ سرور
- خط ۲۶: کلاینت درخواست خود را به سرور ارسال میکند
- خط ۲۸: کلاینت منتظر پاسخ سرور میماند. این یک عملیات مسدودکننده (blocking) است، مشابه خواندن از صفحهکلید. این انتظار با دریافت یک رشته که با «\n» پایان یافته باشد یا زمانی که جریان (stream) به پایان برسد، خاتمه مییابد. مورد دوم زمانی رخ میدهد که سرور ارتباطی را که با کلاینت برقرار کرده است، ببندد.
11.5.3. کلاس TcpListener
کلاس TcpListener در اکثر موارد کلاس مناسبی برای ایجاد یک سرویس TCP است. در میان سازندهای C، متدهای M و ویژگیهای P آن، موارد زیر گنجانده شده است:
C | یک سرویس TCP ایجاد میکند که به درخواستهای مشتری روی پورتی که بهعنوان پارامتر (port) ارسال میشود، گوش میدهد؛ این پورت که پورت گوشدادن (listening port) نامیده میشود. اگر ماشین به چندین شبکه IP متصل باشد، سرویس روی هر یک از آن شبکهها گوش میدهد. | |
C | همانند مورد بالا، اما سرویس فقط روی آدرس IP مشخصشده گوش میدهد. | |
M | شروع گوش دادن به درخواستهای کلاینت | |
Mراهاندازی فرآیند گوش دادن به درخواستهای مشتری را آغاز میکندQZXW2HTMLCODE00 | درخواست مشتری را میپذیرد. سپس یک اتصال جدید با مشتری برقرار میکند که به آن اتصال سرویس گفته میشود. پورت مورد استفاده در سمت سرور تصادفی است و توسط سیستم انتخاب میشود. این پورت به پورت سرویس معروف است. AcceptTcpClient شیء TcpClient را بازمیگرداند که در سمت سرور با اتصال سرویس مرتبط است. | |
M | متوقف کردن گوش دادن به درخواستهای کلاینت | |
P | ساکت گوشدادن سرور |
ساختار پایه یک سرور TCP که با استفاده از پروتکل زیر با کلاینتهای خود ارتباط برقرار میکند:
- کلاینت درخواستی را ارسال میکند که در یک خط واحد جای دارد
- سرور پاسخی را ارسال میکند که در یک خط واحد جای دارد
ممکن است به این شکل باشد:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
namespace ... {
public class ... {
...
//سرویس گوش دادن ایجاد شد
TcpListener ecoute = null;
try {
//سرویس ایجاد میشود – روی تمام رابطهای شبکهٔ ماشین گوش میدهد
ecoute = new TcpListener(IPAddress.Any, port);
// راهاندازی آن
ecoute.Start();
//حلقه سرویس
TcpClient tcpClient = null;
//حلقه بینهایت – با Ctrl-C خاتمه مییابد
while (true) {
// در انتظار یک مشتری
tcpClient = ecoute.AcceptTcpClient();
// سرویس توسط وظیفه دیگری در حال انجام است
ThreadPool.QueueUserWorkItem(Service, tcpClient);
// مشتری بعدی
}
} catch (Exception ex) {
// گزارش خطا
...
} finally {
//پایان سرویس
ecoute.Stop();
}
}
// -------------------------------------------------------
//ارائه خدمات به مشتری
public static void Service(Object infos) {
//بازیابی مشتری برای ارائه سرویس
Client client = infos as Client;
//پردازش اتصال TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//حلقه درخواست خواندن/پاسخ نوشتن
bool fini=false;
while (! fini) != null) {
// انتظار برای درخواست مشتری – عملیات مسدودکننده
demande=reader.ReadLine();
//آمادهسازی پاسخ
réponse=...;
//ارسال پاسخ به مشتری
writer.WriteLine(réponse);
//درخواست بعدی
}
}
}
}
}
} catch (Exception e) {
// خطا
...
} finally {
//پایان جلسه مشتری
...
}
}
}
}
- خط ۱۴: سرویس گوشدادن برای یک پورت مشخص و یک آدرس IP مشخص ایجاد میشود. در اینجا مهم است به یاد داشته باشیم که یک ماشین حداقل دو آدرس دارد: آدرس «127.0.0.1» که آدرس لوپبک آن است، و آدرس «I1.I2.I3.I4» که در شبکهای که به آن متصل است دارد. اگر به چندین شبکه IP متصل باشد، ممکن است آدرسهای دیگری IP نیز داشته باشد. IPAddress.Any به تمام آدرسهای IP یک ماشین اشاره دارد.
- خط ۱۶: سرویس گوش دادن شروع میشود. این سرویس قبلاً ایجاد شده بود اما هنوز در حال گوش دادن نبود. گوش دادن به معنای انتظار برای درخواستها از سوی کلاینتها است.
- خطوط ۲۰–۲۶: حلقهٔ انتظار برای درخواستهای مشتری و پاسخگویی به آنها برای هر مشتری جدید تکرار میشود.
- خط ۲۲: درخواست مشتری پذیرفته میشود. متد AcceptTcpClient یک نمونه از TcpClient را بازمیگرداند که به آن نمونه سرویس گفته میشود:
- مشتری درخواست خود را با استفاده از نمونه سمت-کلاینت خود TcpClient انجام داده است، که ما آن را TcpClientDemande مینامیم
- سرور این درخواست را با استفاده از AcceptTcpClient میپذیرد. این متد یک نمونه سمت سرور TcpClient ایجاد میکند، که ما آن را TcpClientService مینامیم. سپس ما یک اتصال TCP باز با نمونههای TcpClientDemande <--> TcpClientService در هر دو انتها داریم.
- ارتباط کلاینت/سرور که در ادامه رخ میدهد، از طریق همین اتصال برقرار میشود. سرویس گوشدادن دیگر درگیر نیست.
- خط 24: برای اینکه سرور بتواند بهطور همزمان چندین مشتری را مدیریت کند، سرویس توسط رشتهها (threadها) اجرا میشود، بهطوریکه برای هر مشتری یک رشته وجود دارد.
- خط ۳۲: سرویس گوشدادن بسته میشود
- خط ۳۸: متدی که توسط نخ سرویس برای یک مشتری اجرا میشود. این متد یک نمونه از TcpClient را به عنوان پارامتر میپذیرد، نمونهای که از قبل به مشتری مورد نظر متصل است.
- خطوط ۳۸–۷۱: کد اینجا مشابه کد کلاینت پایهای TCP است که قبلاً مورد بحث قرار گرفت.
11.6. نمونههایی از کلاینتها و سرورهای TCP
11.6.1. یک سرور اکو
ما پیشنهاد میکنیم یک سرور اکو بنویسیم که از طریق پنجره DOS با استفاده از دستور زیر راهاندازی شود:
پورت ServeurEcho
سرور بر روی پورتی که بهعنوان پارامتر ارسال میشود، کار میکند. این برنامه بهسادگی درخواستی را که کلاینت برای آن ارسال کرده است، بازمیگرداند. برنامه به شرح زیر است:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Net;
// تماس: serveurEcho پورت
// سرور اکو
// خطی را که مشتری ارسال کرده است، برای مشتری بازمیفرستد
namespace Chap9 {
public class ServeurEcho {
public const string syntaxe = "Syntaxe : [serveurEcho] port";
// برنامهٔ اصلی
public static void Main(string[] args) {
//آیا آرگومانی وجود دارد؟
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
//این آرگومان باید یک عدد صحیح بزرگتر از 0 باشد
int port = 0;
if (!int.TryParse(args[0], out port) || port<=0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
return;
}
// سرویس شنود ایجاد میشود
TcpListener ecoute = null;
int numClient = 0; // شمارهٔ بعدی مشتری
try {
// سرویس ایجاد شد – روی تمام رابطهای شبکهٔ ماشین گوش خواهد داد
ecoute = new TcpListener(IPAddress.Any, port);
// آن را راهاندازی کنید
ecoute.Start();
// نظارت
Console.WriteLine("Serveur d'écho lancé sur le port {0}", ecoute.LocalEndpoint);
// رشتههای سرویس
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
//حلقه سرویس
TcpClient tcpClient = null;
//حلقه بینهایت – با کلید Ctrl-C خاتمه مییابد
while (true) {
// منتظر یک مشتری
tcpClient = ecoute.AcceptTcpClient();
// سرویس توسط وظیفه دیگری در حال انجام است
ThreadPool.QueueUserWorkItem(Service, new Client() { CanalTcp = tcpClient, NumClient = numClient });
// مشتری بعدی
numClient++;
}
} catch (Exception ex) {
// گزارش خطا
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
} finally {
//پایان سرویس
ecoute.Stop();
}
}
// -------------------------------------------------------
// خدمات را به یک مشتری روی سرور اکو ارائه میدهد
public static void Service(Object infos) {
// مشتریای را که باید سرویسدهی شود بازیابی میکند
Client client = infos as Client;
// سرویس را به مشتری ارائه میدهد
Console.WriteLine("Début de service au client {0}", client.NumClient);
// عملیات لینک TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//حلقه درخواست/پاسخ خواندن/نوشتن
string demande = null;
while ((demande = reader.ReadLine()) != null) {
// نظارت کنسول
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
// بازتاب درخواست به کلاینت
writer.WriteLine("[{0}]", demande);
// ردیابی کنسول
Console.WriteLine("---> Client {0} : {1}", client.NumClient, demande);
// سرویس زمانی متوقف میشود که کلاینت «bye» را ارسال میکند
if (demande.Trim().ToLower() == "bye")
break;
}
}
}
}
}
} catch (Exception e) {
// خطا
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
// پایان سمت مشتری
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
}
// اطلاعات مشتری
internal class Client {
public TcpClient CanalTcp { get; set; } // اتصال با مشتری
public int NumClient { get; set; } // شماره مشتری
}
}
ساختار سرور اکو با معماری پایه سرورهای TCP که قبلاً تشریح شد، مطابقت دارد. ما فقط در مورد بخش «خدمات کلاینت» توضیح خواهیم داد:
- خط ۷۹: درخواست مشتری خوانده میشود
- خط ۸۳: به مشتری بازگردانده میشود و در کروشهها قرار میگیرد
- خط ۷۹: سرویس زمانی متوقف میشود که مشتری اتصال را قطع کند
در یک پنجره DOS، از فایل اجرایی پروژه C# استفاده میکنیم:
سپس دو کلاینت putty را راهاندازی میکنیم که به پورت ۱۰۰ روی ماشین localhost متصل میشوند:
![]() |
خروجی کنسول سرور اکو به این صورت درمیآید:
کلاینت ۱، و پس از آن کلاینت ۰، متنهای زیر را ارسال میکنند:
![]() |
- [1]: کلاینت شمارهٔ ۱
- [2]: مشتری شمارهٔ 0
- [3]: کنسول سرور اکو
![]() |
- در [4]: کلاینت ۱ با استفاده از فرمان bye قطع اتصال میکند.
- در [5]: سرور این را تشخیص میدهد
سرور را میتوان با کلید Ctrl-C متوقف کرد. سپس کلاینت شمارهٔ ۰ این را تشخیص میدهد: [6].
11.6.2. یک کلاینت برای سرور اکو
اکنون برای سرور قبلی یک کلاینت مینویسیم. این کلاینت به صورت زیر فراخوانی خواهد شد:
ClientEcho nomServeur پورت
این به ماشین nomServeur روی پورت port متصل میشود و سپس خطوط متنی را به سرور ارسال میکند که آنها را بازتاب میدهد.
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
// به یک سرور اکو متصل میشود
//هر خطی که روی صفحهکلید تایپ میشود، بازتاب داده میشود
class ClientEcho {
static void Main(string[] args) {
// دستور زبان
const string syntaxe = "pg machine port";
// تعداد آرگومانها
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// توجه به نام سرور
string serveur = args[0];
// پورت باید یک عدد صحیح مثبت باشد
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// امکان کار وجود دارد
string demande = null; // درخواست کلاینت
string réponse = null; // پاسخ سرور
try {
// اتصال به سرویس
using (TcpClient tcpClient = new TcpClient(serveur, port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//حلقه درخواست-پاسخ
while (true) {
// درخواست از صفحهکلید میآید
Console.Write("Demande (bye pour arrêter) : ");
demande = Console.ReadLine();
// تمام شد؟
if (demande.Trim().ToLower() == "bye")
break;
//ارسال درخواست به سرور
writer.WriteLine(demande);
// خواندن پاسخ سرور
réponse = reader.ReadLine();
//در حال پردازش پاسخ
Console.WriteLine("Réponse : {0}", réponse);
}
}
}
}
}
} catch (Exception e) {
//خطا
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
ساختار این کلاینت مطابق با معماری کلی پایهای است که برای کلاینتهای cp پیشنهاد شده است. در اینجا نتایج بهدستآمده با پیکربندی زیر آورده شده است:
- سرور در پورت ۱۰۰ در یک پنجره DOS در حال اجرا است
- روی همان ماشین، دو کلاینت در دو پنجرهٔ دیگر DOS در حال اجرا هستند
در پنجرهٔ کلاینت A (شمارهٔ 0)، موارد زیر نمایش داده میشود:
در پنجره مربوط به کلاینت B (شماره ۱):
در پنجرهٔ سرور:
کلاینت A (شماره 0) قطع ارتباط میکند:
کنسول سرور:
11.6.3. یک کلاینت عمومی TCP
ما قصد داریم یک کلاینت TCP عمومی بنویسیم که به شکل زیر راهاندازی خواهد شد: ClientTcpGenerique پورت سرور. این کلاینت به روشی مشابه کلاینت PuTTY عمل خواهد کرد اما دارای رابط کنسول خواهد بود و هیچ گزینه پیکربندیای ارائه نخواهد داد.
در برنامه قبلی، پروتکل گفتگو ثابت بود: کلاینت یک خط ارسال میکرد و سرور با یک خط پاسخ میداد. هر سرویس پروتکل خاص خود را دارد و ممکن است موقعیتهای زیر نیز پیش بیاید:
- کلاینت باید قبل از دریافت پاسخ، چندین خط متن ارسال کند
- پاسخ یک سرور ممکن است شامل چندین خط متن باشد
بنابراین، چرخه ارسال یک خط به سرور و دریافت یک خط از سرور همیشه مناسب نیست. برای مدیریت پروتکلهای پیچیدهتر از پروتکل اکو، کلاینت عمومی TCP دو تار خواهد داشت:
- رشته اصلی خطوط متنی تایپشده روی صفحهکلید را میخواند و آنها را به سرور ارسال میکند.
- یک نخ ثانویه به صورت موازی اجرا میشود و به خواندن خطوط متنی که توسط سرور ارسال شدهاند اختصاص داده شده است. به محض دریافت هر خط، آن را روی کنسول نمایش میدهد. این نخ تنها زمانی متوقف میشود که سرور اتصال را قطع کند. بنابراین به طور مداوم اجرا میشود.
کد به شرح زیر است:
using System;
using System.IO;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
// ویژگیهای یک سرویس را بهعنوان پارامتر به شکل: پورت سرور دریافت میکند
// به سرویس متصل میشود
//هر خطی را که روی صفحهکلید تایپ میشود به سرور ارسال میکند
//یک نخ ایجاد میکند تا خطوط متنی ارسالشده توسط سرور را بهطور مداوم بخواند
class ClientTcpGenerique {
static void Main(string[] args) {
// دستور زبان
const string syntaxe = "pg serveur port";
// تعداد آرگومانها
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// توجه به نام سرور
string serveur = args[0];
// پورت باید یک عدد صحیح بزرگتر از 0 باشد
int port = 0;
if (!int.TryParse(args[1], out port) || port <= 0) {
Console.WriteLine("{0}{1}port incorrect", syntaxe, Environment.NewLine);
return;
}
// اتصال به سرویس
TcpClient tcpClient = null;
try {
tcpClient = new TcpClient(serveur, port);
} catch (Exception ex) {
// خطا
Console.WriteLine("Impossible de se connecter au service ({0},{1}) : erreur {2}", serveur, port, ex.Message);
// پایان
return;
}
// یک نخ جداگانه راهاندازی کنید تا خطوط متنی ارسالشده توسط سرور را بخواند
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
// دستورهای صفحهکلید در نخ اصلی خوانده میشوند
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string demande = null; // درخواست کلاینت
try {
//اتصال مشتری پردازش میشود
using (tcpClient) {
//یک جریان نوشتن به سرور ایجاد میشود
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//حلقه درخواست-پاسخ
while (true) {
demande = Console.ReadLine();
// انجام شد؟
if (demande.Trim().ToLower() == "bye")
break;
// ارسال درخواست به سرور
writer.WriteLine(demande);
}
}
}
}
} catch (Exception e) {
// خطا
Console.WriteLine("L'erreur suivante s'est produite dans le thread principal : {0}", e.Message);
}
}
// رشتهٔ خواندن مشتری <-- سرور
public static void Receive(object infos) {
// دادههای محلی
string réponse = null; // پاسخ سرور
// جریان ورودی ایجاد شد
try {
using (TcpClient tcpClient = infos as TcpClient) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
//حلقهٔ پیوسته برای خواندن خطوط متنی از جریان ورودی
while ((réponse = reader.ReadLine()) != null) {
//خروجی کنسول
Console.WriteLine("<-- {0}", réponse);
}
}
}
}
} catch (Exception ex) {
// خطا
Console.WriteLine("Flux de lecture : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
//پایان نخ خواندن اعلام شد
Console.WriteLine("Fin du thread de lecture des réponses du serveur. Si besoin est, arrêtez le thread de lecture console avec la commande bye.");
}
}
}
}
- خط ۳۴: کلاینت به سرور متصل میشود
- خط ۴۳: یک تِرد برای خواندن خطوط متن از سرور راهاندازی میشود. این تِرد باید متد Receive را در خط ۷۳ فراخوانی کند. نمونه TcpClient که به سرور متصل شده است، به این متد پاس داده میشود.
- خطوط ۵۷–۶۴: حلقه برای ورودی صفحهکلید و ارسال دستورات به سرور. ورودی صفحهکلید توسط نخ اصلی مدیریت میشود.
- خطوط ۷۵–۹۸: متد Receive، که توسط ترید مسئول خواندن خطوط متن اجرا میشود. این متد، نمونه TcpClient را که به سرور متصل شده است، به عنوان پارامتر دریافت میکند.
- خطوط ۸۴–۸۷: حلقهٔ پیوسته برای خواندن خطوط متنی ارسالشده توسط سرور. این حلقه تنها زمانی متوقف میشود که سرور اتصال باز با کلاینت را ببندد.
در اینجا چند مثال بر اساس نمونههای استفادهشده با کلاینت putty در بخش 11.4 آورده شده است. کلاینت در یک کنسول DOS اجرا میشود.
پروتکل HTTP
از خواننده دعوت میشود تا به توضیحات ارائهشده در بخش 11.4.2 مراجعه کند. ما تنها به جنبههای خاص این کاربرد اشاره خواهیم کرد:
- خط ۲۸: پس از ارسال خط ۲۷، سرور HTTP اتصال را قطع کرد که باعث پایان یافتن نخ خواندن شد. نخ اصلی که دستورات تایپشده روی صفحهکلید را میخواند، همچنان فعال باقی میماند. دستور در خط ۲۹ که روی صفحهکلید تایپ شده است، آن را خاتمه میدهد.
پروتکل SMTP
از خوانندگان دعوت میشود تا توضیحات ارائهشده در بخش 11.4.3 را مجدداً مرور کرده و سایر مثالهای استفادهشده با کلاینت putty را آزمایش کنند.
11.6.4. یک سرور عمومی TCP
اکنون به یک سرور نگاه میکنیم
- که دستورات ارسالشده توسط کلاینتهایش را روی صفحه نمایش میدهد
- و در پاسخ، خطوط متنی را که توسط کاربر از روی صفحهکلید تایپ شدهاند، ارسال میکند. بنابراین این کاربر است که نقش سرور را ایفا میکند.
برنامه در یک پنجره DOS با استفاده از دستور زیر اجرا میشود: ServeurTcpGenerique portEcoute، که در آن portEcoute پورتی است که کلاینتها باید به آن متصل شوند. سرویس کلاینت توسط دو نخ (thread) مدیریت میشود:
- رشتهٔ اصلی، که:
- بهصورت پشت سر هم به جای موازی به پردازش کلاینتها میپردازد؛
- که خطوط تایپشده توسط کاربر روی صفحهکلید را میخواند و برای کلاینت ارسال میکند. کاربر با استفاده از دستور «bye» اعلام میکند که اتصال با کلاینت را قطع میکند. دلیل اینکه سرور ما تنها یک کلاینت را در هر زمان مدیریت میکند این است که کنسول نمیتواند همزمان برای دو کلاینت استفاده شود.
- یک نخ ثانویه که منحصراً به خواندن خطوط متنی که توسط کلاینت ارسال میشود اختصاص دارد
خودِ سرور هرگز متوقف نمیشود، مگر اینکه کاربر کلیدهای Ctrl-C را روی صفحهکلید فشار دهد.
بیایید چند مثال را بررسی کنیم. سرور روی پورت ۱۰۰ در حال اجرا است و ما از کلاینت عمومی paragraphe11.6.3 برای ارتباط با آن استفاده میکنیم. پنجره کلاینت به شرح زیر است:
خطوطی که با <-- شروع میشوند، ارسالشده از سرور به کلاینت هستند؛ سایر خطوط از کلاینت به سرور ارسال شدهاند. پنجرهٔ سرور به این شکل است:
خطهایی که با <-- شروع میشوند، ارسالشده از سمت کلاینت به سرور هستند؛ و سایر خطها ارسالشده از سمت سرور به کلاینت میباشند. خط ۹ نشان میدهد که نخ (thread) خواندن درخواستهای کلاینت متوقف شده است. رشته اصلی سرور همچنان منتظر دستورات وارد شده از طریق صفحهکلید برای ارسال به کلاینت است. بنابراین باید دستور bye را از خط ۱۰ تایپ کنید تا به کلاینت بعدی بروید. سرور حتی با اینکه کلاینت ۱ کارش را تمام کرده، همچنان فعال است. ما یک کلاینت دوم برای همان سرور راهاندازی میکنیم:
پنجره سرور اکنون به این شکل است:
پس از خط ۶ بالا، سرور اکنون منتظر یک کلاینت جدید است. میتوان آن را با فشردن Ctrl-C متوقف کرد.
اکنون بیایید با راهاندازی سرور عمومی خود روی پورت 88، یک سرور وب را شبیهسازی کنیم:
حالا یک مرورگر را باز کرده و صفحهٔ http://localhost:88/exemple.html را درخواست میکنیم. مرورگر سپس به پورت 88 روی ماشین localhost متصل شده و صفحه /exemple.html را درخواست میکند:
![]() |
حال بیایید به پنجرهٔ سرور خود نگاه کنیم:
ما میتوانیم سربرگهای HTTP ارسالشده توسط مرورگر را ببینیم. این به ما امکان میدهد تا سایر سربرگهای HTTP را علاوه بر آنهایی که قبلاً با آنها مواجه شدهایم، شناسایی کنیم. بیایید یک پاسخ برای کلاینت خود بسازیم. کاربر پشت کیبورد در این مورد، خود سرور است و میتواند پاسخ را بهصورت دستی بسازد. بیایید پاسخ تولیدشده توسط یک سرور وب را در مثال قبلی به یاد بیاوریم:
بیایید سعی کنیم یک پاسخ مشابه ارائه دهیم، در حالی که آن را به حداقل ممکن محدود میکنیم:
ما پاسخ خود را در خطوط ۱ تا ۴ به سربرگهای HTTP محدود کردهایم. ما اندازه سندی را که قصد ارسال آن را داریم (Content-Length) مشخص نمیکنیم، بلکه صرفاً اعلام میکنیم که پس از ارسال، اتصال را بست خواهیم کرد (Connection: close). این برای مرورگر کافی است. وقتی ببیند اتصال بسته شده است، میداند که پاسخ سرور کامل است و صفحهای را که برایش ارسال شده (HTML) نمایش میدهد. این همان صفحهای است که شامل خطوط ۶ تا ۹ است. سپس کاربر با تایپ کردن دستور bye در خط ۱۰، اتصال را با کلاینت قطع میکند. با دریافت این دستور صفحهکلید، نخ اصلی اتصال با کلاینت را میبندد. این امر استثناء را در خط ۱۱ فعال میکند. نخی که مسئول خواندن خطوط متنی از کلاینت بود، بهطور ناگهانی با بسته شدن اتصال با کلاینت متوقف شد و یک استثناء را پرتاب کرد. پس از خط ۱۲، سرور منتظر یک مشتری جدید میماند.
مرورگر کلاینت اکنون موارد زیر را نمایش میدهد:
![]() |
اگر، همانطور که در بالا نشان داده شده است، Affichage/Source را ارسال کنیم تا ببینیم مرورگر چه چیزی دریافت کرده است، [2] را دریافت میکنیم، که دقیقاً همان چیزی است که از سرور عمومی ارسال شده بود.
کد سرور عمومی TCP به شرح زیر است:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace Chap9 {
public class ServeurTcpGenerique {
public const string syntaxe = "Syntaxe : ServeurGénérique Port";
// برنامهٔ اصلی
public static void Main(string[] args) {
//آیا آرگومانتی وجود دارد؟
if (args.Length != 1) {
Console.WriteLine(syntaxe);
Environment.Exit(1);
}
//این آرگومان باید یک عدد صحیح بزرگتر از 0 باشد
int port = 0;
if (!int.TryParse(args[0], out port) || port <= 0) {
Console.WriteLine("{0} : {1}Port incorrect", syntaxe, Environment.NewLine);
Environment.Exit(2);
}
// ایجاد سرویس گوش دادن
TcpListener ecoute = null;
try {
// سرویس ایجاد میشود
ecoute = new TcpListener(IPAddress.Any, port);
// ما آن را راهاندازی میکنیم
ecoute.Start();
// نظارت
Console.WriteLine("Serveur générique lancé sur le port {0}", ecoute.LocalEndpoint);
while (true) {
// منتظر یک مشتری
Console.WriteLine("Attente du client suivant...");
TcpClient tcpClient = ecoute.AcceptTcpClient();
Console.WriteLine("Client {0}", tcpClient.Client.RemoteEndPoint);
//یک نخ جداگانه برای خواندن خطوط متنی ارسالشده توسط مشتری راهاندازی میشود
ThreadPool.QueueUserWorkItem(Receive, tcpClient);
//دستورات صفحهکلید در نخ اصلی خوانده میشوند
Console.WriteLine("Tapez vos commandes (bye pour arrêter) : ");
string réponse = null; // پاسخ سرور
// پردازش اتصال مشتری
using (tcpClient) {
//یک جریان نوشتن به سمت مشتری ایجاد میشود
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//حلقه برای دریافت پاسخهای صفحهکلید
while (true) {
réponse = Console.ReadLine();
// تمام شد؟
if (réponse.Trim().ToLower() == "bye")
break;
// ارسال درخواست به کلاینت
writer.WriteLine(réponse);
}
}
}
}
}
} catch (Exception ex) {
// گزارش خطا
Console.WriteLine("Main : l'erreur suivante s'est produite : {0}", ex.Message);
} finally {
// پایان شنود
ecoute.Stop();
}
}
// سرور <-- نخ خواندن مشتری
public static void Receive(object infos) {
// دادههای محلی
string demande = null; // درخواست مشتری
string idClient=null; // هویت مشتری
//پردازش اتصال مشتری
try {
using (TcpClient tcpClient = infos as TcpClient) {
// هویت مشتری
idClient = tcpClient.Client.RemoteEndPoint.ToString();
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
// خواندن مداوم خطوط متنی از جریان ورودی
while ((demande = reader.ReadLine()) != null) {
//خروجی کنسول
Console.WriteLine("<-- {0}", demande);
}
}
}
}
} catch (Exception ex) {
// خطا
Console.WriteLine("Flux de lecture des lignes de texte du client {1} : l'erreur suivante s'est produite : {0}", ex.Message,idClient);
} finally {
// پایان نخ خواندن اعلام شد
Console.WriteLine("Fin du thread de lecture des lignes de texte du client {0}. Si besoin est, arrêtez le thread de lecture console du serveur pour ce client, avec la commande bye.", idClient);
}
}
}
}
- خط ۲۹: سرویس گوش دادن ایجاد شده اما شروع نشده است. این سرویس روی تمام رابطهای شبکهٔ ماشین گوش میدهد.
- خط ۳۱: سرویس گوشدادن راهاندازی میشود
- خط ۳۴: حلقه بینهایت در انتظار مشتریان. کاربر با فشردن Ctrl-C سرور را متوقف خواهد کرد.
- خط ۳۷: منتظر یک مشتری – یک عملیات مسدودکننده. وقتی مشتری میرسد، نمونه TcpClient که توسط متد AcceptTcpClient بازگردانده میشود، نمایانگر سمت سرور یک اتصال باز با مشتری است.
- خط ۴۰: جریان خواندن برای درخواستهای مشتری توسط یک نخ جداگانه مدیریت میشود.
- خط ۴۵: اتصال به کلاینت در داخل یک بند using استفاده میشود تا تضمین شود که صرفنظر از هر اتفاقی، بسته خواهد شد.
- خط ۴۷: جریان شبکه در بند using استفاده میشود
- خط ۴۸: ایجاد یک جریان نوشتن بر روی جریان شبکه در یک بند using
- خط ۵۰: جریان نوشتن بدون بافر خواهد بود
- خطوط ۵۲–۵۹: حلقه برای وارد کردن دستورات از طریق صفحهکلید که باید به کلاینت ارسال شود
- خط ۶۹: پایان سرویس گوش دادن. این دستور هرگز در اینجا اجرا نخواهد شد زیرا سرور با کلید Ctrl-C خاتمه مییابد.
- خط ۷۸: متد Receive که خطوط متنی ارسالشده توسط کلاینت را بهطور مداوم روی کنسول نمایش میدهد. این مشابه چیزی است که برای کلاینت عمومی TCP مشاهده شد.
11.6.5. یک کلاینت وب « »
در مثال قبلی، برخی از سربرگهای HTTP ارسالشده توسط یک مرورگر را مشاهده کردیم:
ما قصد داریم یک کلاینت وب بنویسیم که URL بهعنوان پارامتر به آن ارسال شود و متنی را که سرور ارسال کرده است روی صفحه نمایش دهد. فرض میکنیم سرور از پروتکل HTTP نسخه ۱.۱ پشتیبانی میکند. از میان سربرگهای قبلی، تنها موارد زیر را استفاده خواهیم کرد:
- سربرگ اول سند مورد نظر را مشخص میکند
- سربرگ دوم، سرور مورد پرسوجو را مشخص میکند
- سومین هدر نشان میدهد که میخواهیم سرور پس از پاسخ دادن به ما، اتصال را ببندد.
اگر در خط ۱ بالا، GET را با HEAD جایگزین کنیم، سرور فقط سربرگهای HTTP را برای ما ارسال میکند و نه سند مشخصشده در خط ۱.
کلاینت وب ما به شکل زیر فراخوانی خواهد شد: ClientWeb URL cmd، که در آن URL استURL مورد نظر است و «cmd» یکی از دو کلیدواژه GET یا HEAD است تا مشخص کند که آیا ما فقط سربرگها (HEAD) را میخواهیم یا همچنین محتوای صفحه (GET). بیایید به اولین مثال نگاه کنیم:
- خط ۱: ما فقط سربرگها را درخواست میکنیم HTTP (HEAD)
- خطوط ۲–۹: پاسخ سرور
اگر در فراخوانی کلاینت وب به جای HEAD از GET استفاده کنیم، نتیجه مشابه HEAD به علاوه محتوای سند درخواستشده را دریافت میکنیم.
کد کلاینت وب به شرح زیر است:
using System;
using System.IO;
using System.Net.Sockets;
namespace Chap9 {
class ClientWeb {
static void Main(string[] args) {
// دستور زبان
const string syntaxe = "pg URI GET/HEAD";
// تعداد آرگومانها
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// توجه به URI درخواستی
string stringURI = args[0];
string commande = args[1].ToUpper();
// اعتبارسنجی URI
if(! stringURI.StartsWith("http://")){
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI نادرست است
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// تأیید سفارش
if (commande != "GET" && commande != "HEAD") {
// سفارش نادرست
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
// اتصال به سرویس
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
// جریان خروجی بدون بافر
writer.AutoFlush = true;
// درخواست URL – ارسال سربرگها HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// پاسخ را بخوانید
string réponse = null;
while ((réponse = reader.ReadLine()) != null) {
// نمایش پاسخ در کنسول
Console.WriteLine(réponse);
}
}
}
}
}
} catch (Exception e) {
// استثناء نمایش داده میشود
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
تنها ویژگی جدید در این برنامه، استفاده از کلاس Uri است. برنامه یک URL (مکانیاب یکنواخت منابع) یا URI (شناسگر یکنواخت منابع) را به شکل http://serveur:port/cheminPageHTML?param1=val1;param2=val2;.... دریافت میکند. کلاس Uri به ما امکان میدهد رشته URL را به اجزای مختلف آن تجزیه کنیم.
- خطوط ۲۶–۳۳: یک شیء Uri از رشته stringURI که بهعنوان پارامتر دریافت شده، ساخته میشود. اگر رشته URI که بهعنوان پارامتر دریافت شده، یک URI معتبر نباشد (فقدان پروتکل، سرور و غیره)، یک استثنا پرتاب میشود. این امکان را به ما میدهد تا اعتبار پارامتر دریافتی را بررسی کنیم. پس از ساختن شیء Uri، ما به عناصر مختلف این URI دسترسی داریم. بنابراین، اگر شیء uri از کد قبلی از رشته http://serveur:port/document?param1=val1¶m2=val2;... ساخته شده باشد، خواهیم داشت:
- uri.Host=serveur,
- uri.Port=port,
- uri.Path = document,
- uri.Query=param1=val1¶m2=val2;...,
- uri.pathAndQuery= cheminPageHTML?param1=val1¶m2=val2;...,
- uri.Scheme=http.
11.6.6. یک کلاینت وب که هدایتها را مدیریت میکند
کلاینت وب قبلی هیچگونه تغییر مسیر احتمالی از URL که درخواست کرده است را مدیریت نمیکند. در اینجا یک مثال آمده است:
- خط ۲: کد وضعیت ۳۰۲ Found نشاندهندهٔ یک هدایت (redirection) است. آدرسی که مرورگر باید به آن هدایت شود در متن سند، در خط ۱۶ قرار دارد.
یک مثال دوم:
- خط ۲: کد وضعیت ۳۰۱ «جابجایی دائمی» نشاندهنده یک هدایت است. آدرسی که مرورگر باید به آن هدایت شود در خط ۶، در هدر Location با مقدار HTTP مشخص شده است.
یک مثال سوم:
- خط ۲: کد وضعیت ۳۰۲ «جابجایی موقت» (Moved Temporarily) نشاندهنده یک هدایت (redirection) است. آدرسی که مرورگر باید به آن هدایت شود در خط ۵، در هدر Location با کد HTTP مشخص شده است.
یک مثال چهارم با یک سرور IIS محلی روی ماشین:
- خط ۲: کد ۳۰۲ «مورد جابجا شده» (Object moved) نشاندهنده یک هدایت (redirection) است. آدرسی که مرورگر باید به آن هدایت شود در خط ۵، در هدر Location با کد HTTP مشخص شده است. توجه داشته باشید که برخلاف مثالهای قبلی، آدرس هدایت، نسبی است. آدرس کامل در واقع http://localhost/localstart.asp است.
ما پیشنهاد میکنیم که هدایتها را زمانی مدیریت کنیم که خط اول هدرهای HTTP حاوی کلمه کلیدی moved (بدون حساسیت به حروف بزرگ و کوچک) باشد و آدرس هدایت در هدر Location با شناسه HTTP قرار داشته باشد.
اگر سه مثال آخر را در نظر بگیریم، نتایج زیر را خواهیم داشت:
URL: http://www.bull.com
- خط ۱۱: هدایت به آدرسی که در خط ۶ آمده انجام میشود
URL: http://www.gouv.fr
- خط ۱۱: هدایت به آدرس خط ۶ انجام میشود
URL: http://localhost
- خط ۱۳: صفحه به آدرس موجود در خط ۶ هدایت میشود
- خط ۱۵: دسترسی به صفحه http://localhost/localstart.asp رد شد.
برنامهای که هدایت را مدیریت میکند به شرح زیر است:
using System;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace Chap9 {
class ClientWebAvecRedirection {
static void Main(string[] args) {
// دستور زبان
const string syntaxe = "pg URI GET/HEAD";
// تعداد آرگومانها
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// توجه به URI درخواستی
string stringURI = args[0];
string commande = args[1].ToUpper();
//اعتبارسنجی URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI نادرست است
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// تأیید سفارش
if (commande != "GET" && commande != "HEAD") {
// سفارش نادرست
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
const int nbRedirsMax = 1; // حداکثر یک تغییر مسیر مجاز است
int nbRedirs = 0; // تعداد ریدایرکتهای در حال انجام
// بیان منظم برای یافتن یک هدایت URL
Regex location = new Regex(@"^Location: (.+?)$");
try {
// ممکن است چندین URL برای درخواست وجود داشته باشد اگر ریدایرکتها وجود داشته باشند
while (nbRedirs <= nbRedirsMax) {
//مدیریت هدایت
bool redir = false;
bool locationFound = false;
string locationString = null;
// اتصال به سرویس
using (TcpClient tcpClient = new TcpClient(uri.Host, uri.Port)) {
using (StreamReader reader = new StreamReader(tcpClient.GetStream())) {
using (StreamWriter writer = new StreamWriter(tcpClient.GetStream())) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//درخواست برای URL – ارسال سربرگها HTTP
writer.WriteLine(commande + " " + uri.PathAndQuery + " HTTP/1.1");
writer.WriteLine("Host: " + uri.Host + ":" + uri.Port);
writer.WriteLine("Connection: close");
writer.WriteLine();
// خواندن خط اول پاسخ
string premièreLigne = reader.ReadLine();
// اکوی صفحه
Console.WriteLine(premièreLigne);
// بازراستنمایی؟
if (Regex.IsMatch(premièreLigne.ToLower(), @"\s+moved\s*")) {
//یک هدایت وجود دارد
redir = true;
nbRedirs++;
}
// سربرگهای زیر HTTP را تا زمانی که خط خالی که پایان سربرگها را نشان میدهد، یافت شود
string réponse = null;
while ((réponse = reader.ReadLine()) != "") {
// پاسخ را نمایش دهید
Console.WriteLine(réponse);
// اگر هدایت وجود داشته باشد، به دنبال هدر Location بگردید
if (redir && !locationFound) {
// خط فعلی را با عبارت رابط `location` مقایسه کنید
Match résultat = location.Match(réponse);
if (résultat.Success) {
// اگر یافت شد، تغییر مسیر را ثبت میکنیم URL
locationString = résultat.Groups[1].Value;
//توجه کنید که تطابق یافت شده است
locationFound = true;
}
}
}
// سربرگهای HTTP به پایان رسیدهاند – یک خط خالی بنویسید
Console.WriteLine(réponse);
// سپس به بدنهٔ سند میرویم
while ((réponse = reader.ReadLine()) != null) {
Console.WriteLine(réponse);
}
}
}
}
// آیا کارمان تمام شده است؟
if (!locationFound || nbRedirs > nbRedirsMax)
break;
//یک هدایت مجدد باید انجام شود – ما URI جدید را میسازیم
try {
if (locationString.StartsWith("http")) {
// آدرس کامل HTTP
uri = new Uri(locationString);
} else {
// آدرس HTTP نسبی نسبت به URI فعلی
uri = new Uri(uri, locationString);
}
// لاگ کنسول
Console.WriteLine("\n<--Redirection vers l'URL {0}-->\n", uri);
} catch (Exception ex) {
// مشکل با URI
Console.WriteLine("\n<--L'adresse de redirection {0} n'a pas été comprise : {1} -->\n", locationString, ex.Message);
}
}
} catch (Exception e) {
// استثناء نمایش داده میشود
Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
}
}
}
}
در مقایسه با نسخه قبلی، تغییرات به شرح زیر است:
- خط ۴۶: عبارت منظم برای بازیابی آدرس هدایت از Location در HTTP: هدر.
- خط ۴۹: کدی که قبلاً برای یک URI اجرا میشد، اکنون میتواند بهصورت متوالی برای چندین URI اجرا شود.
- خط ۶۶: اولین خط از هدرهای HTTP ارسالشده توسط سرور خوانده میشود. این خط حاوی کلمه کلیدی moved است، در صورتی که سند درخواستشده جابجا شده باشد.
- خطوط ۷۱–۷۵: برنامه بررسی میکند که آیا خط اول حاوی کلیدواژه moved است یا خیر. در صورت مثبت بودن، آن را ثبت میکند.
- خطوط ۷۹–۹۳: سربرگهای باقیمانده HTTP تا زمانی که یک خط خالی خوانده شود و پایان آنها را اعلام کند، خوانده میشوند. اگر خط اول نشاندهندهٔ یک هدایت (redirection) باشد، برنامه سپس هدر Location: HTTP را بررسی میکند تا آدرس هدایت را در locationString ذخیره کند.
- خطوط ۹۸–۱۰۰: باقیمانده پاسخ سرور، HTTP، در کنسول نمایش داده میشود.
- خطوط ۱۰۵–۱۰۶: URI درخواستشده بهطور کامل پردازش و نمایش داده شده است. اگر هیچ مسیریابی برای انجام وجود نداشته باشد یا تعداد مسیریابیهای مجاز فراتر رفته باشد، برنامه خروجی میگیرد.
- خطوط ۱۰۸–۱۲۲: اگر مسیریابی وجود داشته باشد، URI جدید برای درخواست محاسبه میشود. بسته به اینکه آدرس مسیریابی یافتشده مطلق (خط ۱۱۱) یا نسبی (خط ۱۱۴) باشد، کار کمی لازم است.
11.7. کلاسهای .NET که در یک پروتکل اینترنتی خاص تخصص دارند
در مثالهای قبلی کلاینت وب، پروتکل HTTP با استفاده از کلاینت TCP مدیریت میشد. بنابراین، ما مجبور بودیم خودمان پروتکل ارتباطی خاصی را که استفاده میشد مدیریت کنیم. ما میتوانستیم به روشی مشابه، یک کلاینت SMTP یا POP بسازیم. چارچوب .NET کلاسهای تخصصی برای پروتکلهای HTTP و SMTP فراهم میکند. این کلاسها پروتکل ارتباطی بین کلاینت و سرور را درک میکنند و بدین ترتیب توسعهدهنده را از نیاز به مدیریت آنها بینیاز میسازند. اکنون آنها را معرفی خواهیم کرد.
11.7.1. classeWebClient
یک کلاس به نام WebClient وجود دارد که قادر به برقراری ارتباط با یک سرور وب است. بیایید مثال کلاینت وب از بخش 11.6.5 را در نظر بگیریم که در اینجا با استفاده از کلاس WebClient مدیریت میشود.
using System;
using System.IO;
using System.Net;
namespace Chap9 {
public class Program {
public static void Main(string[] args) {
// دستور زبان: [prog] URI
const string syntaxe = "pg URI";
// تعداد آرگومانها
if (args.Length != 1) {
Console.WriteLine(syntaxe);
return;
}
// URI درخواستی ثبت شد
string stringURI = args[0];
// اعتبارسنجی URI
if (!stringURI.StartsWith("http://")) {
Console.WriteLine("Indiquez une Url de la forme http://machine[:port]/document");
return;
}
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI نادرست است
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
try {
// کلاینت وب ایجاد شد
using (WebClient client = new WebClient()) {
// HTTP سربرگ اضافه شد
client.Headers.Add("user-agent", "st");
using (Stream stream = client.OpenRead(uri)) {
using (StreamReader reader = new StreamReader(stream)) {
//نمایش پاسخ وبسرور
Console.WriteLine(reader.ReadToEnd());
//نمایش هدرهای پاسخ سرور
Console.WriteLine("---------------------");
foreach (string clé in client.ResponseHeaders.Keys) {
Console.WriteLine("{0}: {1}", clé, client.ResponseHeaders[clé]);
}
Console.WriteLine("---------------------");
}
}
}
} catch (WebException e1) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e1);
} catch (Exception e2) {
Console.WriteLine("L'exception suivante s'est produite : {0}", e2);
}
}
}
}
- خط ۳۵: کلاینت وب ایجاد شده اما هنوز پیکربندی نشده است
- خط ۳۷: یک هدر HTTP به درخواست HTTP که در شرف ارسال است، اضافه میشود. خواهیم دید که هدرهای دیگری نیز به صورت پیشفرض ارسال خواهند شد.
- خط ۳۸: کلاینت وب URI ارائهشده توسط کاربر را درخواست میکند و سند ارسالشده را میخواند. [WebClient].OpenRead(URI) یک اتصال با Uri برقرار میکند و پاسخ را میخواند. این هدف کلاس است. این کلاس ارتباط با سرور وب را مدیریت میکند. نتیجه متد OpenRead از نوع Stream است و سند درخواستی را نشان میدهد. سربرگهای HTTP که توسط سرور ارسال میشوند و پیش از سند در پاسخ قرار دارند، جزئی از آن نیستند.
- خط ۳۹: یک StreamReader استفاده میشود و در خط ۴۱، از متد آن ReadToEnd برای خواندن کل پاسخ استفاده میشود.
- خطوط ۴۴–۴۶: سربرگهای HTTP از پاسخ سرور نمایش داده میشوند. [WebClient].ResponseHeaders نمایانگر یک مجموعهٔ دارای مقدار است که کلیدهای آن نامهای سربرگهای HTTP هستند و مقادیر آن رشتههای کاراکتری مرتبط با این سربرگها میباشند.
- خط ۵۱: استثناهای ایجادشده در طول تبادل کلاینت/سرور از نوع WebException هستند.
بیایید چند مثال را بررسی کنیم.
ما سرور عمومی TCP را که در بخش 6.4.6 ساخته شده است راهاندازی میکنیم:
ما کلاینت وب قبلی را به شرح زیر راهاندازی میکنیم:
URI درخواستی متعلق به سرور عمومی است. سپس سرور سربرگهای HTTP را که توسط کلاینت وب برای آن ارسال شده است نمایش میدهد:
میتوانیم ببینیم که:
- که کلاینت وب بهطور پیشفرض سه هدر HTTP را ارسال میکند (خطوط ۳، ۵، ۶)
- خط ۴: هدر را خودمان تولید کردهایم (خط ۳۷ کد)
- که کلاینت وب بهطور پیشفرض از روش GET استفاده میکند (خط ۳). روشهای دیگری نیز وجود دارد، از جمله POST و HEAD.
اکنون بیایید یک منبع غیرموجود را درخواست کنیم:
- خط ۲: ما با یک استثنای WebException مواجه شدیم زیرا سرور با کد وضعیت 404 Not Found (یافت نشد) پاسخ داد تا نشان دهد که منبع درخواستشده وجود ندارد.
در نهایت، با درخواست یک منبع موجود کار را به پایان میرسانیم:
فایل istia.univ-angers.txt تولید شده توسط دستور به شرح زیر است:
- خط ۱: سند درخواستی HTML.
- خطوط ۳–۱۰: سربرگهای پاسخ HTTP، به ترتیبی که لزوماً با ترتیب ارسال آنها یکسان نیست.
کلاس WebClient متدهایی برای دریافت یک سند (متدهای DownLoad) یا ارسال آن (متدهای UpLoad) دارد:
برای دانلود یک منبع به صورت آرایه بایت (مثلاً یک تصویر) | |
برای دانلود یک منبع و ذخیره آن در یک فایل محلی | |
برای دانلود یک منبع و بازیابی آن به صورت یک رشته (مثلاً یک فایل HTML) | |
مقابل OpenRead، اما برای ارسال داده به سرور | |
مقابل DownLoadData، اما برای ارسال به سرور | |
همتای DownLoadFile، اما برای ارسال به سرور | |
مقابل DownLoadString اما ارسال شده به سرور | |
برای ارسال دادهها برای یک فرمان POST به سرور و بازیابی نتایج به شکل یک آرایه بایت. فرمان POST یک سند را درخواست میکند در حالی که اطلاعات مورد نیاز برای تعیین سند واقعی که باید ارسال شود را به سرور منتقل میکند. این اطلاعات به عنوان یک سند به سرور ارسال میشود، از این رو نام متد UpLoad است. این اطلاعات پس از خط خالی در هدرهای HTTP به شکل param1=valeur1¶m2=valeur2&... ارسال میشود:
همان سند میتوانست با استفاده از روش GET درخواست شود:
تفاوت بین این دو روش در این است که مرورگری که URI درخواستی را نمایش میدهد، در مورد POST، /document را نشان خواهد داد و در مورد GET، /document?param1=valeur1¶m2=valeur2&... را نمایش خواهد داد. |
11.7.2. کلاسهای WebRequest / WebResponse
گاهی کلاس WebClient به اندازه کافی انعطافپذیر نیست تا آنچه لازم است را انجام دهد. بیایید به مثال مشتری وب با هدایت که در بخش 11.6.6 مورد بحث قرار گرفت، بازگردیم. ما باید هدر HTTP را ارسال کنیم:
ما دیدهایم که سربرگهای HTTP که بهطور پیشفرض توسط کلاینت وب ارسال میشوند، به شرح زیر بودند:
ما همچنین دیدهایم که میتوان با استفاده از مجموعه [WebClient].Headers، سربرگهای HTTP را به موارد فوق اضافه کرد. تنها خط ۱ یک هدر متعلق به مجموعه Headers نیست، زیرا از قالب کلید:مقدار پیروی نمیکند. من نتوانستهام بفهمم چگونه GET را در خط 1 به HEAD تغییر دهم، با شروع از کلاس WebClient (شاید به اندازه کافی دقیق نگاه نکردهام؟). وقتی کلاس WebClient به انتها رسید، میتوانیم به کلاسهای WebRequest / WebResponse برویم:
- WebRequest: نمایانگر کل درخواست از سمت کلاینت وب است.
- WebResponse: نمایانگر کل پاسخ وبسرور است
ما گفتهایم که کلاس WebClient از اسکیماهای http:، https:، ftp: و file: پشتیبانی میکند. درخواستها و پاسخها برای این پروتکلهای مختلف فرمت یکسانی ندارند. بنابراین لازم است نوع دقیق این عناصر را بهجای انواع کلی WebRequest و WebResponse مدیریت کنیم. بنابراین از کلاسهای زیر استفاده خواهیم کرد:
- HttpWebRequest، HttpWebResponse برای یک کلاینت HTTP
- FtpWebRequest و FtpWebResponse برای کلاینتی با شناسه FTP
اکنون از کلاسهای HttpWebRequest و HttpWebresponse برای بررسی مثال کلاینت وب با هدایت (redirection) که در بخش 11.6.6 مورد بحث قرار گرفته است، استفاده خواهیم کرد. کد به شرح زیر است:
using System;
using System.IO;
using System.Net.Sockets;
using System.Net;
namespace Chap9 {
class WebRequestResponse {
static void Main(string[] args) {
// دستور زبان
const string syntaxe = "pg URI GET/HEAD";
// تعداد آرگومانها
if (args.Length != 2) {
Console.WriteLine(syntaxe);
return;
}
// توجه به URI درخواستی
string stringURI = args[0];
string commande = args[1].ToUpper();
// اعتبارسنجی URI
Uri uri = null;
try {
uri = new Uri(stringURI);
} catch (Exception ex) {
// URI نادرست است
Console.WriteLine("L'erreur suivante s'est produite : {0}", ex.Message);
return;
}
// تأیید سفارش
if (commande != "GET" && commande != "HEAD") {
// سفارش نادرست
Console.WriteLine("Le second paramètre doit être GET ou HEAD");
return;
}
try {
//پیکربندی درخواست
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = commande;
httpWebRequest.Proxy = null;
// اجرای آن
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
// نتیجه
Console.WriteLine("---------------------");
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri,(int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
// سربرگها HTTP
Console.WriteLine("---------------------");
foreach (string clé in httpWebResponse.Headers.Keys) {
Console.WriteLine("{0}: {1}", clé, httpWebResponse.Headers[clé]);
}
Console.WriteLine("---------------------");
// سند
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
// پاسخ در کنسول نمایش داده میشود
Console.WriteLine(reader.ReadToEnd());
}
}
} catch (WebException e1) {
// پاسخ بازیابی میشود
HttpWebResponse httpWebResponse = e1.Response as HttpWebResponse;
Console.WriteLine("Le serveur {0} a répondu : {1} {2}", httpWebResponse.ResponseUri, (int)httpWebResponse.StatusCode, httpWebResponse.StatusDescription);
} catch (Exception e2) {
// استثناء نمایش داده میشود
Console.WriteLine("L'erreur suivante s'est produite : {0}", e2.Message);
}
}
}
}
- خط ۴۰: یک شیء از نوع WebRequest با استفاده از متد استاتیک WebRequest.Create(Uri uri) ایجاد میشود، که در آن uri نشانی (URI) سندی است که باید دانلود شود. از آنجا که میدانیم پروتکل URI برابر با HTTP است، نوع نتیجه به HttpWebRequest تغییر داده میشود تا به عناصر خاص پروتکل HTTP دسترسی پیدا شود.
- خط ۴۱: ما متد GET / POST / HEAD را از خط اول هدرها HTTP تنظیم میکنیم. در اینجا این میتواند GET یا HEAD باشد.
- خط ۴۲: در یک شبکه خصوصی شرکتی، رایج است که کامپیوترهای شرکت به دلایل امنیتی از اینترنت ایزوله شوند. برای این منظور، شبکه خصوصی از آدرسهای اینترنتی استفاده میکند که روترهای اینترنت آنها را مسیریابی نمیکنند. شبکه خصوصی از طریق دستگاههای ویژهای به نام پروکسی به اینترنت متصل میشود که هم به شبکه خصوصی شرکت و هم به اینترنت متصل هستند. نمونهای از چنین دستگاهی با آدرسهای متعدد IP است. یک دستگاه در شبکه خصوصی به تنهایی نمیتواند به یک سرور اینترنتی - مانند یک وبسرور - متصل شود. این دستگاه باید از یک ماشین پروکسی بخواهد که این کار را از طرف آن انجام دهد. دستگاهی با آدرس proxy ممکن است سرورهایی با آدرس proxy را برای پروتکلهای مختلف میزبانی کند. اصطلاح «پراکسی HTTP» به سرویسی اطلاق میشود که مسئول ارسال درخواستهای HTTP از طرف دستگاههای موجود در شبکه خصوصی است. اگر چنین سرور پروکسی HTTP وجود داشته باشد، باید در فیلد [WebRequest].proxy مشخص شود. برای مثال، شما مینویسید:
اگر پروکسی HTTP روی پورت ۳۱۲۸ ماشین pproxy.istia.uang کار میکند. اگر ماشین دسترسی مستقیم به اینترنت دارد و نیازی به استفاده از پروکسی ندارد، فیلد [WebRequest].proxy را روی null تنظیم کنید.
- خط ۴۴: متد GetResponse() سند مشخصشده توسط URI آن را درخواست میکند و یک شیء WebRequestResponse را بازمیگرداند که در اینجا به یک شیء HttpWebResponse تبدیل میشود. این شیء پاسخ سرور به درخواست سند را نشان میدهد.
- خط ۴۷:
- [HttpWebResponse].ResponseUri: نشانی URI سروری است که سند را ارسال کرده است. در صورت تغییر مسیر، این نشانی ممکن است با URI سروری که در ابتدا پرسوجو شده بود متفاوت باشد. شایان ذکر است که این کد هدایت (redirection) را مدیریت نمیکند. این کار بهطور خودکار توسط متد GetResponse انجام میشود. بار دیگر، این مزیت کلاسهای سطح بالا نسبت به کلاسهای پایه پروتکل TCP است.
- [HttpWebResponse].StatusCode, [HttpWebResponse].StatusDescription نمایانگر خط اول پاسخ است، برای مثال: HTTP/1.1 200 OK. StatusCode برابر با 200 است و StatusDescription برابر با OK است.
- خط ۵۰: [HttpWebResponse]. Headers مجموعهای از سربرگهای HTTP از پاسخ است.
- خط ۵۵: [HttpWebResponse]. GetResponseStream: استریمی است که برای بازیابی سند موجود در پاسخ استفاده میشود.
- خط ۶۱: ممکن است یک استثنای WebException رخ دهد
- خط ۶۳: [WebException].Response پاسخی است که باعث ایجاد استثنا شده است.
در اینجا مثالی از اجرا آورده شده است:
- خطوط ۱ و ۳: سرور پاسخدهنده با سرور پرسششده یکسان نیست. بنابراین یک هدایت (redirection) رخ داده است.
- خطوط ۵ تا ۱۱: سربرگهای HTTP ارسالشده توسط سرور
11.7.3. کاربرد: یک کلاینت پروکسی برای یک سرور ترجمه وب
اکنون نشان خواهیم داد که کلاسهای قبلی چگونه به ما امکان استفاده از منابع وب را میدهند.
11.7.3.1. L'application
وبسایتهای ترجمه در وب وجود دارند. وبسایتی که در اینجا استفاده خواهیم کرد http://trans.voila.fr/traduction_voila.php است:
![]() | متن مورد نظر برای ترجمه در [1] وارد میشود و جهت ترجمه در [2] انتخاب میگردد. درخواست ترجمه از طریق [3] ارسال و در [4] دریافت میشود. |
ما قصد داریم یک برنامهٔ کلاینت ویندوزی برای برنامهٔ فوق بنویسیم. این برنامه هیچ کاری فراتر از برنامهٔ موجود در سایت [trans.voila.fr] انجام نخواهد داد. رابط کاربری آن به شرح زیر خواهد بود:
![]() |
11.7.3.2. معماری برنامه
برنامه دارای معماری دو لایه زیر خواهد بود:
![]() |
11.7.3.3. پروژه ویژوال استودیو
پروژه ویژوال استودیو به شرح زیر خواهد بود:
![]() |
- در [1]، راهحل شامل دو پروژه است،
- [2]: یکی برای لایه [dao] و اِنتِیتیهای مورد استفاده توسط آن،
- [3]: دیگری برای رابط کاربری ویندوز
11.7.3.4. پروژه [dao]
پروژه [dao] شامل اجزای زیر است:
- IServiceTraduction.cs: رابط ارائهشده به لایه [ui]
- ServiceTraduction: پیادهسازی این رابط
- WebTraductionsException: یک استثنای خاص برنامه
رابط IServiceTraduction به شرح زیر است:
using System.Collections.Generic;
namespace dao {
public interface IServiceTraduction {
// زبانهای مورد استفاده
IDictionary<string, string> LanguesTraduites { get; }
// ترجمه
string Traduire(string texte, string deQuoiVersQuoi);
}
}
- خط ۶: خاصیت LanguesTraduites دیکشنری زبانهای پذیرفتهشده توسط سرور ترجمه را بازمیگرداند. این فرهنگ لغت شامل مدخلهایی به شکل ["fe","Français-Anglais"] است، که در آن مقدار نشاندهنده جهت ترجمه – در این مورد، از فرانسوی به انگلیسی – و کلید "fe" کدی است که توسط سرور ترجمه trans.voila.fr استفاده میشود.
- خط ۸: متد Traduire متد ترجمه است:
- texte متنی است که باید ترجمه شود
- deQuoiVersQuoi یکی از کلیدها در فرهنگ لغت زبانهای ترجمهشده است
- این متد ترجمه متن را بازمیگرداند
ServiceTraduction یک کلاس پیادهسازی از رابط IServiceTraduction است. ما این موضوع را در بخش بعدی به تفصیل توضیح میدهیم.
WebTraductionsException کلاس استثنای زیر است:
using System;
namespace entites {
public class WebTraductionsException : Exception {
// کد خطا
public int Code { get; set; }
// تولیدکنندگان
public WebTraductionsException() {
}
public WebTraductionsException(string message)
: base(message) {
}
public WebTraductionsException(string message, Exception e)
: base(message, e) {
}
}
}
- خط ۷: یک کد خطا
11.7.3.5. کلاینت وب [ServiceTraduction]
بیایید نگاهی دیگر به معماری برنامه خود بیندازیم:
![]() |
کلاس [ServiceTraduction] که باید بنویسیم، مشتری سرویس وب ترجمه [trans.voila.fr] است. برای نوشتن آن، باید درک کنیم
- آنچه سرور ترجمه از کلاینت خود انتظار دارد
- چه چیزی را به کلاینت خود بازمیگرداند
بیایید نگاهی به مثالی از دیالوگ کلاینت/سرور که در طول یک ترجمه رخ میدهد بیندازیم. بیایید مثال ارائهشده در مقدمهٔ برنامه را دوباره مرور کنیم:
![]() | متن مورد ترجمه در [1] وارد میشود و جهت ترجمه در [2] انتخاب میگردد. درخواست ترجمه از طریق [3] ارسال شده و در [4] دریافت میشود. |
برای دریافت ترجمه [4]، مرورگر درخواست زیر GET را ارسال کرد (در نوار آدرس نمایش داده شده):
درک آن نسبتاً ساده است:
- http://trans.voila.fr/traduction_voila.php نشانی (URL) سرویس ترجمه است
- isText=1 به نظر میرسد نشان میدهد که ما با متن سروکار داریم
- translationDirection جهت ترجمه را نشان میدهد، در این مورد Français-Anglais
- stext متنی است که باید ترجمه شود و در قالبی به نام متن رمزگذاریشدهی URL ارائه شده است. این به این دلیل است که برخی کاراکترها نمیتوانند در یک URL ظاهر شوند. برای مثال، این مورد در مورد کاراکتر فاصله صادق است که در اینجا به صورت یک '+' رمزگذاری شده است. چارچوب .NET متد استاتیک System.Web.HttpUtility.UrlEncode را برای انجام این رمزگذاری فراهم میکند.
بنابراین میتوانیم نتیجه بگیریم که کلاس ما [ServiceTraduction] میتواند برای پرسوجو از سرور ترجمه، از رشته
که در آن جایگرینهای {0} و {1} به ترتیب با جهت ترجمه و متن مورد ترجمه جایگزین میشوند.
چگونه میدانیم کدام جهتهای ترجمه توسط سرور پذیرفته میشوند؟ در تصویر بالا، زبانهای مقصد در منوی کشویی فهرست شدهاند. اگر کد منبع HTML صفحه را در مرورگر (View / Source) مشاهده کنیم، برای منوی کشویی موارد زیر را مییابیم:
این کد HTML چندان تمیز نیست، زیرا هر تگ <option> معمولاً باید با یک تگ </option> بسته شود. با این حال، ویژگیهای 'value' لیست کدهای ترجمهای را که باید به سرور ارسال شوند، در اختیار ما قرار میدهند. در فرهنگ لغت LanguesTraduites برای رابط کاربری IServiceTraduction، کلیدها ویژگیهای «value» ذکر شده در بالا خواهند بود و مقادیر، متنهای نمایش داده شده توسط لیست کشویی خواهند بود.
اکنون بیایید (View / Source) را ببینیم تا مشخص کنیم ترجمهای که توسط سرور ترجمه بازگردانده شده، در کجای صفحه HTML قرار دارد:
مترجم درست در وسط صفحه HTML بازگشتی قرار دارد. چگونه میتوانیم آن را پیدا کنیم؟ میتوانیم از یک عبارت منظم با دنباله ... استفاده کنیم زیرا تگ فقط در این نقطه از صفحه HTML ظاهر میشود. عبارت منظم C# که برای بازیابی متن ترجمهشده استفاده میشود به شرح زیر است:
اکنون ما عناصر لازم برای نوشتن کلاس پیادهسازی ServiceTraduction برای رابط IServiceTraduction را داریم:
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using entites;
namespace dao {
public class ServiceTraduction : IServiceTraduction {
// ویژگیهای پیکربندی سرویس خودکار
public IDictionary<string, string> LanguesTraduites { get; set; }
public string UrlServeurTraduction { get; set; }
public string ProxyHttp { get; set; }
public String RegexTraduction { get; set; }
// ترجمه
public string Traduire(string texte, string deQuoiVersQuoi) {
//آیا ترجمهٔ درخواستی امکانپذیر است؟
if (!LanguesTraduites.ContainsKey(deQuoiVersQuoi)) {
throw new WebTraductionsException(String.Format("Le sens de traduction [{0}] n'est pas reconnu")) { Code = 10 };
}
// متن مورد نظر برای ترجمه
string texteATraduire = HttpUtility.UrlEncode(texte);
// URI برای درخواست
string uri = string.Format(UrlServeurTraduction, deQuoiVersQuoi, texteATraduire);
//عبارت منظم برای یافتن ترجمه در پاسخ
Regex patternTraduction = new Regex(RegexTraduction);
// استثناء
WebTraductionsException exception = null;
// ترجمه
string traduction = null;
try {
// پرسوجو را پیکربندی کنید
HttpWebRequest httpWebRequest = WebRequest.Create(uri) as HttpWebRequest;
httpWebRequest.Method = "GET";
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
// اجرا کردن آن
HttpWebResponse httpWebResponse = httpWebRequest.GetResponse() as HttpWebResponse;
//سند
using (Stream stream = httpWebResponse.GetResponseStream()) {
using (StreamReader reader = new StreamReader(stream)) {
bool traductionTrouvée = false;
string ligne = null;
while (!traductionTrouvée && (ligne = reader.ReadLine()) != null) {
// جستجو برای ترجمه در خط فعلی
MatchCollection résultats = patternTraduction.Matches(ligne);
// ترجمه یافت شد؟
if (résultats.Count != 0) {
traduction = résultats[0].Groups[1].Value.Trim();
traductionTrouvée = true;
}
}
// ترجمه یافت شد؟
if (!traductionTrouvée) {
exception = new WebTraductionsException("Le serveur n'a pas renvoyé de réponse") { Code = 12 };
}
}
}
} catch (Exception e) {
exception = new WebTraductionsException("Erreur rencontrée lors de la traduction", e) { Code = 11 };
}
// استثناء؟
if (exception != null) {
throw exception;
} else {
return traduction;
}
}
}
}
- خط ۱۲: خاصیت LanguesTraduites از رابط IServiceTraduction – مقداردهیشده از خارج
- خط ۱۳: ویژگی UrlServeurTraduction آدرس URL است که باید به سرور ترجمه ارسال شود: http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1} که در آن جایگرین {0} باید با جهت ترجمه و جایگرین {1} با متن مورد نظر برای ترجمه جایگزین شود – مقداردهی اولیه از خارج انجام شده است
- خط ۱۴: خاصیت ProxyHttp پروکسی HTTP است که در صورت وجود باید استفاده شود، برای مثال: pproxy.istia.uang:3128 – مقداردهی شده به صورت خارجی
- خط 15: خاصیت RegexTraduction عبارت منظم (regular expression) است که برای استخراج ترجمه از جریان HTML بازگردانده شده توسط سرور ترجمه استفاده میشود، برای مثال @"<div class=""txtTrad"">(.*?)" – مقداردهی اولیه به صورت خارجی
- این چهار ویژگی در برنامه ما توسط Spring مقداردهی اولیه خواهند شد.
- خطوط ۲۰–۲۲: ما بررسی میکنیم که جهت ترجمه درخواستی واقعاً در فرهنگ لغت زبانهای ترجمهشده وجود دارد. اگر اینطور نباشد، یک استثنا پرتاب میشود.
- خط ۲۴: متن مورد نظر برای ترجمه رمزگذاری میشود تا بتواند بخشی از یک URL را تشکیل دهد
- خط ۲۶: URI برای سرویس ترجمه ساخته میشود. اگر ویژگی UrlServeurTraduction رشته http://trans.voila.fr/traduction_voila.php? باشد؟isText=1&translationDirection={0}&stext={1}, نشانگر {0} با جهت ترجمه و نشانگر {1} با متن مورد نظر برای ترجمه جایگزین میشود.
- خط ۲۸: قالب جستجو برای ترجمه در پاسخ HTML از سرور ترجمه ساخته میشود.
- خطوط ۳۳ و ۶۰: عملیات پرسوجو از سرور ترجمه در داخل یک بلوک try/catch انجام میشود
- خط ۳۵: شیء HttpWebRequest، که برای پرسوجو از سرور ترجمه استفاده خواهد شد، با استفاده از URI سند درخواستشده ایجاد میشود.
- خط ۳۶: متد پرسوجو GET است. این دستور میتواند حذف شود، زیرا GET احتمالاً متد پیشفرض برای شیء HttpWebRequest است.
- خط ۳۷: ویژگی Proxy برای شیء HttpWebRequest تنظیم میشود.
- خط ۳۹: درخواستی به سرور ترجمه ارسال میشود و پاسخ آن با نوع HttpWebResponse دریافت میشود.
- خطوط ۴۱–۴۲: از StreamReader برای خواندن هر خط از پاسخ HTML سرور استفاده میشود.
- خطوط ۴۵–۵۳: در هر خط پاسخ، ترجمه را جستجو میکنیم. پس از یافتن آن، خواندن پاسخ HTML را متوقف کرده و تمام استریمهای بازشده را میبندیم.
- خطوط ۵۵–۵۷: اگر در پاسخ HTML ترجمهای یافت نشود، یک استثنای WebTraductionsException برای نشان دادن این موضوع آماده میشود.
- خطوط ۶۰–۶۲: اگر در حین تبادل کلاینت/سرور استثناءای رخ داده باشد، این استثناء در یک استثناء از نوع WebTraductionsException پیچانده میشود تا این موضوع را نشان دهد.
- خطوط ۶۴–۶۸: اگر یک استثنا ثبت شده باشد، پرتاب میشود؛ در غیر این صورت، ترجمه یافتشده بازگردانده میشود.
مثال ما فرض میکند که پروکسی HTTP نیاز به احراز هویت ندارد. اگر اینطور نبود، چیزی شبیه به این مینوشتیم:
httpWebRequest.Proxy = ProxyHttp == null ? null : new WebProxy(ProxyHttp); ;
httpWebRequest.Proxy.Credentials=new NetworkCredential("login","password");
ما در اینجا به جای WebClient از WebRequest / WebResponse استفاده کردهایم، زیرا نیازی به پردازش کل پاسخ HTML از سرور ترجمه نداریم. پس از یافتن ترجمه در این پاسخ، دیگر به بقیه خطوط پاسخ نیازی نداریم. کلاس WebClient این امکان را برای ما فراهم نمیکند.
در اینجا یک برنامه آزمایشی برای کلاس ServiceTraduction آورده شده است:
using System;
using System.Collections.Generic;
using dao;
using entites;
namespace ui {
class Program {
static void Main(string[] args) {
try {
// ایجاد سرویس ترجمه
ServiceTraduction serviceTraduction = new ServiceTraduction();
// بیان منظم برای یافتن ترجمه
serviceTraduction.RegexTraduction = @"<div class=""txtTrad"">(.*?)</div>";
// آدرس سرور ترجمه
serviceTraduction.UrlServeurTraduction = "http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}";
// فرهنگ لغت زبانهای ترجمهشده
Dictionary<string, string> languesTraduites = new Dictionary<string, string>();
languesTraduites["fe"]= "Français-Anglais";
languesTraduites["fs"]= "Français-Espagnol";
languesTraduites["ef"]= "Anglais-Français";
serviceTraduction.LanguesTraduites = languesTraduites;
// پروکسی
//serviceTraduction.ProxyHttp = "pproxy.istia.uang:3128";
//ترجمه
string texte = "ce chien est perdu";
string deQuoiVersQuoi = "fe";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "l'été sera chaud";
deQuoiVersQuoi = "fs";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "my tailor is rich";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
texte = "xx";
deQuoiVersQuoi = "ef";
Console.WriteLine("Traduction [{0}] de [{1}] : [{2}]", languesTraduites[deQuoiVersQuoi], texte, serviceTraduction.Traduire(texte, deQuoiVersQuoi));
} catch (WebTraductionsException e) {
// خطا
Console.WriteLine("L'erreur suivante de code {1} s'est produite : {0}", e.Message, e.Code);
}
}
}
}
نتایج بهدستآمده به شرح زیر است:
پروژه [dao] در راهحل به DLL و HttpTraductions.dll کامپایل میشود:
![]() |
11.7.3.6. رابط کاربری گرافیکی برنامه
بیایید بار دیگر به معماری برنامهٔ خود نگاهی بیندازیم:
![]() |
ما اکنون در حال نوشتن لایه [ui] هستیم. این موضوع پروژه [ui] در راهحل فعلی تحت توسعه است:
![]() |
پوشه [lib] [3] حاوی برخی از فایلهای DLL است که توسط پروژه [4] ارجاع شدهاند:
- فایلهای مورد نیاز Spring: Spring.Core, Common.Logging, antlr.runtime
- آن مربوط به لایه [dao]: HttpTraductions
فایل [App.config] حاوی پیکربندی Spring است:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<description>Traductions sur le web</description>
<!-- سرویس ترجمه -->
<object name="ServiceTraduction" type="dao.ServiceTraduction, HttpTraductions">
<property name="UrlServeurTraduction" value="http://trans.voila.fr/traduction_voila.php?isText=1&translationDirection={0}&stext={1}"/>
<!--
<property name="ProxyHttp" value="pproxy.istia.uang:3128"/>
-->
<property name="RegexTraduction" value="<div class="txtTrad">(.*?)</div>"/>
<property name="LanguesTraduites">
<dictionary key-type="string" value-type="string">
<entry key="fe" value="Français-Anglais"/>
<entry key="ef" value="Anglais-Français"/>
...
<entry key="ei" value="Anglais-Italien"/>
<entry key="ie" value="Italien-Anglais"/>
</dictionary>
</property>
</object>
</objects>
</spring>
</configuration>
- خط ۱۵: اشیایی که باید توسط Spring نمونهسازی شوند. تنها یکی وجود خواهد داشت، همان مورد در خط ۱۸، که سرویس ترجمه را با استفاده از کلاس ServiceTraduction که در DLL HttpTraductions یافت میشود، نمونهسازی میکند.
- خط ۱۹: ویژگی UrlServeurTraduction از کلاس ServiceTraduction. در کاراکتر & در URL یک مشکل وجود دارد. این کاراکتر در یک فایل XML معنای خاصی دارد. بنابراین باید فرار داده شود. این موضوع در مورد سایر کاراکترهایی که بعداً در فایل با آنها مواجه خواهیم شد نیز صدق میکند. آنها باید با توالی [&code;] جایگزین شوند: & به [&]، < به [<]، > به [>]، " به ["].
- خط ۲۱: ویژگی ProxyHttp از کلاس ServiceTraduction. یک ویژگی بدون مقدار اولیه باقی مانده است: null. تعریف نکردن این ویژگی معادل بیان این است که هیچ پروکسی HTTP وجود ندارد.
- خط ۲۳: ویژگی RegexTraduction از کلاس ServiceTraduction. در عبارت منظم، کاراکترهای [< > "] باید با معادلهای فرارشان جایگزین میشدند.
- خطوط ۲۴–۳۳: ویژگی LanguesTraduites از کلاس ServiceTraduction.
برنامه [Program.cs] هنگام راهاندازی برنامه اجرا میشود. کد آن به شرح زیر است:
using System;
using System.Text;
using System.Windows.Forms;
using dao;
using Spring.Context;
using Spring.Context.Support;
namespace ui {
static class Program {
/// <summary>
/// نقطه ورود اصلی برنامه.
/// </summary>
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// --------------- کد توسعهدهنده
// نمونه سازی سرویس ترجمه
IApplicationContext ctx = null;
Exception ex = null;
ServiceTraduction serviceTraduction = null;
try {
// کانتکست Spring
ctx = ContextRegistry.GetContext();
//درخواست ارجاع به سرویس ترجمه
serviceTraduction = ctx.GetObject("ServiceTraduction") as ServiceTraduction;
} catch (Exception e1) {
// ذخیرهٔ استثنا
ex = e1;
}
// فرم برای نمایش
Form form = null;
//آیا خطایی رخ داده است؟
if (ex != null) {
// بله – ایجاد پیام خطا برای نمایش
StringBuilder msgErreur = new StringBuilder(String.Format("Chaîne des exceptions : {0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
Exception e = ex;
while (e != null) {
msgErreur.Append(String.Format("{0}: {1}{2}", e.GetType().FullName, e.Message, Environment.NewLine));
msgErreur.Append(String.Format("{0}{1}", "".PadLeft(40, '-'), Environment.NewLine));
e = e.InnerException;
}
// ایجاد پنجرهٔ خطا که پیام خطا به آن ارسال میشود
Form2 form2 = new Form2();
form2.MsgErreur = msgErreur.ToString();
//این پنجره برای نمایش خواهد بود
form = form2;
} else {
//همهچیز بهخوبی انجام شد
//رابط گرافیکی [Form1] را ایجاد میکند که مرجع سرویس ترجمه به آن ارسال میشود
Form1 form1 = new Form1();
form1.ServiceTraduction = serviceTraduction;
//این پنجره برای نمایش خواهد بود
form = form1;
}
//پنجره نمایش
Application.Run(form);
}
}
}
این کد قبلاً در برنامه مالیاتها، نسخه ۶، در بخش ۷.۶.۲ استفاده شده است.
- سرویس ترجمه در خط ۲۷ توسط Spring ایجاد میشود. اگر این ایجاد با موفقیت انجام شود، فرم [Form1] نمایش داده خواهد شد (خطوط ۵۲–۵۵)؛ در غیر این صورت، فرم خطای [Form2] نمایش داده خواهد شد (خطوط ۳۶–۴۸).
فرم [Form2] همان فرم مورد استفاده در اپلیکیشن مالیات نسخهٔ ۶ است و در بند ۷.۶.۴ توضیح داده شده است.
فرم [Form1] به شرح زیر است:
![]() |
خیر. | نوع | نام | نقش |
۱ | TextBox | textBoxTexteATraduire | جعبه متن برای متنی که باید ترجمه شود چندخطی=true |
2 | ComboBox | comboBoxLangues | فهرست جهتهای ترجمه |
۳ | دکمه | buttonTraduire | برای درخواست ترجمه متن [1] به سمت [2] |
۴ | TextBox | textBoxTraduction | مترجم متن [1] |
کد فرم برای [Form1] به شرح زیر است:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using dao;
namespace ui {
public partial class Form1 : Form {
// سرویس ترجمه
public ServiceTraduction ServiceTraduction { get; set; }
// واژهنامه زبان
Dictionary<string, string> languesInversées = new Dictionary<string, string>();
// سازنده
public Form1() {
InitializeComponent();
}
// بارگذاری فرم اولیه
private void Form1_Load(object sender, EventArgs e) {
// ساخت فرهنگ لغت معکوس
foreach (string code in ServiceTraduction.LanguesTraduites.Keys) {
// زبانها
string langues = ServiceTraduction.LanguesTraduites[code];
// افزودن (زبانها، کد) به فرهنگ معکوس
languesInversées[langues] = code;
}
// پر کردن لیست کشویی بر اساس ترتیب حروف الفبای زبانها
string[] languesCombo = languesInversées.Keys.ToArray();
Array.Sort<string>(languesCombo);
foreach (string langue in languesCombo) {
comboBoxLangues.Items.Add(langue);
}
//انتخاب زبان اول
if (comboBoxLangues.Items.Count != 0) {
comboBoxLangues.SelectedIndex = 0;
}
}
private void buttonTraduire_Click(object sender, EventArgs e) {
// چیزی برای ترجمه هست؟
string texte = textBoxTexteATraduire.Text.Trim();
if (texte == "") return;
// ترجمه
try {
textBoxTraduction.Text = ServiceTraduction.Traduire(texte, languesInversées[comboBoxLangues.SelectedItem.ToString()]);
} catch (Exception ex) {
textBoxTraduction.Text = ex.Message;
}
}
}
}
- خط ۱۰: مرجعی به سرویس ترجمه. این متغیر عمومی توسط [Program.cs] در خط 53 مقداردهی اولیه شده است. هنگامی که متدهای Form1_Load (خط 20) یا buttonTraduire_Click (خط 40) اجرا میشوند، بنابراین این فیلد از قبل مقداردهی اولیه شده است.
- خط ۱۲: فرهنگ لغت زبانهای ترجمهشده با ورودیهایی از نوع ["Français-Anglais","fe"] و c.a.d. معکوس فرهنگ لغت LanguesTraduites که توسط سرویس ترجمه بازگردانده میشود.
- خط ۲۰: متد Form1_Load هنگام بارگذاری فرم اجرا میشود.
- خطوط ۲۲–۲۷: دیکشنریهای serviceTraduction.LanguesTraduites و ["fe","Français-Anglais"] برای ساخت دیکشنریهای languesInversées و ["Français-Anglais", "fe"] استفاده میشوند.
- خط ۲۹: languesCombo جدول کلیدی برای فرهنگهای languesInversées و c.a.d است. یک آرایه از عناصر ["Français-Anglais"]
- خط ۳۰: این آرایه مرتب شده است تا جهتهای ترجمه به ترتیب الفبا در لیست کشویی نمایش داده شوند
- خطوط ۳۱–۳۳: فهرست کشویی زبان پر میشود.
- خط ۴۰: متدی که هنگام کلیک کاربر روی دکمه [Traduire] اجرا میشود
- خط ۴۶: به سادگی متد serviceTraduction.Traduire را برای درخواست ترجمه فراخوانی کنید. پارامتر اول متنی است که باید ترجمه شود و پارامتر دوم کد جهت ترجمه است. این کد بر اساس مورد انتخابشده در لیست کشویی زبان از فرهنگ لغت languesInversées بازیابی میشود.
- خط ۴۸: اگر استثنا رخ دهد، به جای ترجمه نمایش داده میشود.
11.7.3.7. Conclusion
این برنامه نشان داده است که کلاینتهای وب چارچوب .NET به ما امکان میدهند از منابع وب استفاده کنیم. این تکنیک در هر مورد مشابه است:
- تعیین URI مورد پرسوجو. این URI معمولاً پارامتریک است.
- آن را پرسوجو کنید
- یافتن آنچه به دنبال آن هستیم در پاسخ سرور با استفاده از عبارتهای منظم
این تکنیک قابل اعتماد نیست. در واقع، با گذشت زمان، URI مورد پرسش یا عبارت منظم (regular expression) مورد استفاده برای یافتن نتیجه مورد انتظار ممکن است تغییر کند. بنابراین توصیه میشود که این دو مورد اطلاعات را در یک فایل پیکربندی ذخیره کنید. با این حال، این ممکن است ناکافی باشد. در فصل بعدی خواهیم دید که منابع پایدارتری در وب وجود دارند: سرویسهای وب.
11.7.4. یک کلاینت SMTP (پروتکل انتقال ایمیل ساده) با استفاده از کلاس SmtpClient
یک کلاینت SMTP، کلاینت یک سرور SMTP است که یک سرور ارسال ایمیل است. کلاس NET SmtpClient به طور کامل نیازمندیهای چنین کلاینتی را در بر میگیرد. توسعهدهنده نیازی به دانستن جزئیات پروتکل SMTP ندارد. ما با این پروتکل آشنا هستیم؛ این پروتکل در بخش 11.4.3 توضیح داده شده است.
ما کلاس SmtpClient را به عنوان بخشی از یک برنامهٔ پایهای ویندوز ارائه میدهیم که به کاربران امکان ارسال ایمیلهای دارای پیوست را میدهد. این برنامه به پورت ۲۵ یک سرور SMTP متصل خواهد شد. لطفاً توجه داشته باشید که در اکثر سیستمهای ویندوزی، فایروالها یا نرمافزارهای آنتیویروس، اتصالات به پورت 25 را مسدود میکنند. بنابراین برای آزمایش برنامه، لازم است این محافظت را غیرفعال کنید:
![]() |
کلاینت SMTP دارای معماری تکلایه خواهد بود:
![]() |
پروژه Visual Studio به شرح زیر است:
![]() |
رابط کاربری گرافیکی برنامه [SendMailForm.cs] به شرح زیر است:
![]() |
خیر. | نوع | نام | نقش |
1 | TextBox | textBoxServeur | نام سرور SMTP برای اتصال به |
2 | NumericUpDown | numericUpDownPort | پورت اتصال |
3 | TextBox | textBoxExpediteur | آدرس ایمیل فرستنده |
۴ | TextBox | textBoxTo | آدرسهای گیرندگان به صورت: آدرس۱، آدرس۲، ... |
۵ | TextBox | textBoxCc | آدرسهای گیرندگان در رونوشت (CC=Carbon Copy) به صورت: آدرس۱، آدرس۲، ... |
6 | TextBox | textBoxBcc | آدرسهای گیرنده کربن کپی مخفی (Bcc) (BCC=کربن کپی مخفی) به صورت: آدرس۱، آدرس۲، ... تمام آدرسهای موجود در این سه فیلد ورودی، پیام یکسانی را با همان پیوستها دریافت خواهند کرد. گیرندگان پیام قادر خواهند بود آدرسهای موجود در فیلدهای ۴ و ۵ را ببینند، اما آدرسهای فیلد ۶ را نخواهند دید. بنابراین Bcc راهی است برای کپی کردن فردی در پیام بدون اطلاع سایر گیرندگان. |
۷ | دکمه | buttonAjouter | برای افزودن پیوست به ایمیل |
8 | ListBox | listBoxPiecesJointes | فهرست پیوستهای ایمیل |
۹ | TextBox | textBoxSujet | موضوع نامه |
10 | TextBox | textBoxMessage | متن پیام. چندخطی=true |
11 | دکمه | buttonEnvoyer | برای ارسال پیام و هر پیوست |
12 | TextBox | textBoxRésultat | خلاصهای از پیام ارسالشده یا در صورت بروز مشکل، یک پیام خطا را نمایش میدهد |
۱۳ | دکمه | buttonEffacer | برای پاک کردن [12] |
OpenfileDialog | openFileDialog1 | بررسی غیر بصری که امکان انتخاب یک فایل از سیستم فایل محلی را فراهم میکند |
در مثال قبلی، خلاصهای که برای [12] نمایش داده میشود به شرح زیر است:
Envoi réussi...
Sujet : votre demande
Destinataires : y2000@hotmail.com
Cc :
Bcc :
Pièces jointes :
C:\data\travail\2007-2008\recrutements 0809\ing3\documents\ing3.zip
Texte : Bonjour,
Vous trouverez ci-joint le dossier de candidature à l'ISTIA.
Cordialement,
ST
کد فرم برای [SendMailForm.cs] به شرح زیر است:
using System;
using System.Windows.Forms;
using System.Net.Mail;
using System.Text.RegularExpressions;
using System.Text;
namespace Chap9 {
public partial class SendMailForm : Form {
public SendMailForm() {
InitializeComponent();
}
// افزودن پیوست
private void buttonAjouter_Click(object sender, EventArgs e) {
// پیکربندی کادر گفتگو openfileDialog1
openFileDialog1.InitialDirectory = Application.ExecutablePath;
openFileDialog1.Filter = "Tous les fichiers (*.*)|*.*";
openFileDialog1.FilterIndex = 0;
openFileDialog1.FileName = "";
// پنجرهٔ گفتگو را نمایش داده و نتیجهٔ آن را بازیابی میکند
if (openFileDialog1.ShowDialog() == DialogResult.OK) {
//بازیابی نام فایل
listBoxPiecesJointes.Items.Add(openFileDialog1.FileName);
}
}
private void textBoxServeur_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void setStatutEnvoyer() {
buttonEnvoyer.Enabled = textBoxServeur.Text.Trim() != "" && textBoxTo.Text.Trim() != "" && textBoxSujet.Text.Trim() != "";
}
// حذف یک پیوست
private void buttonRetirer_Click(object sender, EventArgs e) {
// پیوست انتخاب شده؟
if (listBoxPiecesJointes.SelectedIndex != -1) {
// آن را حذف کنید
listBoxPiecesJointes.Items.RemoveAt(listBoxPiecesJointes.SelectedIndex);
// بهروزرسانی دکمه «حذف»
buttonRetirer.Enabled = listBoxPiecesJointes.Items.Count != 0;
}
}
private void listBoxPiecesJointes_SelectedIndexChanged(object sender, EventArgs e) {
// پیوست انتخاب شده است؟
if (listBoxPiecesJointes.SelectedIndex != -1) {
// بهروزرسانی دکمه «حذف»
buttonRetirer.Enabled = true;
}
}
// ارسال پیام همراه با پیوستهای آن
private void buttonEnvoyer_Click(object sender, EventArgs e) {
....
}
private void textBoxTo_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void textBoxSujet_TextChanged(object sender, EventArgs e) {
setStatutEnvoyer();
}
private void buttonEffacer_Click(object sender, EventArgs e) {
textBoxResultat.Text = "";
}
}
}
ما در مورد این کد توضیحی نمیدهیم زیرا هیچ ویژگی جدیدی ندارد. برای درک متد buttonAjouter_Click در خط 14، خواننده دعوت میشود به پاراگراف 7.5.1 مراجعه کند.
متد buttonEnvoyer_Click در خط ۵۵ که ایمیل را ارسال میکند، به شرح زیر است:
private void buttonEnvoyer_Click(object sender, EventArgs e) {
try {
// ساعت شنی
Cursor = Cursors.WaitCursor;
// کلاینت SMTP
SmtpClient smtpClient = new SmtpClient(textBoxServeur.Text.Trim(), (int)numericUpDownPort.Value);
// پیام
MailMessage message = new MailMessage();
// فرستنده
message.Sender = new MailAddress(textBoxExpéditeur.Text.Trim());
message.From = message.Sender;
// گیرندگان
Regex marqueur = new Regex("\\s*,\\s*");
string[] destinataires = marqueur.Split(textBoxTo.Text.Trim());
foreach (string destinataire in destinataires) {
if (destinataire.Trim() != "") {
message.To.Add(new MailAddress(destinataire));
}
}
// CC
string[] copies = marqueur.Split(textBoxCc.Text.Trim());
foreach (string copie in copies) {
if (copie.Trim() != "") {
message.CC.Add(new MailAddress(copie));
}
}
// BCC
string[] blindCopies = marqueur.Split(textBoxBcc.Text.Trim());
foreach (string blindCopie in blindCopies) {
if (blindCopie.Trim() != "") {
message.Bcc.Add(new MailAddress(blindCopie));
}
}
// موضوع
message.Subject = textBoxSujet.Text.Trim();
// متن پیام
message.Body = textBoxMessage.Text;
// پیوستها
foreach (string attachement in listBoxPiecesJointes.Items) {
message.Attachments.Add(new Attachment(attachement));
}
// پیام ارسال شد
smtpClient.Send(message);
// OK – خلاصه نمایش داده میشود
StringBuilder msg = new StringBuilder(String.Format("Envoi réussi...{0}", Environment.NewLine));
msg.Append(String.Format("Sujet : {0}{1}", textBoxSujet.Text.Trim(), Environment.NewLine));
textBoxSujet.Clear();
msg.Append(String.Format("Destinataires : {0}{1}", textBoxTo.Text.Trim(), Environment.NewLine));
textBoxTo.Clear();
msg.Append(String.Format("Cc : {0}{1}", textBoxCc.Text.Trim(), Environment.NewLine));
textBoxCc.Clear();
msg.Append(String.Format("Bcc : {0}{1}", textBoxBcc.Text.Trim(), Environment.NewLine));
textBoxBcc.Clear();
msg.Append(String.Format("Pièces jointes :{0}", Environment.NewLine));
foreach (string attachement in listBoxPiecesJointes.Items) {
msg.Append(String.Format("{0}{1}", attachement, Environment.NewLine));
}
msg.Append(String.Format("Texte : {0}{1}", textBoxMessage.Text, Environment.NewLine));
listBoxPiecesJointes.Items.Clear();
textBoxResultat.Text = msg.ToString();
} catch (Exception ex) {
// خطای نمایش داده شده
textBoxResultat.Text = String.Format("L'erreur suivante s'est produite {0}", ex);
}
// نمایندهٔ عادی
Cursor = Cursors.Arrow;
}
- خط ۶: کلاینت SMTP ایجاد میشود. این کلاینت به دو پارامتر نیاز دارد: نام سرور SMTP و پورتی که روی آن کار میکند
- خط ۸: پیامی از نوع MailMessage ایجاد میشود. این پیام، کل پیام ارسالی را در بر میگیرد.
- خط ۱۰: آدرس ایمیل فرستنده مشخص میشود. یک آدرس ایمیل نمونهای از نوع MailAddress است که از رشته کاراکتری "xx@yy.zz" ساخته شده است. این رشته باید در قالب مورد انتظار برای یک آدرس ایمیل باشد؛ در غیر این صورت، یک خطا (exception) صادر میشود. در این حالت، این آدرس در فیلد textBoxResultat (خط 63) به شکلی نسبتاً نامناسب نمایش داده خواهد شد.
- خطوط ۱۳–۱۹: آدرسهای ایمیل دریافتکنندگان در فیلد «To» پیام قرار میگیرند. این آدرسها از فیلد textBoxTo استخراج میشوند. عبارت منظم در خط ۱۳ برای استخراج آدرسهای مختلف که با ویرگول از هم جدا شدهاند، استفاده میشود.
- خطوط ۲۱–۲۶: همین فرایند برای پر کردن فیلد CC پیام با آدرسهای CC از فیلد textBoxCc تکرار میشود.
- خطوط ۲۸–۳۳: همین فرایند برای پر کردن فیلد Bcc پیام با آدرسهای کپی مخفی از فیلد textBoxBcc تکرار میشود.
- خط ۳۵: فیلد «موضوع» پیام بر اساس موضوع موجود در فیلد textBoxSujet تنظیم میشود.
- خط ۳۷: فیلد Body پیام با متن موجود در پیام textBoxMessage مقداردهی اولیه میشود.
- خطوط ۳۹–۴۱: پیوستها به پیام ضمیمه میشوند. هر پیوست به صورت یک شیء `Attachment` به فیلد `Attachments` پیام اضافه میشود. یک شیء `Attachment` با استفاده از مسیر کامل فایل مورد نظر برای پیوست کردن در سیستم فایل محلی ایجاد میشود.
- خط ۴۳: پیام با استفاده از متد Send کلاینت SMTP ارسال میشود.
- خطوط ۴۵–۶۰: خلاصه ارسال در فیلد textBoxResultat نوشته میشود و فرم ریست میشود.
- خط ۶۳: نمایش هرگونه خطا
11.8. یک کلاینت عمومی ناهمزمان TCP
11.8.1. مرور کلی
در تمام مثالهای این فصل، ارتباط کلاینت/سرور در حالت مسدودکننده (که به آن حالت همگام نیز گفته میشود) انجام شد:
- وقتی یک کلاینت به یک سرور متصل میشود، قبل از ادامه کار منتظر پاسخ سرور به آن درخواست میماند.
- وقتی یک کلاینت یک خط متن ارسالشده توسط سرور را میخواند، تا زمانی که سرور آن را ارسال نکرده باشد، مسدود میماند.
- در سمت سرور، نخهای خدماتی که درخواستهای مشتری را پردازش میکنند، به همان شیوهای که در بالا توضیح داده شد عمل میکنند.
در رابطهای کاربری گرافیکی، اغلب لازم است از مسدود شدن کاربر توسط عملیات زمانبر جلوگیری شود. یک مثال رایج، دانلود یک فایل بزرگ است. در حالی که این دانلود در حال انجام است، کاربر باید آزاد باشد تا به تعامل با رابط کاربری گرافیکی ادامه دهد.
در اینجا، پیشنهاد میکنیم که کلاینت عمومی TCP را از بخش 11.6.3 بازنویسی کنیم و تغییرات زیر را اعمال نماییم:
- رابط گرافیکی خواهد بود
- ابزار ارتباط با سرور یک شیء Socket خواهد بود
- ارتباطات ناهمزمان خواهد بود:
- کلاینت ارتباطی را با سرور آغاز میکند اما در حین انتظار برای برقراری آن، مسدود نخواهد ماند
- کلاینت ارسال داده به سرور را آغاز خواهد کرد اما هنگام انتظار برای تکمیل آن مسدود نخواهد ماند
- کلاینت دریافت داده از سرور را آغاز خواهد کرد اما هنگام انتظار برای پایان آن مسدود نخواهد ماند.
بیایید به یاد بیاوریم که شیء ساکت (Socket) در کجای ارتباط کلاینت/سرور TCP قرار دارد:
![]() |
کلاس Socket در سطحی نزدیکترین به شبکه عمل میکند. این کلاس امکان مدیریت دقیق اتصال شبکه را فراهم میآورد. اصطلاح socket به یک پریز برق اشاره دارد. این اصطلاح برای اشاره به یک پریز شبکه نرمافزاری نیز گسترش یافته است. در یک ارتباط TCP-IP بین دو ماشین، A و B، این دو sockets هستند که با یکدیگر ارتباط برقرار میکنند. یک برنامه میتواند مستقیماً با sockets کار کند. این مورد در برنامه A فوق صادق است. یک سوکت میتواند یک سوکت client یا یک سوکت serveur باشد.
11.8.2. رابط کاربری گرافیکی برای کلاینت ناهمزمان TCP
برنامه Visual Studio به شرح زیر است:
![]() |
[ClientTcpAsynchrone.cs] رابط کاربری گرافیکی است. این به شرح زیر است:
![]() |
خیر. | نوع | نام | نقش |
1 | TextBox | textBoxNomServeur | نام سرور TCP برای اتصال |
2 | NumericUpDown | numericUpDownPortServeur | پورت برای اتصال |
3 | RadioButton | radioButtonLF radioButtonRCLF | برای مشخص کردن کاراکتر پایان خطی که کلاینت باید استفاده کند: LF "\n" یا RCLF "\r\n" |
۴ | دکمه | buttonConnexion | برای اتصال به پورت [2] روی سرور [1]. این دکمه زمانی که کلاینت به سرور متصل نیست، برچسب [Connecter] دارد و زمانی که متصل است، برچسب [Déconnecter] را دارد. |
پنج | TextBox | textBoxMsgToServeur | پیامی که پس از برقراری اتصال به سرور ارسال میشود. وقتی کاربر کلید [Entrée] را فشار میدهد، پیام با کاراکتر پایان خط انتخابشده در [3] ارسال میشود. |
۶ | ListBox | listBoxEvts | فهرستی از رویدادهای اصلی در اتصال مشتری/سرور: اتصال، قطع اتصال، پایان جریان، خطاهای ارتباطی |
7 | ListBox | listBoxDialogue | فهرستی که پیامهای دیالوگ کلاینت/سرور را نمایش میدهد |
۸ | دکمه | buttonRazEvts | برای پاک کردن لیست [6] |
۴ | دکمه | buttonRazDialogue | برای پاک کردن لیست [7] |
اصول کاری این رابط به شرح زیر است:
- کاربر با استفاده از [1, 2, 3, 4]، کلاینت گرافیکی TCP خود را به یک سرویس TCP متصل میکند.
- یک نخ ناهمزمان بهطور مداوم تمام دادههای ارسالشده توسط سرور TCP را میپذیرد و آنها را در لیست [7] نمایش میدهد. این نخ مستقل از سایر فعالیتهای رابط است.
- کاربر میتواند با استفاده از [5] پیامها را با سرعت دلخواه خود به سرور ارسال کند. هر پیام از طریق یک نخ ناهمزمان ارسال میشود. برخلاف نخ دریافت که هرگز متوقف نمیشود، نخ ارسال به محض ارسال پیام خاتمه مییابد. برای پیام بعدی از یک نخ ناهمزمان جدید استفاده خواهد شد.
- ارتباط کلاینت/سرور زمانی پایان مییابد که یکی از طرفین اتصال را قطع کند. کاربر میتواند این کار را با استفاده از دکمه [4] آغاز کند، که پس از برقراری اتصال، برچسب [Déconnecter] را دارد.
در اینجا یک تصویر از اجرای برنامه آورده شده است:
![]() |
- در [1]: اتصال به یک سرویس POP
- به [2]: نمایش رویدادهایی که در طول اتصال رخ داده است
- در [3]: پیامی که توسط سرور POP پس از اتمام اتصال ارسال میشود
- در [4]: دکمه [Connecter] به دکمه [Déconnecter] تبدیل شده است
![]() |
- در [1]، فرمان quit به سرور POP ارسال شد. سرور با +OK goodbye پاسخ داد و اتصال را قطع کرد
- در [2]، این قطع اتصال سمت سرور تشخیص داده شد. سپس کلاینت نیز اتصال را از سمت خود بست.
- در [3]، دکمه [Déconnecter] به دکمه [Connecter] بازگشت
11.8.3. اتصال ناهمزمان به سرور
فشردن دکمهی [Connecter] باعث اجرای متد زیر میشود:
private void buttonConnexion_Click(object sender, EventArgs e) {
// ورود یا خروج؟
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- خط ۳: دکمه ممکن است برچسب [Connecter] یا [Déconnecter] داشته باشد.
روش اتصال به شرح زیر است:
using System.Net.Sockets;
...
namespace Chap9 {
public partial class ClientTcp : Form {
const int tailleBuffer = 1024;
private Socket client = null;
private byte[] data = new byte[tailleBuffer];
private string réponse = null;
private string finLigne = "\r\n";
// نمایندگان
public delegate void writeLog(string log);
public ClientTcp() {
InitializeComponent();
}
....................................
private void connexion() {
// بررسی دادهها
string nomServeur = textBoxNomServeur.Text.Trim();
if (nomServeur == "") {
logEvent("indiquez le nom du serveur");
return;
}
// ردیابی
logEvent(String.Format("connexion en cours au serveur {0}", nomServeur));
try {
// ایجاد سوکت
client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// اتصال ناهمزمان
client.BeginConnect(Dns.GetHostEntry(nomServeur).AddressList[0],(int)numericUpDownPortServeur.Value, connecté, client);
} catch (Exception ex) {
logEvent(String.Format("erreur de connexion : {0}", ex.Message));
return;
}
}
//اتصال برقرار شد
private void connecté(IAsyncResult résultat) {
//بازیابی ساکت کلاینت
Socket client = résultat.AsyncState as Socket;
...
}
// ردیابی فرآیند
private void logEvent(string msg) {
....
}
}
}
- خط ۱: کلاس Socket بخشی از فضای نام System.Net.Sockets است.
تعدادی از آیتمهای داده باید بین چندین متد این فرم به اشتراک گذاشته شوند. این موارد به شرح زیر هستند:
- خط ۷: client ساکتی است که برای ارتباط با سرور استفاده میشود
- خطوط ۶ و ۸: کلاینت پیامهای خود را در یک آرایه بایت به نام `data` دریافت خواهد کرد.
- خط ۹: `response` پاسخی است که توسط سرور ارسال میشود.
- خط ۱۰: finLigne نشانگر پایان خط است که توسط کلاینت TCP استفاده میشود – این مقدار بهصورت پیشفرض روی RCLF تنظیم شده است اما کاربر میتواند با استفاده از دکمههای رادیویی [3] آن را تغییر دهد.
روال connexion در خط ۱۹ اتصال به سرور TCP را برقرار میکند:
- خطوط 21–25: یک بررسی انجام میشود تا اطمینان حاصل شود که نام سرور خالی نیست. اگر اینطور نباشد، رویداد در listBoxEvts توسط متد logEvent در خط 49 ثبت میشود.
- خط ۲۷: سیگنالی ارسال میشود که نشان میدهد اتصال در شرف برقراری است
- خط ۳۰: شیء Socket مورد نیاز برای ارتباط TCP/IP ایجاد میشود. سازنده سه پارامتر میگیرد:
- AddressFamily addressFamily: خانواده آدرس (address family) IP کلاینت و سرور؛ در اینجا، آدرسها IPv4 (AddressFamily.InterNetwork) هستند
- SocketType socketType: نوع ساکت. نوع SocketType.Stream برای اتصالات TCP/IP مناسب است
- ProtocolType protocolType: نوع پروتکل اینترنت مورد استفاده؛ در این مورد، پروتکل TCP
- خط ۳۲: اتصال بهصورت ناهمزمان برقرار میشود. اتصال آغاز میشود، اما اجرای برنامه بدون انتظار برای تکمیل آن ادامه مییابد. متد [Socket].BeginConnect چهار پارامتر میگیرد:
- IPAddress ipAddress: آدرس IP ماشینی که سرویس مورد نظر روی آن در حال اجرا است
- Int32 port: پورت سرویس
- AsyncCallBack asyncCallBack: AsyncCallBack یک نوع دِلیگیت است:
متد asyncCallBack که به عنوان سومین پارامتر به متد BeginConnect ارسال میشود، باید متدی باشد که یک نوع IAsyncCallBack را بپذیرد و هیچ نتیجهای بازنگرداند. این همان متدی است که پس از برقرار شدن اتصال، فراخوانی خواهد شد. در اینجا، متد connecté را از خط ۴۱ بهعنوان پارامتر سوم ارسال میکنیم.
- (ادامه)
- شیء state: شیئی که باید به متد asyncCallBack ارسال شود. این متد یک پارامتر `ar` از نوع IAsyncResult را دریافت میکند (به دلهیگ فوق مراجعه کنید). شیء state را میتوان در ar.AsyncState (خط ۴۳) بازیابی کرد. در اینجا، ساکت کلاینت را بهعنوان پارامتر چهارم ارسال میکنیم.
- خط ۳۸: متد به پایان رسید. کاربر میتواند بار دیگر با رابط کاربری گرافیکی تعامل داشته باشد. اتصال در پسزمینه و به موازات رسیدگی به رویدادهای رابط کاربری گرافیکی انجام میشود. همچنین به طور موازی، متد connecté در خط ۴۱ در پایان اتصال، صرفنظر از موفقیت یا شکست آن، فراخوانی خواهد شد.
کد متد connecté به شرح زیر است:
// اتصال برقرار شد
private void connecté(IAsyncResult résultat) {
//بازیابی ساکت کلاینت
Socket client = résultat.AsyncState as Socket;
try {
// عملیات غیرهمزمان تکمیل شد
client.EndConnect(résultat);
// ردیابی
logEvent(String.Format("connecté au service {0}", client.RemoteEndPoint));
//فرم
buttonConnexion.Text = "Déconnecter";
// خواندن ناهمزمان دادهها از سرور
réponse = "";
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
} catch (SocketException e) {
logEvent(String.Format("erreur de connexion : {0}", e.Message));
return;
}
}
//دریافت داده
private void lecture(IAsyncResult résultat) {
// بازیابی ساکت کلاینت
Socket client = résultat.AsyncState as Socket;
...
}
- خط ۴: ساکت (socket) کلاینت از پارامتر résultat که به متد ارسال شده است، بازیابی میشود. توجه داشته باشید که این شیء همان چیزی است که به عنوان پارامتر چهارم به متد BeginConnect ارسال شده است.
- خط ۷: تلاش اتصال توسط متد EndConnect خاتمه داده میشود، که پارامتر résultat دریافتی توسط متد باید به آن ارسال شود.
- خط ۹: رویداد در لیست رویدادها ثبت میشود
- خط ۱۱: دکمه [Connecter] به دکمه [Déconnecter] تبدیل میشود تا کاربر بتواند درخواست خروج را ثبت کند.
- خط ۱۳: پاسخ سرور اولیه میشود. این پاسخ با فراخوانیهای مکرر متد غیرهمزمان BeginReceive بهروزرسانی خواهد شد.
- خط ۱۴: اولین فراخوانی متد غیرهمزمان BeginReceive. این متد با پارامترهای زیر فراخوانی میشود:
- byte[] buffer: بافری که دادههای دریافتی در آن قرار میگیرند – در اینجا، بافر data است
- int offset: موقعیت در بافر که دادههای دریافتی در آن قرار میگیرند – در اینجا آفست 0، c.a.d است، به این معنی که دادهها از اولین بایت بافر قرار میگیرند.
- int size: اندازه بافر به بایت – در اینجا اندازه tailleBuffer است.
- SocketFlags socketFlags: پیکربندی ساکت – در اینجا، هیچ پیکربندی وجود ندارد
- AsyncCallBack asyncCallBack: متد کالبک که باید هنگام اتمام دریافت فراخوانی شود. این اتفاق یا به این دلیل رخ میدهد که بافر دادهای دریافت کرده است یا به این دلیل که اتصال بسته شده است. در اینجا، متد کالبک، متد lecture در خط 22 است.
- شیء state: شیئی که باید به متد فراخوانی asyncCallBack ارسال شود. در اینجا، سوکت کلاینت مجدداً ارسال میشود.
توجه داشته باشید که تمام این موارد بدون هیچگونه اقدام کاربر، به جز درخواست اتصال اولیه از طریق دکمه [Connecter]، انجام میشود. در پایان متد connecté، متد دیگری در پسزمینه اجرا میشود: متد lecture که اکنون آن را بررسی خواهیم کرد.
//دریافت داده
private void lecture(IAsyncResult résultat) {
//بازیابی ساکت کلاینت
Socket client = résultat.AsyncState as Socket;
int nbOctetsReçus = 0;
bool erreur = false;
try {
// تعداد بایتهای دریافتی
nbOctetsReçus = client.EndReceive(résultat);
if (nbOctetsReçus == 0) {
// سرور دیگر پاسخ نمیدهد
logEvent("le serveur a fermé la connexion");
}
} catch (Exception e) {
// یک مشکل دریافت رخ داده است
logEvent(String.Format("erreur de réception : {0}", e.Message));
erreur = true;
}
// تمام شد؟
if (nbOctetsReçus == 0 || erreur) {
// در صورت لزوم، کلاینت قطع میشود
déconnexion();
// پایان پاسخ نمایش داده میشود
afficherRéponseServeur(réponse, true);
//پایان خواندن
return;
}
// بازیابی دادههای دریافتی
string données = Encoding.UTF8.GetString(data, 0, nbOctetsReçus);
// آن را به دادههای دریافتشده اضافه میکند
réponse += données;
// پاسخ را نمایش میدهد
afficherRéponseServeur(réponse, false);
//ادامه خواندن
client.BeginReceive(data, 0, tailleBuffer, SocketFlags.None, lecture, client);
}
- خط ۲: متد lecture در پسزمینه زمانی فراخوانی میشود که بافر data داده دریافت کرده باشد یا اتصال توسط سرور بسته شده باشد.
- خط ۹: درخواست خواندن غیرهمزمان توسط EndReceive خاتمه مییابد. مجدداً، این متد باید با پارامتر دریافتشده توسط تابع فراخوانی (callback) فراخوانی شود. متد EndReceive تعداد بایتهای دریافتشده در بافر خواندن را برمیگرداند.
- خط ۱۰: اگر تعداد بایتها صفر باشد، به این معنی است که اتصال توسط سرور بسته شده است.
- خط ۱۲: رویداد در لیست رویدادها ثبت میشود
- خط ۱۴: هرگونه استثناها مدیریت میشوند
- خطوط ۱۶–۱۷: رویداد در لیست رویدادها ثبت میشود و خطا ثبت میشود
- خط ۲۰: بررسی میکنیم که آیا نیاز به بستن اتصال داریم
- خط ۲۲: اتصال در سمت کلاینت با استفاده از متد déconnexion بسته میشود، که بعداً به آن خواهیم پرداخت.
- خط ۲۴: پاسخ سرور، c.a.d. متغیر سراسری réponse با استفاده از متد خصوصی afficherRéponseServeur در لیست دیالوگ listBoxDialogue نمایش داده میشود.
- خط ۲۶: پایان متد غیرهمزمان lecture
- خط ۲۹: بایتهای دریافتی در یک رشته کاراکتری با فرمت UTF8 قرار میگیرند.
- خط ۳۱: آنها به پاسخ در حال ساخت اضافه میشوند
- خط ۳۳: پاسخ در لیست listBoxDialogue نمایش داده میشود.
- خط ۳۵: برنامه دوباره منتظر دریافت داده از سرور میماند
در نهایت، متد غیرهمزمان lecture هرگز متوقف نمیشود. این متد به طور مداوم دادهها را از سرور میخواند و آنها را در لیست listBoxDialogue نمایش میدهد. این متد تنها زمانی متوقف میشود که اتصال، چه توسط سرور و چه توسط خود کاربر، بسته شود.
11.8.4. قطع اتصال از سرور
فشردن دکمهی [Déconnecter] اجرای متد زیر را آغاز میکند:
private void buttonConnexion_Click(object sender, EventArgs e) {
// اتصال یا قطع؟
if (buttonConnexion.Text == "Déconnecter")
déconnexion();
else
connexion();
}
- خط ۳: دکمه ممکن است برچسب [Connecter] یا [Déconnecter] داشته باشد.
روش déconnexion کاربر را از سیستم خارج میکند:
private void déconnexion() {
// بستن ساکت
if (client != null && client.Connected) {
try {
// ردیابی
logEvent(String.Format("déconnexion du service {0}", client.RemoteEndPoint));
//قطع اتصال
client.Shutdown(SocketShutdown.Both);
client.Close();
// فرم
buttonConnexion.Text = "Connecter";
} catch (Exception ex) {
// ردیابی
logEvent(String.Format("erreur de lors de la déconnexion : {0}", ex.Message));
}
}
}
- خط ۳: اگر کلاینت وجود داشته باشد و متصل باشد
- خط ۶: قطع اتصال در listBoxEvts اعلام میشود. خاصیت client.RemoteEndPoint جفت (آدرس IP، پورت) سمت دیگر اتصال را برمیگرداند؛ در این مورد، c.a.d به سرور اشاره دارد.
- خط ۸: جریان داده ساکت با استفاده از متد ShutDown بسته میشود. جریان داده یک ساکت دوطرفه است: ساکت دادهها را ارسال و دریافت میکند. پارامتر برای متد ShutDown میتواند یکی از موارد زیر باشد: ShutDown.Receive برای بستن جریان دریافت، Shutdonw.Send برای بستن جریان ارسال، یا ShutDown.Both برای بستن هر دو جریان.
- خط ۹: منابع مرتبط با ساکت آزاد میشوند
- خط ۱۱: دکمه [Déconnecter] به دکمه [Connecter] تبدیل میشود
- خطوط ۱۲–۱۵: رسیدگی به هرگونه استثنا
11.8.5. انتقال داده ناهمزمان به سرور
وقتی کاربر پیام را در فیلد textBoxMsgToServeur ارسال میکند، متد زیر اجرا میشود:
private void textBoxMsgToServeur_KeyPress(object sender, KeyPressEventArgs e) {
// دکمه [Entrée] ؟
if (e.KeyChar == 13 && client.Connected) {
envoyerMessage();
}
}
- خطوط ۳–۵: اگر کاربر کلید [Entrée] را فشرده باشد و ساکت کلاینت متصل باشد، پیام در فیلد textBoxMsgToServeur با استفاده از متد envoyerMessage ارسال میشود.
روش envoyerMessage به شرح زیر است:
private void envoyerMessage() {
//ارسال پیام بهصورت غیرهمزمان
// پیام
byte[] message = Encoding.UTF8.GetBytes(textBoxMsgToServeur.Text.Trim() + finLigne);
// ارسال میشود
client.BeginSend(message, 0, message.Length, SocketFlags.None, écriture, client);
// گفتگو
logDialogue("--> " + textBoxMsgToServeur.Text.Trim());
// بازنشانی پیام
textBoxMsgToServeur.Clear();
}
- خط ۴: کاراکتر پایان خط کلاینت به پیام اضافه شده و در آرایه بایت message قرار میگیرد.
- خط ۶: یک انتقال ناهمزمان با استفاده از روش BeginSend آغاز میشود. پارامترهای BeginSend با پارامترهای روش BeginReceive یکسان هستند. پس از اتمام عملیات ارسال پیام ناهمزمان، روش écriture فراخوانی خواهد شد.
- خط ۸: پیام ارسالشده به لیست listBoxDialogue اضافه میشود تا امکان ردیابی دیالوگ کلاینت/سرور فراهم گردد
- خط ۱۰: پیام ارسالشده از رابط کاربری گرافیکی حذف میشود
متد فراخوانی écriture به شرح زیر است:
private void écriture(IAsyncResult résultat) {
//نتیجه ارسال یک پیام
Socket client = résultat.AsyncState as Socket;
try {
client.EndSend(résultat);
} catch (Exception e) {
// در ارسال پیام مشکلی پیش آمد
logEvent(String.Format("erreur d'émission : {0}", e.Message));
}
}
- خط ۴: متد فراخوانی écriture یک پارامتر نتیجه از نوع IAsyncResult دریافت میکند.
- خط ۳: ساکت کلاینت از پارامتر résultat بازیابی میشود. این ساکت پنجمین پارامتر متد BeginSend بود.
- خط ۵: عملیات انتقال ناهمزمان تکمیل میشود.
ما منتظر پایان ارسال پیام نمیمانیم و کنترل را به کاربر بازمیگردانیم. بنابراین کاربر میتواند در حالی که پیام اول هنوز در حال ارسال است، پیام دوم را ارسال کند.
11.8.6. نمایش رویدادها و دیالوگ کلاینت/سرور
رویدادها توسط متد logEvents نمایش داده میشوند:
// ردیابی فرآیند
private void logEvent(string msg) {
listBoxEvts.Invoke(new writeLog(logEventCallBack), msg);
}
private void logEventCallBack(string msg) {
// نمایش پیام
msg = msg.Replace(finLigne, " ");
listBoxEvts.Items.Insert(0, String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
- خط ۲: متد logEvents پیامی را که باید به لیست listBoxEvts اضافه شود، بهعنوان پارامتر میپذیرد.
- خط ۳: کامپوننت listBoxEvents را نمیتوان مستقیماً استفاده کرد. این به این دلیل است که متد logEvents توسط دو نوع نخ فراخوانی میشود:
- رشته اصلی که مالک رابط کاربری گرافیکی است، برای مثال زمانی که سیگنال میدهد که یک تلاش اتصال در حال انجام است
- یک نخ ثانویه که یک عملیات ناهمزمان را انجام میدهد. این نوع نخ مالک کامپوننتها نیست و دسترسی آن به یک کامپوننت C باید توسط یک عملیات C.Invoke کنترل شود. این عملیات به کنترل C اطلاع میدهد که نخی مایل است عملیاتی را روی آن انجام دهد. متد Invoke دو پارامتر میگیرد:
- یک تابع فراخوانی (callback) از نوع delegate. این تابع فراخوانی توسط رشتهای که مالک رابط کاربری گرافیکی است اجرا میشود، نه توسط رشتهای که متد C.Invoke را اجرا میکند.
- یک شیء که باید به تابع فراخوانی پاس داده شود.
در اینجا، اولین پارامتر ارسالشده به متد Invoke یک نمونه از دِلیگیت زیر است:
public delegate void writeLog(string log);
دلیگیت writeLog دارای پارامتر از نوع string است و هیچ نتیجهای بر نمیگرداند. این پارامتر، پیامی خواهد بود که باید در listBoxEvts ذخیره شود.
در خط ۳، اولین پارامتر ارسالشده به متد Invoke، متد logEventCallBack از خط ۶ است. این در واقع با امضای دلیگیت writeLog مطابقت دارد. پارامتر دوم ارسالشده به متد Invoke، پیامی است که بهعنوان پارامتر به متد logEventCallBack ارسال خواهد شد.
عملیات Invoke یک عملیات همگام است. اجرای نخ ثانویه تا زمانی که نخ مالک کنترل، متد کالبک را اجرا نکند، مسدود میشود.
- خط ۶: متد کالبک اجراشده توسط نخ GUI پیام نمایشدادهشده در کنترل listBoxEvts را دریافت میکند.
- خط ۹: رویداد در بالای لیست ثبت میشود تا جدیدترین رویدادها در بالای لیست نمایش داده شوند.
پیامهای دیالوگ کلاینت/سرور توسط متد logDialogue نمایش داده میشوند:
// ردیابی دیالوگ
private void logDialogue(string msg) {
listBoxDialogue.Invoke(new writeLog(logDialogueCallBack), msg);
}
private void logDialogueCallBack(string msg) {
//نمایش پیام
msg = msg.Replace(finLigne, " ");
listBoxDialogue.Items.Add(String.Format("{0:hh:mm:ss} : {1}", DateTime.Now, msg));
}
اصل کار همانند روش logEvent است.
پیامهای دریافتی توسط کلاینت با استفاده از روش afficherRéponseServeur نمایش داده میشوند:
private void afficherRéponseServeur(String msg, bool dernièreLigne) {
...
}
پارامتر اول پیامی است که باید نمایش داده شود. این پیام ممکن است از مجموعهای از خطوط تشکیل شده باشد. دلیل آن این است که کلاینت دادهها را از سرور در بلوکهایی به اندازه tailleBuffer (1024) بایت میخواند. در این 1024 بایت ممکن است خطوط مختلفی وجود داشته باشد که با کاراکتر پایان خط «\n» قابل شناسایی هستند. ممکن است آخرین خط ناقص باشد و کاراکتر پایان خط آن در میان ۱۰۲۴ بایت بعدی ظاهر شود. این متد خطوطی را که در پیام با «\n» پایان مییابند شناسایی کرده و سپس به logDialogue دستور میدهد آنها را نمایش دهد. پارامتر دوم این متد مشخص میکند که آیا آخرین خط یافتشده نمایش داده شود یا برای تکمیل شدن توسط پیام بعدی در بافر باقی بماند. این کد نسبتاً پیچیده است و در اینجا مرتبط نیست؛ بنابراین، در مورد آن توضیح داده نخواهد شد.
11.8.7. نتیجهگیری
همین مثال را میتوان با استفاده از عملیات همگام نیز پیادهسازی کرد. در اینجا، ماهیت غیرهمگام رابط کاربری گرافیکی فایده چندانی برای کاربر ندارد. با این حال، اگر کاربر متصل شود و سپس متوجه شود که سرور «دیگر پاسخ نمیدهد»، میتواند از سیستم خارج شود زیرا رابط گرافیکی در حین اجرای عملیات غیرهمزمان، به رویدادها پاسخ دادن را ادامه میدهد. این مثال نسبتاً پیچیده به ما این امکان را داد که برخی مفاهیم جدید را معرفی کنیم:
- استفاده از ساکتها
- استفاده از روشهای ناهمزمان. آنچه پوشش دادیم بخشی از یک استاندارد است. روشهای ناهمزمان دیگری نیز وجود دارند و بر اساس همین اصل عمل میکنند.
- بهروزرسانی کنترلهای رابط کاربری گرافیکی از طریق نخهای پسزمینه.
ارتباطات ناهمزمان TCP/IP مزایای بسیار مهمتری برای یک سرور نسبت به آنچه در مثال قبلی نشان داده شد، ارائه میدهد. میدانیم که سرور با استفاده از نخهای پسزمینه به کلاینتهای خود خدمترسانی میکند. اگر استخر نخ آن شامل N نخ باشد، این بدان معناست که فقط میتواند به طور همزمان به N کلاینت خدمترسانی کند. اگر تمام N نخ در حال انجام یک عملیات مسدودکننده (همزمان) باشند، هیچ نخی برای یک مشتری جدید در دسترس نخواهد بود تا اینکه یکی از عملیاتهای مسدودکننده به پایان برسد و یک نخ را آزاد کند. اگر به جای عملیات همزمان، عملیات ناهمزمان روی نخها انجام شود، هیچ نخی هرگز مسدود نمیشود و میتواند به سرعت برای مشتریان جدید دوباره استفاده شود.
11.9. مثال کاربردی، نسخه ۸: سرور محاسبه مالیات
11.9.1. معماری نسخه جدید
ما در حال بازنگری برنامه محاسبه مالیات هستیم که پیش از این در اشکال مختلف مورد بررسی قرار گرفته است. بیایید آخرین نسخه آن، یعنی نسخه ۷ را در پاراگراف ۹.۸ به یاد آوریم.
![]() |
دادهها در یک پایگاه داده ذخیره میشدند و لایه [ui] یک رابط کاربری گرافیکی بود:
![]() |
ما قصد داریم این معماری را مجدداً استفاده کرده و آن را در دو ماشین توزیع کنیم:
![]() |
- یک ماشین، [serveur]، میزبان لایههای [metier] و [dao] از نسخه ۷ خواهد بود. یک لایه TCP/IP ([serveur] و [1]) راهاندازی خواهد شد تا به کلاینتهای اینترنت امکان دهد از سرویس محاسبه مالیات استعلام بگیرند.
- یک ماشین، [client]، میزبان لایه [ui] نسخه ۷ خواهد بود. یک لایه TCP/IP ([client], [2]) ساخته خواهد شد تا لایه [ui] را قادر به استعلام از سرویس محاسبه مالیات سازد.
معماری در اینجا بهطور قابلتوجهی تغییر میکند. نسخهٔ ۷ یک برنامهٔ ویندوزی تککاربره بود. نسخهٔ ۸ به یک برنامهٔ کلاینت/سرور تبدیل میشود که در اینترنت اجرا میشود. سرور قادر خواهد بود بهطور همزمان به چندین کلاینت خدمترسانی کند.
ابتدا بخش [serveur] برنامه را مینویسیم.
11.9.2. سرور محاسبه مالیات
11.9.2.1. پروژه ویژوال استودیو
![]() |
پروژه ویژوال استودیو به شرح زیر خواهد بود:
![]() |
- در [1]، پروژه. این شامل اجزای زیر است:
- [ServeurImpot.cs]: سرور TCP/IP برای محاسبه مالیات، به صورت یک برنامه کنسول.
- [dbimpots.sdf]: پایگاه دادهٔ فشردهٔ سرور SQL (نسخهٔ ۷)، همانطور که در بخش 9.8.5 توضیح داده شده است.
- [App.config]: فایل پیکربندی برنامه.
- در [2]، پوشه [lib] شامل فایلهای DLL مورد نیاز برای پروژه است:
- [ImpotsV7-dao]: لایه [dao] برای نسخه ۷
- [ImpotsV7-metier]: لایه [metier] برای نسخه ۷
- [antlr.runtime, CommonLogging, Spring.Core] برای Spring
- در [3]، پروژه ارجاع میدهد به
11.9.2.2. پیکربندی برنامه
فایل [App.config] توسط Spring استفاده میشود. محتوای آن به شرح زیر است:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="dao" type="Dao.DataBaseImpot, ImpotsV7-dao">
<constructor-arg index="0" value="System.Data.SqlServerCe.3.5"/>
<constructor-arg index="1" value="Data Source=|DataDirectory|\dbimpots.sdf;" />
<constructor-arg index="2" value="select data1, data2, data3 from data"/>
</object>
<object name="metier" type="Metier.ImpotMetier, ImpotsV7-metier">
<constructor-arg index="0" ref="dao"/>
</object>
</objects>
</spring>
</configuration>
- خطوط ۱۶–۲۰: پیکربندی لایه [dao] مرتبط با پایگاه داده Server Compact SQL
- خطوط ۲۱–۲۳: پیکربندی لایه [metier].
این فایل پیکربندی است که در لایه [ui] نسخه ۷ استفاده میشود. در بخش ۹.۸.۴ توضیح داده شده است.
11.9.2.3. عملکرد سرور
هنگام راهاندازی سرور، برنامهٔ سرور لایههای [metier] و [dao] را ایجاد میکند و سپس رابط کنسول مدیریت را نمایش میدهد:
کنسول مدیریت دستورات زیر را میپذیرد:
برای راهاندازی سرویس روی پورت مشخصشده | |
برای متوقف کردن سرویس. سپس میتوان آن را روی همان پورت یا پورت دیگری مجدداً راهاندازی کرد. | |
برای فعال کردن اکو دیالوگ کلاینت/سرور در کنسول | |
برای غیرفعال کردن اکو | |
برای نمایش وضعیت فعال/غیرفعال سرویس | |
برای خروج از برنامه |
بیایید سرور را راهاندازی کنیم:
حال بیایید مشتری گرافیکی ناهمزمان TCP را که پیشتر در بخش 11.8 مورد بحث قرار گرفت، اجرا کنیم.

کلاینت متصل است. میتواند دستورات زیر را به سرور محاسبه مالیات ارسال کند:
برای دریافت فهرست دستورات مجاز | |
برای محاسبه مالیات برای فردی با nbEnfants فرزند و حقوقی به مبلغ salaireAnnuel یورو. marié عدد o است اگر فرد متأهل باشد، و n در غیر این صورت. | |
برای بستن اتصال به سرور |
در اینجا مثالی از یک گفتگوی دیالوگی آورده شده است:
![]() |
در سمت سرور، کنسول موارد زیر را نمایش میدهد:
بیایید اکو را فعال کرده و یک گفتگوی جدید را از سمت کلاینت گرافیکی آغاز کنیم:
![]() |
سپس کنسول مدیریت موارد زیر را نمایش میدهد:
- خط ۱: اکو دیالوگ کلاینت/سرور فعال است
- خط ۲: یک کلاینت وارد شده است
- خط ۳: دستور [aide] ارسال شده است
- خطوط ۴–۷: پاسخ سرور، شامل ۴ خط.
بیایید سرویس را متوقف کنیم:
- خط ۱: ما درخواست توقف سرویس را داریم (نه خود برنامه)
- خط ۲: یک استثنا رخ داده است زیرا سرور، که در حین انتظار برای یک کلاینت مسدود شده بود، به دلیل خاموش شدن سرویس گوشدادن به طور ناگهانی متوقف شد.
- خط ۳: اکنون میتوان سرویس را با استفاده از `start port` مجدداً راهاندازی کرد یا با استفاده از `quit` متوقف ساخت.
قبل از متوقف شدن سرویس گوشدادن، یک کلاینت از طریق یک اتصال دیگر در حال سرویسدهی بود. این اتصال هنگام بستهشدن سوکت گوشدادن بسته نمیشود. کلاینت میتواند به ارسال دستورات ادامه دهد: نخ سرویس که قبل از متوقف شدن سرویس گوشدادن به آن اختصاص داده شده بود، به پاسخدادن به آن ادامه میدهد:

11.9.3. کد سرور TCP برای محاسبه مالیات
![]() |
1 ![]() |
کد سرور [ServeurImpot.cs] به شرح زیر است:
...
namespace Chap9 {
public class ServeurImpot {
//دادهها بین نخها و متدها مشترک هستند
private static IImpotMetier metier = null;
private static int port;
private static TcpListener service;
private static bool actif = false;
private static bool echo = false;
// برنامه اصلی
public static void Main(string[] args) {
// نمونه سازی لایهها [metier] و [dao]
IApplicationContext ctx = null;
metier = null;
try {
// زمینهٔ Spring
ctx = ContextRegistry.GetContext();
// یک مرجع در لایه [metier] درخواست شده است
metier = (IImpotMetier)ctx.GetObject("metier");
//پیکربندی استخر نخ
ThreadPool.SetMinThreads(10, 10);
ThreadPool.SetMaxThreads(10, 10);
// دستورات مدیریت سرور را که از طریق صفحهکلید وارد میشوند در یک حلقه بینهایت میخواند
string commande = null;
string[] champs = null;
while (true) {
// دستور
Console.Write("Serveur de calcul d'impôt >");
// خواندن فرمان
commande = Console.ReadLine().Trim().ToLower();
champs = Regex.Split(commande, @"\s+");
// اجرای فرمان
switch (champs[0]) {
case "start":
//فعال؟
if (actif) {
//خطا
Console.WriteLine("Le serveur est déjà actif");
} else {
// بررسی پورت
if (champs.Length != 2 || !int.TryParse(champs[1], out port) || port <= 0) {
Console.WriteLine("Syntaxe : start port. Port incorrect");
} else {
// راهاندازی سرویس گوش دادن
ThreadPool.QueueUserWorkItem(doEcoute, null);
}
}
break;
case "echo":
// اکو شروع / توقف
if (champs.Length != 2 || (champs[1] != "start" && champs[1] != "stop")) {
Console.WriteLine("Syntaxe : echo start / stop");
} else {
echo = champs[1] == "start";
}
break;
case "stop":
// سرویس پایان یافت
if (actif) {
service.Stop();
actif = false;
}
break;
case "status":
//وضعیت سرور
if (actif) {
Console.WriteLine("Le service est lancé sur le port {0}", port);
} else {
Console.WriteLine("Le service n'est pas lancé}");
}
break;
case "quit":
//خروج از برنامه
Console.WriteLine("Fin du service");
Environment.Exit(0);
break;
default:
// دستور نادرست
Console.WriteLine("Commande incorrecte. Utilisez (start,stop,echo, status, quit)");
break;
}
}
} catch (Exception e1) {
// استثناء نمایش داده شد
Console.WriteLine("L'erreur suivante s'est produite à l'initialisation de l'application : {0}", e1.Message);
return;
}
}
private static void doEcoute(Object data) {
...
}
....
}
}
- خطوط ۱۸–۲۱: لایههای [metier] و [dao] توسط Spring ایجاد نمونه میشوند که توسط [App.config] پیکربندی شده است. سپس متغیر سراسری metier در خط ۶ مقداردهی اولیه میشود.
- خطوط ۲۴–۲۵: استخر نخهای برنامه با حداقل و حداکثر ۱۰ نخ پیکربندی میشود.
- خطوط ۳۰–۸۶: حلقهٔ مربوط به پردازش دستورات مدیریت سرویس (start, stop, quit, echo, status).
- خط ۳۲: فراخوانی سرور برای هر فرمان جدید
- خط ۳۴: فرمان مدیر را میخواند
- خط ۳۵: فرمان به فیلدها برای تجزیه و تحلیل تقسیم میشود
- خطوط ۳۸–۵۲: فرمان «start port» که برای راهاندازی سرویس گوشدادن استفاده میشود
- خط ۴۰: اگر سرویس از قبل در حال اجرا باشد، نیازی به اقدام نیست
- خط ۴۵: بررسی میکنیم که پورت وجود دارد و صحیح است. در صورت صحت، متغیر سراسری port در خط ۷ مقداردهی میشود.
- خط ۴۹: سرویس گوش دادن توسط یک نخ ثانویه مدیریت میشود تا نخ اصلی بتواند به اجرای دستورات کنسول ادامه دهد. اگر متد doEcoute به موفقیت آمیز اتصال را برقرار کند، متغیرهای سراسری service در خط ۸ و actif در خط ۹ مقداردهی اولیه میشوند.
- خطوط ۵۳–۶۰: دستور `echo start / stop` که بازتاب دیالوگ کلاینت/سرور را در کنسول فعال یا غیرفعال میکند
- خط ۵۸: متغیر سراسری echo از خط ۷ مقداردهی میشود
- خطوط ۶۱–۶۷: فرمان stop که سرویس گوشدادن را متوقف میکند.
- خط ۶۴: سرویس گوشدادن متوقف میشود
- خطوط ۶۸–۷۵: دستور status که وضعیت فعال/غیرفعال سرویس را نمایش میدهد
- خطوط ۷۶–۸۰: فرمان quit که همه چیز را متوقف میکند.
رشتهای که مسئول شنود درخواستهای مشتری است، روش زیر را اجرا میکند: doEcoute:
private static void doEcoute(Object data) {
// رشته در حال گوش دادن به درخواستهای مشتری
try {
// سرویس در حال ایجاد است
service = new TcpListener(IPAddress.Any, port);
// راهاندازی آن
service.Start();
// سرور فعال است
actif = true;
// نظارت
Console.WriteLine("Serveur de calcul d'impôt lancé sur le port {0}", port);
//حلقه سرویس مشتری
TcpClient tcpClient = null;
//شماره مشتری
int numClient = 0;
//حلقه بینهایت
while (true) {
// در انتظار مشتری
tcpClient = service.AcceptTcpClient();
// خدمات توسط وظیفه دیگری انجام میشود
ThreadPool.QueueUserWorkItem(doService, new Client() { CanalTcp = tcpClient, NumClient = numClient });
//مشتری بعدی
numClient++;
}
} catch (Exception ex) {
// خطا گزارش شد
Console.WriteLine("L'erreur suivante s'est produite sur le serveur : {0}", ex.Message);
}
}
// اطلاعات مشتری
internal class Client {
public TcpClient CanalTcp { get; set; } // اتصال با مشتری
public int NumClient { get; set; } //شماره مشتری
}
این کد مشابه کد سرور echo مورد بحث در بخش 11.6.1 است. ما تنها به تفاوتها اشاره خواهیم کرد:
- خط ۷: خط تماس پشتیبانی راهاندازی شده است
- خط ۹: ذکر شده است که سرویس اکنون فعال است
خط ۲۱: مشتریان توسط نخهای خدماتی که روش زیر را اجرا میکنند، doService، خدمترسانی میشوند:
private static void doService(Object infos) {
//بازیابی مشتری برای ارائه خدمات
Client client = infos as Client;
// به مشتری خدمترسانی میکند
Console.WriteLine("Début du service au client {0}", client.NumClient);
//پردازش اتصال TcpClient
try {
using (TcpClient tcpClient = client.CanalTcp) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
//ارسال پیام خوشآمدگویی به مشتری
writer.WriteLine("Bienvenue sur le serveur de calcul de l'impôt");
//حلقهٔ خواندن درخواست/پاسخ نوشتن
string demande = null;
bool serviceFini = false;
while (!serviceFini && (demande = reader.ReadLine()) != null) {
// نظارت کنسول
if (echo) {
Console.WriteLine("<--- Client {0} : {1}", client.NumClient, demande);
}
// تحلیل درخواست
demande = demande.Trim().ToLower();
// درخواست خالی؟
if (demande.Length == 0) {
// درخواست نامعتبر
writeClient(writer,client.NumClient,"Commande non reconnue. Utilisez la commande aide.");
return;
}
// تفکیک درخواست به فیلدها
string[] champs = Regex.Split(demande, @"\s+");
// تحلیل
switch (champs[0].ToLower()) {
case "aide":
writeClient(writer, client.NumClient, "Commandes acceptées\n1-aide\n2-impot marié(O/N) nbEnfants salaireAnnuel\n3-aurevoir");
break;
case "impot":
// محاسبه مالیات
writeClient(writer, client.NumClient, calculImpot(writer, client.NumClient, champs));
break;
case "aurevoir":
serviceFini = true;
writeClient(writer, client.NumClient, "Au revoir...");
break;
default:
writeClient(writer, client.NumClient, "Commande non reconnue. Utilisez la commande aide.");
break;
}
}
}
}
}
}
} catch (Exception e) {
// خطا
Console.WriteLine("L'erreur suivante s'est produite lors du service au client {0} : {1}", client.NumClient, e.Message);
} finally {
Console.WriteLine("Fin du service au client {0}", client.NumClient);
}
}
private static void writeClient(StreamWriter writer, int numClient, string message) {
//خروجی کنسول؟
if (echo) {
Console.WriteLine("---> Client {0} : {1}", numClient, message);
}
// ارسال پیام به مشتری
writer.WriteLine(message);
}
بار دیگر، این کد مشابه کد سرور اکو مورد بحث در بخش 11.6.1 است. ما تنها به تفاوتها اشاره خواهیم کرد:
- خط ۱۵: پس از اتصال مشتری، سرور برای او پیامی خوشآمدگویی ارسال میکند.
- خطوط ۱۹–۵۲: حلقهٔ خواندن دستورات مشتری. این حلقه زمانی متوقف میشود که مشتری دستور «aurevoir» را ارسال کند.
- خط ۲۷: رسیدگی به یک فرمان خالی
- خط ۳۴: درخواست به فیلدها برای تجزیه و تحلیل تقسیم میشود
- خط ۳۷: فرمان aide: کلاینت فهرستی از فرمانهای مجاز را درخواست میکند
- خط ۴۰: فرمان impot: کلاینت درخواست محاسبه مالیات میکند. پاسخی ارسال میشود که حاوی پیامی است که توسط متد calculImpot بازگردانده شده است، که به زودی به تفصیل آن خواهیم پرداخت.
- خط ۴۴: فرمان aurevoir: کلاینت اعلام میکند که کارش تمام شده است.
- خط ۴۵: ما برای خروج از حلقه خواندن درخواستهای مشتری (خطوط ۱۹–۵۲) آماده میشویم.
- خط ۴۶: ما با یک پیام خداحافظی به مشتری پاسخ میدهیم
- خط ۴۸: یک فرمان نامعتبر. یک پیام خطا برای مشتری ارسال میشود.
پردازش دستور impot توسط روش زیر، calculImpot، انجام میشود:
private static string calculImpot(StreamWriter writer, int numClient, string[] champs) {
//درخواست محاسبه: متأهل (بله/خیر) nbEnfants salaireAnnuel
// ۴ فیلد مورد نیاز
if (champs.Length != 4) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// fields [1]
string marié = champs[1];
if (marié != "o" && marié != "n") {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// fields [2]
int nbEnfants;
if (!int.TryParse(champs[2], out nbEnfants)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// fields [3]
int salaireAnnuel;
if (!int.TryParse(champs[3], out salaireAnnuel)) {
return "Commande calcul incorrecte. Utilisez la commande aide.";
}
// مشکلی نیست – ما مالیات را محاسبه خواهیم کرد
int impot = 0;
try {
impot = metier.CalculerImpot(marié == "o", nbEnfants, salaireAnnuel);
return impot.ToString();
} catch (Exception ex) {
return ex.Message;
}
}
- خط ۱: متد به عنوان سومین پارامتر، آرایه فیلدها را از فرمان impot دریافت میکند. اگر این آرایه به درستی فرموله شده باشد، به شکل زیر است: income married nbEnfants salaireAnnuel. متد پاسخ ارسالی به کلاینت را برمیگرداند.
- خط ۴: بررسی میکنیم که فرمان ۴ فیلد دارد
- خط ۸: بررسی میکنیم که فیلد marié معتبر است
- خط ۱۴: بررسی میکنیم که فیلد nbEnfants معتبر است
- خط ۱۹: بررسی میکند که فیلد salaireAnnuel معتبر است
- خط 25: مالیات با استفاده از روش CalculerImpot از لایه [metier] محاسبه میشود. توجه داشته باشید که این لایه در یک DLL جای گرفته است.
- خط ۲۶: اگر لایه [metier] نتیجهای برگردانده باشد، این نتیجه به کلاینت بازگردانده میشود.
- خط ۲۸: اگر لایه [metier] یک استثنا پرتاب کرده باشد، پیام استثنا به کلاینت بازگردانده میشود.
11.9.4. کلاینت گرافیکی برای سرور TCP محاسبه مالیات
11.9.4.1. پروژه ویژوال استودیو « »
![]() |
پروژه ویژوال استودیو برای کلاینت گرافیکی به شرح زیر خواهد بود:
![]() |
- در [1]، دو پروژه در راهحل وجود دارد – یکی برای هر یک از دو لایهٔ برنامه
- در [2]، کلاینت TCP که بهعنوان لایه [metier] برای لایه [ui] عمل میکند. بنابراین از هر دو اصطلاح استفاده خواهیم کرد.
- در [3]، لایه [ui] از نسخه ۷، با یک تفاوت جزئی که به آن خواهیم پرداخت
11.9.4.2. لایه [metier]
رابط IImpotMetier تغییر نکرده است. این رابط همچنان مانند نسخهٔ ۷ است:
namespace Metier {
public interface IImpotMetier {
int CalculerImpot(bool marié, int nbEnfants, int salaire);
}
}
پیادهسازی این رابط، کلاس زیر است: [ImpotMetierTcp]:
using System.Net.Sockets;
using System.IO;
namespace Metier {
public class ImpotMetierTcp : IImpotMetier {
// اطلاعات [serveur]
private string Serveur { get; set; }
private int Port { get; set; }
//محاسبه مالیات
public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
//اتصال به سرویس
using (TcpClient tcpClient = new TcpClient(Serveur, Port)) {
using (NetworkStream networkStream = tcpClient.GetStream()) {
using (StreamReader reader = new StreamReader(networkStream)) {
using (StreamWriter writer = new StreamWriter(networkStream)) {
//جریان خروجی بدون بافر
writer.AutoFlush = true;
// نادیده گرفتن پیام خوشآمدگویی
reader.ReadLine();
// درخواست
writer.WriteLine(string.Format("impot {0} {1} {2}",marié ? "o" : "n",nbEnfants, salaire));
// پاسخ
return int.Parse(reader.ReadLine());
}
}
}
}
}
}
}
- خط ۷: نام یا آدرس IP سرور محاسبه مالیات TCP
- خط ۸: پورت گوشدادن این سرور
- این دو ویژگی توسط Spring هنگام نمونهسازی کلاس [ImpotMetierTcp] مقداردهی اولیه میشوند.
- خط ۱۱: روش محاسبه مالیات. هنگامی که این متد اجرا میشود، ویژگیهای Serveur و Port از قبل مقداردهی شدهاند. این کد رویکرد استاندارد برای یک کلاینت TCP را دنبال میکند.
- خط ۱۳: اتصال به سرور باز میشود
- خطوط ۱۴–۱۶: جریان شبکه مرتبط با این اتصال بازیابی میشود (خط ۱۴)، که از آن یک جریان خواندن (خط ۱۵) و یک جریان نوشتن (خط ۱۶) مشتق میشوند.
- خط ۱۸: جریان نوشتن باید بدون بافر باشد
- خط ۲۰: در اینجا، مهم است به یاد داشته باشیم که وقتی اتصال برقرار میشود، سرور یک خط اول حاوی پیام خوشآمدگویی «خوش آمدید به سرور محاسبه مالیات» را برای کلاینت ارسال میکند. این پیام خوانده شده و نادیده گرفته میشود.
- خط ۲۲: یک دستور به شکل «impot o 2 60000» به سرور ارسال میشود تا از آن بخواهد مالیات یک فرد متأهل با دو فرزند و حقوق سالانه ۶۰۰۰۰ یورو را محاسبه کند.
- خط ۲۴: سرور با مقدار مالیات به فرمت «4282» پاسخ میدهد، یا در صورت نادرست بودن دستور (که در اینجا رخ نخواهد داد) یا بروز مشکل در حین محاسبه مالیات، با یک پیام خطا. در اینجا مورد دوم مدیریت نمیشود، اما قطعاً مرتبتر بود اگر این کار انجام میشد. در واقع، اگر خط خواندهشده یک پیام خطا باشد، یک استثنا پرتاب خواهد شد زیرا تبدیل به عدد صحیح شکست خواهد خورد. استثنایی که توسط رابط کاربری گرافیکی گرفته میشود یک خطای تبدیل خواهد بود، در حالی که استثنای اصلی ماهیتی کاملاً متفاوت دارد. از خواننده دعوت میشود این کد را بهبود بخشد.
- خطوط ۲۵–۲۸: آزادسازی تمام منابع مورد استفاده با یک عبارت «using».
لایه [metier] به DLL و ImpotsV8-metier.dll کامپایل میشود:

11.9.4.3. لایه [ui]
![]() |
لایه [ui] [1,3] همان لایهای است که در نسخه ۷، بخش ۹.۸.۴ مورد بحث قرار گرفته است، با سه تفاوت:
- پیکربندی لایه [metier] در [App.config] متفاوت است زیرا پیادهسازی آن تغییر کرده است
- رابط کاربری گرافیکی [Form1.cs] برای نمایش هرگونه استثنا اصلاح شده است
- لایه [metier] در داخل لایههای DLL و [ImpotsV8-metier.dll] قرار دارد.
فایل [App.config] به شرح زیر است:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
</configSections>
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object name="metier" type="Metier.ImpotMetierTcp, ImpotsV8-metier">
<property name="Serveur" value="localhost"/>
<property name="Port" value="27"/>
</object>
</objects>
</spring>
</configuration>
- خط ۱۶: نمونهسازی لایه [metier] با استفاده از کلاس Metier.ImpotMetierTcp از DLL ImpotsV8-metier.dll
- خطوط 17–18: ویژگیهای Server و Port کلاس Metier.ImpotMetierTcp مقداردهی اولیه میشوند. سرور روی ماشین localhost قرار خواهد داشت و روی پورت 27 کار خواهد کرد.
رابط کاربری گرافیکی نمایش داده شده به کاربر به شرح زیر است:
![]() |
- در [1]، یک TextBox برای نمایش هرگونه استثنا اضافه شده است. این فیلد در نسخه قبلی وجود نداشت.
به جز این جزئیات، کد فرم با آنچه در بخش 6.4.3 قبلاً مورد بحث قرار گرفته، یکسان است. از خواننده دعوت میشود به آن بخش مراجعه کند. در [2]، مثالی از خروجی حاصل هنگام راهاندازی سرور به شرح زیر نشان داده شده است:
اسکرینشات کلاینت [2] مربوط به خطوط مربوط به کلاینت ۹ در بالا است.
11.9.5. نتیجهگیری
بار دیگر، ما توانستیم کدهای موجود را یا بدون هیچ تغییری (لایههای سرور [metier] و [dao]) یا با تغییرات بسیار اندک (لایه کلاینت [ui]) مجدداً استفاده کنیم. این امر با استفاده سیستماتیک ما از رابطها و نمونهسازی آنها با Spring امکانپذیر شد. اگر در نسخه ۷، منطق کسبوکار را مستقیماً در داخل رویدادپردازهای GUI قرار میدادیم، آن منطق کسبوکار قابلاستفاده مجدد نبود. این بزرگترین عیب معماریهای تکلایه است.
در نهایت باید توجه داشت که لایه [ui] هیچ اطلاعی ندارد که یک سرور راه دور در حال محاسبه مبلغ مالیات برای آن است.























































