Ví Dụ Về Vòng Đệm (Ringbuffer) Trong Mạng Máy Tính
Gần đây tôi có chia sẻ về ứng dụng của vòng đệm vòng (vòng đệm dạng vòng tròn) trong các hệ thống truyền thông mạng. Rất nhiều bạn đã gửi email cho tôi để thảo luận chi tiết về cách triển khai.
Vào kỳ nghỉ lễ Thanh Minh, rảnh rỗi ở nhà tôi quyết định thử cài đặt một phiên bản đơn giản. Dù quá trình viết có chút phức tạp, nhưng tôi vẫn kiểm soát được độ phức tạp của chương trình và hoàn thành cơ bản chức năng mong muốn.
Giả sử chúng ta có yêu cầu sau: chương trình bind và listen một cổng mạng, xử lý tất cả kết nối TCP đến cổng này. Khi dữ liệu đến, phải thu thập đủ các gói tin hoàn chỉnh, sau đó chuyển giao cho tầng logic xử lý. Nếu dữ liệu chưa đủ để xác định một gói hoàn chỉnh, cần tạm giữ dữ liệu đó cho đến khi nhận đủ mới tiến hành xử lý.
Module nhỏ tôi xây dựng đã thực hiện được nhóm chức năng này. Đặc biệt nhờ sử dụng một vòng đệm duy nhất để quản lý bộ nhớ cho tất cả kết nối, chương trình hoàn toàn không cần thực hiện các thao tác cấp phát bộ nhớ bổ sung trong suốt quá trình vận hành.
Ban đầu tôi cân nhắc đến tính di động của chương trình nên muốn sử dụng các thư viện cross-platform như libev, libuv hay libevent. Tuy nhiên sau khi phân tích kỹ, tôi nhận thấy mô hình callback của các thư viện này thật sự khó sử dụng, không phản ánh đúng luồng xử lý dữ liệu tự nhiên. Việc tích hợp quản lý buffer với các framework sẵn có sẽ biến quá trình triển khai thành cơn ác mộng.
Trong quá trình nghiên cứu mã nguồn Redis, tôi thấy họ không dùng thư viện mạng bên thứ ba mà tự xây dựng logic xử lý epoll/kqueue/select. Cách tiếp cận đơn giản này khiến tôi ấn tượng. Các API như epoll vốn đã rất nhẹ nhàng, việc sử dụng thư viện nặng hàng nghìn dòng sẽ chỉ làm phức tạp vấn đề.
Vì vậy tôi quyết định tự định nghĩa một bộ API đáp ứng yêu cầu:
|
|
Hiện tại tôi chưa xử lý phần gửi dữ liệu, để người dùng tự sử dụng hàm send hệ thống, chỉ tập trung vào xử lý recv. Với bộ API này, hàm create sẽ thiết lập lắng nghe cổng mạng với số lượng kết nối tối đa và kích thước buffer mong muốn.
Thư viện sẽ phân phát một chỉ số kết nối con (không phải socket descriptor hệ thống) giúp tầng ứng dụng dễ dàng quản lý. Nếu thiết lập số kết nối tối đa là 1024, thư viện sẽ đảm bảo chỉ số nằm trong khoảng [0,1023], cho phép bạn trực tiếp tạo mảng phân phối theo chỉ số.
Hàm poll trả về chỉ số kết nối có dữ liệu nhận, tham số timeout cho phép trả về -1 khi không có kết nối nào khả dụng. Sau khi poll, hàm pull được dùng để thu thập dữ liệu từ kết nối đang kích hoạt. Bạn có thể chỉ định kích thước cần nhận, hàm đảm bảo tính nguyên tử: hoặc trả về đủ số byte yêu cầu, hoặc không trả về gì.
Nhờ thư viện quản lý buffer nội bộ, bạn không cần cấp phát buffer bên ngoài. Cơ chế thông minh sẽ: nếu dữ liệu liên tục trong vòng đệm, trả trực tiếp con trỏ; nếu dữ liệu phân mảnh, tự động sao chép thành khối liên tục trước khi trả về. Vùng nhớ này có hiệu lực đến khi gọi poll hoặc yield lần sau.
Hàm yield đóng vai trò quan trọng trong xử lý gói tin logic. Nếu bạn chưa gọi yield, việc pull dữ liệu sẽ luôn trả về cùng khối dữ liệu cũ khi poll lại kết nối đó. Ví dụ với gói tin có header 2 byte chỉ độ dài, bạn có thể: pull 2 byte đầu để xác định kích thước, rồi pull tiếp theo kích thước đã biết. Nếu dữ liệu chưa đủ, không cần xử lý; nếu đủ, gọi yield để xóa dữ liệu đã xử lý khỏi vòng đệm.
Khi pull trả về con trỏ NULL, có thể là do dữ liệu chưa đủ hoặc kết nối bị ngắt. Lúc này có thể dùng hàm closed để kiểm tra trạng thái kết nối.
Nhiều bạn hẳn đã nóng lòng muốn xem mã nguồn. Đừng lo, tôi đã đăng lên GitHub tại địa chỉ:
Cần lưu ý đây chỉ là dự án giải trí trong kỳ nghỉ của tôi, chưa qua kiểm thử kỹ lưỡng. Việc sử dụng trong môi trường sản phẩm cần cực kỳ thận trọng vì chắc chắn còn lỗi chưa phát hiện. Hiện tại mới chỉ có phần triển khai epoll, các nền tảng khác như kqueue/select/iocp có thể mở rộng nhưng hiện chưa có thời gian hoàn thiện.
Tôi rất mong nhận được sự đóng góp của các bạn để dự án trở nên hoàn thiện và thực tế hơn. Gửi email cho tôi, tôi sẽ sẵn sàng hỗ trợ.
Tại sao tôi lại tự tin về thư viện này?
-
Hiệu suất cao: Thiết kế API cho phép tối ưu hiệu suất nhờ trực tiếp gọi các hàm hệ thống như epoll. Số lần gọi hệ thống được giảm thiểu tối đa.
-
Hiệu quả quản lý bộ nhớ: Không phân bổ buffer riêng biệt cho từng kết nối. Bạn có thể tính toán kích thước vòng đệm tối ưu dựa trên thông lượng mạng và khả năng xử lý của ứng dụng.
-
Quản lý kết nối thông minh: Khi vòng đệm đầy, hệ thống sẽ loại bỏ các kết nối tồn đọng lâu nhất. Nếu client hoạt động đúng (gửi đủ gói tin), xác suất bị loại bỏ gần như bằng 0.
So với các thư viện như libevent dùng mô hình callback (xử lý dữ liệu nhận được thông qua hàm gọi lại), thư viện này có ưu điểm vượt trội về tính tự nhiên trong luồng xử lý dữ liệu. Người dùng không cần vướng bận nhiều cấu hình phức tạp, độ phức tạp được đóng gói bên trong module. Giao diện ngoài đơn giản như socket API nhưng còn dễ sử dụng hơn nhờ đảm bảo tính nguyên tử cho gói tin logic.