用Python开发你的第一款聊天软件

一、实验介绍

1、知识点

asyncore 、asynchat模块使用

wxPython 图形开发

2、实验环境

python3.5

二、原理解析

由于 Python 是一门带 GIL 的语言,所以在 Python 中使用多线程处理IO操作过多的任务并不是很好的选择。同时聊天服务器将同多个 socket 进行通信,所以我们可以基于 asyncore 模块实现聊天服务器。aysncore 模块是一个异步的 socket 处理器,通过使用该模块将大大简化异步编程的难度。asynchat 模块在 asyncore 模块的基础上做了进一步封装,简化了基于文本协议的通信任务的开发难度。

既然要开发聊天程序,那必然需要设计聊天时使用的协议。为了简单起见,我们将要开发的聊天服务器只支持文本协议,通过command message的方式调用相关的操作。比如如果客户端发送以下文本,将执行相应的操作

# 登录操作login\n# 在聊天室中发表 hello 内容say hello\n# 查看聊天室在线用户look\n# 退出登录logout\n

以上协议流中,login, say, look, logout就是相关协议代码。

然后使用下面的命令在/home/shiyanlou/Code目录下创建我们需要的server.py和client.py文件:

$ touch ~/Code/server.py$ touch ~/Code/client.py

三、服务器类

这里我们首先需要一个聊天服务器类,通过继承 asyncore 的 dispatcher 类来实现,我们编写 server.py 文件:

importasynchatimportasyncore# 定义端口PORT =6666# 定义结束异常类classEndSession(Exception):passclassChatServer(asyncore.dispatcher):"""

    聊天服务器

    """def__init__(self, port):asyncore.dispatcher.__init__(self)# 创建socketself.create_socket()# 设置 socket 为可重用self.set_reuse_addr()# 监听端口self.bind(('', port))        self.listen(5)        self.users = {}        self.main_room = ChatRoom(self)defhandle_accept(self):conn, addr = self.accept()        ChatSession(self, conn)

这里需要补充说明的是,对于asyncore和asynchat模块来讲,在 python3.6 中,使用asyncio模块代替,但是实验环境中我们使用的是 python 3.5 ,也由于wxPython对于Linux 下CPython的支持,所以我们依然使用 python 3.5。

1、会话类

有了服务器类还需要能维护每个用户的连接会话,这里继承 asynchat 的 async_chat 类来实现,在server.py文件中定义,代码如下:

classChatSession(asynchat.async_chat):"""

    负责和客户端通信

    """def__init__(self, server, sock):        asynchat.async_chat.__init__(self, sock)self.server = serverself.set_terminator(b'\n')self.data = []self.name = Noneself.enter(LoginRoom(server))defenter(self, room):# 从当前房间移除自身,然后添加到指定房间try:cur =self.room        exceptAttributeError:passelse:cur.remove(self)self.room = room        room.add(self)defcollect_incoming_data(self, data):# 接收客户端的数据self.data.append(data.decode("utf-8"))deffound_terminator(self):# 当客户端的一条数据结束时的处理line =''.join(self.data)self.data = []try:self.room.handle(self, line.encode("utf-8"))# 退出聊天室的处理exceptEndSession:self.handle_close()defhandle_close(self):# 当 session 关闭时,将进入 LogoutRoomasynchat.async_chat.handle_close(self)self.enter(LogoutRoom(self.server))

2、协议命令解释器

在之前的分析中,我们设计了聊天服务器的协议,我们需要实现协议命令的相应方法,具体来说就是处理用户登录,退出,发消息,查询在线用户的代码。在server.py文件中定义,

classCommandHandler:"""

    命令处理类

    """defunknown(self, session, cmd):# 响应未知命令# 通过 aynchat.async_chat.push 方法发送消息session.push(('Unknown command {} \n'.format(cmd)).encode("utf-8"))defhandle(self, session, line):line = line.decode()# 命令处理ifnotline.strip():returnparts = line.split(' ',1)        cmd = parts[0]try:            line = parts[1].strip()exceptIndexError:            line =''# 通过协议代码执行相应的方法method = getattr(self,'do_'+ cmd,None)try:            method(session, line)exceptTypeError:            self.unknown(session, cmd)

3、房间

接下来就需要实现聊天室的房间了,这里我们定义了三种房间,分别是用户刚登录时的房间、聊天的房间和退出登录的房间,这三种房间都继承自 CommandHandler,在server.py文件中定义,代码如下:

classRoom(CommandHandler):"""

    包含多个用户的环境,负责基本的命令处理和广播

    """def__init__(self, server):self.server = server        self.sessions = []defadd(self, session):# 一个用户进入房间self.sessions.append(session)defremove(self, session):# 一个用户离开房间self.sessions.remove(session)defbroadcast(self, line):# 向所有的用户发送指定消息# 使用 asynchat.asyn_chat.push 方法发送数据forsessioninself.sessions:            session.push(line)defdo_logout(self, session, line):# 退出房间raiseEndSessionclassLoginRoom(Room):"""

    处理登录用户

    """defadd(self, session):# 用户连接成功的回应Room.add(self, session)# 使用 asynchat.asyn_chat.push 方法发送数据session.push(b'Connect Success')defdo_login(self, session, line):# 用户登录逻辑name = line.strip()# 获取用户名称ifnotname:            session.push(b'UserName Empty')# 检查是否有同名用户elifnameinself.server.users:            session.push(b'UserName Exist')# 用户名检查成功后,进入主聊天室else:            session.name = name            session.enter(self.server.main_room)classLogoutRoom(Room):"""

    处理退出用户

    """defadd(self, session):# 从服务器中移除try:delself.server.users[session.name]exceptKeyError:passclassChatRoom(Room):"""

    聊天用的房间

    """defadd(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)defremove(self, session):# 广播用户离开Room.remove(self, session)        self.broadcast((session.name +' has left the room.\n').encode("utf-8"))defdo_say(self, session, line):# 客户端发送消息self.broadcast((session.name +': '+ line +'\n').encode("utf-8"))defdo_look(self, session, line):# 查看在线用户session.push(b'Online Users:\n')forotherinself.sessions:            session.push((other.name +'\n').encode("utf-8"))if__name__ =='__main__':    s = ChatServer(PORT)try:        print("chat serve run at '0.0.0.0:{0}'".format(PORT))        asyncore.loop()exceptKeyboardInterrupt:        print("chat server exit")

四、登陆窗口

完成了服务器端后,就需要实现客户端了。客户端将基于 wxPython 模块实现。 wxPython 模块是wxWidgetsGUI 工具的 Python 绑定。所以通过 wxPython 模块我们就可以实现 GUI 编程了。同时我们的聊天协议基于文本,所以我们和服务器之间的通信将基于 telnetlib 模块实现。

登录窗口通过继承 wx.Frame 类来实现,编写client.py文件,代码如下:

import wximport telnetlibfrom time import sleepimport _thread as threadclassLoginFrame(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()deflogin(self, event):# 登录处理try:serverAddress =self.serverAddress.GetLineText(0).split(':')            con.open(serverAddress[0], port=int(serverAddress[1]), timeout=10)            response = con.read_some()ifresponse != b'Connect Success':self.showDialog('Error','Connect Fail!', (200,100))returncon.write(('login '+ str(self.userName.GetLineText(0)) +'\n').encode("utf-8"))            response = con.read_some()ifresponse == 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))        exceptException:self.showDialog('Error','Connect Fail!', (95,20))defshowDialog(self, title, content, size):# 显示错误信息对话框dialog = wx.Dialog(self, title=title, size=size)        dialog.Center()        wx.StaticText(dialog, label=content)        dialog.ShowModal()

1、聊天窗口

聊天窗口中最主要的就是向服务器发消息并接受服务器的消息,这里通过子线程来接收消息,在client.py文件中定义,代码如下:

classChatFrame(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()defsend(self, event):# 发送消息message = str(self.message.GetLineText(0)).strip()ifmessage !='':            con.write(('say '+ message +'\n').encode("utf-8"))            self.message.Clear()deflookUsers(self, event):# 查看当前在线用户con.write(b'look\n')defclose(self, event):# 关闭窗口con.write(b'logout\n')        con.close()        self.Close()defreceive(self):# 接受服务器的消息whileTrue:            sleep(0.6)            result = con.read_very_eager()ifresult !='':                self.chatFrame.AppendText(result)if__name__ =='__main__':    app = ICANN Verification Required()    con = telnetlib.Telnet()    LoginFrame(None,-1, title="Login", size=(320,250))    app.MainLoop()

五、执行

首先,我们执行server.py,如下图所示:

用Python开发你的第一款聊天软件_第1张图片

这时,我们再打开一个终端,执行client.py文件,如下图:

用Python开发你的第一款聊天软件_第2张图片

输入对应的信息之后,点击Login,再次重复上一步骤,使用另一用户名shiyanlou002登陆,如下图:

用Python开发你的第一款聊天软件_第3张图片

在最终的示例中,我们可以分别通过shiyanlou001和shiyanlou002的客户端发送消息,此时,所有的在线用户都可以收到对应的消息。

六、小结

最后就可以运行程序进行聊天了,注意需要先启动服务器再启动客户端。这个项目中使用了 asyncore 的 dispatcher 来实现服务器,asynchat 的 asyn_chat 来维护用户的连接会话,用 wxPython 来实现图形界面,用 telnetlib 来连接服务器,在子线程中接收服务器发来的消息,由此一个简单的聊天室程序就完成了。

作者:实验楼

链接:https://www.jianshu.com/p/ad5231ef8ba8

來源:

著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

你可能感兴趣的:(用Python开发你的第一款聊天软件)