Skip to content

Commit 05bd58d

Browse files
committed
Update the specification of the new SDK to create clients and servers
1 parent 512f496 commit 05bd58d

1 file changed

Lines changed: 63 additions & 43 deletions

File tree

simulaqron/sdk/protocol.py

Lines changed: 63 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import asyncio
2+
from asyncio import StreamWriter, StreamReader
13
from enum import IntEnum
2-
from pathlib import Path
3-
from typing import Any, List, Callable, Optional
4+
from typing import Any, Awaitable, Optional, Callable
45

5-
from netqasm.sdk.external import NetQASMConnection
6-
from simulaqron.sdk import SimulaQronConnection, Socket
6+
from simulaqron.general.host_config import SocketsConfig
77

88

99
class ServingStatus(IntEnum):
@@ -51,37 +51,47 @@ def stop_serving(self) -> ServingStatus:
5151
return ServingStatus.STOP
5252

5353

54-
class SimulaQronProtocol:
55-
def __init__(self, network_config: str | Path, name: str):
56-
self._network_config = network_config
57-
self._node_name = name
58-
# TODO - Load the network configuration in simulaqron!
59-
pass
60-
61-
62-
class SimulaQronClassicalClient(SimulaQronProtocol):
63-
def __init__(self, network_config: str | Path, name: str):
54+
class SimulaQronClassicalClient:
55+
def __init__(self, sockets_config: SocketsConfig):
6456
"""
6557
Classical client used to send classical messages to remote nodes. The given node name
6658
must exist on the given network configuration.
6759
68-
:param network_config: The path of the Network configuration.
69-
:type network_config: str | Path
70-
:param name: The name of the node to connect to. The name *must* exist in the network
71-
configuration file.
72-
:type name: str
60+
:param sockets_config: The sockets configuration for the whole network.
61+
:type sockets_config: SocketsConfig
7362
"""
74-
super().__init__(network_config, name)
63+
self._sockets_config = sockets_config
7564

76-
def connect_to(self, node_name: str) -> None:
65+
async def _run_client(self, hostname: str, port: int, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]):
66+
reader, writer = await asyncio.open_connection(hostname, port)
67+
await callback(reader, writer)
68+
69+
70+
def run_client(self, node_name: str, callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]) -> None:
7771
"""
78-
Connects to the node with the given name.
72+
Connects to the node with the given name. Once the connection has been established,
73+
the given callback will be executed to start the interaction with the server.
74+
The given function must have the following signature::
75+
76+
async def connected_handler(reader: StreamReader, writer: StreamWriter):
77+
# Send a message to the server
78+
writer.write("Hello world!".encode("utf-8"))
79+
# Afterwards, you might want to receive an answer
80+
message = await reader.read(255)
81+
print(message.decode("utf-8"))
82+
83+
After calling this function, the
7984
8085
:param node_name: The name of the node to connect to. The name *must* exist in the
8186
configuration file given when constructing this client.
8287
:type node_name: str
88+
:param callback: The function to be called when the connection is established.
89+
:type callback: Callable[[StreamReader, StreamWriter], Awaitable[None]]
8390
"""
84-
pass
91+
if node_name not in self._sockets_config.hostDict:
92+
raise RuntimeError(f"The node with name '{node_name}' is not on the network configuration.")
93+
socket_config = self._sockets_config.hostDict[node_name]
94+
asyncio.run(self._run_client(socket_config.hostname, socket_config.port, callback))
8595

8696
def send_message(self, message: str) -> None:
8797
"""
@@ -90,36 +100,46 @@ def send_message(self, message: str) -> None:
90100
:param message: The message to send.
91101
:type message: str
92102
"""
103+
pass
93104

94105

95-
class SimulaQronClassicalServer(SimulaQronProtocol):
96-
def __init__(self, network_config: str | Path, name: str, simulaqron_connection: Optional[SimulaQronConnection] = None):
97-
super().__init__(network_config, name)
98-
self._message_handlers: List[Callable[[SimulaQronState, str], ServingStatus]] = []
99-
self._connection = simulaqron_connection
100-
self._message_handlers: List[Callable[[SimulaQronState, str, NetQASMConnection], ServingStatus]] = []
101-
# TODO - Define what else to do in the constructor
106+
class SimulaQronClassicalServer:
107+
def __init__(self, sockets_config: SocketsConfig, name: str):
108+
self._node_name = name
109+
self._sockets_data = sockets_config.hostDict[self._node_name]
110+
self._connection_handler: Optional[Callable[[StreamReader, StreamWriter], Awaitable[None]]] = None
102111

103-
def register_message_handler(self, handler: Callable[[SimulaQronState, str], ServingStatus]) -> None:
112+
def register_client_handler(self, handler: Callable[[StreamReader, StreamWriter], Awaitable[None]]) -> None:
104113
"""
105-
Registers the given function as a message handler. The given function must have the following signature::
114+
Registers the given function as a client handler. The given function must have the following signature::
106115
107-
def handler(state: SimulaQronState, message: str, connection: NetQASMConnection) -> ServingStatus:
108-
return ServingStatus.CONTINUE
116+
async def handler(reader: StreamReader, writer: StreamWriter):
117+
reader = await reader.read(255)
118+
...
119+
# Handle a new connection here
120+
# E.g. send a response to the client
121+
writer.write("answer".encode("utf-8"))
109122
110-
The passed function will be called once a message arrives form the remote.
111-
The function must return a value to signal the server loop to keep handling or not.
112-
Any other value that must be returned to the remote, must be passed using the ``add_return_values`` method
113-
from the ``SimulaQronState`` object passed to the handler.
123+
The passed function will be called once a new client connects.
114124
115-
:param handler: The function to be used as a handler.
116-
:type handler: Callable[[SimulaQronState, str], ServingStatus]
125+
:param handler: The function to be used as a handler. This *must* be a python coroutine
126+
(python "async" function).
127+
:type handler: Callable[[StreamReader, StreamWriter], Awaitable[None]]
117128
"""
118-
self._message_handlers.append(handler)
129+
self._connection_handler = handler
130+
131+
async def _build_server(self):
132+
if self._connection_handler is None:
133+
print("No connection handler - Did you forget to register it?")
134+
return
135+
server = await asyncio.start_server(self._connection_handler, self._sockets_data.hostname, self._sockets_data.port)
136+
print(f"BOB INFO: === {self._node_name} Server ===")
137+
print(f"BOB DEBUG: Listening on {self._sockets_data.hostname}:{self._sockets_data.port}")
138+
async with server:
139+
await server.serve_forever()
119140

120141
def start_serving(self) -> None:
121142
"""
122143
Starts the serving the clients using the registered handlers.
123144
"""
124-
# TODO - Implement
125-
pass
145+
asyncio.run(self._build_server())

0 commit comments

Comments
 (0)