TCP未读完便CLOSE会发送RST

博主用Rust重写C语言的http服务器tinyhttpd时遇到问题,最初以为是HTTP协议问题,后发现是TCP问题。TCP连接关闭时,若未读完读缓冲数据会发RST。介绍了两种情况及相应解决办法,最终得出稳妥方案是读到对方FIN才CLOSE,且双方发完数据都调用shutdown write。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在用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}");
}
<think>好的,我现在需要解决用户的问题:当客户端调用close()后,服务器端recv()的返回值是什么。首先,我得回忆一下TCP连接关闭的过程和相关系统调用的行为。 首先,TCP是全双工的,关闭连接需要双方各自完成关闭。当客户端调用close()时,它会发送FIN包给服务器,表示客户端不再发送数据,但还可以接收数据。这时候,服务器的TCP协议栈会确认这个FIN包,使得服务器端的recv()调用行为生变化。 接下来,服务器可能处于几种状态。如果服务器在客户端调用close()之后调用recv(),此时需要考虑接收缓冲区中是否有数据。如果接收缓冲区还有数据没有被读取,recv()会先返回这些数据,直到缓冲区为空。之后,当缓冲区为空时,再次调用recv()会返回0,表示客户端已经关闭连接,也就是EOF。 但是,用户的问题中客户端已经调用了close(),所以服务器在读取完所有数据后,recv()会返回0。不过,如果服务器在客户端关闭之前已经读取了所有数据,那么下一次recv()就会立即返回0。如果还有数据读,那recv()会先返回数据,直到读完再返回0。 另外,需要考虑异常情况。例如,如果连接被重置(RST包),recv()会返回-1,并设置errno为ECONNRESET。但这种情况通常生在客户端异常关闭,比如崩溃,而不是正常调用close()。 还要注意,当服务器收到FIN时,处于CLOSE_WAIT状态,此时服务器还可以发送数据。不过,如果服务器尝试发送数据,而客户端已经关闭,可能会导致错误,但接收端的行为主要影响recv()的返回值。 总结一下,正常流程下,客户端close()后,服务器的recv()在读完数据后会返回0,表示连接已关闭。如果有数据读,先返回数据,然后返回0。异常情况下可能会返回错误,但正常关闭不会。 可能需要查阅UNIX网络编程的相关章节,确认recv()在接收到FIN后的行为是否正确。例如,Stevens的书里提到,当TCP接收缓冲区中的数据已被应用层读取完毕,再次recv()会返回0,表示对端已经关闭连接。 所以,最终的结论应该是,服务器recv()返回0表示客户端已关闭连接。如果还有数据,先返回数据长度,最后返回0。</think>客户端调用`close()`后服务器端`recv()`的行为可通过以下步骤分析: --- ### 一、TCP连接关闭流程 1. **客户端发送FIN** 客户端调用`close()`会发送FIN包,表明**停止发送数据**(但可继续接收数据)。 2. **服务器进入CLOSE_WAIT状态** 服务器收到FIN后,内核自动回复ACK确认,此时连接处于半关闭状态(服务器仍可发送数据)。 --- ### 二、服务器端`recv()`返回值 1. **若接收缓冲区有数据** - 返回实际读取的字节数(例如返回`N > 0`),直到缓冲区清空。 2. **若接收缓冲区无数据** - **返回`0`**,表示客户端已关闭连接(EOF)。 - 这是正常关闭的唯一返回值,需服务器主动调用`close()`释放资源。 3. **异常情况** - 若连接被RST重置(如客户端崩溃),`recv()`返回`-1`,错误码为`ECONNRESET`。 --- ### 三、代码逻辑示例 ```c // 服务器端循环读取 while (1) { int n = recv(sockfd, buf, sizeof(buf), 0); if (n > 0) { // 处理数据 } else if (n == 0) { // 客户端正常关闭,关闭socket close(sockfd); break; } else { // 处理错误(如ECONNRESET) } } ``` --- ### 四、总结 | 场景 | `recv()`返回值 | 含义 | |-----------------------|---------------|--------------------| | 有读数据 | `N > 0` | 成功读取数据 | | 数据已读完且收到FIN | `0` | 客户端正常关闭连接 | | 连接异常(如RST) | `-1` | 需检查`errno` | --- 通过检测`recv()`返回值是否为`0`,服务器可安全地关闭对应socket。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值