How to detect TCP disconnection?

I'm coding a tcp server programe. It needs to load some resources after TCP connection and release resources when the connection is disconnected.

I used to implement it with C++,the C++ network framework provides some Listener interface like this:

public:
virtual EnHandleResult OnPrepareListen(ITcpServer* pSender, SOCKET soListen);
virtual EnHandleResult OnAccept(ITcpServer* pSender, CONNID dwConnID, UINT_PTR soClient);
virtual EnHandleResult OnHandShake(ITcpServer* pSender, CONNID dwConnID);
virtual EnHandleResult OnReceive(ITcpServer* pSender, CONNID dwConnID, int iLength);
virtual EnHandleResult OnSend(ITcpServer* pSender, CONNID dwConnID, const BYTE* pData, int iLength);
virtual EnHandleResult OnClose(ITcpServer* pSender, CONNID dwConnID, EnSocketOperation enOperation, int iErrorCode);
virtual EnHandleResult OnShutdown(ITcpServer* pSender);

So I can write the code in OnHandShake and OnClose.
But how can I implement it with rust? Is there any framework that can provide similar functions?

1 Like

Rust normally handles these sorts of conditions by returning an error which describes what went wrong instead of registering callbacks.

So whenever reading from a TcpStream fails, it will return a std::io::Error. This has a kind() method which you can use to figure out what kind of error the OS returned, and the ErrroKind::ConnectionAborted error seems to be what you are looking for. You could also match on the ErrorKind if there are other variants you care about.

use std::{
    io::{ErrorKind, Read},
    net::TcpStream,
};

fn read_and_handle_errors(stream: &mut TcpStream) {
    let mut buffer = [0; 1024];

    match stream.read(&mut buffer) {
        Ok(bytes_read) => {
            println!("Read {:?}", &buffer[..bytes_read]);
        }
        Err(e) if e.kind() == ErrorKind::ConnectionAborted => {
            println!("Other side disconnected");
        }
        Err(e) => {
            println!("Some other error occurred: {e}");
        }
    }
}

(playground)

What I would probably do is create some sort of is_spurious() function which, given a reference to some std::io::Error, will tell you whether it's a spurious error and retrying the read might work (e.g. the read timed out or the network temporarily dropped) or whether there's no easy way to recover from the error (i.e. everything else).

From there, you can handle a read error by checking is_spurious() and either bailing from the function (which should clean up resources automatically as they are dropped) or retrying.

2 Likes

Thanks for your reply, it solved my problem very well. :+1:

Note that there are two ways to "disconnect" a TCP connection (see also this older post):

  • According to Internet Standard STD 7 (aka RFC 793), TCP connections can be closed by applications in two ways:
    • close (sending FIN)
    • abort (sending RST)
  • Peer applications can distinguish whether a connection was successfully closed (they receive an EOF) or was aborted (they receive an error).
  • […]

I assume that a "close" rather gives ErrorKind::UnexpectedEof. So it can be either of those two (UnexpectedEof or ConnectionAborted), depending on the situation.

1 Like

I didn't know there were two cases, thanks!

I've so far only been matching on ErrorKind::UnexpectedEof and it worked fine, but I guess it's because I have control of both client and server. I'll add the case ErrorKind::ConnectionAborted for completeness.

One scenario is that a peer with a dynamic IP disconnects and the IP is assigned to another machine. That other machine receives a TCP packet and will respond with a TCP RST packet.

I'm currently not sure what happens if the peer is just shut down (i.e. power turned off) and won't respond to packets anymore. I think the connection will eventually be closed by the operating system with a timeout, which might result in ErrorKind::TimedOut, being a third option of what can happen. Maybe some routers might also send an ICMP(6) Destination Unreachable packet (which I believe will be reported as ConnectionAborted), but AFAIK you cannot always rely on that.

Summarizing, a TCP disconnect can have several causes.

Maybe you want to treat TimedOut and ConnectionAborted just as any other "I/O error", and UnexpectedEof as the "graceful shutdown" from your peer.

Just a drive-by note that a half-closed connection is not necessarily an error state.

1 Like

Yes, right. A half-closed connection would result in an UnexpectedEof, I think (when attempting to read from it). Of course, such an "unexpected EOF" could be expected and not really be an error.

Thus the name UnexpectedEof can be a bit misleading, I guess. It could or should be interepreted as "Eof" (whether being expected or unexpected).

Hi guys,I've tried the solution and write a demo, but it does't work.

The demo code:

fn new_connection(stream: TcpStream) {
    println!("New Connection");

    let mut ss = stream;
    let mut buffer = [0; 128];

    match  ss.read(&mut buffer) {
        Ok(_) => {
            println!("Read {:?}", buffer);
        }
        Err(e) if e.kind() == ErrorKind::ConnectionAborted => {
            println!("ErrorKind::ConnectionAborted");
        }
        Err(e) => {
            println!("ErrorKind: {e}");
        }
    }
   println!("Done");
   return;
}

