برنامه نويسي نرم افزارهاي تحت شبکه
بررسي مباحث تئوري و عملي براي برنامه نويسان حيطه شبکه
مقدمه
*استريم ها
*مفاهيم کلي TCP
*نحوه يافتن اطلاعات آدرس IP
*مفهوم سوکت در ارتباطات شبکه اي
*تعريف برنامه سرويس گيرنده(client) و سرويس دهنده (Server)
*برنامه نويسي تحت شبکه براي سوکت هاي اتصال گرا (TCP)
*مشکلات ارتباط TCPو مقابله با آنها
استريم
خصوصيات
در نظر داشته باشيد که صحت داده ها TCP را تضمين مي کند، اما عدم ذخيره کردن محدوده هاي پيغام (ابتدا و انتهاي پيغام)در اين نوع ارتباط باعث بروز مشکلاتي مي شود که در ادامه مقاله به بررسي برخي از آنها و راه حل هاي مقابله خواهيم پرداخت.
پس از پذيرفته شدن داده (Data1)براي ارسال، TCP مدتي آن را در بافر خود نگه داشته و حتي در صورت ارسال داده ديگري توسط برنامه شما، مجدداً در کنار اولين داده نگهداري مي گردد. همان طور که پيشتر گفته شد، استريم ها بلوکي از داده ها را ارسال کرده و به همين جهت داده از بافر، بصورت يک بسته ارسال خواهد شد (Data1وData2). در سمت ديگر، ماشين دريافت کننده داده ها تنها يک بسته اطلاعاتي دريافت مي کند و چون محدوده هاي پيغام (ابتدا و انتهاي پيغام) در TCP ذخيره نمي شود، ماشين دريافت کننده، بسته هاي دريافتي را تنها بصورت يک پيغام خواهد ديد (Data2+Data1).
يافتن اطلاعات آدرس IP
*دستور IPConfig
*استفاده از رجيستري ويندوز
*استفاده از پايگاه داده WMI
*استفاده از Domain Name System) DNS)
کتابخانهNet. در فضاي نام System.Net، کلاس DNS را تدارک ديده و از ميان متدهاي مختلفي که اين کلاس در اختيار برنامه نويسان قرار داده است، دو متد براي يافتن آدرس IPسيستم محلي کاربرد دارند. متد() GetHostname، نام ميزبان را بر مي گرداند و متد () GetHostByName(در نسخه از NET 2.0متد () GetHostEntry به جاي اين متد مي توانيد استفاده کنيد) آدرس IP ميزبان مشخص شده را استخراج مي کند. با ترکيب اين دو متد، مي توان آدرسIPسيستمي را بدست آورد. براي درک بهتر طريقه استفاده از اين متدها به تکه برنامه زير توجه نماييد:
using System
using System.Text
using System.Net
using System.Net.Sockets
namespace TestSocket
{
class Program {
static void Main(string[]args) {
try {
IPHostEntry IPHost=Dns.GetHostEntry("www.hotmail.com")
IPAddress[]ipAddress=IPHost.AddressList
StringBuilder strIpAddress=new StringBuilder()
for (int i=0,i<ipAddress.Length,i++) {
strIpAddress.Append(ipAddress[i].ToString()),
}
Console.WriteLine("IP is:"+strIpAddress.ToString())
}
catch (SocketException ex) {
Console.WriteLine("Error Occured! "+ex)
}
Console.Read()
} } }
سوکت
برنامه هاي سرويس دهنده و سرويس گيرنده
برنامه سرويس دهنده (Server)
برنامه سرويس گيرنده (Client)
نحوه مديريت آدرس هاي IP در Net.
کلاس IPAddress
System.Net.IPAddress ip=System.Net.IPAddress.Parse(string ipString)
پارامتر ipStringدر ساختار فوق، آدرسIP کارت شبکه را به صورت رشته اي مشخص مي کند. حال پس از تعريف يک Instanceاز اين شئي، مي توانيد به خصوصيات آن دست يابيد. در عين حال علاوه بر اين روش، درNet frame work 2.0 نيز مي توانيد از متد زير استفاده کنيد:
byte[]arrIp={192,168.0.1}
System.Net.IPAddress ip=new System.Net.IPAddress(arrIp)
کلاس IPEndPoint
IPEndPoint(IPAddress address,int port)
همان طور که ملاحظه مي کنيد، پارامتر اول، نمونه اي از شي و پارامتر دوم شماره پورت اتصال را مشخص مي کند.
توجه:
از آنجا که شماره هاي کمتر از 1024توسط سيستم عامل مورد استفاده قرار مي گيرد، بنابراين از شماره پورت هاي بالاتر از 1024استفاده کنيد.
نحوه استفاده از سوکت ها
public Socket (AddressFamily addressFamily,Socketype Socketype,ProtocolType ProtocolType)
پارامتراول در فرم فوق، "نوع شبکه" و پارامتر دوم "نوع اتصال" و پارامتر سوم "پروتکل ارتباطي" را تعيين کرده که تمامي پارامترهاي اين ساختار از نوع شمارشي (عددي) مي باشند. بايد توجه داشته باشيد که به هيچ عنوان اجازه ترکيب پارامترهاي دوم و سوم را نداشته و براي هر SocketType، مي بايست از يک ProtocolType ويژه استفاده کنيد. ضمناً فراموش نکنيد که براي استفاده ProtocolType با مقدارStream، مقدار ProtocolType را به Tcp تنظيم نماييد.
پس از نشان دادن هر يک از قسمت هاي اصلي تشکيل دهنده يک برنامه تحت شبکه، حال به بررسي نحوه ايجاد برنامه هاي تحت شبکه مي پردازيم.
انتخاب نوع سوکت ارتباطي
در سوکت هاي اتصال گرا مانند TCP، براي مبادله داده ها بين دو ماشين، حتماً بايد پيشتر، اتصالي برقرار شده باشد. اما در سوکت هاي بدون اتصال مانندUDP، نيازي به برقراري اتصال نبوده و در عوض، به ازاي هر بار ارسال داده به ماشين ديگر، آدرسIP ماشين مقصد مي بايست مشخص شود. از آنجايي که سوکت هاي اتصال گرا از محبوبيت بيشتري در ايجاد برنامه هاي تحت شبکه برخوردار هستند، لذا در اين بخش مقاله فقط به بررسي اين نوع سوکت ها پرداخته ايم.
سوکت اتصال گرا (Connection-Oriented)
عمليات برنامه سرويس دهنده (سرور)
*ايجاد سوکت
*مقيد کردن(تخصيص)سوکت به يک کارت شبکه (آدرسIP معين)
*گوش دادن به درخواست ها
*پذيرش درخواست ها براي برقراري اتصال
*مبادله داده ها
*پايان دا دن به ارتباط و بستن سوکت
در مرحله اول پس از ايجاد نمونه اي از شيsocket، براي مقيد کردن سوکت به يک آدرسIP خاص، بايد متد()Bindرا فراخواني کنيد:
IPAddress ipaddr=IPAdress.Parse("192.168.0.1")
IPEndPoint iep=new IPEndPoint(ipaddr,9050)
socket.Bind(iep)
پارامترiep نمونه اي از شيIPEndPoint مي باشد که در مرحله دوم، اختصاص شي socket توسط آدرسIP و شماره پورت مشخص شده در آن، انجام مي شود. گام بعدي، گوش دادن به درخواست هاي ورودي است که براي اين منظور، متد()Listen را فراخواني کنيد:
(5)socket.Listen
ضمناً پارامتر backlog، مشخص کننده تعداد درخواست هاي اتصالي است که مي توانند در صف قرار گيرند تا در زمان مناسب به درخواست آنها رسيدگي شود. هر در خواستي که بيش از اين تعداد باشد، ناديده گرفته خواهد شد. هراندازه مقدار اين پارامتر بزرگ باشد، به همان اندازه فضاي کمتري براي ارسال و دريافت بسته ها خواهيد داشت.سپس با فراخواني متد()Accept به درخواست اتصال ورودي پاسخ داده و توسط سوکت جديدي که اين متد بر مي گرداند، در مرحله چهارم مي توانيد با سرويس گيرنده مبادله داده ها را آغاز کنيد. همچنين متدهاي()Sendو()Receive به ترتيب براي ارسال و دريافت بسته هاي TCP بکار مي روند. هر يک از اين متدها به چهار شکل مختلف سربار گذاري شده اند که جدول 1-1، آنها را به زبان#C نشان مي دهد.
جدول1-1:متدهای Send() , Receive | |
متد | توضیحات |
Receive (byte [] data) | داده ها را دریافت و در آرایه بایتی قرار می دهد |
Receive (byte [] data, Socket Flags sf) | صفات مربوط به سوکت را تنظیم کرده، داده ها را دریافت می کند و آن ار در آرایه بایتی قرار می دهد. |
Receive (byte [] data, int size, Socket Flags sf) | صفات مربوط به سوکت را تنظیم کرده، سایز مشخصی از داده را دریافت می کند و آن را در آرایه بایتی قرار می دهد. |
Receive (byte [] data, int offset, int size, Socket Flags sf) | صفات مربوط به سوکت را تنظیم کرده، سایز مشخصی از داده را دریافت می کند و آن را از offset،در آرایه بایتی قرار می هد. |
Send (byte [] data) | داده ها را دریافت و در آرایه بایتی را ارسال می کند. |
Receive (byte [] data, Socket Flags sf) | صفات مربوط به سوکت را تنظیم کرده، داده ها را دریافت می کند و آن ار در آرایه بایتی ارسال می کند. |
Send (byte [] data, int size, Socket Flags sf) | صفات مربوط به سوکت را تنظیم کرده، سایز مشخصی از داده را دریافت می کند و آن را در آرایه بایتی را ارسال می کند. |
Send (byte [] data, int offset,, int size, Socket Flags sf) | صفات مربوط به سوکت را تنظیم کرده، سایز مشخصی از داده قرار گرفته در آرایه بایتی با شروع از افست offset،ارسال می هد. |
در نهايت براي خاتمه دادن به ارتباط، دو متد ()Shutdown و ()Closeدر اختيار شما قرار دارند. متد()Close، پس از فراخوانيه ي سريعاً سوکت را بسته و منابع تخصيص يافته را آزاد مي کند. اما متد()Shutdown، پارامتري دريافت مي کند که اين پارامتر مشخص کننده نحوه بستن سوکت است. مقادير مجاز براي اين پارامتر در جدول 2-1آمده است.
جدول 2-1:مقادیر SocketShutdown | |
مقدار | توضیحات |
SocketShutdown.Both | از ارسال و دریافت داده ممانعت می کند. |
SocketShutdown. Receive | از دریافت داده ممانعت کرده و اگر داده دیگری دریافت شود، یک RSTارسال میشود. |
SocketShutdown. Send | از ارسال داده بر روی سوکت جلوگیری کرده و پس از ارسال همه داده بافر، یک FINارسال می شود. |
عمليات برنامه سرويس گيرنده
*متصل شدن به سرورها
*ارسال و دريافت داده ها
*پايان دادن به ارتباط و بستن سوکت
همانند برنامه سرور، ايجاد سوکت در برنامه سرويس گيرنده(Client)جزو نخستين مرحله ايجاد نمونه اي از شي Socket است. پس از انجام اين عمل، حال مي بايست متد ()Connect را براي ارسال درخواست اتصال، فراخواني نماييد. توجه داشته باشيد که اين متد براي متصل شدن به سرويس دهنده، به آدرس IP مقصد و شماره پورتي که اتصال بر روي آن انجام خواهد پذيرفت، نياز دارد که توسط پارامتري از نوع مشخص IPEndPoint مي گردد.
socket.()Connect(IPAddress.Parse("192.168.0.1",9050))
توجه: اين امکان وجود دارد که برنامه سرويس دهنده به هنگام ارسال درخواست اتصال توسط سرويس گيرنده، به درخواست هاي ورودي گوش نکرده و با خطايي مواجه گردد که براي رفع اين مشکل احتمالي، بهتر است متد()Connect را داخل بلوکtry-Catch بنويسيد.
توجه داشته باشيد که به هنگام بروز خطا در هر يک از متدهاي Socket، استثنايي از نوع SocketException رخ خواهد داد. در ضمن به علت اينکه پيغام ها بايد به صورت آرايه هاي بايتي مبادله شوند، بنابراين مي بايست داده ها را به آرايه هاي بايتي تبديل کرده و در سمت دريافت کننده، مجدداً آرايه بايتي به پيغام مناسب تبديل شود.براي تبديل رشته ها به آرايه هاي بايتي و بالعکس، مي توانيد از کلاس Encoding واقع در فضاي نام System.Text استفاده نماييد. اگر رشته مورد نظر فقط شامل کاراکترهاي اسکي باشد، با استفاده از خصوصيت ASCII و متدهاي ()GetString و ()GetBytes ، رشته ها را به آرايه هاي بايتي و آرايه هاي بايتي را به رشته تبديل کنيد.
byte buffer=System.Text.Encoding.ASCII.GetBytes(Sample Text")
string str =System .Text.Encoding.ASCII.GetString (buffer)
نکته: اگر رشته ارسالي شامل کاراکترهاي فارسي است، در صورت استفاده از خصوصيت ASCII براي تبديل رشته به آرايه بايتي، با نتايج ناصحيح در ماشين دريافت کننده مواجه خواهيد شد (بجاي کاراکترهاي فارسي، "؟" قرار مي گيرد). براي تبديل رشته هايي که در آن از کاراکترهاي فارسي استفاده شده است، از خصوصيت UTF-8 و متدهاي ()GetString و ()GetBytes آن استفاده کنيد.
byte buffer=System.Text.Encoding.UTF8.GetBytes("متن فارسي")
string str=System.Text.Encoding.UTF8.GetString (buffer)
مشکلات TCP
*استفاده نامناسب و دستکاري ناصحيح بافر داده
*اندازه نامناسب پيغام ها در شبکه
دقت نظر داشته باشيد که در برنامه هايي که ذکر گرديد، همه پيغام ها قالب ثابت و مشخصي داشته و اندازه آنها کنترل شده بود، اما در اصل هنگام برقراري ارتباط سرويس دهنده با سرويس گيرنده، هيچ يکي از آنها شناختي در مورد طول يا نوع داده هاي ارسالي صرف مقابل ندارند. حال اين سوال مطرح مي شود که هنگام دريافت اطلاعاتي که حجمشان بيش از حجم تعيين شده است، چه بايد کرد؟
خب استفاده از بافر داده بزرگ يا بافر داده کوچک هر يک مزايا و معايب خود را دارند که بررسي آنها خارج از حوصله اين مقاله است ولي به طور خلاصه بدانيد که بر حسب عملکرد برنامه، مي بايست سايز بافر داده را تعيين کنيم. ضمناً مشکل ديگر ارتباط TCP، عدم نگهداري محدوده هاي پيغام در اين نوع ارتباط است که براي بر طرف کردن آن سه روش وجود دارد:
*ارسال پيغام با طول ثابت
*ارسال اندازه پيغام به همراه پيغام
*استفاده از سيستم نشانه گذاري براي جدا کردن پيغام ها
حال از آنجايي که روش سوم از سه روش ياد شده، کاراتر بوده و ضريب اطمينان بالاتري دارد، در ادامه به بررسي آن خواهيم پرداخت.
ارسال اندازه پيغام به همراه خود پيغام
همچنين راه حل ديگري به منظور ارسال مقدار عددي اندازه پيغام وجود دارد که برطبق آن، مقدار صحيح عددي را به يک آرايه بايتي تبديل کرده و قبل از ارسال پيغام، آن را بفرستيد. براي تبديل مقدار صحيح عددي به آرايه عددي به آرايه بايتي، از متد()GetBytes کلاس BitConverter که در فضاي نام System قرار دارد، استفاده شده است. ضمناً در سمت ماشين دريافت کننده نيز بايد ابتدا اندازه پيغام دريافت شده و سپس خود پيغام، با توجه به اندازه دريافتي، دريافت گردد.
اخطار:
متدهاي کلاس BitConverter ، بسته به ماشيني که بر روي آن اجرا مي شوند، عمل تبديل را انجام داده و اگر هر دو ماشين (سرويس دهنده و سرويس گيرنده) از سيستم عامل ويندوز به همراه پردازنده اينتل به همراه پردازنده AMDاستفاده کنند، نتيجه درست خواهد بود. اما در صورتي که يکي از آنها از نوع ديگري باشد، امکان بروز نتايج نادرست وجود دارد. البته براي رفع اين مشکل نيز راه حلي وجود دارد که با تبديل کردن ترتيب بايت هاي ارسالي به ترتيب بايت شبکه انجام مي گيرد. لازم به ذکر است که در اين مورد نيزNet.دو متد استاتيک در کلاس IPAddress به نام هاي()HostToNetworkByteOrder و() NetworkToHostByteOrder در اختيار برنامه نويسان قرار داده است.
استفاده از سيستم نشانه گذاري
مثال زير نمونه ساده اي از يک برنامه سرور است:
using System
using System.Text
using System.Net
using System.Net.Sockets
class Socketserver {
public static void Main() {
StreamWriter StreamWriter
StreamReader StreamReader
NetworkStream NetworkStream
tcpListener tcpListener=new tcpListener(5555)
tcpListener.Start()
Console.WriteLine("The Server has started on port5555")
Socket serverSocket=tcpListener.AccptSocket()
try {
if(serverSocket.Connected) {
while (true) {
Console.WriteLine("Client connected")
NetworkStream=new NeworkStream(serverSocket)
StreamWriter=new streamWriter(networkStream)
StreamReader=new streamReader(networkStream)
Console.WriteLine(StreamReader.ReaderLine())
}
}
if(serverSocket.Connected)
serverSocket.Close()
Consol.Read()
}
catch(SocketException ex) {
Console.WriteLine(ex)
} } }
مثال زير نمونه ساده اي از يک برنامه سرويس گيرنده (Client) است:
using System
using System.Text
using System.Net
using System.Net.Sockets
class SocketClient {
static void Main(string[]args) {
TcpClient tcpClient
NetworkStream networkStream
StreamReader streamReader
StreamWriter streamWriter
try {
TcpClient=new TcpClient("localhost",5555)
NetworkStream=TcpClient.GetStream()
StreamReader=new streamReader(networkStream)
StreamWriter=new streamWriter(networkStream)
StreamWriter.WriteLine("Message from the Client...")
StreamWriter.Flush() }
catch(SocketException ex) {
Console.WriteLine(ex)
}
Consol.Read()
} }
سوکت هاي بلوکه کننده
*استفاده از سوکت هاي non-block
*استفاده از سوکت هاي Multiplex
*استفاده از سوکت هاي آسنکرون (Asynchronous)
*چند نخي کردن (Multithreading)
همچنين مفاهيم پيشرفته تري که در زير آمده، مي تواند زمينه اي براي مطالعه بيشتر خوانندگان باشد.
*بکارگيري سوکت هاي بدون اتصال (uDP)
*مشکلات uDPو مقابله با آنها
*استفاده از سوکت هاي non-block
*استفاده از سوکت هاي multiplex
*کلاس هاي TCPListener،
، TCPClient و UDPClient
*نحوه ارسال داده هاي پيچيده تر(مانند کلاس ها)
*استفاده از سوکت هاي آسنکرون
*Multicasting
*Broadcasting
*نحوه تبديل ترتيب بايت ماشين به ترتيب بايت شبکه و بالعکس
*استفاده از نخ ها (Thread) براي متصل شدن چندين سرويس گيرنده بطور همزمان به سرويس دهنده
سخن پاياني
منبع:نشريه دانش و کامپيوتر شماره 85 /س