4.4. Socket

4.4.1. socket

../../_images/socket-flow.jpg

Figure 4.50. Socket Flow

4.4.2. Protocols

  • IPv4 - socket.AF_INET

  • IPv6 - socket.AF_INET6

  • UDP - socket.SOCK_DGRAM

  • TCP - socket.SOCK_STREAM

  • Broadcast - socket.SO_BROADCAST

4.4.3. API

  • The bufsize argument of 1024 used above is the maximum amount of data to be received at once

  • accept(), connect(), send(), and recv() are blocking

  • Blocking calls have to wait on system calls (I/O) to complete before they can return a value

Table 4.23. API

Method

Description

socket()

bind()

listen()

accept()

connect()

connect_ex()

send()

recv()

close()

4.4.4. TCP

Server:

Code 4.91. socket TCP Server
import socket


SERVER_HOST = '127.0.0.1'
SERVER_PORT = 1337


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:

    print(f'Listening on {SERVER_HOST}:{SERVER_PORT}/TCP...')
    sock.bind((SERVER_HOST, SERVER_PORT))
    sock.listen(1)

    while True:
        conn, addr = sock.accept()

        received = conn.recv(1024).decode()
        print(f'From: {addr}, received: "{received}"')

        response = 'Thanks'
        conn.sendall(response.encode())

        if not received:
            print(f'Client {addr} disconnected.')
            conn.close()

Client:

Code 4.92. socket TCP Client
import socket


SERVER_HOST = '127.0.0.1'
SERVER_PORT = 1337


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:

    print(f'Connecting to {SERVER_HOST}:{SERVER_PORT}/TCP')
    sock.connect((SERVER_HOST, SERVER_PORT))

    payload = 'Hello World'
    sock.sendall(payload.encode())

    data = sock.recv(1024).decode()
    print(f'Received: "{data}"')

4.4.5. UDP

Server:

Code 4.93. socket UDP Server
import socket


SERVER_HOST = '127.0.0.1'
SERVER_PORT = 1337


with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:

    print(f'Listening on {SERVER_HOST}:{SERVER_PORT}/UDP...')
    sock.bind((SERVER_HOST, SERVER_PORT))

    while True:
        received, addr = sock.recvfrom(1024)
        received = received.decode()

        print(f'From: {addr}, received: "{received}"')

Client:

Code 4.94. socket UDP Client
import socket


SERVER_HOST = '127.0.0.1'
SERVER_PORT = 1337


with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:

    print(f'Connecting to {SERVER_HOST}:{SERVER_PORT}/UDP')
    sock.connect((SERVER_HOST, SERVER_PORT))

    payload = 'Hello World'
    sock.sendall(payload.encode())

    received = sock.recv(1024).decode()
    print(f'Received: {received}')

4.4.6. Multicast

Code 4.95. socket Multicast Client
import socket


def send(data, port=50000, addr='239.192.1.100'):
    """send(data[, port[, addr]]) - multicasts a UDP datagram."""
    # Create the socket
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    # Make the socket multicast-aware, and set TTL.
    s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 20)  # Change TTL (=20) to suit
    # Send the data
    s.sendto(data, (addr, port))


def recv(port=50000, addr="239.192.1.100", buf_size=1024):
    """recv([port[, addr[,buf_size]]]) - waits for a datagram and returns the data."""

    # Create the socket
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # Set some options to make it multicast-friendly
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    try:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
    except AttributeError:
        pass  # Some systems don't support SO_REUSEPORT
    s.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, 20)
    s.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, 1)

    # Bind to the port
    s.bind(('', port))

    # Set some more multicast options
    iface = socket.gethostbyname(socket.gethostname())
    s.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_IF, socket.inet_aton(iface))
    s.setsockopt(socket.SOL_IP, socket.IP_ADD_MEMBERSHIP, socket.inet_aton(addr) + socket.inet_aton(iface))

    # Receive the data, then unregister multicast receive membership, then close the port
    data, sender_addr = s.recvfrom(buf_size)
    s.setsockopt(socket.SOL_IP, socket.IP_DROP_MEMBERSHIP, socket.inet_aton(addr) + socket.inet_aton('0.0.0.0'))
    s.close()
    return data

4.4.7. socketserver

4.4.8. TCP

Server:

Code 4.96. socketserver TCP Server
from socketserver import BaseRequestHandler, TCPServer


SERVER_HOST = '127.0.0.1'
SERVER_PORT = 1337


class RequestHandler(BaseRequestHandler):
    def handle(self):
        received = self.request.recv(1024).decode()
        print(f'From: {self.client_address}/TCP, received: "{received}"')

        response = 'Thanks'
        self.request.sendall(response.encode())


with TCPServer((SERVER_HOST, SERVER_PORT), RequestHandler) as server:
    print(f'Accepting connections on {SERVER_HOST}:{SERVER_PORT}/TCP...')
    print(f'To stop the server use `Ctrl-C`\n')
    server.serve_forever()

Client:

Code 4.97. socket TCP Client
import socket


SERVER_HOST = '127.0.0.1'
SERVER_PORT = 1337


with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:

    print(f'Connecting to {SERVER_HOST}:{SERVER_PORT}/TCP')
    sock.connect((SERVER_HOST, SERVER_PORT))

    payload = 'Hello World'
    sock.sendall(payload.encode())

    data = sock.recv(1024).decode()
    print(f'Received: "{data}"')

4.4.9. Asynchronous

Threaded:
  • socketserver.ThreadingTCPServer

  • socketserver.ThreadingUDPServer

Code 4.98. socketserver TCP Threaded Client
import socket
import threading
from socketserver import ThreadingTCPServer, BaseRequestHandler


SERVER_HOST = '127.0.0.1'
SERVER_PORT = 1337


class RequestHandler(BaseRequestHandler):
    def handle(self):
        received = self.request.recv(1024).decode()
        print(f'From: {self.client_address}/TCP, received: "{received}"')

        response = 'Thanks'
        self.request.sendto(response.encode(), self.client_address)


def send_message(server_host, server_port, message):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((server_host, server_port))
        sock.sendall(message.encode())
        response = sock.recv(1024).decode()
        print(f'Received: {response}')


with ThreadingTCPServer((SERVER_HOST, SERVER_PORT), RequestHandler) as server:
    # Start a thread with the server -- that thread will then start one more thread for each request
    server_thread = threading.Thread(target=server.serve_forever)

    # Exit the server thread when the main thread terminates
    server_thread.daemon = True
    server_thread.start()
    print(f'Server loop running in thread: {server_thread.name}')

    send_message(SERVER_HOST, SERVER_PORT, 'Hello World 1')
    send_message(SERVER_HOST, SERVER_PORT, 'Hello World 2')
    send_message(SERVER_HOST, SERVER_PORT, 'Hello World 3')

    server.shutdown()

Forking:

  • socketserver.ForkingTCPServer

  • socketserver.ForkingUDPServer

Code 4.99. socketserver TCP Forking Client

4.4.10. References

4.4.11. Assignments

4.4.11.1. Heartbeat

  • Assignment: Heartbeat

  • Complexity: medium

  • Lines of code: 20 lines

  • Time: 21 min

English:
  1. Create a Heart Beat client and server

  2. Both the client and the server are to be run in threads

  3. The server is to accept UDP/IPv4 messages on port 1337

  4. Communication is to be done using the JSON protocol

  5. The client is to have information about its IP and PORT

  6. The client is to send information to the server every 5 seconds about its IP and PORT

  7. Print:

    1. UTC date of packet arrival,

    2. IP and PORT sent by the client.

  8. Run doctests - all must succeed

Polish:
  1. Stwórz klienta i serwer Heart Beat

  2. Zarówno klient jak i serwer ma być uruchamiany w wątkach

  3. Serwer ma przyjmować komunikaty UDP/IPv4 na porcie 1337

  4. Komunikacja ma odbywać się za pomocą protokołu JSON

  5. Klient ma mieć informację o swoim adresie IP i PORT

  6. Klient ma co 5 sekund wysyłać informację do serwera o swoim IP i PORT

  7. Wypisz:

    1. datę UTC przyjścia pakietu,

    2. IP i PORT przesłany przez klienta.

  8. Uruchom doctesty - wszystkie muszą się powieść

Hints:
  • threading.Timer(frequency: int, fn: Callable).start()

  • socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

  • socketserver.ThreadingUDPServer

4.4.11.2. Backdoor

  • Assignment: Backdoor

  • Complexity: medium

  • Lines of code: 150 lines

  • Time: 34 min

English:
  1. Create a TCP server running in a thread

  2. The server is to be run on a random port in the range 1025-65535 (why this range of ports?)

  3. Extract information about the IP address and PORT on which the server is listening

  4. The server expects messages in JSON format:

    1. date: datetime (UTC),

    2. command: str,

    3. timeout: int.

  5. The server executes the command saved in command in the operating system, taking into account timeout

  6. Send the sender JSON with the result of the command execution, i.e.:

    1. date: datetime (UTC),

    2. host: str,

    3. port: int,

    4. stdout: str,

    5. stderr: str,

    6. exit_code: int

  7. Run doctests - all must succeed

Polish:
  1. Stwórz uruchamiany w wątku serwer TCP

  2. Serwer ma być uruchamiany na losowym porcie z przedziału 1025-65535 (dlaczego taki zakres portów?)

  3. Wyciągnij informację o adresie IP i PORT na którym nasłuchuje serwer

  4. Serwer oczekuje na komunikaty w formacie JSON:

    1. date: datetime (UTC),

    2. command: str,

    3. timeout: int.

  5. Serwer wykonuje polecenie zapisane w command w systemie operacyjnym uwzględniając timeout

  6. Prześlij nadawcy JSON z wynikiem wykonania polecenia, tj.:

    1. date: datetime (UTC),

    2. host: str,

    3. port: int,

    4. stdout: str,

    5. stderr: str,

    6. exit_code: int

  7. Uruchom doctesty - wszystkie muszą się powieść

Hints:
  • random.randint()

  • socket.socket(socket.AF_INET, socket.SOCK_STREAM)

  • socketserver.ThreadingTCPServer

  • subprocess.run(cmd: str, timeout: int, shell: bool = True)

  • json.dumps(obj: Any)

  • json.loads(s: str)