The HTTP protocol is the cornerstone of the modern web, yet many developers use it daily without a deep understanding of its underlying details. This article will guide you through implementing a simple HTTP server from scratch using the Rust programming language, allowing you to “tear apart” the mysteries of the HTTP protocol.
The Core Components of the HTTP Protocol
The communication in the HTTP protocol mainly consists of two parts: requests and responses. We will first abstract these two core concepts through data structures.
The Data Structure of an HTTP Request
An HTTP request consists of three parts: the request line, request headers, and the request body. The corresponding data structure is designed as follows:
#[derive(Debug, Clone, PartialEq)]
pub struct HttpRequest {
pub method: String, // Request method (GET/POST, etc.)
pub path: String, // Request path
pub version: String, // HTTP version
pub headers: Vec<String>, // Request headers
pub body: String, // Request body
}
Parsing the HTTP Request
Parsing the HTTP request from the raw TCP stream is a key step. We need to read the data line by line, first parsing the request line, then reading the request headers, and finally processing the request body:
impl HttpRequest {
fn new(request: &str) -> Self {
let mut lines = request.lines();
// Parse the request line (format: METHOD PATH VERSION)
let req_line = lines.next().unwrap();
// Parse the request headers (until an empty line)
let headers = lines
.clone()
.take_while(|line| !line.is_empty())
.map(|line| line.to_string())
.collect();
// Parse the request body (remaining content)
let body = lines
.map(|line| line.to_string())
.collect::<Vec<String>>()
.join("\n");
// Extract method, path, and version from the request line
let method = req_line.split(' ').nth(0).unwrap().to_string();
let path = req_line.split(' ').nth(1).unwrap().to_string();
let version = req_line.split(' ').nth(2).unwrap().to_string();
Self {
method,
path,
version,
headers,
body,
}
}
}
Implementing the HTTP Response
Corresponding to the request, the HTTP response also contains a status line, response headers, and a response body:
The Data Structure of the Response
#[derive(Debug, Clone, PartialEq)]
pub struct HttpResponse {
pub version: String, // HTTP version
pub status: u16, // Status code
pub reason: String, // Status description
pub headers: Vec<String>, // Response headers
pub body: String, // Response body
}
Formatting the Response Output
The HTTP response needs to be converted to a string in a specific format before being sent over TCP:
impl Display for HttpResponse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_'>) -> std::fmt::Result {
write!(
f,
"{} {} {}\r\n{}\r\n{}",
self.version,
self.status,
self.reason,
self.headers.join("\r\n"),
self.body
)
}
}
Note the <span>\r\n</span> delimiter in the format, which is the line break method specified by the HTTP protocol; it cannot simply be replaced with <span>\n</span>.
Implementing the TCP Server
With the request and response handling in place, we also need a TCP server to listen on a port, receive requests, and return responses:
fn main() {
// Bind to local port 6167
let listener = TcpListener::bind("127.0.0.1:6167").unwrap();
println!("server listening on port 6167");
// Loop to accept connections
for stream in listener.incoming() {
let mut stream = stream.unwrap();
let mut buffer = [0; 1024]; // Buffer
// Read request data
let len = stream.read(&mut buffer).unwrap();
// Convert to string and parse into HttpRequest
let request: HttpRequest = String::from_utf8_lossy(&buffer[..len]).to_string().into();
println!("request: {:?}", request);
// Handle specific path requests
if request.path.eq("/hello") {
let html_content = read_html(); // Read HTML file content
// Build response
let response: HttpResponse = HttpResponse {
version: "HTTP/1.1".to_string(),
status: 200,
reason: "OK".to_string(),
headers: vec![
"Content-Type: text/html; charset=utf-8".to_string(),
format!("Content-Length: {}", html_content.len()),
],
body: html_content,
};
// Send response
stream.write(response.to_string().as_bytes()).unwrap();
stream.flush().unwrap();
}
}
}
The content of the read index.html file
<html>
<head>
<title>Rust Service</title>
</head>
<body>
<h1>Welcome to Rust Service</h1>
<p>This is a simple Rust service.</p>
</body>
</html>
Request effect demonstration:
Summary of Core Principles
Through this simple implementation, we can clearly see the workflow of the HTTP protocol:
- The client sends a request text in HTTP format via a TCP connection
- The server parses the request text and extracts key information
- The server processes the request information (in this case, reading the HTML file in the current directory)
- The server constructs a response text in HTTP format
- The server returns the response text via the TCP connection
Currently, mainstream services like Tomcat and Nginx are built upon more refined implementations of this code, primarily focusing on the request and response handlers, allowing us to concentrate on the main logic during coding.
By implementing this simple server yourself, you will gain a deeper understanding of the HTTP protocol, moving beyond surface-level knowledge to the essence of the protocol’s text format and interaction. This is the significance of “tearing apart” the protocol—seeing the essence through abstraction.