Chương 2: Vấn đề I/O trong .NET

1. Giới thiệu

I/O (Input/Output) là một trong những vấn đề cốt lõi trong lập trình ứng dụng. Mọi chương trình thực tế đều cần đọc dữ liệu từ đâu đó (file, mạng, bàn phím) và ghi kết quả ra đâu đó (màn hình, file, cơ sở dữ liệu). .NET cung cấp kiến trúc thống nhất dựa trên Stream để xử lý tất cả các loại I/O này một cách nhất quán.


2. Streams

2.1 Khái niệm Stream

Trong .NET, Stream là một lớp trừu tượng (System.IO.Stream) đại diện cho một chuỗi byte có thể được đọc hoặc ghi tuần tự. Thay vì phải viết code khác nhau cho từng thiết bị I/O, .NET sử dụng mô hình stream để chuẩn hoá mọi hoạt động I/O.

Các thiết bị I/O mà stream có thể đại diện bao gồm:

  • Đĩa cứng (file)
  • Mạng (network socket)
  • Bộ nhớ (memory buffer)
  • Máy in, thiết bị ngoại vi khác

2.2 Hướng truyền dữ liệu

Dữ liệu được truyền theo hai chiều:

  • Đọc (Read): Chương trình kéo dữ liệu từ nguồn (source) qua stream vào bộ nhớ chương trình.
  • Ghi (Write): Chương trình đẩy dữ liệu từ bộ nhớ qua stream ra đích (destination).
[Source] ──stream──▶ [Program]   (Đọc)
[Program] ──stream──▶ [Destination]  (Ghi)

2.3 Hai Stream quan trọng nhất

  • FileStream — làm việc với file trên đĩa.
  • NetworkStream — làm việc với kết nối mạng TCP/IP.

2.4 Đồng bộ vs Bất đồng bộ

.NET cung cấp hai cách sử dụng stream:

Chế độMô tảVấn đề
Đồng bộ (Synchronous)Thread hiện tại bị block (tạm ngưng) cho đến khi tác vụ I/O hoàn thànhGiao diện bị “đóng băng” khi đọc file lớn
Bất đồng bộ (Asynchronous)Thread hiện tại tiếp tục chạy, một callback được gọi khi I/O xongPhức tạp hơn nhưng không block UI

3. FileStream — Streams cho tập tin

FileStream là lớp cụ thể để đọc/ghi file nhị phân. Namespace cần dùng: System.IO.

3.1 Ví dụ: Đọc file bất đồng bộ

Khai báo biến:

FileStream fs;
byte[] fileContents;
AsyncCallback callback;
delegate void InfoMessageDel(String info);

Xử lý sự kiện nhấn nút “Đọc bất đồng bộ”:

private void btnReadAsync_Click(object sender, EventArgs e)
{
    openFileDialog.ShowDialog();

    callback = new AsyncCallback(fs_StateChanged);

    // Mở FileStream ở chế độ bất đồng bộ (tham số cuối = true)
    fs = new FileStream(
        openFileDialog.FileName,
        FileMode.Open,
        FileAccess.Read,
        FileShare.Read,
        4096,   // Buffer size: 4096 bytes/lần là hiệu quả nhất
        true    // useAsync = true
    );

    fileContents = new Byte[fs.Length];

    // Bắt đầu đọc bất đồng bộ; callback sẽ được gọi khi xong
    fs.BeginRead(fileContents, 0, (int)fs.Length, callback, null);
}

Hàm callback khi đọc xong:

private void fs_StateChanged(IAsyncResult asyncResult)
{
    if (asyncResult.IsCompleted)
    {
        // Chuyển mảng byte thành string theo encoding UTF-8
        string s = Encoding.UTF8.GetString(fileContents);
        InfoMessage(s);  // Cập nhật UI
        fs.Close();
    }
}

3.2 Ví dụ: Đọc file đồng bộ (chạy trong thread riêng)

Do I/O đồng bộ block thread, ta chạy nó trong một thread riêng để không ảnh hưởng UI:

private void btnReadSync_Click(object sender, EventArgs e)
{
    openFileDialog.ShowDialog();

    // Tạo và khởi động thread mới để đọc file
    Thread thdSyncRead = new Thread(new ThreadStart(syncRead));
    thdSyncRead.Start();
}

public void syncRead()
{
    FileStream fs;
    try
    {
        fs = new FileStream(openFileDialog.FileName, FileMode.OpenOrCreate);
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
        return;
    }

    fs.Seek(0, SeekOrigin.Begin);   // Di chuyển con trỏ về đầu file
    byte[] fileContents = new byte[fs.Length];
    fs.Read(fileContents, 0, (int)fs.Length);

    string s = Encoding.UTF8.GetString(fileContents);
    InfoMessage(s);
    fs.Close();
}

