python

超轻量级php框架startmvc

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

更新时间:2020-06-09 10:48:01 作者:startmvc
本文实例讲述了Python实现基于C/S架构的聊天室功能。分享给大家供大家参考,具体如下:一

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

一、课程介绍

1.简介

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

2.知识点

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

3.所需环境

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


$ sudo apt-get install python-wxtools

密码为shiyanlou

4.项目效果截图

登录窗口

聊天窗口

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 C/S架构 聊天室