I always wanted to learn about sockets and networking in general and had ‘implement a chat server’ on my todo list for a while. Naturally, when James Katz mentioned last week that he was working on IRC chat in C, I decided to do the same, but in Python.

Here’s the course James mentioned he was following – http://chi.cs.uchicago.edu/chirc/intro.html. I read first few chapters plus some of the Python socket module docs at https://docs.python.org/3/library/socket.html on my way back home on Wednesday and on Thursday morning I was already coding together with Fahri Cihan Demirci and Ivo Sánchez Checa. In just a few hours we had a working IRC server that accepted NICK and USER commands and replied with an appropriate 001 message. I then paired with Leeor Baskin to implement QUIT command and make the server persistent. Now users could join and quit, and the server kept running.

I spent the rest of the day working on other things (mostly deep learning course on udacity and some problem solving practice) but took an opportunity to present the IRC server in the afternoon – turns out Thursday is presentations day. It went really well and we all shared some laughs when Juliano Bortolozzo covertly connected to the server during my presentation and started sending funny messages like ‘help! i’m stuck inside your computer!’…

Now, for some code:

class Server:
    def __init__(self):
        self.addr = None
        self.session = None
        self.commands = {'QUIT': (0, self.quit), 'NICK': (1, self.nick), 
                         'USER': (4, self.user)}

There’s an address associated with the server and it allows for one session (I wanted to start with the least possible set of features), which is the name of connected user. It lists commands that it can handle along with number of arguments for each.

There’s a method that ‘handles’ every command:

def handle(self, message, conn):
    messageparts = message.split()
    if messageparts[0] in self.commands:
        print(conn, messageparts[1:])
        self.commands[messageparts[0]][1](conn, messageparts[1:])

Commands are implemented as follows:

def nick(self, _, messageparts):
    print('got NICK command')
    self.session = messageparts[0]

def user(self, conn, args):
    print('got USER command')
    print(self.session, args[0])
    if self.session != args[0]:
        print('wrong user')
    else:
        print('User ' + self.session + ' connected')
        reply = Reply(self, {'nick': self.session, 'host': args[1]})
        message = (reply.prefix + ' ' + Reply.codes['RPL_WELCOME'] 
                   + ' :' + reply.text + ' ' + reply.nick + '!' 
                   + reply.nick + '@' + reply.host)
        conn.send(message.encode('ascii'))

def quit(self, conn, args=None):
    print('got QUIT command')
    conn.send('Bye!'.encode('ascii'))
    conn.close()
    self.session = None

I don’t list the Reply class code here and ideally I would like to move the message rendering into it. Also Reply class inherits from an ‘abstract’ class Message and I’m not really sure if I need all that complexity…

Here’s the main entry point into the program:

if __name__ == '__main__':   
    server = Server()
    s = socket.socket()
    server.addr = socket.gethostname()
    port = int(sys.argv[1])
    s.bind((server.addr, port))
    s.listen(5)
    while True:
        conn, addr = s.accept()
        print('connection from ' + str(addr))
        while True:
            try:
                message = conn.recv(1024).decode('ASCII')
                server.handle(message, conn)
            except OSError:
                print("connection closed")
                break

It’s all pretty straightforward: server is initialized, then a socket is allocated and bound to an address and a port. Another socket is created as the original one accepts a connection, messages are handled and connection is closed when client QUITs or an OSError occurs.

Share →