15
The remainder of MainWindowThe rest of the MainWindow class has plenty of comments to explain itself, as follows. One thing to note is that when a client sends a message, it doesn’t display it in the text view straight away. The server is going to echo the message to each client, so the client simply displays its own message when the server echoes it back. This means that you can tell if the server is not receiving your messages when you don’t see a message that you send.
def add_text(self, new_text):
# Add text to the text view
text_with_timestamp = "{0} {1}".format(datetime.datetime.now(), new_text)
# Get the position of the end of the text buffer, so we know where to insert new text
end_itr = self.text_buffer. get_end_iter()
# Add new text at the end of the buffer
self.text_buffer.insert(end_ itr, text_with_timestamp)
def send_message(self, widget):
# Clear the text entry and send the message to the server
# We don't need to display it as it will be echoed back to each client, including us. new_text = self.text_entry. get_text() self.text_entry.set_text("") message = "{0} says: {1}\n". format(self.username, new_text) self.network.send(message) def graceful_quit(self, widget):
# When the application is closed, tell GTK to quit, then tell the server we are quitting and tidy up the network gtk.main_quit() self.network.send("QUIT") self.network.tidy_up() hbox.pack_start(self.text_ entry) hbox.pack_end(send_button, expand = False) vbox.pack_end(hbox, expand = False) # Show ourselves self.add(vbox) self.show_all()
# Go through the configuration process
self.configure() def ask_for_info(self, question):
# Shows a message box with a text entry and returns the response
dialog = gtk.
MessageDialog(parent = self, type = gtk.MESSAGE_QUESTION, flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, buttons = gtk.BUTTONS_OK_CANCEL, message_format = question) entry = gtk.Entry() entry.show() dialog.vbox.pack_end(entry) response = dialog.run() response_text = entry. get_text() dialog.destroy() if response == gtk.RESPONSE_ OK: return response_text else: return None
Work with Python
17
Running a function as a threadThe listener function above will be run as a thread. This is trivial to do. Enabling the daemon option on the thread means that it will die if the main thread unexpectedly ends. def listen(self):
# Start the listening thread
self.listen_thread = threading.Thread(target=self. listener)
# Stop the child thread from keeping the application open
self.listen_thread.daemon = True
self.listen_thread.start()
18
Finishing the Networking classAgain, most of this code is similar to the code in the server’s Networking class. One
19
Starting the clientThe main window is started by initialising an instance of the class. Notice that we don’t need to store anything that is returned. We then start the GTK thread by calling gtk.main(). if __name__ == "__main__":
# Create an instance of the main window and start the gtk main loop
MainWindow() gtk.main()
21
That’s it!So, it’s not perfect and could be a little more robust in terms of error handling, but we have a working instant messenger server that can accept multiple clients and relay messages between them. More importantly, we have learned a bunch of new concepts and methods of working.
20
Trying it outYou’ll want a few terminals: one to start the server, and some to run clients. Once
you’ve started the server, open an instance of the client and enter 127.0.0.1:port, where ‘port’ is the port you decided to use. The server will print the port it’s listening on to make this easy. Then enter a username and click OK. Here is an example output from the server with two clients. You can use the client over a network by replacing 127.0.0.1 with the IP address of the server. You may have to let the port through your computer’s firewall if it’s not working.
[liam@liam-laptop Python]$ ./IM- Server.py
Listening on port 59091 Listening for more clients Starting client thread for ('127.0.0.1', 38726)
('127.0.0.1', 38726) sent: USERNAME client1
echoing: client1 has joined. Listening for more clients Starting client thread for ('127.0.0.1', 38739)
('127.0.0.1', 38739) sent: USERNAME client2
echoing: client2 has joined. Listening for more clients
('127.0.0.1', 38739) sent: client2 says: Hi
echoing: client2 says: Hi
('127.0.0.1', 38726) sent: client1 says: Hi
echoing: client1 says: Hi ('127.0.0.1', 38726) sent: QUIT echoing: client1 has quit. Ending client thread for ('127.0.0.1', 38726) ^CTidying up
echoing: QUIT
Could not accept any more connections
('127.0.0.1', 38739) sent: echoing: client2 has quit. Ending client thread for ('127.0.0.1', 38739) difference is that we want to add some things to
the text view of our window. We do this by using the idle_add function of GObject. This allows us to call a function that will update the window running in the main thread when it is not busy. def send(self, message):
# Send a message to the server
print "Sending: {0}". format(message) try: self.socket. sendall(message) except socket.error: print "Unable to send message"
def tidy_up(self):
# We'll be tidying up if either we are quitting or the server is quitting
self.listening = False self.socket.close()
# We won't see this if it's us that's quitting as the window will be gone shortly
gobject.idle_add(self. window.add_text, "Server has quit.\n")
def handle_msg(self, data): if data == "QUIT":
# Server is quitting
self.tidy_up() elif data == "":
# Server has probably closed unexpectedly
self.tidy_up() else:
# Tell the GTK thread to add some text when it's ready
gobject.idle_add(self. window.add_text, data)
16
The client’s Networking classMuch of the client’s Networking class is similar to that of the server’s. One difference is that the class doesn’t inherit the Thread class – we just start one of its functions as a thread. class Networking():
def __init__(self, window, username, server, port):
# Set up the networking class
self.window = window self.socket = socket. socket(socket.AF_INET, socket.SOCK_ STREAM) self.socket.connect((server, port)) self.listening = True
# Tell the server that a new user has joined
self.send("USERNAME {0}". format(username))
def listener(self):
# A function run as a thread that listens for new messages
while self.listening: data = "" try: data = self.socket. recv(1024) except socket.error: "Unable to recieve data" self.handle_msg(data)
# Don't need the while loop to be ridiculously fast
94 The Python Book