Các tính năng và kỹ thuật nâng cao trong C#

Danh sách (List)

1. Phần tử bên trong là kiểu tham chiếu

src[0].Name là "11", cho thấy tham chiếu đã bị sửa đổi.

List<NguonDuLieu> danhSachNguon = new List<NguonDuLieu>();
danhSachNguon.Add(new NguonDuLieu() { Ten = "1", Tuoi = 1, MoTa = "1" });
danhSachNguon.Add(new NguonDuLieu() { Ten = "2", Tuoi = 2, MoTa = "2" });
foreach (var nguon in danhSachNguon)
{
    nguon.Ten = "11";
}
Console.WriteLine(danhSachNguon[0].Ten);

2. Phần tử bên trong không phải là kiểu tham chiếu

str[0] = "1", cho thấy bản sao đã được sửa đổi.

List<string> danhSachChuoi = new List<string>();
danhSachChuoi.Add("1");
danhSachChuoi.Add("2");
for (int i = 0; i < danhSachChuoi.Count; i++)
{
    danhSachChuoi[i] = "11";
}
Console.WriteLine(danhSachChuoi[0]);

Chương trình bất đồng bộ (Asynchronous)

1. wait() và await

  • Sử dụng wait() sẽ làm cho tác vụ bị chặn, không thể thoát ra.
async void vongLapVoHuong()
{
    while (true)
    { 
        Task.Delay(1000).Wait();
    }  
}
  • Sử dụng await sẽ trả về điểm gọi sau khi vào tác vụ.
async void vongLapVoHuong()
{
    while (true)
    { 
        await Task.Delay(1000);
    }  
}

2. SemaphoreSlim

Dùng để thay thế cho Lock, SemaphoreSlim cho phép sử dụng await bên trong.

SemaphoreSlim semaphoreTieuChuan = new SemaphoreSlim(1, 1);
await thucHienTest();
Console.WriteLine("hoan thanh");

async Task thucHienTest()
{
    Task congViec1 = Task.Run(() => phanVungCritial("Cong Viec 1"));
    Task congViec2 = Task.Run(() => phanVungCritial("Cong Viec 2"));

    await Task.WhenAll(congViec1, congViec2);
}
async Task phanVungCritial(string tenCongViec)
{
    await semaphoreTieuChuan.WaitAsync(); // Yêu cầu bất đồng bộ để vào vùng quan trọng
    try
    {
        Console.WriteLine($"{tenCongViec} da vao phan vung critial.");
        // Mô phỏng thao tác bất đồng bộ
        await Task.Delay(1000);
        Console.WriteLine($"{tenCongViec} da roi khoi phan vung critial.");
    }
    finally
    {
        semaphoreTieuChuan.Release(); // Đảm bảo giải phóng semaphore
    }
}

3. await

  • thucHienTest sẽ bị chặn, có thể vì đây là await cuối cùng.
  • congViec1 và congViec2 do semaphore sẽ không cùng vào, và đã bắt đầu khi gọi Task.run.
  • Khi gặp await trong hàm bất đồng bộ, nó sẽ trả về nơi gọi, đợi đến khi tác vụ await hoàn thành, hàm mới tiếp tục.
SemaphoreSlim semaphoreTieuChuan = new SemaphoreSlim(1, 1);
await thucHienTest();
Console.WriteLine("hoan thanh");
async Task thucHienTest()
{
    Task congViec1 = Task.Run(() => phanVungCritial("Cong Viec 1"));
    Task congViec2 = Task.Run(() => phanVungCritial("Cong Viec 2"));
    await Task.Delay(1000);
    var dongHo = Stopwatch.StartNew();
    await Task.WhenAll(congViec1, congViec2);
    Console.WriteLine("Khi tat ca hoan thanh: " + dongHo.ElapsedMilliseconds);
}
async Task phanVungCritial(string tenCongViec)
{
    await semaphoreTieuChuan.WaitAsync();
    try
    {
        Console.WriteLine($"{tenCongViec} da vao phan vung critial.");
        await Task.Delay(2000);
        Console.WriteLine($"{tenCongViec} da roi khoi phan vung critial.");
    }
    finally
    {
        semaphoreTieuChuan.Release(); // Đảm bảo giải phóng semaphore
    }
}
  • Bỏ await trước thucHienTest() sẽ không bị chặn, thực hiện "hoan thanh" ngay, sau đó khi congViec1-2 hoàn thành, in ra "Khi tat ca hoan thanh: 3011", cho thấy khi gặp await trong hàm bất đồng bộ, nó sẽ trả về nơi gọi (thucHienTest), sau đó đến "hoan thanh", đợi đến khi tác vụ await (WhenAll) hoàn thành, hàm mới tiếp tục.
  • Thêm log bắt đầu và kết thúc延时, có thể thấy sau khi thực hiện await Task.Delay(1000) đã trả về nơi gọi thucHienTest, in ra "hoan thanh". Đợi 1000ms延时 kết thúc, mới tiếp tục thực hiện trong hàm thucHienTest, in ra log kết thúc延时.

4. Hủy và hủy sau thời gian chờ

Nếu bị hủy, token.IsCancellationRequested sẽ là true.

// Tạo CancellationTokenSource
var nguonTokenHuyBo = new CancellationTokenSource();
// Đặt thời gian chờ là 5 giây
nguonTokenHuyBo.CancelAfter(TimeSpan.FromSeconds(5));
// Bắt đầu một tác vụ có thể bị hủy
var congViec = Task.Run(() => lamViec(nguonTokenHuyBo.Token), nguonTokenHuyBo.Token);

try
{
    await congViec;
}
catch (OperationCanceledException)
{
    Console.WriteLine("Tác vụ bị hủy (hết thời gian chờ).");
}

static void lamViec(CancellationToken token)
{
    for (int i = 0; i < 10; i++)
    {
        // Kiểm tra nếu có yêu cầu hủy
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("Yêu cầu hủy, thoát khỏi công việc...");
            return;
        }

        Console.WriteLine($"Dang lam viec... {i}");
        Thread.Sleep(1000); // Mô phỏng công việc
    }
}

5. await và luồng (thread)

  • Mã nguồn
Vidu vidu = new Vidu();
Console.WriteLine($"ID luong truoc await trong ham chinh: {Thread.CurrentThread.ManagedThreadId}");
await vidu.XuLyDuLieuBatDongBo();
Console.WriteLine($"ID luong sau await trong ham chinh: {Thread.CurrentThread.ManagedThreadId}");
Console.WriteLine("Hoan thanh thuc hien du lieu");

public class Vidu
{
    public async Task XuLyDuLieuBatDongBo()
    {
        Console.WriteLine("1. Bat dau xu ly");
        Console.WriteLine($"ID luong truoc await trong ham con: {Thread.CurrentThread.ManagedThreadId}");
        var duLieu = await TaiDuLieuBatDongBo();
        Console.WriteLine($"ID luong sau await trong ham con: {Thread.CurrentThread.ManagedThreadId}");
        Console.WriteLine("3. Hoan thanh xu ly du lieu");
    }
    public async Task<string> TaiDuLieuBatDongBo()
    {
        Console.WriteLine("2. Dang tai du lieu");
        Console.WriteLine($"ID luong truoc await trong ham delay: {Thread.CurrentThread.ManagedThreadId}");
        await Task.Delay(1000);
        Console.WriteLine($"ID luong sau await trong ham delay: {Thread.CurrentThread.ManagedThreadId}");
        return "Du lieu";
    }
}
  • Kết quả
ID luong truoc await trong ham chinh: 1
1. Bat dau xu ly
ID luong truoc await trong ham con: 1
2. Dang tai du lieu
ID luong truoc await trong ham delay: 1
ID luong sau await trong ham delay: 3
ID luong sau await trong ham con: 3
ID luong sau await trong ham chinh: 3
Hoan thanh thuc hien du lieu

Nếu không có delay, luồng sẽ không bị chuyển đổi.

6. Thao tác bất đồng bộ thực sự

Thao tác bất đồng bộ thực sự thường bao gồm:

  • Hoạt động I/O (tệp, mạng, cơ sở dữ liệu)
// Hoạt động tệp
await File.ReadAllTextAsync("file.txt");
// Yêu cầu mạng
await httpClient.GetAsync("https://api.example.com");
// Hoạt động cơ sở dữ liệu
await dbContext.Users.ToListAsync();
  • Task.Delay (đồng hồ bấm giờ)
  • Các hoạt động khác cần chờ tài nguyên bên ngoài
// Tính toán nặng CPU (chuyển đổi hoạt động đồng bộ thành bất đồng bộ bằng Task.Run)
await Task.Run(() => {
    // Tính toán phức tạp
    for(int i = 0; i < 1000000; i++) { ... }
});

