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

Thiết kế giao thức mạng

Đâu cần nhà khoa học

Theo wiki thì giao thức là một tập hợp các quy tắc chuẩn cho phép hai hoặc nhiều thực thể trong một hệ thống thông tin liên lạc để trao đổi thông tin. Và với lập trình socket cũng vậy, chúng ta cần định nghĩa ra các giao thức để client và server có thể nói chuyện được với nhau. Nếu là một nhà khoa học thì có thể việc này khá đơn giản, nhưng nếu chúng ta chỉ đơn giản là một kỹ sư lập trình thì việc thiết kế ra 1 giao thức mạng cũng không phải cái gì đó quá khó khăn.

Mục tiêu của giao thức

Hiện nay thì có rất nhiều các giao thức mạng (bạn có thể tham khảo tại wiki) và mỗi một giao thức lại có những thành phần khác nhau nên mình cũng không dám phán bừa, mình sẽ chỉ nói về giao thức easyezyfox-server đang sử dụng thôi nhé.

Giao thức easy ra đời với 2 mục tiêu chính:

  1. Đưa ra một quy tắc tiêu chuẩn để client và server có thể nói chuyện được với nhau: đó là các bước từ lúc connect cho đến khi login thành công.
  2. Đảm bảo cho dữ liệu của một gói tin là nhỏ nhất có thể: sử dụng các phép toán bit để giảm thiểu tối đa số lượng byte cần sử dụng.

Thành phần của giao thức

Để đảm bảo được 2 mục tiêu quan trọng kể trên thì easy bao gồm 2 thành phần chính:

  1. Cấu trúc của một gói tin
  2. Luồng kết nối giữa client và server

Ngoài ra nó còn một số thành phần liên quan đến việc giữ và ngắt kết nối.

Cấu trúc gói tin

Một gói tin cơ bản sẽ có 2 thành phần chính:

  1. Header: dùng để mô tả thông tin của gói tin
  2. Payload: là nơi chứa thông tin thực tế của một gói tin

Hai thành phần tuỳ chọn bao gồm:

  1. Header dự trữ: trong trường hợp 1 byte là không đủ, chúng ta có thể sử dụng thêm các byte khác để làm header
  2. Payload Length: dùng để lưu kích thước của Payload, chúng ta hoàn toàn có thể bỏ qua thành phần này và sử dụng 1 thuật toán kiểu Ký Pháp Ba Lan kết hợp với stack để lấy được payload, tuy nhiên nhiên nó chỉ có thể làm được với các payload có cấu trúc kiếu json, message pack hay floatbuffers, còn những payload không có cấu trúc như protobuf thì bắt buộc sẽ cần đến Payload Length.

Note: Nếu bạn đang sử dụng thuần websocket thì bạn có thể bỏ qua phần này đến phần luồng kết nối luôn, vì websocket đã thiết kế sẵn cấu trúc gói tin rồi (bạn có thể tham khảo tại đây) và chúng ta không nên thiết kế một lớp gói tin khác chồng lên nữa.

Header

Nếu là bạn, bạn sẽ tổ chức header thế nào? Sẽ kiểu này đúng không?

{
    "isBigSize": true,
    "isEncrypted": true,
    "isCompressed": true,
    "isText": true,
    "isRawBytes": true,
    "isUdpHandshake": true
}

Nếu mỗi 1 ký tự là 1 byte thì chúng ta sẽ phải sử dụng rất nhiều dung lượng cho header này. Và mục tiêu easy phải đảm bảo dung lượng gói tin là nhỏ nhất nên nó chỉ được phép sử dụng 1 byte duy nhất mà thôi. Vậy nên nó phải sử tận dụng từng bit trong 1 byte để đại diện cho các thông tin ở trên:

  1. bit đầu tiên: định nghĩa rằng gói tin này là là lớn hay nhỏ, nếu lớn thì PayLoad Length sẽ có kích thước tối đa là 4 bytes tương đương với 2^31 - 1 bytes, và nếu gói tin này là nhỏ thì PayLoad Length sẽ có kích thước tối đa là 2 bytes tương đương vói 2^16-1 bytes.
  2. bit thứ 2: định nghĩa rằng gói tin này bị mã hoá hay không
  3. bit thứ 3: định nghĩa gói tin này có bị nén 2 không
  4. bit thứ 4: định nghĩa gói tin này là text hay là byte array
  5. bit thứ 5: định nghĩa gói tin này có cấu trúc không hay chỉ là mảng byte đơn thuần
  6. bit thứ 6: định nghĩa gói tin này có phải là bản tin bắt tay của UDP hay không
  7. bit thứ 7: dùng để dự trữ
  8. bit thứ 8: dùng để định nghĩa rằng có 1 byte làm header nữa hay không

Bạn thấy không, 8 thông tin dài dằng dặc thế này mà easy chỉ cần duy 1 byte mà thôi, thế nên nó sẽ đảm bảo cho ezyfox-server một hiệu năng tuyệt vời nhất có thể.

Luồng kết nối

Sau khi đã định nghĩa được cấu trúc 1 gói tin, giờ là lúc chúng ta cần xác định xem client và server cần sẽ trải qua những bước nào để không dư thừa nhưng vẫn đảm bảo được khả năng bảo mật. Với easy thì các bước này bao gồm:

  1. Connect: Chắc chắn rồi đầu tiên client và server phải kết nối với nhau cái đã.
  2. Client sẽ gửi yêu cầu bắt tay đến server: Ở bước này cũng giống như con người chúng ta, client sẽ gửi lời chào hỏi đến server với một số thông tin như: loại sdk client đang sử dụng, phiên bản sdk, client token nếu có, và quan trọng nhất là RSA public key để server mã hoá client key session key sẽ dùng để mã hoá dữ liệu sau này, các bạn có thể đọc thêm bài viết về SSL này để hiểu thêm nhé.
  3. Server cũng bắt tay với client: Server lưu lại khoá công khai của client và gửi khoá công khai của mình kèm theo session key (khoá cho mã hoá đối xứng, có thể là AES) cho client. Session key này sẽ được mã hoá với khoá công khai của client. Ở bước này server có thể không cần thiết phải gửi khoá công khai của mình cho client nếu client không có nhu cầu sử dụng.
  4. Login: client gửi yêu cầu login với username password hoặc token lên server.
  5. Server xác thực và trả về kết quả cho client
  6. Access App: Vì ezyfox-server giống như 1 platform, nó sẽ có nhiều app và plugin chạy trên nó, nên client sẽ cần phải tham gia vào 1 app nào đó hoặc lấy thông tin của plugin để gọi
  7. Server sẽ xác nhận có cho phép user tham gia vào app hay không
  8. Client và server tiến hành trao đổi các thông tin khác với nhau thông qua việc gọi các API.

Mỗi khi client connect đến server đều sẽ cần trải qua các bước này, thế nên bạn cũng cần phải xử lý logic cho phần reconnect để client có khả năng khôi phục lại trạng thái trước khi nó bị ngắt kết nối nhé.

Tổng kết

Giao thức là thành phần tối quan trọng của mọi socket server, vì sao nhỉ? Vì dữ liệu gửi nhận qua client và server là liên tục, một giao thức đủ tốt sẽ tiết kiệm cho thế giới này hàng tỉ tỉ byte, ngược lại nó sẽ gây ra sự lãng phí không cần thiết.

Việc thiết kế một giao thức cũng không phải quá khó khăn, tuy nhiên nó cần thời gian để chứng minh sự phù hợp và mỗi người lại có những cách thiết kế với nhau, chính vì thế mới tạo ra sự đa dạng cho thế giới lập trình ngày nay.

Share: