一.实验介绍
1.1 项目目标
本项目使用wxpython、asynchat、_thread等模块开发一个图形界面的聊天室程序。
1.2知识点
- asyncore 、asynchat模块使用
- wxPython 图形开发
1.3项目环境
- Python3.5
二.服务器端
首先创建一个聊天服务器类,通过继承asyncore.dispatcher类来实现,创建server.py文件,写入如下代码:
import asynchat import asyncore # 定义端口 PORT = 2222 # 定义结束异常类 class EndSession(Exception): pass class ChatServer(asyncore.dispatcher): """ 聊天服务器 """ def __init__(self, port): asyncore.dispatcher.__init__(self) # 创建socket self.create_socket() # 设置 socket 为可重用 self.set_reuse_addr() # 监听端口 self.bind(('0.0.0.0', port)) self.listen(5) self.users = {} self.main_room = ChatRoom(self) def handle_accept(self): conn, addr = self.accept() ChatSession(self, conn)
上述代码完成后,在服务器中还需要能维护每个用户的链接会话,这个功能使用asynchat.async_chat类来实现,在server.py中继续写下如下代码:
class ChatSession(asynchat.async_chat): """ 负责和客户端通信 """ def __init__(self, server, sock): asynchat.async_chat.__init__(self, sock) self.server = server self.set_terminator(b'\n') self.data = [] self.name = None self.enter(LoginRoom(server)) def enter(self, room): # 从当前房间移除自身,然后添加到指定房间 try: cur = self.room except AttributeError: pass else: cur.remove(self) self.room = room room.add(self) def collect_incoming_data(self, data): # 接收客户端的数据 self.data.append(data.decode("utf-8")) def found_terminator(self): # 当客户端的一条数据结束时的处理 line = ''.join(self.data) self.data = [] try: self.room.handle(self, line.encode("utf-8")) # 退出聊天室的处理 except EndSession: self.handle_close() def handle_close(self): # 当 session 关闭时,将进入 LogoutRoom asynchat.async_chat.handle_close(self) self.enter(LogoutRoom(self.server))
接下来实现命令处理类,实现用户的登陆,退出。发消息、查询在线用户的功能,并且使得在房间中的用户类可以继承该命令处理类。在server.py中继续写入如下代码:
class CommandHandler: """ 命令处理类 """ def unknown(self, session, cmd): # 响应未知命令 # 通过 aynchat.async_chat.push 方法发送消息 session.push(('Unknown command {} \n'.format(cmd)).encode("utf-8")) def handle(self, session, line): line = line.decode() # 命令处理 if not line.strip(): return parts = line.split(' ', 1) cmd = parts[0] try: line = parts[1].strip() except IndexError: line = '' # 通过协议代码执行相应的方法 method = getattr(self, 'do_' + cmd, None) try: method(session, line) except TypeError: self.unknown(session, cmd) class Room(CommandHandler): """ 包含多个用户的环境,负责基本的命令处理和广播 """ 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): # 向所有的用户发送指定消息 # 使用 asynchat.asyn_chat.push 方法发送数据 for session in self.sessions: session.push(line) def do_logout(self, session, line): # 退出房间 raise EndSession class LoginRoom(Room): """ 处理登录用户 """ def add(self, session): # 用户连接成功的回应 Room.add(self, session) # 使用 asynchat.asyn_chat.push 方法发送数据 session.push(b'Connect Success') def do_login(self, session, line): # 用户登录逻辑 name = line.strip() # 获取用户名称 if not name: session.push(b'UserName Empty') # 检查是否有同名用户 elif name in self.server.users: session.push(b'UserName Exist') # 用户名检查成功后,进入主聊天室 else: session.name = name session.enter(self.server.main_room) class LogoutRoom(Room): """ 处理退出用户 """ def add(self, session): # 从服务器中移除 try: del self.server.users[session.name] except KeyError: pass class ChatRoom(Room): """ 聊天用的房间 """ def add(self, session): # 广播新用户进入 session.push(b'Login Success') self.broadcast((session.name + ' has entered the room.\n').encode("utf-8")) self.server.users[session.name] = session Room.add(self, session) def remove(self, session): # 广播用户离开 Room.remove(self, session) self.broadcast((session.name + ' has left the room.\n').encode("utf-8")) def do_say(self, session, line): # 客户端发送消息 self.broadcast((session.name + ': ' + line + '\n').encode("utf-8")) def do_look(self, session, line): # 查看在线用户 session.push(b'Online Users:\n') for other in self.sessions: session.push((other.name + '\n').encode("utf-8"))
在server.py的最下民输入如下代码,启动server端。
if __name__ == '__main__': s = ChatServer(PORT) try: print("chat serve run at '127.0.0.1:{0}'".format(PORT)) asyncore.loop() except KeyboardInterrupt: print("chat server exit")
三.客户端
完成了服务器端后,就需要实现客户端了。客户端将基于 wxPython 模块实现。 wxPython 模块是 wxWidgets GUI 工具的 Python 绑定。所以通过 wxPython 模块我们就可以实现 GUI 编程了。同时我们的聊天协议基于文本,所以我们和服务器之间的通信将基于 telnetlib 模块实现。代码如下:
import telnetlib,wx from time import sleep import _thread as thread class LoginFrame(wx.Frame): """ 登录窗口 """ def __init__(self, parent, id, title, size): # 初始化,添加控件并绑定事件 wx.Frame.__init__(self, parent, id, title) self.SetSize(size) self.Center() self.serverAddressLabel = wx.StaticText(self, label="Server Address", pos=(10, 50), size=(120, 25)) self.userNameLabel = wx.StaticText(self, label="UserName", pos=(40, 100), size=(120, 25)) self.serverAddress = wx.TextCtrl(self, pos=(120, 47), size=(150, 25)) self.userName = wx.TextCtrl(self, pos=(120, 97), size=(150, 25)) self.loginButton = wx.Button(self, label='Login', pos=(80, 145), size=(130, 30)) # 绑定登录方法 self.loginButton.Bind(wx.EVT_BUTTON, self.login) self.Show() def login(self, event): # 登录处理 try: serverAddress = self.serverAddress.GetLineText(0).split(':') con.open(serverAddress[0], port=int(serverAddress[1]), timeout=10) response = con.read_some() if response != b'Connect Success': self.showDialog('Error', 'Connect Fail!', (200, 100)) return con.write(('login ' + str(self.userName.GetLineText(0)) + '\n').encode("utf-8")) response = con.read_some() if response == b'UserName Empty': self.showDialog('Error', 'UserName Empty!', (200, 100)) elif response == b'UserName Exist': self.showDialog('Error', 'UserName Exist!', (200, 100)) else: self.Close() ChatFrame(None, 2, title='ShiYanLou Chat Client', size=(500, 400)) except Exception: self.showDialog('Error', 'Connect Fail!', (95, 20)) def showDialog(self, title, content, size): # 显示错误信息对话框 dialog = wx.Dialog(self, title=title, size=size) dialog.Center() wx.StaticText(dialog, label=content) dialog.ShowModal() class ChatFrame(wx.Frame): """ 聊天窗口 """ def __init__(self, parent, id, title, size): # 初始化,添加控件并绑定事件 wx.Frame.__init__(self, parent, id, title) self.SetSize(size) self.Center() self.chatFrame = wx.TextCtrl(self, pos=(5, 5), size=(490, 310), style=wx.TE_MULTILINE | wx.TE_READONLY) self.message = wx.TextCtrl(self, pos=(5, 320), size=(300, 25)) self.sendButton = wx.Button(self, label="Send", pos=(310, 320), size=(58, 25)) self.usersButton = wx.Button(self, label="Users", pos=(373, 320), size=(58, 25)) self.closeButton = wx.Button(self, label="Close", pos=(436, 320), size=(58, 25)) # 发送按钮绑定发送消息方法 self.sendButton.Bind(wx.EVT_BUTTON, self.send) # Users按钮绑定获取在线用户数量方法 self.usersButton.Bind(wx.EVT_BUTTON, self.lookUsers) # 关闭按钮绑定关闭方法 self.closeButton.Bind(wx.EVT_BUTTON, self.close) thread.start_new_thread(self.receive, ()) self.Show() def send(self, event): # 发送消息 message = str(self.message.GetLineText(0)).strip() if message != '': con.write(('say ' + message + '\n').encode("utf-8")) self.message.Clear() def lookUsers(self, event): # 查看当前在线用户 con.write(b'look\n') def close(self, event): # 关闭窗口 con.write(b'logout\n') con.close() self.Close() def receive(self): # 接受服务器的消息 while True: sleep(0.6) result = con.read_very_eager() if result != '': self.chatFrame.AppendText(result) if __name__ == '__main__': app = wx.App() con = telnetlib.Telnet() LoginFrame(None, -1, title="Login", size=(320, 250)) app.MainLoop()