Telematica: finished client side section of report.

parent 1da87927
......@@ -2,14 +2,15 @@ import asyncore, socket
from Queue import Queue
import re
import random
from asyncbase import AsyncBase
from asyncbase import AsyncBase, MAJOR_VERSION, MINOR_VERSION
# Socket error "Resource temporarily unavailable": try again ;)
EAGAIN = 11
class ClientConnection(object, AsyncBase, asyncore.dispatcher):
"""
Client connection implementing the asynchronous chat connection according to
the provided chat protocol.
"""
def __init__(self):
......@@ -28,6 +29,10 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
self.user = 'Anonymous' + str(int(random.random()*10000))
def connect(self, addr):
"""
Connect to a chat server, specified by the address tuple (hostname,
port).
"""
self.host = addr[0]
self.port = addr[1]
super(ClientConnection, self).connect(addr)
......@@ -36,12 +41,14 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
"""
Retreive list of users active at the chat server.
"""
self.request_user_list_sent = True
self.retrieved_user_list = False
self.send_queue.put('NAMES')
def username(self, name=''):
"""
Get or set the user name of the client.
"""
if not name:
return self.user
self.user = name
......@@ -51,13 +58,19 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
def handle_connect(self):
"""
Called when a connection is established.
Event callback, which is called when a connection is established.
"""
if 'connect' in self.event_list:
self.event_list['connect'](self)
def handle_error(self):
"""
Event callback, which is triggered when an error occured. This callback
function will transform the exception's traceback into a condensed
string and will append that string to the debug log. Also the info bar
is changed to notify the user of the error occured.
"""
if hasattr(self, 'main'):
import sys
t, v, tb = sys.exc_info()
......@@ -76,14 +89,18 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
% (function, file, line))
self.debug_log(info)
self.main.display_info(str(v))
#self.main.execute('disconnect')
#self.main.display_info('Error raised due to a broken or refused'\
# + ' connection')
def handle_close(self):
"""
Event callback, which will be called when the connection is about to
be closed.
"""
self.close()
def handle_read(self):
"""
Event callback, which is triggered when data is available to read.
"""
buf = ''
multi_line = False
response_format = False
......@@ -129,11 +146,18 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
self.parse_notification(buf)
def verify_server(self, buf):
"""
Helper function used to verify the server version according to the
client major and minor version number. If the server version does not
match, disconnect immediately.
"""
match = re.match('^CHAT/(\d\.\d)/([^\x00-\x1F/:]+)$', buf)
if not match:
if not match \
or match.group(1) != '%d.%d' % (MAJOR_VERSION, MINOR_VERSION):
self.debug_log('Failed to verify connection')
sys.exit(-1)
self.main.execute('disconnect')
return
self.debug_log('Server version is %s' % match.group(1))
......@@ -144,6 +168,10 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
self.send_queue.put('CHAT')
def parse_response(self, buf):
"""
Helper function used to parse the server response. This way, message
parsing is separated from the response logic.
"""
if 'response' in self.event_list:
self.event_list['response'](self, buf)
......@@ -167,7 +195,6 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
self.request_user_list_sent = True
self.retrieve_users()
else:
# TODO: process user list
self.retrieved_user_list = True
msg = 'Users: [%s]' % '] ['.join(buf.split('\r\n')[1:])
if 'info' in self.event_list:
......@@ -176,6 +203,10 @@ class ClientConnection(object, AsyncBase, asyncore.dispatcher):
self.debug_log(msg)
def parse_notification(self, buf):
"""
Helper function used to parse the server notifications. This way,
message parsing is separated from the notification logic.
"""
match = re.match('^SAY ([^\r\n:/]+)/(.+)', buf)
if match:
if 'message' in self.event_list:
......
import asyncore, socket
from Queue import Queue
# Major and minor version of this server.
MAJOR_VERSION = 1
MINOR_VERSION = 0
class AsyncBase(asyncore.dispatcher):
"""
Base class of the asynchronous connection of the chat system. The server and
......
......@@ -5,10 +5,76 @@ Implementation details
Client side
----------
The client side of our chat system is generally divided into two parts:
1. **Connection to server.** We use an asynchronous connection to the server
(and also for the server itself to handle the clients; see "Server side"
section) to send and receive messages from/to the chat server.
2. **User interface for user.** We use a text-based user interface built with
the python bindings for the curses library, which is the de-facto standard
for portable advanced terminal handling.
The user interface depends on the client connection and the connection itself is
a completely independent component. Due to its event mechanism (see below), the
client connection can easily be used for different user interfaces.
Client chat connection
~~~~~~~~~~~~~~~~~~~~~~
Foo
First, we implemented the chat connection and we built the user interface on
top of the connection. This way, we're able to test the connection with the
test server provided by the assignment. Otherwise, debugging the connection is
much harder; text-based interface means the entire console is used to display
the interface, so it's not possible to report any exceptions to stdout.
Since some functionality of the chat connection is used in both the client and
server, it is wise to create a base class containing those methods: *AsyncBase*.
We used the *asyncore.dispatcher* class (part of the python standard library) to
create a nice abstraction layer on top of the socket. The abstraction layer is
event-oriented and provides various event callback functions:
1. **handle_write**.
2. **handle_read**.
3. **handle_connect**.
4. **handle_close**.
Because those function callbacks have self-documenting names, the callbacks are
not further discussed here (for more information about those callbacks, see the
implementation in *async.py* and *asyncbase.py*). This event callback mechanism
allowed us to easily built an interactive user interface on top of the
connection. Those various callback functions will handle the connection-specific
logic and triggers a new UI event based on the event occurred.
For example, if an authentication response is sent to the client (when the
client sent **USER foo\\r\\n** to the server), the function **handle_read()** is
called. This callback will parse the response and, if the authentication is done
successfully, trigger an *authenticated* UI event (which will be received by the
text-based user interface). This event mechanism allows a developer to built
different user interfaces on top to a client connection, instead of developing
the response handling over and over again.
The connection runs in a separated thread from the user interface thread. The
reason behind this decision is based on experiences with user interface; when an
user presses a key (and holds it), the connection will not respond until the
user releases the key pressed. If the user interface is separated into a
different thread, the connection is able to respond (due to context switches)
and the user can presses a key as long as he wants. Note: there are different
ways to deal with this problem, but this is a solid solution and it is easy to
implement.
In order to send chat messages to the chat server in a thread-safe manner, we
used a synchronized queue as message buffer, since we're using the two threads
(one for the UI and one for the connection to the server). This synchronized
queue is part of the python standard library and called Queue.Queue. The user
interface adds its messages to the message queue and the client connection reads
the messages from the queue in FIFO (first-in, first-out) order. The client
connection does take messages from the queue, if its local message buffer is
empty (which means all its data is sent to the server or the connection is
just initialised).
Text-based interface (using curses)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
......
......@@ -8,15 +8,11 @@ import re
import socket
import sys
from asyncbase import AsyncBase
from asyncbase import AsyncBase, MAJOR_VERSION, MINOR_VERSION
# Greeting message sent to a connected client.
GREETING_MSG = 'Hi there!'
# Major and minor version of this server.
MAJOR_VERSION = 1
MINOR_VERSION = 0
class SocketError(RuntimeError):
pass
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment