监听TCP连接

http请求

  1. use std::{
  2. io::{prelude::*, BufReader},
  3. net::{TcpListener, TcpStream},
  4. };
  5. fn main() {
  6. // 监听地址: 127.0.0.1:7878
  7. let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
  8. // 建立连接
  9. for stream in listener.incoming() {
  10. let stream = stream.unwrap();
  11. handle_connection(stream);
  12. }
  13. }
  14. fn handle_connection(mut stream: TcpStream) {
  15. let buf_reader = BufReader::new(&mut stream);
  16. let http_request: Vec<_> = buf_reader
  17. .lines()
  18. .map(|result| result.unwrap())
  19. .take_while(|line| !line.is_empty())
  20. .collect();
  21. // let response = "HTTP/1.1 200 OK\r\n\r\n";
  22. // stream.write_all(response.as_bytes()).unwrap();
  23. println!("Request: {:#?}", http_request);
  24. }

只有请求,没有响应,页面报错如下:

单线程版本 - 图1

http请求长啥样

  1. Request: [
  2. "GET / HTTP/1.1",
  3. "Host: 127.0.0.1:7878",
  4. "Connection: keep-alive",
  5. "Cache-Control: max-age=0",
  6. "sec-ch-ua: \"Not A(Brand\";v=\"99\", \"Google Chrome\";v=\"121\", \"Chromium\";v=\"121\"",
  7. "sec-ch-ua-mobile: ?0",
  8. "sec-ch-ua-platform: \"macOS\"",
  9. "Upgrade-Insecure-Requests: 1",
  10. "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
  11. "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
  12. "Sec-Fetch-Site: none",
  13. "Sec-Fetch-Mode: navigate",
  14. "Sec-Fetch-User: ?1",
  15. "Sec-Fetch-Dest: document",
  16. "Accept-Encoding: gzip, deflate, br",
  17. "Accept-Language: zh-CN,zh;q=0.9,en;q=0.8",
  18. ]

也就是以下格式

Method Request-URI HTTP-Version headers CRLF message-body

  • 第一行 Method 是请求的方法,例如 GET、POST 等,Request-URI 是该请求希望访问的目标资源路径,例如 /、/hello/world 等
  • 类似 JSON 格式的数据都是 HTTP 请求报头 headers,例如 “Host: 127.0.0.1:7878”
  • 至于 message-body 是消息体, 它包含了用户请求携带的具体数据,例如更改用户名的请求,就要提交新的用户名数据,至于刚才的 GET 请求,它是没有 message-body 的

请求应答

放开http请求的如下这两行代码

单线程版本 - 图2

重新启动服务器,然后再观察下浏览器中的输出,这次应该不再有报错,而是一个空白页面,因为没有返回任何具体的数据( message-body ),上面只是一条最简单的符合 HTTP 格式的数据。

单线程版本 - 图3

http响应长啥样

应答的格式与请求相差不大,其中 Status-Code 是最重要的,它用于告诉客户端,当前的请求是否成功,若失败,大概是什么原因,它就是著名的 HTTP 状态码,常用的有 200: 请求成功,404 目标不存在,等等。
  1. HTTP-Version Status-Code Reason-Phrase CRLF
  2. headers CRLF
  3. message-body
为了帮助大家更直观的感受下应答格式第一行长什么样,下面给出一个示例:
  1. HTTP/1.1 200 OK\r\n\r\n

返回html页面

空白页面显然会让人不知所措,那就返回一个简单的 HTML 页面,给用户打给招呼。
1.在项目的根目录下创建 hello.html 文件并写入如下内容:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>Hello!</title>
  6. </head>
  7. <body>
  8. <h1>Hello!</h1>
  9. <p>Hi from Rust</p>
  10. </body>
  11. </html>

2.读取HTML 的内容,并按照 HTTP 格式,将内容传回给客户端。

单线程版本 - 图4

  1. let status_line = "HTTP/1.1 200 OK";
  2. let contents = fs::read_to_string("hello.html").unwrap();
  3. let length = contents.len();
  4. let response =
  5. format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

重新启动服务器,页面响应如下:

单线程版本 - 图5

验证请求和选择性应答

针对用户的不同请求给出相应的不同回复,让场景模拟更加真实。以下代码判断了用户是否请求了 / 根路径,如果是,返回之前的 hello.html 页面;如果不是…尚未实现。重新运行服务器,如果你继续访问 127.0.0.1:7878 ,那么看到的依然是 hello.html 页面,因为默认访问根路径,但是一旦换一个路径访问,例如 127.0.0.1:7878/something-else,那你将继续看到之前看过多次的连接错误
  1. fn handle_connection(mut stream: TcpStream) {
  2. let buf_reader = BufReader::new(&mut stream);
  3. let request_line = buf_reader.lines().next().unwrap().unwrap();
  4. if request_line == "GET / HTTP/1.1" {
  5. let status_line = "HTTP/1.1 200 OK";
  6. let contents = fs::read_to_string("hello.html").unwrap();
  7. let length = contents.len();
  8. let response = format!(
  9. "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
  10. );
  11. stream.write_all(response.as_bytes()).unwrap();
  12. } else {
  13. // some other request
  14. }
  15. }

访问根路径 请求其他路径

下面来完善下,当用户访问根路径之外的页面时,给他展示一个友好的 404 页面( 相比直接报错 )。

访问根路径之外的页面,展示404 页面

  1. 在根路径下创建 404.html并填入下面内容:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>404</title>
  6. </head>
  7. <body>
  8. <h1>很抱歉!</h1>
  9. <p>由于运维删库跑路,我们的数据全部丢失,总监也已经准备跑路,88</p>
  10. </body>
  11. </html>

2.完善rust文件

  1. // --snip--
  2. } else {
  3. let status_line = "HTTP/1.1 404 NOT FOUND";
  4. let contents = fs::read_to_string("404.html").unwrap();
  5. let length = contents.len();
  6. let response = format!(
  7. "{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}"
  8. );
  9. stream.write_all(response.as_bytes()).unwrap();
  10. }

重启服务器,效果如下:

单线程版本 - 图8

最后,上面的代码其实有很多重复,可以提取出来进行简单重构:

单线程版本 - 图9

完整rust代码如下:

  1. use std::{
  2. fs,
  3. io::{prelude::*, BufReader},
  4. net::{TcpListener, TcpStream},
  5. };
  6. fn main() {
  7. // 监听地址: 127.0.0.1:7878
  8. let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
  9. // 建立连接
  10. for stream in listener.incoming() {
  11. let stream = stream.unwrap();
  12. handle_connection(stream);
  13. }
  14. }
  15. fn handle_connection(mut stream: TcpStream) {
  16. let buf_reader = BufReader::new(&mut stream);
  17. let request_line = buf_reader.lines().next().unwrap().unwrap();
  18. let (status_line, filename) = if request_line == "GET / HTTP/1.1" {
  19. ("HTTP/1.1 200 OK", "hello.html")
  20. } else {
  21. ("HTTP/1.1 404 NOT FOUND", "404.html")
  22. };
  23. let contents = fs::read_to_string(filename).unwrap();
  24. let length = contents.len();
  25. let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");
  26. stream.write_all(response.as_bytes()).unwrap();
  27. }
至此,单线程版本的服务器已经完成,但是存在请求排队的糟糕事实。