Python实现基于C/S架构的聊天室功能详解


Posted in Python onJuly 07, 2018

本文实例讲述了Python实现基于C/S架构的聊天室功能。分享给大家供大家参考,具体如下:

一、课程介绍

1.简介

本次项目课是实现简单聊天室程序的服务器端和客户端。

2.知识点

服务器端涉及到asyncoreasynchatsocket这几个模块,客户端用到了telnetlibwxtimethread这几个模块。

3.所需环境

本次课中编写客户端需要用到wxPython,它是一个GUI工具包,请先使用下面的命令安装:

$ sudo apt-get install python-wxtools

密码为shiyanlou

4.项目效果截图

登录窗口

Python实现基于C/S架构的聊天室功能详解

聊天窗口

Python实现基于C/S架构的聊天室功能详解

5.源代码下载

git clone https://github.com/shiyanlou/pythonchat.git

说明:如果你不理解上述代码的下载方式或者下载后在环境中找不到代码,可以点击查看这里

二、项目实战(服务器端)

1.服务器类

首先需要一个聊天服务器,这里继承asyncore的dispatcher类来实现,代码如下

class ChatServer(dispatcher):
  """
  聊天服务器
  """
  def __init__(self, port):
    dispatcher.__init__(self)
    self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    self.set_reuse_addr()
    self.bind(('', port))
    self.listen(5)
    self.users = {}
    self.main_room = ChatRoom(self)
  def handle_accept(self):
    conn, addr = self.accept()
    ChatSession(self, conn)

2.会话类

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

class ChatSession(async_chat):
  """
  负责和单用户通信
  """
  def __init__(self, server, sock):
    async_chat.__init__(self, sock)
    self.server = server
    self.set_terminator('\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)
  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(LogoutRoom(self.server))

3.命令解释器

现在就需要一个命令解释器能够解释用户的命令,例如登录、查询在线用户和发消息等,代码如下:

class CommandHandler:
  """
  命令处理类
  """
  def unknown(self, session, cmd):
    '响应未知命令'
    session.push('Unknown command: %s\n' % cmd)
  def handle(self, session, line):
    '命令处理'
    if not line.strip():
      return
    parts = line.split(' ', 1)
    cmd = parts[0]
    try:
      line = parts[1].strip()
    except IndexError:
      line = ''
    meth = getattr(self, 'do_' + cmd, None)
    try:
      meth(session, line)
    except TypeError:
      self.unknown(session, cmd)

4.房间

接下来就需要实现聊天室的房间了,这里我们定义了三种房间,分别是用户刚登录时的房间、聊天的房间和退出登录的房间,这三种房间都有一个公共的父类,代码如下:

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):
    '向所有的用户发送指定消息'
    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)
    session.push('Connect Success')
  def do_login(self, session, line):
    '登录命令处理'
    name = line.strip()
    if not name:
      session.push('UserName Empty')
    elif name in self.server.users:
      session.push('UserName Exist')
    else:
      session.name = name
      session.enter(self.server.main_room)
class ChatRoom(Room):
  """
  聊天用的房间
  """
  def add(self, session):
    '广播新用户进入'
    session.push('Login Success')
    self.broadcast(session.name + ' has entered the room.\n')
    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')
  def do_say(self, session, line):
    '客户端发送消息'
    self.broadcast(session.name + ': ' + line + '\n')
  def do_look(self, session, line):
    '查看在线用户'
    session.push('Online Users:\n')
    for other in self.sessions:
      session.push(other.name + '\n')
class LogoutRoom(Room):
  """
  用户退出时的房间
  """
  def add(self, session):
    '从服务器中移除'
    try:
      del self.server.users[session.name]
    except KeyError:
      pass

5.服务器端完整代码

#!/usr/bin/python
# encoding: utf-8
from asyncore import dispatcher
from asynchat import async_chat
import socket, asyncore
PORT = 6666 #端口
class EndSession(Exception):
  """
  自定义会话结束时的异常
  """
  pass
