رضوى العربي

القراءة من والكتابة إلى المجارى القياسية

يُمثِل النوع Console كلًا من مَجْاري الخْرج والدخْل والخْطأ القياسية ببرامج الطرفية.

الكتابة إلى مجرى الخرج القياسي Stdout

يَستقبِل التابع السِلسِلة النصية Hello World ويُرسلها إلى مَجْرى الخْرج القياسي، كالتالي:

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World");
    }
}

تَتوفَّر بصمات أُخرى من التابع Console.WriteLine. يَستقبِل إحداها مُعامِل من النوع Object، ويُرسله أيضًا إلى مَجْرى الخْرج القياسي، لكن بعد اِستدعاء التابع ToString من خلاله.

الكتابة إلى مجرى الخطأ القياسي StdErr

var sourceFileName = "NonExistingFile";
try
{
    System.IO.File.Copy(sourceFileName, "DestinationFile");
}
catch (Exception e)
{
    var stdErr = Console.Error;
    stdErr.WriteLine($"Failed to copy '{sourceFileName}': {e.Message}");
}

قراءة مجرى الخطأ القياسي الخاص بعملية فرعية (child process)

var errors = new System.Text.StringBuilder();

var process = new Process
{
    StartInfo = new ProcessStartInfo
    {
        RedirectStandardError = true,
        FileName = "xcopy.exe",
        Arguments = "\"NonExistingFile\" \"DestinationFile\"",
        UseShellExecute = false
    },

};

process.ErrorDataReceived += (s, e) => errors.AppendLine(e.Data);

process.Start();
process.BeginErrorReadLine();
process.WaitForExit();

if (errors.Length > 0) // something went wrong
    System.Console.Error.WriteLine($"Child process error: \r\n {errors}");

العمليات على الملفات

يُوفر النوع File -الموجود بفضاء الاسم System.IO- العديد من التوابع الساكنة (static methods) لإجراء العمليات المختلفة على الملفات، كـقراءة ملف أو حفظ بيانات بملف إِمّا بالكتابة (write) وإِمّا بالإضافة فيه (append) وغيره.

فحص وجود ملف

يَستقبِل التابع File.Exists مُعامِلًا من النوع String يحتوي على مسار ملف مُعين (قد يكون المسار نسبيًا relative أو مُطلقًا absolute/fully-qualified)، ثم يُعيد قيمة منطقية (bool) تُحدد ما إذا كان الملف موجودًا أم لا، كالتالي:

using System;
using System.IO;

public class Program
{
    public static void Main()
    {
        string filePath = "somePath";

        if(File.Exists(filePath))
        {
            Console.WriteLine("Exists");
        }
        else
        {
            Console.WriteLine("Does not exist");
        }
    }
}

كأيّ تابع يُعيد قيمة منطقية، يُمكن استخدامه مع المُعامِل الشرطي الثلاثي :? (ternary)، كالتالي:

Console.WriteLine(File.Exists(pathToFile) ? "Exists" : "Does not exist");

قراءة ملف

قراءة ملف باستخدام النوع File

يُوفر النوع File ثلاثة توابع لقراءة البيانات من ملف معين. تَستقبِل التوابع الثلاثة مسار الملف المطلوب قراءته، ولكنها تختلف في طريقة القراءة.

  • التابع File.ReadAllText: يقرأ محتويات الملف بالكامل إلى مُتغيِّر من النوع string.
  • التابع File.ReadAllLines: يقرأ محتويات الملف إلى مصفوفة من السَلاسِل النصية string[]‎، بحيث يُقابل كل عنصر منها سطرًا واحدًا من الملف.
  • التابع File.ReadAllBytes: يُعامِل الملف بِعدّه مجموعة بايتات Bytes ويُعيد مصفوفة بايتات byte[]‎.

لاحظ الأمثلة التالية:

string fileText = File.ReadAllText(file);
string[] fileLines = File.ReadAllLines(file);
byte[] fileBytes = File.ReadAllBytes(file);

قراءة ملف باستخدام النوع StreamReader

string fullOrRelativePath = "testfile.txt";
string fileData;
using (var reader = new StreamReader(fullOrRelativePath))
{
    fileData = reader.ReadToEnd();
}

كتابة ملف

تختلف كتابة البيانات (writing) بملف عن إضافة بيانات إليه (appending) به. ففي الأولى، تُكتَب البيانات على محتويات الملف الحالية (overwrite)، أما في الثانية، تُضاف البيانات إلى نهاية الملف مما يعني الحفاظ على محتوياته الحالية.

ملحوظة: تُنشِئ جميع التوابع المذكورة بالأسفل الملف بصورة آلية -إذا لم يَكُنْ موجودًا- قبل محاولة إلحاق/كتابة البيانات به.

كتابة ملف باستخدام النوع File

يُوفر النوع File ثلاثة توابع لكتابة البيانات (writing) بملف.

  • التابع File.WriteAllText: يَستقبِل سِلسِلة نصية string ويكتبها بالملف.
  • التابع File.WriteAllLines: يَستقبِل مصفوفة من السَلاسِل النصية string[]‎ ويَكتِب كل عنصر بها إلى سطر منفصل بالملف.
  • التابع File.WriteAllBytes: يَسمَح بكتابة مصفوفة بايتات byte[]‎ بملف.

لاحظ الأمثلة التالية بلغة C#‎:

File.WriteAllText(file, "here is some data\nin this file.");
File.WriteAllLines(file, new string[2] { "here is some data", "in this file" });
File.WriteAllBytes(file, new byte[2] { 0, 255 });

أو بلغة الفيجوال بيسك VB، كالتالي:

using System.IO;
using System.Text;
string filename = "c:\path\to\file.txt";
File.writeAllText(filename, "Text to write\n");

كتابة ملف باستخدام النوع StreamWriter

يُمكنك أيضًا الكتابة إلى ملف باستخدام كائن من النوع StreamWriter. عادةً ما يُنشَئ كائن المَجْرَى (stream) ضِمْن كتلة using، مما يَضمَن استدعاء التابع Dispose الذي يُفرّغ المَجْرَى ويُغلقه بمجرد الخروج من الكتلة.

using System.Text;
using System.IO;
string filename = "c:\path\to\file.txt";

using (StreamWriter writer = new StreamWriter(filename))
{
    writer.WriteLine("Text to Write\n");
}

بالمثل بلغة الفيجوال بيسك VB، يُمكنك الكتابة إلى الملفات باستخدام كائن من النوع StreamWriter، كالتالي:

Dim filename As String = "c:\path\to\file.txt"
If System.IO.File.Exists(filename) Then
    Dim writer As New System.IO.StreamWriter(filename)
    writer.Write("Text to write" & vbCrLf) 'Add a newline
    writer.close()
End If

إضافة بيانات إلى ملف

يُوفر النوع File ثلاثة توابع لإضافة بيانات إلى ملف (appending).

  • التابع File.AppendAllText: يَستقبِل سِلسِلة نصية string ويُلحِقها بنهاية الملف.
  • التابع File.AppendAllLines: يَستقبِل مصفوفة من السَلاسِل النصية string[] ويُلحِق كل عنصر بها إلى الملف بـسطر منفصل.
  • التابع File.AppendText: يَفتَح مَجْرَى (stream) من النوع StreamWriter. يُلحِق هذا المَجْرَى أي بيانات تُكتَب إليه بنهاية الملف.

لاحظ الأمثلة التالية:

File.AppendAllText(file, "Here is some data that is\nappended to the file.");

File.AppendAllLines(file, new string[2] { 
    "Here is some data that is", 
    "appended to the file."
});

using (StreamWriter stream = File.AppendText(file))
{
    stream.WriteLine("Here is some data that is");
    stream.Write("appended to the file.");
}

حَذْف ملف

يَحذِف التابع File.Delete الملف في حالة تَوفُّر الصلاحيات المطلوبة، كالمثال التالي:

File.Delete(path);

ومع ذلك، قد يَحدُث خطأ أثناء تنفيذ التَعلِيمة البرمجية بالأعلى، ربما لأحد الأسباب التالية:

  • في حالة عدم تَوفُّر الصلاحيات المطلوبة، ويُبلَّغ عن اعتراض من نوع UnauthorizedAccessException.

  • إذا كان الملف قَيْد الاستخدام أثناء محاولة الحذف، ويُبلَّغ عن اعتراض من نوع IOException.

  • في حالة حُدوث خطأ مُنخفِض المستوى (low level) أو كان الملف مُعدّ للقراءة فقط، ويُبلَّغ عن اعتراض من نوع IOException.

  • إذا لم يَكن الملف مَوجودًا، ويُبلَّغ عن اعتراض من نوع IOException.

عادةً ما يتم التحايل على المشكلة الأخيرة باستخدام الشيفرة التالية:

if (File.Exists(path))
    File.Delete(path);

مع ذلك فإن هذه الطريقة غير مَضْمُونَة؛ لأنها ليست عملية ذرية (atomic) حيث تَتِم على خُطوتين، فمن الممكن أن يُحذَف الملف -بواسطة عملية أخرى- بَعْد اجتياز عملية الفحص بنجاح وقبل محاولة إجراء الحَذْف الفعلي. لذلك فإن الطريقة الأصح للتعامُل مع عمليات الخْرج والدخْل (I/O) لابد وأن تتضمَّن معالجة للاعتراضات (exception handling)، والتي تعني اتخاذ مسار آخر من الأحداث في حالة فَشَل العملية، كالتالي:

if (File.Exists(path))
{
    try
    {
        File.Delete(path);
    }
    catch (IOException exception)
    {
        if (!File.Exists(path))
            return; 
        // قام شخص آخر بحذف الملف
    }
    catch (UnauthorizedAccessException exception)
    {
        // لا تتوفر الصلاحيات المطلوبة
    }
}

لاحظ أنه أحيانًا ما تكون أخطاء الخْرج والدخْل (I/O) مؤقتة (مثلًا قد يكون الملف قَيْد الاستخدام)، وفي حالة استخدام الاتصال الشبكي، قد تُحَلّ المشكلة آليًا دون الحاجة لأيّ إجراء. لذا فمن الشائع إعادة محاولة إجراء عمليات الدخْل والخْرج (I/O) عدة مرات مع الانتظار لمُهلة قصيرة بين كل محاولة وأخرى، كالمثال التالي:

public static void Delete(string path)
{
    if (!File.Exists(path))
        return;
    for (int i=1; ; ++i)
    {
        try
        {
            File.Delete(path);
            return;
        }
        catch (IOException e)
        {
            if (!File.Exists(path))
                return;
            if (i == NumberOfAttempts)
                throw;
            Thread.Sleep(DelayBetweenEachAttempt);
        }
        // (*)
    }
}

private const int NumberOfAttempts = 3;
private const int DelayBetweenEachAttempt = 1000; // ms

إذا كنت تَستخدِّم نظام التشغيل Windows وكان ملف ما مفتوحًا بوَضْع FileShare.Delete، ثم حاولت حَذْف ذلك الملف باستخدام التابع File.Delete، فستُعدّ عملية الحَذْف مقبولة، ولكن في الواقع لن يُحذَف الملف إلا بَعْدَما يُغلق.

(*): // ‫ربما تقوم بنفس الشئ مع الاعتراض UnauthorizedAccessException مع أنه ليس من المحتمل حل مثل هذه المشكلة خلال ثوان.

File.WriteAllLines(
    path,
    File.ReadAllLines(path).Where(x => !String.IsNullOrWhiteSpace(x)));

نَقْل ملف من مسار إلى آخر

يَستقبِل التابع File.Move مُعامِلين هما ملف المصدر (source) يُحدد الملف المُراد نَقْله، وملف المَقصِد (destination) يُحدد المسار المطلوب نَقْل ملف المصدر إليه. كالمثال التالي:

File.Move(@"C:\TemporaryFile.txt", @"C:\TemporaryFiles\TemporaryFile.txt");

مع ذلك، قد يَحدُث خطأ أثناء تنفيذ التعليمة البرمجية بالأعلى. على سبيل المثال:

  • ماذا لو كان مُستخدِم البرنامج لا يَملِك قُرْص بعنوان C؟ أو كان يَملِكه، ولكنه غَيّر عنوانه إلى B أو M؟
  • ماذا لو كان ملف المصدر (source) قد نُقل بدون عِلمك؟ أو لم يكن موجودًا من الأساس؟

يُمكن التحايل على تلك المشاكل بالتأكد من وجود ملف المصدر أولًا قبل محاولة نَقْله، كالتالي:

string source = @"C:\TemporaryFile.txt", destination = @"C:\TemporaryFiles\TemporaryFile.txt";

if(File.Exists("C:\TemporaryFile.txt"))
{
    File.Move(source, destination);
}

بهذه الطريقة سـنتأكد من وجود ملف المصدر (source) في تلك اللحظة، مما يعني إمكانية نَقْله إلى مكان آخر. لاحظ أنه أحيانًا، قد لا تكون هذه الطريقة كافية.

أما في حالة لم يَكن الملف موجودًا، قُم بعملية الفحص مُجددًا، وإذا فَشَلت، يُمكنك مُعالجة الاعتراض أو تَبلِّيغ المُستخْدِم بذلك.

ملحوظة: يُعدّ الاعتراض FileNotFoundException واحدًا فقط ضِمْن عِدّة اعتراضات قد تواجهك.

يَعرِض الجدول التالي الاعتراضات المُحتملة:

| نوع الاعتراض | الوصف | | :-------------------------: | :----------------------------------------------------------: | | IOException | إذا كان ملف المقصد موجود بالفعل أو كان ملف المصدر (source) غير موجود | | ArgumentNullException | إذا كانت قيمة ملف المصدر (source) أو ملف المقصد (destination) فارغة | | ArgumentException | إذا كانت قيمة ملف المصدر (source) أو ملف المقصد (destination) فارغة أو تحتوي محارِف غير صالحة | | UnauthorizedAccessException | إذا لم تتَوفَّر الصلاحيات المطلوبة لإجراء العملية | | PathTooLongException | إذا كان ملف المصدر (source) أو ملف المقصد (destination) أو المسارات المحددة تتعدّى الطول المسموح به. | | DirectoryNotFoundException | إذا كان المجلد المُحدد غير موجود | | NotSupportedException | إذا كان مسار ملف المصدر (source) أو ملف المقصد (destination) أو أسماء تلك الملفات بصيغة غير صالحة |

تَعدِّيد ملفات أقدم من مدة محددة

يُعرِّف المقطع البرمجي التالي دالة مُساعدة (helpers) تحمل الاسم EnumerateAllFilesOlderThan. تُعدِّد هذه الدالة الملفات الأقدم من عُمر معين بالاعتماد على التابع Directory.EnumerateFiles. تُعدّ هذه الدالة مفيدة لحذْف ملفات التسجيل (log files) القديمة أو البيانات المُخزَّنة مؤقتًا (cached).

static IEnumerable<string> EnumerateAllFilesOlderThan(
    TimeSpan maximumAge,
    string path,
    string searchPattern = "*.*",
    SearchOption options = SearchOption.TopDirectoryOnly)
{
    DateTime oldestWriteTime = DateTime.Now - maximumAge;
    return Directory.EnumerateFiles(path, searchPattern, options)
        .Where(x => Directory.GetLastWriteTime(x) < oldestWriteTime);
}

يُمكن استدعائها كالتالي:

var oldFiles = EnumerateAllFilesOlderThan(TimeSpan.FromDays(7), @"c:\log", "*.log");

تَستخْدِم الدالة المُساعِدّة بالأعلى التابع Directory.EnumerateFiles()‎ بدلًا من التابع Directory.GetFiles()‎. لذا لن تحتاج أن تنتظر جَلْب جميع الملفات قبل البدء بمعالجتها.

