Lập trình với socket request / response của NetMQ

    0

    Kỹ thuật lập trình với thư viện NetMQ tương đối đơn giản và phụ thuộc vào loại socket cụ thể được sử dụng. Sau đây chúng ta sẽ xem xét chi tiết các kỹ thuật sử dụng cho socket request / response và socket publisher / subscriber. Bài học này sẽ giải thích chi tiết code đã xây dựng ở bài thực hành lập trình ZeroMQ cơ bản.

    Khởi tạo socket

    Bước đầu tiên trong lập trình với loại socket này là khởi tạo socket. Việc khởi tạo socket này thực hiện như khởi tạo một object thông thường trong C#.

    Đối với server (tức là chương trình nhận truy vấn), chúng ta cần dùng class ResponseSocket. Code khởi tạo như sau:

    using var server = new ResponseSocket();
    server.Bind("tcp://*:1308");
    

    Đoạn code này sẽ thực hiện hai công việc: (1) khởi tạo đối tượng của lớp ResponseSocket, (2) gắn nó với một điểm cuối cố định của tcp (ở cổng 1308).

    Ở đây cần lưu ý, server trong mô hình request / response là bên thụ động nhận truy vấn nên nó phải có IP và cổng cố định. NetMQ cung cấp hai cách để gắn socket với IP và cổng cố định.

    Cách thứ nhất là dùng phương thức Bind như trong ví dụ trên. Phương thức Bind nhận tham số là một chuỗi ký tự thể hiện giao thức, địa chỉ IP và số cổng. Chuỗi này gọi là chuỗi địa chỉ.

    Chuỗi này cần viết theo quy tắc “giao_thức://địa_chỉ_ip:số_cổng”. Như trong ví dụ trên, chuỗi này có dạng “tcp://*:1308”. Trong đó lưu ý, ký tự * trong phần IP thể hiện rằng socket này sẽ lắng nghe gói tin tcp từ tất cả các giao diện mạng. Giá trị * tương đương với hằng số IPAddress.Any trong lập trình socket thông thường chúng ta đã học.

    Cách thứ hai là trực tiếp chỉ định chuỗi địa chỉ trong hàm tạo của ResponseSocket như sau:

    using var server = new ResponseSocket("@tcp://*:1308");

    Có thể để ý rằng, chuỗi địa chỉ này có thêm ký tự @ đứng trước. Ký tự @ được sử dụng để chỉ định rằng đây sẽ là socket thụ động với IP và số cổng cố định. Khi khởi tạo socket theo cách này, NetMQ sẽ tự động gọi phương thức Bind.

    Đối với client (tức là chương trình phát truy vấn), chúng ta sử dụng lớp RequestSocket. Tương tự, chúng ta có hai cách làm việc:

    using var client = new RequestSocket();
    client.Connect("tcp://127.0.0.1:1308");
    và 
    using var client = new RequestSocket(">tcp://127.0.0.1:1308");
    

    Ở cả hai cách, chúng ta đều thực hiện hai việc: (1) khởi tạo đối tượng của lớp RequestSocket, (2) kết nối tới tiến trình chủ.

    Ở cách thứ nhất, chúng ta chủ động sử dụng phương thức Connect của lớp RequestSocket với tham số là chuỗi địa chỉ của tiến trình chủ. 

    Ở cách thứ hai, chúng ta cung cấp luôn chuỗi địa chỉ của tiến trình chủ làm tham số cho hàm tạo. Cần lưu ý ký tự > trước chuỗi địa chỉ. Ký tự này báo hiệu cho NetMQ cần kết nối tới địa chỉ được cung cấp. Ở cách này, NetMQ sẽ tự gọi tới phương thức Connect.

    Hai ký tự @ và > trong chuỗi địa chỉ được sử dụng cho các loại socket khác với ý nghĩa tương tự.

    Gửi và nhận dữ liệu

    Gửi dữ liệu trong mô hình truy vấn phản hồi được thực hiện ở cả socket request và socket response. Code thực hiện gửi dữ liệu như sau:

    // với socket request
    client.SendFrame(request);
    // với socket response
    server.SendFrame($"Hi, this is a server response. It's {DateTime.Now}");
    

    Như vậy, việc gửi thông điệp ở cả request socket và response socket là như nhau, cùng thông qua việc gọi phương thức SendFrame.

    Phương thức SendFrame chứa một số overload để tiện lợi cho việc gửi hai loại dữ liệu thông dụng nhất là chuỗi byte và chuỗi ký tự. Trong trường hợp gửi chuỗi ký tự, NetMQ sẽ tự động chuyển đổi thành chuỗi byte.

    Việc nhận dữ liệu có thể thực hiện được ở cả socket request và socket response. Code thực hiện nhận dữ liệu như sau:

    // với request socket
    var message = client.ReceiveFrameString();
    // với response socket
    string message = server.ReceiveFrameString();
    

    Chúng ta có thể nhận thấy, việc đọc dữ liệu ở cả hai loại socket đều sử dụng phương thức ReceiveFrameString.

    Cần chú ý quá trình gửi truy vấn và nhận phản hồi là quá trình đồng bộ. Luồng thực thi sẽ bị khóa trong khi socket ZMQ chờ nhận phản hồi.

    Thực tế, việc gửi và nhận dữ liệu trong NetMQ phức tạp hơn một chút. Điều này có liên quan đến cách thức ZMQ đóng gói thông điệp. Vấn đề này chúng ta sẽ xem xét chi tiết ở phần gửi nhận dữ liệu trong mô hình publisher / subscriber.

    Nhận và xử lý thông điệp với proactor

    Mô hình xử lý mặc định trong NetMQ là đơn luồng và đồng bộ. Để thực hiện xử lý đa luồng, NetMQ cung cấp công cụ gọi là Proactor. Proactor giúp nhanh chóng xử lý thông điệp nhận được bằng cách tạo ra một luồng riêng để xử lý.

    Sau đây là một ví dụ về sử dụng proactor với socket response:

    static void ReqresServerProactor() {
       WriteLine("-- ZeroMQ server --");
       using var server = new ResponseSocket("@tcp://*:1308");
       //server.Bind("tcp://*:1308");
       using var proactor = new NetMQProactor(server, (s, m) => {      
           WriteLine($"#Request: {Encoding.UTF8.GetString(m.First.Buffer)}");
           WriteLine("{0} {1}", m.First.BufferSize, m.First.MessageSize);
           ((ResponseSocket)s).SendFrame($"Hi, this is a server response. It's {DateTime.Now}");
       });   
    }
    

    Để sử dụng proactor, NetMQ cung cấp lớp NetMQProactor. Khởi tạo đối tượng của lớp này yêu cầu hai tham số: một socket nơi nhận thông điệp, một phương thức dùng để xử lý thông điệp.

    Phương thức xử lý thông điệp là một delegate đòi hỏi hai tham số đầu vào: một biến kiểu socket và một biến kiểu NetMQMessage. Phương thức xử lý thông điệp có thể cung cấp ở dạng một phương thức lambda, một phương thức thông thường, một phương thức tĩnh, hoặc một hàm cục bộ. Trong ví dụ trên chúng ta cung cấp ở dạng một phương thức lambda.

    Khi sử dụng proactor, NetMQ sẽ tạo một luồng mới để thực thi phương thức xử lý. Nếu có thông điệp tới, NetMQ sẽ gọi phương thức xử lý. Khi gọi phương thức này, NetMQ sẽ truyền chính socket nhận, và bản thân thông điệp (dưới dạng một đối tượng NetMQMessage).

    Do vậy, ở trong phương thức xử lý của proactor, chúng ta có thể sử dụng chính socket và đọc dữ liệu từ thông điệp. Chúng ta cũng có thể sử dụng socket này để gửi thông điệp phản hồi, ví dụ, trong trường hợp ResponseSocket.

    Việc đọc dữ liệu từ đối tượng NetMQMessage có chút khác biệt với việc đọc trực tiếp từ socket. NetMQMessage yêu cầu tự mình đọc dữ liệu thô (dạng byte[]) từ từng frame và biến đổi về dạng mong muốn. Kiểu đọc dữ liệu này tương tự như đối với lập trình Socket API.

    WriteLine($"#Request: {Encoding.UTF8.GetString(m.<strong>First</strong>.<strong>Buffer</strong>)}");

    Lớp NetMQMessage cung cấp các thuộc tính First, Last, FrameCount để truy cập vào frame đầu tiên, frame cuối cùng, và số lượng frame trong thông điệp. Do lớp NetMQMessage cũng thực thi indexer nên chúng ta cũng có thể truy xuất vào frame bất kỳ qua phép toán truy xuất phần tử mảng []. Trong ví dụ trên, đối tượng NetMQMessage có tên là m, và chúng ta có thể truy xuất frame thứ 2 qua lệnh m[2].

    Giải phóng socket

    Khi hoạt động, NetMQ tạo ra một số luồng thực thi phụ. Khi đóng luồng chính (thoát chương trình), các luồng thực thi phụ sẽ bị huỷ bỏ theo. Điều này có nghĩa là bạn không cần tự mình giải phóng socket khi kết thúc chương trình.

    Tuy nhiên, NetMQ khuyến nghị nên thực hiện thao tác chủ động giải phóng socket như sau:

    Khi truy cập vào frame nào, chúng ta có thể sử dụng thuộc tính Buffer để lấy ra chuỗi byte dữ liệu, và thuộc tính MessageSize để biết có bao nhiêu byte trong phần buffer đó.

    Proactor có thể sử dụng với tất cả các loại socket trong NetMQ. Tính năng này rất hữu dụng khi phát triển thành phần server với giao diện đồ hoạ. Nếu không sử dụng proactor, chúng ta cần sử dụng kỹ thuật bất đồng bộ hoặc kỹ thuật đa luồng để tránh khóa luồng thực thi chính.

    NetMQConfig.Cleanup(false);
    // hoặc
    NetMQConfig.Cleanup(true);
    

    Lệnh Cleanup(false) sẽ huỷ bỏ tất các luồng thực thi phu ngay lập tức. Lệnh Cleanup(true) sẽ chờ các socket gửi nốt dữ liệu đang chờ rồi mới huỷ bỏ các luồng thực thi.

    Việc giải phóng socket chủ động rất quan trọng khi xây dựng ứng dụng với giao diện đồ họa. Nếu không chủ động giải phóng socket, các luồng thực thi phụ của NetMQ sẽ không được giải phóng hoàn toàn, gây ảnh hưởng đến lần chạy chương trình tiếp theo.

    + Nếu bạn thấy site hữu ích, trước khi rời đi hãy giúp đỡ site bằng một hành động nhỏ để site có thể phát triển và phục vụ bạn tốt hơn.
    + Nếu bạn thấy bài viết hữu ích, hãy giúp chia sẻ tới mọi người.
    + Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần thảo luận cuối trang.
    Cảm ơn bạn!

    Kết luận

    Trong bài học này chúng ta phân tích chi tiết kỹ thuật lập trình với socket request / response trong thư viện ZeroMQ. Có thể thấy rằng, kỹ thuật lập trình này rất tương đồng với kỹ thuật lập trình TCP socket mà chúng ta đã biết. Tuy nhiên, sử dụng ZeroMQ giúp việc lập trình đơn giản và an toàn hơn rất nhiều.

    Theo dõi
    Thông báo của
    guest

    0 Thảo luận
    Phản hồi nội tuyến
    Xem tất cả bình luận