• No results found

He’re we’ll be implementing an instant messenger in Python with a client-server architecture. This means each client connects to the server, which relays any message that one client sends to all other clients. The server will also notify the other clients when someone joins or leaves the server. The instant messenger can work anywhere a TCP socket can: on the same computer with the loopback interface, across various computers on a LAN, or even over the internet if you were to confi gure your router correctly. However, our messages aren’t encrypted, so we wouldn’t recommend that.

Writing an instant messenger is an interesting technical problem that covers a bunch of areas that you may not have come across while programming before:

• We’ll be employing sockets, which are used to transmit data across networks.

• We’ll also be using threading, which allows a program to do multiple things at once.

• We’ll cover the basics of writing a simple graphical user interface with GTK, as well as how to interact with that from a different thread.

• Finally, we’ll be touching on the use of regular expressions to easily analyse and extract data from strings.

Before getting started, you’ll need to have a Python2.x interpreter installed, as well as the PyGTK bindings and the Python2 GObject bindings. The chances are that if you have a system with a fair amount of software on it, you will already have these packages, so it may be easier to wait and see if you’re missing any libraries when you attempt to import them. All of the above packages are commonly used, so you should be able to install them using your distro’s package manager.

Resources

A computer

– running your favourite Linux

distribution

Internet connection

– to access

documentation

Python 2.x, PyGTK and GObject

packages installed

The server notifi es all clients when a new client joins Each message

has a time stamp prefi xed to it

Similarly, the server notifi es all clients when a client leaves A client can detect

when the server exits without crashing or hanging

Work with Python

01

The server

The server will do the following jobs:

• Listen for new clients

• Notify all clients when a new client joins

• Notify all clients when a client leaves

• Receive and deliver messages to all clients

We’re going to write the server side of the instant messenger fi rst, as the client requires it. There will be two code fi les, so it’s a good idea to make a folder to keep them inside. You can create an empty fi le with the command touch [filename], and mark that fi le as executable using chmod+x [filename]. This fi le is now ready to edit in your favourite editor. [liam@liam-laptop Python]$ mkdir Python-IM

[liam@liam-laptop Python]$ cd Python-IM/

[liam@liam-laptop Python-IM]$ touch IM-Server.py

[liam@liam-laptop Python-IM]$ chmod +x IM-Server.py

02

Starting off

As usual, we need to start off with the line that tells the program loader what it needs to interpret the rest of the fi le with. In your advisor’s case, that line is:

#!/usr/bin/env python2.

On your system, it may need to be changed to #!/usr/bin/env/ python2.6 or #!/usr/ bin/env python2.7

After that, we’ve written a short comment about what the application does, and imported the required libraries. We’ve already mentioned what the threading and socket libraries are for. The re library is used for searching strings with regular expressions. The signal library is used for dealing with signals that will kill the program, such as SIGINT. SIGINT is sent when Ctrl+C is pressed. We handle these signals so that the program can tell the clients that it’s exiting rather than dying unexpectedly. The sys library is used to exit the program. Finally, the time library is used to put a sensible limit on how frequently the body of while loops execute. #!/usr/bin/env python2

# The server side of an instant messaging application. Written as part of a Linux User & Developer tutorial by Liam Fraser in 2013.

import threading

03

The Server class

The Server class is the main class of our instant messenger server. The initialiser of this class accepts a port number to start listening for clients on. It then creates a socket, binds the socket to the specifi ed port on all interfaces, and then starts to listen on that port. You can optionally include an IP address in the tuple that contains the port. Passing in a blank string like we have done causes it to listen on all interfaces. The value of 1 passed to the listen function specifi es the maximum number of queued connections we can accept. This shouldn’t be a problem as we’re not expecting a bunch of clients to connect at exactly the same time.

Now that we have a socket, we’ll create an empty array that will be later used to store a collection of client sockets that we can echo messages to. The fi nal part is to tell the signal library to run the self.signal_handler function, which we have yet to write, when a SIGINT or SIGTERM is sent to the application so that we can tidy up nicely.

class Server():

def __init__(self, port):

# Create a socket and bind it to a port self.listener = socket. socket(socket.AF_INET, socket.SOCK_ STREAM) self.listener.bind((‘’, port)) self.listener.listen(1) print “Listening on port {0}”.format(port)

# Used to store all of the client sockets we have, for echoing to them

self.client_sockets = []

# Run the function self.signal_ handler when Ctrl+C is pressed

signal.signal(signal.SIGINT, self.signal_handler)

signal.signal(signal. SIGTERM, self.signal_handler)

04

The server’s main loop

The server’s main loop essentially accepts new connections from clients, adds that client’s socket to the collection of import socket

import re import signal import sys

import time

Threading:

docs.python.org/2/library/

threading.html

Sockets:

docs.python.org/2/library/ socket.html

Regular expressions:

docs.python. org/2/library/re.html

The signal handler:

docs.python.org/ 2/library/signal.html

PyGTK:

www.pygtk.org/ pygtk2reference

GObject:

www.pygtk.org/ pygtk2reference/gobject-functions.html

Useful

documentation

sockets and then starts an instance of the ClientListener class, which we have yet to write, in a new thread. Sometimes, defi ning interfaces you are going to call before you’ve written them is good, because it can give an overview of how the program will work without worrying about the details.

Note that we’re printing information as we go along, to make debugging easier should we need to do it. Sleeping at the end of the loop is useful to make sure the while loop can’t run quickly enough to hang the machine. However, this is unlikely to happen as the line that accepts new connections is blocking, which means that the program waits for a connection before moving on from that line. For this reason, we need to enclose the line in a try block, so that we can catch the socket error and exit when we can no longer accept connections. This will usually be when we’ve closed the socket during the process of quitting the program.

def run(self): while True:

# Listen for clients, and create a ClientThread for each new client

print “Listening for more clients” try: (client_socket, client_address) = self.listener. accept() except socket.error: sys.exit(“Could not

90 The Python Book

Work with Python

09

Tidying up

We need to have a function to tidy up the thread. We’ll call this either when the client sends us a blank string (indicating that it’s stopped listening on the socket) or sends us the string “QUIT”. When this happens, we’ll echo to every client that the user has quit.

def quit(self):

# Tidy up and end the thread self.listening = False self.socket.close() self.server.remove_ socket(self.socket) self.server.echo("{0} has quit.\n".format(self.username))

05

The echo function

We need a function that can be called from a client’s thread to echo a message to each client. This function is pretty simple. The most important part is that sending data to sockets is in a try block, which means that we can handle the exception if the operation fails, rather than having the program crash.

def echo(self, data):

# Send a message to each socket in self.client_socket

print "echoing: {0}". format(data)

for socket in self.client_ sockets:

# Try and echo to all clients

try:

socket.sendall(data) except socket.error: print "Unable to send message"

06

Finishing the Server class

The remainder of the Server class is taken up with a couple of simple functions; one to remove a socket from the collection of sockets, which doesn’t need an explanation, and the signal_handler function that we talked about in the initialiser of the class. This function stops listening for new connections, and unbinds the socket from the port it was listening on. Finally, we send a message to each client to let them know that we are exiting. The signal will continue to close the program as expected once the signal_handler function has ended. def remove_socket(self, socket):

07

The client thread

The class that is used to deal with each client inherits the Thread class. This means that the class can be created, then started with client_thread.start(). At this point, the code in the run function of the class will be run in the background and the main loop of the Server class will continue to accept new connections.

We have to start by initialising the Thread base class, using the super keyword. You may have noticed that when we created a new instance of the ClientListener class in the server’s main loop, we passed through the server’s self variable. We do this because it’s better for each instance of the ClientListener class to have its own reference to the server, rather than using the global one that we’ll create later to actually start the application. class ClientListener(threading. Thread):

def __init__(self, server, socket, address):

# Initialise the Thread base class super(ClientListener, self).__init__()

# Store the values that have been passed to the constructor

self.server = server self.address = address self.socket = socket self.listening = True self.username = "No Username"

08

The client thread’s loop

The loop that runs in the client thread is pretty similar to the one in the server. It keeps listening for data while self.listening is true, and passes any data it gets to a handle_msg function that we will write shortly. The value passed to the socket.recv function is the size of

10

Handling messages

There are three possible messages our clients can send:

• QUIT

• USERNAME user

• Arbitrary string to be echoed to all clients

The client will also send a bunch of empty messages if the socket has been closed, so we will end their thread if that happens. The code should be pretty self-explanatory apart from the regular expression part. If someone sends the USERNAME message, then the server tells every client that a new user has joined. This is tested with a regular expression. ^ indicates the start of the string, $ indicates the end, and the brackets containing .* extract whatever comes after “USERNAME ”.

# Remove the specified socket from the client_sockets list

self.client_sockets. remove(socket)

def signal_handler(self, signal, frame):

# Run when Ctrl+C is pressed

print "Tidying up"

# Stop listening for new connections

self.listener.close()

# Let each client know we are quitting

self.echo("QUIT") accept any more connections”)