تَفحَّص الدالة المُساعِدّة توقيت آخِر كتابة (last write time)، ولكن يمكنك أيضًا الاعتماد على وقت الإنشاء (creation time) أو وقت آخر وصول (last access time) وقد يكون ذلك مُفيدًا لحذْف ملفات التخزين المؤقت غير المُستخدَمة. انتبه فقد يكون وقت الوصول غيْر مُفعّل.

حذف سطور من ملف نصي

لا يُعدّ التعديل على الملفات النصية أمرًا يسيرًا؛ لأنه لابُدّ من قراءة محتواها أولًا.

إذا كان حجم الملف صغيرًا، فإن أسهل طريقة هي قراءة الملف بالكامل إلى الذاكرة (memory)، وإجراء التعديلات، ثم إعادة كتابة النص المُعدّل إلى الملف.

في المثال التالي، قُرِأت كل سطور الملف، وحُذْف الفارغ منها، ثم كُتبت بملف بنفس المسار.

File.WriteAllLines(path,
                   File.ReadAllLines(path).Where(x => !String.IsNullOrWhiteSpace(x)));

أما إذا كان الملف كبيرًا لتقرأه بالكامل إلى الذاكرة، فيُفضَّل استخدام التابع File.ReadLines الذي يُعيد معدَّد من السَلاسِل النصية IEnumerable<string>‎. بهذه الطريقة، يُمكنك البدء بتعدِّيد التَجمِيعَة ومُعالجتها بدون استرجاعها بالكامل، بِعَكْس التابع File.ReadAllLines الذي لابُدّ عند استخدامه من الانتظار حتي تُقرأ محتويات الملف بالكامل إلى مصفوفة string[]‎، وبعدها يُمكن البدء بعملية المُعالجة.

لكن انتبه، في هذه الحالة، سيختلف مسار كلًا من مَلفّي الدخْل والخْرج.

File.WriteAllLines(outputPath,
                   File.ReadLines(inputPath).Where(x => !String.IsNullOrWhiteSpace(x)));

تغيير ترميز ملف نصي

تُخزَّن النصوص مرمَّزة (enocoded). أحيانًا قد ترغب بتَغْيير الترميز المُستخدَم، وعندها يُمكنك تمرير مُعامِل إضافي من النوع Encoding للتابع WriteAllText؛ لتخصيص الترميز المطلوب، كالمثال التالي:

public static void ConvertEncoding(string path, Encoding from, Encoding to)
{
    File.WriteAllText(path, File.ReadAllText(path, from), to);
}

ملحوظة: افترضنا أن الملف صغيرًا كفاية ليُقرأ بالكامل إلى الذاكرة بغرض التبسيط.

قد يحتوي الملف على BOM. يُمكنك الاطلاع على "لا يأخذ التابع Encoding.UTF8.GetString بالحسبان Preamble/BOM" لفهم أعمق لكيفية التعامل معها.

التعامل مع الملفات بامتداد Zip

عَرْض قائمة محتويات ملف بامتداد Zip

تقوم الشيفرة التالية بعَرْض قائمة بأسماء الملفات الموجودة بملف أرشيفي:

using (FileStream fs = new FileStream("archive.zip", FileMode.Open))
using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read))
{
    for (int i = 0; i < archive.Entries.Count; i++)
    {
        Console.WriteLine($"{i}: {archive.Entries[i]}");
    }
}

لاحظ أن أسماء الملفات تكون نسبية (relative) وفقًا للملف الأرشيفي.

استخراج (Extracting) محتويات ملف بامتداد Zip

يَستخرِج التابع ExtractToDirectory الملفات الموجودة ضِمْن ملف أرشيفي كالتالي:

using (FileStream fs = new FileStream("archive.zip", FileMode.Open))
using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read))
{
    archive.ExtractToDirectory(
        AppDomain.CurrentDomain.BaseDirectory);
}

سيُبلَّغ عن اعتراض من النوع System.IO.IOException إذا كان المجلد المُستخرَج إليه يحتوي على ملف يحمل نفس الاسم.

