Building a chat application

Next, let's build a multi-client chat room. The goal of this program is to explore socket programming in further detail. This section also implements and discusses the client-server architecture that is so common in all network programs.

Our chat program will consist of a chat server, which listens for and receives all incoming messages on a given port.

It also maintains a list of chat clients that connect to the server. It then broadcasts any incoming messages to all connected clients:

Let's start with the code for the chat server.

A server runs on a remote host and has a socket bound to a specific port number. The server just waits, listening to the socket for a client to make a connection request.

Here's the code for a chat server (see 9.09_chat_server.py):

class ChatServer:
clients_list = []
last_received_message = ""

def __init__(self):
self.create_listening_server()

def create_listening_server(self):
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
local_ip = '127.0.0.1'
local_port = 10319
self.server_socket.setsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR, 1)
self.server_socket.bind((local_ip, local_port))
print("Listening for incoming messages..")
self.server_socket.listen(5)
self.receive_messages_in_a_new_thread()

def receive_messages_in_a_new_thread(self):
while 1:
client = so, (ip, port) = self.server_socket.accept()
self.add_to_clients_list(client)
print ('Connected to ', ip , ':' , str(port))
t = threading.Thread(target=self.receive_messages, args=(so,))
t.start()

def receive_messages(self, so):
while True:
incoming_buffer = so.recv(256)
if not incoming_buffer: break
self.last_received_message = incoming_buffer.decode('utf-8')
self.broadcast_to_all_clients(so)
so.close()

def broadcast_to_all_clients(self, senders_socket):
for client in self.clients_list:
socket, (ip, port) = client
if socket is not senders_socket:
socket.sendall(self.last_received_message.encode('utf-8'))

def add_to_clients_list(self, client):
if client not in self.clients_list:
self.clients_list.append(client)

if __name__ == "__main__":
ChatServer()

The description of the preceding code is as follows:

  • We create a TCP socket with an address family of IPv4 using the line self.server_socket = socket(AF_INET, SOCK_STREAM). The IPv4 socket uses a 32-bit number to represent the address size. It is the most popular addressing scheme and accounts for most current internet traffic. IPv6 is a newer numbering system with a 128-bit address size, thereby providing a much larger pool of addresses. IPv6 has seen some adoption but it has not yet become the mainstream standard.
  • The SOCK_STREAM parameter means that we will be using a TCP connection for the communication. Another less popular option is to use SOCK_DGRAM, which refers to the UDP mode of transmission.
  • TCP is a more reliable protocol for communication than UDP as it offers a guarantee against packet loss. It also takes care of the proper ordering of bytes at the receiving end. If we use a UDP protocol, we will have to take care of handling packet loss, duplication, and the ordering of packets at the receiving end.
  • We used socket.bind(('127.0.01', 10319)) in the preceding code to bind the socket. We could have alternatively used socket.bind ((socket.gethostname( ), 10319) so that the socket would have been visible to the outside world. Alternatively, we could have specified an empty string such as socket.bind((' ', 10319)) to make the socket reachable by any address the machine could have.
  • The socket.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) line of code allows other sockets to bind() to this local port, unless there is an active socket already bound to the port. This lets us get around the Address already in use error message when a server is restarted after a crash.
  • The line self.server_socket.accept() returns a value of the form (socket, (ip, port)) as soon as a remote client connects to the server. Each client is then uniquely identified by the following data: (socket, (ip, port)).
  • The line Thread(target=self.receive_messages, args=(so,)) receives each new message on a new thread.
  • Finally, the line socket.sendall(self.last_received_message.encode('utf-8')) sends the message to individual clients.
  • The receive_messages method receives messages using the socket.recv method. The socket.recv method receives messages in buffers. It is your responsibility to call the method again and again until the entire message has been dealt with. When the socket.recv method returns 0 bytes, it means that the sender has closed the connection. We then break out of the infinite loop and get the complete message from the buffer.

Also note that message transmission over the network occurs in bytes.

Any message that we send must be converted to byte form using outgoing_message.encode('utf-8'). Similarly, any message that we receive from the network must be converted from bytes to a string or any other format.

To convert bytes to a string, we use incoming_bytes.decode('utf-8').

Our chat server is now ready. Next, let's build the chat client.

Our chat client should connect to the remote server and send a message to the server. It should also be listening for any incoming messages from the central chat server. We do not reproduce the entire code for our chat client. Specifically, we omit the code that produces the GUI for our chat client as we have coded similar widgets in the past.

The partial code in our chat client that sends and receives messages to and from the chat server is as follows (see 9.10_chat_client.py):

class ChatClient:
client_socket = None
last_received_message = None

def __init__(self, root):
self.root = root
self.initialize_socket()
self.initialize_gui()
self.listen_for_incoming_messages_in_a_thread()

def initialize_socket(self):
self.client_socket = socket(AF_INET, SOCK_STREAM)
remote_ip = '127.0.0.1'
remote_port = 10319
self.client_socket.connect((remote_ip, remote_port))

def listen_for_incoming_messages_in_a_thread(self):
t = Thread(target=self.recieve_message_from_server,
args=(self.client_socket,))
t.start()

def recieve_message_from_server(self, so):
while True:
buf = so.recv(256)
if not buf:
break
self.chat_transcript_area.insert('end',buf.decode('utf-8') + ' ')
self.chat_transcript_area.yview(END)
so.close()

def send_chat(self):
senders_name = self.name_widget.get().strip() + ":"
data = self.enter_text_widget.get(1.0, 'end').strip()
message = (senders_name + data).encode('utf-8')
self.chat_transcript_area.insert('end', message.decode('utf-8') + ' ')
self.chat_transcript_area.yview(END)
self.client_socket.send(message)
self.enter_text_widget.delete(1.0, 'end')
return 'break'

This code is very similar to the code of our chat server. Here's a short description of the code:

  • We first create a socket using socket(AF_INET, SOCK_STREAM)
  • We then connect the socket to the remote IP and the remote port of our chat server using socket.connect()
  • We receive messages from the server using socket.recv()
  • We send messages to the server using socket.send()

Note that when a client attempts to connect to the server using the socket.connect method, the operating system will assign a unique but random port to identify the client when a message is returned by the server. 

The port numbers from 0 to 1023 are referred to as the well-known ports, reserved ports, or system ports. They are used by the operating system to provide widely used network services. For example, port 21 is reserved for FTP services, port 80 is reserved for HTTP services, port 22 is reserved for SSH and SFTP, and port 443 is reserved for a secure HTTP service (HTTPS) over TLS/SSL.
The random port that the operating system assigns to our client is selected from a pool of ports that are above the system-reserved ports. The list of all reserved ports can be found at
https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers.

The full code of the chat client can be found in 9.10_chat_client.py. The chat is now functional, but note that we have not coded the logic for removing users from the clients_list in ChatServer. This means that even if you close a chat window, the chat server will still try to send a chat message to the closed client as we have not removed the client from the server. We will not implement it here, but should you wish to implement this, you can easily override the window's close method and send a message to ChatServer to delete the client from the client list.

That concludes the chat application project.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.37.250