Search code examples
pythonsocketsexceptionnonblocking

Python socket non-blocking recv() exception(s) and sendall() exception


I'm writing a simple TCP relay server, which is gonna be deployed both on a Windows and a Linux machine (same code base). Naturally there're gonna be two sockets to work with. I would like to know which exceptions exactly do get raised for the following cases:

  1. recv() returns when no data is available to read.
  2. sendall() cannot complete (dispose of the whole to-send data)
  • Do I have to check for both errnos (socket.EWOULDBLOCK and socket.EAGAIN) when expecting to return from a non-blocking sockets recv()?
  • Which errno (.args[0]) do I get when sendall() fails?

Here's my code so far:

try:
    socket1.setblocking(False)
    socket2.setblocking(False)

    while True:
        try:
            sock1_data = socket1.recv(1024)
            if sock1_data:
                socket2.sendall(sock1_data)
        except socket.error as e:
            if e.args[0] != socket.EAGAIN and e.args[0] != socket.EWOULDBLOCK:
                raise e

        try:
            sock2_data = socket2.recv(1024)
            if sock2_data:
                socket1.sendall(sock2_data)
        except socket.error as e:
            if e.args[0] != socket.EAGAIN and e.args[0] != socket.EWOULDBLOCK:
                raise e
except:
    pass
finally:
    if socket2:
        socket2.close()
    if socket1:
        socket1.close()

My main concern is: What socket.error.errno do I get when sendall() fails?

Lest the socket.error.errno I get from a failing sendall() is EAGAIN or EWOULDBLOCK, in which case it'd be troubling!!

Also, do I have to check for both EAGAIN and EWOULDBLOCK when handling a non-blocking recv()?


Solution

  • A Deep Dive

    In CPython, socket.sendall is implemented by sock_sendall in socketmodule.c, which calls sock_send_impl indirectly via sock_call_ex (as does socket.send too). Ultimately, this results in calling the system call send(...), which will set errno accordingly. sock_call_ex will handle some of the potential errors- it may automatically retry on EINTR, and for EWOULDBLOCK and EAGAIN it will retry if there's a positive timeout.

    Apart from that, however, socket.sendall can by and large produce most of the same errors that send can. There are a few, related to malformed arguments that should be impossible, assuming no bugs in python's socket module implementation, but the rest are valid, including EAGAIN and EWOULDBLOCK. You can consult the man pages for a list of errors that send can produce, or see this online list.

    As for socket.recv, the implementation is at sock_recv_impl, which calls recv directly, resulting in a similar situation. Again, see the man pages, or online.

    In short, for recv(), you indeed need to handle both EAGAIN and EWOULDBLOCK for no data available, unless you know your platform so you can determine which it will raise. For sendall(), the errors you need to handle will depend on the errors you want to support / recover from. As an extreme example- you could get ENOMEM if you ran out of memory as it tries to fulfill the send request, however that's an extremely unlikely scenario, and you would already have bigger problems at that point.

    XY Problem

    Luckily, in your case there's an easy solution, since you're really just concerned with distinguishing the expected errors on nonblocking recv from actual errors during either recv or send. Just move the socket.sendall out of the try body, using the else construct:

    while True:
        try:
            sock1_data = socket1.recv(1024)
        except socket.error as e:
            if e.args[0] != socket.EAGAIN and e.args[0] != socket.EWOULDBLOCK:
                raise e
        else:  # only runs if there was no exception
            if sock1_data:
                socket2.sendall(sock1_data)
    
        try:
            sock2_data = socket2.recv(1024)
        except socket.error as e:
            if e.args[0] != socket.EAGAIN and e.args[0] != socket.EWOULDBLOCK:
                raise e
        else:  # only runs if there was no exception
            if sock2_data:
                socket1.sendall(sock2_data)