self.client_sockets. append(client_socket)

print “Starting client thread for {0}”.format(client_ address) client_thread = ClientListener(self, client_socket, client_address) client_thread.start() time.sleep(0.1)

the buffer to use while receiving data. def run(self):

# The thread's loop to receive and process messages while self.listening: data = "" try: data = self.socket. recv(1024) except socket.error: "Unable to recieve data" self.handle_msg(data) time.sleep(0.1)

# The while loop has ended

print "Ending client thread for {0}".format(self.address)

Work with Python

11

Starting the server

The code that actually starts the Server class is as follows. Note that you are probably best picking a high-numbered port as you need to be root to open ports <1024.

if __name__ == "__main__":

# Start a server on port 59091 server = Server(59091)

server.run()

13

The client graphical user interface

The user interface of the client isn’t the main focus of the tutorial, and won’t be explained in as much detail as the rest of the code. However, the code should be fairly straightforward to read and we have provided links to documentation that will help.

Our MainWindow class inherits the gtk Window class, so we need to start by initialising that using the super keyword. Then we create the controls that will go on the window, connect any events they have to functions, and fi nally lay out the controls how we want. The destroy event is raised when the program is closed, and the other events should be obvious.

GTK uses a packing layout, in which you use Vboxes and Hboxes to lay out the controls. V and H stand for vertical and horizontal. These controls essentially let you split a window up almost like a table, and will automatically decide the size of the controls depending on the size of the application.

GTK doesn’t come with a control to enter basic information, such as the server’s IP address, port and your chosen username, so we’ve made a function called ask_for_info, which creates a message box, adds a text box to it and then retrieves the results. We’ve done this because it’s simpler and uses less code than creating a new window to accept the information.

12

The client

Create a new fi le for the client as we did for the server and open it in your favourite editor. The client requires the same imports as the server, as well as the gtk, gobject and datetime libraries. One important thing we need to do is to tell GObject that we’ll be using threading, so we can call functions from other threads and have the main window, which is running in the main GTK thread, update.

class MainWindow(gtk.Window): def __init__(self):

# Initialise base gtk window class

super(MainWindow, self).__ init__() # Create controls self.set_title("IM Client") vbox = gtk.VBox() hbox = gtk.HBox() self.username_label = gtk. Label() self.text_entry = gtk. Entry() send_button = gtk. Button("Send") self.text_buffer = gtk. TextBuffer() text_view = gtk. TextView(self.text_buffer) # Connect events self.connect("destroy", self.graceful_quit) send_button. connect("clicked", self.send_ message)

# Activate event when user presses Enter self.text_entry. connect("activate", self.send_ message) # Do layout vbox.pack_start(text_view) hbox.pack_start(self. username_label, expand = False)

We need to tell GObject that we’ll be

using threading