用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,如下图所示:

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

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

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

六、小结

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

作者:实验楼

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

來源:简书

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,427评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,551评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,747评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,939评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,955评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,737评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,448评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,352评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,834评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,992评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,133评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,815评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,477评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,022评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,147评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,398评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,077评论 2 355

推荐阅读更多精彩内容