يُمكنك اِستِخراج ملفات بعينها. فمثلًا، يُستخدَم التابع GetEntry لاختيار ملف معين عن طريق اسمه تمهيدًا لاستخراجه باستخدام التابع ExtractToFile. يُمكنك أيضًا الولوج للخاصية Entries والبحث فيها عن ملف يُحقق شرطًا معينًا، كالتالي:

using (FileStream fs = new FileStream("archive.zip", FileMode.Open))
using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read))
{         
    // اختر ملف بالمجلد الرئيسي
    archive.GetEntry("test.txt")
        .ExtractToFile("test_extracted_getentries.txt", true);

    // أضف مسارًا إذا كنت تريد استهداف ملف بـمجلد فرعي
    archive.GetEntry("sub/subtest.txt")
        .ExtractToFile("test_sub.txt", true);

    archive.Entries
        .FirstOrDefault(f => f.Name == "test.txt")?
        .ExtractToFile("test_extracted_linq.txt", true);
}

إذا اخترت اسم ملف غير موجود، سُيبلَّغ عن اعتراض من النوع System.ArgumentNullException، كالتالي:

using (FileStream fs = new FileStream("archive.zip", FileMode.Open))
using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Read))
{    
    archive.GetEntry("nonexistingfile.txt")
        .ExtractToFile("fail.txt", true);
}

تحديث ملف بامتداد Zip

لتحديث ملف بامتداد zip، يجب فتح الملف باستخدام الوَضْع ZipArchiveMode.Update، حتى تستطيع إضافة ملفات جديدة إليه. يُستخدَم التابع CreateEntryFromFile لإضافة ملف للمجلد الرئيسي أو لمجلد فرعي، كالتالي:

using (FileStream fs = new FileStream("archive.zip", FileMode.Open))
using (ZipArchive archive = new ZipArchive(fs, ZipArchiveMode.Update))
{
    // أضف ملف إلى الأرشيف
    archive.CreateEntryFromFile("test.txt", "test.txt");
    // أضيف ملف إلى مجلد فرعي بالملف الأرشيفي
    archive.CreateEntryFromFile("test.txt", "symbols/test.txt");
}

يُمكن أيضًا الكتابة مباشرة بملف داخل الأرشيف باستخدام التابع CreateEntry، كالتالي:

var entry = archive.CreateEntry("createentry.txt");
using(var writer = new StreamWriter(entry.Open()))
{
    writer.WriteLine("Test line");
}

القراءة من والكتابة إلى المَنافِذ التَسلسُليّة (Serial Ports)

يُوفِّر اطار عمل ‎.NET النوع SerialPort بفضاء الاسم System.IO.Ports للاتصالات التَسلسُليّة (serial communication).

عرض قائمة بأسماء المَنافِذ المتاحة

يُعيدّ التابع SerialPort.GetPortNames()‎ قائمة بأسماء المَنافِذ المتاحة، كالتالي:

using System.IO.Ports;
string[] ports = SerialPort.GetPortNames();
for (int i = 0; i < ports.Length; i++)
{
    Console.WriteLine(ports[i]);
}

تنشئة كائن من النوع SerialPort

يُمثِل كائن من النوع SerialPort مَنفَذ تَسلسُليّ مُعين، ويُستخدَم لإرسال الرسائل النصية واستقبالها عبْر ذلك المَنفَذ. تُنشِئ الشيفرة التالية كائنات من النوع SerialPort باستخدَام بَوانِي الكائن:

using System.IO.Ports;
SerialPort port = new SerialPort();
SerialPort port = new SerialPort("COM 1"); ;
SerialPort port = new SerialPort("COM 1", 9600);

قراءة وكتابة البيانات من وإلى المَنافِذ التَسلسُليّة

يُعدّ اِستخدَام التابعين SerialPort.Read و SerialPort.Write من أسهل الطرائق للقراءة من والكتابة إلى المَنافِذ التَسلسُليّة على الترتيب، كالتالي:

int length = port.BytesToRead;

byte[] buffer = new byte[length];
port.Read(buffer, 0, length);
port.Write("here is some text");
byte[] data = new byte[1] { 255 };
port.Write(data, 0, data.Length);

يُمكِن قراءة جميع البيانات المُتوفِّرة باستخدام التابع ReadExisting:

string curData = port.ReadExisting();

أو قراءة السطر التالي باستخدام التابع ReadLine:

string line = port.ReadLine();

مثال آخر:

var serialPort = new SerialPort("COM1", 9600, Parity.Even, 8, StopBits.One);

serialPort.Open();

serialPort.WriteLine("Test data");
string response = serialPort.ReadLine();
Console.WriteLine(response);

serialPort.Close();

يُمكن أيضًا استخدام التابع SerialPort.BaseStream لتنشئة مَجْرى من النوع System.IO.Stream، واِستخدَامه لكتابة البيانات إلى المَنفَذ.

خدمة صَدَى نصي مُتزامِنة (synchronous)

يُستخدَم التابع ReadLine لقراءة سطر من مَنفَذ من النوع SerialPort بشكل مُتزامِن (synchronous) مما يعني التَسبُّب بتعطيل (blocking) في حالة عدم توفُّر سطر للقراءة.

تَستعرِض الشيفرة التالية خِدمة صدى نصي (text echo) بسيطة بحيث تَبقَى الشيفرة في وَضْع اِستماع للمَنْفَذ إلى حين وصول سطر جديد. عندها يُقرأ السطر ويُعاد إرساله عبر نفس المَنْفَذ إلا في حالة كان يَحوِي السِلسِلة النصية quit.

using System.IO.Ports;
namespace TextEchoService
{
    class Program
    {
        static void Main(string[] args)
        {
            var serialPort = new SerialPort("COM1", 9600, Parity.Even, 8, StopBits.One);
            serialPort.Open();

            string message = "";
            while (message != "quit")
            {
                message = serialPort.ReadLine();
                serialPort.WriteLine(message);
            }

            serialPort.Close();
        }
    }
}

قراءة غير متزامنة (asynchronous)

يُوفِّر النوع SerialPort مجموعة من الأحداث (events) مما يَسمَح بكتابة شيفرة غير مُتزامِنة (asynchronous).

مثلا، يُثار الحَدَث DataReceived عند استقبال بيانات عبر المَنفَذ (port). وبالتالي، يُمكن لمُعالِج حَدَث (event handler) التسجيل (subscribe) بالحَدَث المذكور وقراءة البيانات المُستلَمة بطريقة غير مُتزامِنة، كالمثال التالي:

void SetupAsyncRead(SerialPort serialPort)
{
    serialPort.DataReceived += (sender, e) => {
        byte[] buffer = new byte[4096];
        switch (e.EventType)
        {
            case SerialData.Chars:
                var port = (SerialPort)sender;
                int bytesToRead = port.BytesToRead;
                if (bytesToRead > buffer.Length)
                    Array.Resize(ref buffer, bytesToRead);
                int bytesRead = port.Read(buffer, 0, bytesToRead);
                // يمكنك معالجة البيانات المقروءة هنا

                break;
            case SerialData.Eof:
                // انهي العملية هنا
                break;
        }
    };
}

مُستقبِل رسائل غير مُتزامِن (asynchronous)

تَعرِض الشيفرة التالية مُستقبِل رسائل مَبني على المثال السابق:

using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Text;
using System.Threading;

namespace AsyncReceiver
{
    class Program
    {
        const byte STX = 0x02;
        const byte ETX = 0x03;
        const byte ACK = 0x06;
        const byte NAK = 0x15;
        static ManualResetEvent terminateService = new ManualResetEvent(false);
        static readonly object eventLock = new object();
        static List<byte> unprocessedBuffer = null;

        static void Main(string[] args)
        {
            try
            {
                var serialPort = new SerialPort("COM11", 9600, Parity.Even, 8, StopBits.One);
                serialPort.DataReceived += DataReceivedHandler;
                serialPort.ErrorReceived += ErrorReceivedHandler;
                serialPort.Open();
                terminateService.WaitOne();
                serialPort.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception occurred: {0}", e.Message);
            }
            Console.ReadKey();
        }

