一、知识点介绍:

asyncore 、asynchat模块使用

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

  • 异步socket处理器-asyncore

python 3.6 版后已移除: 请使用asyncio模块替代

asyncore模块提供了以异步的方式写入套接字服务的客户端和服务器的基础架构。

模块主要包括:

  1. asyncore.loop(…) #用于循环监听网络事件。loop()函数负责检测一个字典,字典中保存dispatcher的实例,这个字典被称为channel。
  1. asyncore.dispatcher.__init__(self) #一个底层套接字对象的简单封装,相当于一个socket对象。此类有少数由异步循环调用的,用来事件处理的函数。

dispatcher类中的writable()和readable()在检测到一个socket可以写入或者数据到达的时候被调用,并返回一个bool值,决定是否调用handle_read或者handle_write,也就是说,一旦检测到可读或可写,就调用handle_read/handle_write。打开asyncore.py可以看到,dispatcher类中定义的方法writable和readable的定义相当的简单:

  1. asyncore.dispatcher_with_send #一个 dispatcher的子类,添加了简单的缓冲输出能力,对简单的客户端很有用。

每次创建一个dispatcher对象,都可以看做我们需要处理的一个socket(可以TCP也可以是UDP,甚至是一些不常用的),都会把自己加入到一个默认的dict里面去(当然也可以自己指定channel)。当对象被加入到channel中的时候,socket的行为都已经被定义好,程序只需要调用loop(),一切功能就实现了。

  • 异步 socket 指令/响应 处理器-asynchat

    此模块建立在asyncore模块的基础上,简化了异步客户端和服务器,并使处理元素由任意字符串终止或长度可变的协议变得更加容易。 asynchat定义了子类的抽象类async_chat,提供了collect_incoming_data()和found_terminator()方法的实现。它使用与asyncore相同的异步循环,并且两种通道类型asyncore.dispatcher和asynchat.async_chat可以在通道中自由混合。通常,asyncore.dispatcher服务器通道在接收到传入的连接请求时会生成新的asynchat.async_chat通道对象。
  1. asynchat.async_chat.__init__(self, sock) #此类是asyncore.dispatcher的抽象子类。一般使用其collect_incoming_data()和found_terminator()方法。
  1. async_chat.collect_incoming_data() #接收数据。
  1. async_chat.found_terminator() #当输入数据流符合由 set_terminator() 设置的终止条件时被调用。
  1. async_chat.set_terminator() #设置终止条件。
  1. async_chat.push() #向通道压入数据以确保其传输。

二、代码实现

服务器端

1.服务器类

这里我们首先需要一个聊天服务器类,通过继承 asyncore 的 dispatcher 类来实现

  1. import asynchat
  2. import asyncore
  3. # 定义端口
  4. PORT = 6666
  5. # 定义结束异常类
  6. class EndSession(Exception):
  7. pass
  8. class ChatServer(asyncore.dispatcher):
  9. """
  10. 聊天服务器
  11. """
  12. def __init__(self, port):
  13. asyncore.dispatcher.__init__(self)
  14. # 创建socket
  15. self.create_socket()
  16. # 设置 socket 为可重用
  17. self.set_reuse_addr()
  18. # 监听端口
  19. self.bind(('0.0.0.0', port))
  20. self.listen(5)
  21. self.users = {}
  22. self.main_room = ChatRoom(self)
  23. def handle_accept(self):
  24. conn, addr = self.accept()
  25. ChatSession(self, conn)

2.会话类

有了服务器类还需要能维护每个用户的连接会话,这里通过继承 asynchat 的 async_chat 类来实现。

  1. class ChatSession(asynchat.async_chat):
  2. """
  3. 负责和客户端通信
  4. """
  5. def __init__(self, server, sock):
  6. asynchat.async_chat.__init__(self, sock)
  7. self.server = server
  8. self.set_terminator(b'\n')
  9. self.data = []
  10. self.name = None
  11. self.enter(LoginRoom(server))
  12. def enter(self, room):
  13. # 从当前房间移除自身,然后添加到指定房间
  14. try:
  15. cur = self.room
  16. except AttributeError:
  17. pass
  18. else:
  19. cur.remove(self)
  20. self.room = room
  21. room.add(self)
  22. def collect_incoming_data(self, data):
  23. # 接收客户端的数据
  24. self.data.append(data.decode("utf-8"))
  25. def found_terminator(self):
  26. # 当客户端的一条数据结束时的处理
  27. line = ''.join(self.data)
  28. self.data = []
  29. try:
  30. self.room.handle(self, line.encode("utf-8"))
  31. # 退出聊天室的处理
  32. except EndSession:
  33. self.handle_close()
  34. def handle_close(self):
  35. # 当 session 关闭时,将进入 LogoutRoom
  36. asynchat.async_chat.handle_close(self)
  37. self.enter(LogoutRoom(self.server))

