这一篇教程,我们继续使用Python完成带有更多功能的聊天室。
因为功能比较多,这里我们先把功能归类,然后在此基础上编写代码。
分类示意图:
上图所示,在新的功能中,我们要支持一些命令。
所以,需要一个对命令进行处理的类(CMDHandler)。
然后,房间实际上有三个,一个用于用户登入的房间(CheckInRoom),一个用于用户登出的房间(CheckOutRoom),还有就是进行聊天的房间(ChatRoom)。
房间都要包含一些功能,例如进入房间、离开房间、广播以及退出聊天室。
所以,我们抽象出一个房间类(Room),定义这些功能,再让具体的房间去继承。
除了以上所述内容,我们还要处理每一个来自客户端的连接与通信,这就需要创建聊天会话的类(ChatSession)和结束会话的类(EndSession)。
最后,还有启动服务创建与客户端连接的类(ChatServer)。
有了功能的具体划分之后,我们就依次来实现各个类。
1、导入模块
示例代码:
from asyncore import dispatcher
from asynchat import async_chat
import asyncore
'''
想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
'''
2、CMDHandler类
在前面的示意图中,我们看到了如下命令:
- login <用户名>:登录
- logout:退出
- say <发言内容>:发言
- look:查看当前房间中的在线用户
- who:查看当前服务器中的所有在线用户
注意:在当前案例中,我们只创建一个聊天室,所以房间中的用户和服务器中的用户是一样的,也就意味着look和who这两个命令的效果是相同的。
对应上述这些命令,我们需要编写相应的方法。
为了能够简单的调用命令所对应的方法,我们可以将方法名定义为:
- login:do_login()
- logout:do_logout()
- say:do_say()
- look:do_look()
- who:do_who()
如上所述,每个方法名都是前缀“do_”和命令m名称组成。
所以,当我们获取到来自客户端的数据,就要对数据进行分析,获取数据中包含的命令,并根据不同的命令调用不同的方法。
不过,我们还要想到,用户可能会有错误的输入,没有输入命令,或者输入了错误的命令,这种情况我们也需要进行处理。
接下来,大家可以通过示例代码中的注释了解整个处理过程。
示例代码:
'''
想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
'''
class CMDHandler:
def unknown(self, session, cmd): # 定义未知命令的处理方法
session.push('不支持命令:{}\r\n请重新输入!\r\n'.format(cmd).encode('GBK')) # 向客户端推送错误提示
def handle(self, session, data): # 定义命令的处理方法
if data.strip(): # 判断去除空格后是否还有数据
parts = data.split(' ', 1) # 对数据以空格为分割符进行分割(最大分割次数为1次)
cmd = parts[0] # 分割后的第1部分为命令
try:
line = parts[1].strip() # 将分割后的第2部分去除空格保存到变量
except IndexError: # 如果捕获索引错误
line = '' # 设置变量为空值
method = getattr(self, 'do_' + cmd, None) # 获取指定名称的方法对象
try:
method(session, line) # 调用获取到的方法对象
except TypeError: # 如果捕获类型错误(没有找到方法)
self.unknown(session, cmd) # 调用未知命令的处理方法
3、Room类
这个类要定义房间的功能。
示例代码:
class Room(CMDHandler):
def __init__(self, server): # 定义初始化功能
self.server = server # 保存传入的服务器对象
self.sessions = [] # 初始化会话列表
def add(self, session): # 定义单个用户进入房间的处理方法
self.sessions.append(session) # 将单个用户的会话添加到会话列表
def remove(self, session): # 定义单个用户离开房间的处理方法
self.sessions.remove(session) # 从会话列表中移除单个用户的会话
def broadcast(self, line): # 定义广播信息的处理方法
for session in self.sessions: # 遍历所有的用户会话
session.push(line.encode('GBK')) # 向每一个用户会话广播信息(注意编码)
def do_logout(self, session, line): # 定义退出命令的处理方法
raise EndSession # 抛出结束会话的异常
这里要注意remove()方法和do_logout()方法的区别。
remove()方法是从一个房间离开的方法,例如离开CheckInRoom进入ChatRoom就会调用这个方法。
do_logout()方法是关闭客户端与服务器的连接对话的方法。
3、EndSession类
在上一段代码中我们看到了EndSession,它是我们定义的异常类,所以这个类要继承Exception类。
在这个类中,无需添加什么内容。
示例代码:
class EndSession(Exception): pass
4、CheckInRoom类
为了避免和登录这个动作混淆,这里我使用了CheckInRoom这个名称。
这就好像我们去开房,要先在登记处的房间进行登记,然后才能进入入住的房间。
在这个类中,我们要实现登录的相关处理,包括:
- 用户进入房间时发送欢迎信息
- 未使用login命令登录时的处理
- 使用login命令登录时的的处理
并且,使用login命令登录时也有不同的情形需要处理:
- 未带有用户名
- 用户名已使用
- 正确输入
接下来,大家还是通过代码中的注释理解这些功能的实现。
示例代码:
'''
想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
'''
class CheckInRoom(Room):
def add(self, session): # 重写超类的方法
Room.add(self, session) # 重载超类的方法
session.push('欢迎进入{}聊天室!\r\n'.format(self.server.name).encode('GBK')) # 推送欢迎信息
def unknown(self, session, cmd): # 重写位置命令的处理方法
session.push('请使用命令 <login 用户名> 进行登录!\r\n'.encode('GBK')) # 推送命令错误的提示
def do_login(self, session, line): # 定义登录命令的处理方法
name = line.strip() # 数据中命令后方的内容去重空格后作为用户名
if not name: # 如果没有内容
session.push('请输入用户名!\r\n'.encode('GBK')) # 推送提示
elif name in self.server.users: # 如果服务器的用户列表中已有这个用户名
session.push('用户名 <{}> 已被使用!\r\n'.format(name).encode('GBK')) # 推送提示
else: # 正确输入时
session.name = name # 将用户名保存到会话
session.enter(self.server.main_room) # 将会话进入到主聊天房间
在上方代码中,enter()方法我们还没有定义,它是一个会话进入某个房间的方法,需要在ChatSession类中定义这个方法。
5、ChatRoom类
这个是聊天室的类,主要实现功能如下:
- 一个会话进入当前房间的处理方法
- 一个会话离开当前房间的处理方法
- 处理say命令的方法
- 处理look命令的方法
- 处理who命令的方法
大家通过示例代码中的注释理解实现过程。
示例代码:
class ChatRoom(Room):
def add(self, session):
self.broadcast('用户 <' + session.name + '> 进入聊天室!\r\n') # 广播用户进入房间的信息
Room.add(self, session) # 重载超类的方法
self.server.users[session.name] = session # 向服务器的用户列表添加会话的用户名
def remove(self, session):
self.broadcast('用户 <' + session.name + '> 离开聊天室!\r\n') # 广播用户离开房间的信息
Room.remove(self, session) # 重载超类的方法
def do_say(self, session, line): # 定义say命令的处理方法
self.broadcast(session.name + ':' + line + '\r\n') # 广播用户的发言信息
def do_look(self, session, line): # 定义look命令的处理方法
session.push('当前房间在线用户\r\n'.encode('GBK'))
session.push('----------------------\r\n'.encode('GBK'))
for user in self.sessions: # 遍历所有会话
session.push('{}\r\n'.format(user.name).encode('GBK')) # 推送每个会话中的用户名信息
def do_who(self, session, line): # 定义who命令的处理方法
session.push('当前服务器在线用户\r\n'.encode('GBK'))
session.push('----------------------\r\n'.encode('GBK'))
for user in self.server.users: # 遍历服务器用户列表
session.push('{}\r\n'.format(user).encode('GBK')) # 推送服务器中所有的用户名信息
注意,上方代码中的“server.users”是服务器的用户列表,users需要在ChatServer类中定义。
6、CheckOutRoom类
还是以开房举例(虽然我很少干这个事),退房时,我们不能一走了之,也要到登记处的房间进行登记。
在这个类中,我们只需要定义进入登记房间的方法。
示例代码:
class CheckOutRoom(Room):
def add(self, session): # 重写进入房间的方法
try:
del self.server.users[session.name] # 从服务器用户列表中移除当前会话的用户名
except KeyError:
pass
7、ChatSession类
在上一篇教程中我们编写过这个类,现在,我们再给它添加一些新的功能。
在这个类中,我们要对会话进行处理,包括会话进入房间、会话中的数据处理以及会话关闭的处理。
新增部分,在示例代码中添加了注释,大家还是通过注释进行理解。
class ChatSession(async_chat):
def __init__(self, server, sock):
async_chat.__init__(self, sock)
self.server = server # 保存传入服务器对象
self.set_terminator('\r\n'.encode())
self.data = []
self.name = None # 创建会话的用户名变量
self.enter(CheckInRoom(server)) # 将当前会话添加到登录的房间
def enter(self, room): # 定义进入房间的方法
try:
current = self.room # 获取当前会话的房间
except AttributeError: # 如果当前会话没有在任何房间
pass
else: # 如果当前会话没有在某个房间
current.remove(self) # 从当前会话所在的房间移除当前会话
self.room = room # 设置当前会话的房间为传入的房间
room.add(self) # 传入的房间添加当前会话
def collect_incoming_data(self, data):
self.data.append(data.decode())
def found_terminator(self):
line = ''.join(self.data)
self.data = []
try:
self.room.handle(self, line) # 处理客户端发来的数据
except EndSession: # 捕获结束会话异常
self.handle_close() # 调用关闭连接的处理方法
def handle_close(self): # 重写关闭连接的处理方法
async_chat.handle_close(self) # 重载超类的方法
self.enter(CheckOutRoom(self.server)) # 将当前会话进入退出登记的房间
8、ChatSever类
在上一篇教程中我们页编写过这个类,同样,我们在这里要添加和修改的内容。
具体添加的内容,大家看代码中的注释。
示例代码:
class ChatSever(dispatcher):
def __init__(self, name, port):
dispatcher.__init__(self)
self.create_socket()
self.set_reuse_addr()
self.bind(('', port))
self.listen(5)
self.name = name # 保存传入的聊天室服务器名称
self.users = {} # 初始化服务器用户列表
self.main_room = ChatRoom(self) # 设定主聊天房间
def handle_accept(self):
conn, addr = self.accept()
ChatSession(self, conn) # 将服务器对象和连接对象传入会话对象
以上就是所有类的编写。
最后,我们就可以运行服务器和Telnet客户端进行测试了。
示例代码:(启动服务器)
if __name__ == '__main__':
name = '心动'
port = 6666
server = ChatSever(name, port)
try:
asyncore.loop()
except KeyboardInterrupt:
print('服务器已关闭!')