Search code examples
pythonsocketssslselect

select.select() isn't accepting my socket and returns empty lists


I wished to write a script for something that would act as both a server and a client. For the server part I decided I should create an SSL socket accept() loop that would listen to new connections forever, and to make it non-blocking so that the client part can work I decided to use select.

Here's the script:

from socket import (socket, 
                    AF_INET, 
                    SOCK_STREAM, 
                    create_connection, 
                    SOL_SOCKET, 
                    SO_REUSEADDR)
from ssl import (SSLContext, 
                 PROTOCOL_TLS_SERVER, 
                 PROTOCOL_TLS_CLIENT)


import select

import threading

import time
from tqdm.auto import tqdm


            

def handle_client(client, address):
    request_bytes = b"" + client.recv(1024)

    if not request_bytes:
        print("Connection closed")
        client.close()
    request_str = request_bytes.decode()
    print(f"we've received {request_str}")



ip = '127.0.0.1'
port = 8443
server_context = SSLContext(PROTOCOL_TLS_SERVER)
server_context.load_cert_chain('cert_ex1.pem', 'key_ex1.pem')


initial_counter = 1


server_socket = socket(AF_INET, SOCK_STREAM)
server_socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
server_socket.bind((ip, port))
server_socket.listen(5)



print("forming lists for select...")
inputs = [server_socket]
print(f"we now have list inputs: {inputs}")
outputs = []

while True:
    print(f"checking: inputs still has a single socket in it: {type(inputs[0])}")
    readable, writable, exceptional = select.select(inputs, outputs, inputs, 1)
    print(f"readable is {readable}, \n writable is {writable}, \n exceptional is {exceptional}")
    for s in tqdm(readable):
        if s is server_socket:
            print("wrapping server socket in SSL...")
            with server_context.wrap_socket(server, server_side=True) as tls:
                connection, client_address = tls.accept()
                print("making the connection non-blocking...")
                connection.setblocking(0)
                inputs.append(connection)
                print("starting a thread that'd handle the messages...")
                threading.Thread(target=handle_client, args=(connection, client_address)).start()                
            
        else:
            print(f"dealing with socket {s}")

        
hostname='example.org'
client_context = SSLContext(PROTOCOL_TLS_CLIENT)
client_context.load_verify_locations('cert_ex1.pem')


with create_connection((ip, port)) as client:
    with client_context.wrap_socket(client, server_hostname=hostname) as tls:
        print(f'Using {tls.version()}\n')
        print("client is sending data...")
        tls.sendall(int.to_bytes(initial_counter, 4, 'little'))

        while True:
            data = tls.recv(1024*8)
            if not data:
                print("Client received no data")
                break
            
            new_data = int.from_bytes(data, 'little')
            print(f'Server says: {new_data}')
            new_data = int.to_bytes(new_data+1, 4, 'little')
            print("sleeping for 0.15...")
            time.sleep(0.15)
            tls.sendall(new_data)

The problem with running this script is that it creates the socket properly, passes a list with just this socket to select.select(), but then select() returns 3 empty lists.

  1. Is there a reason for this?
  2. Is there anything else wrong with my code (the general idea, trying to make this work with SSL, using connection.setblocking(0), anything else)?

Solution

  • Here's an example using non-blocking sockets on the server side – the client is still spawned into a different thread, but that's done in the "idle callback", so now you can see you can accept clients and client data and still occasionally do other work in the same program.

    I skipped the ssl bits because I don't have your certs at hand, but be sure to only wrap both sockets once...

    import random
    import select
    import socket
    import threading
    import time
    
    hostname = "example.org"
    ip = "127.0.0.1"
    port = 8443
    client_counter = 0
    
    
    def server(idle_callback):
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        server_socket.bind((ip, port))
        server_socket.listen(5)
        # server_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        # server_socket = server_context.wrap_socket(server_socket, server_side=True)
    
        clients = []
    
        while True:
            sockets = [server_socket, *clients]
            readable, writable, exceptional = select.select(sockets, [], sockets, 0.1)
            for s in readable:
                if s is server_socket:
                    connection, client_address = server_socket.accept()
                    connection.setblocking(False)
                    clients.append(connection)
                    print(f"new connection from {client_address}")
                else:  # must be a client socket
                    try:
                        msg = s.recv(1024)
                        print(f"{s}: received {msg}")
                        if msg.startswith(b"The time is"):
                            s.sendall(b"The eagle flies at midnight...\n")
                        else:
                            s.sendall(f"Sorry, I don't understand {msg}\n".encode())
                    except ConnectionError as exc:
                        print(f"{s}: {exc}")
                        s.close()
                        clients.remove(s)
                        continue
            for x in exceptional:
                print(f"exceptional condition on {x}")
                x.close()
                clients.remove(x)
            idle_callback()
    
    
    def client():
        global client_counter
        client_counter += 1
        client_id = client_counter
        print(f"[{client_id}] Starting client...")
    
        with socket.create_connection((ip, port)) as client:
            # client_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
            # with client_context.wrap_socket(client, server_hostname=hostname) as client:
            #     print(f'Using {client.version()}\n')
    
            while True:
                if random.random() < 0.1:
                    print(f"[{client_id}] Time to go...")
                    break
    
                if random.random() < 0.5:
                    client.sendall(f"The time is {time.asctime()}\n".encode("utf-8"))
                else:
                    client.sendall(b"Hello, server\n")
    
                data = client.recv(1024 * 8)
                if not data:
                    print(f"[{client_id}] Client received no data")
                    break
                print(f"[{client_id}] Server says: {data}")
                time.sleep(0.5)
    
    
    def idle_callback():
        if random.random() < 0.1:
            threading.Thread(target=client).start()
    
    
    def main():
        server(idle_callback)
    
    
    if __name__ == "__main__":
        main()