├── 0_base64 ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ ├── base64.rs │ └── main.rs ├── 1_sha1 ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ ├── main.rs │ └── sha1.rs ├── 2_websocket ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── client.py ├── client │ └── index.html └── src │ ├── base64.rs │ ├── main.rs │ ├── sha1.rs │ └── websocket.rs ├── 3_io_uring ├── .gitignore ├── Cargo.toml ├── README.md ├── build.rs ├── src │ └── main.rs └── wrapper.h ├── 4_io_uring_echo_server ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── echo_client.py ├── src │ ├── echo_server.rs │ ├── entry.rs │ ├── iouring.rs │ └── main.rs └── wrapper.h └── README.md /0_base64/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /0_base64/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "base64" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /0_base64/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "base64" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /0_base64/README.md: -------------------------------------------------------------------------------- 1 | ### Base64 in Rust 2 | 3 | This is part of a project I'm working on where I plan to build a game using the Rust standard library. Each part is a project unto itself, with this being an implementation of the Base64 that I will use in the websocket library. 4 | 5 | You can find an [article](https://www.thespatula.io/rust/rust_base64/) going over this here and a [video](https://youtu.be/VYU38e-g9sM) here. 6 | 7 | To set this up you need to clone the repo and run it: 8 | 9 | ``` 10 | git clone https://github.com/kilroyjones/bas64-tutorial/ 11 | cd base64-tutorial 12 | cargo run 13 | ``` 14 | 15 | As long as you have Rust installed it should run immediately. 16 | -------------------------------------------------------------------------------- /0_base64/src/base64.rs: -------------------------------------------------------------------------------- 1 | use std::string::FromUtf8Error; 2 | 3 | const BASE64_CHARSET: &[u8; 64] = 4 | b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 5 | 6 | pub struct Base64; 7 | 8 | #[derive(Debug)] 9 | pub enum Base64Error { 10 | InvalidCharacter, 11 | Utf8Error(FromUtf8Error), 12 | } 13 | 14 | impl std::fmt::Display for Base64Error { 15 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 16 | match *self { 17 | Base64Error::InvalidCharacter => write!(f, "Invalid character in input"), 18 | Base64Error::Utf8Error(ref e) => e.fmt(f), 19 | } 20 | } 21 | } 22 | 23 | impl Base64 { 24 | pub fn new() -> Self { 25 | Base64 {} 26 | } 27 | 28 | pub fn encode(&mut self, input: &str) -> Result { 29 | let mut encoded = Vec::new(); 30 | let bytes = input.as_bytes(); 31 | let mut buffer: u32; 32 | 33 | for chunk in bytes.chunks(3) { 34 | buffer = match chunk.len() { 35 | 3 => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]), 36 | 2 => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8), 37 | 1 => u32::from(chunk[0]) << 16, 38 | _ => 0, 39 | }; 40 | 41 | let output_chars = chunk.len() + 1; 42 | 43 | for i in 0..4 { 44 | if i < output_chars { 45 | let shift = 18 - i * 6; 46 | let temp = buffer >> shift; 47 | let index = (temp & 63) as usize; 48 | encoded.push(BASE64_CHARSET[index]); 49 | } else { 50 | encoded.push(b'='); 51 | } 52 | } 53 | } 54 | 55 | String::from_utf8(encoded).map_err(Base64Error::Utf8Error) 56 | } 57 | 58 | pub fn decode(&mut self, input: &str) -> Result { 59 | let mut decoded = Vec::new(); 60 | let mut buffer = 0u32; 61 | let mut bits_collected = 0; 62 | 63 | for c in input.chars() { 64 | if c != '=' { 65 | let position = BASE64_CHARSET.iter().position(|&x| x == c as u8); 66 | 67 | match position { 68 | Some(pos) => { 69 | buffer = (buffer << 6) | pos as u32; 70 | bits_collected += 6; 71 | 72 | while bits_collected >= 8 { 73 | bits_collected -= 8; 74 | let byte = (buffer >> bits_collected) & 0xFF; 75 | decoded.push(byte as u8); 76 | } 77 | } 78 | None => return Err(Base64Error::InvalidCharacter), 79 | } 80 | } 81 | } 82 | 83 | String::from_utf8(decoded).map_err(Base64Error::Utf8Error) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /0_base64/src/main.rs: -------------------------------------------------------------------------------- 1 | mod base64; 2 | 3 | use base64::Base64; 4 | fn main() { 5 | let original = "abcde"; 6 | let mut base64 = Base64::new(); 7 | 8 | let encoded = base64.encode(original).unwrap(); 9 | // let encoded = match base64.encode(original) { 10 | // Ok(e) => e, 11 | // Err(e) => panic!("{}", e), 12 | // }; 13 | 14 | let decoded = base64.decode(&encoded).unwrap(); 15 | // let decoded = match base64.decode(&encoded) { 16 | // Ok(d) => d, 17 | // Err(e) => panic!("{}", e), 18 | // }; 19 | 20 | println!("Original: {}", original); 21 | println!("Encoded: {}", encoded); 22 | println!("Decoded: {}", decoded); 23 | } 24 | 25 | #[cfg(test)] 26 | mod tests { 27 | use super::base64::Base64; 28 | 29 | #[test] 30 | fn test_base64_encode_decode() { 31 | let original = "hello world"; 32 | let mut base64 = Base64::new(); 33 | 34 | let encoded = base64.encode(original).unwrap(); 35 | let decoded = base64.decode(&encoded).unwrap(); 36 | 37 | assert_eq!( 38 | original, decoded, 39 | "Original and decoded messages do not match" 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /1_sha1/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /1_sha1/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "sha1" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /1_sha1/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sha1" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | 11 | # Updated to make use of LTO optimizations 12 | [profile.release] 13 | lto = true -------------------------------------------------------------------------------- /1_sha1/README.md: -------------------------------------------------------------------------------- 1 | ### SHA-1 in Rust 2 | 3 | This is part of a project I'm working on where I plan to build a game using the Rust standard library. Each part is a project unto itself, with this being an implementation of the SHA-1 hashing algorithm that I will use in the websocket library. 4 | 5 | You can find an article going over this [here](https://www.thespatula.io/rust/rust_sha1/) and a video [here](https://youtu.be/A_VMlxonLXs). 6 | 7 | To set this up you need to clone the repo and run it: 8 | 9 | ```bash 10 | git clone https://github.com/kilroyjones/sha1-tutorial/ 11 | cd sha1-tutorial 12 | cargo run 13 | ``` 14 | 15 | As long as you have Rust installed it should run immediately. 16 | -------------------------------------------------------------------------------- /1_sha1/src/main.rs: -------------------------------------------------------------------------------- 1 | mod sha1; 2 | 3 | use sha1::Sha1; 4 | 5 | fn main() { 6 | let mut hasher = Sha1::new(); 7 | let res = hasher.hash("knownhash".to_owned()); 8 | println!("{:?}", res); 9 | } 10 | -------------------------------------------------------------------------------- /1_sha1/src/sha1.rs: -------------------------------------------------------------------------------- 1 | // SHA-1 hashing algorithm initial hash values. 2 | // These constants are derived from the fractional parts of the square roots of the first five primes. 3 | const H0: u32 = 0x67452301; 4 | const H1: u32 = 0xEFCDAB89; 5 | const H2: u32 = 0x98BADCFE; 6 | const H3: u32 = 0x10325476; 7 | const H4: u32 = 0xC3D2E1F0; 8 | 9 | pub struct Sha1; 10 | 11 | impl Sha1 { 12 | /// Constructs a new `Sha1` hasher. 13 | pub fn new() -> Self { 14 | Sha1 {} 15 | } 16 | 17 | /// Computes the SHA-1 hash of the input string by taking in either a String of str type. 18 | pub fn hash(&mut self, key: String) -> [u8; 20] { 19 | // Initialize variables to the SHA-1's initial hash values. 20 | let (mut h0, mut h1, mut h2, mut h3, mut h4) = (H0, H1, H2, H3, H4); 21 | let (mut a, mut b, mut c, mut d, mut e); 22 | 23 | // Pad our key 24 | let msg = self.pad_message(key.as_ref()); 25 | 26 | // Process each 512-bit chunk of the padded message. 27 | for chunk in msg.chunks(64) { 28 | // Get the message schedule and copies of our initial SHA-1 values. 29 | let schedule = self.build_schedule(chunk); 30 | 31 | a = h0; 32 | b = h1; 33 | c = h2; 34 | d = h3; 35 | e = h4; 36 | 37 | // Main loop of the SHA-1 algorithm using predefind values based on primes numbers. 38 | for i in 0..80 { 39 | let (f, k) = match i { 40 | 0..=19 => ((b & c) | ((!b) & d), 0x5A827999), 41 | 20..=39 => (b ^ c ^ d, 0x6ED9EBA1), 42 | 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC), 43 | _ => (b ^ c ^ d, 0xCA62C1D6), 44 | }; 45 | 46 | // Update the temporary variable and then update the hash values 47 | // in a manner that enforces both diffusion and confusion. Note 48 | // how the "scrambled" data trickles through the variables as we 49 | // loop through. 50 | let temp = a 51 | .rotate_left(5) 52 | .wrapping_add(f) 53 | .wrapping_add(e) 54 | .wrapping_add(k) 55 | .wrapping_add(schedule[i]); 56 | e = d; 57 | d = c; 58 | c = b.rotate_left(30); 59 | b = a; 60 | a = temp; 61 | } 62 | 63 | // Add the compressed chunk to the current hash value. 64 | h0 = h0.wrapping_add(a); 65 | h1 = h1.wrapping_add(b); 66 | h2 = h2.wrapping_add(c); 67 | h3 = h3.wrapping_add(d); 68 | h4 = h4.wrapping_add(e); 69 | } 70 | 71 | // Produce the final hash value as a 20-byte array. 72 | let mut hash = [0u8; 20]; 73 | 74 | hash[0..4].copy_from_slice(&h0.to_be_bytes()); 75 | hash[4..8].copy_from_slice(&h1.to_be_bytes()); 76 | hash[8..12].copy_from_slice(&h2.to_be_bytes()); 77 | hash[12..16].copy_from_slice(&h3.to_be_bytes()); 78 | hash[16..20].copy_from_slice(&h4.to_be_bytes()); 79 | 80 | hash 81 | } 82 | 83 | /// Pads the input message according to SHA-1 specifications. 84 | /// This includes appending a '1' bit followed by '0' bits and finally the message length. 85 | fn pad_message(&self, input: &str) -> Vec { 86 | let mut bytes = input.as_bytes().to_vec(); 87 | 88 | // Save the original message length for appending below. 89 | let original_bit_length = bytes.len() as u64 * 8; 90 | 91 | // Append the '1' at the most most significant bit: 10000000 92 | bytes.push(0x80); 93 | 94 | // Pad with '0' bytes until the message's length in bits modules 512 is 448. 95 | while (bytes.len() * 8) % 512 != 448 { 96 | bytes.push(0); 97 | } 98 | 99 | // Append the original message length. 100 | bytes.extend_from_slice(&original_bit_length.to_be_bytes()); 101 | 102 | bytes 103 | } 104 | 105 | /// Builds the message schedule array from a 512-bit chunk. 106 | fn build_schedule(&mut self, chunk: &[u8]) -> [u32; 80] { 107 | let mut schedule = [0u32; 80]; 108 | 109 | // Initialize the first 16 words in the array from the chunk. 110 | for (i, block) in chunk.chunks(4).enumerate() { 111 | schedule[i] = u32::from_be_bytes(block.try_into().unwrap()); 112 | } 113 | 114 | // Extend the schedule array using previously defined values and the XOR (^) operation. 115 | for i in 16..80 { 116 | schedule[i] = schedule[i - 3] ^ schedule[i - 8] ^ schedule[i - 14] ^ schedule[i - 16]; 117 | schedule[i] = schedule[i].rotate_left(1); 118 | } 119 | 120 | schedule 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /2_websocket/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /2_websocket/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "websocket" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /2_websocket/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "websocket" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | -------------------------------------------------------------------------------- /2_websocket/README.md: -------------------------------------------------------------------------------- 1 | The article associated with this can be found [here](https://www.thespatula.io/rust/rust_websocket/) 2 | 3 | You can test the code out on your own by following the steps in the README or the directions below. 4 | 5 | First, clone the repo: 6 | 7 | ```bash 8 | git clone https://github.com/kilroyjones/websockets_from_scratch 9 | ``` 10 | 11 | Then run it: 12 | 13 | ```bash 14 | cd websockets_from_scratch/2_websocket 15 | cargo run 16 | ``` 17 | 18 | In the same folder, there is a client folder which contains a single HTML page that can be run to test the server. How you run this is up to you, but if you have python3 installed, you can do: 19 | 20 | ````bash 21 | python3 -m http.server 22 | ``` 23 | 24 | After that, you’ll be able to navigate to http://localhost:8000 and see that everything is working. 25 | ```` 26 | -------------------------------------------------------------------------------- /2_websocket/client.py: -------------------------------------------------------------------------------- 1 | import websocket 2 | import time 3 | import threading 4 | 5 | def on_message(ws, message): 6 | print("Received from server: " + message) 7 | 8 | def on_error(ws, error): 9 | print("Error: " + str(error)) 10 | 11 | def on_close(ws, close_status_code, close_msg): 12 | print("### Closed ###") 13 | print("Close status code: ", close_status_code) 14 | print("Close message: ", close_msg) 15 | 16 | def on_open(ws): 17 | def run(*args): 18 | for i in range(10): 19 | time.sleep(3) 20 | message = "Hello Server {}".format(i) 21 | ws.send(message) 22 | print("Sent to server: " + message) 23 | time.sleep(1) 24 | ws.close() 25 | print("Thread terminating...") 26 | thread = threading.Thread(target=run) 27 | thread.start() 28 | 29 | if __name__ == "__main__": 30 | websocket.enableTrace(True) 31 | ws = websocket.WebSocketApp("ws://127.0.0.1:8080/", 32 | on_open=on_open, 33 | on_message=on_message, 34 | on_error=on_error, 35 | on_close=on_close) 36 | 37 | ws.run_forever() 38 | -------------------------------------------------------------------------------- /2_websocket/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebSocket Echo Client 7 | 21 | 22 | 23 |

WebSocket Echo Client

24 |
    25 |
    26 | 27 |
    28 | 29 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /2_websocket/src/base64.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_variables)] 2 | 3 | //! Type for representing Base64 numbers 4 | //! 5 | //! Implements the following: 6 | //! - encoode: takes in a u8 char array of 20 characters and 7 | //! 8 | 9 | use std::fmt; 10 | use std::string::FromUtf8Error; 11 | 12 | const BASE64_CHARSET: &[u8; 64] = 13 | b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 14 | 15 | pub struct Base64; 16 | 17 | #[derive(Debug)] 18 | pub enum Base64Error { 19 | InvalidCharacter, 20 | Utf8Error(FromUtf8Error), 21 | } 22 | 23 | impl fmt::Display for Base64Error { 24 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 25 | match *self { 26 | Base64Error::InvalidCharacter => write!(f, "Invalid character in input"), 27 | Base64Error::Utf8Error(ref e) => e.fmt(f), 28 | } 29 | } 30 | } 31 | 32 | impl Base64 { 33 | pub fn new() -> Self { 34 | Base64 {} 35 | } 36 | 37 | pub fn encode(&mut self, input: [u8; 20]) -> Result { 38 | let mut encoded = Vec::new(); 39 | let mut buffer: u32; 40 | 41 | for chunk in input.chunks(3) { 42 | buffer = match chunk.len() { 43 | 3 => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8) | u32::from(chunk[2]), 44 | 2 => (u32::from(chunk[0]) << 16) | (u32::from(chunk[1]) << 8), 45 | 1 => u32::from(chunk[0]) << 16, 46 | _ => 0, 47 | }; 48 | 49 | let output_chars = chunk.len() + 1; 50 | 51 | for i in 0..4 { 52 | if i < output_chars { 53 | let shift = 18 - i * 6; 54 | let temp = buffer >> shift; 55 | let index = (temp & 63) as usize; 56 | encoded.push(BASE64_CHARSET[index]); 57 | } else { 58 | encoded.push(b'='); 59 | } 60 | } 61 | } 62 | 63 | String::from_utf8(encoded).map_err(Base64Error::Utf8Error) 64 | } 65 | 66 | pub fn decode(&mut self, input: &str) -> Result { 67 | let mut decoded = Vec::new(); 68 | let mut buffer = 0u32; 69 | let mut bits_collected = 0; 70 | 71 | for c in input.chars() { 72 | if c != '=' { 73 | let position = BASE64_CHARSET.iter().position(|&x| x == c as u8); 74 | 75 | match position { 76 | Some(pos) => { 77 | buffer = (buffer << 6) | pos as u32; 78 | bits_collected += 6; 79 | 80 | while bits_collected >= 8 { 81 | bits_collected -= 8; 82 | let byte = (buffer >> bits_collected) & 0xFF; 83 | decoded.push(byte as u8); 84 | } 85 | } 86 | None => return Err(Base64Error::InvalidCharacter), 87 | } 88 | } 89 | } 90 | 91 | String::from_utf8(decoded).map_err(Base64Error::Utf8Error) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /2_websocket/src/main.rs: -------------------------------------------------------------------------------- 1 | mod base64; 2 | mod sha1; 3 | mod websocket; 4 | 5 | use std::net::{TcpListener, TcpStream}; 6 | use std::thread; 7 | 8 | use websocket::WebSocket; 9 | 10 | /// Handles a connection using our websockets 11 | /// 12 | /// We create a new WebSocket instance, pass it the stream and then connect. 13 | /// 14 | fn handle_client(stream: TcpStream) { 15 | let mut ws = WebSocket::new(stream); 16 | 17 | match ws.connect() { 18 | Ok(()) => { 19 | println!("WebSocket connection established"); 20 | match ws.handle_connection() { 21 | Ok(_) => { 22 | println!("Connection ended without error"); 23 | } 24 | Err(e) => { 25 | println!("Connection ended with error {:?}", e); 26 | } 27 | } 28 | } 29 | Err(e) => { 30 | println!("Failed to establish a WebSocket connection: {}", e); 31 | } 32 | } 33 | } 34 | 35 | /// Listens for incoming connections 36 | /// 37 | /// We listen to incoming connections and create new threads for each one of them. 38 | /// 39 | fn main() { 40 | // 41 | let listener = TcpListener::bind("127.0.0.1:8080").expect("Could not bind to port"); 42 | println!("WebSocket server is running on ws://127.0.0.1:8080/"); 43 | 44 | for stream in listener.incoming() { 45 | match stream { 46 | Ok(stream) => { 47 | thread::spawn(move || { 48 | handle_client(stream); 49 | }); 50 | } 51 | Err(e) => { 52 | println!("Failed to accept client: {}", e); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /2_websocket/src/sha1.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | // SHA-1 hashing algorithm initial hash values. 4 | // These constants are derived from the fractional parts of the square roots of the first five primes. 5 | const H0: u32 = 0x67452301; 6 | const H1: u32 = 0xEFCDAB89; 7 | const H2: u32 = 0x98BADCFE; 8 | const H3: u32 = 0x10325476; 9 | const H4: u32 = 0xC3D2E1F0; 10 | 11 | #[derive(Debug)] 12 | pub enum Sha1Error { 13 | InputConversionFailure(String), 14 | } 15 | 16 | impl fmt::Display for Sha1Error { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | match *self { 19 | Sha1Error::InputConversionFailure(ref msg) => { 20 | write!(f, "Input conversion failed: {}", msg) 21 | } 22 | } 23 | } 24 | } 25 | 26 | impl std::error::Error for Sha1Error {} 27 | 28 | pub struct Sha1; 29 | 30 | impl Sha1 { 31 | /// Constructs a new `Sha1` hasher. 32 | pub fn new() -> Self { 33 | Sha1 {} 34 | } 35 | 36 | /// Computes the SHA-1 hash of the input string by taking in either a String of str type. 37 | pub fn hash(&mut self, key: String) -> Result<[u8; 20], Sha1Error> { 38 | // Initialize variables to the SHA-1's initial hash values. 39 | let (mut h0, mut h1, mut h2, mut h3, mut h4) = (H0, H1, H2, H3, H4); 40 | let (mut a, mut b, mut c, mut d, mut e); 41 | 42 | // Pad our key 43 | let msg = self.pad_message(key.as_ref()); 44 | 45 | // Process each 512-bit chunk of the padded message. 46 | for chunk in msg.chunks(64) { 47 | // Get the message schedule and copies of our initial SHA-1 values. 48 | let schedule = self.build_schedule(chunk)?; 49 | 50 | a = h0; 51 | b = h1; 52 | c = h2; 53 | d = h3; 54 | e = h4; 55 | 56 | // Main loop of the SHA-1 algorithm using predefind values based on primes numbers. 57 | for i in 0..80 { 58 | let (f, k) = match i { 59 | 0..=19 => ((b & c) | ((!b) & d), 0x5A827999), 60 | 20..=39 => (b ^ c ^ d, 0x6ED9EBA1), 61 | 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDC), 62 | _ => (b ^ c ^ d, 0xCA62C1D6), 63 | }; 64 | 65 | // Update the temporary variable and then update the hash values 66 | // in a manner that enforces both diffusion and confusion. Note 67 | // how the "scrambled" data trickles through the variables as we 68 | // loop through. 69 | let temp = a 70 | .rotate_left(5) 71 | .wrapping_add(f) 72 | .wrapping_add(e) 73 | .wrapping_add(k) 74 | .wrapping_add(schedule[i]); 75 | e = d; 76 | d = c; 77 | c = b.rotate_left(30); 78 | b = a; 79 | a = temp; 80 | } 81 | 82 | // Add the compressed chunk to the current hash value. 83 | h0 = h0.wrapping_add(a); 84 | h1 = h1.wrapping_add(b); 85 | h2 = h2.wrapping_add(c); 86 | h3 = h3.wrapping_add(d); 87 | h4 = h4.wrapping_add(e); 88 | } 89 | 90 | // Produce the final hash value as a 20-byte array. 91 | let mut hash = [0u8; 20]; 92 | 93 | hash[0..4].copy_from_slice(&h0.to_be_bytes()); 94 | hash[4..8].copy_from_slice(&h1.to_be_bytes()); 95 | hash[8..12].copy_from_slice(&h2.to_be_bytes()); 96 | hash[12..16].copy_from_slice(&h3.to_be_bytes()); 97 | hash[16..20].copy_from_slice(&h4.to_be_bytes()); 98 | 99 | Ok(hash) 100 | } 101 | 102 | /// Pads the input message according to SHA-1 specifications. 103 | /// This includes appending a '1' bit followed by '0' bits and finally the message length. 104 | fn pad_message(&self, input: &str) -> Vec { 105 | let mut bytes = input.as_bytes().to_vec(); 106 | 107 | // Save the original message length for appending below. 108 | let original_bit_length = bytes.len() as u64 * 8; 109 | 110 | // Append the '1' at the most most significant bit: 10000000 111 | bytes.push(0x80); 112 | 113 | // Pad with '0' bytes until the message's length in bits modules 512 is 448. 114 | while (bytes.len() * 8) % 512 != 448 { 115 | bytes.push(0); 116 | } 117 | 118 | // Append the original message length. 119 | bytes.extend_from_slice(&original_bit_length.to_be_bytes()); 120 | 121 | bytes 122 | } 123 | 124 | /// Builds the message schedule array from a 512-bit chunk. 125 | fn build_schedule(&mut self, chunk: &[u8]) -> Result<[u32; 80], Sha1Error> { 126 | let mut schedule = [0u32; 80]; 127 | 128 | // Initialize the first 16 words in the array from the chunk. 129 | for (i, block) in chunk.chunks(4).enumerate() { 130 | // Attempt to convert the block into u32 131 | let converted_block = block.try_into().map_err(|_| { 132 | Sha1Error::InputConversionFailure("Failed to convert chunk into u32".to_string()) 133 | })?; 134 | schedule[i] = u32::from_be_bytes(converted_block); 135 | } 136 | 137 | // Extend the schedule array using previously defined values and the XOR (^) operation. 138 | for i in 16..80 { 139 | schedule[i] = schedule[i - 3] ^ schedule[i - 8] ^ schedule[i - 14] ^ schedule[i - 16]; 140 | schedule[i] = schedule[i].rotate_left(1); 141 | } 142 | 143 | Ok(schedule) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /2_websocket/src/websocket.rs: -------------------------------------------------------------------------------- 1 | //! Websocket 2 | //! 3 | //! This is a "from scratch" websocket implementation in that it uses onlhy the 4 | //! Rust standard library. This is a minimal implementation is meant as a 5 | //! learning tool only. 6 | //! 7 | 8 | use crate::base64::Base64; 9 | use crate::sha1::Sha1; 10 | 11 | use std::fmt; 12 | use std::io::{self, Read, Write}; 13 | use std::net::TcpStream; 14 | use std::str; 15 | use std::time::Duration; 16 | 17 | /// Frame 18 | /// 19 | /// Denotes the types of websocket frames we'll be working with. Frames are a 20 | /// "header + data" and that data could be binary or text as denoted by "Data" 21 | /// below. Alternatively, it could frame for a ping, pong or to close a the 22 | /// socket (the shortest of frames) 23 | /// 24 | #[derive(Debug)] 25 | pub enum Frame { 26 | Text(Vec), 27 | Binary(Vec), 28 | Ping, 29 | Pong, 30 | Close, 31 | } 32 | 33 | /// WebSocketError 34 | /// 35 | /// These are our custom error messages. 36 | /// 37 | /// HandshakeError: Provides errors during the initial connection process. 38 | /// 39 | /// IoError: Primarily details with errors that occur during sending and 40 | /// receiving messages. 41 | /// 42 | /// NonGetRequest: A one-off request used upon connection. 43 | /// 44 | /// ProtocolError: When parsing the frame these messages will occur if the 45 | /// frame is malformed. 46 | /// 47 | /// Utf8Error: Used when checking incoming data. 48 | /// 49 | #[derive(Debug)] 50 | pub enum WebSocketError { 51 | HandshakeError(String), 52 | IoError(io::Error), 53 | NonGetRequest, 54 | ProtocolError(String), 55 | Utf8Error(str::Utf8Error), 56 | } 57 | 58 | /// WebSocketError Display implementation 59 | /// 60 | /// These are wrappers for writing our error messages out. 61 | /// 62 | impl fmt::Display for WebSocketError { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | match *self { 65 | WebSocketError::HandshakeError(ref msg) => write!(f, "Handshake error: {}", msg), 66 | WebSocketError::IoError(ref err) => write!(f, "I/O error: {}", err), 67 | WebSocketError::NonGetRequest => write!(f, "Received non-GET request"), 68 | WebSocketError::ProtocolError(ref msg) => write!(f, "Protocol error: {}", msg), 69 | WebSocketError::Utf8Error(ref err) => write!(f, "UTF-8 decoding error: {}", err), 70 | } 71 | } 72 | } 73 | 74 | /// Allows for automatic conversion from io:Error to WebSocketError 75 | /// 76 | impl From for WebSocketError { 77 | fn from(err: io::Error) -> WebSocketError { 78 | WebSocketError::IoError(err) 79 | } 80 | } 81 | 82 | /// Allows for automatic conversion from str::Utf8Error to WebSocketError 83 | /// 84 | impl From for WebSocketError { 85 | fn from(err: str::Utf8Error) -> WebSocketError { 86 | WebSocketError::Utf8Error(err) 87 | } 88 | } 89 | 90 | /// Defines the WebSocket 91 | /// 92 | /// For now the WebSocket is only composed of a TcpStream, but normally we'd 93 | /// want to attach other information about the connection to it. 94 | /// 95 | pub struct WebSocket { 96 | stream: TcpStream, 97 | } 98 | 99 | impl WebSocket { 100 | /// Creates the WebSocket instance 101 | /// 102 | pub fn new(stream: TcpStream) -> WebSocket { 103 | WebSocket { stream } 104 | } 105 | 106 | /// Connect the websocket 107 | /// 108 | /// This will read in the HTTP request and check if it's a GET or not. It will then 109 | /// call the handle_handshake function which parses the request header. 110 | /// 111 | pub fn connect(&mut self) -> Result<(), WebSocketError> { 112 | let mut buffer: [u8; 1024] = [0; 1024]; 113 | 114 | // From the stream read in the HTTP request 115 | let byte_length = match self.stream.read(&mut buffer) { 116 | Ok(bytes) => bytes, 117 | Err(e) => return Err(WebSocketError::IoError(e)), 118 | }; 119 | 120 | // Read only the request from the buffer 121 | let request = str::from_utf8(&buffer[..byte_length])?; 122 | 123 | // We only want to deal with GET requests for the upgrade 124 | if request.starts_with("GET") == false { 125 | return Err(WebSocketError::NonGetRequest); 126 | } 127 | 128 | // Get the HTTP response header and send it back 129 | let response = self.handle_handshake(request)?; 130 | self.stream 131 | .write_all(response.as_bytes()) 132 | .map_err(WebSocketError::IoError)?; 133 | 134 | self.stream.flush().map_err(WebSocketError::IoError)?; 135 | Ok(()) 136 | } 137 | 138 | /// Validate the websocket upgrade request 139 | /// 140 | /// Checks that the Sec-WebSocket-Key exists and then formulates a response 141 | /// key, hashing it using sha-1 and then encoding with base64. There is a hardcoded 142 | /// HTTP response attached to the header to upgrade the connection to websockets. 143 | /// 144 | fn handle_handshake(&mut self, request: &str) -> Result { 145 | let mut base64 = Base64::new(); 146 | let mut sha1 = Sha1::new(); 147 | 148 | let key_header = "Sec-WebSocket-Key: "; 149 | 150 | // Given the request we find the line starting the the key_header and then find the 151 | // key sent from the client. 152 | let key = request 153 | .lines() 154 | .find(|line| line.starts_with(key_header)) 155 | .map(|line| line[key_header.len()..].trim()) 156 | .ok_or_else(|| { 157 | WebSocketError::HandshakeError( 158 | "Could not find Sec-WebSocket-Key in HTTP request header".to_string(), 159 | ) 160 | })?; 161 | 162 | // Append key with the necessary id as per the WebSocket Protocol specification 163 | let response_key = format!("{}258EAFA5-E914-47DA-95CA-C5AB0DC85B11", key); 164 | 165 | // First we take the hash of the random key sent by the client 166 | let hash = sha1.hash(response_key).map_err(|_| { 167 | WebSocketError::HandshakeError("Failed to hash the response key".to_string()) 168 | })?; 169 | 170 | // Second we encode that hash as Base64 171 | let header_key = base64.encode(hash).map_err(|_| { 172 | WebSocketError::HandshakeError("Failed to encode the hash as Base64".to_string()) 173 | })?; 174 | 175 | // Lastly we attach that key to the our response header 176 | Ok(format!( 177 | "HTTP/1.1 101 Switching Protocols\r\n\ 178 | Upgrade: websocket\r\n\ 179 | Connection: Upgrade\r\n\ 180 | Sec-WebSocket-Accept: {}\r\n\r\n", 181 | header_key 182 | )) 183 | } 184 | 185 | /// Handles the connection 186 | /// 187 | /// This is a loop which will continue until either the connection is 188 | /// terminated (Frame::Close) or a connection timeout which is currently 189 | /// hardcoded as 5 seconds. 190 | /// 191 | /// Currently it handles PING, PONG, CLOSE and TEXT or BINARY data. 192 | /// 193 | /// Note: Later I will move this functionality outside of websocket.rs. 194 | /// 195 | pub fn handle_connection(&mut self) -> Result<(), WebSocketError> { 196 | // A buffer of 2048 should be large enough to handle incoming data. 197 | let mut buffer = [0; 2048]; 198 | 199 | // Send initial ping 200 | self.send_ping()?; 201 | let mut last_ping = std::time::Instant::now(); 202 | let mut pong_received = false; 203 | 204 | // Primary loop which runs inside the thread spawned in main.rs 205 | loop { 206 | // This is the check to see if the connection has timed out or not. 207 | // We've hardcoded it to a default of 10 seconds, but it would be 208 | // good have this configurable later on. 209 | if last_ping.elapsed() > Duration::from_secs(10) { 210 | if pong_received == false { 211 | println!("Pong not received; disconnecting client."); 212 | break; 213 | } 214 | 215 | if let Err(_) = self.send_ping() { 216 | println!("Ping failed; disconnecting client."); 217 | break; 218 | } 219 | 220 | pong_received = false; 221 | last_ping = std::time::Instant::now(); 222 | } 223 | 224 | // Read in the current stream or data. 225 | match self.stream.read(&mut buffer) { 226 | // read(&mut buffer) will return a usize, and we'll want to process that if and only 227 | // if it's larger than 0. We then parse the frame in the parse_frame function. 228 | Ok(n) if n > 0 => match self.parse_frame(&buffer[..n]) { 229 | Ok(Frame::Pong) => { 230 | println!("Pong received"); 231 | pong_received = true; 232 | continue; 233 | } 234 | 235 | Ok(Frame::Ping) => { 236 | if self.send_pong().is_err() { 237 | println!("Failed to send pong"); 238 | break; 239 | } 240 | } 241 | 242 | Ok(Frame::Close) => { 243 | println!("Client initiated close"); 244 | break; 245 | } 246 | 247 | Ok(Frame::Text(data)) => match String::from_utf8(data) { 248 | Ok(valid_text) => { 249 | println!("Received data: {}", valid_text); 250 | if self.send_text(&valid_text).is_err() { 251 | println!("Failed to send echo message"); 252 | break; 253 | } 254 | } 255 | Err(utf8_err) => { 256 | return Err(WebSocketError::Utf8Error(utf8_err.utf8_error())); 257 | } 258 | }, 259 | 260 | // We are not going to handle this binary data at this point. 261 | Ok(Frame::Binary(data)) => { 262 | println!("Binary data received: {:?}", data); 263 | continue; 264 | } 265 | 266 | Err(e) => { 267 | println!("Error parsing frame: {}", e); 268 | break; 269 | } 270 | }, 271 | Ok(_) => {} 272 | // If there's an error, end the connection 273 | Err(e) => { 274 | println!("Error reading from stream: {}", e); 275 | break; 276 | } 277 | } 278 | } 279 | Ok(()) 280 | } 281 | 282 | /// Parses in incoming frame 283 | /// 284 | /// This function goes through the following steps: 285 | /// 1. Validates length 286 | /// 2. Checks if frame is masked 287 | /// 3. Checks extended payload length 288 | /// 4. Decodes the using XOR with the mask 289 | /// 5. Returns an Ok with opcode and data (if exists) 290 | /// 291 | fn parse_frame(&mut self, buffer: &[u8]) -> Result { 292 | // The smallest length it can be is two bytes for a Close frame 293 | if buffer.len() < 2 { 294 | return Err(WebSocketError::ProtocolError("Frame too short".to_string())); 295 | } 296 | 297 | let first_byte = buffer[0]; 298 | 299 | // This is not needed for this demo, but will need to implemented later on in 300 | // order to deal with fragmented message. If, for example we'd like to send 301 | // messages which exceed the buffer length defined in handle_connection, then 302 | // this will be needed to detect and act accordingly. 303 | // let fin = (first_byte & 0x80) != 0; 304 | 305 | let opcode = first_byte & 0x0F; // Determines opcode 306 | 307 | // Extract the mask 308 | let second_byte = buffer[1]; 309 | let masked = (second_byte & 0x80) != 0; 310 | 311 | // Determine payload length by getting the last 7 bits. If they are set 312 | // to 126, then it will include the next 16 bits, providing a maximum of 313 | // 65535 bytes. 314 | let mut payload_len = (second_byte & 0x7F) as usize; 315 | 316 | // If no masks exists, bail 317 | if masked == false { 318 | return Err(WebSocketError::ProtocolError( 319 | "Frames from client must be masked".to_string(), 320 | )); 321 | } 322 | 323 | // Set initially to 2 so that we skip over the first and second byte as 324 | // used above. 325 | let mut offset = 2; 326 | 327 | if payload_len == 126 { 328 | // If the payload has been noted as an extended payload, but the buffer 329 | // content is not long enough then throw an error. 330 | if buffer.len() < 4 { 331 | return Err(WebSocketError::ProtocolError( 332 | "Frame too short for extended payload length".to_string(), 333 | )); 334 | } 335 | 336 | // Get the current payload length and advance two bytes since the 337 | // length of the payload will be contained in those two bytes. 338 | payload_len = u16::from_be_bytes([buffer[offset], buffer[offset + 1]]) as usize; 339 | offset += 2; 340 | } else if payload_len == 127 { 341 | // We will ignore extra large payload lengths for now. This would be 342 | // payloads that are 2^64, or a size denoted by 4 bytes. 343 | return Err(WebSocketError::ProtocolError( 344 | "Extended payload length too large".to_string(), 345 | )); 346 | } 347 | 348 | // Given this, we have the initial two bytes + the offset from the 349 | // payload length and then finally the next 4 bytes are the masking key. 350 | // Here we check if payload length plus all that actually exists or not. 351 | // If the overall buffer is shorter then error out. 352 | if buffer.len() < offset + 4 + payload_len { 353 | return Err(WebSocketError::ProtocolError( 354 | "Frame too short for mask and data".to_string(), 355 | )); 356 | } 357 | 358 | // Extract the masking key 359 | let mask = &buffer[offset..offset + 4]; 360 | 361 | // Advance past the masking key and start on the data 362 | offset += 4; 363 | 364 | // Extract and apply the masking key via XOR 365 | let mut data = Vec::with_capacity(payload_len); 366 | for i in 0..payload_len { 367 | data.push(buffer[offset + i] ^ mask[i % 4]); 368 | } 369 | 370 | // Return the opcode and data if given 371 | Ok(match opcode { 372 | 0x01 => Frame::Text(data), // text frame 373 | 0x02 => Frame::Binary(data), // binary frame 374 | 0x08 => Frame::Close, // close frame 375 | 0x09 => Frame::Ping, // ping frame 376 | 0x0A => Frame::Pong, // pong frame 377 | _ => return Err(WebSocketError::ProtocolError("Unknown opcode".to_string())), 378 | }) 379 | } 380 | 381 | /// Sends a ping 382 | /// 383 | /// 0x89 is made of 0x80, indicating FIN bit set and it's the end of the 384 | /// message, as well as 0x09, which indicates it's a ping. The 0x00 is no 385 | /// data being sent. 386 | /// 387 | fn send_ping(&mut self) -> io::Result { 388 | println!("Ping sent"); 389 | self.stream.write(&[0x89, 0x00]) 390 | } 391 | 392 | /// Sends a pong 393 | /// 394 | /// 0x8A is made of 0x80, indicating FIN bit set and it's the end of the 395 | /// message, as well as 0x0A, which indicates it's a pong. The 0x00 is no 396 | /// data being sent. 397 | /// 398 | fn send_pong(&mut self) -> Result<(), WebSocketError> { 399 | println!("Pong sent"); 400 | self.stream.write(&[0x8A, 0x00])?; 401 | Ok(()) // Opcode for pong is 0xA and FIN set 402 | } 403 | 404 | /// Sends text 405 | /// 406 | /// Creates a frame and then sends through the current TcpStream. 407 | /// 408 | fn send_text(&mut self, data: &str) -> Result<(), WebSocketError> { 409 | let mut frame = Vec::new(); 410 | 411 | // FIN bit and code 0x01 for text data 412 | frame.push(0x81); 413 | 414 | let data_bytes = data.as_bytes(); 415 | let length = data_bytes.len(); 416 | 417 | // These sets payload length information within the initial bytes 418 | if length <= 125 { 419 | frame.push(length as u8); // Payload length fits in one byte 420 | } else if length <= 65535 { 421 | frame.push(126); // Signal that the next two bytes contain the payload length 422 | frame.extend_from_slice(&(length as u16).to_be_bytes()); 423 | } else { 424 | frame.push(127); // Signal that the next eight bytes contain the payload length 425 | frame.extend_from_slice(&(length as u64).to_be_bytes()); 426 | } 427 | 428 | // Append the data itself as bytes. 429 | frame.extend_from_slice(data_bytes); 430 | 431 | self.stream.write_all(&frame)?; 432 | self.stream.flush()?; 433 | Ok(()) 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /3_io_uring/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .env 4 | 5 | -------------------------------------------------------------------------------- /3_io_uring/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "basic_io_uring" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | 10 | 11 | -------------------------------------------------------------------------------- /3_io_uring/README.md: -------------------------------------------------------------------------------- 1 | You can find the article for this [here](https://www.thespatula.io/rust/rust_io_uring_bindings/). 2 | 3 | What we do in here is: 4 | 5 | 1. Create our own bindings to the io_uring C library. 6 | 2. Build a basic example of creating a queue and using it. 7 | -------------------------------------------------------------------------------- /3_io_uring/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use std::process::Command; 4 | 5 | fn main() { 6 | println!("cargo:rustc-link-search=native=/usr/lib"); 7 | println!("cargo:rustc-link-lib=dylib=uring"); 8 | 9 | println!("cargo:rerun-if-changed=wrapper.h"); 10 | 11 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 12 | let extern_c_path = env::temp_dir().join("bindgen").join("extern.c"); 13 | 14 | // Generate bindings using command-line bindgen 15 | let bindgen_output = Command::new("bindgen") 16 | .arg("--experimental") 17 | .arg("--wrap-static-fns") 18 | .arg("wrapper.h") 19 | .arg("--output") 20 | .arg(out_path.join("bindings.rs")) 21 | .output() 22 | .expect("Failed to generate bindings"); 23 | 24 | if !bindgen_output.status.success() { 25 | panic!( 26 | "Could not generate bindings:\n{}", 27 | String::from_utf8_lossy(&bindgen_output.stderr) 28 | ); 29 | } 30 | 31 | // Compile the generated wrappers (As per article) 32 | // let gcc_output = Command::new("gcc") 33 | // .arg("-c") 34 | // .arg("-fPIC") 35 | // .arg("-I/usr/include") 36 | // .arg("-I.") 37 | // .arg(&extern_c_path) 38 | // .arg("-o") 39 | // .arg(out_path.join("extern.o")) 40 | // .output() 41 | // .expect("Failed to compile C code"); 42 | 43 | // Updated to make use of LTO optimizations as per this link: 44 | // https://github.com/rust-lang/rust-bindgen/discussions/2405 45 | let gcc_output = Command::new("gcc") 46 | .arg("-c") 47 | .arg("-fPIC") 48 | .arg("-flto") // Enable LTO 49 | .arg("-O3") // Optimize for performance 50 | .arg("-I/usr/include") 51 | .arg("-I.") 52 | .arg(&extern_c_path) 53 | .arg("-o") 54 | .arg(out_path.join("extern.o")) 55 | .output() 56 | .expect("Failed to compile C code"); 57 | if !gcc_output.status.success() { 58 | panic!( 59 | "Failed to compile C code:\n{}", 60 | String::from_utf8_lossy(&gcc_output.stderr) 61 | ); 62 | } 63 | 64 | // Create a static library for the wrappers (As per article) 65 | // let ar_output = Command::new("ar") 66 | // .arg("crus") 67 | // .arg(out_path.join("libextern.a")) 68 | // .arg(out_path.join("extern.o")) 69 | // .output() 70 | // .expect("Failed to create static library"); 71 | 72 | // Update to follow through with LTO optimization changes 73 | let ar_output = Command::new("gcc-ar") 74 | .arg("crus") 75 | .arg(out_path.join("libextern.a")) 76 | .arg(out_path.join("extern.o")) 77 | .output() 78 | .expect("Failed to create static library"); 79 | 80 | if !ar_output.status.success() { 81 | panic!( 82 | "Failed to create static library:\n{}", 83 | String::from_utf8_lossy(&ar_output.stderr) 84 | ); 85 | } 86 | 87 | // Tell Cargo where to find the new library 88 | println!("cargo:rustc-link-search=native={}", out_path.display()); 89 | println!("cargo:rustc-link-lib=static=extern"); 90 | 91 | // Updated to enable LTO for Rust 92 | println!("cargo:rustc-link-arg=-flto"); 93 | } 94 | -------------------------------------------------------------------------------- /3_io_uring/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_upper_case_globals)] 2 | #![allow(non_camel_case_types)] 3 | #![allow(non_snake_case)] 4 | 5 | // This will appear as "inactive" in VSCode, but it's necessary for the 6 | // bindings. 7 | #[cfg(not(rust_analyzer))] 8 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 9 | 10 | use std::io; 11 | use std::mem::zeroed; 12 | use std::ptr::null_mut; 13 | 14 | /// Initializes an io_uring instance 15 | /// 16 | /// The queue_depth will be the size for the submission queue while the function 17 | /// will double that value for the completition queue. All other parameters are 18 | /// set to 0, and thus the default value. 19 | /// 20 | fn setup_io_uring(queue_depth: u32) -> io::Result { 21 | let mut ring: io_uring = unsafe { zeroed() }; 22 | let ret = unsafe { io_uring_queue_init(queue_depth, &mut ring, 0) }; 23 | if ret < 0 { 24 | return Err(io::Error::last_os_error()); 25 | } 26 | 27 | Ok(ring) 28 | } 29 | 30 | /// Submits a NOOP to the submission queue 31 | /// 32 | /// We get a pointer to the shared memory instance of an SQE, or submission 33 | /// queue entry. This is then loaded with some dummy data and submitted. 34 | /// 35 | fn submit_noop(ring: &mut io_uring) -> io::Result<()> { 36 | unsafe { 37 | let sqe = io_uring_get_sqe(ring); 38 | if sqe.is_null() { 39 | return Err(io::Error::new(io::ErrorKind::Other, "Failed to get SQE")); 40 | } 41 | 42 | io_uring_prep_nop(sqe); 43 | (*sqe).user_data = 0x88; 44 | 45 | let ret = io_uring_submit(ring); 46 | if ret < 0 { 47 | return Err(io::Error::last_os_error()); 48 | } 49 | } 50 | 51 | Ok(()) 52 | } 53 | 54 | /// Wait for our submission to complete 55 | /// 56 | /// We're blocking on the queue waiting for any thing finish. When we get one, 57 | /// we print out the details. 58 | /// 59 | fn wait_for_completion(ring: &mut io_uring) -> io::Result<()> { 60 | let mut cqe: *mut io_uring_cqe = null_mut(); 61 | let ret = unsafe { io_uring_wait_cqe(ring, &mut cqe) }; 62 | 63 | if ret < 0 { 64 | return Err(io::Error::last_os_error()); 65 | } 66 | 67 | unsafe { 68 | println!("NOP completed with result: {}", (*cqe).res); 69 | println!("User data: 0x{:x}", (*cqe).user_data); 70 | io_uring_cqe_seen(ring, cqe); 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | fn main() -> io::Result<()> { 77 | let queue_depth: u32 = 1; 78 | let mut ring = setup_io_uring(queue_depth)?; 79 | 80 | println!("Submitting NOP operation"); 81 | submit_noop(&mut ring)?; 82 | 83 | println!("Waiting for completion"); 84 | wait_for_completion(&mut ring)?; 85 | 86 | unsafe { io_uring_queue_exit(&mut ring) }; 87 | 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /3_io_uring/wrapper.h: -------------------------------------------------------------------------------- 1 | #include "/usr/include/liburing.h" 2 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .env 3 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "io_uring_tcp" 7 | version = "0.1.0" 8 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "io_uring_tcp" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::path::PathBuf; 3 | use std::process::Command; 4 | 5 | fn main() { 6 | println!("cargo:rustc-link-search=native=/usr/lib"); 7 | println!("cargo:rustc-link-lib=dylib=uring"); 8 | 9 | println!("cargo:rerun-if-changed=wrapper.h"); 10 | 11 | let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); 12 | let extern_c_path = env::temp_dir().join("bindgen").join("extern.c"); 13 | 14 | // Generate bindings using command-line bindgen 15 | let bindgen_output = Command::new("bindgen") 16 | .arg("--experimental") 17 | .arg("--wrap-static-fns") 18 | .arg("wrapper.h") 19 | .arg("--output") 20 | .arg(out_path.join("bindings.rs")) 21 | .output() 22 | .expect("Failed to generate bindings"); 23 | 24 | if !bindgen_output.status.success() { 25 | panic!( 26 | "Could not generate bindings:\n{}", 27 | String::from_utf8_lossy(&bindgen_output.stderr) 28 | ); 29 | } 30 | 31 | // Compile the generated wrappers 32 | let gcc_output = Command::new("gcc") 33 | .arg("-c") 34 | .arg("-fPIC") 35 | .arg("-I/usr/include") 36 | .arg("-I.") 37 | .arg(&extern_c_path) 38 | .arg("-o") 39 | .arg(out_path.join("extern.o")) 40 | .output() 41 | .expect("Failed to compile C code"); 42 | 43 | if !gcc_output.status.success() { 44 | panic!( 45 | "Failed to compile C code:\n{}", 46 | String::from_utf8_lossy(&gcc_output.stderr) 47 | ); 48 | } 49 | 50 | // Create a static library for the wrappers 51 | let ar_output = Command::new("ar") 52 | .arg("crus") 53 | .arg(out_path.join("libextern.a")) 54 | .arg(out_path.join("extern.o")) 55 | .output() 56 | .expect("Failed to create static library"); 57 | 58 | if !ar_output.status.success() { 59 | panic!( 60 | "Failed to create static library:\n{}", 61 | String::from_utf8_lossy(&ar_output.stderr) 62 | ); 63 | } 64 | 65 | // Tell Cargo where to find the new library 66 | println!("cargo:rustc-link-search=native={}", out_path.display()); 67 | println!("cargo:rustc-link-lib=static=extern"); 68 | } 69 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/echo_client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | def interactive_echo_client(host='localhost', port=8080): 4 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: 5 | s.connect((host, port)) 6 | print(f"Connected to {host}:{port}") 7 | print("Type your messages (press Ctrl+C to exit):") 8 | 9 | 10 | try: 11 | while True: 12 | message = input("> ") 13 | if not message: 14 | continue 15 | 16 | s.sendall(message.encode()) 17 | data = s.recv(1024) 18 | print(f"Received: {data.decode()}") 19 | 20 | except KeyboardInterrupt: 21 | print("\nDisconnecting from server...") 22 | 23 | except Exception as e: 24 | print(f"An error occurred: {e}") 25 | 26 | if __name__ == "__main__": 27 | interactive_echo_client() -------------------------------------------------------------------------------- /4_io_uring_echo_server/src/echo_server.rs: -------------------------------------------------------------------------------- 1 | /// Echo server 2 | /// 3 | /// This echo server is based on on bindings to the Linux liburing library (see 4 | /// build.rs). It will only work if the liburing library has been installed. 5 | /// 6 | use crate::bindings::*; 7 | use crate::iouring::IoUring; 8 | use std::collections::HashMap; 9 | use std::io; 10 | use std::net::TcpListener; 11 | use std::os::unix::io::{AsRawFd, RawFd}; 12 | use std::ptr; 13 | use std::time::Duration; 14 | 15 | const QUEUE_DEPTH: u32 = 256; 16 | const BUFFER_SIZE: usize = 1024; 17 | 18 | /// Operation types 19 | /// 20 | /// This defines the operation types we'll be using. This setup leaves it open 21 | /// to easily adding more. Note, both Receive and Send the location of the 22 | /// operation's associated buffer. 23 | /// 24 | enum Operation { 25 | Accept, 26 | Receive(*mut u8), 27 | Send(*mut u8), 28 | } 29 | 30 | /// Operation data 31 | /// 32 | /// This will be part of a key-value pair, as the value, which holds the 33 | /// operation information and the associated file descriptor of the socket. 34 | /// 35 | struct OperationData { 36 | op: Operation, 37 | fd: RawFd, 38 | } 39 | 40 | /// Echo serer 41 | /// 42 | /// Holds the ring, the primary TcpListener (this could alternatively be 43 | /// represented by a file descriptor, but this makes it easier). Lastly, we have 44 | /// our operations look up which uses a unique u64 id for each queue entry that 45 | /// is match to our operation data. 46 | /// 47 | pub struct EchoServer { 48 | ring: IoUring, 49 | listener: TcpListener, 50 | operations: HashMap, 51 | next_id: u64, 52 | } 53 | 54 | impl EchoServer { 55 | /// Create a new server instance 56 | /// 57 | /// This will create a non-blocking TcpListener and the io-uring queue. The 58 | /// fd_map will be used to track connections. 59 | /// 60 | pub fn new(port: u16) -> io::Result { 61 | let listener = TcpListener::bind(("0.0.0.0", port))?; 62 | listener.set_nonblocking(true)?; 63 | let ring = IoUring::new(QUEUE_DEPTH)?; 64 | 65 | Ok(Self { 66 | ring, 67 | listener, 68 | operations: HashMap::new(), 69 | next_id: 0, 70 | }) 71 | } 72 | 73 | /// Run the server 74 | /// 75 | /// When run, we first add the listener to the shared memory space, then we 76 | /// submit it to the queue, after which we start looping. The queue is 77 | /// peeked for completions which are then handled. 78 | /// 79 | /// The sleep is to keep us from hammering too hard. 80 | /// 81 | pub fn run(&mut self) -> io::Result<()> { 82 | self.add_accept()?; 83 | self.ring.submit()?; 84 | 85 | loop { 86 | match self.ring.peek_completion() { 87 | Some(cqe) => self.handle_completion(cqe)?, 88 | None => { 89 | self.ring.submit()?; 90 | std::thread::sleep(Duration::from_millis(1)); 91 | } 92 | } 93 | } 94 | } 95 | 96 | /// Accept connections 97 | /// 98 | /// We create an accept empty accept entry and then add the listener's file 99 | /// descriptor. We set the parameters for addr and addrlen to null since we 100 | /// don't care about the IP address for now. Later, we'll want to grab these. 101 | /// 102 | fn add_accept(&mut self) -> io::Result<()> { 103 | let user_data = self.generate_entry_id(Operation::Accept, self.listener.as_raw_fd()); 104 | self.ring.create_entry().set_accept( 105 | self.listener.as_raw_fd(), 106 | ptr::null_mut(), 107 | ptr::null_mut(), 108 | user_data, 109 | ); 110 | Ok(()) 111 | } 112 | 113 | /// Receive information 114 | /// 115 | /// We create a buffer to store the incoming information and a recv entry 116 | /// and then use the unique memory address of that buffer as the key in our 117 | /// hash with the value as the fd. 118 | /// 119 | fn add_receive(&mut self, fd: RawFd) -> io::Result<()> { 120 | let buffer = Box::into_raw(Box::new([0u8; BUFFER_SIZE])) as *mut u8; 121 | let user_data = self.generate_entry_id(Operation::Receive(buffer), fd); 122 | 123 | self.ring 124 | .create_entry() 125 | .set_receive(fd, buffer as *mut u8, BUFFER_SIZE, 0, user_data); 126 | 127 | Ok(()) 128 | } 129 | 130 | /// Send information 131 | /// 132 | /// When sending we create a unique id, which we'll store in the user_data 133 | /// portion of the iouring submission queue entry. That entry is created in 134 | /// the shared memory of the queue that exists between user and kernel 135 | /// space. 136 | /// 137 | fn add_send(&mut self, fd: RawFd, buffer: *mut u8, len: usize) -> io::Result<()> { 138 | let user_data = self.generate_entry_id(Operation::Send(buffer), fd); 139 | 140 | self.ring 141 | .create_entry() 142 | .set_send(fd, buffer as *const u8, len, 0, user_data); 143 | 144 | Ok(()) 145 | } 146 | 147 | /// Creates entry id 148 | /// 149 | /// This is needed because when we create an entry, say for reading from a 150 | /// user, we'll need to know the associated file descriptor (socket) to echo 151 | /// an answer to. It's a way to match submission and completition queue 152 | /// entries with the given file descriptor. 153 | /// 154 | fn generate_entry_id(&mut self, op: Operation, fd: RawFd) -> u64 { 155 | let user_data = self.next_id; 156 | self.next_id = self.next_id.wrapping_add(1); 157 | self.operations.insert(user_data, OperationData { op, fd }); 158 | user_data 159 | } 160 | 161 | /// Handles completed queue entries 162 | /// 163 | /// Grade the user_data from our completion queue entry (cqe) and then remove it 164 | /// from our operations hashmap. Each operation has a variant and associated file 165 | /// description AND possibly buffer (Receive/Send). We then pass those along to the 166 | /// respective handler. 167 | /// 168 | fn handle_completion(&mut self, cqe: io_uring_cqe) -> io::Result<()> { 169 | let user_data = cqe.user_data; 170 | let res = cqe.res; // This indicates the succces or failure or the operation. 171 | 172 | if let Some(op_data) = self.operations.remove(&user_data) { 173 | match op_data.op { 174 | Operation::Accept => self.handle_accept(res)?, 175 | Operation::Receive(buffer) => self.handle_receive(res, buffer, op_data.fd)?, 176 | Operation::Send(buffer) => self.handle_send(res, buffer, op_data.fd)?, 177 | } 178 | } 179 | 180 | Ok(()) 181 | } 182 | 183 | /// Handle Accept 184 | /// 185 | /// We check the result to see if a connection is being made, if so we queue 186 | /// of a receive. If result is negative, then queue may be full. No matter 187 | /// what happens we queue up another accept, which keeps us listening for 188 | /// more connections. 189 | /// 190 | fn handle_accept(&mut self, res: i32) -> io::Result<()> { 191 | if res >= 0 { 192 | println!("Accepted new connection: {}", res); 193 | self.add_receive(res)?; 194 | } else if res == -(EAGAIN as i32) { 195 | println!("No new connection available"); 196 | } else { 197 | eprintln!("Accept failed with error: {}", -res); 198 | } 199 | 200 | self.add_accept() 201 | } 202 | 203 | /// Handle receive 204 | /// 205 | /// If we get a successful receive we convert the buffer to a readable string, 206 | /// otherwise if we get 0 the connection is closed and we release the 207 | /// buffer. 208 | /// 209 | /// Releasing the buffer is a bit odd. We take it, wrap it in a box so that 210 | /// Rust will be able to clean it up after it does out of scope. We do this 211 | /// on connection closed or failure. 212 | /// 213 | fn handle_receive(&mut self, res: i32, buffer: *mut u8, fd: RawFd) -> io::Result<()> { 214 | if res > 0 { 215 | let slice = unsafe { std::slice::from_raw_parts(buffer, res as usize) }; 216 | let text = String::from_utf8_lossy(slice); 217 | println!("Read {} bytes: {}", res, text); 218 | 219 | self.add_send(fd, buffer, res as usize)?; 220 | } else if res == 0 { 221 | println!("Connection closed"); 222 | unsafe { 223 | let _ = Box::from_raw(buffer); 224 | } 225 | } else { 226 | eprintln!("Read failed with error: {}", -res); 227 | unsafe { 228 | let _ = Box::from_raw(buffer); 229 | } 230 | } 231 | 232 | Ok(()) 233 | } 234 | 235 | /// Handle send 236 | /// 237 | /// The information is sent and another receive is queued up. In all cases 238 | /// we release the buffer pointer. 239 | /// 240 | fn handle_send(&mut self, res: i32, buffer: *mut u8, fd: RawFd) -> io::Result<()> { 241 | if res >= 0 { 242 | println!("Send completed: {} bytes", res); 243 | self.add_receive(fd)?; 244 | } else { 245 | eprintln!("Write failed with error: {}", -res); 246 | } 247 | 248 | unsafe { 249 | let _ = Box::from_raw(buffer); 250 | } 251 | 252 | Ok(()) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/src/entry.rs: -------------------------------------------------------------------------------- 1 | /// Entry 2 | /// 3 | /// This defines iouring entries for the echo server 4 | use crate::bindings::*; 5 | use std::os::unix::io::RawFd; 6 | 7 | pub struct Entry<'a> { 8 | ring: &'a mut io_uring, 9 | } 10 | 11 | impl<'a> Entry<'a> { 12 | /// Create initial Entry 13 | /// 14 | /// We create an Entry with a reference to the io_uring instance. 15 | /// 16 | pub fn new(ring: &'a mut io_uring) -> Self { 17 | Entry { ring } 18 | } 19 | 20 | pub fn set_accept( 21 | &mut self, 22 | fd: RawFd, 23 | addr: *mut sockaddr, 24 | addrlen: *mut u32, 25 | user_data: u64, 26 | ) { 27 | let sqe = unsafe { io_uring_get_sqe(self.ring) }; 28 | if !sqe.is_null() { 29 | unsafe { 30 | io_uring_prep_accept(sqe, fd, addr, addrlen, 0); 31 | (*sqe).user_data = user_data; 32 | } 33 | } 34 | } 35 | 36 | pub fn set_receive(&mut self, fd: RawFd, buf: *mut u8, len: usize, flags: i32, user_data: u64) { 37 | let sqe = unsafe { io_uring_get_sqe(self.ring) }; 38 | if !sqe.is_null() { 39 | unsafe { 40 | io_uring_prep_recv(sqe, fd, buf as *mut _, len, flags); 41 | (*sqe).user_data = user_data; 42 | } 43 | } 44 | } 45 | 46 | pub fn set_send(&mut self, fd: RawFd, buf: *const u8, len: usize, flags: i32, user_data: u64) { 47 | let sqe = unsafe { io_uring_get_sqe(self.ring) }; 48 | if !sqe.is_null() { 49 | unsafe { 50 | io_uring_prep_send(sqe, fd, buf as *mut _, len, flags); 51 | (*sqe).user_data = user_data; 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/src/iouring.rs: -------------------------------------------------------------------------------- 1 | /// IoUring 2 | /// 3 | /// This crate sits between our IoUring instance and the bindings from liburing. 4 | /// It uses a limited subset of iouring's functionality. Just enough to get a basic 5 | /// echo server running. 6 | /// 7 | use crate::bindings::*; 8 | use crate::entry::Entry; 9 | use std::io; 10 | use std::mem::zeroed; 11 | use std::ptr; 12 | 13 | pub struct IoUring { 14 | ring: io_uring, 15 | } 16 | 17 | impl IoUring { 18 | /// Creates an io-uring instance 19 | /// 20 | /// We create a default (zeroed) out queue. The size of this queue is 21 | /// dependent on the version of the kernel you're using. 22 | /// 23 | pub fn new(entries: u32) -> io::Result { 24 | let mut ring: io_uring = unsafe { zeroed() }; 25 | let ret = unsafe { io_uring_queue_init(entries, &mut ring, 0) }; // This will return and -errno upon failure 26 | 27 | if ret < 0 { 28 | return Err(io::Error::from_raw_os_error(-ret)); 29 | } 30 | Ok(Self { ring }) 31 | } 32 | 33 | /// Create a new Entry 34 | pub fn create_entry(&mut self) -> Entry { 35 | Entry::new(&mut self.ring) 36 | } 37 | 38 | /// Submits the entries 39 | /// 40 | /// We can create multiple or a single entry before submitting. 41 | /// 42 | pub fn submit(&mut self) -> io::Result { 43 | let ret = unsafe { io_uring_submit(&mut self.ring) }; 44 | 45 | if ret < 0 { 46 | Err(io::Error::from_raw_os_error(-ret)) 47 | } else { 48 | Ok(ret as usize) 49 | } 50 | } 51 | 52 | /// Peeks the completion queue for completions 53 | /// 54 | /// This creates space for a completion queue entry (CQE), then attempt to 55 | /// fill it with a pointer to a completed entry. It either returns None or 56 | /// will read the entry based on the returned pointer to return and then 57 | /// register it as "seen" so that it can be cleaned up. 58 | /// 59 | pub fn peek_completion(&mut self) -> Option { 60 | let mut cqe: *mut io_uring_cqe = ptr::null_mut(); 61 | let ret = unsafe { io_uring_peek_cqe(&mut self.ring, &mut cqe) }; 62 | 63 | if ret < 0 || cqe.is_null() { 64 | None 65 | } else { 66 | let result = unsafe { ptr::read(cqe) }; 67 | unsafe { io_uring_cqe_seen(&mut self.ring, cqe) }; 68 | Some(result) 69 | } 70 | } 71 | } 72 | 73 | impl Drop for IoUring { 74 | fn drop(&mut self) { 75 | unsafe { io_uring_queue_exit(&mut self.ring) }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/src/main.rs: -------------------------------------------------------------------------------- 1 | #[allow(non_upper_case_globals)] 2 | #[allow(non_camel_case_types)] 3 | #[allow(non_snake_case)] 4 | #[allow(dead_code)] 5 | mod bindings { 6 | #[cfg(not(rust_analyzer))] 7 | include!(concat!(env!("OUT_DIR"), "/bindings.rs")); 8 | } 9 | mod echo_server; 10 | mod entry; 11 | mod iouring; 12 | 13 | use crate::echo_server::EchoServer; 14 | use std::io; 15 | 16 | fn main() -> io::Result<()> { 17 | let mut server = EchoServer::new(8080)?; 18 | println!("Echo server listening on port 8080"); 19 | server.run() 20 | } 21 | -------------------------------------------------------------------------------- /4_io_uring_echo_server/wrapper.h: -------------------------------------------------------------------------------- 1 | #include "/usr/include/liburing.h" 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a project where I'm creating game from scratch use Rust with a modicum of JavaScript. Each repo has its own README and links to a write up I've done on each section. 2 | --------------------------------------------------------------------------------