You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Tokio的自定义HTTP服务器:浏览器无法接收响应问题排查

问题:Tokio实现的HTTP服务器无法向浏览器返回响应

我使用Tokio框架实现了一个简易HTTP服务器,服务器可以接收到浏览器的请求,但浏览器却无法收到服务器的响应。

依赖配置:

# Cargo.toml
[dependencies]
tokio = { version = "1", features = ["full"] }

实现代码:

use tokio::{self, net};
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::TcpStream;

async fn process(mut stream: TcpStream) {
    let mut buf = [0u8; 4096];
    loop {
        let n = stream.read(&mut buf).await.unwrap();
        if n == 0 {
            break;
        }
        let s = String::from_utf8_lossy(&buf[0..n]).to_string();
        print!("{}", s);
        if s.ends_with("\r\n\r\n") {
            println!("request received");
            break;
        }
    }

    let response_str = "HTTP/1.1 200 OK
<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"udf-8\">
<title>Hello!</title>
</head>
<body>
HELLO
</body>
</html>
        ";
    println!("response");
    // browser doesn't receive the response that this line of code send.
    stream.write(response_str.as_bytes()).await.unwrap();
    stream.flush().await.unwrap();
    println!("response DONE");
}

#[tokio::main]
async fn main() {
    let listener = net::TcpListener::bind("127.0.0.1:9998").await.unwrap();
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        tokio::spawn(async move{
            process(stream).await;
        });
    }
}

请问我的代码中存在什么bug吗?


问题分析与修复方案

你的代码存在两个核心问题,导致浏览器无法接收响应:

1. HTTP响应格式不符合规范

HTTP协议明确要求:

  • 响应头与响应体之间必须用\r\n\r\n分隔
  • 必须包含关键响应头(比如Content-Type,告知浏览器响应内容类型;Content-Length,告知浏览器响应体长度)

当前的响应字符串直接在状态行后拼接HTML内容,既没有正确的头体分隔,也缺少必要的响应头,浏览器无法识别这是合法的HTTP响应。

2. 请求读取逻辑存在缺陷

TCP是流式协议,请求数据可能被拆分成多个数据包分批到达。当前代码仅判断单次读取的片段是否以\r\n\r\n结尾,若请求结束标记被拆分到两次读取中,判断条件永远无法满足,代码会一直卡在读取循环里,根本无法进入发送响应的步骤。


修复后的完整代码

use tokio::{self, net};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;

async fn process(mut stream: TcpStream) {
    let mut buf = [0u8; 4096];
    // 累积读取的请求内容,避免拆分读取导致的判断失效
    let mut request = String::new();
    
    loop {
        let n = stream.read(&mut buf).await.unwrap();
        if n == 0 {
            break;
        }
        let chunk = String::from_utf8_lossy(&buf[0..n]).to_string();
        print!("{}", chunk);
        request.push_str(&chunk);
        
        // 判断累积的请求内容是否包含请求结束标记
        if request.contains("\r\n\r\n") {
            println!("request received");
            break;
        }
    }

    // 构造符合HTTP规范的响应:添加必要头,确保头和体用\r\n\r\n分隔
    let html_body = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello!</title>
</head>
<body>
HELLO
</body>
</html>"#;
    let content_length = html_body.len();
    let response_str = format!(
        "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\n\r\n{}",
        content_length, html_body
    );
    
    println!("response");
    stream.write(response_str.as_bytes()).await.unwrap();
    stream.flush().await.unwrap();
    println!("response DONE");
}

#[tokio::main]
async fn main() {
    let listener = net::TcpListener::bind("127.0.0.1:9998").await.unwrap();
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        tokio::spawn(async move{
            process(stream).await;
        });
    }
}

关键修复点说明

  • 请求读取逻辑:改用String累积所有读取到的请求数据,判断累积内容是否包含\r\n\r\n,确保即使请求分多次到达也能正确识别结束。
  • 响应格式修正:
    • 添加Content-Type头,明确告知浏览器响应是UTF-8编码的HTML文本
    • 添加Content-Length头,告知浏览器响应体的长度
    • 用\r\n\r\n分隔响应头和响应体,严格符合HTTP协议规范
    • 修正了原HTML中udf-8的拼写错误为utf-8

内容的提问来源于stack exchange,提问作者Keith

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 16:01:17