class CommandHandler:
  """
  命令处理类
  """
  def unknown(self, session, cmd):
    '响应未知命令'
    session.push('Unknown command: %s\n' % cmd)
  def handle(self, session, line):
    '命令处理'
    if not line.strip():
      return
    parts = line.split(' ', 1)
    cmd = parts[0]
    try:
      line = parts[1].strip()
    except IndexError:
      line = ''
    meth = getattr(self, 'do_' + cmd, None)
    try:
      meth(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):
    '向所有的用户发送指定消息'
    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)
    session.push('Connect Success')
  def do_login(self, session, line):
    '登录命令处理'
    name = line.strip()
    if not name:
      session.push('UserName Empty')
    elif name in self.server.users:
      session.push('UserName Exist')
    else:
      session.name = name
      session.enter(self.server.main_room)
class ChatRoom(Room):
  """
  聊天用的房间
  """
  def add(self, session):
    '广播新用户进入'
    session.push('Login Success')
    self.broadcast(session.name + ' has entered the room.\n')
    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')
  def do_say(self, session, line):
    '客户端发送消息'
    self.broadcast(session.name + ': ' + line + '\n')
  def do_look(self, session, line):
    '查看在线用户'
    session.push('Online Users:\n')
    for other in self.sessions:
      session.push(other.name + '\n')
class LogoutRoom(Room):
  """
  用户退出时的房间
  """
  def add(self, session):
    '从服务器中移除'
    try:
      del self.server.users[session.name]
    except KeyError:
      pass
class ChatSession(async_chat):
  """
  负责和单用户通信
  """
  def __init__(self, server, sock):
    async_chat.__init__(self, sock)
    self.server = server
    self.set_terminator('\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)
  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(LogoutRoom(self.server))
class ChatServer(dispatcher):
  """
  聊天服务器
  """
  def __init__(self, port):
    dispatcher.__init__(self)
    self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    self.set_reuse_addr()
    self.bind(('', port))
    self.listen(5)
    self.users = {}
    self.main_room = ChatRoom(self)
  def handle_accept(self):
    conn, addr = self.accept()
    ChatSession(self, conn)
if __name__ == '__main__':
  s = ChatServer(PORT)
  try:
    asyncore.loop()
  except KeyboardInterrupt:
    print

三、项目实战(客户端)

完成了服务器端后,就需要实现客户端了,这里客户端连接服务器使用了telnetlib模块。

1.登录窗口

这里的图形界面包选择了wxPython,前面有安装说明,登录窗口通过继承wx.Frame类来实现,代码如下:

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 != 'Connect Success':
        self.showDialog('Error', 'Connect Fail!', (95, 20))
        return
      con.write('login ' + str(self.userName.GetLineText(0)) + '\n')
      response = con.read_some()
      if response == 'UserName Empty':
        self.showDialog('Error', 'UserName Empty!', (135, 20))
      elif response == 'UserName Exist':
        self.showDialog('Error', 'UserName Exist!', (135, 20))
      else:
        self.Close()
        ChatFrame(None, -2, title = 'ShiYanLou Chat Client', size = (500, 350))
    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()

2.聊天窗口

聊天窗口中最主要的就是向服务器发消息并接受服务器的消息,这里通过子线程来接受,代码如下:

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)
    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')
      self.message.Clear()
  def lookUsers(self, event):
    '查看当前在线用户'
    con.write('look\n')
  def close(self, event):
    '关闭窗口'
    con.write('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)

3.客户端完整代码

#!/usr/bin/python
# encoding: utf-8
import wx
import telnetlib
from time import sleep
import 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 != 'Connect Success':
        self.showDialog('Error', 'Connect Fail!', (95, 20))
        return
      con.write('login ' + str(self.userName.GetLineText(0)) + '\n')
      response = con.read_some()
      if response == 'UserName Empty':
        self.showDialog('Error', 'UserName Empty!', (135, 20))
      elif response == 'UserName Exist':
        self.showDialog('Error', 'UserName Exist!', (135, 20))
      else:
        self.Close()
        ChatFrame(None, -2, title = 'ShiYanLou Chat Client', size = (500, 350))
    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)
    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')
      self.message.Clear()
  def lookUsers(self, event):
    '查看当前在线用户'
    con.write('look\n')
  def close(self, event):
    '关闭窗口'
    con.write('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 = (280, 200))
  app.MainLoop()

四、小结

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

更多关于Python相关内容可查看本站专题:《Python Socket编程技巧总结》、《Python数据结构与算法教程》、《Python函数使用技巧总结》、《Python字符串操作技巧汇总》、《Python入门与进阶经典教程》及《Python文件与目录操作技巧汇总》

希望本文所述对大家Python程序设计有所帮助。

Python 相关文章推荐
Python Trie树实现字典排序
Mar 28 Python
在Python的Django框架中加载模版的方法
Jul 16 Python
详解Python中的Descriptor描述符类
Jun 14 Python
Python通过OpenCV的findContours获取轮廓并切割实例
Jan 05 Python
numpy 进行数组拼接,分别在行和列上合并的实例
May 08 Python
Python中的集合介绍
Jan 28 Python
Python动态赋值的陷阱知识点总结
Mar 17 Python
Python Multiprocessing多进程 使用tqdm显示进度条的实现
Aug 13 Python
windows环境中利用celery实现简单任务队列过程解析
Nov 29 Python
Python内置数据类型list各方法的性能测试过程解析
Jan 07 Python
python 办公自动化——基于pyqt5和openpyxl统计符合要求的名单
May 25 Python
Python趣味挑战之给幼儿园弟弟生成1000道算术题
May 28 Python
Python实现的txt文件去重功能示例
Jul 07 #Python
Django 多语言教程的实现(i18n)
Jul 07 #Python
python利用requests库进行接口测试的方法详解
Jul 06 #Python
python生成密码字典的方法
Jul 06 #Python
Python 3.x 判断 dict 是否包含某键值的实例讲解
Jul 06 #Python
使用python中的in ,not in来检查元素是不是在列表中的方法
Jul 06 #Python
python 实现将字典dict、列表list中的中文正常显示方法
Jul 06 #Python
You might like
php数据库连接时容易出错的特殊符号问题
2010/09/01 PHP
使用XDebug调试及单元测试覆盖率分析
2011/01/27 PHP
destoon常用的安全设置概述
2014/06/21 PHP
用PHP生成excel文件到指定目录
2015/06/22 PHP
php cookie 详解使用实例
2016/11/03 PHP
PHP实现的微信APP支付功能示例【基于TP5框架】
2019/09/16 PHP
js style动态设置table高度
2014/10/21 Javascript
jquery实现多屏多图焦点图切换特效的方法
2015/05/04 Javascript
js跨域请求的5中解决方式
2015/07/02 Javascript
浅谈JavaScript中的作用域和闭包问题
2015/07/07 Javascript
详解页面滚动值scrollTop在FireFox与Chrome浏览器间的兼容问题
2015/12/03 Javascript
JavaScript小技巧整理
2015/12/30 Javascript
浅析JavaScript 箭头函数 generator Date JSON
2016/05/23 Javascript
JS实现的验证身份证及获取地区功能示例
2017/01/16 Javascript
JavaScript实现写入文件到本地的方法【基于FileSaver.js插件】
2018/03/15 Javascript
vue多次循环操作示例
2019/02/08 Javascript
微信小程序蓝牙连接小票打印机实例代码详解
2019/06/03 Javascript
Jquery滑动门/tab切换实现方法完整示例
2020/06/05 jQuery
在Python中操作时间之strptime()方法的使用
2020/12/30 Python
详解用python写一个抽奖程序
2019/05/10 Python
浅谈pytorch grad_fn以及权重梯度不更新的问题
2019/08/20 Python
python 决策树算法的实现
2020/10/09 Python
基于HTML5 Canvas 实现商场监控实例详解
2017/11/20 HTML / CSS
澳大利亚最好的厨具店:Kitchen Warehouse
2018/03/13 全球购物
Genny意大利官网:意大利高级时装品牌
2020/04/15 全球购物
德国便宜的宠物店:Brekz.de
2020/10/23 全球购物
华为慧通笔试题
2016/04/22 面试题
幼儿园老师辞职信
2014/01/20 职场文书
《秋游》教学反思
2014/04/24 职场文书
八项规定个人对照检查材料思想汇报
2014/09/25 职场文书
2014年财政工作总结
2014/12/10 职场文书
论文评审意见
2015/06/05 职场文书
2015年小学教师培训工作总结
2015/07/21 职场文书
培训后的感想
2015/08/07 职场文书
2019年学校消防安全责任书(2篇)
2019/10/09 职场文书
《我的美好婚事》动画化决定纪念插画与先导PV公开
2022/04/06 日漫