Mà chỉ đánh dấu là async và trả về Task, nếu bên trong không có thao tác bất đồng bộ thực sự, thực tế vẫn thực thi đồng bộ. Đây là lý do tại sao trước khi gặp Task.Delay, tất cả mã đều chạy trên cùng một luồng. Cũng chính vì vậy chúng ta thường nói: "async một đường" (async all the way) - Trong chuỗi gọi bất đồng bộ, tính bất đồng bộ thực sự đến từ hoạt động bất đồng bộ thực sự ở tầng thấp nhất, còn các tầng trên chủ yếu để truyền tính chất này.

Hoạt động không thực sự bất đồng bộ thường bao gồm:

// 1. Chỉ bao bọc giá trị trả về
public async Task<int> KhongThucSuBatDongBo()
{
    return 42;  // Trả về ngay lập tức, không có thao tác tốn thời gian
}
// 2. Hàm bất đồng bộ rỗng
public async Task RongBatDongBo()
{
    // Không làm gì cả
}
// 3. Chỉ làm tính toán đơn giản
public async Task<int> TinhToanDonGian()
{
    var ketQua = 1 + 1;
    return ketQua;
}

Mục đích sử dụng hoạt động bất đồng bộ chính là:

  • Tránh chặn luồng chính (đặc biệt là luồng UI)
  • Tăng khả năng phản hồi của ứng dụng
  • Sử dụng tài nguyên hệ thống hiệu quả hơn

Vì vậy, khi thiết kế phương thức bất đồng bộ, chúng ta nên cân nhắc:

  • Hoạt động này có thực sự cần chờ không?
  • Có liên quan đến I/O hoặc hoạt động tốn thời gian khác không?
  • Có chặn luồng không?

Nếu tất cả câu trả lời đều là không, thì phương thức này có thể không cần thiết phải thiết kế thành bất đồng bộ.

7. Ba cách viết vòng lặp nền

1. Sử dụng BackgroundService (khuyến nghị, phù hợp với .NET Core/.NET 5+)

  • try viết trong while, có token hủy, nếu hủy thì thoát while
public class DichVuNenCuaToi : BackgroundService
{
    private readonly ILogger<DichVuNenCuaToi> _logger;

    public DichVuNenCuaToi(ILogger<DichVuNenCuaToi> logger)
    {
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken tokenHuyBo)
    {
        while (!tokenHuyBo.IsCancellationRequested)
        {
            try
            {
                _logger.LogInformation("Dich vu nen dang chay...");
                // Thực hiện tác vụ vòng lặp của bạn
                await lamViecNenAsync();
                
                // Có thể thêm延时 để tránh thực hiện quá thường xuyên
                await Task.Delay(1000, tokenHuyBo);
            }
            catch (OperationCanceledException)
            {
                // Hủy hoạt động bình thường, không cần xử lý đặc biệt
                break;
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Dich vu nen xay ra loi");
                // Có thể thêm延时 rồi thử lại
                await Task.Delay(5000, tokenHuyBo);
            }
        }
    }

    private async Task lamViecNenAsync()
    {
        // Thực hiện tác vụ cụ thể của bạn
    }
}

// Trong Program.cs, đăng ký dịch vụ
services.AddHostedService<DichVuNenCuaToi>();

2. Vòng lặp bất đồng bộ đơn giản

public class TácVụNềnĐơnGiản
{
    private CancellationTokenSource _nguonTokenHuyBo;
    private Task _tácVụNền;

    public void BatDau()
    {
        _nguonTokenHuyBo = new CancellationTokenSource();
        _tácVụNền = chayBatDongBo(_nguonTokenHuyBo.Token);
    }

    private async Task chayBatDongBo(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            try
            {
                // Thực hiện tác vụ vòng lặp của bạn
                Console.WriteLine($"Thuc hien cong viec - {DateTime.Now}");

                // Thêm延时 phù hợp
                await Task.Delay(1000, token);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Xay ra loi: {ex.Message}");
                await Task.Delay(5000, token);
            }
        }
    }

    public async Task DungAsync()
    {
        if (_nguonTokenHuyBo != null)
        {
            _nguonTokenHuyBo.Cancel();
            await _tácVụNền;
            _nguonTokenHuyBo.Dispose();
        }
    }
}

// Sử dụng ví dụ
var tácVụ = new TácVụNềnĐơnGiản();
tácVụ.BatDau();
// ... 
await tácVụ.DungAsync(); // Dừng tác vụ

8. Không thể dùng out làm tham số cho Task

Như đã nói, vì vậy kết quả trả về của các thao tác cơ sở dữ liệu thường chỉ là dữ liệu cuối cùng, sau đó kiểm tra xem có rỗng hay không, hoặc truy vấn kiểm tra thành công, trả về boolean hoặc số nguyên.

Ghi chú về kết nối lại

using System;  
using System.Threading;  
using ThuVienPlcCuaBan; // Thay thế bằng thư viện PLC thực tế  

public class KetNoiPlc  
{  
    private YourPlcClient khachHangPlc; // Thể hiện client PLC  
    private string diaChiIpPlc;  
    private int congPlc;  
    private int soLanThuKetNoiLai = 5; // Số lần thử kết nối lại  
    private bool daKetNoi = false; // Trạng thái kết nối  
    private readonly object doiTuongKhoa = new(); // Đối tượng khóa, dùng cho an toàn luồng  

    public KetNoiPlc(string ip, int port)  
    {  
        diaChiIpPlc = ip;  
        congPlc = port;  
        khachHangPlc = new YourPlcClient(diaChiIpPlc, congPlc);  
    }  
    // Kết nối PLC  
    private bool KetNoi()  
    {  
        try  
        {  
            if (!daKetNoi) // Chỉ thực hiện kết nối khi chưa kết nối  
            {  
                daKetNoi = khachHangPlc.Connect();  
                if (!daKetNoi)  
                {  
                    Console.WriteLine("Ket noi that bai.");  
                }  
            }  
            return daKetNoi;  
        }  
        catch (Exception ex)  
        {  
            Console.WriteLine($"Ket noi that bai: {ex.Message}");  
            return false;  
        }  
    }  
    // Ngắt kết nối  
    private void NgatKetNoi()  
    {  
        if (daKetNoi)  
        {  
            khachHangPlc.Disconnect();  
            daKetNoi = false;  
            Console.WriteLine("Da ngat ket noi voi PLC.");  
        }  
    }  
    // Đọc dữ liệu  
    public int DocDuLieu(string diaChi)  
    {  
        int giaTri = 0;  
        for (int thu = 0; thu < soLanThuKetNoiLai; thu++)  
        {  
            lock (doiTuongKhoa) // Đảm bảo chỉ một luồng thử kết nối tại một thời điểm  
            {  
                KetNoi(); // Thử kết nối  
            }  

            if (daKetNoi)  
            {  
                try  
                {  
                    giaTri = khachHangPlc.ReadInt(diaChi); // Giả sử ReadInt là phương thức đọc số nguyên  
                    return giaTri; // Trả về dữ liệu đã đọc  
                }  
                catch (Exception ex)  
                {  
                    Console.WriteLine($"Doc du lieu that bai: {ex.Message}");  
                }  
                finally  
                {  
                    NgatKetNoi(); // Đảm bảo ngắt kết nối  
                }  
            }  
            Thread.Sleep(1000); // Chờ một lúc rồi thử kết nối lại  
        }  
        Console.WriteLine("Khong the doc du lieu, tat ca cac thu ket noi lai that bai.");  
        return giaTri; // Trả về 0 hoặc giá trị mặc định phù hợp nếu đọc thất bại  
    }  
    // Ghi dữ liệu  
    public bool GhiDuLieu(string diaChi, int giaTri)  
    {  
        for (int thu = 0; thu < soLanThuKetNoiLai; thu++)  
        {  
            lock (doiTuongKhoa) // Đảm bảo chỉ một luồng thử kết nối tại một thời điểm  
            {  
                KetNoi(); // Thử kết nối  
            }  

            if (daKetNoi)  
            {  
                try  
                {  
                    khachHangPlc.WriteInt(diaChi, giaTri); // Giả sử WriteInt là phương thức ghi số nguyên  
                    return true; // Trả về thành công  
                }  
                catch (Exception ex)  
                {  
                    Console.WriteLine($"Ghi du lieu that bai: {ex.Message}");  
                }  
                finally  
                {  
                    NgatKetNoi(); // Đảm bảo ngắt kết nối  
                }  
            }  
            Thread.Sleep(1000); // Chờ một lúc rồi thử kết nối lại  
        }  
        Console.WriteLine("Khong the ghi du lieu, tat ca cac thu ket noi lai that bai.");  
        return false; // Trả về false nếu ghi thất bại  
    }  
}  

ASP.NET Core

Thứ tự thực thi

  1. Thực thi mã cấu hình trong Program.cs trước:
    • Đăng ký dịch vụ (builder.Services.AddXXX)
    • Cấu hình middleware
    • Cấu hình container dependency injection
  2. Khi gọi app.Build():
    • Xây dựng container dependency injection
    • Phân giải tất cả các dịch vụ đã đăng ký
  3. Khi gọi app.Run():
    • Bắt đầu host web
    • Bắt đầu tất cả các BackgroundService đã đăng ký
// 1. Đăng ký BackgroundService trước
builder.Services.AddHostedService<DichVuNen>();
// 2. Đăng ký các dịch vụ khác
builder.Services.AddSingleton();
// ... các dịch vụ khác ...
// 3. Xây dựng ứng dụng
var app = builder.Build();  // Lúc này container dependency injection được xây dựng
// 4. Cấu hình middleware
app.UseSession();
// ... các middleware khác ...

// 5. Bắt đầu ứng dụng
app.Run();  // Lúc này BackgroundService bắt đầu thực thi

Lưu ý quan trọng:

  • BackgroundService chỉ bắt đầu thực thi khi ứng dụng khởi động (app.Run())
  • Trước khi BackgroundService thực thi, tất cả dependency injection đã hoàn thành
  • Bạn có thể sử dụng an toàn bất kỳ dịch vụ đã inject nào trong BackgroundService
  • Nếu bạn muốn đảm bảo một số khởi tạo hoàn thành trước BackgroundService, bạn có thể:
public class DichVuNen : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken tokenHuyBo)
    {
        // Đảm bảo tất cả khởi tạo hoàn thành
        await doiKhoiTaoHoanThanh();

        while (!tokenHuyBo.IsCancellationRequested)
        {
            try
            {
                // Mã tác vụ nền của bạn
            }
            catch (Exception ex)
            {
                // Xử lý lỗi
            }
            await Task.Delay(1000, tokenHuyBo);
        }
    }
}

Dependency Injection

AddSingleton (Đơn lẻ)

  • Tạo một instance duy nhất cho toàn bộ ứng dụng
  • Tất cả các yêu cầu đều chia sẻ một instance
  • Phù hợp với: thông tin cấu hình, cache, dịch vụ ghi log không cần thay đổi thường xuyên
public class DichVuDonLe
{
    private readonly Guid _id;
    public DichVuDonLe()
    {
        _id = Guid.NewGuid();
    }
    public string LayId() => $"ID Don Le: {_id}";
}

AddScoped (Theo phạm vi)

  • Tạo một instance mới cho mỗi yêu cầu (HTTP Request)
  • Các dịch vụ trong cùng một yêu cầu chia sẻ một instance
  • Phù hợp với: ngữ cảnh cơ sở dữ liệu, dịch vụ phiên người dùng
public class DichVuTheoPhamVi
{
    private readonly Guid _id;
    public DichVuTheoPhamVi()
    {
        _id = Guid.NewGuid();
    }
    public string LayId() => $"ID Theo Pham Vi: {_id}";
}

AddTransient (Tạm thời)

  • Tạo một instance mới mỗi khi yêu cầu dịch vụ
  • Ngay cả trong cùng một yêu cầu, mỗi lần lấy dịch vụ cũng là instance mới
  • Phù hợp với: dịch vụ nhẹ, không trạng thái
public class DichVuTamThoi
{
    private readonly Guid _id;
    public DichVuTamThoi()
    {
        _id = Guid.NewGuid();
    }
    public string LayId() => $"ID Tam Thoi: {_id}";
}

Kịch bản

Dưới đây là một ví dụ về controller để minh họa sự khác biệt:

public class BoDieuKhienVidu : Controller
{
    private readonly DichVuDonLe _dichVuDonLe1;
    private readonly DichVuDonLe _dichVuDonLe2;
    private readonly DichVuTheoPhamVi _dichVuTheoPhamVi1;
    private readonly DichVuTheoPhamVi _dichVuTheoPhamVi2;
    private readonly DichVuTamThoi _dichVuTamThoi1;
    private readonly DichVuTamThoi _dichVuTamThoi2;

    public BoDieuKhienVidu(
        DichVuDonLe dichVuDonLe1,
        DichVuDonLe dichVuDonLe2,
        DichVuTheoPhamVi dichVuTheoPhamVi1,
        DichVuTheoPhamVi dichVuTheoPhamVi2,
        DichVuTamThoi dichVuTamThoi1,
        DichVuTamThoi dichVuTamThoi2)
    {
        _dichVuDonLe1 = dichVuDonLe1;
        _dichVuDonLe2 = dichVuDonLe2;
        _dichVuTheoPhamVi1 = dichVuTheoPhamVi1;
        _dichVuTheoPhamVi2 = dichVuTheoPhamVi2;
        _dichVuTamThoi1 = dichVuTamThoi1;
        _dichVuTamThoi2 = dichVuTamThoi2;
    }

    public IActionResult Index()
    {
        var ketQua = new List<string>
        {
            // Singleton - ID giống nhau khi lấy hai lần
            _dichVuDonLe1.LayId(),  // Ví dụ: "ID Don Le: abc..."
            _dichVuDonLe2.LayId(),  // ID giống: "ID Don Le: abc..."
            
            // Scoped - ID giống nhau trong cùng một yêu cầu
            _dichVuTheoPhamVi1.LayId(),     // Ví dụ: "ID Theo Pham Vi: def..."
            _dichVuTheoPhamVi2.LayId(),     // ID giống: "ID Theo Pham Vi: def..."
            
            // Transient - Mỗi lần lấy là ID mới
            _dichVuTamThoi1.LayId(),  // Ví dụ: "ID Tam Thoi: ghi..."
            _dichVuTamThoi2.LayId()   // ID khác: "ID Tam Thoi: jkl..."
        };

        return Json(ketQua);
    }
}

Quy tắc quan trọng cần lưu ý:

  • Dịch vụ Singleton không thể phụ thuộc vào dịch vụ Scoped hoặc Transient
  • Dịch vụ Scoped có thể phụ thuộc vào Singleton và Transient
  • Dịch vụ Transient có thể phụ thuộc vào bất kỳ dịch vụ nào

▷Dịch vụ BackgroundService là singleton, không thể inject dịch vụ scoped trực tiếp, chỉ có thể thông qua IServiceScopeFactory scope.ServiceProvider.GetRequiredService(); Ví dụ:

builder.Services.AddSingleton<DichVuDonLe>();
builder.Services.AddHostedService<DichVuNenCuaToi>();

var app = builder.Build();
...
app.Run();

Trong hàm khởi tạo có thể lấy được giá trị của serviceSingleTon

public class DichVuNenCuaToi : BackgroundService
{
    private DichVuDonLe _dichVuDonLe;
    public DichVuNenCuaToi(DichVuDonLe dichVuDonLe)    
    {
        _dichVuDonLe = dichVuDonLe;
    }

    protected override async Task ExecuteAsync(CancellationToken tokenHuyBo)
    {
        while (!tokenHuyBo.IsCancellationRequested)
        {
            Console.WriteLine(_dichVuDonLe.i);
            await Task.Delay(1000, tokenHuyBo);
        }
    }
}

Đăng ký

// Đăng ký HttpOptions, đọc từ appsetting.json
builder.Services.Configure<HttpOptions>(builder.Configuration.GetSection("HttpOptions"));
// Đăng ký HttpClientService
builder.Services.AddHttpClient((dichVu, client) =>
{
    var options = dichVu.GetRequiredService().Value;
    // Cấu hình HttpClient
    if (!string.IsNullOrEmpty(options.BaseUrl))
    {
        client.BaseAddress = new Uri(options.BaseUrl);
    }
    client.Timeout = TimeSpan.FromSeconds(options.TimeoutSeconds);
    
    // Thêm header mặc định
    foreach (var header in options.DefaultHeaders)
    {
        client.DefaultRequestHeaders.Add(header.Key, header.Value);
    }
});

services.AddScoped(); 
_myService = serviceProvider.GetRequiredService<IMyService>();

Socket

Server

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
class ServerTcp
{
    static void Main(string[] args)
    {
        // 1. Định nghĩa IP và cổng lắng nghe (IPAddress.Any nghĩa là lắng nghe tất cả giao diện mạng)
        IPAddress diaChiIp = IPAddress.Any;
        int cong = 8888;
        TcpListener server = new TcpListener(diaChiIp, cong);
        try
        {
            // 2. Bắt đầu server và bắt đầu lắng nghe
            server.Start();
            Console.WriteLine($"Server da khoi dong, lang nghe cong {cong}");
            // 3. Vòng lặp chờ client kết nối (trong dự án thực tế có thể xử lý đa luồng cho nhiều client)
            while(true)
            {
                // Chấp nhận kết nối client (chặn luồng hiện tại cho đến khi có client kết nối)
                TcpClient client = server.AcceptTcpClient();
                Console.WriteLine("Client da ket noi!");
                // 4. Lấy luồng mạng (dùng để đọc/ghi dữ liệu)
                using(NetworkStream stream = client.GetStream())
                {
                    byte[] boDem = new byte[1024]; // Bộ đệm (điều chỉnh kích thước theo nhu cầu)
                    int soByteDoc;
                    // 5. Vòng lặp đọc dữ liệu client gửi (chặn cho đến khi có dữ liệu)
                    while((soByteDoc = stream.Read(boDem, 0, boDem.Length)) != 0)
                    {
                        // Chuyển byte thành chuỗi
                        string duLieu = Encoding.UTF8.GetString(boDem, 0, soByteDoc);
                        Console.WriteLine($"Nhan du lieu tu client: {duLieu}");
                        // 6. Gửi phản hồi về client
                        string phanHoi = $"Server da nhan: {duLieu}";
                        byte[] bytePhanHoi = Encoding.UTF8.GetBytes(phanHoi);
                        stream.Write(bytePhanHoi, 0, bytePhanHoi.Length);
                        Console.WriteLine($"Da phan hoi client: {phanHoi}");
                    }
                }
                // 7. Đóng kết nối client
                client.Close();
                Console.WriteLine("Client da ngat ket noi!");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Loi server: {ex.Message}");
        }
        finally
        {
            // 8. Dừng server
            server.Stop();
            Console.WriteLine("Server da dung!");
        }
    }
}

Client

using System;
using System.IO;
using System.Net.Sockets;
using System.Text;

class KhachHangTcpVidu
{
    static void Main(string[] args)
    {
        string diaChiIpServer = "127.0.0.1";
        int cong = 8888;
        try
        {
            using(TcpClient client = new TcpClient())
            {
                client.Connect(diaChiIpServer, cong);
                Console.WriteLine($"Da ket noi toi server");
                using(NetworkStream stream = client.GetStream())
                {
                    while(true)
                    {
                        Console.Write("Nhap tin nhan: ");
                        string tinNhan = Console.ReadLine();
                        if(tinNhan?.ToLower() == "exit")
                            break;
                            
                        byte[] duLieu = Encoding.UTF8.GetBytes(tinNhan);
                        stream.Write(duLieu, 0, duLieu.Length);
                        Console.WriteLine("Tin nhan da gui");
                        byte[] boDem = new byte[1024];
                        int soByteDoc = stream.Read(boDem, 0, boDem.Length);
                        string phanHoi = Encoding.UTF8.GetString(boDem, 0, soByteDoc);
                        Console.WriteLine($"Nhan duoc phan hoi tu server: {phanHoi}");
                    }
                }
                Console.WriteLine("Da ngat ket noi voi server!");
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine($"Loi client: {ex.Message}");
        }
    }
}

Gói bọc Client

  • isRunning để xác định hai tác vụ nền có cần thoát không
  • Đọc là liên tục, phát hiện
  • Bất cứ khi nào thất bại, sẽ tự động kết nối lại, lần thất bại đó sẽ thất bại, kết nối lại có thành công hay không, lần đó chưa chắc
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// Gói bọc TCP client (có tự động kết nối lại)
/// </summary>
public class BaoVoKhachHangTcp
{
    private readonly string _diaChiIpServer;
    private readonly int _congServer;
    private TcpClient _khachHang;
    private NetworkStream _luongMang;
    private bool _dangChay; // Client có đang chạy không
    private bool _daKetNoi; // Đã kết nối chưa
    private int _thoiGianKetNoiLai = 3000; // Khoảng thời gian chờ kết nối lại (ms)
    private readonly CancellationTokenSource _nguonToken = new CancellationTokenSource();

    // Sự kiện: Nhận tin nhắn
    public event Action<string> OnNhanTinNhan;
    // Sự kiện: Thay đổi trạng thái kết nối
    public event Action<bool> OnTrangThaiKetNoiThayDoi;
    // Sự kiện: Xảy ra lỗi
    public event Action<Exception> OnXayRaLoi;

    /// <summary>
    /// Khởi tạo client
    /// </summary>
    public BaoVoKhachHangTcp(string diaChiIpServer, int congServer)
    {
        _diaChiIpServer = diaChiIpServer;
        _congServer = congServer;
    }

    /// <summary>
    /// Bắt đầu client (bắt đầu kết nối/tự động kết nối lại)
    /// </summary>
    public void BatDau()
    {
        _dangChay = true;
        _ = BatDauVongLapKetNoi(); // Bắt đầu vòng lặp kết nối (bất đồng bộ)
    }

    /// <summary>
    /// Dừng client (ngắt kết nối và dừng tự động kết nối lại)
    /// </summary>
    public void Dung()
    {
        _dangChay = false;
        _nguonToken.Cancel();
        NgatKetNoi();
    }

    /// <summary>
    /// Gửi tin nhắn đến server
    /// </summary>
    public async Task GuiTinNhanBatDongBo(string tinNhan)
    {
        if (!_daKetNoi || _luongMang == null)
        {
            OnXayRaLoi?.Invoke(new InvalidOperationException("Chua ket noi toi server, khong the gui tin nhan"));
            return;
        }

        try
        {
            byte[] duLieu = Encoding.UTF8.GetBytes(tinNhan);
            await _luongMang.WriteAsync(duLieu, 0, duLieu.Length, _nguonToken.Token);
            await _luongMang.FlushAsync(_nguonToken.Token);
        }
        catch (Exception ex)
        {
            OnXayRaLoi?.Invoke(ex);
            NgatKetNoi(); // Gửi thất bại thì chủ động ngắt kết nối, kích hoạt kết nối lại
        }
    }

    /// <summary>
    /// Vòng lặp kết nối (logic tự động kết nối lại)
    /// </summary>
    private async Task BatDauVongLapKetNoi()
    {
        while (_dangChay && !_nguonToken.Token.IsCancellationRequested)
        {
            if (!_daKetNoi)
            {
                try
                {
                    Console.WriteLine($"Dang thu ket noi toi {_diaChiIpServer}:{_congServer}...");
                    _khachHang = new TcpClient();
                    await _khachHang.ConnectAsync(_diaChiIpServer, _congServer, _nguonToken.Token);
                    _luongMang = _khachHang.GetStream();
                    _daKetNoi = true;
                    OnTrangThaiKetNoiThayDoi?.Invoke(true);
                    Console.WriteLine("Ket noi thanh cong!");

                    // Bắt đầu vòng lặp nhận tin nhắn
                    _ = BatDauVongLapNhanTin();
                }
                catch (Exception ex) when (!(ex is OperationCanceledException))
                {
                    OnXayRaLoi?.Invoke(ex);
                    Console.WriteLine($"Ket noi that bai, thu lai sau {_thoiGianKetNoiLai}ms...");
                    await Task.Delay(_thoiGianKetNoiLai, _nguonToken.Token);
                }
            }

            await Task.Delay(100, _nguonToken.Token); // Tránh CPU rỗng
        }
    }

    /// <summary>
    /// Vòng lặp nhận tin nhắn
    /// </summary>
    private async Task BatDauVongLapNhanTin()
    {
        byte[] boDem = new byte[1024];
        while (_daKetNoi && !_nguonToken.Token.IsCancellationRequested)
        {
            try
            {
                	// Kiểm tra luồng có thể đọc được không, thời gian chờ 1 giây (dùng để phát hiện mất kết nối)
                	var taskDoc = _luongMang.ReadAsync(boDem, 0, boDem.Length, _nguonToken.Token);
							// Chờ đọc xong hoặc hết thời gian chờ (1 giây)
							if (!await taskDoc.WaitAsync(TimeSpan.FromSeconds(1), _nguonToken.Token))
							{
							    continue; // Hết thời gian, không có dữ liệu
							}
							
							// Lấy số byte thực tế đã đọc (lúc này taskDoc đã hoàn thành)
							int soByteDoc = taskDoc.Result;
							if (soByteDoc <= 0)
							{
							    // Kết nối đã bị ngắt
							    Console.WriteLine("Server da ngat ket noi");
							    NgatKetNoi();
							    break;
							}
							
							// Xử lý dữ liệu đã đọc
							string tinNhan = Encoding.UTF8.GetString(boDem, 0, soByteDoc);
							OnNhanTinNhan?.Invoke(tinNhan);
            }
            catch (Exception ex) when (!(ex is OperationCanceledException))
            {
                OnXayRaLoi?.Invoke(ex);
                NgatKetNoi(); // Nhận thất bại thì ngắt kết nối
                break;
            }
        }
    }

    /// <summary>
    /// Ngắt kết nối
    /// </summary>
    private void NgatKetNoi()
    {
        if (!_daKetNoi) return;

        _daKetNoi = false;
        _luongMang?.Dispose();
        _khachHang?.Close();
        OnTrangThaiKetNoiThayDoi?.Invoke(false);
        Console.WriteLine("Da ngat ket noi");
    }
}

Ghi chú

1. Khi thao tác tệp, cần chú ý đến tên tệp, phải tuân thủ quy tắc đặt tên tệp

Như đã nói, trước đây có tên chứa "/", dẫn đến không thể tạo tệp mới.

Thẻ: C# dotnet async-await ASP.NET Core socket-programming

Đăng vào ngày 9 tháng 7 lúc 15:48