3.协议命令解释器

我们需要实现协议命令的相应方法,具体来说就是处理用户登录,退出,发消息,查询在线用户的代码。

  1. class CommandHandler:
  2. """
  3. 命令处理类
  4. """
  5. def unknown(self, session, cmd):
  6. # 响应未知命令
  7. # 通过 asynchat.async_chat.push 方法发送消息
  8. session.push(('Unknown command {} \n'.format(cmd)).encode("utf-8"))
  9. def handle(self, session, line):
  10. line = line.decode()
  11. # 命令处理
  12. if not line.strip():
  13. return
  14. parts = line.split(' ', 1)
  15. cmd = parts[0]
  16. try:
  17. line = parts[1].strip()
  18. except IndexError:
  19. line = ''
  20. # 通过协议代码执行相应的方法
  21. method = getattr(self, 'do_' + cmd, None)
  22. try:
  23. method(session, line)
  24. except TypeError:
  25. self.unknown(session, cmd)

4. 聊天室

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

  1. class Room(CommandHandler):
  2. """
  3. 包含多个用户的环境,负责基本的命令处理和广播
  4. """
  5. def __init__(self, server):
  6. self.server = server
  7. self.sessions = []
  8. def add(self, session):
  9. # 一个用户进入房间
  10. self.sessions.append(session)
  11. def remove(self, session):
  12. # 一个用户离开房间
  13. self.sessions.remove(session)
  14. def broadcast(self, line):
  15. # 向所有的用户发送指定消息
  16. # 使用 asynchat.asyn_chat.push 方法发送数据
  17. for session in self.sessions:
  18. session.push(line)
  19. def do_logout(self, session, line):
  20. # 退出房间
  21. raise EndSession
  22. class LoginRoom(Room):
  23. """
  24. 处理登录用户
  25. """
  26. def add(self, session):
  27. # 用户连接成功的回应
  28. Room.add(self, session)
  29. # 使用 asynchat.asyn_chat.push 方法发送数据
  30. session.push(b'Connect Success')
  31. def do_login(self, session, line):
  32. # 用户登录逻辑
  33. name = line.strip()
  34. # 获取用户名称
  35. if not name:
  36. session.push(b'UserName Empty')
  37. # 检查是否有同名用户
  38. elif name in self.server.users:
  39. session.push(b'UserName Exist')
  40. # 用户名检查成功后,进入主聊天室
  41. else:
  42. session.name = name
  43. session.enter(self.server.main_room)
  44. class LogoutRoom(Room):
  45. """
  46. 处理退出用户
  47. """
  48. def add(self, session):
  49. # 从服务器中移除
  50. try:
  51. del self.server.users[session.name]
  52. except KeyError:
  53. pass
  54. class ChatRoom(Room):
  55. """
  56. 聊天用的房间
  57. """
  58. def add(self, session):
  59. # 广播新用户进入
  60. session.push(b'Login Success')
  61. self.broadcast((session.name + ' has entered the room.\n').encode("utf-8"))
  62. self.server.users[session.name] = session
  63. Room.add(self, session)
  64. def remove(self, session):
  65. # 广播用户离开
  66. Room.remove(self, session)
  67. self.broadcast((session.name + ' has left the room.\n').encode("utf-8"))
  68. def do_say(self, session, line):
  69. # 客户端发送消息
  70. self.broadcast((session.name + ': ' + line + '\n').encode("utf-8"))
  71. def do_look(self, session, line):
  72. # 查看在线用户
  73. session.push(b'Online Users:\n')
  74. for other in self.sessions:
  75. session.push((other.name + '\n').encode("utf-8"))
  76. if __name__ == '__main__':
  77. s = ChatServer(PORT)
  78. try:
  79. print("chat server run at '0.0.0.0{0}'".format(PORT))
  80. asyncore.loop()
  81. except KeyboardInterrupt:
  82. print("chat server exit")

客户端

