Posted in Better Programming, Python, Python Libraries

How to create a simple Python TCP/IP Server and Client?

Before we begin, let’s start with some basics.

Inter Process Communication (IPC)

IPC is a communication mechanism that an Operating System offers for processes to communicate with each other. There are various types of IPCs such as:

  • Pipes
  • Sockets
  • Files
  • Signals
  • Shared Memory
  • Message Queues/ Message Passing

Sockets

Sockets are used to send data over the network either to a different process on the same computer or to another computer on the network.

There are four types of sockets namely,

  • Stream Sockets
  • Datagram Sockets
  • Raw Sockets
  • Sequenced Packet Sockets

Stream sockets and datagram sockets are the two most popular choices.

Stream SocketsDatagram Sockets
Guaranteed deliveryNo delivery guarantees
Uses TCP (Transmission Control Protocol)Used UDP (User Datagram Protocol)
Needs an open connectionDon’t need to have an open connection

How are sockets used in Distributed Systems?

Distributed Systems are built using the concept of Client Service architectures.

  • Clients send requests to servers
  • Servers send back responses or error codes accordingly

The communication across servers and clients in a distributed system uses sockets as a popular form of IPC. Sockets are nothing but a combination of

  • IP Address. Ex: localhost
  • Port number. Ex: 80

Each machine (with an IP address) has several applications running on it. We need to know on which port an application is running in to send requests to it.

What is TCP/IP?

We will go into the details of communication protocols in a different article and stick to the basics for today. TCP stands for Transmission Control Protocol, a communications protocol for computers to exchange information over a network.

IP stands for Internet Protocol. IP identifies the IP address of the applications or devices to send data to and forms the Network Layer in the OSI stack. TCP defines how to transport the data over the network. Ensuring delivery guarantee is still TCP’s job.

When we send an HTTP request to a server, we first establish a TCP connection, so HTTP sits on top of TCP as the transport layer. When a user types a URL into the browser, the browser sets up a TCP socket using the IP address and port number and starts sending data to that socket. This request is sent as bytes in the form of data packets over the network. The server will then respond to the request. The benefits of a TCP connection is that a server sends acknowledgement of each packet based on which the client retransmits data in case some packets get dropped. Each packet has a sequence number that the server uses to assemble them upon receiving.

Now let’s look at an example Python program on how to write a simple script to setup a TCP/IP server and client.

Python TCP/IP server

import socket

# Set up a TCP/IP server
tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# Bind the socket to server address and port 81
server_address = ('localhost', 81)
tcp_socket.bind(server_address)

# Listen on port 81
tcp_socket.listen(1)

while True:
	print("Waiting for connection")
	connection, client = tcp_socket.accept()

	try:
		print("Connected to client IP: {}".format(client))
        
        # Receive and print data 32 bytes at a time, as long as the client is sending something
		while True:
			data = connection.recv(32)
			print("Received data: {}".format(data))

			if not data:
				break

	finally:
		connection.close()

Python TCP/IP Client

import socket

# Create a connection to the server application on port 81
tcp_socket = socket.create_connection(('localhost', 81))

try:
	data = str.encode(‘Hi. I am a TCP client sending data to the server’)
	tcp_socket.sendall(data)

finally:
	print("Closing socket")
	tcp_socket.close()

Terminal Output

Waiting for connection
Connected to client IP: ('127.0.0.1', 65483)
Received data: Hi. I am a TCP c
Received data: lient sending da
Received data: ta to the server
Received data:
Waiting for connection

Note:

To find and kill any applications running on a port.

List the processes running on port 81

sudo lsof -i:81

Get the PID number and kill the process

sudo kill -9 <PID>

Hope you enjoyed learning how to setup a simple TCP/IP server and client using Python.

Posted in Arrays and Strings, June Leetcoding Challenge, Python

5 Pythonic ways to reverse a string

Write a function that reverses a string. The input string is given as an array of characters char[].

Input: A = [“h”,”e”,”l”,”l”,”o”]
Output: [“o”,”l”,”l”,”e”,”h”]

Solution #1: Arrays ie., lists in Python are mutable data structures which means they can be modified in place without having to allocate extra space. For this problem, creating a new array using the input array is a trivial solution. So let’s directly dive into an O(1) memory solution.

In Python, we can refer to the elements in reverse order starting at the index -1.

A[-1] = “o”

A[-2] = “l”

A[-5] = “h”

This is quite helpful in manipulating array indexes. In order to reverse the elements, we don’t have to traverse the full list but swap the first half of the list with the second half starting from the end.

ie., swap A[i] with A[-(i+1)] for i = 0 to i = len(A) / 2

Time Complexity: We traverse only half the list while swapping elements with the second half of the list. So the time complexity will be O(N/2) which is O(N) overall.

Space Complexity: O(1)

Code for solution #1:

def reverseString(self, s: List[str]) -> None:
    """
    Do not return anything, modify s in-place instead.
    """
    for i in range(len(s) // 2):
        s[i], s[-(i+1)] = s[-(i+1)], s[i]

Solution #2: A Python in-built function that does exactly what we did above is reverse(). The list is modified in place by swapping the first half of the list with the second half the same way as shown above. Read more about reverse() here.

Code for solution #2:

def reverseString(self, s: List[str]) -> None:
    s.reverse()

Time Complexity: O(N)

Space Complexity: O(1)

Solution #3: If we are allowed to create and return a new list, then we can use Python reversed() function as shown below. reversed() returns an iterator in Python, so we have to wrap its result in a list(). Read more about it here.

Code for solution #3:

def reverseString(self, s: List[str]) -> List[str]:
    return list(reversed(s))

Time Complexity: O(N)

Space Complexity: O(N) since we are creating a new list.

What if we are given a string and not an array of characters?

An important distinction here is that strings are immutable, so we cannot leverage any in-place reversals here. Let’s look at some Pythonic ways of reversing a string.

Input: “hello”

Output: “olleh”

Solution #4: Using Python’s elegant index slicing.

Code for solution #4:

def reverseString(self, s: str) -> str:
    return s[::-1]

Okay, what was that? In Python there is a concept called slicing. The syntax for slicing is shown below.

A[start: stop: step]

  • start – The index to start slicing (default: 0)
  • stop – The index to stop slicing (default: len(s))
  • step – The increment between each index for slicing

Examples:

  • A[0:1] slices the first element
  • A[1:4] slices the elements from index 1 to 3

The -1 step in the code indicates that we will start at the end and stop at the start instead of the other way around, thereby reversing the string.

Read more about Python slicing here.

Time Complexity: O(N)

Space Complexity: O(N) since a new string is created.

Solution #5: Another way to improve the readability of your code instead of using the above syntax which a non-python developer may not immediately be able to comprehend is to use reversed() to reverse a string.

  • Use reversed() to create an iterator of the string characters in the reverse order.
  • Use join to merge all the characters returned by the iterator in the reverse order.
def reverseString(self, s: str) -> str:
    return "".join(reversed(s))

Time Complexity: O(N)

Space Complexity: O(N) since a new string is created.

Let’s summarize all the different ways.

InputMethodTime ComplexitySpace Complexity
list[str]Swap elements from first half with second half of the array.O(N)O(1)
list[str]Built-in list.reverse()O(N)O(1)
list[str]list(reversed(s))O(N)O(N)
strSlicing str[: : -1]O(N)O(N)
str“”.join(reversed(str))O(N)O(N)
Comparison of string reversal methods

Hope you learnt many Pythonic ways to reverse a string or an array of chars[]. There are many non-Pythonic ways as well which we will discuss in a separate post. Have a great day!

Posted in Python, Python Libraries

[Python] Quick Intro To Argparse

The Argparse library is an elegant way to parse command line arguments in Python. At some point in your coding career, you might have done something like this.

import sys

if __name__ == '__main__':

    if len(sys.argv) < 3:
        print("Usage my_script.py <param1> <param2>")
        sys.exit(1)

The above code snippet checks whether a user ran the python script my_script.py with less than 3 command line arguments. If so, it prints an error message and exits the program. This is a perfectly fine way to handle command line arguments in Python. What is missing?

  • We need to explicitly check whether the user provided sufficient arguments
  • Check whether all the arguments are of the correct data type
  • Print usage/ help and error statements manually

The Argparse module in Python helps us write user-friendly command-line interfaces. One of the best aspects of it is the auto-generation of usage and help messages as well as error handling when insufficient arguments or arguments of the wrong data types are provided by the user. Let’s look at a simple example.

import argparse

if __name__ == "__main__":

    parser = argparse.ArgumentParser(description="Read a file and remove a given list of users")

    parser.add_argument(
        'file',
        type=str,
        help='Path to the input file')

    parser.add_argument(
        '-l',
        '--list',
        help='List of users to remove',
        type=str,
        required=True)

    args = parser.parse_args()
    file_path = args.file
    users = args.list

Now try running the above code snippet without any arguments. The code will throw an error as shown below.

$ python my_scipt.py
usage: my_script.py [-h] -l LIST file
my_script.py: error: the following arguments are required: file, -l/--list

Try using -h or –help to find out more about the usage of this file. You will see something like this. Beautiful, isn’t it?

$ python my_script.py -h
usage: my_script.py [-h] -l LIST file

Read a file and remove a given list of users

positional arguments:
  file                  Path to the input file

optional arguments:
  -h, --help            show this help message and exit
  -l LIST, --list LIST  List of users to remove

What this tells you is that the script should be run with two arguments.

  • file
  • – l “user1, user2, user3”

Let’s dive into how argparse works.

Create a parser

import argparse

parser = argparse.ArgumentParser(
             usage="Example usage: my_script.py [-h] -l 'user1, user2, user3' file_path"
             description="Read a file and remove a given list of users",
         )
  • Import argparse into your Python program.
  • argparse.ArgumentParser creates an ArgumentParser object. This parses the command line arguments into Python data types. A couple of useful parameters are description and usage fields.
    • description gives a brief summary of the functionality offered by the script.
    • usage overrides the default program usage printed on the terminal, with your message.

Add arguments to the parser

parser.add_argument(
    'file',
    type=str,
    help='Path to the input file')

parser.add_argument(
    '-l',
    '--list',
    help='List of users to remove',
    type=str,
    required=True)
  • You can see from the above output that argparse allows positional (file) and optional arguments (-l, –list, -h). The parameter required can be set to True to mark an optional argument as required. In the above example -l or –list which refers to the list of users to be removed is a required option from the user. Add argument also allows the usage of shorthand arguments like -l, -h similar to what we see in bash scripts.
  • Only optional arguments can be omitted. Positional arguments are compulsory and do not allow the setting of required parameter.
  • type refers to the Python type to which the argument should be converted. In the above example we are reading the contents that follow -l as a Python string. We can process this string later to get the individual users.
  • help gives a brief description of the argument.

Parse the arguments

args = parser.parse_args()
file_path = args.file
users = args.list.split(",")

parse_args() converts argument strings to objects and assigns them as attributes. The default set of arguments are taken from sys.argv.

Hope you enjoyed this quick intro on getting started with the argparse library. For detailed reading, check out Python docs. Have a great day!