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.
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 ơ.
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.
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ề.
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.
Đầ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ểuResult
. Nôm na là sau khiunwrap()
một biếnResult
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.
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.
Vì 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.
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.
Đầ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
).
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.
Với hai hàm read_one_byte
và read_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.
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);
}
}
Đơ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.
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.
Giới thiệu RFC7234 và một số kỹ thuật tăng tốc web với HTTP/1.1 Caching.
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.