Xây dựng thư viện gọi hàm từ xa (RPC) với tcp socket và C# (2)

0

Trong post trước chúng ta đã biết khái niệm chung về RPC cũng như đã cùng nhau xây dựng một thư viện RPC đơn giản. Trong post này chúng ta sẽ vận dụng bộ thư viện mới tạo để xây dựng một vài ứng dụng client/server.

Chúng ta sẽ xây dựng hai ứng dụng đơn giản. Ứng dụng thứ nhất thực hiện các phép toán số học. Ứng dụng thứ hai mô phỏng một phần mềm quản lý. Cũng lưu ý rằng, đây chỉ là những ví dụ minh họa cho việc gọi hàm RPC chứ không phải là những ứng dụng thực sự.

Đây là phần cuối của loạt bài về xây dựng thư viện RPC đơn giản sử dụng socket Tcp:

Loạt bài Xây dựng thư viện RPC đơn giản:
Phần 1 – Xây dựng thư viện RPC
Phần 2 – Ví dụ minh họa

Ví dụ thứ nhất – tính toán

Tạo các project

Cùng trong solution với bộ thư viện Rpc đã xây dựng ở bài trước tạo thêm một thư mục MathExample. Thư mục này tạo trực tiếp trong solution (click phải vào tên solution, chọn Add => New Solution Folder).

Trong thư mục MathExample tạo ba project:

  • MathClient thuộc loại ConsoleApp (.NET framework). Đây là chương trình client.
  • MathServer thuộc loại ConsoleApp (.NET framework). Đây là chương trình server.
  • MathContract thuộc loại Class Library (.NET framework). Đây là thư viện dùng chung của client và server, chứa “bản hợp đồng” về các phương thức mà hai bên cần thực thi.
Cấu trúc solution với thư viện Rpc (đã có), 3 project mới trong thư mục MathExample.
Cấu trúc solution với thư viện Rpc (đã có), 3 project mới trong thư mục MathExample.

Tạo các file mã nguồn

Trong project MathClient tạo mới class MathClient (file MathClient.cs)

Trong project MathServer tạo mới class MathService (file MathService.cs)

Trong project MathContract tạo mới interface IMathService (file IMathService.cs)

Các file mã nguồn của MathExample
Các file mã nguồn của MathExample

Thiết lập References để MathClient và MathServer tham chiếu đến Rpc và MathContract.

MathClient và MathServer tham chiếu đến Rpc và MathContract
MathClient và MathServer tham chiếu đến Rpc và MathContract

Viết code

namespace MathContract
{
    public interface IMathService
    {
        double Add(double a, double b);
        double Sub(double a, double b);
        double Mul(double a, double b);
        double Div(double a, double b);
        double Pow(double a, double b);
    }
}
using MathContract;
using System;

namespace MathServer
{
    class MathService : IMathService
    {
        public double Add(double a, double b) => a + b;

        public double Div(double a, double b) => a / b;

        public double Mul(double a, double b) => a * b;

        public double Pow(double a, double b) => Math.Pow(a, b);

        public double Sub(double a, double b) => a - b;
    }
}
using Rpc.Client;
using MathContract;

namespace MathClient
{
    class MathClient : ClientStub<MathClient>, IMathService
    {
        public MathClient(string ip, int port) : base(ip, port, "MathService") {}

        public double Add(double a, double b) => Execute<double>(nameof(Add), a, b);

        public double Div(double a, double b) => Execute<double>(nameof(Div), a, b);

        public double Mul(double a, double b) => Execute<double>(nameof(Mul), a, b);

        public double Pow(double a, double b) => Execute<double>(nameof(Pow), a, b);

        public double Sub(double a, double b) => Execute<double>(nameof(Sub), a, b);
    }
}

Các class hỗ trợ đã xây dựng xong. Giờ có thể bắt đầu viết client code cho cả Client và Server

using System;

namespace MathClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var mathClient = new MathClient("127.0.0.1", 1308);
            Console.WriteLine($"2 + 4 = {mathClient.Add(2, 4)}");
            Console.WriteLine($"2 - 4 = {mathClient.Sub(2, 4)}");
            Console.WriteLine($"2 * 4 = {mathClient.Mul(2, 4)}");
            Console.WriteLine($"2 / 4 = {mathClient.Div(2, 4)}");
            Console.WriteLine($"2 ^ 4 = {mathClient.Pow(2, 4)}");

            Console.ReadKey();
        }
    }
}
using Rpc.Server;
using System;

namespace MathServer
{
    class Program
    {
        static void Main(string[] args)
        {
            var server = new ServerProxy(1308)
            {
                Process = ServerStub.GetInstance("MathServer").Process,
                Log = Console.WriteLine
            };
            server.Run();
        }
    }
}

Khi đọc client code của MathClient (Program.cs) chúng ta thấy không thể xác định được lời gọi các phương thức của lớp MathClient (Add, Sub, ..) là lời gọi cục bộ hay lời gọi từ xa. Bộ thư viện RPC chúng ta viết đã giúp che đi các chi tiết về marshalling hay truyền thông mạng. Client code sử dụng các phương thức này như phương thức cục bộ thông thường.

Dịch và chạy thử

Thiết lập để đồng thời chạy debug MathClient và MathServer, trong đó MathServer chạy trước.

Thiết lập chạy debug nhiều project cùng lúc
Kết quả chạy thử

Kết luận

Qua việc thực hiện ví dụ trên có thể rút ra quy trình vận dụng bộ thư viện Rpc:

  1. Tạo bản “hợp đồng” về các phương thức sẽ được cả client và server thực thi. Hợp đồng này xây dựng dưới dạng một interface.
  2. Server thực thi interface theo cách “thông thường”, tức là thực sự làm chức năng xử lý, tính toán.
  3. Client thực thi interface bằng cách gọi client stub giúp mang thông tin đến server và nhận kết quả. Client không thực hiện tính toán hay xử lý thông tin.

Thư viện Rpc chịu trách nhiệm cho tất cả những công việc còn lại. Bản thân client code không biết những gì diễn ra bên dưới lời gọi của mình và coi như đó là lời gọi hàm cục bộ thông thường.

Ví dụ thứ hai – quản lý sinh viên

Các project

Tương tự ví dụ số 1, chúng ta tạo ra thư mục solution StudentExample với ba project:

  1. StudentContract (Class Library) chứa hai file mã nguồn: IStudentService.cs (cho interface IStudentService) và Student.cs (cho class Student).
  2. StudentClient (ConsoleApp) chứa file mã nguồn StudentClient.cs (cho lớp StudentClient). Project hày tham chiếu tới thư viện Rpc và StudentContract.
  3. StudentServer (ConsoleApp) với file mã nguồn StudentService.cs (cho class StudentService). Project này tham chiếu tới thư viện Rpc và StudentContract.
Các project và file mã nguồn cho StudentExample
Các project và file mã nguồn cho StudentExample

Thiết lập để đồng thời chạy debug StudentClient và StudentServer

Thiết lập để đồng thời chạy debug StudentClient và StudentServer

Viết code

using System.Collections.Generic;

namespace StudentContract
{
    public interface IStudentService
    {
        List<string> RetrieveNames();
        List<Student> RetrieveAll();
        Student RetrieveSingle(int id);
        void Add(Student item);
        void Create(int id, string name, string email);
        void Delete(int id);        
    }
}
using System;

namespace StudentContract
{
    [Serializable]
    public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Email { get; set; }
    }
}
using StudentContract;
using System.Collections.Generic;
using System.Linq;

namespace StudentServer
{
    class StudentService : IStudentService
    {
        private static List<Student> _students = new List<Student>
        {
            new Student{Id = 1, Name = "Nguyen Van A", Email = "a@google.com"},
            new Student{Id = 2, Name = "Nguyen Van B", Email = "b@google.com"},
            new Student{Id = 3, Name = "Nguyen Van C", Email = "c@google.com"},
            new Student{Id = 4, Name = "Nguyen Van D", Email = "d@google.com"},
            new Student{Id = 5, Name = "Nguyen Van E", Email = "e@google.com"},
            new Student{Id = 6, Name = "Nguyen Van F", Email = "f@google.com"},
            new Student{Id = 7, Name = "Nguyen Van G", Email = "g@google.com"},
            new Student{Id = 8, Name = "Nguyen Van H", Email = "h@google.com"},
            new Student{Id = 9, Name = "Nguyen Van I", Email = "i@google.com"},
        };

        public void Create(int id, string name, string email)
        {
            _students.Add(new Student { Id = id, Name = name, Email = email });
        }

        public void Add(Student item) => _students.Add(item);

        public List<Student> RetrieveAll() => _students;

        public List<string> RetrieveNames() => _students.Select(s => s.Name).ToList();

        public void Delete(int id)
        {
            var s = _students.FirstOrDefault(i => i.Id == id);
            _students.Remove(s);
        }

        public Student RetrieveSingle(int id) => _students.FirstOrDefault(i => i.Id == id);
    }
}
using Rpc.Client;
using StudentContract;
using System;
using System.Collections.Generic;

namespace StudentClient
{
    class StudentClient : ClientStub<StudentClient>, IStudentService
    {
        public StudentClient(string ip, int port) : base(ip, port, "StudentService") { }
        public void Create(int id, string name, string email) => Execute(nameof(Create), id, name, email);
        public void Add(Student item) => Execute(nameof(Add), item);
        public List<Student> RetrieveAll() => Execute<List<Student>>();
        public List<string> RetrieveNames() => Execute<List<string>>();
        public void Delete(int id) => Execute(nameof(Delete), id);
        public Student RetrieveSingle(int id) => Execute<Student>(nameof(RetrieveSingle), id);
    }
}

Client code

using StudentContract;
using System;

namespace StudentClient
{
    class Program
    {
        static void Main(string[] args)
        {Console.Title = "Client";
            var client = new StudentClient("127.0.0.1", 1308);

            Console.WriteLine("All student names:");
            foreach (var s in client.RetrieveNames())
                Console.WriteLine(s);
            Console.WriteLine();

            Console.WriteLine("Create two new students");
            client.Add(new Student { Id = 10, Name = "New Student 01", Email = "01@email.com" });
            client.Create(11, "New Student 02", "02@email.com");
            Console.WriteLine();

            Console.WriteLine("Remove the first student");
            client.Delete(1);
            Console.WriteLine();

            Console.WriteLine("All students now:");
            foreach (var s in client.RetrieveAll())
                Console.WriteLine($"[{s.Id}] {s.Name}, Emai: {s.Email}");
            Console.WriteLine();

            Console.WriteLine("Get student detail:");
            var std = client.RetrieveSingle(11);
            Console.WriteLine($"[{std.Id}] {std.Name}, Emai: {std.Email}");

            Console.ReadLine();
        }
    }
}
using Rpc.Server;
using System;

namespace StudentServer
{
    class Program
    {
        static void Main(string[] args)
        {Console.Title = "Server";
            var server = new ServerProxy(1308)
            {
                Process = ServerStub.GetInstance("StudentServer").Process,
                Log = Console.WriteLine
            };
            server.Run();
        }
    }
}

Dịch và chạy thử

Như vậy có thể thấy thư viện Rpc chúng ta xây dựng ra có thể nhận đóng gói cả những tham số thuộc dạng class phức tạp (như class Student ở trên) chứ không dừng lại ở những kiểu tham số cơ bản. Do đó, bộ thư viện này có thể thực hiện được những thao tác cơ bản của bài toán quản lý.

Kết luận

Trong loạt hai bài này chúng ta đã cùng xây dựng một thư viện Rpc đơn giản nhưng có thể hỗ trợ xây dựng các ứng dụng client/server một cách dễ dàng.

Cũng nên lưu ý rằng, thư viện này được xây dựng với mục đích học thuật là chính. Nó chưa thể ứng dụng thực tế vì còn thiếu nhiều khả năng (như mã hóa, xác thực, sinh code client tự động). Thư viện cũng chỉ sử dụng được khi cả client và server cùng viết trên .NET framework (do sử dụng BinaryFormatter).

Để hiểu rõ hơn các kỹ thuật lập trình socket có thể đọc loạt bài giảng này.

Loạt bài Xây dựng thư viện RPC đơn giản:
Phần 1 – Xây dựng thư viện RPC
Phần 2 – Ví dụ minh họa

Bình luận

avatar
  Đăng ký theo dõi  
Thông báo về