        static void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
        {
            lock (eventLock)
            {
                byte[] buffer = new byte[4096];
                switch (e.EventType)
                {
                    case SerialData.Chars:
                        var port = (SerialPort)sender;
                        int bytesToRead = port.BytesToRead;
                        if (bytesToRead > buffer.Length)
                            Array.Resize(ref buffer, bytesToRead);
                        int bytesRead = port.Read(buffer, 0, bytesToRead);
                        ProcessBuffer(buffer, bytesRead);
                        break;
                    case SerialData.Eof:
                        terminateService.Set();
                        break;
                }
            }
        }

        static void ErrorReceivedHandler(object sender, SerialErrorReceivedEventArgs e)
        {
            lock (eventLock)
                if (e.EventType == SerialError.TXFull)
                {
                    Console.WriteLine("Error: TXFull. Can't handle this!");
                    terminateService.Set();
                }
            else
            {
                Console.WriteLine("Error: {0}. Resetting everything", e.EventType);
                var port = (SerialPort)sender;
                port.DiscardInBuffer();
                port.DiscardOutBuffer();
                unprocessedBuffer = null;
                port.Write(new byte[] { NAK }, 0, 1);
            }
        }

        static void ProcessBuffer(byte[] buffer, int length)
        {
            List<byte> message = unprocessedBuffer;
            for (int i = 0; i < length; i++)
                if (buffer[i] == ETX)
                {
                    if (message != null)
                    {
                        Console.WriteLine("MessageReceived: {0}",
                                          Encoding.ASCII.GetString(message.ToArray()));
                        message = null;
                    }
                }
            else if (buffer[i] == STX)
                message = null;
            else if (message != null)
                message.Add(buffer[i]);
            unprocessedBuffer = message;
        }

    }
}

ينتظر البرنامج بالأعلى الرسائل المُضمَّنة بين بايتات STX و ETX، ثم يُرسِل النص الفعلّي إلى مَجْرى الخْرج، أي شئ آخر غيْر ذلك يتم إهماله. يُوقَف البرنامج في حالة حدوث طفحان بالمَخزَن المؤقت (buffer overflow) باستخدام التابع Set. أما في حالة حدوث أي أخطاء أُخرى، يُفرَّغ مَخزَني الدخْل والخْرج المؤقتين باستخدام التابعين DiscardInBuffer و DiscardOutBuffer، ثم تُنتظَر أي رسائل أُخرى.

تُوضِح الشيفرة بالأعلى النقاط التالية:

  • قراءة مَنفَذ تَسلسُليّ بشكل غير مُتزامِن عن طريق التسجيل بالحَدَث SerialPort.DataReceived.
  • مُعالجة أخطاء مَنفَذ تَسلسُليّ عن طريق التسجيل بالحَدَث SerialPort.ErrorReceived.
  • تَّنفيذ لبروتوكول مَبنى على الرسائل غير النصية.
  • قراءة جزئية للرسائل:
  • قد يحدث ذلك؛ ربما لأن الحَدَث SerialPort.DataReceived قد أُثير قبل استلام المَنْفَذ للرسالة بأكملها (وفقًا لـETX)، أو ربما لأن التابع SerialPort.Read(..., ..., port.BytesToRead) لم يقرأ الرسالة بأكملها بل قرأ فقط جزءً منها، مما يترتب عليه عدم تَوفُّر كامل الرسالة بمَخزَن الدخْل المؤقت (input buffer). في هذه الحالة، يُنَحَّى الجزء المُستلَم غيْر المُعالَج (unprocessed) جانبًا لحين استلام بقية الرسالة.
  • التَعامُل مع وصول أكثر من رسالة بدَفْعَة واحدة:
  • قد يُثار الحَدَث SerialPort.DataReceived فقط بعدما يَستلِم عدة رسائل من الطرف الآخر.

ترجمة -وبتصرف- للفصول 16-17-18-19-57-52 من كتاب ‎.NET Framework Notes for Professionals





تفاعل الأعضاء


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن