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.
connection.setblocking(0)
, anything else)?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()