Files
docs-rust/ch21/ch21-01-single-threaded.html
2026-06-22 21:27:36 +05:30

599 lines
33 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Building a Single-Threaded Web Server</title>
</head>
<body>
<h2 id="building-a-single-threaded-web-server"><a class="header" href="#building-a-single-threaded-web-server">Building a Single-Threaded Web Server</a></h2>
<p>Well start by getting a single-threaded web server working. Before we begin,
lets look at a quick overview of the protocols involved in building web
servers. The details of these protocols are beyond the scope of this book, but
a brief overview will give you the information you need.</p>
<p>The two main protocols involved in web servers are <em>Hypertext Transfer
Protocol</em> <em>(HTTP)</em> and <em>Transmission Control Protocol</em> <em>(TCP)</em>. Both protocols
are <em>request-response</em> protocols, meaning a <em>client</em> initiates requests and a
<em>server</em> listens to the requests and provides a response to the client. The
contents of those requests and responses are defined by the protocols.</p>
<p>TCP is the lower-level protocol that describes the details of how information
gets from one server to another but doesnt specify what that information is.
HTTP builds on top of TCP by defining the contents of the requests and
responses. Its technically possible to use HTTP with other protocols, but in
the vast majority of cases, HTTP sends its data over TCP. Well work with the
raw bytes of TCP and HTTP requests and responses.</p>
<h3 id="listening-to-the-tcp-connection"><a class="header" href="#listening-to-the-tcp-connection">Listening to the TCP Connection</a></h3>
<p>Our web server needs to listen to a TCP connection, so thats the first part
well work on. The standard library offers a <code>std::net</code> module that lets us do
this. Lets make a new project in the usual fashion:</p>
<pre><code class="language-console">$ cargo new hello
Created binary (application) `hello` project
$ cd hello
</code></pre>
<p>Now enter the code in Listing 21-1 in <em>src/main.rs</em> to start. This code will
listen at the local address <code>127.0.0.1:7878</code> for incoming TCP streams. When it
gets an incoming stream, it will print <code>Connection established!</code>.</p>
<figure class="listing" id="listing-21-1">
<span class="file-name">Filename: src/main.rs</span>
<pre class="playground"><code class="language-rust no_run edition2024">use std::net::TcpListener;
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
println!("Connection established!");
}
}</code></pre>
<figcaption><a href="#listing-21-1">Listing 21-1</a>: Listening for incoming streams and printing a message when we receive a stream</figcaption>
</figure>
<p>Using <code>TcpListener</code>, we can listen for TCP connections at the address
<code>127.0.0.1:7878</code>. In the address, the section before the colon is an IP address
representing your computer (this is the same on every computer and doesnt
represent the authors computer specifically), and <code>7878</code> is the port. Weve
chosen this port for two reasons: HTTP isnt normally accepted on this port, so
our server is unlikely to conflict with any other web server you might have
running on your machine, and 7878 is <em>rust</em> typed on a telephone.</p>
<p>The <code>bind</code> function in this scenario works like the <code>new</code> function in that it
will return a new <code>TcpListener</code> instance. The function is called <code>bind</code>
because, in networking, connecting to a port to listen to is known as “binding
to a port.”</p>
<p>The <code>bind</code> function returns a <code>Result&lt;T, E&gt;</code>, which indicates that its
possible for binding to fail, for example, if we ran two instances of our
program and so had two programs listening to the same port. Because were
writing a basic server just for learning purposes, we wont worry about
handling these kinds of errors; instead, we use <code>unwrap</code> to stop the program if
errors happen.</p>
<p>The <code>incoming</code> method on <code>TcpListener</code> returns an iterator that gives us a
sequence of streams (more specifically, streams of type <code>TcpStream</code>). A single
<em>stream</em> represents an open connection between the client and the server.
<em>Connection</em> is the name for the full request and response process in which a
client connects to the server, the server generates a response, and the server
closes the connection. As such, we will read from the <code>TcpStream</code> to see what
the client sent and then write our response to the stream to send data back to
the client. Overall, this <code>for</code> loop will process each connection in turn and
produce a series of streams for us to handle.</p>
<p>For now, our handling of the stream consists of calling <code>unwrap</code> to terminate
our program if the stream has any errors; if there arent any errors, the
program prints a message. Well add more functionality for the success case in
the next listing. The reason we might receive errors from the <code>incoming</code> method
when a client connects to the server is that were not actually iterating over
connections. Instead, were iterating over <em>connection attempts</em>. The
connection might not be successful for a number of reasons, many of them
operating system specific. For example, many operating systems have a limit to
the number of simultaneous open connections they can support; new connection
attempts beyond that number will produce an error until some of the open
connections are closed.</p>
<p>Lets try running this code! Invoke <code>cargo run</code> in the terminal and then load
<em>127.0.0.1:7878</em> in a web browser. The browser should show an error message
like “Connection reset” because the server isnt currently sending back any
data. But when you look at your terminal, you should see several messages that
were printed when the browser connected to the server!</p>
<pre><code class="language-text"> Running `target/debug/hello`
Connection established!
Connection established!
Connection established!
</code></pre>
<p>Sometimes youll see multiple messages printed for one browser request; the
reason might be that the browser is making a request for the page as well as a
request for other resources, like the <em>favicon.ico</em> icon that appears in the
browser tab.</p>
<p>It could also be that the browser is trying to connect to the server multiple
times because the server isnt responding with any data. When <code>stream</code> goes out
of scope and is dropped at the end of the loop, the connection is closed as
part of the <code>drop</code> implementation. Browsers sometimes deal with closed
connections by retrying, because the problem might be temporary.</p>
<p>Browsers also sometimes open multiple connections to the server without sending
any requests so that if they <em>do</em> later send requests, those requests can
happen more quickly. When this occurs, our server will see each connection,
regardless of whether there are any requests over that connection. Many
versions of Chrome-based browsers do this, for example; you can disable that
optimization by using private browsing mode or using a different browser.</p>
<p>The important factor is that weve successfully gotten a handle to a TCP
connection!</p>
<p>Remember to stop the program by pressing <kbd>ctrl</kbd>-<kbd>C</kbd> when
youre done running a particular version of the code. Then, restart the program
by invoking the <code>cargo run</code> command after youve made each set of code changes
to make sure youre running the newest code.</p>
<h3 id="reading-the-request"><a class="header" href="#reading-the-request">Reading the Request</a></h3>
<p>Lets implement the functionality to read the request from the browser! To
separate the concerns of first getting a connection and then taking some action
with the connection, well start a new function for processing connections. In
this new <code>handle_connection</code> function, well read data from the TCP stream and
print it so that we can see the data being sent from the browser. Change the
code to look like Listing 21-2.</p>
<figure class="listing" id="listing-21-2">
<span class="file-name">Filename: src/main.rs</span>
<pre class="playground"><code class="language-rust no_run edition2024">use std::{
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
handle_connection(stream);
}
}
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&amp;stream);
let http_request: Vec&lt;_&gt; = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
println!("Request: {http_request:#?}");
}</code></pre>
<figcaption><a href="#listing-21-2">Listing 21-2</a>: Reading from the <code>TcpStream</code> and printing the data</figcaption>
</figure>
<p>We bring <code>std::io::BufReader</code> and <code>std::io::prelude</code> into scope to get access
to traits and types that let us read from and write to the stream. In the <code>for</code>
loop in the <code>main</code> function, instead of printing a message that says we made a
connection, we now call the new <code>handle_connection</code> function and pass the
<code>stream</code> to it.</p>
<p>In the <code>handle_connection</code> function, we create a new <code>BufReader</code> instance that
wraps a reference to the <code>stream</code>. The <code>BufReader</code> adds buffering by managing
calls to the <code>std::io::Read</code> trait methods for us.</p>
<p>We create a variable named <code>http_request</code> to collect the lines of the request
the browser sends to our server. We indicate that we want to collect these
lines in a vector by adding the <code>Vec&lt;_&gt;</code> type annotation.</p>
<p><code>BufReader</code> implements the <code>std::io::BufRead</code> trait, which provides the <code>lines</code>
method. The <code>lines</code> method returns an iterator of <code>Result&lt;String, std::io::Error&gt;</code> by splitting the stream of data whenever it sees a newline
byte. To get each <code>String</code>, we <code>map</code> and <code>unwrap</code> each <code>Result</code>. The <code>Result</code>
might be an error if the data isnt valid UTF-8 or if there was a problem
reading from the stream. Again, a production program should handle these errors
more gracefully, but were choosing to stop the program in the error case for
simplicity.</p>
<p>The browser signals the end of an HTTP request by sending two newline
characters in a row, so to get one request from the stream, we take lines until
we get a line that is the empty string. Once weve collected the lines into the
vector, were printing them out using pretty debug formatting so that we can
take a look at the instructions the web browser is sending to our server.</p>
<p>Lets try this code! Start the program and make a request in a web browser
again. Note that well still get an error page in the browser, but our
programs output in the terminal will now look similar to this:</p>
<!-- manual-regeneration
cd listings/ch21-web-server/listing-21-02
cargo run
make a request to 127.0.0.1:7878
Can't automate because the output depends on making requests
-->
<pre><code class="language-console">$ cargo run
Compiling hello v0.1.0 (file:///projects/hello)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/hello`
Request: [
"GET / HTTP/1.1",
"Host: 127.0.0.1:7878",
"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0",
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language: en-US,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"DNT: 1",
"Connection: keep-alive",
"Upgrade-Insecure-Requests: 1",
"Sec-Fetch-Dest: document",
"Sec-Fetch-Mode: navigate",
"Sec-Fetch-Site: none",
"Sec-Fetch-User: ?1",
"Cache-Control: max-age=0",
]
</code></pre>
<p>Depending on your browser, you might get slightly different output. Now that
were printing the request data, we can see why we get multiple connections
from one browser request by looking at the path after <code>GET</code> in the first line
of the request. If the repeated connections are all requesting <em>/</em>, we know the
browser is trying to fetch <em>/</em> repeatedly because its not getting a response
from our program.</p>
<p>Lets break down this request data to understand what the browser is asking of
our program.</p>
<!-- Old headings. Do not remove or links may break. -->
<p><a id="a-closer-look-at-an-http-request"></a>
<a id="looking-closer-at-an-http-request"></a></p>
<h3 id="looking-more-closely-at-an-http-request"><a class="header" href="#looking-more-closely-at-an-http-request">Looking More Closely at an HTTP Request</a></h3>
<p>HTTP is a text-based protocol, and a request takes this format:</p>
<pre><code class="language-text">Method Request-URI HTTP-Version CRLF
headers CRLF
message-body
</code></pre>
<p>The first line is the <em>request line</em> that holds information about what the
client is requesting. The first part of the request line indicates the method
being used, such as <code>GET</code> or <code>POST</code>, which describes how the client is making
this request. Our client used a <code>GET</code> request, which means it is asking for
information.</p>
<p>The next part of the request line is <em>/</em>, which indicates the <em>uniform resource
identifier</em> <em>(URI)</em> the client is requesting: A URI is almost, but not quite,
the same as a <em>uniform resource locator</em> <em>(URL)</em>. The difference between URIs
and URLs isnt important for our purposes in this chapter, but the HTTP spec
uses the term <em>URI</em>, so we can just mentally substitute <em>URL</em> for <em>URI</em> here.</p>
<p>The last part is the HTTP version the client uses, and then the request line
ends in a CRLF sequence. (<em>CRLF</em> stands for <em>carriage return</em> and <em>line feed</em>,
which are terms from the typewriter days!) The CRLF sequence can also be
written as <code>\r\n</code>, where <code>\r</code> is a carriage return and <code>\n</code> is a line feed. The
<em>CRLF sequence</em> separates the request line from the rest of the request data.
Note that when the CRLF is printed, we see a new line start rather than <code>\r\n</code>.</p>
<p>Looking at the request line data we received from running our program so far,
we see that <code>GET</code> is the method, <em>/</em> is the request URI, and <code>HTTP/1.1</code> is the
version.</p>
<p>After the request line, the remaining lines starting from <code>Host:</code> onward are
headers. <code>GET</code> requests have no body.</p>
<p>Try making a request from a different browser or asking for a different
address, such as <em>127.0.0.1:7878/test</em>, to see how the request data changes.</p>
<p>Now that we know what the browser is asking for, lets send back some data!</p>
<h3 id="writing-a-response"><a class="header" href="#writing-a-response">Writing a Response</a></h3>
<p>Were going to implement sending data in response to a client request.
Responses have the following format:</p>
<pre><code class="language-text">HTTP-Version Status-Code Reason-Phrase CRLF
headers CRLF
message-body
</code></pre>
<p>The first line is a <em>status line</em> that contains the HTTP version used in the
response, a numeric status code that summarizes the result of the request, and
a reason phrase that provides a text description of the status code. After the
CRLF sequence are any headers, another CRLF sequence, and the body of the
response.</p>
<p>Here is an example response that uses HTTP version 1.1 and has a status code of
200, an OK reason phrase, no headers, and no body:</p>
<pre><code class="language-text">HTTP/1.1 200 OK\r\n\r\n
</code></pre>
<p>The status code 200 is the standard success response. The text is a tiny
successful HTTP response. Lets write this to the stream as our response to a
successful request! From the <code>handle_connection</code> function, remove the
<code>println!</code> that was printing the request data and replace it with the code in
Listing 21-3.</p>
<figure class="listing" id="listing-21-3">
<span class="file-name">Filename: src/main.rs</span>
<pre class="playground"><code class="language-rust no_run edition2024"><span class="boring">use std::{
</span><span class="boring"> io::{BufReader, prelude::*},
</span><span class="boring"> net::{TcpListener, TcpStream},
</span><span class="boring">};
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
</span><span class="boring">
</span><span class="boring"> for stream in listener.incoming() {
</span><span class="boring"> let stream = stream.unwrap();
</span><span class="boring">
</span><span class="boring"> handle_connection(stream);
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span>fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&amp;stream);
let http_request: Vec&lt;_&gt; = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let response = "HTTP/1.1 200 OK\r\n\r\n";
stream.write_all(response.as_bytes()).unwrap();
}</code></pre>
<figcaption><a href="#listing-21-3">Listing 21-3</a>: Writing a tiny successful HTTP response to the stream</figcaption>
</figure>
<p>The first new line defines the <code>response</code> variable that holds the success
messages data. Then, we call <code>as_bytes</code> on our <code>response</code> to convert the
string data to bytes. The <code>write_all</code> method on <code>stream</code> takes a <code>&amp;[u8]</code> and
sends those bytes directly down the connection. Because the <code>write_all</code>
operation could fail, we use <code>unwrap</code> on any error result as before. Again, in
a real application, you would add error handling here.</p>
<p>With these changes, lets run our code and make a request. Were no longer
printing any data to the terminal, so we wont see any output other than the
output from Cargo. When you load <em>127.0.0.1:7878</em> in a web browser, you should
get a blank page instead of an error. Youve just handcoded receiving an HTTP
request and sending a response!</p>
<h3 id="returning-real-html"><a class="header" href="#returning-real-html">Returning Real HTML</a></h3>
<p>Lets implement the functionality for returning more than a blank page. Create
the new file <em>hello.html</em> in the root of your project directory, not in the
<em>src</em> directory. You can input any HTML you want; Listing 21-4 shows one
possibility.</p>
<figure class="listing" id="listing-21-4">
<span class="file-name">Filename: hello.html</span>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;title&gt;Hello!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Hello!&lt;/h1&gt;
&lt;p&gt;Hi from Rust&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<figcaption><a href="#listing-21-4">Listing 21-4</a>: A sample HTML file to return in a response</figcaption>
</figure>
<p>This is a minimal HTML5 document with a heading and some text. To return this
from the server when a request is received, well modify <code>handle_connection</code> as
shown in Listing 21-5 to read the HTML file, add it to the response as a body,
and send it.</p>
<figure class="listing" id="listing-21-5">
<span class="file-name">Filename: src/main.rs</span>
<pre class="playground"><code class="language-rust no_run edition2024">use std::{
fs,
io::{BufReader, prelude::*},
net::{TcpListener, TcpStream},
};
// --snip--
<span class="boring">fn main() {
</span><span class="boring"> let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
</span><span class="boring">
</span><span class="boring"> for stream in listener.incoming() {
</span><span class="boring"> let stream = stream.unwrap();
</span><span class="boring">
</span><span class="boring"> handle_connection(stream);
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span>fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&amp;stream);
let http_request: Vec&lt;_&gt; = buf_reader
.lines()
.map(|result| result.unwrap())
.take_while(|line| !line.is_empty())
.collect();
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}</code></pre>
<figcaption><a href="#listing-21-5">Listing 21-5</a>: Sending the contents of <em>hello.html</em> as the body of the response</figcaption>
</figure>
<p>Weve added <code>fs</code> to the <code>use</code> statement to bring the standard librarys
filesystem module into scope. The code for reading the contents of a file to a
string should look familiar; we used it when we read the contents of a file for
our I/O project in Listing 12-4.</p>
<p>Next, we use <code>format!</code> to add the files contents as the body of the success
response. To ensure a valid HTTP response, we add the <code>Content-Length</code> header,
which is set to the size of our response body—in this case, the size of
<code>hello.html</code>.</p>
<p>Run this code with <code>cargo run</code> and load <em>127.0.0.1:7878</em> in your browser; you
should see your HTML rendered!</p>
<p>Currently, were ignoring the request data in <code>http_request</code> and just sending
back the contents of the HTML file unconditionally. That means if you try
requesting <em>127.0.0.1:7878/something-else</em> in your browser, youll still get
back this same HTML response. At the moment, our server is very limited and
does not do what most web servers do. We want to customize our responses
depending on the request and only send back the HTML file for a well-formed
request to <em>/</em>.</p>
<h3 id="validating-the-request-and-selectively-responding"><a class="header" href="#validating-the-request-and-selectively-responding">Validating the Request and Selectively Responding</a></h3>
<p>Right now, our web server will return the HTML in the file no matter what the
client requested. Lets add functionality to check that the browser is
requesting <em>/</em> before returning the HTML file and to return an error if the
browser requests anything else. For this we need to modify <code>handle_connection</code>,
as shown in Listing 21-6. This new code checks the content of the request
received against what we know a request for <em>/</em> looks like and adds <code>if</code> and
<code>else</code> blocks to treat requests differently.</p>
<figure class="listing" id="listing-21-6">
<span class="file-name">Filename: src/main.rs</span>
<pre class="playground"><code class="language-rust no_run edition2024"><span class="boring">use std::{
</span><span class="boring"> fs,
</span><span class="boring"> io::{BufReader, prelude::*},
</span><span class="boring"> net::{TcpListener, TcpStream},
</span><span class="boring">};
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
</span><span class="boring">
</span><span class="boring"> for stream in listener.incoming() {
</span><span class="boring"> let stream = stream.unwrap();
</span><span class="boring">
</span><span class="boring"> handle_connection(stream);
</span><span class="boring"> }
</span><span class="boring">}
</span>// --snip--
fn handle_connection(mut stream: TcpStream) {
let buf_reader = BufReader::new(&amp;stream);
let request_line = buf_reader.lines().next().unwrap().unwrap();
if request_line == "GET / HTTP/1.1" {
let status_line = "HTTP/1.1 200 OK";
let contents = fs::read_to_string("hello.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
} else {
// some other request
}
}</code></pre>
<figcaption><a href="#listing-21-6">Listing 21-6</a>: Handling requests to <em>/</em> differently from other requests</figcaption>
</figure>
<p>Were only going to be looking at the first line of the HTTP request, so rather
than reading the entire request into a vector, were calling <code>next</code> to get the
first item from the iterator. The first <code>unwrap</code> takes care of the <code>Option</code> and
stops the program if the iterator has no items. The second <code>unwrap</code> handles the
<code>Result</code> and has the same effect as the <code>unwrap</code> that was in the <code>map</code> added in
Listing 21-2.</p>
<p>Next, we check the <code>request_line</code> to see if it equals the request line of a GET
request to the <em>/</em> path. If it does, the <code>if</code> block returns the contents of our
HTML file.</p>
<p>If the <code>request_line</code> does <em>not</em> equal the GET request to the <em>/</em> path, it
means weve received some other request. Well add code to the <code>else</code> block in
a moment to respond to all other requests.</p>
<p>Run this code now and request <em>127.0.0.1:7878</em>; you should get the HTML in
<em>hello.html</em>. If you make any other request, such as
<em>127.0.0.1:7878/something-else</em>, youll get a connection error like those you
saw when running the code in Listing 21-1 and Listing 21-2.</p>
<p>Now lets add the code in Listing 21-7 to the <code>else</code> block to return a response
with the status code 404, which signals that the content for the request was
not found. Well also return some HTML for a page to render in the browser
indicating the response to the end user.</p>
<figure class="listing" id="listing-21-7">
<span class="file-name">Filename: src/main.rs</span>
<pre class="playground"><code class="language-rust no_run edition2024"><span class="boring">use std::{
</span><span class="boring"> fs,
</span><span class="boring"> io::{BufReader, prelude::*},
</span><span class="boring"> net::{TcpListener, TcpStream},
</span><span class="boring">};
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
</span><span class="boring">
</span><span class="boring"> for stream in listener.incoming() {
</span><span class="boring"> let stream = stream.unwrap();
</span><span class="boring">
</span><span class="boring"> handle_connection(stream);
</span><span class="boring"> }
</span><span class="boring">}
</span><span class="boring">
</span><span class="boring">fn handle_connection(mut stream: TcpStream) {
</span><span class="boring"> let buf_reader = BufReader::new(&amp;stream);
</span><span class="boring"> let request_line = buf_reader.lines().next().unwrap().unwrap();
</span><span class="boring">
</span><span class="boring"> if request_line == "GET / HTTP/1.1" {
</span><span class="boring"> let status_line = "HTTP/1.1 200 OK";
</span><span class="boring"> let contents = fs::read_to_string("hello.html").unwrap();
</span><span class="boring"> let length = contents.len();
</span><span class="boring">
</span><span class="boring"> let response = format!(
</span><span class="boring"> "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
</span><span class="boring"> );
</span><span class="boring">
</span><span class="boring"> stream.write_all(response.as_bytes()).unwrap();
</span> // --snip--
} else {
let status_line = "HTTP/1.1 404 NOT FOUND";
let contents = fs::read_to_string("404.html").unwrap();
let length = contents.len();
let response = format!(
"{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
);
stream.write_all(response.as_bytes()).unwrap();
}
<span class="boring">}</span></code></pre>
<figcaption><a href="#listing-21-7">Listing 21-7</a>: Responding with status code 404 and an error page if anything other than <em>/</em> was requested</figcaption>
</figure>
<p>Here, our response has a status line with status code 404 and the reason phrase
<code>NOT FOUND</code>. The body of the response will be the HTML in the file <em>404.html</em>.
Youll need to create a <em>404.html</em> file next to <em>hello.html</em> for the error
page; again, feel free to use any HTML you want, or use the example HTML in
Listing 21-8.</p>
<figure class="listing" id="listing-21-8">
<span class="file-name">Filename: 404.html</span>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
&lt;meta charset="utf-8"&gt;
&lt;title&gt;Hello!&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Oops!&lt;/h1&gt;
&lt;p&gt;Sorry, I don't know what you're asking for.&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<figcaption><a href="#listing-21-8">Listing 21-8</a>: Sample content for the page to send back with any 404 response</figcaption>
</figure>
<p>With these changes, run your server again. Requesting <em>127.0.0.1:7878</em> should
return the contents of <em>hello.html</em>, and any other request, like
<em>127.0.0.1:7878/foo</em>, should return the error HTML from <em>404.html</em>.</p>
<!-- Old headings. Do not remove or links may break. -->
<p><a id="a-touch-of-refactoring"></a></p>
<h3 id="refactoring"><a class="header" href="#refactoring">Refactoring</a></h3>
<p>At the moment, the <code>if</code> and <code>else</code> blocks have a lot of repetition: Theyre
both reading files and writing the contents of the files to the stream. The
only differences are the status line and the filename. Lets make the code more
concise by pulling out those differences into separate <code>if</code> and <code>else</code> lines
that will assign the values of the status line and the filename to variables;
we can then use those variables unconditionally in the code to read the file
and write the response. Listing 21-9 shows the resultant code after replacing
the large <code>if</code> and <code>else</code> blocks.</p>
<figure class="listing" id="listing-21-9">
<span class="file-name">Filename: src/main.rs</span>
<pre class="playground"><code class="language-rust no_run edition2024"><span class="boring">use std::{
</span><span class="boring"> fs,
</span><span class="boring"> io::{BufReader, prelude::*},
</span><span class="boring"> net::{TcpListener, TcpStream},
</span><span class="boring">};
</span><span class="boring">
</span><span class="boring">fn main() {
</span><span class="boring"> let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
</span><span class="boring">
</span><span class="boring"> for stream in listener.incoming() {
</span><span class="boring"> let stream = stream.unwrap();
</span><span class="boring">
</span><span class="boring"> handle_connection(stream);
</span><span class="boring"> }
</span><span class="boring">}
</span>// --snip--
fn handle_connection(mut stream: TcpStream) {
// --snip--
<span class="boring"> let buf_reader = BufReader::new(&amp;stream);
</span><span class="boring"> let request_line = buf_reader.lines().next().unwrap().unwrap();
</span>
let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
("HTTP/1.1 200 OK", "hello.html")
} else {
("HTTP/1.1 404 NOT FOUND", "404.html")
};
let contents = fs::read_to_string(filename).unwrap();
let length = contents.len();
let response =
format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
stream.write_all(response.as_bytes()).unwrap();
}</code></pre>
<figcaption><a href="#listing-21-9">Listing 21-9</a>: Refactoring the <code>if</code> and <code>else</code> blocks to contain only the code that differs between the two cases</figcaption>
</figure>
<p>Now the <code>if</code> and <code>else</code> blocks only return the appropriate values for the
status line and filename in a tuple; we then use destructuring to assign these
two values to <code>status_line</code> and <code>filename</code> using a pattern in the <code>let</code>
statement, as discussed in Chapter 19.</p>
<p>The previously duplicated code is now outside the <code>if</code> and <code>else</code> blocks and
uses the <code>status_line</code> and <code>filename</code> variables. This makes it easier to see
the difference between the two cases, and it means we have only one place to
update the code if we want to change how the file reading and response writing
work. The behavior of the code in Listing 21-9 will be the same as that in
Listing 21-7.</p>
<p>Awesome! We now have a simple web server in approximately 40 lines of Rust code
that responds to one request with a page of content and responds to all other
requests with a 404 response.</p>
<p>Currently, our server runs in a single thread, meaning it can only serve one
request at a time. Lets examine how that can be a problem by simulating some
slow requests. Then, well fix it so that our server can handle multiple
requests at once.</p>
</body>
</html>