[DIY] Tự viết driver cho Redis trong 30 phút với Rust
Rust
27
Redis
10
driver
1
White

Cẩm Huỳnh viết ngày 27/09/2017

Tình hình là mình vừa bị ném đá hội nghị trên diễn đàn Rust vì một câu hỏi ất ơ.

alt text

Nên mình đã quyết tâm viết cái gì đó bằng Rust để hiểu thêm về ngôn ngữ này.

Redis protocol

Mình chọn viết driver cho Redis vì protocol của nó khá đơn giản, có thể tóm gọn thành 3 bước chính:

1) Tạo một kết nối TCP.
2) Gửi buffer qua kết nối đến Redis server.
3) Hứng và xử lý kết quả do server trả về.

Rust

Theo những bài viết mà mình đọc được trên Kipalog thì Rust là một ngôn ngữ không ăn được, không có NULL :scream:, có thể viết test theo đúng thuần phong mĩ tuột, có một compiler cực xịn và quan trọng nhất là có thể tự học.

Xong! Xem như ta đã biết đầy đủ để tiến hành viết một Redis driver.

Tạo một kết nối TCP

Đầu tiên ta sẽ tạo một struct tên là RedisClient, với stream là kết nối TCP được lưu lại.

pub struct RedisClient {
    stream: TcpStream,
}

Để thêm các phương thức và hàm bà con vào một struct, ta có thể dùng từ khóa impl.

impl RedisClient {
    pub fn connect(address: String) -> RedisClient {
        let stream = TcpStream::connect(address).unwrap();
        return RedisClient { stream: stream };
    }
}

Rust là một ngôn ngữ static typing (tạm dịch: kiểu tĩnh), nên ta cần phải khai báo kiểu cho tham số truyền vào và kết quả trả về, ở đây hàm RedisClient::connect của chúng ta sẽ nhận vào một địa chỉ (của Redis) và trả về một struct RedisClient mà ta đã khai báo ở trên.

upwrap() là một tính năng khá đặc biết của Rust. Rust không sử dụng NULL và thay vào đó dùng kiểu Result. Nôm na là sau khi unwrap() một biến Result ta sẽ có thứ mà ta mong đợi. Đọc thêm về các cách xử lý lỗi trong Rust tại đây.

Gửi lệnh đến Redis server

impl RedisClient {
    pub fn command(&mut self, command: String){
        self.stream.write(command.as_bytes()).unwrap();
        self.stream.write(&[10]).unwrap(); // Redis đánh dấu sự kết thúc của một câu lệnh bằng kí hiệu xuống dòng
        self.stream.flush().unwrap();
    }
}

Ở đây ta sẽ cho cài đặt cho mỗi struct được tạo ra một phương thức thực thể (instance method) với &self (khá giống với Python nếu bạn biết ngôn ngữ này), ta dùng từ khóa mut (đừng đọc mút, nghe rất kì, hãy đọc là mutation) để khai báo rằng hàm này sẽ biến đổi self.

Trong Rust mặc định mọi biến đều là immutable (bất biến), bạn cần đặt mut trước một biến để khai báo rằng biến đó có thể bị biến đổi.

self là một RedisClient struct, nên tất nhiên là ta có thể truy cập đến thuộc tính stream: TcpStream của nó, ta sẽ cho stream viết dữ liệu và truyền đến cho Redis server, phương thức flush đảm bảo tất cả buffer được truyền đi.

Nhận phản hồi từ Redis server

Phản hồi cho một dòng lệnh từ Redis có dạng như sau

  • +OK\r\n cho lệnh thành công.
  • -ERR [msg]\r\n cho lệnh không thành công.

Ok, ta sẽ dựa vào đó để viết đoạn code nhận phản hồi.

Đọc một byte từ stream

Đầu tiên ta sẽ cài đặt một phương thức để đọc một byte từ stream

impl RedisClient {
    fn read_one_byte(stream: &mut TcpStream, buf: &mut [u8; 1]) {
        stream.read(buf).unwrap();
    }
}

[u8;1] là cách khai báo kiểu rằng đây là một mảng u8 có kích thước là 1. Ở hàm trên ta sẽ đọc 1 byte từ stream và ghi dữ liệu vào biến buf (lí do buf cần phải được mut).

Đọc hết các byte còn sót lại

impl RedisClient {
    fn read_full_response(stream: &mut TcpStream, vector: &mut Vec<u8>) {
        let mut output_buffer = [0u8; 1];

        while output_buffer[0] != b'\n' {
            RedisClient::read_one_byte(stream, &mut output_buffer);
            vector.push(output_buffer[0]);
        }
    }
}

Ở hàm này, ta sẽ đọc hết tất cả dữ liệu còn lại của stream và ghi vào vector cho đến khi gặp \n. Ta sẽ dùng vector vì không biết kích thước bytes trả về của Redis server là bao nhiêu.

Xử lý phản hồi từ server

Với hai hàm read_one_byteread_full_response, giờ ta có thể tiến hành cài đặt hàm xử lý phản hồi trả về.

impl RedisClient {
    fn handle_response(stream: &mut TcpStream) -> Result<String, String> {
        // Đọc byte đầu tiên trả về
        let mut sign_buf = [0u8; 1];
        RedisClient::read_one_byte(stream, &mut sign_buf);

        // Đọc tất cả các bytes còn lại
        let mut msg_vec = Vec::new();
        RedisClient::read_full_response(stream, &mut msg_vec);
        let msg = String::from_utf8(msg_vec).unwrap();

        match sign_buf[0] {
            b'+' => return Ok(msg),
            b'-' => return Err(msg),
            _ => return Err(format!("Got unknown message: {}", msg)),
        }
    }

}

Như phần ghi chú trong đoạn code trên đã nêu khá rõ, chúng ta sẽ đọc byte đầu tiên và kiểm tra nó là dấu + hay dấu -, rồi tiến về trả về Ok() hay Err()kèm theo thông điệp mà Redis đã gửi.

Gắn xử lý phản hồi trong phương thức command

Xong xuôi bây giờ ta sẽ gắn đoạn code xử lý phản hồi vào phương thức command, để hoàn thành nốt driver của chúng ta.

impl RedisClient {
    pub fn command(&mut self, command: String) -> Result<String, String> {
        self.stream.write(command.as_bytes()).unwrap();
        self.stream.write(&[10]).unwrap(); // new line
        self.stream.flush().unwrap();

        return RedisClient::handle_response(&mut self.stream);
    }
}

Tận hưởng thành quả

Đơn giản như đánh vần chữ thuở (u ơ uơ thờ uơ thuơ hỏi thuở).

fn main() {
    let string = String::from("127.0.0.1:6379");
    let mut client = RedisClient::connect(string);

    // command hợp lệ
    let command = String::from("SET a 200");
    let response = client.command(command);
    println!("{:?}", buffer);

    // command không hợp lệ
    let command = String::from("ET a 200");
    let response = client.command(command);
    println!("{:?}", buffer);
}

Ta sẽ chạy nó với cargo run.

Compiling redis-rs v0.1.0 (file:///Users/hqc/workspace/redis-rs)
Finished dev [unoptimized + debuginfo] target(s) in 1.19 secs
Running `target/debug/redis-rs`

Ok("OK\r\n")
Err("ERR unknown command \'ET\'\r\n")

Tất cả code trong bài viết này có thể được xem tại Github.

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Cẩm Huỳnh

40 bài viết.
336 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
39 9
(Ảnh) Vì sao lại là Bật Đèn? Ai từng đọc qua Tắt Đèn hẳn đã biết tác phẩm được kết thúc bằng tình huống: Buông tay, chị vội choàng dậy, mở cửa...
Cẩm Huỳnh viết 1 năm trước
39 9
White
37 6
Làm thế nào để chỉ với một đoạn text vài trăm ký tự, bạn có thể làm ngốn vài gigabyte bộ nhớ và từ chối dịch vụ của một hệ thống dùng XML? _____ ...
Cẩm Huỳnh viết 4 tháng trước
37 6
White
34 25
Vừa rồi mình vừa tiết kiệm được $5 mỗi tháng sau khi migrate cái (Link) từ Digital Ocean sang Heroku Free Dyno. (Ảnh) Kết quả thật mĩ mãn vì hầu ...
Cẩm Huỳnh viết 12 tháng trước
34 25
Bài viết liên quan
White
3 3
Hôm nay tự viết lại cái note này về quá trình học Rust, xem như là tự giúp mình nhớ sâu và rõ hơn về Rust. Note này sẽ ngắn gọn hơn bản (Link). Sơ...
Giang Nguyen viết hơn 1 năm trước
3 3
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
40 bài viết.
336 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!