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之有容乃大的list(4)
Sep 28 Python
Python多进程同步Lock、Semaphore、Event实例
Nov 21 Python
python去除字符串中的换行符
Oct 11 Python
pycharm远程调试openstack代码
Nov 21 Python
Python自定义函数实现求两个数最大公约数、最小公倍数示例
May 21 Python
Sanic框架安装与简单入门示例
Jul 16 Python
python将控制台输出保存至文件的方法
Jan 07 Python
Python判断两个文件是否相同与两个文本进行相同项筛选的方法
Mar 01 Python
Python实现定制自动化业务流量报表周报功能【XlsxWriter模块】
Mar 11 Python
python设置代理和添加镜像源的方法
Feb 14 Python
详解用selenium来下载小姐姐图片并保存
Jan 26 Python
tensorflow学习笔记之tfrecord文件的生成与读取
Mar 31 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
要会喝咖啡也要会知道咖啡豆
2021/03/03 咖啡文化
解析PHP中的unset究竟会不会释放内存
2013/07/18 PHP
Parse正式发布开源PHP SDK
2014/08/11 PHP
PHP实现生成带背景的图形验证码功能
2016/10/03 PHP
PHP实现将base64编码字符串转换成图片示例
2018/06/22 PHP
PHP SESSION跨页面传递失败解决方案
2020/12/11 PHP
javascript入门·图片对象(无刷新变换图片)\滚动图像
2007/10/01 Javascript
jquery 实现checkbox全选,反选,全不选等功能代码(奇数)
2012/10/24 Javascript
js时间戳转为日期格式的方法
2015/12/28 Javascript
JavaScript鼠标事件,点击鼠标右键,弹出div的简单实例
2016/08/03 Javascript
基于JavaScript实现跳转提示页面
2016/09/24 Javascript
js HTML5上传示例代码完整版
2016/10/10 Javascript
JavaScript正则表达式替换字符串中图片地址(img src)的方法
2017/01/13 Javascript
基于JavaScript实现窗口拖动效果
2017/01/18 Javascript
老生常谈js中的MVC
2017/07/25 Javascript
利用vue + koa2 + mockjs模拟数据的方法教程
2017/11/22 Javascript
Angular2进阶之如何避免Dom误区
2018/04/02 Javascript
Vue页面骨架屏注入方法
2018/05/13 Javascript
angular 服务的单例模式(依赖注入模式下)详解
2018/10/22 Javascript
vue设计一个倒计时秒杀的组件详解
2019/04/06 Javascript
前后端常见的几种鉴权方式(小结)
2019/08/04 Javascript
js函数和this用法实例分析
2020/03/13 Javascript
python构造icmp echo请求和实现网络探测器功能代码分享
2014/01/10 Python
Python的re模块正则表达式操作
2016/05/25 Python
Python 查找list中的某个元素的所有的下标方法
2018/06/27 Python
python输出100以内的质数与合数实例代码
2018/07/08 Python
法国滑雪假期的专家:Ski Planet
2019/11/02 全球购物
Haggar官网:美国男装品牌
2020/02/16 全球购物
工作检讨书范文
2015/01/23 职场文书
高中生综合素质自我评价
2015/03/06 职场文书
离婚纠纷代理词
2015/05/23 职场文书
战马观后感
2015/06/08 职场文书
教务处干事工作总结
2015/08/14 职场文书
《妈妈别哭,有我在》读后感3篇
2020/01/13 职场文书
Python进程间的通信之语法学习
2022/04/11 Python
python绘制简单直方图(质量分布图)的方法
2022/04/21 Python