Python 实现文字聊天室-功能拓展

本文是在实验楼-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 来连接服务器,在子线程中接收服务器发来的消息,由此一个简单的聊天室程序就完成了。
聊天页面如下图:

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值