Quần Cam

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

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 , 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.

NOTE: Trong thực tế không ai đọc từng byte một cả vì nó có hao phí khi copy dữ liệu từ kernel space sang user space.

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.


NGUY HIỂM! KHU VỰC NHIỀU GIÓ!
Khuyến cáo giữ chặt bàn phím và lướt thật nhanh khi đi qua khu vực này.
Chức năng này hỗ trợ markdown và các thứ liên quan.

Bài viết cùng chủ đề

Elixir - Ngôn ngữ được viết bằng macros

Macro không những là một trong những viên gạch giúp bạn meta-program với Elixir, mà nó còn là công cụ giúp Elixir meta-program … chính nó nữa.

Một số kĩ thuật caching với HTTP/1.1

Giới thiệu RFC7234 và một số kỹ thuật tăng tốc web với HTTP/1.1 Caching.

IO data và Vectored IO

Bài viết giới thiệu về IO data, Vectored I/O và tối ưu hóa hệ thống dùng Elixir bằng cách tận dụng Vectored I/O.