I use a TCP tool "Swiddler" as the client, then I connect to the server and then disconnect without sending any data.

The following is output:

New Connection
Read [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
Done

The stream returns without any error.

Why does this happen?

However, if I kill the Swiddler process after connection, it returns a Error:

New Connection
ErrorKind: Connection reset by peer (os error 104)
Done

It's seems to be caused by the Swiddler client. But I can't make sure the client can disconnect properly as a server developer.

Maybe cleanly closing the connection makes the read method return Ok(0). You should check that case instead of only checking for a connection reset.

Perhaps this error is really only used in case of "unexpected" EOF? Not sure.


Why I think so (I haven't tested):

std::io::Read::read's documentation says:

This function does not provide any guarantees about whether it blocks waiting for data, but if an object needs to block for a read and cannot, it will typically signal this via an Err return value.

If the return value of this method is Ok(n), then implementations must guarantee that 0 <= n <= buf.len(). A nonzero n value indicates that the buffer buf has been filled in with n bytes of data from this source. If n is 0, then it can indicate one of two scenarios:

  1. This reader has reached its “end of file” and will likely no longer be able to produce bytes. Note that this does not mean that the reader will always no longer be able to produce bytes. As an example, on Linux, this method will call the recv syscall for a TcpStream, where returning zero indicates the connection was shut down correctly. While for File, it is possible to reach the end of file and get zero as result, but if more data is appended to the file, future calls to read will return more data.
  2. The buffer specified was 0 bytes in length.

This is all a bit vague ("it will typically signal this"). Now it's unclear to me whether a TcpStream's implementation of Read will raise an UnexpectedEof or simply returns Ok(0). I guess I'd need to check the source, and not sure even if checking the source for a particular implementation will give clarity, as I don't know whether this is guaranteed for all operating systems.

read returns how many bytes were actually read into the buffer, while you are always printing the whole buffer.

I'd do something along these lines (not tested):

loop {
    match ss.read(&mut buffer) {
        Ok(n) => {
            println!("Read {} bytes: {:?}", n, &buffer[..n]);
        }
        Err(e) => {
            match e.kind() {
                ErrorKind::ConnectionAborted | ErrorKind::UnexpectedEof => println!("Client disconnected"),
                kind => eprintln!("Other error: {kind}"),
            }
            break;
        }
    }
}
1 Like

@rikyborg I refined your example to handle the "gracefully closed connection" case properly (on FreeBSD at least):

use std::io::{ErrorKind, Read};
use std::net::{TcpListener, TcpStream};

fn new_connection(stream: TcpStream) {
    println!("New Connection");

    let mut ss = stream;
    let mut buffer = [0; 128];

    loop {
        match ss.read(&mut buffer) {
            Ok(nbytes) => {
                if nbytes > 0 {
                    println!("Read {:?}", &buffer[..nbytes]);
                } else {
                    println!("Connection closed gracefully by peer (reported as zero bytes read)");
                    break;
                }
            }
            Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
                // This doesn't happen with FreeBSD
                println!(
                    ")Connection closed gracefully by peer (reported as ErrorKind::UnexpectedEof)"
                );
                break;
            }
            Err(e) if e.kind() == ErrorKind::ConnectionAborted => {
                println!("ErrorKind::ConnectionAborted");
                break;
            }
            Err(e) => {
                println!("ErrorKind: {e}");
                break;
            }
        }
    }
    println!("Done");
    return;
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("[::1]:1234")?;
    for stream in listener.incoming() {
        new_connection(stream?);
    }
    Ok(())
}

@zfc1996 Does that work for you?

Note: If you do not use IPv6, replace "[::1]" with "127.0.0.1" or something else.

I would like to note that the capabilities of Rust's standard library regarding networking are somewhat limited. It might make sense to use a different interface, e.g. tokio (which is asynchronous Rust though, and might complicate things). Perhaps there are some non-async libraries too that are better suited than std::net (depending on what you plan to do).

This code should work, we must handle the case where the return value is zero. Thanks for your help!

fn new_connection(stream: TcpStream) {
    println!("New Connection");

    let mut ss = stream;
    let mut buffer = [0; 128];

    let _t1 = thread::spawn(move || {
        loop {
            match ss.read(&mut buffer) {
                Ok(n) => {
                    if n > 0 {
                        let s = String::from_utf8_lossy(&buffer);
                        println! {"Receive: {}",s};
                    } else {
                        break;
                    }
                }

                Err(e) => {
                    println!("ErrorKind: {e}");
                    break;
                }
            }
        }
        release_something();
        println!("Connection Disconnect!");
    });
}

You probably want to only use the part of buffer that was actually written to:

                    if n > 0 {
-                       let s = String::from_utf8_lossy(&buffer);
+                       let s = String::from_utf8_lossy(&buffer[..n]);
                        println! {"Receive: {}",s};
                    } else {

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.