I'm new to both Python and socket programming and figured setting up a simple chat server would be good practice. In addition to general critiques of the code, I have specific questions:
What are the best practices for network programming in Python? Is the object-oriented style I went with advisable or should I structure it differently?
How can I write effective unit tests for a client-server program when both client and server seem to depend on each other for execution? I've heard of mocking, but I have no firsthand experience with it and don't know whether mocking would be applicable in this case.
Normally I would have assumed that a server with multiple clients would be multithreaded, but the Python networking tutorials I could find all used select. In socket programming should I prefer to use select over threads?
server.py
import socket
import select
import sys
class Server:
def __init__(self, host, port, max_clients):
self.host = host
self.port = port
self.max_clients = max_clients
self.clients = {}
def run(self):
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.bind((self.host, self.port))
self.server.listen(self.max_clients)
print 'Listening on %s' % ('%s:%s' % self.server.getsockname())
self.clients[self.server] = 'server'
while True:
read_sockets, write_sockets, error_sockets = select.select(self.clients.keys(), [], [])
for connection in read_sockets:
if connection == self.server:
client_connection, addr = self.server.accept()
self.setup_user(client_connection)
else:
try:
message = connection.recv(4096)
if message != '':
self.broadcast(connection, '\n<' + self.clients[connection] + '>' + message)
except:
self.broadcast(connection, '\n[%s has left the chat]' % self.clients[connection])
connection.close()
del self.clients[connection]
continue
self.server.close()
def setup_user(self, connection):
try:
name = connection.recv(1024).strip()
except socket.error:
return
if name in self.clients.keys():
connection.send('Username is already taken\n')
else:
self.clients[connection] = name
self.broadcast(connection, '\n[%s has enterred the chat]' % name)
def broadcast(self, sender, message):
print message,
for connection, name in self.clients.items():
if connection != sender:
try:
connection.send(message)
except socket.error:
pass
if __name__ == '__main__':
if (len(sys.argv) < 3):
print 'Format requires: python server.py hostname portno'
sys.exit()
server = Server(sys.argv[1], int(sys.argv[2]), 10)
server.run()
client.py
import socket
import select
import sys
class Client:
def __init__(self, username):
self.username = username
def prompt(self):
sys.stdout.write('<You> ')
sys.stdout.flush()
def connect_to(self, hostname, portno):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.settimeout(2)
try:
server_socket.connect((hostname, portno))
except:
print 'Connection error'
sys.exit()
server_socket.send(self.username)
print 'Connected to host'
self.prompt()
while True:
socket_list = [sys.stdin, server_socket]
read_sockets, write_sockets, error_sockets = select.select(socket_list, [], [])
for chosen_socket in read_sockets:
if chosen_socket == server_socket:
message = chosen_socket.recv(4096)
if not message:
print 'Connection error: no data'
sys.exit()
else:
sys.stdout.write(message)
self.prompt()
else:
message = sys.stdin.readline()
server_socket.send(message)
self.prompt()
if __name__ == '__main__':
if (len(sys.argv) < 4):
print 'Format requires: python client.py username hostname portno'
sys.exit()
client = Client(sys.argv[1])
client.connect_to(sys.argv[2], int(sys.argv[3]))