QQ聊天软件代码功能编写
一,Tkinter聊天界面编写
1,聊天软件客户端界面开发-1
- Tkinter的模块(“TK接口”)是标准的Python接口从Tk的GUI工具包
- https://i.cnblogs.com/EditPosts.aspx?opt=1 ###Tkinter官方文档:关于相关函数具体看文档介绍
Tkinter模块:自带的,跨平台
做GUI:界面软件 不难,但是复杂,东西多,窗口控件多,设计,美感,抠图 界面开发设计
设置窗口:像素大小- 拜访控件:按钮,复选框,文本框
- root控件流程:
- 1:导入模块 import tkinter
- 2:创建主窗口root = tkinter.Tk()
- 3:创建其他窗口以及控件(按钮,文本框,等等)
- 4:摆放到root上
- 5:开启root的事件循环:root.mainloop()
- 6:如果不需要该窗口,可以使用root.destory()
- 组件:
- 文本框
- tkinter.Text()
- insert():插入数据
- tkinter.END,从末尾插入
- delete(‘0.0’,tkinter.END):删除全部文本框数据
- tag_config(\'标签名\',foreground=\'颜色\',):标签配置,设置字体颜色, 第一个字符串参数是标签名,第二个是颜色
- get(\'0.0\',thinter.END):阅读拿出文本框的内容
- 按钮
- tkinter.Button()
- text:按钮名字
- command:回调函数,按钮点击时的工作任务列表框
- 列表框
- tkinter.Listbox()
- insert(tkinter.END, 追加的数据)
- 事件绑定
- 双击事件:<Double-Button-1>
事件绑定的函数必须有一个参数,这个参数就是事件- self.right_listbox.curselection()
返回当前选择的列表框索引位置值,返回结果为单个数据的元组- index = self.listbox_user.curselection()[0]
self.talk_user_name = self.listbox_user.get(index)
- index #获取的当前列表框中的索引位置的值
root.title(\'**\')
- 设置上标字符
- grid()
- x,y轴的摆放方式
- row:行
column:列 rowspan:行宽,跨行数 pady:表格间距,x轴 padx:y轴的表格间距 sticky
对其方向,上北下南左西右东N,S,W,E grid_propagate(0)
0代表禁止容器窗口缩放- root.resizable(width=False,height=Flase)
- 禁止窗口缩放
root.winfo_screenwidth()
自动获取用户的屏幕宽度 root.winfo_screenheight()
自动获取用户的屏幕高度 root.geometry("%dx%d%+d%+d" % (width, height, xoffset, yoffset))
设置控件所处的屏幕位置- tkinter.Label(所属的主窗口,text=\'名字\',font=("黑体",9, "bold"))
- 添加标签lable
tkinter.Entry()
单行文本框 获取数据直接使用get() 函数即可,不需要传参。 from tkinter import messagebox ##展示报错信息
\'showerror\', \'showinfo\', \'showwarning\'
title=None, message=None
messagebox.showerror(title=\'登录失败\',message=\'登录失败\')
- 客户端登录及聊天窗口代码编写整合如下:
from tkinter import Tk import tkinter import time from multiprocessing.pool import ThreadPool import socket from tkinter import messagebox import pickle import _pickle #scroolbar class TalkRoot: \'\'\' 实现用户聊天的主要界面 \'\'\' def __init__(self,user_name,client,user_id): self.root = Tk() self.root.title(\'欢迎你:%s\' % user_name) self.client = client self.user_id = user_id #-----------------创建进程池---------------------- self.thread_pool = ThreadPool(5) #5个线程的线程池 #-------功能变量---------------------- self.talk_user_name = \'\' #你要聊天的人 self.client = client #套接字 #------------------设置窗口所处的用户界面位置及窗口大小在屏幕中间------------ self.root_width=550 self.root_height=420 self.user_screen_width = self.root.winfo_screenwidth()#自动获取用户的屏幕宽度 self.user_screen_height = self.root.winfo_screenheight()#用户的屏幕高 self.root.geometry("%dx%d%+d%+d" % (self.root_width, self.root_height, (self.user_screen_width - self.root_width) / 2, (self.user_screen_height - self.root_height) / 2,) ) #-------容器-------- #上: 输出 self.frame_top = tkinter.Frame(width=380,height=270) #中: 输入 self.frame_center = tkinter.Frame(width=380,height=100) #下: 按钮 self.frame_bottom = tkinter.Frame(width=380,height=30) #右: 列表 self.frame_right = tkinter.Frame(width=170,height=400) #--------控件-------- #上: 输出 self.text_output = tkinter.Text(self.frame_top,height=260) self.text_output.tag_config(\'time_stamp\',foreground=\'green\') #标签配置颜色 self.text_output.tag_config(\'self_msg\',foreground=\'blue\')#标签配置颜色 self.text_output.tag_config(\'other_msg\',foreground=\'red\')#标签配置颜色 #中: 输入 self.text_input = tkinter.Text(self.frame_center) self.text_input.bind(\'<Return>\',self.button_send_msg) #下: 按钮 self.button_send = tkinter.Button(self.frame_bottom,text=\'发送\',command=self.button_send_msg) #发送按钮 self.button_cancle = tkinter.Button(self.frame_bottom,text=\'取消\',command=self.fc_button_cancle) #取消按钮 #右: 列表 self.listbox_user = tkinter.Listbox(self.frame_right,width=150,height=25) self.listbox_user.bind(\'<Double-Button-1>\',self.get_talk_user)#绑定双击事件 #--------摆放生效容器--------- self.frame_top.grid(row=0,column=0,padx=2,pady=5) self.frame_center.grid(row=1,column=0,padx=2,pady=5) self.frame_bottom.grid(row=2,column=0,sticky=tkinter.E) self.frame_right.grid(row=0,column=1,rowspan=3,padx=5,pady=5) #--------摆放生效控件--------- self.text_output.grid() self.text_input.grid() self.button_send.grid(row=0,column=1,padx=300) self.button_cancle.grid(row=0,column=0) self.listbox_user.grid() #--------禁止容器窗口缩放grid_-------------------- self.frame_top.grid_propagate(0) self.frame_center.grid_propagate(0) self.frame_bottom.grid_propagate(0) self.frame_right.grid_propagate(0) #------------专门开一个接受消息聊天的线程------------ self.thread_pool.apply_async(func=self.check_recv_msg) #非阻塞线程 #------------窗口循环--------------------------------------- self.root.mainloop() def check_recv_msg(self): while True: #循环窗口聊天 try: data = pickle.loads(self.client.recv(1024)) except _pickle.UnpicklingError: messagebox.showinfo(title=\'服务端消息错误\',message=\'服务端发来无法解析的错误\') self.root.destroy() exit() except Exception as e: messagebox.showinfo(title=\'服务端断开\',message=\'服务端断开\') self.root.destroy() exit() if data.get(\'flag\') == \'flush\': #服务端返回最新在线用户 self.thread_pool.apply_async(func=self.flush_onlien_user,args=(data,)) elif data.get(\'flag\') == \'forward\': #服务端返回别人发来的消息 self.thread_pool.apply_async(func=self.show_other_msg,args=(data,)) elif not data: #服务端发来空消息,断开连接 exit() def show_other_msg(self,data): \'\'\' 展示别人发来的消息 \'\'\' send_name = data.get(\'data\')[\'send_name\'] other_msg = data.get(\'data\')[\'message\'] print(\'别人发来了消息:\',other_msg) time_stamp = time.strftime(\'%Y-%m-%d %H:%M:%S\', time.localtime() ) + \'\n\' self.text_output.insert(tkinter.END,time_stamp,\'green\') self.text_output.insert(tkinter.END,\'%s:%s\' % (send_name,other_msg),\'red\') def flush_onlien_user(self,data): self.listbox_user.delete(0,tkinter.END) #清空在线用户列表 for user in data.get(\'data\'): #user : {id:name} id_ = list(user.keys())[0] name = list(user.values())[0] self.listbox_user.insert(tkinter.END, \'%s:%s\' % (name,id_)) def button_send_msg(self,events=None): self_msg = self.text_input.get(\'0.0\',tkinter.END) self.text_input.delete(\'0.0\',tkinter.END) if not self_msg.isspace(): time_stamp = time.strftime(\'%Y-%m-%d %H:%M:%S\', time.localtime() ) + \'\n\' self.text_output.insert(tkinter.END,time_stamp,\'time_stamp\') self.text_output.insert(tkinter.END,\' \' + self_msg,\'self_msg\') self.thread_pool.apply_async(func=self.clear_input_text) #id:name if self.talk_user_name: print(\'捕捉到在线用户,准备发送消息\') recv_id = self.talk_user_name.split(\':\')[0] print(recv_id) send_data = { \'flag\': \'send\', \'data\': { \'send_id\': self.user_id, \'recv_id\': recv_id, \'message\': self_msg, } } self.client.send(pickle.dumps(send_data)) else: \'\'\' 群发功能 \'\'\' send_data = { \'flag\': \'part\', \'data\': { \'send_id\': self.user_id, \'message\': self_msg, } } def clear_input_text(self): time.sleep(0.1) self.text_input.delete(\'0.0\',tkinter.END) def get_talk_user(self,events): \'\'\' 获取当前聊天用户 \'\'\' if self.listbox_user.size() != 0: index = self.listbox_user.curselection()[0] self.talk_user_name = self.listbox_user.get(index) print(self.talk_user_name) def fc_button_cancle(self): \'\'\' 聊天界面的销毁 销毁窗口 客户端套接字释放 \'\'\' exit() def __del__(self): self.client.close() #套接字释放 self.root.destroy() #销毁窗口 class LoginRoot(): def __init__(self): self.root = tkinter.Tk() self.root.title(\'登陆\') self.root.resizable(width=False,height=False) #禁止窗口缩放调整 self.root_width = 197 self.root_height = 75 #自适应,首先要获取到用户的屏幕分辨率 self.user_screen_width = self.root.winfo_screenwidth() #用户的屏幕也宽度 self.user_screen_height = self.root.winfo_screenheight() #用户的屏幕高度 self.root.geometry("%dx%d%+d%+d" % #设置窗口所处的用户界面位置及窗口大小 (self.root_width, self.root_height, (self.user_screen_width - self.root_width) / 2, (self.user_screen_height - self.root_height) / 2,) ) #创建提示label self.label_id = tkinter.Label(self.root,text=\'I D:\',font=("黑体",10, "bold")) # font设置字体样式 self.label_name = tkinter.Label(self.root,text=\'昵称:\',font=("黑体",10, "bold")) #摆放提示label self.label_id.grid(row=0,column=0,sticky=tkinter.W) self.label_name.grid(row=1,column=0,) #创建输入单行文本框 self.entry_id = tkinter.Entry(self.root) self.entry_name = tkinter.Entry(self.root) #摆放文本框 self.entry_id.grid(row=0,column=1) self.entry_name.grid(row=1,column=1) #创建功能按钮 self.button_login = tkinter.Button(self.root,text=\'登陆\',command=self.user_login) self.button_cancle = tkinter.Button(self.root,text=\'取消\',command=self.user_cancle) #摆放功能按钮 self.button_login.grid(row=2,column=1,sticky=tkinter.E) self.button_cancle.grid(row=2,column=0,sticky=tkinter.W) self.root.mainloop() def user_login(self): \'\'\' 要确定用户提供的ID是唯一的 把昵称传递给TalkRoot \'\'\' user_id = self.entry_id.get() user_name = self.entry_name.get() send_login_msg = { \'flag\': \'login\', \'data\':{ \'id\': user_id, \'name\': user_name } } ip = \'192.168.1.101\' port = 8000 try: self.client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.client.connect( (ip,port) ) except Exception as e: print(\'登陆连接时的错误:\',e) messagebox.showerror(title=\'登陆失败\',message=\'登陆失败\') else: self.client.send(pickle.dumps(send_login_msg)) data = pickle.loads(self.client.recv(1024)) if data.get(\'flag\') == \'login\': if data.get(\'data\')[\'state\']: #登陆成功 self.root.destroy() tkr = TalkRoot(user_name,self.client,user_id) else: self.client.close() messagebox.showwarning(title=\'登陆失败\',message=\'%s\' % (data.get(\'data\')[\'message\'])) def user_cancle(self): self.root.destroy() def main(): lgr = LoginRoot() if __name__ == \'__main__\': main()
二,聊天类软件后台架构及请求数据设计
一,服务端功能
1:接收用户登陆
- 主线程,消息收集
- 服务端套接字,唯一ID的校验
2:告知在线用户
- 另开线程,遍历访问服务端保存的客户端序列数据
3:处理在线用户的消息发送
- 主线程,消息收集 一旦捕获到了一个消息是要发送给别人的
- 另开线程,处理消息转发,发给另外一个在线的套接字
二,客户端功能
1:登陆
- 套接字,发送ID和昵称即可
- 2:接收处理服务端发来的在线用户列表
- 3:发送消息,展示消息
三,请求数据结构体 / 客户端
- 1:pickle.dumps() #打包二进制数据体
{ #客户端登陆发送的数据体 \'flag\': \'login\', #数据标志位 \'data\': { \'id\': \'123456\', #用户ID \'name: \'xxxxx\', #用户昵称 } } { #客户端发送数据体 \'flag\': \'send\' #数据标志位 \'data\': { \'send_id\': \'123455\', #发送者的ID \'recv_id\': \'123456\', #接收人的ID号 \'message: \'xxxxx\', #发给别人的数据 } } { #客户端注册数据体 \'flag\': \'register\', #数据标志位 \'data\': { \'id\': \'123456\', #用户ID \'name: \'xxxxx\', #用户昵称 \'password\': \'xxxxx\', # } }四,服务端保存在线用户数据体
- 1:直接连进来的,不进入这个数据体,不算有效用户
- 2:某个线程,捕捉这个套接字发来的第一个login的数据体
- 3:判断ID:
- ID不重复:登陆保存
- ID重复:当用户发送重复id的时,返回的数据
{ \'flag\':\'login\', \'data\': { \'state\':True,/False \'message\':\'登陆成功\' } }
- 4:成功登陆的用户保存在字典中的数据结构体
#字典: dict_onlien_user = { xxxx:{ \'name\':\'666\', \'socket\':client, }, xxxx:{, name:\'666\', socket:client, }, }- onlien_user.get(recv_id)[\'socket\'].send(message) #如果再转发消息的时候,准确快速的找到接收者
- 5:成功的用户发来的数据,要给别人发消息,服务端怎么保存这个数据?
{ #客户端发送数据体 \'flag\': \'send\' #数据标志位 \'data\': { \'send_id\': \'123455\', #发送者的ID \'recv_id\': \'123456\', #接收人的ID号 \'message: \'xxxxx\', #发给别人的数据 } }
- 6:客户端接收服务端发来的在线用户数据结构体
list_onlien_user = [ {\'id1\':\'name1\'}, {\'id2\':\'name2\'}, }
- 7: 刷新的用户列表数据是一个列表,现在要更改
{ #服务端发给客户端的数据体 \'flag\': \'flush\' #数据标志位 \'data\': [在线用户列表] }- 8: 发给其他用户的数据,怎么发过去?
{ #服务端发给客户端的数据体 \'flag\': \'forward\' #数据标志位 \'data\': { \'send_name\': \'123455\', #发送者的ID \'message\': \'123456\', #接收人的ID号 } }五,代码编写如下:
#服务端 import socket from multiprocessing.pool import ThreadPool import pickle import copy import time class TalkServer: def __init__(self,ip,port): self.ip = ip self.port = port self.socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket_server.setblocking(0) #服务端套接字设置为非阻塞 self.socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #设置端口复用 self.socket_server.bind( (self.ip, self.port) ) self.dict_online_user = {} #保存未来的在线用户字典 self.list_online_user = [] #保存即将发送给别人在现在用户列表 self.thread_pool = ThreadPool(10) self.is_flush = False #用来判断是否需要刷新在线用户的 def run(self, num_=5): self.socket_server.listen(num_) #2:用户发送给别人的消息处理 self.thread_pool.apply_async(func=self.inform_online_user_list) self.thread_pool.apply_async(func=self.check_send_data) while True: \'\'\' 接收用户来访 \'\'\' try: client,client_addr = self.socket_server.accept() except BlockingIOError: pass else: print(\'有人来了:[%s|%s]\' % client_addr) #没报错,那么就是用户真正的连接到了,accept捕捉到了返回值 #1:用户登陆发来的消息处理,开一个线程去等待他发来对应的数据,或者说判断这个ID是否是重复的 self.thread_pool.apply_async(func=self.check_login_data,args=(client,)) #login数据 def inform_online_user_list(self): \'\'\' 告知客户端现在的在线用户,就是直接发送一个在线用户的列表 \'\'\' while True: if self.is_flush: time.sleep(1) print(\'刷新在线用户\') send_online_user_msg = { \'flag\': \'flush\', \'data\': self.list_online_user, } for id in self.dict_online_user: client = self.dict_online_user[id][\'socket\'] client.send(pickle.dumps(send_online_user_msg)) self.is_flush = False def check_send_data(self): \'\'\' 处理用户的发送数据,要发给别人拉 还会碰到用户发来的断开连接的数据 \'\'\' while True: list_onlien_user_bak = copy.copy(self.list_online_user) for data in list_onlien_user_bak: #[{\'id1\':\'name1\'},{\'id1\':\'name1\'}...] id = list(data.keys())[0] client = self.dict_online_user[id][\'socket\'] #套接字拿到 name = self.dict_online_user[id][\'name\'] #print(\'当前遍历到的在线用户,%s:%s\' % (id,name)) try: recv_msg = client.recv(1024) #非阻塞形式获取客户端发来的数据 except BlockingIOError as e: pass #当前该在线用户并没有发送任何消息 except ConnectionResetError as e: \'\'\' 客户端断开连接 \'\'\' pass else: #1:判断是否是断开的数据 if not recv_msg: print(\'离开了:%s,%s\' % (id,name)) client.close() del self.dict_online_user[id] self.list_online_user.remove(data) self.is_flush = True else: \'\'\' { #发送数据体 \'flag\': \'send\' #数据标志位 \'data\': { \'send_id\': \'123455\', #发送者的ID \'recv_id\': \'123456\', #接收人的ID号 \'message: \'xxxxx\', #发给别人的数据 } } \'\'\' clear_recv_msg = pickle.loads(recv_msg) if clear_recv_msg.get(\'flag\') == \'send\': #确实是要给别人发送消息 clear_recv_data = clear_recv_msg.get(\'data\') if clear_recv_data: #发来的数据中确实有data send_id = clear_recv_data.get(\'send_id\') recv_id = clear_recv_data.get(\'recv_id\') message = clear_recv_data.get(\'message\') if self.dict_online_user.get(recv_id): recv_socket = self.dict_online_user.get(recv_id)[\'socket\'] #接收者的套接字 else: continue send_name = self.dict_online_user[send_id][\'name\']#发送者的名字 \'\'\' { #服务端发给客户端的数据体 \'flag\': \'forward\' #数据标志位 \'data\': { \'send_name\': \'123455\', #发送者的ID \'message\': \'123456\', #接收人的ID号 } } \'\'\' forward_data = { \'flag\': \'forward\', \'data\': { \'send_name\': send_name, \'message\': message, } } recv_socket.send(pickle.dumps(forward_data)) print(\'消息转发完毕:\n\',forward_data) def check_login_data(self,client): \'\'\' { #登陆数据体 \'flag\': \'login\', #数据标志位 \'data\': { \'id\': \'123456\', #用户ID \'name: \'xxxxx\', #用户昵称 } } \'\'\' recv_msg = client.recv(1024) #接收用户发来的login数据体 if recv_msg: #发来的数据是有效的 clear_recv_msg = pickle.loads(recv_msg) if clear_recv_msg.get(\'flag\') == \'login\': #发来的确实是login的数据 clear_recv_data = clear_recv_msg.get(\'data\') if clear_recv_data: #发来的数据中确实有data id = clear_recv_data.get(\'id\') name = clear_recv_data.get(\'name\') #判断ID是否重复 if id in self.dict_online_user: print(\'[%s:%s]该用户出现重复ID\' % (id, name)) send_login_msg = { \'flag\': \'login\', \'data\': { \'state\': False, \'message\': \'ID使用重复\', } } client.send(pickle.dumps(send_login_msg)) #给客户端返回错误信息 elif not id or not name: print(\'[%s:%s]该用户所需数据为空\' % (id, name)) send_login_msg = { \'flag\': \'login\', \'data\': { \'state\': False, \'message\': \'ID或名字为空\', } } client.send(pickle.dumps(send_login_msg)) #给客户端返回错误信息 else: \'\'\' dict_onlien_user = { xxxx:{ \'name\':\'666\', \'socket\':client, ... }, \'\'\' client.setblocking(0) #真正存储在在线字典里的用户套接字是个非阻塞的 self.dict_online_user[id] = { \'name\': name, \'socket\': client, } self.list_online_user.append( {id:name} ) #刷新在线用户 print(\'在线用户已添加\') self.is_flush = True send_login_msg = { \'flag\': \'login\', \'data\': { \'state\': True, \'message\': \'登陆成功\', } } client.send(pickle.dumps(send_login_msg)) return client.close() #用户数据无效,关闭连接 def main(): ip = \'\' port = 8000 tks = TalkServer(ip, port) tks.run() if __name__ == \'__main__\': main()
运行结果: