Skip to content

Commit

Permalink
Created UDP sockets (#36)
Browse files Browse the repository at this point in the history
* Created UDP Sockets

* Addressed Comments

* Added send() function and override recv() function in client_socket.py, addressed minor changes in socket_wrapper.py

* Made requested changes

* Updated docstrings
  • Loading branch information
HermanG05 authored Jul 2, 2024
1 parent 781edf0 commit 157846f
Show file tree
Hide file tree
Showing 3 changed files with 329 additions and 0 deletions.
119 changes: 119 additions & 0 deletions network/modules/UDP/client_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import socket
from network.modules.UDP.socket_wrapper import UdpSocket


class UdpClientSocket(UdpSocket):
"""
Wrapper for client socket operations
"""

__create_key = object()

def __init__(
self,
class_private_create_key: object,
socket_instance: socket.socket,
server_address: tuple,
) -> None:
"""
Private Constructor, use create() method.
"""
assert class_private_create_key is UdpClientSocket.__create_key
self.__socket = socket_instance
self.server_address = server_address

@classmethod
def create(
cls, host: str = "localhost", port: int = 5000, connection_timeout: float = 10.0
) -> "tuple[bool, UdpClientSocket | None]":
"""
Initializes UDP client socket with the appropriate server address.
Parameters
----------
host: str, default "localhost"
The hostname of the server.
port: int, default 5000
The port number of the server.
connection_timeout: float, default 10.0
Timeout for establishing connection, in seconds
Returns
-------
tuple[bool, UdpClientSocket | None]
The boolean value represents whether the initialization was successful or not.
- If it is not successful, the second parameter will be None.
- If it is successful, the method will return True and a UdpClientSocket object will be created.
"""

if connection_timeout <= 0:
print("Must provide positive non-zero value.")
return False, None

try:
socket_instance = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
socket_instance.settimeout(connection_timeout)
server_address = (host, port)
return True, UdpClientSocket(cls.__create_key, socket_instance, server_address)

except TimeoutError as e:
print(f"Connection timed out: {e}")

except socket.gaierror as e:
print(
f"Could not connect to socket, address related error: {e}. Make sure the host and port are correct."
)

except socket.error as e:
print(f"Could not connect: {e}")

return False, None

def send(self, data: bytes) -> bool:
"""
Sends data to the specified server address
Parameters
----------
data: bytes
Takes in raw data that we wish to send
Returns
-------
bool: True if data is sent successfully, and false if it fails to send
"""

try:
host, port = self.server_address
super().send_to(data, host, port)
except socket.error as e:
print(f"Could not send data: {e}")
return False

return True

def recv(self, buf_size: int) -> None:

"""
Receive data method override to prevent client sockets from receiving data.
Parameters
----------
bufsize: int
The maximum amount of data to be received at once.
Raises
------
NotImplementedError
Always raised because client sockets should not receive data.
"""

raise NotImplementedError("Client sockets cannot receive data as they are not bound by a port.")




68 changes: 68 additions & 0 deletions network/modules/UDP/server_socket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import socket
from network.modules.UDP.socket_wrapper import UdpSocket


class UdpServerSocket(UdpSocket):
"""
Wrapper for server socket operations.
"""

__create_key = object()

def __init__(
self,
class_private_create_key: object,
socket_instance: socket.socket,
server_address: tuple,
) -> None:
"""
Private Constructor, use create() method.
"""
assert class_private_create_key is UdpServerSocket.__create_key, "Use create() method"
super().__init__(socket_instance=socket_instance)
self.__socket = socket_instance
self.server_address = server_address

@classmethod
def create(cls, host: str = "", port: int = 5000, connection_timeout: float = 10.0) -> "tuple[bool, UdpServerSocket | None]":
"""
Creates a UDP server socket bound to the provided host and port.
Parameters
----------
host: str (default "")
The hostname or IP address to bind the socket to.
port: int (default 5000)
The port number to bind the socket to.
connection_timeout: float (default 10.0)
Timeout for establishing connection, in seconds
Returns
-------
tuple[bool, UdpServerSocket | None]
The first parameter represents if the socket creation is successful.
- If it is not successful, the second parameter will be None.
- If it is successful, the second parameter will be the created
UdpServerSocket object.
"""

if connection_timeout <= 0:
print(f"Must provide a positive non-zero value.")
return False, None

try:
socket_instance = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
socket_instance.settimeout(connection_timeout)
server_address = (host, port)
socket_instance.bind(server_address)
return True, UdpServerSocket(cls.__create_key, socket_instance, server_address)

except TimeoutError as e:
print(f"Connection timed out.")
except socket.error as e:
print(f"Could not create socket, error: {e}.")
return False, None


142 changes: 142 additions & 0 deletions network/modules/UDP/socket_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import socket
import struct

CHUNK_SIZE = 4096

class UdpSocket:
"""
Wrapper for Python's socket module.
"""



def __init__(self, socket_instance: socket.socket = None) -> None:
"""
Parameters
----------
instance: socket.socket
For initializing Socket with an existing socket object.
"""

if socket_instance is None:
self.__socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.__socket.settimeout(10.0)
else:
self.__socket = socket_instance

@classmethod
def send_to(self, data: bytes, host: str = "", port: int = 5000) -> bool:
"""
Sends data to specified address
Parameters
----------
data: bytes
host: str (default "")
Empty string is interpreted as '0.0.0.0' (IPv4) or '::' (IPv6), which is an open address
port: int (default 5000)
The host, combined with the port, will form the address as a tuple
Returns
-------
bool: if data was transferred successfully
"""
address = (host, port)

data_sent = 0
data_size = len(data)

while data_sent < data_size:

if data_sent + CHUNK_SIZE > data_size:
chunk = data[data_sent:data_size]
else:
chunk = data[data_sent:data_sent+CHUNK_SIZE]

try:
self.__socket.sendto(chunk, address)
data_sent += len(chunk)
except socket.error as e:
print(f"Could not send data: {e}")
return False

return True

def recv(self, buf_size: int) -> "tuple[bool, bytes | None]":
"""
Parameters
----------
buf_size: int
The number of bytes to receive
Returns
-------
tuple:
bool - True if data was received and unpacked successfully, False otherwise
bytes | None - The received data, or None if unsuccessful
"""
data = b''
addr = None
data_size = 0

while data_size < buf_size:

try:
packet, current_addr = self.__socket.recvfrom(buf_size)
if addr is None:
addr = current_addr
elif addr != current_addr:
print(f"Data received from multiple addresses: {addr} and {current_addr}")
packet = b''

# Add the received packet to the accumulated data and increment the size accordingly
data += packet
data_size += len(packet)


except socket.error as e:
print(f"Could not receive data: {e}")
return False, None


return True, data


def close(self) -> bool:
"""
Closes the socket object. All future operations on the socket object will fail.
Returns
-------
bool: If the socket was closed successfully.
"""
try:
self.__socket.close()
except socket.error as e:
print(f"Could not close socket: {e}")
return False

return False

def address(self) -> "tuple[str, int]":
"""
Retrieves the address that the socket is listening on.
Returns
-------
tuple[str, int]
The address in the format (ip address, port).
"""
return self.__socket.getsockname()

def get_socket(self) -> socket.socket:
"""
Getter for the underlying socket objet.
"""
return self.__socket

0 comments on commit 157846f

Please sign in to comment.