TCP未读完便CLOSE会发送RST

最近在用Rust重写一个C语言写的http服务器,tinyhttpd,以此来学习Rust,重写的过程中发现了一个小坑。

tinyhttpd里有几处read & discard headers的代码,就是读然后丢弃掉读到的http headers:

while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
    numchars = get_line(client, buf, sizeof(buf));

我想既然读了这些东西,但是什么也没干,不如直接忽略掉这两行代码,然后果不其然,服务器寄了…浏览器打不开网页。

我最初以为是HTTP协议的问题,为了探究原因,自己又写了个HTTP GET的程序,然后发现跟HTTP没关系,是TCP的问题。为了解释问题,极简版的代码如下:

// server.rs
let listener = TcpListener::bind("127.0.0.1:4000").unwrap();

for stream in listener.incoming() {
    let mut stream = stream.unwrap();
    stream.read_exact(&mut [0; 4]).unwrap(); // 注意这一行,记为A
    stream.write_all(b"pong").unwrap();
}


// client.rs
let mut stream = TcpStream::connect("127.0.0.1:4000").unwrap();

stream.write_all(b"ping").unwrap();

let mut text = String::new();
stream.read_to_string(&mut text).unwrap();
println!("Received: {text}");

server先跑起来,再cargo run --bin client,输出:

Received: pong

但是如果把行A注释掉,则出现报错:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 10054, kind: ConnectionReset, message: "An existing connection was forcibly closed by the remote host." }', src/client.rs:12:38
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\client.exe` (exit code: 101)

这个报错的意思就是TCP中对方发了个RST,强制把连接关闭了。

TCP在三次握手之后,双方就是完全对等的连接。正常关闭连接的话,某一方会先发送一个FIN,含义为:“我已经发送完了”,此后对方再READ的话,就会返回EOF。对方发送完后,也会发送FIN,告知自己已经发送完,这样TCP双方都是到对话已经完成,可以释放资源了。

但是有一个细节,当一方关闭连接的时候,如果并没有读完读缓冲里的东西,就会发送一个RST,这在 RFC 1122 4.2.2.13节第三段有提到:

A host MAY implement a “half-duplex” TCP close sequence, so
that an application that has called CLOSE cannot continue to
read data from the connection. If such a host issues a
CLOSE call while received data is still pending in TCP, or
if new data is received after CLOSE is called, its TCP
SHOULD send a RST to show that data was lost.

这里面提到了两种情况:

  1. 没读完就调用CLOSE
  2. CLOSE完又收到了新数据

这两种情况都很重要,第一种可以通过shutdown write来解决,改写后的server代码如下:

// server.rs
let listener = TcpListener::bind("127.0.0.1:4000").unwrap();

for stream in listener.incoming() {
    let mut stream = stream.unwrap();
    stream.write_all(b"pong").unwrap();
    stream.shutdown(Shutdown::Write).unwrap();
}

这样,shutdown write先发送了个FIN,然后stream drop的时候调用CLOSE,即使此时接收缓冲区还有数据,也不会发送RST。

大多数情况这样的解决方法都是可以的,但是有时候client要发送的数据很多,因此可能会出现第二种情况:CLOSE完又收到了新数据。

把client代码再改写如下:

// client.rs
let mut stream = TcpStream::connect("127.0.0.1:4000").unwrap();

stream.write_all(b"p".repeat(1024 * 1024).as_slice()).unwrap();  // 发送1MiB的数据

let mut text = String::new();
stream.read_to_string(&mut text).unwrap();
println!("Received: {text}");

这时候再运行就又报错了,因为client还没发完数据,server便CLOSE掉了TCP连接,client再发送数据的时候,server会返回RST。虽然client之前收到过FIN了(这样调用READ读完会返回EOF),但是又收到RST,强制中止连接,因此调用READ会直接报错。

所以最稳妥的解决方案还是读到对方发送FIN才CLOSE,我们把server改成这样:

// server.rs
let listener = TcpListener::bind("127.0.0.1:4000").unwrap();

for stream in listener.incoming() {
    let mut stream = stream.unwrap();
    stream.write_all(b"pong").unwrap();
    while stream.read(&mut [0; 1024]).unwrap() > 0 {} // 等待FIN
}

再运行发现,无响应?

细心的读者可能发现了,我把shutdown write删了,这样,两边都发送完了自己的数据,都在等待对方发送FIN,然后连接结束不了。其实只要一方shutdown write就行,最好的话是双方发送完数据都调用shutdown write。最终完整版代码如下:

// server.rs
use std::{
    io::{Read, Write},
    net::{Shutdown, TcpListener, TcpStream},
};

fn main() {
    let listener = TcpListener::bind("127.0.0.1:4000").unwrap();

    for stream in listener.incoming() {
        // 服务器一般会先从stream里读一些东西
        // 如果读到的东西已经足够,剩下的没有意义了
        // 比如HTTP第一行会写请求地址,然后服务器发现这个地址不存在
        // 就可以调用下面这个函数发送个404 NOT FOUND的页面什么的
        write_then_gracefully_shutdown(stream.unwrap(), b"pong");
    }
}

fn write_then_gracefully_shutdown(mut stream: TcpStream, data: &[u8]) {
    stream.write_all(data).unwrap();
    stream.shutdown(Shutdown::Write).unwrap(); // 发送FIN

    while stream.read(&mut [0; 1024]).unwrap() > 0 {} // 等待FIN
}


// client.rs
use std::{
    io::{Read, Write},
    net::{Shutdown, TcpStream},
};

fn main() {
    let mut stream = TcpStream::connect("127.0.0.1:4000").unwrap();

    stream
        .write_all(b"p".repeat(1024 * 1024).as_slice())
        .unwrap();
    stream.shutdown(Shutdown::Write).unwrap(); // 发送FIN

    let mut text = String::new();
    stream.read_to_string(&mut text).unwrap(); // 等待FIN
    println!("Received: {text}");
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值