本文是在实验楼-Pyhon实现文字聊天室的功能基础上进行的拓展功能开发
具体增加功能如下:
1.增加对指定用户发送消息功能
2.增加不同的frame框来分别显示发送和接收的消息
3.增加msg窗口来显示系统提示消息,如上下线消息,在线用户消息
4.使用panel改写设计客户端的控件展示
5.聊天窗口的title增加显示当前的用户名称
主要修改代码为客户端代码,服务器代码修改略少。
client.py客户端代码如下:
import wx
import telnetlib
from time import sleep
import _thread as thread
class LoginFrame(wx.Frame):
"""
登录窗口
"""
def __init__(self, parent, id, title, size):
# 初始化,添加控件并绑定事件
wx.Frame.__init__(self, parent, id, title)
self.SetSize(size)#设置窗口尺寸大小
self.Center()#设置登录弹窗在桌面中心
#使用尺寸器改写,改写后拉大或者缩小窗口,中间的控件会随着窗口的大小已固定的尺寸而改变
panel=wx.Panel(self)#创建一个面板,self表示实例即LoginFrame
#定义panel中的控件
self.serverAddressLabel = wx.StaticText(panel,label="Server Address")
self.userNameLabel = wx.StaticText(panel,label="UserName")
self.serverAddress = wx.TextCtrl(panel)
self.userName = wx.TextCtrl(panel)
self.loginButton = wx.Button(panel,label='Login')
#定义一个横向的box1
self.box1=wx.BoxSizer()
#添加box1中的元素
self.box1.Add(self.serverAddressLabel, proportion=5, flag=wx.EXPAND | wx.ALL,border=5)#该元素占box1的比例为50%,方式为伸缩,边界为5
self.box1.Add(self.serverAddress, proportion=5,flag=wx.EXPAND | wx.ALL,border=5)
#定义一个横向的box2
self.box2=wx.BoxSizer()
#添加box2中的元素
self.box2.Add(self.userNameLabel, proportion=5,flag=wx.EXPAND | wx.ALL,border=5)
self.box2.Add(self.userName, proportion=5,flag=wx.EXPAND | wx.ALL,border=5)
#定义一个纵向的v_box
self.v_box=wx.BoxSizer(wx.VERTICAL)
#添加v_box中的元素
self.v_box.Add(self.box1, proportion=3,flag=wx.EXPAND | wx.ALL,border=5)#添加box1,比例为3
self.v_box.Add(self.box2, proportion=3,flag=wx.EXPAND | wx.ALL,border=5)#添加box2,比例为3
self.v_box.Add(self.loginButton, proportion=2,flag=wx.EXPAND | wx.ALL,border=30)#添加登录按钮,比例为3
panel.SetSizer(self.v_box)
# 绑定登录方法
self.loginButton.Bind(wx.EVT_BUTTON, self.login)
self.Show()#显示以上控件
def login(self, event):
# 登录处理
try:
serverAddress = self.serverAddress.GetLineText(0).split(':')#获取serverAddress处的值并以:做为分隔符
con.open(serverAddress[0], port=int(serverAddress[1]), timeout=10)#open方法连接主机
response = con.read_some()#接收服务端返回的数据
if response != b'Connect Success':
self.showDialog('Error', 'Connect Fail!', (200, 100))
return
con.write(('login ' + str(self.userName.GetLineText(0)) + '\n').encode("utf-8"))#通过write写给服务器端
loginname=str(self.userName.GetLineText(0))#获取登录用户名称
# print(loginname)
response = con.read_some()
if response == 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 - '+loginname, size=(500, 400))
except Exception:
self.showDialog('Error', 'Connect Fail!', (200, 150))
def showDialog(self, title, content, size):
# 显示错误信息对话框
dialog = wx.Dialog(self, title=title, size=size)
dialog.Center()
wx.StaticText(dialog, label=content)
dialog.ShowModal()#showModal() 方法用于显示对话窗口
class ChatFrame(wx.Frame):
"""
聊天窗口
"""
def __init__(self, parent, id, title, size):
# 初始化,添加控件并绑定事件
wx.Frame.__init__(self, parent, id, title)
self.Title=title#'ShiYanLou Chat Client - '+self.loginname
self.SetSize(780,500)#设置对话框的大小
self.Center()#设置弹窗在屏幕中间
#使用尺寸器改写,改写后拉大或者缩小窗口,中间的控件会随着窗口的大小已固定的尺寸而改变
panel=wx.Panel(self)#创建一个面板,self表示实例即ChatFrame
#定义panel中的控件
self.receiveLabel = wx.StaticText(panel,label="Receive Msg")
self.sendLabel = wx.StaticText(panel,label="Send Msg")
self.noticeLabel = wx.StaticText(panel,label="Notice")
self.chatFrame1 = wx.TextCtrl(panel,style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_LEFT)
self.chatFrame2 = wx.TextCtrl(panel,style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RIGHT)
self.noticeFrame = wx.TextCtrl(panel,style=wx.TE_MULTILINE | wx.TE_READONLY )
self.message = wx.TextCtrl(panel,value='input message')#设置发送消息的文本输入框的位置和尺寸
self.toUser=wx.TextCtrl(panel,value='input username')#设置指定用户的文本输入框的位置和尺寸
self.sendButton = wx.Button(panel, label="Send")
self.sendDesignButton = wx.Button(panel, label="SendDesign")
self.closeButton = wx.Button(panel, label="Close")
self.usersButton = wx.Button(panel, label="Online")
#定义横向的box1
self.box1=wx.BoxSizer()
#添加box1中的元素
self.box1.Add(self.receiveLabel, proportion=4, flag=wx.EXPAND | wx.ALL,border=5)#该元素占box1的比例为40%,方式为伸缩,边界为5
self.box1.Add(self.sendLabel, proportion=4,flag=wx.EXPAND | wx.ALL,border=5)
self.box1.Add(self.noticeLabel, proportion=2,flag=wx.EXPAND | wx.ALL,border=5)
#定义横向的box2
self.box2=wx.BoxSizer()
#添加box2中的元素
self.box2.Add(self.chatFrame1, proportion=4,flag=wx.EXPAND | wx.ALL,border=5)
self.box2.Add(self.chatFrame2, proportion=4,flag=wx.EXPAND | wx.ALL,border=5)
self.box2.Add(self.noticeFrame, proportion=2,flag=wx.EXPAND | wx.ALL,border=5)
#定义横向的box3
self.box3=wx.BoxSizer()
#添加box3中的元素
self.box3.Add(self.message, proportion=6, flag=wx.EXPAND | wx.ALL,border=5)
self.box3.Add(self.sendButton, proportion=2,flag=wx.EXPAND | wx.ALL,border=5)
self.box3.Add(self.usersButton, proportion=2,flag=wx.EXPAND | wx.ALL,border=5)
#定义横向的box4
self.box4=wx.BoxSizer()
#添加box4中的元素
self.box4.Add(self.toUser, proportion=6, flag=wx.EXPAND | wx.ALL,border=5)
self.box4.Add(self.sendDesignButton, proportion=2,flag=wx.EXPAND | wx.ALL,border=5)
self.box4.Add(self.closeButton, proportion=2,flag=wx.EXPAND | wx.ALL,border=5)
#定义一个纵向的v_box
self.v_box=wx.BoxSizer(wx.VERTICAL)
#添加v_box中的元素
self.v_box.Add(self.box1, proportion=1,flag=wx.EXPAND | wx.ALL,border=5)#添加box1,比例为1
self.v_box.Add(self.box2, proportion=7,flag=wx.EXPAND | wx.ALL,border=5)#添加box2,比例为7
self.v_box.Add(self.box3, proportion=1,flag=wx.EXPAND | wx.ALL,border=5)#添加box3,比例为1
self.v_box.Add(self.box4, proportion=1,flag=wx.EXPAND | wx.ALL,border=5)#添加box4,比例为1
panel.SetSizer(self.v_box)
# 发送按钮绑定发送消息方法
self.sendButton.Bind(wx.EVT_BUTTON, self.send)
#发送指定人按钮绑定方法
self.sendDesignButton.Bind(wx.EVT_BUTTON,self.sendDesign)
# 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()
def send(self, event):
#群发消息
message = str(self.message.GetLineText(0)).strip()
if message != '':
con.write(('say ' + message + '\n').encode("utf-8"))#通过write写给服务器端
self.message.Clear()
def sendDesign(self, event):
#给指定用户发送消息
message = str(self.message.GetLineText(0)).strip()
username=str(self.toUser.GetLineText(0)).strip()
if message != '' and username!='':
con.write(('DesignSay ' + message + ' '+username+'\n').encode("utf-8"))#通过write写给服务器端
self.message.Clear()
self.toUser.Clear()
def lookUsers(self, event):
# 查看当前在线用户
con.write(b'look\n')
def close(self, event):
# 关闭窗口
con.write(b'logout\n')
con.close()#Close the connection
self.Close()
def receive(self):
# 接受服务器的消息
title1=self.Title.strip().split('-',1)#去掉Title的左右空格后将Title按照-符号分隔开,获取到登录用户名
loginname=title1[1].strip()+':'#去掉登录名的左右空格,加上:符号,确保是该用户发送的消息
while True:
sleep(0.6)
result = con.read_very_eager()#不断接收来自服务器的消息
commandList=['Online Users','entered','left']#系统通知消息的指令
for com in commandList:
if com in str(result):
self.noticeFrame.AppendText(result)#将通知消息显示在noticeFrame中
break
else:
if loginname in str(result) or 'Username not exist' in str(result):#如果用户登录名在服务器发送的消息中可以查找到,即代表是本人发送的消息
self.chatFrame2.AppendText(result)#将聊天消息显示在本人的聊天窗口chatFrame2中
else:
self.chatFrame1.AppendText(result)#否则将消息显示在别人消息的聊天窗口chatFrame1中
if __name__ == '__main__':
app = wx.App()#实例化一个主循环
#聊天协议基于文本,和服务器之间的通信将基于 telnetlib模块实现
#连接主机----两种方法,
#一种是在实例化时传入ip地址连接主机(con = telnetlib.Telnet(host_ip,port=23))
#第二种是,先不传参数进行实例化再用open方法连接主机
con = telnetlib.Telnet()#实例化一个telnet连接主机
LoginFrame(None, -1, title="Login", size=(320, 250))#id为-1表示主窗口
app.MainLoop()#启动主循环
server.py客户端代码如下:
#-*-coding:utf-8-*-
import asynchat
import asyncore
# 定义端口
PORT = 6666
# 定义结束异常类
class EndSession(Exception):
pass
class ChatServer(asyncore.dispatcher):
#dispatcher是asyncore中一个socket框架,为socket添加一些通用的回调方法
"""
聊天服务器
"""
def __init__(self, port):
asyncore.dispatcher.__init__(self)
# 创建socket
self.create_socket()
# 设置 socket 为可重用
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()#accept()会等待并返回一个客户端的连接
ChatSession(self, conn)
class ChatSession(asynchat.async_chat):
#负责和客户端通信
def __init__(self,server,sock):
# print(server,sock)
#server:<__main__.ChatServer listening :6666 at 0x2994860>
#sock:<socket.socket fd=204, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 6666), raddr=('127.0.0.1', 52148)>
asynchat.async_chat.__init__(self, sock)
self.server=server
self.set_terminator(b'\n')#定义终止符
self.data=[]
self.name=None
self.enter(LoginRoom(server))
def enter(self,room):
#从当前房间移除自身,然后添加到指定房间
try:
cur=self.room
except AttributeError:#该错误是python找不到对应的对象的属性
pass
else:
cur.remove(self)#如果try内的语句正常执行,接着执行else里的语句
self.room=room
room.add(self)
def collect_incoming_data(self, data):#接收客户端的数据
self.data.append(data.decode("utf-8"))
def found_terminator(self):#当客户端的一条数据结束时的处理
line=''.join(self.data)
self.data=[]
try:
self.room.handle(self,line.encode("utf-8"))
#退出聊天室的处理
except EndSession:
self.handle_close()
def handle_close(self):#当session关闭时,将进入logoutRoom
asynchat.async_chat.handle_close(self)
self.enter(LogoutRoom(self.server))
class CommandHandler:#命令处理类
def unknown(self,session,cmd):
#响应未知命令
#通过aynchat.async_chat.push方法发送消息
session.push(('Unknown command {} \n'.format(cmd)).encode('utf-8'))
def handle(self,session,line):
line=line.decode()
# print(line)
#命令处理
if not line.strip():#如果line去掉左右空格后为空
return
parts=line.split(' ',1)#以空格为分隔符,分隔成两个
cmd=parts[0]
try:
line=parts[1].strip()
except IndexError:
line=''
#通过协议代码执行相应的方法
method=getattr(self,'do_'+cmd,None)#getattr()函数用于返回一个对象属性值。class A(object):bar = 1 >>>a = A(),getattr(a, 'bar')# 获取属性 bar值=1
# print(method)
try:
method(session,line)#跳转到对应的方法,如do_look,do_say
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):
#向所有用户发送指定消息
#使用asynchat.async_chat.push方法发送数据
for session in self.sessions:
session.push(line)
def sendDesignMsg(self,msg,sendpeople,topeople):
#对指定用户发送消息
print(sendpeople,topeople,self.sessions,self.server.users)
if topeople in self.server.users:
session1=self.server.users[sendpeople]#获取发信人的session
session2=self.server.users[topeople]#获取收信人的session
session1.push(msg)#发信人和收信人的聊天页面均显示消息
session2.push(msg)
else:
session=self.server.users[sendpeople]
session.push(b'Username not exist\n')
def do_logout(self,session,line):
#退出房间
raise EndSession
class LoginRoom(Room):
#处理登录用户
def add(self,session):
#用户连接成功的回应
Room.add(self, session)
#使用asynchat.async_chat.push方法发送数据
'''
Python3的字符串的编码语言用的是unicode编码,由于Python的字符串类型是str,
在内存中以Unicode表示,一个字符对应若干字节,如果要在网络上传输,
或保存在磁盘上就需要把str变成以字节为单位的bytes
python对bytes类型的数据用带b前缀的单引号或双引号表示:
'''
session.push(b'Connect Success')
def do_login(self,session,line):
#用户登录逻辑
name=line.strip()
#获取用户名称
if not name:
session.push(b'UserName Empty')
#检查是否是同名用户
elif name in self.server.users:
session.push(b'UserName Exist')
else:
session.name=name
session.enter(self.server.main_room)
class LogoutRoom(Room):
#处理退出用户
def add(self,session):
#从服务器中移除
try:
del self.server.users[session.name]
except KeyError:
pass
class ChatRoom(Room):
#聊天用的房间
def add(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)
def remove(self,session):
#广播用户离开
Room.remove(self, session)
self.broadcast((session.name+' has left the room.\n').encode('utf-8'))
def do_say(self,session,line):
#发送消息
self.broadcast((session.name+':'+line+'\n').encode('utf-8'))
def do_DesignSay(self,session,line):
#发送消息给指定的用户
words=line.split('&',1)#以&为分隔符,分隔成两个,发送的消息和指定收信人的姓名
msg=words[0]#获取发送消息内容
topeople=words[1]#获取收信人名称
sendpeople=session.name#获取发信人的名称
self.sendDesignMsg((session.name+':'+msg+'\n').encode('utf-8'),sendpeople,topeople)
def do_look(self,session,line):
#查看在线用户
session.push(b'Online Users:\n')
for other in self.sessions:
session.push((other.name+'\n').encode('utf-8'))
if __name__=='__main__':
s=ChatServer(PORT)
try:
print("chat serve run at '127.0.0.1:{0}'".format(PORT))
asyncore.loop()
except KeyboardInterrupt:
print('chat server exit')
完成后就可以运行程序进行聊天了,注意需要先启动服务器再启动客户端。
这个项目中使用了 asyncore 的 dispatcher 来实现服务器,asynchat 的 asyn_chat 来维护用户的连接会话,用 wxPython 来实现图形界面,用 telnetlib 来连接服务器,在子线程中接收服务器发来的消息,由此一个简单的聊天室程序就完成了。
聊天页面如下图: