Chia sẻ kiến thức lập trình

Luồng dữ liệu socket

Cần một chút tinh tế

Chúng ta đã quá quen với HTTP rồi, đã quá quen với các thư viện dùng sẵn rồi nhỉ? Điều này giúp chúng ta không cần quan tâm đến những thứ đau đầu như byte bit, mà chỉ cần quan tâm đến đối tượng hay dữ liệu gửi nhận mà thôi. Nhưng để chương trình của chúng ta đạt hiệu suất tối đa, đặc biệt với lập trình socket, chúng ta vẫn cần phải biết điều gì đang xảy ra với dữ liệu của mình, và chỉ cần thêm một chút tinh tế, chúng ta sẽ có thể nâng tầm cho ứng dụng của mình thậm chí với ít tài nguyên hơn so với phiên bản cũ.

Luồng gửi nhận thông thường

Trong các framework http hay các framework cho socket và websocket như netty, vertx, ... dữ liệu đều phải trải qua các bước cơ bản như:

  1. receive: ở bước này socket server sẽ nhận được dữ liệu sẽ đến từ internet ở dạng byte array.
  2. buffer: dữ liệu đến từ socket có thể sẽ không đủ một message ngay lập tức nên chúng ta cần buffer để tích lại cho đến khi nào nhận đủ số byte thì thôi.
  3. deserialize: một message ở dạng byte array sẽ không phù hợp để xử lý, nên chúng ta cần chuyển đổi sang dạng đối tượng request, chúng ta có thể dùng bất kì định dạng nào chúng ta muốn, nhưng tốt nhất ở thời điểm này có lẽ là json, protobuf hay msgpack sẽ giúp chúng ta giảm được rất nhiều size cho một message
  4. handle request: đây là nơi chúng ta sẽ xử lý logic, cập nhật, cắt ghép dữ liệu để trả về kết quả cho client
  5. response: ở bước này chúng ta sẽ gửi kết quả đến socket channel để phản hồi cho client
  6. serialize: dữ liệu để gửi được qua socket thì phải ở dạng byte array nên chúng ta cần chuyển đối tượng thành byte array và một lần sử dụng json, protobuf hay msgpack sẽ giúp chúng ta tiết kiệm được rất nhiều bit và byte.
  7. send: dữ liệu sau khi thành byte array sẽ được gửi xuống socket cho client

Broadcast dữ liệu

Đối với luồng gửi nhận dữ liệu thông thường, việc xử lý request từ 1 client và trả lại kết quả chỉ cho client đó thì đơn giản rồi, tuy nhiên chúng ta có một bài toán lớn hơn đó là broadcast dữ liệu. Đây là bài toán rất phổ biến mà tất cả các framework socket giải giải quyết, tuy nhiên không phải framework nào cũng hỗ trợ sẵn. Ví dụ với netty, chúng ta hay làm thế này:

channels.forEach(channel -> channel.writeAndFlush(response));

Tuy nhiên làm như vậy sẽ dẫn điến việc mỗi message khi vào từng channel sẽ bị serialize 1 lần, và điều này làm giảm hiệu năng đi đáng kể.

Cải thiện hiệu năng

Chúng ta có thể thấy rõ ràng là cùng một response mà bị serialize nhiều lần sẽ không ổn, bây giờ chúng sẽ serialize response ra byte array trước rồi mới gửi đến socket client, nó sẽ kiểu thế này:

byte[] bytes = serializer.serialize(response);
channels.forEach(channel -> channel.writeAndFlush(bytes));

Nhìn thì có vẻ đơn giản đúng không? Nhưng thực tế với những framework được thiết kế đo ni đóng giày với chain of responsibility design pattern kiểu thế này:

Thì việc chúng ta thay đổi không hề đơn giản, chúng ta sẽ phải làm rất nhiều việc để wrap lại framework đã có và phải cẩn thận để tránh tạo ra một mớ hỗn độn. Hãy khéo léo gộp chung EncoderResponseHandler vào làm một.

Tổng kết

Với những bài toán gửi nhận dữ liệu 1-1 thông thường thì không có gì phức tạp, tuy nhiên đối với bài toàn broadcast dữ liệu thì câu chuyện lại khác, cần phải serialize đối tượng thành byte array trước khi gửi xuống tất cả các socket client. Tuy nhiên việc này không hề đơn giản vì dữ liệu qua socket phải trải qua tầng tầng lớp các queue và handler trước khi được gửi xuống socket client. Nắm được điều này ezyfox-server đã đóng gọi việc chuyển dữ liệu thành byte array và gửi xuống socket client, bạn chỉ cần sử dụng đối tượng EzyResponse và thế là xong.

Share: