ساخت چت روم با سی شارپ: قسمت اول


ساخت چت روم با سی شارپ: قسمت اول


در این قسمت از اموزش قصد داریم یک چت روم در گیم میکر با استفاده از سوکت TCP و زبان سی شارپ با نسخه دات نت 4.5 ایجاد کنیم، این چت روم فقط اطلاعات را به سرور ارسال و چت های رسیده توسط سایر اعضا را نمایش میدهد و وظیفه سرور ارسال چت های رسیده به تمام اعضا است.

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

ابتدا از سرور شروع میکنیم، برای راه اندازی یک سوکت سرور چند گره ای(multithread) ما از کتابخانه simpletcp استفاده میکنیم، وظیفه این کتابخانه راه اندازی سوکت سرور اعلان اتصال،قطع شدن و دریافت داده توسط یک سوکت در یک گره(thread) جداگانه است.

برای شروع یک پروژه ویندوز فرم(winform) ایجاد میکنیم(هیچ محدودیتی در انتخاب نوع پروژه از بین console,wpf,winform وجود ندارد)  سپس از بخش رفرنس کتابخانه simpletcp را به پروژه اضافه میکنیم و فضای نام انرا مینویسیم ، علاوه بر این کتابخانه ما به فضای نام های system.net و  system.net.socket و system.ioنیاز داریم  تا اینجا کدهای ما به شکل زیر است:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

using System.Windows.Forms;

using SimpleTCP;

using System.Net;

using System.Net.Sockets;

using System.IO;

 

namespace server

{

    public partial class Form1 : Form

    {

       

        public Form1()

        {

            InitializeComponent();

        }

 

    }

}

حالا نوبت ساخت یک شی از کلاس simpletcpserver برای راه اندازی سوکت سرور است،این شی باید به صورت گلوبال در محیط کلاس با دسترسی public تعیین شود

SimpleTcpServer server_socket = new SimpleTcpServer();

کلاس server_socket سه رویداد(event) دارد که توسط انها میتوانیم از وضعیت سوکت هایی که به عنوان کلاینت به سرور متصل می شوند اطلاع پیدا کنیم ، این ایونت ها عبارت اند از

  • ایونت اتصال کلاینت
  • ایونت قطع اتصال کلاینت
  • ایونت دریافت داده

حالا باید کد های این ایونت ها را در تابع کانستراکتور کلاس form1  (نام این تابع public Form1())بنویسیم ، شکل کلی سرور به این صورت است

 

namespace server

{

    public partial class Form1 : Form

    {

        SimpleTcpServer server_socket = new SimpleTcpServer();

        public Form1()

        {

            InitializeComponent();

 

            server_socket.ClientConnected += Server_socket_ClientConnected;

            server_socket.ClientDisconnected += Server_socket_ClientDisconnected;

            server_socket.DataReceived += Server_socket_DataReceived;

 

        }

 

        private void Server_socket_DataReceived(object sender, SimpleTCP.Message Message)

        {

           

        }

 

        private void Server_socket_ClientDisconnected(object sender, TcpClient Client)

        {

           

        }

 

        private void Server_socket_ClientConnected(object sender, TcpClient Client)

        {

           

        }

    }

}

هنگامی که یک سوکت به سرور ما متصل می شود رویداد Server_socket_ClientConnected اجرا می شود در این رویداد یک شی از کلاس tcpclient که حاوی اطلاعات سوکت متصل شده است برای ما ارسال می شود، ما باید ابجکت client را برای ارسال داده و شناسایی کلاینت ذخیره کنیم.

هنگامی که یک سوکت ارتباط خود را بنا به هر دلیلی قطع میکند این رویداد اجرا می شود ،در این رویداد یک شی از کلاس tcpclient همانند رویداد کانکت به ما ارسال می شود با استفاده از این ابجکت میتوانیم کلاینتی که قطع شده را از لیست کلاینت های متصل پیدا و حذف کنیم.

زمانی که ما از سوکت های متصل اطلاعاتی را دریافت میکنیم یک شی با نام message از کلاس message ایجاد و برای ما ارسال می شود،این ابجکت حاوی اطلاعاتی از قبیل کلاینت ارسال کننده داده(از نوع tcpclient)، داده های ارسالی(ارایه ای از بایت) است، با تبدیل این داده ها به داده های قابل خواندن میتوانیم پاسخ مناسب برای کلاینت ارسال کنیم.

نکته: این سه رویداد در گره مورد استفاده توسط کلاس سرور اجرا می شوند بنابراین شما نمیتوانید داخل انها به عناصر رابط کاربری داخل فرم دسترسی مستقیم داشته باشید(در ادامه در این مورد توضیحاتی خواهیم داد)

حالا باید سرور نوشته شده را استارت کنیم تا سوکت هایی که درخواست اتصال میدهند را به ما تحویل دهد برای این کار از تابع زیر استفاده میکنیم،پارامتر این تابع پورتی است که سرور باید بر روی ان کار خود را اغاز کند، ما این تابع را بعد از کد های نوشته شده برای ایونت و در داخل تابع form1 انجام میدهیم

 

        public Form1()

        {

            InitializeComponent();

 

            server_socket.ClientConnected += Server_socket_ClientConnected;

            server_socket.ClientDisconnected += Server_socket_ClientDisconnected;

            server_socket.DataReceived += Server_socket_DataReceived;

 

            server_socket.Start(5036);

        }

همانطور که مشاهده میکنید ما سرور را در پورت 5036 در حالت listening قرار دادیم تا سوکت هایی که روی این پورت برای ما درخواست اتصال میدهند را متصل کنیم.

از انجایی که قرار است چندین کلاینت به سرور ما متصل شوند و با یک دیگر چت کنند پس باید یک لیست برای کلاینت هایی که متصل می شوند ایجاد کنیم،در جایی که ابجکت سرور خود را ایجاد کردیم یک لیست به شکل زیر تعریف میکنیم:

List list_users = new List();

همانطور که مشاهده میکنید یک لیست از نوع tcpclient ایجاد کردیم زیرا با اتصال هر کلاینت ما باید شی tcpclient انها را برای دسترسی های بعدی داشته باشیم.

به رویداد اتصال کلاینت برمیگردیم در این رویداد باید هنگام اتصال یک کلاینت ابجکتی که برای ما ارسال شده است را در لیست قرار دهیم کد های زیر را در رویداد Server_socket_ClientConnected وارد میکنیم که این رویداد به شکل زیر خواهد بود:

        private void Server_socket_ClientConnected(object sender, TcpClient Client)

        {

            list_users.Add(Client);

        }

حالا هر زمان که یک کاربر به سرور ما متصل شود ما ان را در لیست خود داریم و میتوانیم چت هایی که را دیگران ارسال میکنند را برای او ارسال کنیم.

اما هنوز تمام نشده، باید هنگامی که کلاینت از سرور ما قطع شد او را از لیست کلاینت ها حذف کنیم تا داده ها برای او ارسال نشود(عملا ارسال نمی شوند و سرور به ما خطا خواهد داد)،پس برای جلوگیری از بروز خطا و مشکل، ما ان را بلافاصله از لیست حذف میکنیم ، برای حذف کلاینت از لیست کد زیر را در Server_socket_ClientDisconnected می نویسیم:

      private void Server_socket_ClientDisconnected(object sender, TcpClient Client)
        {
            Client.Close();
            list_users.Remove(Client);
        }

تا اینجا هر کلاینتی که به سرور ما متصل شود به لیست کلاینت های ما اضافه می شود و هنگامی که ارتباط خود را قطع کند از لیست حذف می شود.

حالا ما به یک تابع برای ارسال داده ها به کلاینت ها نیاز داریم برای این مسئله یک تابع به شکل زیر مینویسیم:

   

    private void send_packet(byte[] raw_byte, TcpClient client)

        {

            try

            {

                client.GetStream().Write(raw_byte, 0, raw_byte.Length);

            }

            catch (Exception)

            {

 

            }

        }

در کد های بالا پارامتر های این تابع ارایه ای از بایت و کلاینتی که قصد ارسال داده ها به ان را داریم است،برای این که در هنگام ارسال داده در صورت بروز خطا در هنگام ارسال، سرور بسته نشود کد ارسال را در داخل یک بلاک try/catch قرار میدهیم، تابع write وظیفه نوشتن بایت ها را برروی استریم و ارسال ان را برعهده دارد.

پس از انجام همه این موارد نوبت به دریافت داده ها و کد های مربوط به ان است.

به طور کلی ما در اینجا(و تقریبا در همه جا) اطلاعات را بر روی ارایه ای از بایت ها مینویسیم و انها را ارسال و دریافت میکنیم،هر ارایه از بایت را میتوان یک بسته داده نامید، ما در اینجا یک ساختار ساده برای بسته های داده در نظر گرفته ایم،هر بسته داده شامل اطلاعات مختلفی از قبیل مقدار های صحیح،اعشار و یا رشته ای میتواند باشد،از این رو طول هر بسته متفاوت است همچنین چون هر بسته وظیفه انجام یک کار را دارد بنابراین هر بسته باید یک شناسه یکتا داشته باشد به عنوان مثال بسته با شناسه 1 برای ثبت نام و بسته با شماره 2 برای ورود با ایمیل ،بسته با شماره 3 برای فراموشی رمز عبور،  مکان این شناسه عددی برای همه بسته ها ثابت است یعنی در ابتدای هر بسته یک مقدار عددی 2 بایتی برای شناسه بسته در نظر گرفته می شود اما قبل از ان باید مسئله دیگری را در نظر بگیریم

در سی شارپ و گیم میکر به دلیل مسائلی که در لایه های زیرین شبکه(یا همزمانی ارسال داده) رخ میدهد دو بسته ممکن است با یک دیگر ملحق شوند،به عنوان مثال رویداد دریافت داده اجرا می شود و ما 100 بایت دریافت کرده ایم در اینجا ما با خواندن یک بایت به عنوان شناسه بسته کاری که ان بسته باید انجام دهد را اعمال میکنیم اما مسئله این است که این 100 بایت ممکن است شامل دو بسته باشد که به هم چسبیده باشند بنابراین ما باید این دو بسته را ابتدا تفکیک کنیم و سپس وظایف هر بسته را انجام دهیم،برای حل این مسئله باید پس از این که داده را در بافر نوشتیم طول داده های نوشته شده را یک متغیر دو بایتی در ابتدای بافر قرار دهیم

به عبارتی ترتیب داده های یک بسته به این شکل هستند، یک متغیر دو بایتی برای طول بافر،یک متغیر دو بایتی برای شناسه بسته و سپس داده ها

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

قبل از این که به توضیح الگوریتم تفکیک بسته ها بپردازیم باید یک تابع برای انجام وظایف بسته ها ایجاد کنیم ، کار این تابع این است که بسته ای که دریافت کرده است را براساس شناسه بسته تفکیک و وظایف ان را انجام دهد. تعریف این تابع به این شکل است:

        private void handle_packet(byte[] data,TcpClient client)

        {  

        }

با این مقدمه حالا باید به سراغ الگوریتم تفکیک بسته ها برویم،این الگوریتم باید به شکلی باشد که برای داده هایی که فقط شامل یک بسته هستند مشکلی  ایجاد نکند و از طرف دیگر بتواند وجود چند بسته در یک ارایه بزرگ از داده را تشخیص دهد.

نوشتن این الگوریتم به این شکل است که ابتدا درون یک حلقه موقعیت فعلی بافر و طول کلی بافر را شرط قرار میدهیم تا جایی که موقعیت فعلی بافر به طول کل بافر برسد، به عبارت دیگر ما تا زمانی که موقعیت فعلی که در حال خواندن ان هستیم به طول کل داده هایی که دریافت کرده ایم نرسد به کار جدا سازی بسته ها ادامه میدهیم،درون این حلقه ما یک متغیر دوبایتی که قبلا در سمت کلاینت ان را نوشته ایم را میخوانیم این متغیر دو بایتی طول بسته فعلی را به ما اعلام میکند به عنوان مثال اگر ما 100 بایت دریافت کرده باشیم ممکن است بسته اول 20 بایت باشد و بسته دوم 30 بایت و بسته سوم 50 بایت بنابراین در ابتدای هر بسته یک مقدار دوبایتی از طول ان بسته وجود دارد که ان را میخوانیم و تعداد بایت هایی که ان بسته دارد را جدا میکنیم درست مانند بریدن یک قطعه پنیر از یک قالب پنیر.

کد های تفکیک داده در تابع Server_socket_DataReceived به این شکل هستند:

private void Server_socket_DataReceived(object sender, SimpleTCP.Message Message)

        {

            using(BinaryReader this_reader = new BinaryReader(new MemoryStream(Message.Data)))

            {

                while (this_reader.BaseStream.Position<this_reader.BaseStream.Length)

                {

                    short packet_len = this_reader.ReadInt16();

                    byte[] this_packet = this_reader.ReadBytes(packet_len-2);

                    handle_packet(this_packet, Message.TcpClient);

                }

            }

        }

همانطور که قبلا توضیح دادیم،ما تا زمانی که موقعیت فعلی به طول بافر نرسد ما دو بایت ابتدای بسته را میخوانیم و به میزان طول ان منهای 2 بایت ان را برش میدهیم و ان را به تابع handle_packet ارسال میکنیم ، در کنار پارامتر داده این تابع ما کلاینتی که از ان بسته را دریافت کرده ایم را ارسال میکنیم ،وظیفه این تابع خواندن شناسه بسته و انجام وظایف ان است.

این بخش از اموزش را به انتها میبریم، در کامنت میتوانید نظرات و سوالات خود را نسبت به این بخش از اموزش به ما اعلام کنید


    نظرات


    نام: علی

    23 اردیبهشت 1397
    مطلب شما کامل و عالیه ولی ، اگه کسی بعضی از اصطلاحات شما برای افراد مبتدی کمی نا شناخته است. بهتر روان تر توضیح دهید تا تازه کاران نیز بتوانند راحت تر یاد بگیرند.261
    نام: hadi eb

    23 اردیبهشت 1397
    چشم،تمام سعیمون رو میکنیم،برای این اموزش کاربران دانستن زبان سی شارپ الزامی هست






ارسال نظر




رفتن به بالا