3.3 Thread-safe UI update với Invoke

Khi một thread phụ cần cập nhật UI (chỉ UI thread mới được phép làm điều này), ta dùng InvokeRequired + Invoke:

public void InfoMessage(String info)
{
    if (tbResults.InvokeRequired)
    {
        // Đang ở thread khác → marshal về UI thread
        InfoMessageDel method = new InfoMessageDel(InfoMessage);
        tbResults.Invoke(method, new object[] { info });
        return;
    }

    // Đang ở UI thread → cập nhật trực tiếp
    tbResults.Text = info;
}

3.4 Bảng phương thức/thuộc tính của FileStream

Phương thức / Thuộc tínhMục đích
ConstructorKhởi tạo FileStream với tên file, FileMode, FileAccess, FileShare, buffer size, async flag
Read(byte[], offset, count)Đọc đồng bộ count byte vào mảng
Write(byte[], offset, count)Ghi đồng bộ count byte từ mảng
BeginRead(...)Bắt đầu đọc bất đồng bộ
EndRead(IAsyncResult)Kết thúc tác vụ đọc bất đồng bộ
Seek(offset, SeekOrigin)Di chuyển con trỏ đến vị trí cụ thể
Close()Đóng stream và giải phóng tài nguyên
LengthKích thước file tính bằng byte
PositionVị trí con trỏ hiện tại
CanRead, CanWrite, CanSeekKhả năng của stream

4. Encoding Data

Khi đọc file text, mảng byte cần được giải mã thành chuỗi string theo đúng bảng mã.

// Các cách dùng Encoding:
string s1 = Encoding.UTF8.GetString(fileContents);
string s2 = Encoding.Unicode.GetString(fileContents);   // UTF-16
string s3 = Encoding.ASCII.GetString(fileContents);
string s4 = Encoding.UTF32.GetString(fileContents);
EncodingSố byte/ký tựGhi chú
ASCII1 byteChỉ hỗ trợ 128 ký tự Latin cơ bản
UTF-81–4 bytePhổ biến nhất trên web; tương thích ASCII
Unicode (UTF-16)2 byteDùng trong .NET nội bộ, hỗ trợ đầy đủ Unicode
UTF-324 byteCố định, hỗ trợ mọi ký tự Unicode

5. Binary và Text Streams

.NET cung cấp các lớp chuyên biệt bọc trên FileStream để đọc/ghi dữ liệu có cấu trúc hơn.

5.1 StreamReader — Đọc file text

StreamReader giúp đọc file text theo từng dòng, tiện lợi hơn nhiều so với đọc mảng byte thô.

Ví dụ: Đếm số dòng trong file

private void btnRead_Click(object sender, EventArgs e)
{
    OpenFileDialog ofd = new OpenFileDialog();
    ofd.ShowDialog();

    FileStream fs = new FileStream(ofd.FileName, FileMode.OpenOrCreate);
    StreamReader sr = new StreamReader(fs);

    int lineCount = 0;
    while (sr.ReadLine() != null)
    {
        lineCount++;
    }

    fs.Close();
    MessageBox.Show($"There are {lineCount} lines in {ofd.FileName}");
}

Bảng phương thức StreamReader:

Phương thức / Thuộc tínhMục đích
Constructor(Stream)Khởi tạo từ một stream hoặc đường dẫn file
ReadLine()Đọc một dòng, trả về null khi hết file
ReadToEnd()Đọc toàn bộ nội dung còn lại thành một string
Read()Đọc một ký tự đơn
Peek()Xem ký tự tiếp theo mà không tiêu thụ nó
EndOfStreamtrue nếu đã đọc hết
Close()Đóng StreamReader và stream bên dưới

5.2 BinaryWriter — Ghi dữ liệu nhị phân

BinaryWriter cho phép ghi các kiểu dữ liệu nguyên thủy (int, float, bool, string…) vào stream ở dạng nhị phân compact, không phải text.

Ví dụ: Ghi mảng 1000 số nguyên ra file nhị phân:

private void btnWrite_Click(object sender, EventArgs e)
{
    SaveFileDialog sfd = new SaveFileDialog();
    sfd.ShowDialog();

    FileStream fs = new FileStream(sfd.FileName, FileMode.CreateNew);
    BinaryWriter bw = new BinaryWriter(fs);

    int[] myArray = new int[1000];
    for (int i = 0; i < 1000; i++)
    {
        myArray[i] = i;
        bw.Write(myArray[i]);   // Ghi từng int (4 byte) vào file
    }

    bw.Close();   // Tự động flush và đóng stream bên dưới
}

Bảng phương thức BinaryWriter:

Phương thức / Thuộc tínhMục đích
ConstructorKhởi tạo từ một Stream
Write(value)Ghi các kiểu: bool, byte, char, double, float, int, long, string, v.v.
Seek(offset, SeekOrigin)Định vị con trỏ trên stream
Write7BitEncodedInt(int)Ghi số nguyên 32-bit dạng nén (tiết kiệm byte với số nhỏ)
Close()Đóng BinaryWriter và stream liên quan

6. Serialization

6.1 Khái niệm

Serialization là quá trình chuyển đổi một đối tượng .NET trong bộ nhớ thành một luồng byte để có thể:

  • Lưu xuống đĩa (persistence).
  • Truyền qua mạng (network transmission).
  • Sao chép đối tượng (deep copy).

Deserialization là quá trình ngược lại: từ luồng byte, tái tạo lại đối tượng trong bộ nhớ.

[Object in RAM] ──serialize──▶ [Byte stream] ──deserialize──▶ [Object in RAM]

6.2 Ví dụ Domain Model: Hệ thống đặt hàng

public enum purchaseOrderStates
{
    ISSUED,
    DELIVERED,
    INVOICED,
    PAID
}

[Serializable]   // Bắt buộc phải có attribute này!
public class lineItem
{
    public string description;
    public int quantity;
    public float unitCost;
}

[Serializable]
public class company
{
    public string name;
    public string phone;
}

[Serializable]
public class purchaseOrder
{
    private DateTime _issuanceDate;
    private DateTime _deliveryDate;
    private purchaseOrderStates _purchaseOrderStatus;

    public company vendor;
    public company buyer;
    public lineItem[] items;

    public purchaseOrder()
    {
        _purchaseOrderStatus = purchaseOrderStates.ISSUED;
        _issuanceDate = DateTime.Now;
    }
}

6.3 Serialization dùng SoapFormatter (XML/SOAP)

SOAP (Simple Object Access Protocol) serialize object thành định dạng XML, dễ đọc và tương thích nhiều nền tảng.

Serialize:

// Tạo các đối tượng
company Vendor = new company();
Vendor.name = "Acme Inc.";
Vendor.phone = "555-1234";

company Buyer = new company();
Buyer.name = "Wiley Coyote";

lineItem Goods = new lineItem();
Goods.description = "Anti-RoadRunner cannon";
Goods.quantity = 1;
Goods.unitCost = 599.99f;

purchaseOrder po = new purchaseOrder();
po.items = new lineItem[1];
po.items[0] = Goods;
po.buyer = Buyer;
po.vendor = Vendor;

// Ghi ra file dùng SoapFormatter
SoapFormatter sf = new SoapFormatter();
FileStream fs = new FileStream("order.soap", FileMode.Create);
sf.Serialize(fs, po);
fs.Close();

Deserialize:

SoapFormatter sf = new SoapFormatter();
FileStream fs = new FileStream("order.soap", FileMode.Open);
purchaseOrder po = (purchaseOrder)sf.Deserialize(fs);
fs.Close();

// Sử dụng object vừa khôi phục
Console.WriteLine($"Customer: {po.buyer.name}");
Console.WriteLine($"Vendor: {po.vendor.name}");

6.4 Serialization dùng BinaryFormatter

Định dạng SOAP dễ đọc nhưng khá “nặng” (verbose XML). BinaryFormatter serialize thành dạng nhị phân compact hơn, nhưng không thể đọc bằng mắt thường.

// Serialize
BinaryFormatter bf = new BinaryFormatter();
FileStream fs = new FileStream("order.bin", FileMode.Create);
bf.Serialize(fs, po);
fs.Close();

// Deserialize
BinaryFormatter bf = new BinaryFormatter();
FileStream fs = new FileStream("order.bin", FileMode.Open);
purchaseOrder po = (purchaseOrder)bf.Deserialize(fs);
fs.Close();
Tiêu chíSoapFormatterBinaryFormatter
Định dạng outputXML textBinary
Kích thước fileLớn (verbose)Nhỏ (compact)
Đọc được bằng mắt✅ Có❌ Không
Hiệu năngChậm hơnNhanh hơn
Tương thích nền tảngTốt hơn (XML standard)Chỉ .NET CLR

6.5 Shallow Serialization và XmlSerializer

BinaryFormatterSoapFormatter thực hiện deep serialization — tất cả các field, kể cả private, đều được serialize. Trong một số trường hợp điều này gây ra vấn đề khi clone object.

Shallow Serialization với XmlSerializer chỉ serialize các public property/field, và sử dụng XML Schema Definition (XSD) để mô tả cấu trúc, đảm bảo tương thích đa nền tảng.

// Serialize bằng XmlSerializer
company Vendor = new company();
Vendor.name = "Microsoft Inc.";
Vendor.phone = "425-555-1234";

// ... (tạo object tương tự)

