Underneath the FTP and email protocols, which allow interfaces to applications, lies a communications layer, the programs that actually send bytes between computers or between programs on the same computer. It is conducted very much like a conversation. One person, the client, initiates the conversation (“Hi there!”). The other (the server) responds (“Hello. Nice to see you.”). Now, it
is the client’s turn again. They take turns sending and accepting messages until one says “goodbye.” These messages might contain email, or FTP data, or TV programs. This layer does not care what the data is, its job is to deliver it.
Data are delivered in packets, each containing a certain amount. In order for the client to deliver the data, there must be a server willing to connect to it. The client needs to know the address of a server, just as an FTP address or email destination was required before, but now all that is needed is the host name and a port number. A port is really a logical construction, something akin to an element of a list. If two programs agree to share data by having one of them place it in location 50001 of a list and the other one read it from there, it gives an approximate idea of what a port is. Some port numbers are assigned and should not be used for anything else; FTP and email have assigned ports. Others are available for use, and any two processes can agree to use one.
A module named socket, based on the inter-process communication scheme on UNIX of the same name, is used with Python to send messages back and forth. To create an example, two computers should be used, one being the client and one the server, and the IP address of the server is required, too.
1. Example: A Server That Calculates Squares
The client opens a communications link (socket) to the server, which has a known IP address. The server engages in a short handshake (exchange of strings), and then expects to receive a number for the client. The client sends an integer, the server receives it, squares it, and sends back the answer. This simple exchange is really the basis for all communications between computers: one machine sends information, the other receives it, processes it, and returns a reply based on the data it received.
The client begins the conversation. It creates a connection, a socket, to the server using the socket() function of the socket module. Protocols must be specified, and the most common ones are used:
import socket
HOST = ’19*.***.*.***’ # The remote host
PORT = 50007 # The same port as used by the server
s = socket.socket(socket.AF INET, socket.SOCK STREAM)
s.connect((HOST, PORT))
Port 50007 is used because nothing else is using it. Now the client starts the conversation, just as it appears at the beginning of this section:
s.send(b’Hi there!’)
The send() function sends the message passed as a parameter. The string (as bytes) is transmitted to the server through the variable s, which represents the server. The client now waits for the confirmation string from the server, which should be “Hello. Nice to see you.” The client calls:
data = s.recv(1024)
which waits for a response from the server. This response is 1024 bytes long at most, and it waits only for a short time, at which point it gives up and an error is reported. When this client gets the response, it proceeds to send numbers to the server. They are converted into the bytes type before transmission. In this example, it simply loops through 100 consecutive integers:
for i in range (0, 100):
data = str(i).encode()
s.send (data)
After sending to the server, it waits for the answer. Actually, that’s a part of the receive function:
data = s.recv(1024)
after 100 integers, the loop ends and the connection is closed:
s.close()
The server is always listening. It creates a socket on a particular port so that the operating system knows something is possible there, but because the server cannot predict when a client will connect or what client it will be it simply listens for a connection, by calling a function named listen():
import socket
from random import *
HOST = ” # A null string is correct here.
PORT = 50007
s = socket.socket(socket.AF INET, socket.SOCK STREAM)
s.bind ((HOST, PORT))
s.listen()
AF_INET and SOCK_STREAM are constants that tell the system which protocols are being used. These are the most common, but see the documentation for others. The bind() and the listen() functions are new. Associating this connection with a specific port is done using bind(). The tuple (HOST, PORT) says to connect this host to this port. The empty string for HOST implies this computer. The listen() call starts the server process, this program, accepting connections when asked. A process connecting on the port that was specified in bind() will now result in this process, the server, being notified. When a connection request occurs, the server must accept it before doing any input or output:
conn, addr = s.accept()
In the tuple (conn, addr) that is returned, conn represents the connection, like a file descriptor returned from open(), and it is used to send and receive data. addr is the address of the sender, the client, and it is a string. If the addr were printed,
print (“Connected to “, addr)
it would look like an IP address:
Connected to 423.121.12.211
Now, the server can receive data across the connection, and does so by calling recv():
data = conn.recv(1024)
print (“Server heard ‘”, data, “‘”)
The parameter 1024 specifies the size of the buffer, or the maximum number of bytes that can be received in one call. The variable data is of type bytes, just as the parameter to send() was in the client. The client was the first to send, and it sent the message “Hi there!” That should be the value of data now, if it has been received properly. The response from the server should be “Hello, nice to see you.”
conn.send (b’Hello. Nice to see you.’)
The same connection is used for sending and receiving.
Now the real data gets exchanged. The server accepts integers, sent as bytes. It squares them and transmits the answer back.
while True:
data = conn.recv(1024) # Read the incoming data
if data:
i = int(data) # Convert it to integer
print (“Received “, i)
data = str(i*i).encode() # Square it and convert
# to bytes
conn.send (data) # Send to the client
The server can tell when the connection is closed by the client, but it is also polite to say “goodbye” somehow, perhaps by sending a particular code. If the loop ever terminates, the server should close the connection:
conn.close()
This is a pretty good example of a data exchange and a contract, because there are specified requirements for each side of this conversation that will result in success if done correctly and failure if messed up. Failure is sometimes indicated by an error message, often a timeout, where the client or server was expecting something that never arrived. In other cases, failure is not formally indicated at all; the program simply “hangs” there and does nothing. If at any time, both processes are trying to receive data, then the program will fail.
Figure 13.2 shows the communication between the client and the server as a diagram. If the client and the server are at any time both trying to accept data from the connection, then the program will fail. In the diagram, all data trans-fers are transmit-accept pairs between the two processes and as read-write pairs within the server and write-read pairs within the client.
The FTP protocol can now be seen as a socket connection, wherein the client sends strings (commands) to the server, which parses them, carries out the request, and then sends an acknowledgement back.
Source: Parker James R. (2021), Python: An Introduction to Programming, Mercury Learning and Information; Second edition.