Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to limit the number of connections to a socket and trigger timeout on client (Python)

Tags:

python

sockets

How can I set a limit on the number of connections that a server socket can accept at once? I want to be able to set a max number of connections, and then once that limit is reached, any further attempts from clients to connect will result in a timeout. So far, I have tried something like this for the server:

sock = socket.socket()
sock.setblocking(0)
sock.bind(address)
sock.listen(0)

connections = []

while True:
    readable, writable, exceptional = select.select([sock], [], [])

    if readable and len(connections) < MAX_CONNECTIONS:
        connection, client_address = s.accept()
        connections.append(connection)
        # Process connection asynchronously

and for the client:

try:
    sock = socket.create_connection(self.address, timeout=TIMEOUT)
    sock.settimeout(None)
    print "Established connection."
except socket.error as err:
    print >> sys.stderr, "Socket connection error: " + str(err)
    sys.exit(1)

# If connection successful, do stuff

Because of the structure of the rest of the program, I have chosen to use a non-blocking socket on the server and I do not wish to change this. Right now, the clients are able to connect to the server, even after the limit is reached and the server stops accepting them. How do I solve this? Thanks.

like image 444
b_pcakes Avatar asked Oct 31 '22 10:10

b_pcakes


1 Answers

I believe there might be a slight misunderstanding of select() at play here. According to the manpage, select() returns file descriptors that are "ready for some class of IO operation", where ready means "it is possible to perform a corresponding IO operation without blocking". The corresponding IO operation on a listening socket is accept(), which can only be performed without blocking if the OS already made the full TCP handshake for you, otherwise you might block waiting for the client's final ACK, for instance.

This means that as long as the listening socket is open, connections will be accepted by the OS, even if not being handled by the application. If you want to reject connections after a set number, you have basically two options:

  • simply accept and close directly after accepting.
  • close the listening socket upon reaching the limit and reopen when done.

The second option is more convoluted and requires use of the SO_REUSEADDR option, which might not be the right thing in your case. It might also not work on all OSs, though it does seem to work reliably on Linux.

Here's a quick sketch of the second solution (since the first is pretty straightforward).

def get_listening_socket():
    sock = socket.socket()
    sock.setblocking(0)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('0.0.0.0', 5555))
    sock.listen(0)
    return sock

sock = get_listening_socket()

LIMIT = 1
conns = {sock}
while True:
    readable, writable, exceptional = select.select(conns, [], [])
    if sock in readable: # new connection on the listening socket
        conn, caddr = sock.accept()
        conns.add(conn)
        if len(conns) > LIMIT: # ">" because "sock" is also in there
            conns.remove(sock)
            sock.close()
    else: # reading from an established connection
        for c in readable:
            buf = c.recv(4096)
            if not buf:
                conns.remove(c)
                sock = get_listening_socket()
                conns.add(sock)
            else:
                print("received: %s" % buf)

You may, however, want to rethink why you'd want to do this in the first place. If it's only about saving some memory on the server, than you might be over-optimizing and should be looking into syn-cookies instead.

like image 146
Leo Antunes Avatar answered Nov 14 '22 12:11

Leo Antunes