Skip to content

Commit

Permalink
Implement Client and Server
Browse files Browse the repository at this point in the history
  • Loading branch information
realshouzy committed May 15, 2024
1 parent c99e472 commit ea87049
Show file tree
Hide file tree
Showing 4 changed files with 489 additions and 5 deletions.
4 changes: 3 additions & 1 deletion nrw/network/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

from __future__ import annotations

__all__: Final[tuple[str]] = ("Connection",)
__all__: Final[tuple[str, str, str]] = ("Connection", "Client", "Server")

from typing import Final

from nrw.network._client import Client
from nrw.network._connection import Connection
from nrw.network._server import Server
17 changes: 13 additions & 4 deletions nrw/network/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# pylint: skip-file
__all__: Final[tuple[str]] = ("Connection",)
__all__: Final[tuple[str, str, str]] = ("Connection", "Client", "Server")

from abc import ABC, abstractmethod
from typing import Final

class Connection:
__slots__: Final[tuple[str, str, str]] = ("_socket", "_to_server", "_from_server")

def __init__(self, server_ip: str, server_port: int) -> None: ...
def receive(self) -> str | None: ...
def send(self, message: str) -> None: ...
def close(self) -> None: ...

class Client(ABC):
__slots__: Final[tuple[str, str]] = ("_socket_wrapper", "_active")
def __init__(self, server_ip: str, server_port: int) -> None: ...
@property
def is_connected(self) -> bool: ...
Expand All @@ -22,6 +22,11 @@ class Client(ABC):
def process_message(self, message: str) -> None: ...

class Server(ABC):
__slots__: Final[tuple[str, str, str]] = (
"_connection_handler",
"_message_handlers",
"_lock",
)
def __init__(self, port: int) -> None: ...
@property
def is_open(self) -> bool: ...
Expand All @@ -35,9 +40,13 @@ class Server(ABC):
@abstractmethod
def process_message(
self,
client_ip: str,
client_ip: str | None,
client_port: int,
message: str,
) -> None: ...
@abstractmethod
def process_closing_connection(self, client_ip: str, client_port: int) -> None: ...
def process_closing_connection(
self,
client_ip: str | None,
client_port: int,
) -> None: ...
126 changes: 126 additions & 0 deletions nrw/network/_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Klasse `Client`."""

from __future__ import annotations

__all__: Final[tuple[str]] = ("Client",)

import socket
import threading
from abc import ABC, abstractmethod
from contextlib import suppress
from typing import TYPE_CHECKING, Final

if TYPE_CHECKING:
from io import TextIOWrapper


class _SocketWrapper:
__slots__: Final[tuple[str, str, str]] = ("_socket", "_to_server", "_from_server")

def __init__(self, server_ip: str, server_port: int) -> None:
try:
self._socket: socket.socket | None = socket.socket(
socket.AF_INET,
socket.SOCK_STREAM,
)
self._socket.connect((server_ip, server_port))
self._to_server: TextIOWrapper | None = self._socket.makefile(
mode="w",
encoding="utf-8",
)
self._from_server: TextIOWrapper | None = self._socket.makefile(
mode="r",
encoding="utf-8",
)
except OSError:
self._socket = None
self._to_server = None
self._from_server = None

def receive(self) -> str | None:
if self._from_server is not None:
with suppress(OSError, ValueError):
received_line: str = self._from_server.readline().strip()
return received_line if received_line else None
return None

def send(self, message: str) -> None:
if self._to_server is not None:
with suppress(OSError, ValueError):
self._to_server.write(f"{message}\n")
self._to_server.flush()

def close(self) -> None:
if self._socket is not None:
with suppress(OSError):
self._to_server.close()
self._from_server.close()
self._socket.shutdown(socket.SHUT_RDWR)
self._socket.close()


class Client(ABC):
"""Objekte von Unterklassen der abstrakten Klasse `Client` ermöglichen
Netzwerkverbindungen zu einem Server mittels TCP/IP-Protokoll.
Nach Verbindungsaufbau können Zeichenketten (`Strings`) zum Server gesendet und von
diesem empfangen werden, wobei der Nachrichtenempfang nebenläufig geschieht.
Zur Vereinfachung finden Nachrichtenversand und -empfang zeilenweise statt, d. h.,
beim Senden einer Zeichenkette wird ein Zeilentrenner ergänzt und
beim Empfang wird dieser entfernt. Jede empfangene Nachricht wird einer
Ereignisbehandlungsmethode übergeben, die in Unterklassen implementiert werden muss.
Es findet nur eine rudimentäre Fehlerbehandlung statt, so dass z. B.
Verbindungsabbrüche nicht zu einem Programmabbruch führen.
Eine einmal unterbrochene oder getrennte Verbindung kann nicht reaktiviert werden.
"""

__slots__: Final[tuple[str, str]] = ("_socket_wrapper", "_active")

def __init__(self, server_ip: str, server_port: int) -> None:
self._socket_wrapper: _SocketWrapper = _SocketWrapper(server_ip, server_port)
if self._socket_wrapper._socket is not None:
self._active: bool = True
else:
self._active = False
thread = threading.Thread(target=self._run)
thread.start()

def _run(self) -> None:
message: str | None = None
while self._active:
message = self._socket_wrapper.receive()
if message is not None:
self.process_message(message)
else:
self.close()

def send(self, message: str) -> None:
"""Die Nachricht `message` wird - um einen Zeilentrenner ergänzt - an den Server
gesendet. Schlägt der Versand fehl, geschieht nichts.
"""
if self._active:
self._socket_wrapper.send(message)

def close(self) -> None:
"""Die Verbindung zum Server wird getrennt und der Client kann nicht mehr
verwendet werden. Ist `Client` bereits vor Aufruf der Methode in diesem Zustand,
geschieht nichts.
"""
if self._active:
self._active = False
self._socket_wrapper.close()

@property
def is_connected(self) -> bool:
"""Die Anfrage liefert den Wert `True`, wenn der Client mit dem Server
aktuell verbunden ist. Ansonsten liefert sie den Wert `False`.
"""
return self._active

@abstractmethod
def process_message(self, message: str) -> None:
"""Diese Methode wird aufgerufen, wenn der Client die Nachricht `message` vom
Server empfangen hat. Der vom Server ergänzte Zeilentrenner wurde zuvor
entfernt. Die Methode ist abstrakt und muss in einer Unterklasse der Klasse
`Client` überschrieben werden, so dass auf den Empfang der Nachricht reagiert
wird. Der Aufruf der Methode erfolgt nicht synchronisiert.
"""
Loading

0 comments on commit ea87049

Please sign in to comment.