1.登录窗口

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

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

  1. import wx
  2. import telnetlib
  3. from time import sleep
  4. import _thread as thread
  5. class LoginFrame(wx.Frame):
  6. """
  7. 登录窗口
  8. """
  9. def __init__(self, parent, id, title, size):
  10. # 初始化,添加控件并绑定事件
  11. wx.Frame.__init__(self, parent, id, title)
  12. self.SetSize(size)
  13. self.Center()
  14. self.serverAddressLabel = wx.StaticText(self, label="Server Address", pos=(10, 50), size=(120, 25))
  15. self.userNameLabel = wx.StaticText(self, label="UserName", pos=(40, 100), size=(120, 25))
  16. self.serverAddress = wx.TextCtrl(self, pos=(120, 47), size=(150, 25))
  17. self.userName = wx.TextCtrl(self, pos=(120, 97), size=(150, 25))
  18. self.loginButton = wx.Button(self, label='Login', pos=(80, 145), size=(130, 30))
  19. # 绑定登录方法
  20. self.loginButton.Bind(wx.EVT_BUTTON, self.login)
  21. self.Show()
  22. def login(self, event):
  23. # 登录处理
  24. try:
  25. serverAddress = self.serverAddress.GetLineText(0).split(':')
  26. con.open(serverAddress[0], port=int(serverAddress[1]), timeout=10)
  27. response = con.read_some()
  28. if response != b'Connect Success':
  29. self.showDialog('Error', 'Connect Fail!', (200, 100))
  30. return
  31. con.write(('login ' + str(self.userName.GetLineText(0)) + '\n').encode("utf-8"))
  32. response = con.read_some()
  33. if response == b'UserName Empty':
  34. self.showDialog('Error', 'UserName Empty!', (200, 100))
  35. elif response == b'UserName Exist':
  36. self.showDialog('Error', 'UserName Exist!', (200, 100))
  37. else:
  38. self.Close()
  39. ChatFrame(None, 2, title='H4ck3R Chat Client', size=(500, 400))
  40. except Exception:
  41. self.showDialog('Error', 'Connect Fail!', (95, 20))
  42. def showDialog(self, title, content, size):
  43. # 显示错误信息对话框
  44. dialog = wx.Dialog(self, title=title, size=size)
  45. dialog.Center()
  46. wx.StaticText(dialog, label=content)
  47. dialog.ShowModal()

2.聊天窗口

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

  1. class ChatFrame(wx.Frame):
  2. """
  3. 聊天窗口
  4. """
  5. def __init__(self, parent, id, title, size):
  6. # 初始化,添加控件并绑定事件
  7. wx.Frame.__init__(self, parent, id, title)
  8. self.SetSize(size)
  9. self.Center()
  10. self.chatFrame = wx.TextCtrl(self, pos=(5, 5), size=(490, 310), style=wx.TE_MULTILINE | wx.TE_READONLY)
  11. self.message = wx.TextCtrl(self, pos=(5, 320), size=(300, 25))
  12. self.sendButton = wx.Button(self, label="Send", pos=(310, 320), size=(58, 25))
  13. self.usersButton = wx.Button(self, label="Users", pos=(373, 320), size=(58, 25))
  14. self.closeButton = wx.Button(self, label="Close", pos=(436, 320), size=(58, 25))
  15. # 发送按钮绑定发送消息方法
  16. self.sendButton.Bind(wx.EVT_BUTTON, self.send)
  17. # Users按钮绑定获取在线用户数量方法
  18. self.usersButton.Bind(wx.EVT_BUTTON, self.lookUsers)
  19. # 关闭按钮绑定关闭方法
  20. self.closeButton.Bind(wx.EVT_BUTTON, self.close)
  21. thread.start_new_thread(self.receive, ())
  22. self.Show()
  23. def send(self, event):
  24. # 发送消息
  25. message = str(self.message.GetLineText(0)).strip()
  26. if message != '':
  27. con.write(('say ' + message + '\n').encode("utf-8"))
  28. self.message.Clear()
  29. def lookUsers(self, event):
  30. # 查看当前在线用户
  31. con.write(b'look\n')
  32. def close(self, event):
  33. # 关闭窗口
  34. con.write(b'logout\n')
  35. con.close()
  36. self.Close()
  37. def receive(self):
  38. # 接受服务器的消息
  39. while True:
  40. sleep(0.6)
  41. result = con.read_very_eager()
  42. if result != '':
  43. self.chatFrame.AppendText(result)
  44. if __name__ == '__main__':
  45. app = wx.App()
  46. con = telnetlib.Telnet()
  47. LoginFrame(None, -1, title="Login", size=(320, 250))
  48. app.MainLoop()

三、运行

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

  2. 这时,我们再另一台机器(我的是虚拟机)打开一个终端,执行 client.py 文件,输入服务端的地址、端口,及自己在聊天室的ID,点击 Login ,即可进入

此时,点击Users,将显示处于聊天室的当前用户:

  1. 同样,在另外一台机器中重复上述操作:

以下是模拟的两个用户的对话(当然,也可以更多用户),但只要在线的用户都可以收到对话消息。此时,再次点击Users,显示处于聊天室的两个用户:

Simple Chat Application for Python的更多相关文章

  1. ASP.NET AJAX web chat application

    ASP.NET AJAX web chat application The project illustrates how to design a simple AJAX web chat appli ...

  2. openresty+websocket+redis simple chat

    openresty 很早就支持websocket了,但是早期的版本cosocket是单工的,处理起来比较麻烦参见邮件列表讨论 websocket chat,后来的版本cosocket是双全工的,就可以 ...

  3. Python创建命令行应用的工具 tools for command line application in python

    工具1:Docopt 地址:http://docopt.org/ 这个工具是根据模块的文档注释来确定参数的.注释分为两部分:Usage, option. \``` Usage: naval_fate ...

  4. webSocket开发chat application过程

    本次使用websocket开发chat的功能已经接近尾声,等到压力测试结束之后就可以上线了.在此记录一下整个开发过程. ---------------------------------------- ...

  5. A WCF-WPF Chat Application

    http://www.codeproject.com/Articles/25261/A-WCF-WPF-Chat-Application

  6. Writing a Simple YARN Application 从hadoop生态抽出yarn ,单独使用yarn

    Apache Hadoop 2.9.1 – Hadoop: Writing YARN Applications https://hadoop.apache.org/docs/current/hadoo ...

  7. Simple BeamSearch Codes for Python

    Code from: https://github.com/SeitaroShinagawa/simple_beamsearch probs = [[[],[0.3,0.7]], [[0],[0.1, ...

  8. Node.js + Web Socket 打造即时聊天程序嗨聊

    前端一直是一块充满惊喜的土地,不仅是那些富有创造性的页面,还有那些惊赞的效果及不断推出的新技术.像node.js这样的后端开拓者直接将前端人员的能力扩大到了后端.瞬间就有了一统天下的感觉,来往穿梭于前 ...

  9. SingalR--demo

    原文链接 : http://www.asp.net/signalr/overview/getting-started/tutorial-getting-started-with-signalr-and ...

随机推荐

  1. Journal of Proteome Research | Lipidomics reveals similar changes in serum phospholipid signatures of overweight and obese paediatric subjects (分享人:赵倩倩)

    文献名:Lipidomics reveals similar changes in serum phospholipid signatures of overweight and obese paed ...

  2. Java容器的常见问题

    记录Java容器中的常见概念和原理 参考: https://github.com/wangzhiwubigdata/God-Of-BigData#三Java并发容器 https://blog.csdn ...

  3. datetime和time

    datetime和time 1.datetime模块 import datetimenow = datetime.datetime.now() #时间对象print(now,type(now))pri ...

  4. drf 权限认证

    目录 复习 前期准备 三大认证简介 AbstracUser源码分析 自定义User下的权限六表 models.py 到settings.py中注册 注意点: 执行数据迁移的俩条命令 创建超级用户 t_ ...

  5. HDU-4252 A Famous City(单调栈)

    最后更新于2019.1.23 A Famous City ?戳这里可以前往原题 Problem Description After Mr. B arrived in Warsaw, he was sh ...

  6. 干货|Python基础入门 课程笔记(三)

    目录 列表 元组 字典 三元表达式 一.列表 前面学习的字符串可以用来存储一串信息,那么想一想,如果现在有很多人,总不能每个人都起一个变量名把?那岂不得疯~ 咱们可以使用列表. (1)列表得格式和输出 ...

  7. Ribbon负载均衡实现

    1,在之前的博文中,我通过eureka,consul,zookeeper 实现了注册中心,在实现的服务发现过程中,都是通过RstTemplate 来实现RPC 远程调用 RestTemplate 封装 ...

  8. 【LeetCode】18.四数之和

    题目描述 18. 四数之和 给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 t ...

  9. 白嫖Office365

    写作不易,资瓷一下呗!个人博客:https://raycoder.me 最近系统升级到1909, 送了我一套Office365. 我也很无奈啊, 送了让我激活也是够了... 用了各种激活软件都无效,比 ...

  10. [洛谷1437&Codevs1257]敲砖块<恶心的dp>

    题目链接:https://www.luogu.org/problem/show?pid=1437#sub http://codevs.cn/problem/1257/ 不得不说,这个题非常的恶心,在初 ...