这个项目目的是编写一个聊天服务器,该聊天服务器的功能有:
服务器能同时接收来自不同用户的连接
允许用户同时操作
能够解释命令,例如,say或者logout命令
服务器容易扩展
这个项目里面我们会使用到的模块式asyncore,使用asyncore框架,程序可以处理同时连接多个用户。asyncore框架基于一些底层的机制,这些机制允许服务器逐个的对于连接上的用户进行服务。在处理下一个连接前,它并不读取当前用户的所有可用数据,而只读取一部分。除此之外,服务器只从那些需要读取数据的套接字中读取。程序就这样一遍遍的循环。写入操作同理。只使用socket和select模块就可以实现,但是asyncore和asynchat提供了一个可以处理所有细节的有用框架。
首先我们需要一个客户端来测试服务器,在windows下我们可以用telnet服务端,在unix内也可以用telnet。
初次实现:
首先我们需要创建两个类:一个作为聊天服务器,一个用于表示每个聊天会话(以连接用户)
要生成基本的ChatServer类,需要继承asyncore模块中的dispatcher类,dispatcher基本上就是一个套接字对象,但是可以利用它额外的事件处理特性。
下面是可以接受连接的服务器代码
1 from asyncore import dispatcher 2 import socket, asyncore 3 4 class ChatServer(dispatcher): 5 def handle_accept(self): 6 conn, addr = self.accept() 7 print ‘Connection attempt from ‘,addr[0] 8 9 s = ChatServer() 10 s.create_socket(socket.AF_INET, socket.SOCK_STREAM) 11 s.bind((‘‘.5005)) 12 s.listen(5) 13 asyncore.loop()
handle_accpet方法会调用允许客户端连接的self.accept函数。她会返回一个连接和一个地址。handle_accept方法知识打印有关连接尝试的信息。
当我们运行这个服务器并且用客户端连接他的时候,我们可以再服务器的控制台上看到返回的连接信息。
ChatSession类的主要任务是为每一个连接创建一个对象,该对象负责收集来自客户端的数据并且进行响应。我们可以自己继承dispatcher并且重写一些方法来实现这个功能,但是幸运的是已经有现成的模块能够完成绝大多数的工作,asynchat
下面便是一个简单的聊天服务器
1 # -*- coding:utf-8 -*- 2 ‘‘‘ 3 xianghang 4 2015.4.9 5 虚拟聊天室 6 ‘‘‘ 7 8 from asyncore import dispatcher 9 from asynchat import async_chat 10 import socket, asyncore 11 12 PORT = 5005 13 NAME = ‘TestChat‘ 14 class ChatSessioin(async_chat): 15 ‘‘‘ 16 处理服务器和一个用户之间连接的类 17 ‘‘‘ 18 def __init__(self, server, sock): 19 #标准设置任务 20 async_chat.__init__(self, sock) 21 self.server = server 22 self.set_terminator(‘\r\n‘) 23 self.data = [] 24 #问候用户 25 self.push(‘Welcome to %s\r\n‘ % self.server.name) 26 27 def collect_incoming_data(self, data): 28 self.data.append(data) 29 30 def found_terminator(self): 31 ‘‘‘ 32 如果发现了一个终止对象,也就意味着读入了一个完整的行,将广播给所有人 33 ‘‘‘ 34 line = ‘‘.join(self.data) 35 self.data = [] 36 self.server.broadcast(line) 37 38 def handle_close(self): 39 async_chat.handle_close(self) 40 self.server.disconnect(self) 41 42 class ChatServer(dispatcher): 43 ‘‘‘ 44 接收连接并且产生单个会话的类。他还会处理到其他会话的广播 45 ‘‘‘ 46 def __init__(self, port, name): 47 #标准设置任务 48 dispatcher.__init__(self) 49 self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 50 self.set_reuse_addr() 51 self.bind((‘‘, port)) 52 self.listen(5) 53 self.name = name 54 self.sessions = [] 55 def disconnect(self, session): 56 self.sessions.remove(session) 57 58 def broadcast(self, line): 59 for session in self.sessions: 60 session.push(line+‘\r\n‘) 61 62 def handle_accept(self): 63 conn, addr = self.accept() 64 self.sessions.append(ChatSessioin(self, conn)) 65 66 if __name__ == ‘__main__‘: 67 s = ChatServer(PORT, NAME) 68 try: asyncore.loop() 69 except KeyboardInterrupt: print
运行这个程序后会发现,客户端虽然能够显示其它用户发送的信息,但不能识别是哪一个用户,而且整个程序没有登录登出等功能,下面我们就来扩展这些功能。
再次实现:
首先我们把服务器分为三个房间,登录房间,聊天室,登出房间,三个房间分别为三个类,这些类的父类拥有添加和删除会话 的功能。代码如下
1 class Room(CommandHandler): 2 ‘‘‘ 3 包括一个或多个用户(会话)的泛型环境,他负责基本的命令处理和广播 4 ‘‘‘ 5 6 def __init__(self, server): 7 self.server = server 8 self.sessions = [] 9 10 def add(self, session): 11 ‘一个会话用户已进入房间‘ 12 self.sessions.append(session) 13 14 def remove(self, session): 15 ‘一个会话用户已离开房间‘ 16 self.sessions.remove(session) 17 18 def broadcast(self, line): 19 ‘向房间中的所有会话发送一行‘ 20 for session in self.sessions: 21 session.push(line) 22 23 def do_logout(self, session, line): 24 ‘响应logout命令‘ 25 raise EndSession
登录房间继承room父类
1 class LoginRoom(Room): 2 ‘‘‘ 3 为刚刚连接上的用户准备的房间 4 ‘‘‘ 5 def add(self, session): 6 Room.add(self, session) 7 #当用户进入时,问候他 8 self.broadcast(‘Welcome to %s \r\n‘ % self.server.name) 9 10 def unknown(self, session, cmd): 11 #所有未知命令(除了login或者logout除外) 12 #会导致一个警告 13 session.push(‘Please log in \nUse "login <nick>"\r\n‘) 14 15 def do_login(self, session, line): 16 name = line.strip() 17 #确保用户输入了名字 18 if not name: 19 session.push(‘Please enter a name\r\n‘) 20 #确保用户名没有被使用 21 elif name in self.server.users: 22 session.push(‘The name "%s" is taken.\r\n‘ % name) 23 session.push(‘Please try again.\r\n‘) 24 else: 25 #名字没问题,所以存储在会话中 26 #将用户移动到主聊天室 27 session.name = name 28 session.enter(self.server.main_room)
登录房间是让用户登录时所在的房间,在该房间内接收用户的登录命令,登陆成功后进入聊天房间。同样的,也有登出房间登出房间同理。
聊天室房间负责处理say,look命令,say命令可以让用户在该房间内发言,并显示在其他的客户端上;look命令可以让用户查看当前聊天室所存在的用户。
1 class ChatRoom(Room): 2 ‘‘‘ 3 为多用户相互聊天准备房间 4 ‘‘‘ 5 def add(self, session): 6 #告诉所有人有新用户进入: 7 self.broadcast(session.name + ‘ has entered the room\r\n‘) 8 self.server.users[session.name] = session 9 Room.add(self, session) 10 11 def remove(self, session): 12 Room.remove(self, session) 13 #告诉所有人有用户离开 14 self.broadcast(session.name + ‘ has left the room.\r\n‘) 15 16 def do_say(self, session, line): 17 self.broadcast(session.name +‘:‘ + line + ‘\r\n‘) 18 19 def do_look(self, session, line): 20 ‘处理look命令,该命令用于查看谁在房间‘ 21 session.push(‘The following are in this room:\r\n‘) 22 for other in self.sessions: 23 session.push(other.name + ‘\r\n‘)
整个服务器程序的结构如下图: