Client/Server Interactions ========================== With the socket module in Python, we can define client/server interactions. Computer Networks ----------------- We classify types of connections based on the transmission media and the network topologies. The unit for transmission speed is ``bps``, short for *bits per second*. We distinguish four categories of media used for data transmission: * **twisted pair wire:** pair of copper wires used for telephone communications. Transmission speeds vary from 2,400 to 33,600 ``bps``. * **coaxial cable:** for many telephone lines or television Data transmission over short distances guarantees speeds up to :math:`10^7` ``bps``. * **optical fibre:** signals transmitted via light-emitting diodes encode bits as presence or absence of light. Transmission speeds reach up to :math:`10^9` ``bps``. * **electromagnetic waves:** wireless over short distances or via satellites across long distances. Three of the most common regular topologies are shown in :numref:`fignetworktopologies`. .. _fignetworktopologies: .. figure:: ./fignetworktopologies.png :align: center A star, ring, and bus network. In a *star network*, there is one central node. All other nodes are connected to this central node. In a *ring network*, the nodes from a closed circuit, message circle around the ring of nodes. In a *bus network*, all nodes are connect on a single bus, as used in the Von Neumann architecture. Client/server architecture defines the communication between two computers: one is the server and the other acts as the client. A client places a request or order to a server. The server processes the request. The client does not need to know how the server processes the requests. We distinguish between software and hardware client/server architectures: * web and database servers offer software services; * file and print servers offer hardware services. The four Layers in Internet Communication are application, transport, network, and link. * The *application layer* consists of client and server software. The application prepares the message and defines the destination address. * The *transport layer* formats the message by chopping it into packets attaching a sequence number and destination address to each packet. When receiving, packets are collected and reassembled into a message. * The *network layer* determines the (intermediate) address for each packet. This layer detects when a packet has reached its final destination. * The *link layer* is responsible for the actual transmission of packets. The workings of the layers are illustrated in :numref:`fignetworklayers`, as we follow one message from source to destination. .. _fignetworklayers: .. figure:: ./fignetworklayers.png :align: center Following a message from source to destination. Network protocols are rules for network communication. We consider two types of protocols: * **TCP** Transmission Control Protocol First a message is sent that data is coming. Only after the receiver acknowledges this message will the sender send the data. All successful transmissions are confirmed, and retransmissions are acknowledged. * **UDP** User Datagram Protocol Unlike TCP, no connection is established prior to sending data. The sender just carries on after sending a message. TCP is connection oriented, UDP is connectionless. TCP is more reliable whereas UDP is more streamlined. Sockets are objects programs use to connect. Sockets were introduced in 1981 in BSD Unix. Originally used for communication between processes, i.e.: between two programs on same computer. Sockets support communication across platforms, independent of the operating system. In addition to the IP address of the computer, both server and client must use the same *port*. A port is a 16-bit integer, some are reserved for particular protocols. Any port between 1,024 and 65,535 is free. Sockets support both TCP and UDP. Defining a Simple Client/Server Interaction ------------------------------------------- A simple client/server interaction is defined by the scripts ``tcp_server.py`` and ``tcp_client.py`` which respectively define the actions of the server and the client. Both scripts runs in separate terminal windows. First we start ``tcp_server.py``, as shown in :numref:`figtcpserver0`. .. _figtcpserver0: .. figure:: ./figtcpserver0.png :align: center Server waiting for a connection from a client. The client ``tcp_client.py`` runs in another terminal, shown in :numref:`figtcpclient0`. .. _figtcpclient0: .. figure:: ./figtcpclient0.png :align: center The client makes the connect to the server. As soon as we start the script ``tcp_client.py`` we notice a change in the terminal window where the server script runs, shown in :numref:`figtcpserver1`. .. _figtcpserver1: .. figure:: ./figtcpserver1.png :align: center Server and client are connected. In this simple client/server interaction, a string will be transmitted from the client to the server. The user of the client script ``tcp_client.py`` types in the message, as shown in :numref:`figtcpclient1`. .. _figtcpclient1: .. figure:: ./figtcpclient1.png :align: center The users enter a message prompted by the client script. Then the server script confirms the message, as shown in :numref:`figtcpserver2`. .. _figtcpserver2: .. figure:: ./figtcpserver2.png :align: center The server confirms the message received from the client. The socket module exports the class ``socket()`` which returns an object representing a socket. The first argument of ``socket`` is the Address Family (AF): * ``AF_UNIX`` : for UNIX sockets; * ``AF_INET`` : most commonly used for internet ``AF_INET`` supports both TCP and UDP, given respectively by ``SOCK_STREAM`` and ``SOCK_DGRAM`` as the second argument of ``socket()``. For example: :: from socket import socket as Socket from socket import AF_INET, SOCK_STREAM sock = Socket(AF_INET, SOCK_STREAM) The methods ``bind`` and ``connect`` are called to define connections between client and server. After a socket is created, *both server and client* define :: server_address = (hostname, number) To bind the socket to the address, *the server* does :: sock.bind(server_address) and *the client* contacts the server then via :: sock.connect(server_address) With the methods ``listen`` and ``accept``, requests are taken. With ``listen()`` the server indicates how many incoming connections will be accepted: :: sock.listen(2) # accept at most 2 connections The server takes requests via the ``accept()`` method: :: client, client_address = sock.accept() The ``accept()`` method returns 1. a socket ``client`` for receiving data; 2. the address of the client. The methods ``send`` and ``recv`` are used to send and receive data. The *client* sends data with ``send()``. :: sock.send(data) The *server* receives data applying ``recv()``. :: data = client.recv(buffer) to the socket ``client`` obtained with ``accept()``. When all is over, *both client and server* do :: sock.close() *What is sent must be of type bytes.* The methods ``encode()`` and ``decode()`` are applied to convert strings into bytes and bytes into strings. To avoid an error such as :: TypeError: a bytes-like object is required, not 'str' we have to work with ``encode()`` and ``decode()``, as illustrated in the session below. :: >>> n = 4 >>> b = str(n).encode() >>> b b'4' >>> type(b) >>> c = int(b.decode()) >>> c 4 Now we are ready to look at the definition of the actions of the server in ``tcp_server.py``. This script with the definition of the setup starts as follows: :: from socket import socket as Socket from socket import AF_INET, SOCK_STREAM HOSTNAME = '' # blank so any address can be used PORTNUMBER = 41267 # number for the port BUFFER = 80 # size of the buffer SERVER_ADDRESS = (HOSTNAME, PORTNUMBER) SERVER = Socket(AF_INET, SOCK_STREAM) SERVER.bind(SERVER_ADDRESS) SERVER.listen(2) Then the interaction with the client are defined in the continuation of the script ``tcp_server.py`` listed below: :: print('server waits for connection') CLIENT, CLIENT_ADDRESS = SERVER.accept() print('server accepted connection request from ',\ CLIENT_ADDRESS) print('server waits for data') DATA = CLIENT.recv(BUFFER).decode() print('server received \"%s\"' % DATA) SERVER.close() The script ``tcp_client.py`` is shorter than ``tcp_server.py``. The client script is listed below. :: from socket import socket as Socket from socket import AF_INET, SOCK_STREAM HOSTNAME = 'localhost' # on same host PORTNUMBER = 41267 # same port number BUFFER = 80 # size of the buffer SERVER_ADDRESS = (HOSTNAME, PORTNUMBER) CLIENT = Socket(AF_INET, SOCK_STREAM) CLIENT.connect(SERVER_ADDRESS) print('client is connected') DATA = input('Give message : ') CLIENT.send(DATA.encode()) Guessing a Secret ----------------- As an application, consider a little game of a player/dealer communication. The dealer generates a secret number. The player must guess the number. After each guess, the dealer sends feedback: ``too low``, ``too high``, or ``found the secret``. The game terminates when the secret is found. The player is the client, the dealer is the server The setup of the server (the dealer) starts as follows: :: from random import randint from socket import socket as Socket from socket import AF_INET, SOCK_STREAM HOSTNAME = '' # blank so any address can be used PORTNUMBER = 11267 # number for the port BUFFER = 80 # size of the buffer DEALER_ADDRESS = (HOSTNAME, PORTNUMBER) DEALER = Socket(AF_INET, SOCK_STREAM) DEALER.bind(DEALER_ADDRESS) DEALER.listen(1) print('dealer waits for player to connect') PLAYER, PLAYER_ADDRESS = DEALER.accept() print('dealer accepted connection request from ',\ PLAYER_ADDRESS) The script for the server continues below. :: SECRET = randint(0, 9) print('the secret is %d' % SECRET) while True: print('dealer waits for a guess') GUESS = PLAYER.recv(BUFFER).decode() print('dealer received ' + GUESS) if int(GUESS) < SECRET: REPLY = 'too low' elif int(GUESS) > SECRET: REPLY = 'too high' else: REPLY = 'found the secret' PLAYER.send(REPLY.encode()) if REPLY == 'found the secret': break DEALER.close() The code for the client (the player) starts with the setup of the communication. :: from socket import socket as Socket from socket import AF_INET, SOCK_STREAM HOSTNAME = 'localhost' # on same host PORTNUMBER = 11267 # same port number BUFFER = 80 # size of the buffer DEALER = (HOSTNAME, PORTNUMBER) PLAYER = Socket(AF_INET, SOCK_STREAM) PLAYER.connect(DEALER) The player is prompted for a guess until the secret number is found. The script for the player then continues below: :: print('player is ready to guess') while True: GUESS = input('Give number : ') PLAYER.send(GUESS.encode()) ANSWER = PLAYER.recv(BUFFER).decode() print('>', ANSWER) if ANSWER == 'found the secret': break PLAYER.close() Exercises --------- 1. Extend the client/server interaction to simulate a password dialogue. After receiving data from a client, the server returns ``access granted`` or ``access denied`` depending on whether the received data matches the password. 2. Describe how you would implement the game of rock, paper, and scissors in a client/server manner. 3. Redefine the mancala game of Project Two as a client/server interaction. 4. Write an implementation of the server in the previous exercise. 5. Write an implementation of the client in the previous exercise.