DEV Community

Cover image for Socket Programming in Python.
Pratik Mishra
Pratik Mishra

Posted on • Updated on

Socket Programming in Python.

Today, let's see how sockets work in Python.
First of all this tutorial will only work with sockets in your Local Area Network as it will use your Internal Ip Address within your network. If you want to build sockets worldwide for like everyone you can update the Ip Adress to your global Ip but to do so there is lot of things to keep into considerations like make sure your Firewall allows the connections and so on which is beyond the scope of this tutorial.

Now that's getting out of the way let's continue,
Let's first start with the server,
First let's import all the modules:

import socket
import threading
Enter fullscreen mode Exit fullscreen mode

As you can see we'll be using the socket and the threading module.
Now, you might be wondering what is threading, threading is nothing but it let's you run code parallely to each other.
what do I mean by that, like consider you have a server and there are multiple clients on your server now if a client is executing some commands we don't want our other clients to keep waiting untill the first one is done executing we want all of them to access our server at the same time. So by using the concept of threading you can run multiple programs simultaneously i.e concurrently without them interfering with each other.

Let's define all the constants that we are going to use:

# Defining the size of the Header.
HEADER = 64

# The Port you want to use for your server.
PORT = 5050

# The IP address of your machine or the machine on which you want to host your server with the help of python as shown below.
# You can either hard code your Ip address but when you change your machine you'll have to do it again manually.
# Instead I have used a code to get the Ip address of the host machine.
SERVER = socket.gethostbyname(socket.gethostname())

# Tuple of Ip adress and Port number to be used for the socket
ADDR = (SERVER, PORT)

# Defining the encoding Format
FORMAT = 'utf-8'

# Defining the Disconect Message for our protocol to end the connection.
DISCONNECT_MESSAGE = '!DISCONNECT'
Enter fullscreen mode Exit fullscreen mode

Here, The port is the Port number which you want to use for your server.
I generally use 5050 but you can use any other "free" port on your machine. Also, notice I have used Capitals for the variables as this is the industry standards for defining constant variables.

Now, let's configure our server,

# AF_INET -> Category or family of connections such as IPV4 Adresses.
# SOCK_STREAM -> Streaming data through the socket.
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

server.bind(ADDR) # Here addr is a tuple consisting of Ip Adress and the Port of your Server.
Enter fullscreen mode Exit fullscreen mode

The bind() method binds your server with the mentioned Ip Address and the Port Number.

Now the main fun part begins:
I have used functions for writing every part of the program to keep the code cleaner.
Let's start with the start Function:

def start():
    server.listen()
    while True:
        # Get the socket connect object and Ip address of the client.
        # server.accept() is a block code that means it will wait until a client connects to it.
        conn, addr = server.accept()
        thread = threading.Thread(target= handle_clients, args= (conn, addr))
        thread.start()
        # There is one thread always running that is the start() [function] thread.
        # Hence, substracting one to get the active clients apart from start thread.
        print(f'[ACTIVE CONNECTIONS] : {threading.activeCount() - 1}')
Enter fullscreen mode Exit fullscreen mode

Let's break down what's going on here,
First we want our server to listen for new connections for that we use the listen() method.

Next I have used a While loop to run the loop infinitely to keep on accepting the clients.

The accept() methods accepts the clients who want to connect to our server and stores the socket object and the client address in the conn and addr variables.

Now the concept of threading comes into play,
You see for every client we are starting a new thread so that all of them can run concurrently. In Thread declaration you just provide the name of the functions without the Parenthesis and in the args we provide all the parameters or the arguments of the function as shown above.

The last line of code I have used to print all the active connections to our server just to keep a track of the number.
Notice that I have substracted 1 from the activeCount that is because start() function is also a thread which is infinitely running so we don't want to include that in the number of clients.

Now, Let's write another function to handle all the connected clients on our server as follows:

def handle_clients(conn, addr):
    print(f'[New Connection] : {addr} connected.')

    connected = True
    while connected:
        # Blocking line of code.
        # This will not pass this line of code until it gets a message from the client.
        # RECV takes the size of the message to recieve.
        msg_length = conn.recv(HEADER).decode(FORMAT)
        if msg_length:
            msg_length = int(msg_length)
            msg = conn.recv(msg_length).decode(FORMAT)
            print(f'[{addr}] : {msg}')
            if msg == DISCONNECT_MESSAGE:
                connected = False
                print(f'[{addr}] : Disconnected.')

    # Closing the Connection.
    conn.close()
Enter fullscreen mode Exit fullscreen mode

Now, again let's break the function line by line,
we want the loop to run continuously till a client is connected on to our server for that I have used a variable connected.

Now, Let's talk about the protocal that we'll be using, the protocol goes somewhat like this(Client Side):

  1. Send the length of the message that you'll be sending onto the server.
  2. Then send that many bytes of message to the server.
  3. IF Disconnect message is sent then close the connection and exit.

The recv() method recieves the bytes from the client, all of this will be much more clearer once we start with the client side program.

Now let's run our server and see if everything is working fine:

print(f'[Starting] Server starting on {SERVER}.....')
start()
Enter fullscreen mode Exit fullscreen mode

The entire server code is:

import socket
import threading

# Defining the size of the Header.
HEADER = 64

# The Port you want to use for your server.
PORT = 5050
# The IP address of your machine or the machine on which you want to host your server.
SERVER = socket.gethostbyname(socket.gethostname())
# Tuple of Ip adress and Port number to be used for the socket
ADDR = (SERVER, PORT)
# DEfining the Disconect Message for our protocol to end the connection.
DISCONNECT_MESSAGE = '!DISCONNECT'

# AF_INET -> Category or family of connections such as IPV4 Adresses.
# SOCK_STREAM -> Streaming data through the socket.
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDR)

FORMAT = 'utf-8'

def handle_clients(conn, addr):
    print(f'[New Connection] : {addr} connected.')

    connected = True
    while connected:
        # Blocking line of code.
        # This will not pass this line of code until it gets a message from the client.
        # RECV takes the size of the message to recieve.
        msg_length = conn.recv(HEADER).decode(FORMAT)
        if msg_length:
            msg_length = int(msg_length)
            msg = conn.recv(msg_length).decode(FORMAT)
            print(f'[{addr}] : {msg}')
            if msg == DISCONNECT_MESSAGE:
                connected = False
                print(f'[{addr}] : Disconnected.')

    # Closing the Connection.
    conn.close()


def start():
    server.listen()
    while True:
        # Get the socket connect object and Ip address of the client.
        # server.accept() is a block code that means it will wait until a client connects to it.
        conn, addr = server.accept()
        thread = threading.Thread(target= handle_clients, args= (conn, addr))
        thread.start()
        # There is one thread always running that is the start() [function] thread.
        # Hence, substracting one to get the active clients apart from start thread.
        print(f'[ACTIVE CONNECTIONS] : {threading.activeCount() - 1}')



print(f'[Starting] Server starting on {SERVER}.....')
start()
Enter fullscreen mode Exit fullscreen mode

If Everything went well you'll see something like this:
Alt Text

Now, Let's move on to our client side code:

import socket

HEADER = 64

# Port of the server to connect to..
PORT = 5050
# Ip Adrress of the Server..
SERVER = '192.168.56.1'

FORMAT = 'utf-8'

ADDR = (SERVER, PORT)
DISCONNECT_MESSAGE = '!DISCONNECT'

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(ADDR)


def send(msg):
    message = msg.encode(FORMAT)
    msg_length = len(message)
    send_length = str(msg_length).encode(FORMAT)
    send_length += b' ' * (HEADER - len(send_length))
    client.send(send_length)
    client.send(message)


# m = input("Enter your message:\n")
send('Hi')
send(DISCONNECT_MESSAGE)
Enter fullscreen mode Exit fullscreen mode

Again let's understand it line by line,
First we import all the necessary modules,
Then define all the constants and then setup the client socket.

Note: The Server and port in the client will be the server's IP Address and the port on which the server is listening on to.

Next, the connect() method is used to connect a client on the server. Once the client get's connected it's Ip Adress and the Port number is given to the server through the accept() method on our server side code and a new thread is started for this client.
Now as we have defined our protocol to first send the length of the message and then the actual message, we follow the same protocol to do so.

As you can see First we calculate the length of the message and then pad the length with white spaces to fill the remaining bytes as we have declared the size of the header. Now why are we padding the message that is because in our server we have passed the size of the receiving message as Header now the recv() method will wait until it has received that many bytes so we have to pass the exact same length message.

And the final one is the send() method which as the name suggests is used to send the data.

For those who are Wondering what is this encode and decode mentioned everywhere in the code. As we know python works in Unicode whereas the web and most of the places work in utf-8 encodings. Now when we need to transfer data over the web it should be in proper encodings that is why we encode it in utf-8 encoding standards using the encode method and when we receive the data it's in utf-8 encoding but python unbderstands Unicode so we decode it back to Normal.

Here is a screenshot of the clients connected on to our server:
Alt Text

Hope you liked it !! This was just a High Level overview just to get started with socket programming in Python.

Alt Text

Top comments (0)