1

I'm trying to write my own TCP Non-Blocking Server to handle multiple long lasting socket connections rather than opening many threads to handle them.

I've written my over-complicated, hard to use syntax but have the issue forms when I'm trying to detect a closed socket.

In a normal threaded TCP Socket Server I would use detected a b'' from the socket.read(size) function, However this is not possible with a nonblocking socket as it will always return a BlockingIOError

I have also tried catching theese following events

except BrokenPipeError:
    conn.abort()
except ConnectionResetError:
    conn.abort()
except ConnectionAbortedError:
    conn.abort()
except socket.error:
    conn.abort()

(conn is a class that houses the client socket and address from socket.accept())

I'm unsure what to do, but here is a deeply simplified extract from my code:

def loop_listen(self):
    while self.running == True:
    cr, addr = self.server.accept()
    crs = SocketHandler(self, cr, addr)
    self.client_handler(crs)
    self.connections.append(crs)
    crs.events["open"]()
    crs.cr.setblocking(0)

def loop_recv(self):
    while self.running == True:
        time.sleep(self.poll_time)

        for conn in self.connections:
        try:
            data = conn.cr.recv(self.poll_size)
            print(data)
            if (data == b''):
                conn.abort()
        except BlockingIOError:
            data = None
        except BrokenPipeError:
            conn.abort()
        except ConnectionResetError:
            conn.abort()
        except ConnectionAbortedError:
            conn.abort()
        except socket.error:
            conn.abort()
        if (data != None):
            conn.events["msg"](data)

(Both loops are separate threads)

And incase you wanted it, here is the conn class

class SocketHandler:
    def __init__(self, server, cr, addr):
        self.server = server
        self.cr = cr
        self.addr = addr
        self.events = {"msg": emptyCallback, "close": "emptyCallback","open":emptyCallback}
        self.cache = b""

    def message(self, func):
        self.events["msg"] = func

    def close(self, func):
        self.events["close"] = func

    def open(self, func):
        self.events["open"] = func

    def send(self, data):
        self.cr.send(data)
    def abort(self):
        self.cr.close()
        self.events["close"]()
        self.server.connections.remove(conn)

This works fine on Windows but on Ubuntu it does not call the conn.abort().

Any help would be greatly appreciated.

Thanks, Sam.

SamHDev
  • 172
  • 1
  • 14
  • 2
    If the connection is closed, the recv should return immediately with a zero-length bytes object. What does the `print(data)` statement give you after the other side closed the connection? Also, consider using `select` to wait for events on the socket instead of sleeping between polls. – Roland W Jun 30 '19 at 22:31
  • The recv does not give me back an empty bytes object after disconnect as mentioned above, the `print(data)` outputs `b'Hello World'` (the message it should receive) – SamHDev Jun 30 '19 at 22:40
  • You call `abort` on your socket class. But do you actually remove it from `self.connections`? – Finomnis Jun 30 '19 at 22:42
  • Yes `self.server.connections.remove(conn)` – SamHDev Jun 30 '19 at 22:48
  • This question is not about the socket being closed. It is about the *connection* being closed by the peer. The socket is not closed. Otherwise you couldn't read from it at all. – user207421 Jul 01 '19 at 00:33

1 Answers1

1

The official way to detect a closed connection on a non-blocking socket is exactly the same as blocking sockets. They return empty data from recv().

Example:

# Server
import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 12345))
s.listen(1)

while True:
    conn, addr = s.accept()
    conn.setblocking(0)
    print("New connection from " + str(addr) + ".")
    while True:
        try:
            data = conn.recv(1024)
            if not data:
                break
            print("Received:", data)
        except BlockingIOError:
            time.sleep(0.001) 

    print("Closed.")
# Client
import socket
import time

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 12345))

for i in range(5):
    time.sleep(0.3)
    s.send(str(i).encode('utf-8'))
s.close()

There is one special case where this won't work, as described in the official docs, section When Sockets Die. It happens, when sockets don't shut down gracefully. There basically is no way for recv() to detect when a socket is dead without a graceful shutdown. It might be that this is what you are seeing.

There are multiple ways to resolve that. For one, create some kind of timeout that closes and discards a socket if it didn't receive a message for a sensible amount of time. Secondly, you could actively send messages. Detecting a dead socket is much easier for send() than for recv().

Further, this works on Linux. I didn't test it on Windows. The internal implementation of the sockets class is very platform dependent, so it might be a Windows bug.

Finomnis
  • 18,094
  • 1
  • 20
  • 27
  • Hello, The only output I get from is the message from the client. The code should handle a empty bytes to detect a close yet it does not work. This is because the `.recv()` function raises a `BlockingIOError` error. This fit in with what you mention – SamHDev Jun 30 '19 at 22:47
  • It works for me, definitely. Which operating system are you on? – Finomnis Jun 30 '19 at 22:49
  • Is there anyway to send that wont harm the trafic? I'm dealing with HTTP Traffic and WS traffic, and from past experience it can be temperamental – SamHDev Jun 30 '19 at 22:49
  • Which python version? I run Linux Mint 19.1 (=Ubuntu 18.04) with Python 3.6.7, and it definitely works. But I remember having problems in the past. Might have been a bug, and got patched in newer versions. Anyway, not sure what else to say ... I don't think another way exists, if this one doesn't work, I'm afraid you either have to do blocking sockets or a different language ... I'm not sure if there are any ways to ping HTTP/WS connections, sorry :/ – Finomnis Jun 30 '19 at 23:02
  • I was running 3.5.2 but installed 3.7 with still no luck. Gonna Explore TCP_INFO to try poll the socket state. Thanks for your help! -Sam – SamHDev Jun 30 '19 at 23:16
  • 1
    Good luck ... sounds like a dumb workaround for something that python *should* have implemented correctly. Yay. To make sure it is not an operating system problem, I would write a short test in C, directly utilizing the socket api. It would be interesting if it behaves the same. – Finomnis Jun 30 '19 at 23:21
  • It isn't OS-dependent. The Python guys have screwed up somehow. The behaviour is specified in the Sockets API. – user207421 Jul 01 '19 at 00:10
  • Yah, I know, but I mean the implementation is heavily os-dependent. Inside the library. Therefore it isn't surprising a bug like this only exists on one version of one OS. – Finomnis Jul 01 '19 at 00:20
  • So you should say 'the implementation of sockets *in Python*'. – user207421 Jul 01 '19 at 00:34
  • I disagree, I think it is quite clear how to understand it. Nonetheless I changed it to stop further discussion. – Finomnis Jul 01 '19 at 00:45