XmlSerializer xs = new XmlSerializer(typeof(purchaseOrder));
FileStream fs = new FileStream("order.xml", FileMode.Create);
xs.Serialize(fs, po);
fs.Close();
// Deserialize bằng XmlSerializer
purchaseOrder po = new purchaseOrder();
XmlSerializer xs = new XmlSerializer(typeof(purchaseOrder));
FileStream fs = new FileStream("order.xml", FileMode.Open);
po = (purchaseOrder)xs.Deserialize(fs);
fs.Close();

Bảng phương thức XmlSerializer:

Phương thức / Thuộc tínhMục đích
Constructor(Type)Khởi tạo cho một kiểu cụ thể
Serialize(Stream, object)Serialize object thành XML
Deserialize(Stream)Deserialize XML thành object
CanDeserialize(XmlReader)Kiểm tra có thể deserialize document đó không
FromTypes(Type[])Tạo mảng XmlSerializer cho nhiều kiểu

7. Ghi một Database vào Stream

Hầu hết ứng dụng thương mại đều dùng cơ sở dữ liệu. Để truyền dữ liệu qua mạng hoặc lưu snapshot, ta cần serialize kết quả query vào stream.

7.1 Kết nối CSDL — Connection Strings

Namespace cần dùng:

  • System.Data.OleDb — cho Access, Oracle, v.v.
  • System.Data.SqlClient — cho SQL Server.

Chuỗi kết nối theo loại CSDL:

Loại DatabaseConnection String mẫu
Microsoft AccessProvider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\path\db.mdb
SQL ServerServer=.\SQLEXPRESS;Database=myDB;Trusted_Connection=True
OracleProvider=MSDAORA;Data Source=MyOracle;User ID=sa;Password=...

Ví dụ kết nối SQL Server:

private static string strCon;
public static SqlConnection Connect(string serverName, string dbName)
{
    strCon = $"Server={serverName};Database={dbName};Trusted_Connection=True";
    SqlConnection conn = new SqlConnection(strCon);
    try
    {
        conn.Open();
        return conn;
    }
    catch (Exception e)
    {
        MessageBox.Show($"Chi tiết kỹ thuật: {e.Message}");
        return null;
    }
}

Ví dụ kết nối Access (OleDb):

public static OleDbConnection Connect(string dbPath)
{
    strCon = $"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={dbPath}";
    OleDbConnection conn = new OleDbConnection(strCon);
    try
    {
        conn.Open();
        return conn;
    }
    catch (Exception e)
    {
        MessageBox.Show($"Chi tiết kỹ thuật: {e.Message}");
        return null;
    }
}

7.2 Bốn thao tác CRUD cơ bản

Thao tácCâu lệnh SQL tổng quát
Đọc dữ liệuSELECT * FROM table WHERE column = 'value'
Thêm dòng mớiINSERT INTO table (col1, col2) VALUES ('val1', 'val2')
Cập nhật dòngUPDATE table SET column = 'value' WHERE column = 'cond'
Xóa dòngDELETE FROM table WHERE column = 'value'

7.3 DataSet Serialization — Xuất CSDL ra Stream

DataSet là đại diện in-memory của dữ liệu từ database, và nó hỗ trợ serialize trực tiếp sang XML.

Thực hiện query và đưa vào DataSet:

public DataSet Query(string sql, OleDbConnection conn)
{
    OleDbCommand cmd = new OleDbCommand(sql, conn);
    OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
    DataSet ds = new DataSet();
    adapter.Fill(ds);
    return ds;
}

Serialize DataSet ra XML:

string szDSN = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=mydb.mdb";
OleDbConnection conn = new OleDbConnection(szDSN);
conn.Open();

OleDbCommand cmd = new OleDbCommand(tbSQL.Text, conn);
OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
DataSet ds = new DataSet();
adapter.Fill(ds);

// Serialize DataSet thành XML stream
StringWriter sw = new StringWriter();
ds.WriteXml(sw);
tbResults.Text = sw.ToString();   // Hiển thị XML lên textbox

Kết quả XML trông như sau:

<NewDataSet>
  <companies diffgr:id="companies1" msdata:rowOrder="0">
    <id>1</id>
    <name>Wiley E. Coyote</name>
  </companies>
  <companies diffgr:id="companies2" msdata:rowOrder="1">
    <id>2</id>
    <name>Acme Inc.</name>
  </companies>
</NewDataSet>

8. Tổng kết

graph TD A[System.IO.Stream] --> B[FileStream] A --> C[NetworkStream] A --> D[MemoryStream] B --> E[StreamReader] B --> F[StreamWriter] B --> G[BinaryReader] B --> H[BinaryWriter] I[Serialization] --> J[SoapFormatter] I --> K[BinaryFormatter] I --> L[XmlSerializer] I --> M[DataSet.WriteXml]