Build a Chatroom App with Python
Tags: python, tutorial, network, socket
Published: 2020-06-18 14:57:53

### Socket programming + Tkinter GUI

![Photo by [Volodymyr Hryshchenko](https://unsplash.com/@lunarts?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/800/0*GgSj8u7KwTit1IBa)
<figcaption style="text-align:center">Photo by Volodymyr Hryshchenko on Unsplash</figcaption>

I’ve recently delved into the wonderful world of computer networking. One of the fun projects I’ve created is a simple chatroom application that facilitates real-time messaging between different clients. 

{% github https://github.com/zeyu2001/pychat no-readme %}

At any point in this tutorial, you may refer to my [source code](https://github.com/zeyu2001/pychat) in GitHub. The aim of this tutorial is to introduce some basic networking theory while providing practical socket programming experience. If, at any point, you find that you are already comfortable with the relevant theory, please feel free to skip ahead!

## Prerequisites

You should know basic Python. Other than that, nothing! In the process of creating this application, you will also learn basic computer networking and client-server architecture.

We are going to use Tkinter and sockets, both of which are available in the Python standard library. There is nothing you need to install!

## Client-Server Architecture

![Photo by [İsmail Enes Ayhan](https://unsplash.com/@ismailenesayhan?utm_source=medium&utm_medium=referral) on [Unsplash](https://unsplash.com?utm_source=medium&utm_medium=referral)](https://cdn-images-1.medium.com/max/800/0*7BucOLCRTOJ_m66S)
<figcaption style="text-align:center">Photo by İsmail Enes Ayhan on Unsplash</figcaption>

The client-server architecture is a basic computing model where the client *requests* services from the server, while the server processes these requests and *provides* services to the clients. 

In fact, your web browser is a client requesting web services from Medium’s web server through the HyperText Transfer Protocol (HTTP) right now! Medium’s web server processes your request and returns HTML documents, CSS stylesheets, JavaScript files and images so that your web browser can display the website the way it is.

![Client-Server Architecture](https://cdn-images-1.medium.com/max/2392/1*DixueByHI55JwHRBo9DPYA.png)
<figcaption style="text-align:center">Client-Server Architecture</figcaption>

Furthermore, a server can serve *multiple* clients! In our application, we want many clients to be talking in real-time. The client software will send a message to the chatroom server, and the chatroom server will broadcast our message to *all* other connected clients.

## Protocols, Protocols, Protocols

Communications over networks use what we call a *protocol stack* — building higher-level, more sophisticated conversations on top of simpler, more rudimentary conversations. Network layers can be represented by the OSI model and the TCP/IP model. Each network layer corresponds to a group of layer-specific network protocols.

![Network Protocol Stack](https://cdn-images-1.medium.com/max/2000/0*zwEyy3HRra-j5a4G.png)
<figcaption style="text-align:center">Network Protocol Stack</figcaption>

For the purpose of this application, we need not concern ourselves with many of the lower-level protocols. But we do need to know that we are using something called the *Transmission Control Protocol*(TCP). 

*TCP* and *UDP* are transport-layer protocols — they govern how data is sent from one point to another. We are building *on top of* *TCP*, which means we do not need to care about *how* data is sent, only *what* and *where* data is sent. 

The main difference between TCP and UDP is that TCP guarantees reliable delivery, without any information lost, duplicated, or out-of-order. UDP does not guarantee the same and leaves it up to the application layer to handle dropped packets. It requires the server to acknowledge receipt of data.

![TCP vs UDP](https://cdn-images-1.medium.com/max/2000/0*3UKurt9qle5Wg1AC.png)
<figcaption style="text-align:center">TCP vs UDP</figcaption>

UDP is normally used for time-sensitive transmissions where a dropped packet is preferred to waiting for lost packets to be re-transmitted. Imagine a real-time voice call, for example. If a packet has been lost, it won’t make sense to wait for that piece of audio to arrive because, by the time it does, it would already be too old. Furthermore, you would probably still be able to piece the conversation together without that packet of audio.

TCP, on the other hand, would have kept stubbornly resending that piece of lost audio even though it is already too old to be of any use. However, for most other applications, this aspect of TCP is extremely valuable. In our chat application, we don’t want to have to deal with lost packets because we *always* want to receive *complete* messages without any errors.

## Chatroom Server

Let’s begin with our server script. We start by defining a `Server` class in `server.py`.

```python
#!/usr/bin/env python3

import threading
import socket
import argparse
import os

class Server(threading.Thread):

    def __init__(self, host, port):
        super().__init__()
        self.connections = []
        self.host = host
        self.port = port
    
    def run(self):
        pass
```

### Threading Basics

Note that we are using *multithreading* to allow multiple pieces of code to run concurrently. Our `Server` class inherits from Python’s `threading.Thread` class, thus creating a thread. We need to define the logic for our `Server` thread in the `run()` method. When `start()` is called on the `Server` object, `run()` will be executed in parallel to the main thread.

### Socket Basics

A socket is an IP address + port number pair. An IP address identifies a host. However, a host can have many different applications running at the same time. How does your operating system (OS) know that an HTTP response is meant for your web browser, and not Call of Duty: Modern Warfare? Port numbers are used to identify the particular application that should receive the data. Call of Duty: Modern Warfare uses the TCP port range 27014–27050, for example. 

Thus, any communication between two network devices needs a socket *pair*:

    Source (IP: Port Number) → Destination (IP: Port Number)

### Creating a Socket

Let’s start defining our thread logic. Add the following to the `run()` method.

```python
def run(self):

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((self.host, self.port))

    sock.listen(1)
    print('Listening at', sock.getsockname())
```

First, we created a `socket.socket` object. `socket` takes two arguments: the *address family* and the *socket type*. The `AF_INET` address family is used for IP networking. The `SOCK_STREAM` socket type is used for reliable flow-controlled data streams, such as those provided by TCP. UDP, on the other hand, requires a packet-based socket type, `SOCK_DGRAM`.

Next, we set the `SO_REUSEADDR` option. You can read more about socket options in the Linux manual page [*socket(7)*](https://man7.org/linux/man-pages/man7/socket.7.html). This option allows the server to use the same port after an old connection was closed (normally, you would have to wait for a few minutes).

Then, we use `bind()` to bind the `socket` object to a socket address on the server machine. `bind()` takes in a tuple, in the format 

    (IP Address (str), Port Number (int)) 

Note that a machine can have many external IP interfaces: you can find yours by using the `ifconfig` command at the terminal (or `ipconfig` for Windows).

![Running ifconfig command from the terminal](https://cdn-images-1.medium.com/max/2000/1*RgIEXW9EtmO8PGtRkSqT0Q.png)
<figcaption style="text-align:center">Running ifconfig command from the terminal</figcaption>

You should know that the very first result I got, the `lo0` interface, is a very special one. This is the *loopback interface*, which is only reachable by other programs running on the same machine. It has an IP address of 127.0.0.1 and is only locally significant within your own machine. It has the hostname ‘*localhost*’ and provides a safe testing environment.

Within the `bind()` command, we can specify any IP interface (even the loopback interface!), or we can use an empty string `''` as a wildcard to indicate that we are willing to receive packets arriving at the server via *any* of its network interfaces. In our program, we will leave this up to the user, as you will see later.

Finally, we use the `listen()` to indicate that this is a *listening* socket. TCP uses two types of sockets: *listening* sockets and *connected* sockets. After calling `listen()` on the socket, this becomes a listening socket and can only facilitate *establishing* TCP connections through handshakes, but not the actual transfer of data. We will need to create an entirely new socket whenever a client connects, in order to send and receive data. Let’s continue:

### Accepting Connections

```python
def run(self):

    ...

    while True:

        # Accept new connection
        sc, sockname = sock.accept()
        print('Accepted a new connection from {} to {}'.format(sc.getpeername(), sc.getsockname()))

        # Create new thread
        server_socket = ServerSocket(sc, sockname, self)
        
        # Start new thread
        server_socket.start()

        # Add thread to active connections
        self.connections.append(server_socket)
        print('Ready to receive messages from', sc.getpeername())
```

We create an infinite loop to listen for new client connections. The `accept()` call will wait for a new client to connect and when it does, returns a new *connected* socket along with the socket address of the connected client.

A few more methods to introduce: `getpeername()` returns the socket address on the other end of the connection (in this case, the client) while `getsockname()` returns the socket address to which the socket object is bound.

We need a way to communicate with each individual client, but at the same time, we need to be listening for new connections from other potential clients. The way we do this is by creating a new `ServerSocket` thread (we will define this later) every time a new client connects, and this thread runs alongside the `Server` thread. The server also needs a way to manage *all* active client connections, so it stores the active connections as `ServerSocket` objects in `self.connections`.

### ‘Broadcasting’

How our little chatroom app will work is:

1. A client sends a message to the server from the command line or GUI.

1. The server receives and processes the message.

1. The server sends the message to all other connected clients.

1. The clients will display the message in the command line or GUI.

Let’s add the ‘broadcasting’ functionality from step 3 to the `Server` class. I use the word ‘broadcasting’ with caution because a *broadcast* refers to a completely different concept in the context of computer networking. We are really sending many *unicasts*, which are one-to-one transmissions to each individual connected client. 

```python
class Server(threading.Thread):
        
    ...

    def run(self):
        
        ...

    def broadcast(self, message, source):
    
        for connection in self.connections:

            # Send to all connected clients except the source client
            if connection.sockname != source:
                connection.send(message)
```

Note that `self.connections` is a list of `ServerSocket` objects representing active client connections. We will define the `ServerSocket` class below.

### Sending and Receiving

The `ServerSocket` class will facilitate communications with *individual* clients.

```python
#!/usr/bin/env python3

...

class ServerSocket(threading.Thread):

    def __init__(self, sc, sockname, server):
        super().__init__()
        self.sc = sc
        self.sockname = sockname
        self.server = server
    
    def run(self):

        while True:
            message = self.sc.recv(1024).decode('ascii')
            if message:
                print('{} says {!r}'.format(self.sockname, message))
                self.server.broadcast(message, self.sockname)
            else:
                # Client has closed the socket, exit the thread
                print('{} has closed the connection'.format(self.sockname))
                self.sc.close()
                server.remove_connection(self)
                return
    
    def send(self, message):
        self.sc.sendall(message.encode('ascii'))
```

Again, we create an infinite loop. This time, instead of listening for new connections, we are listening for data sent by the client.

When `recv()` is called, it will wait for data to arrive. If no data is available, `recv()` will not return (it ‘blocks’) and the program pauses until data arrives. Calls like `accept()` and `recv()` that make the program wait until some new data has arrived, allowing it to return, are called *blocking* calls. Data is sent and received over the network as *bytestrings*, and hence need to be encoded and decoded using `encode()` and `decode()` respectively. 

`recv()` takes in one argument, `bufsize`, which specifies the maximum amount of data to be received at once. 

`send()`, on the other hand, sends data from the socket to its connected peer. 

One problem with both `send()` and `recv()` is that there is a slight possibility that only *part* of the data is sent or received, because the outgoing or incoming buffers are *almost* full, so it queues whatever data it could, while leaving the rest of the data unprocessed. This becomes problematic when `send()` returns, but in fact, some of the data is still left unsent. We could put `send()` in a loop, or we could use `sendall()` as a simple way to say “I want to send *all* of the data”.

### Closed Sockets

One thing you would notice in the above code is that we are able to tell if the client has closed its end of the connection. When the client socket is closed, `recv()` returns an empty string `''` immediately, similarly to how `read()` behaves when an end-of-file is reached. 

Thus, when we see that `recv()` returns an empty string (in the else statement), we `close()` our side of the connection as well, remove the `ServerSocket` thread from the list of active connections, and end the thread.

### Final Touches

```python
#!/usr/bin/env python3

...

def exit(server):

    while True:
        ipt = input('')
        if ipt == 'q':
            print('Closing all connections...')
            for connection in server.connections:
                connection.sc.close()
            print('Shutting down the server...')
            os._exit(0)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Chatroom Server')
    parser.add_argument('host', help='Interface the server listens at')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()

    # Create and start server thread
    server = Server(args.host, args.p)
    server.start()

    exit = threading.Thread(target = exit, args = (server,))
    exit.start()
```

We allow the user to specify the host address and port number at the command line and enter ‘q’ at any point from the command line to terminate the program.

You can now run your server script!

![Running server.py from the terminal](https://cdn-images-1.medium.com/max/2000/1*b3_44Uopxhxk7MPNTOhIJw.png)
<figcaption style="text-align:center">Running server.py from the terminal</figcaption>

## Chatroom Client

The server doesn’t really do much without a client connecting to it. Let’s create a `client.py` file:

```python
#!/usr/bin/env python3

import threading
import socket
import argparse
import os

class Send(threading.Thread):

    def __init__(self, sock, name):
        super().__init__()
        self.sock = sock
        self.name = name

    def run(self):

        while True:
            message = input('{}: '.format(self.name))

            # Type 'QUIT' to leave the chatroom
            if message == 'QUIT':
                self.sock.sendall('Server: {} has left the chat.'.format(self.name).encode('ascii'))
                break
            
            # Send message to server for broadcasting
            else:
                self.sock.sendall('{}: {}'.format(self.name, message).encode('ascii'))
        
        print('\nQuitting...')
        self.sock.close()
        os._exit(0)

class Receive(threading.Thread):

    def __init__(self, sock, name):
        super().__init__()
        self.sock = sock
        self.name = name

    def run(self):

        while True:
            message = self.sock.recv(1024)
            if message:
                print('\r{}\n{}: '.format(message.decode('ascii'), self.name), end = '')
            else:
                # Server has closed the socket, exit the program
                print('\nOh no, we have lost connection to the server!')
                print('\nQuitting...')
                self.sock.close()
                os._exit(0)
```

While creating the server script, I hope I have made much of the theory clear to you. The same rules apply here, except we have a Send thread that is always listening for user input from the command line. We will add a GUI later, but it follows much of the same logic. Any data received will be displayed on the client interface, and any data sent will be processed by the server to be broadcast to other connected clients.

Again, we make use of multithreading. This time, it is to let the sending and receiving operations run alongside each other. This way, our chatroom is real-time (instead of alternating between `send()` and `recv()` calls).

### Connecting to the Server

```python
#!/usr/bin/env python3

...

class Client:

    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    def start(self):
        print('Trying to connect to {}:{}...'.format(self.host, self.port))
        self.sock.connect((self.host, self.port))
        print('Successfully connected to {}:{}'.format(self.host, self.port))
        
        print()
        name = input('Your name: ')

        print()
        print('Welcome, {}! Getting ready to send and receive messages...'.format(name))

        # Create send and receive threads
        send = Send(self.sock, name)
        receive = Receive(self.sock, name)

        # Start send and receive threads
        send.start()
        receive.start()

        self.sock.sendall('Server: {} has joined the chat. Say hi!'.format(name).encode('ascii'))
        print("\rAll set! Leave the chatroom anytime by typing 'QUIT'\n")
        print('{}: '.format(name), end = '')
```

The client calls the `connect()` method to connect to a specified socket address (again, in the form of a tuple) of the server. If the specified socket is not ready to receive connections (e.g. you did not run the server script yet), the `connect()` call will fail.

![connect() fails with ConnectionRefusedError](https://cdn-images-1.medium.com/max/2000/1*E_vBMr-7QgWAaFcLk04V_Q.png)
<figcaption style="text-align:center">connect() fails with ConnectionRefusedError</figcaption>

### Final Touches

```python
#!/usr/bin/env python3

...

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Chatroom Server')
    parser.add_argument('host', help='Interface the server listens at')
    parser.add_argument('-p', metavar='PORT', type=int, default=1060,
                        help='TCP port (default 1060)')
    args = parser.parse_args()

client = Client(args.host, args.p)
    client.start()
```

## Testing our Command-Line App

What we have done is built a fully-functioning command-line chatroom. 

You can run your server script by giving it any of your machine’s IP interfaces (you can also use your hostname!). If you like, you can just use ‘127.0.0.1’ or ‘localhost’ if you are only going to be testing this on one machine.

![Running server.py with an IP address](https://cdn-images-1.medium.com/max/2000/1*xBSbbPEF6LHw5SkjXPdF2A.png)
<figcaption style="text-align:center">Running server.py with an IP address</figcaption>

![Running server.py with a hostname](https://cdn-images-1.medium.com/max/2000/1*mU4_x7ouzYUNrrkSo1v__A.png)
<figcaption style="text-align:center">Running server.py with a hostname</figcaption>

Same rules apply for the client script. Open a separate terminal window and run `client.py`.

![Running client.py with a server IP address](https://cdn-images-1.medium.com/max/2000/1*KB0kOXVI-p-5TTSmVB9aMw.png)
<figcaption style="text-align:center">Running client.py with a server IP address</figcaption>

![Running client.py with a server hostname](https://cdn-images-1.medium.com/max/2000/1*CXgkKirIQlZKqn5VbFeV0w.png)
<figcaption style="text-align:center">Running client.py with a server hostname</figcaption>

Open up three or more terminal windows, and you can have multiple clients! Alternatively, grab another machine connected to the same LAN and run `client.py` from there. If it is in a different LAN, your firewall might block it.

![Running Multiple Clients in iTerm](https://cdn-images-1.medium.com/max/2872/1*yjCHWcmIiao3ggE6mN0YXg.png)
<figcaption style="text-align:center">Running Multiple Clients in iTerm</figcaption>

In the above screenshot, I am running three terminal windows, with two clients connected to the same server.

## Creating a GUI

We are not quite done yet. A command-line app is nice and all, but how about making a GUI? We are going to use Tkinter, which is included in the Python standard library. 

We just need to make a few adjustments to our `client.py`, and add some more code to create the GUI. You can learn more about Tkinter by reading its documentation, but that is beyond the scope of this tutorial. 

The complete `client.py` code is available below:

{% gist https://gist.github.com/zeyu2001/82e41a27dbf88c1aa6769e65d9b51bc0.js %}

and for reference, the complete `server.py` code:

{% gist https://gist.github.com/zeyu2001/a3c3a20d655632cbbd2bec844347b725.js %}

## The Final Product

Run `server.py` and `client.py` the same way as you did before. After entering your name, the GUI will pop up! 

I have designed this so that you can use *either* the command line *or* the GUI to interact with the application so that it is easier to debug and see what is going on behind the scenes. 

![](https://cdn-images-1.medium.com/max/2880/1*EjcHnH1pQP3U-aj-QnLQcw.png)

## Conclusion

That’s all! I hope that you’ve enjoyed reading this as much as I have enjoyed writing it. The world of computer networking is really quite interesting and this was only the tip of the iceberg. 

If you have any questions, please feel free to let me know in the comments.